多进程并发服务器
我们来考虑有多个客户同时连接一个服务器的情况。在前面的TCP套接字编程的例子中,我们已经看到,服务器程序在接受来自客户端的一个新连接时,会创建出一个新的套接字(已连接套接字),而原先的监听套接字则继续监听后面的连接请求。如果服务器不能立刻接受后来的连接,它们将被放到队列中以等待处理。
原先的套接字仍然可用并且套接字的行为就像文件描述符,这一事实给我们提供了一种同时服务多个客户的方法。如果服务器调用fork
为自己创建第二份副本,打开的套接字就将被新的子进程所继承。新的子进程可以和连接的客户进行通信,而主服务器进程可以继续接受以后的客户连接。
为了了解这是如何工作的,假设我们有两个客户端和一个服务器,服务器正在监听一个监听套接字(比如描述符3)上的连接请求。
现在假设服务器接受了客户端1的连接请求,并返回一个已连接套接字(比如描述符4),如图1所示。
图1:第一步:服务器接受客户端的连接请求
在接受连接请求后,服务器派生一个子进程,这个子进程获得服务器描述符表的完整拷贝。子进程关闭它的拷贝中的监听套接字(描述符为3),而父进程关闭它的已连接套接字(描述符为4)的拷贝,因为不需要这些描述符了。这就得到图2中的状态,其中的子进程正忙于为客户端提供服务。
因为父、子进程中的已连接套接字描述符都指向同一个文件表表项,所以父进程关闭它的已连接套接字描述符的拷贝是至关重要的。否则,将永远不会释放已连接套接字描述符4的文件表表项,这会导致存储器资源泄漏并将最终消耗尽可用的存储器,使系统崩溃。
图2:第二步:服务器派生一个子进程为这个客户端服务
现在,假设在父进程为客户端1创建了子进程后,它接受一个新的客户端2的连接请求,并返回一个新的已连接套接字(比如描述符5),如图3所示。
图3:第三步:服务器接受另一个连接请求
然后父进程又派生另一个子进程,这个子进程利用已连接套接字(描述符为5)为它的客户端提供服务,如图4所示。
图4:服务器派生另一个子进程为新的客户端服务
此时,父进程继续等待下一个连接请求,而两个子进程正在并发地为它们各自的客户端提供服务。
例子
我们现在来看下如何使用代码来实现多进程并发服务器。在编写代码时,有几点需要着重强调的:
- 因为我们创建子进程,但并不等待子进程的完成,所以安排服务器忽略SIGCHLD信号以避免出现僵尸进程。
- 父子进程必须关闭它们各自的已连接套接字拷贝,如上面所述,这样才能避免存储器资源泄漏。
- 因为套接字的文件表项中的引用计数,直到父子进程的已连接套接字描述符都关闭了,到客户端的连接才会终止。
1 |
|
运行上面的代码,然后使用客户端测试程序(见本文附录)来测试多进程并发服务器的实现。
运行客户端程序:
$ ./client3 & ./client3 & ./client3
客户端终端输出:
char from server = B
char from server = B
char from server = B
同时,可以看到服务器程序输出:
server waiting
server waiting
server waiting
server waiting
subprocess, ch: 66, exit
subprocess, ch: 66, exit
subprocess, ch: 66, exit
多进程优劣
在父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。这样一来,一个进程不可能覆盖另一个进程的用户地址空间。这就消除了许多令人迷惑的错误。这是多进程实现并发服务器的优点。
另一方面,独立的地址空间使用进程共享状态信息变得困难,为了共享信息,必须使用IPC(进程间通信机制)。多进程的另一个缺点是,它们往往比较慢,因为进程控制和IPC的开销都较高。
参考资料
- 深入理解计算机系统,第2版,机械工业出版社
- Linux程序设计(第4版),Neil Matthew等著,人民邮电出版社,2010年
- UNIX 网络编程卷1:套接字联网API(第三版), W.Richard Stevens 等著
附:客户端测试代码
1 | /* Make the necessary includes and set up the variables. */ |