到目前为止都是循环客户端连接,处理完一个客户端再处理另外一个。现实是一个服务器可以同时处理多个客户端请求。这里通过多进程实现网络服务器
第一步,使用 fork
分配多个进程处理客户端请求
所有进程都会从操作系统分配到 ID,1 要分配给操作系统启动后的首个进程,因此用户进程无法得到进程 ID 1
1 |
|
这样记返回值:因为父进程无法获取子进程 id,所以需要在 fork 直接将 pid_t 返回给父进程,通过
getppid
可以获取父进程 id
示例地址:https://github.com/XBoom/network-ip.git 中的 apps/socket/12/
1 | ./client |
从上面的运行结果来看
- 虽然子进程复制了父进程的内存结构,但是两者拥有的是独立的内存结构,不会相互影响
fork
后面的代码指令也会复制一份,独立运行
第二步,有了子进程,那么如何管理子进程的死亡
父子进程的退出情况一共存在以下三种情况
- 父进程比子进程先退出
- 父进程比子进程后退出
- 子进程一直不退出
孤儿进程(Orphan Process)和僵尸进程(Zombie Process)是与进程状态相关的两个概念,通常在操作系统中用来描述进程的状态变化。
- 孤儿进程(Orphan Process):
- 当一个父进程创建了子进程,但父进程在子进程之前终止,而子进程继续运行时,子进程就成为孤儿进程。
- 孤儿进程会被操作系统的init进程(在Unix/Linux系统中通常是进程号1的init)接管。init进程会成为孤儿进程的新父进程,并负责收养和管理这个孤儿进程。
- 孤儿进程不会影响系统的正常运行,因为它已经有了新的父进程(init进程)来照顾它。
- 僵尸进程(Zombie Process):
- 当一个子进程终止时,它并不会立即从系统中移除,而是留下一个僵尸进程。
- 僵尸进程仍然占用系统资源,但它不再执行任何代码。僵尸进程的存在是为了让父进程获取子进程的退出状态。
- 父进程可以通过调用
wait()
或waitpid()
等系统调用来获取子进程的退出状态,一旦获取完成,僵尸进程就会被完全移除。 - 如果父进程没有及时处理子进程的退出状态,可能会导致系统中积累大量的僵尸进程,从而占用系统资源。
防止僵尸进程的方法通常是在父进程中使用合适的系统调用来等待子进程的终止,并获取其退出状态。
孤儿进程会被 init 进程(PID为1的进程)领养,init 进程会负责回收孤儿进程的资源,因此孤儿进程不会变成僵尸进程
也就是说,如果父进程比子进程后退出,且父进程并没有管子进程,那么就会僵尸进程,为了防止僵尸进程,可用如下函数销毁僵尸进程
1 |
|
如果调用此函数的时候已经有子进程终止,那么子进程 终止时传递的返回值(exit函数返回值、main函数的返回值) 将保存到该函数的参数所指内存空间
函数参数 statloc
所指向的单元中还含有其他信息,需要宏进行分离
WIFEXITED
子进程正常终止时返回 true(错误系统idaoyong,信号,异常退出,资源耗尽等表示异常退出)WEXITSTATUS
返回子进程的返回值
1 | if (WIFEXITED(status)) { |
调用 wait
函数时,如果没有已终止的子进程,那么程序将阻塞直到有子进程终止,就有了第二种
1 |
|
示例地址:https://github.com/XBoom/network-ip.git 中的 apps/socket/13/
,运行结果
1 | [root]#./client |
可以看到 waitpid
并没有阻塞,而是看一下当前有没有子进程退出,如果没有则返回 0 并退出。
但是父进程不能啥也不干,就循环判断子进程是否退出了,这个时候就用到了信号
1 | /** |
signal
很少使用,仅仅是为了向前兼容。所以最好使用 sigaction
处理信号, 常用的处理信号有
SIGALRM
: 已经通过调用alarm函数注册的时间,通过 alarm 函数注册的时间表示:在接收到 SIGALRM 信号之前的秒数。当调用alarm(seconds)
后,系统会在指定的秒数后发送 SIGALRM 信号给调用进程SIGINT
: 输入 CTRL + CSIGCHLD
: 子进程终止
第四步,基于前面的内容实现一个并发回声服务器
示例地址:https://github.com/XBoom/network-ip.git 中的 apps/socket/14/
,运行结果
1 | ./server 5555 |
中间使用 fork
出来的子进程处理客户端发送的请求,通过信号处理子进程退出
1 | struct sigaction act; |
而信号处理函数仅干了一件事,就是处理任意退出的子进程
1 | //子进程处理 |
另外每次循环子进程都会关闭一次 server_socket
与 client_socket
,父进程关闭 client_socket
1 | if(child_pid == 0) //子进程 |
参考文档
- 《TCPIP 网络编程》