一、基本介绍
1、如果逻辑控制流在时间上重叠,那么它们就是并发的。到目前为止,我们主要将并发看作是一种操作系统内核用来运行多个应用程序的机制。当然并发不仅仅局限于内核,应用级并发也属于并发。
(1)访问慢速I/O设备。当一个应用正在等待来自慢速I/O设备(如磁盘)的数据到达时,内核会运行其他进程,使CPU保持繁忙。每个应用都可以按照类似的方式,通过交替执行I/O请求和其他有用视为工作来并发。
(2)与人交互。计算机需要有同时执行多个任务的能力。如边看新闻听音乐,用的是并发。每次用户请求某种操作,一个
独立的并发逻辑流被创建来执行这个操作。
(3)通过推迟工作以降低延迟。
(4)服务多个网络客户端。迭代网络服务器在实际使用中是不现实的。实际中需要创建并发服务器。
(5)在多核机器上进行并行计算。
2、使用应用级并发的应用程序称为并发程序。现代操作系统提供了三种基本的构造并发程序的方法:
(1)进程。用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信(IPC)机制。
(2)I/O多路复用。在这种形式的并发编程中,应用程序在一个进程的上下文中显式地调度它们自己的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换为另一个状态。因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。
(3)线程。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。可以把线程看成是其他两种方式的混合体,像进程流一样由内核进行调度,而像I/O多路复用流一样共享同一个虚拟地址空间。
二、基于进程的并发编程
构造并发程序最简单的方法就是用进程,使用那些大家都熟悉的函数,像fork、exec和waitpid。例如,一个构造并发服务器的自然方法就是,在父进程中接收客户端连接请求,然后创建一个新的子进程来为每个新客户提供服务。为了理解这是如何工作的,假设我们有两个客户端和一个服务器,服务器正在监听一个监听描述符(比如描述符3)上的连接请求。现在假设服务器接受了客户端1的连接请求,并返回一个已连接描述符(比如描述符4),如图12-1。在接受连接请求之后,服务器派生一个子进程,这个子进程获得服务器描述表的完整副本。子进程关闭它的副本中整监听描述符3,而父进程关闭它的已连接描述符4的副本,因为不在需要这些描述符了。这就得到了图12-2的状态,其中子进程正忙于为客户端提供服务。
因为父、子进程中的已连接描述符都指向同一个文件表表项,所以父进程关闭它的已连接描述符的副本是至关重要的。否则,将永远不会释放已连接描述符4的文件表条目,而且由此引起的内存泄露将最终消耗光可用的内存,使系统奔溃。现在,假设在父进程为客户端1创建了子进程之后,它接受一个新的客户端2的连接请求,并返回一个新的已连接描述符(比如描述符5),如图12-3。然后,父进程又派生另一个子进程,这个子进程已连接描述符5为它的客户端提供服务,如图12-4.此时,父进程正在等待下一个连接请求,而两个子进程正在并发地为它们各自的客户端提供服务。
1、示例代码
//
#include "csapp.h"
void echo(int connfd);
void sigchld_handler(int sig)
{
while (waitpid(-1, 0, WNOHANG) > 0)
;
return;
}
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_srorage clientaddr;
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argc[0]);
exit(0);
}
Signal(SIGCHLD, sigchld_handler);
listenfd = open_listenfd(argv[]);
while (1) {
clientlen = sizeof(struct sockaddr_srorage);
connfd = accept(listenfd, (SA *) &clientaddr, &clientlen);
if (fork() == 0) {
close(listenfd); /* child closes its listening socket */
echo(connfd); /* child services client */
close(connfd); /* child closes connection with client */
exit(0); /* child exits */
}
close(connfd); /* parent clises connected socket (important!) */
}
}
//
此代码是基于上一章的echo代码,上一章是实现的是迭代服务器,本代码实现的是基于进程的并发echo服务器的代码。
(1)通常服务器会运行很长时间,所以我们必须要包括一个SIGCHLD处理程序,来回收僵死(zombie)子进程的资源(sigchld_handler函数)。因为当SIGCHLD处理程序执行时,SIGCHLD信号是堵塞的,而linux信号是不排队的,所以SIGCHLD处理程序必须准备好回收多个僵死子进程的资源。
(2)父子进程必须关闭它们各自的connfd副本(close(connfd))。这对父进程来说非常重要,它必须关闭它的已连接
描述符,以避免内存泄漏。
(3)最后,因为套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。
2、进程的优劣
对于在父子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。进程有独立的地址空间即使优点优点也是缺点。
(1)优点:有独立的地址空间,一个进程不可能不小心覆盖另一个进程的虚拟内存,这就消除了许多令人迷惑的错误。
(2)缺点:独立的地址空间使得进程共享状态信息变得更加困难。为了共享信息,它们必须使用显式的IPC(进程间通信)机制。
(3)缺点:基于进程的设计另一个缺点是,它们往往比较慢,因为进程控制和IPC的开销很高。
三、基于I/O多路复用的并发事件驱动服务器
I/O多路复用可以用作并发事件驱动程序的基础,在事件驱动程序中,某些事件会导致流向前推送。
致谢
1、《深入理解计算机系统》[第3版],作者 Randal E.Bryant, David R.O`Hallaron 译者 龚奕利 贺莲