MST

星途 面试题库

面试题:C语言多进程网络编程中的资源管理

在C语言网络编程中使用多进程模型,每个进程都可能打开多个网络套接字(socket)。阐述在这种情况下如何有效地管理文件描述符,避免资源泄漏。如果某个子进程异常退出,如何确保父进程能正确处理其遗留的网络连接和其他资源?请给出具体的思路和关键代码片段。
45.3万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

文件描述符管理及资源泄漏避免思路

  1. 共享文件描述符表:在C语言中,当使用fork创建子进程时,子进程会继承父进程的文件描述符表。这意味着父进程打开的socket等文件描述符在子进程中同样可用。为了避免资源泄漏,父进程和子进程需要明确各自对文件描述符的使用责任。
  2. 合理关闭文件描述符
    • 父进程:在创建子进程后,对于仅子进程需要使用的socket文件描述符,父进程应及时关闭。这样可以防止父进程占用不必要的资源,同时避免在父进程后续操作中误操作这些子进程专用的文件描述符。
    • 子进程:在完成任务后,应关闭所有打开的文件描述符,包括socket。如果子进程执行exec系列函数替换自身进程映像,exec函数会自动关闭所有未标记为FD_CLOEXEC的文件描述符,但为了清晰和可靠,在代码中显式关闭文件描述符是良好的编程习惯。
  3. 使用FD_CLOEXEC标志:在调用socket等函数创建文件描述符后,可以通过fcntl函数设置FD_CLOEXEC标志。这样,当进程调用exec函数时,设置了该标志的文件描述符会自动关闭,进一步防止资源泄漏。

处理子进程异常退出及遗留资源思路

  1. 信号处理:父进程可以通过signalsigaction函数注册信号处理函数,捕捉子进程退出的信号(通常是SIGCHLD)。
  2. 回收子进程资源:在信号处理函数中,使用waitpid函数等待子进程结束并获取其退出状态。waitpid函数的WNOHANG标志可以使调用非阻塞,即如果没有子进程退出,函数立即返回。通过循环调用waitpid并处理返回值,可以确保所有已退出的子进程资源都被回收。
  3. 清理遗留网络连接:在获取子进程退出状态后,父进程可以根据具体情况决定如何处理子进程遗留的网络连接。例如,如果子进程正在处理一个TCP连接,父进程可以关闭对应的socket文件描述符,或者重新分配该连接给其他子进程处理。

关键代码片段

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>

#define PORT 8080
#define BACKLOG 10

void sigchld_handler(int signum) {
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        // 处理子进程退出后的遗留资源,如关闭相关socket等
        printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));
    }
}

int main() {
    int sockfd, new_sockfd;
    struct sockaddr_in servaddr, cliaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    int flags = fcntl(sockfd, F_GETFD, 0);
    fcntl(sockfd, F_SETFD, flags | FD_CLOEXEC);

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    if (listen(sockfd, BACKLOG) < 0) {
        perror("listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    while (1) {
        socklen_t len = sizeof(cliaddr);
        new_sockfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
        if (new_sockfd < 0) {
            perror("accept failed");
            continue;
        }

        pid_t pid = fork();
        if (pid < 0) {
            perror("fork failed");
            close(new_sockfd);
        } else if (pid == 0) { // 子进程
            close(sockfd); // 子进程关闭监听socket
            // 处理新连接的逻辑,这里省略具体处理代码
            close(new_sockfd);
            exit(EXIT_SUCCESS);
        } else { // 父进程
            close(new_sockfd); // 父进程关闭新连接socket,由子进程处理
        }
    }
    close(sockfd);
    return 0;
}
  1. sigchld_handler函数:处理SIGCHLD信号,回收已退出子进程的资源。
  2. main函数
    • 创建socket并设置FD_CLOEXEC标志。
    • 绑定、监听socket。
    • 注册SIGCHLD信号处理函数。
    • while循环中接受新连接,通过fork创建子进程处理连接,父进程和子进程各自关闭不需要的文件描述符。