文件描述符管理及资源泄漏避免思路
- 共享文件描述符表:在C语言中,当使用
fork
创建子进程时,子进程会继承父进程的文件描述符表。这意味着父进程打开的socket等文件描述符在子进程中同样可用。为了避免资源泄漏,父进程和子进程需要明确各自对文件描述符的使用责任。
- 合理关闭文件描述符:
- 父进程:在创建子进程后,对于仅子进程需要使用的socket文件描述符,父进程应及时关闭。这样可以防止父进程占用不必要的资源,同时避免在父进程后续操作中误操作这些子进程专用的文件描述符。
- 子进程:在完成任务后,应关闭所有打开的文件描述符,包括socket。如果子进程执行exec系列函数替换自身进程映像,exec函数会自动关闭所有未标记为
FD_CLOEXEC
的文件描述符,但为了清晰和可靠,在代码中显式关闭文件描述符是良好的编程习惯。
- 使用
FD_CLOEXEC
标志:在调用socket
等函数创建文件描述符后,可以通过fcntl
函数设置FD_CLOEXEC
标志。这样,当进程调用exec函数时,设置了该标志的文件描述符会自动关闭,进一步防止资源泄漏。
处理子进程异常退出及遗留资源思路
- 信号处理:父进程可以通过
signal
或sigaction
函数注册信号处理函数,捕捉子进程退出的信号(通常是SIGCHLD
)。
- 回收子进程资源:在信号处理函数中,使用
waitpid
函数等待子进程结束并获取其退出状态。waitpid
函数的WNOHANG
标志可以使调用非阻塞,即如果没有子进程退出,函数立即返回。通过循环调用waitpid
并处理返回值,可以确保所有已退出的子进程资源都被回收。
- 清理遗留网络连接:在获取子进程退出状态后,父进程可以根据具体情况决定如何处理子进程遗留的网络连接。例如,如果子进程正在处理一个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;
}
sigchld_handler
函数:处理SIGCHLD
信号,回收已退出子进程的资源。
main
函数:
- 创建socket并设置
FD_CLOEXEC
标志。
- 绑定、监听socket。
- 注册
SIGCHLD
信号处理函数。
- 在
while
循环中接受新连接,通过fork
创建子进程处理连接,父进程和子进程各自关闭不需要的文件描述符。