阻塞I/O + 多进程模型
阻塞I/O + 进程模型是最简单的解决C10K问题的方法,它是通过为每个连接创建一个独立的进程(子进程)去服务的。
fork 函数
函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
一个进程拥有完整的地址空间、程序计数器等,为了创建一个新的进程,可以使用fork函数。
返回值
该函数的每次调用都返回两次,在父进程中返回子进程的PID,在子进程中返回0,要想判断当前执行的进程是父进程还是子进程需要通过该函数的返回值进行判断。如果fork调用失败。则返回-1,并设置errno。
fork函数会复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,包括地址空间、打开的文件描述符、程序计数器、堆指针、栈指针、标志寄存器的值等。但也有一些属性被赋予了新的值,比如该进程的PPID被设置称原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。
子进程的代码与父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。
数据的复制采用写时复制,即只有任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(复制时首先产生缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。不过,我们应该尽量避免没必要的内存分配和数据复制。
创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数会加1(调用close可以令文件描述符的引用计数-1),此外,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。
fork编程范式
if(fork() == 0) {
do_child_process(); // 子进程执行代码
}
else {
do_parent_process(); // 父进程执行代码
}
僵尸进程
当一个子进程退出时,系统内核还保留了该进程的若干信息,比如退出状态。内核不会立即释放该进程的进程表表项,是为了满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。
在多进程程序中,父进程需要跟踪子进程的退出状态。
有两种情况会使子进程进入僵尸态:
- 子进程结束运行,父进程读取其退出状态之前,子进程处于僵尸态。
- 父进程结束或者异常终止,而子进程继续运行。此时子进程的PPID将被操作系统设置为1,即init进程。用init进程接管该子进程,并等待它结束。所以,在父进程退出之后,子进程退出之前,该子进程处于僵尸态。
如果子进程处于僵尸态,那么仍然占据着内核资源,由于内核资源有限,所以我们需要即使处理子进程的返回信息。
有两种方式可以在子进程退出后回收资源,分别是wait和waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* stat_loc);
pid_t waitpid(pid_t pid, int* stat_loc, int options);
这对函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息。
wait和waitpid都可以返回两个值,一个是函数返回值,表示已终止子进程的PID,另一个是通过指针stat_loc所返回的子进程的退出状态信息。这个状态的可能的值为正常终止、被信号杀死、作业控制停止等。
wait函数将阻塞进程,直到该进程的某个子进程结束运行为止。
而waitpid函数解决了这个问题,waitpid函数只等待由pid参数指定的子进程,如果pid取值为-1,那么它就和wait函数相同,等待第一个终止的子进程。waitpid函数通过options参数控制自身的行为,该参数最常用的取值为 WNOHANG,该取值代表waitpid的调用将是非阻塞的:如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0;如果目标子进程确实正常退出了,则waitpid返回该子进程的PID。调用失败时,返回-1,并设置errno。
对于非阻塞调用,需要在事情已经发生的情况下执行才能提高效率。所以,waitpid函数需要在某个子进程退出之后再调用。
当一个子进程结束时,它将给其父进程发送一个 SIGCHLD 信号,因此,我们可以在父进程中捕获 SIGCHLD 信号,并在信号处理函数中调用 waitpid 函数以回收子进程的资源:
void handle_child(int sig) {
pid_t pid;
int stat;
while((pid = waitpid(pid, &stat, WNOHANG)) > 0) {
/* 对子进程的后续处理 */
}
}
signal(SIGCHLD, handle_child);
注意:在信号处理函数中,需要用while循环对waitpid函数的返回值进行判断,这是因为同一时间可能有多个子进程退出,此时内核只会发出一次 SIGCHLD 信号,所以需要通过 while 循环,以确保所有退出的子进程都能被处理。
当使用阻塞I/O和多进程模型时,假设有两个客户端,服务器初始监听在套接字 lisnted_fd 上。当第一个客户端发起连接请求,连接建立后产生出连接套接字,此时,父进程派生出一个子进程,在子进程中,使用连接套接字和客户端通信,因此子进程不需要关心监听套接字,只需要关心连接套接字;父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心连接套接字,只需要关心监听套接字。假设父进程之后又接收了新的连接请求,从 accept 调用返回新的已连接套接字,父进程又派生出另一个子进程,这个子进程用第二个已连接套接字为客户端服务。过程如下: