《深入理解计算机系统》Tiny服务器2——多进程版和selectIO复用版Tiny

  在上个博客中,我们根据csapp里的源码实现了一个小型的web服务器Tiny,通过在浏览器地址栏输入本机地址和所设置的端口号,我们可以访问到静态网页和动态的CGI程序。但我们之前的那个Tiny存在一个很大的问题,那就是每次只能接受一个请求,这在实际的服务器里肯定是不存在的。所以,这次我们就来把Tiny改为支持多个访问的并发服务器。
  实现并发服务器程序一般有三种方法:
  1) 利用多进程。

  当监听套接字收到来自浏览器的连接请求之后,通过调用fork()函数,产生一个子进程。在子进程中,进行与浏览器的交互。这种方式的优点就是实现起来很简单,缺点也很明显,由于fork()子进程和进程上下文切换需要移动相当多的资源,造成多进程的程序效率不高。
  2) 利用IO复用。

  IO复用就是由服务器维护一个客户端池,其中存放着服务器的监听套接字和已连接的客户端文件描述符。服务器通过调用select(),poll()或者epoll()来观察哪些套接字或文件描述符发生了响应。举例来说就是,有外来客户端发来connect()连接请求时,监听套接字就进行响应;当已连接的客户端发来请求报文时,相对应的客户端文件描述符会进行响应。之后就是对满足响应条件的套接字或描述符进行相应的处理。如果有新的客户端发送了连接请求,就调用accept()函数,与之建立连接,同时将新建立的与客户端相连的连接文件描述符放入客户端池;如果是已连接的客户端发来请求报文,请求访问静态或动态数据,就进行数据的处理和发送。IO复用的缺点也很明显,在三种并发模型里是最复杂的一种。其优点是效率最高,在现在的主流商业服务器中,都是采用的IO多路复用技术。
  3) 利用多线程。

  通过调用子线程来进行与客户端的数据传递,多线程技术像是上述两种方案的综合。由于线程比进程的资源占用小,上下文切换快,使得多线程技术无论在复杂度上还是效率上都是中等的水平。
    这篇博客就从这三种并发模型来进行Tiny的改进。

(1) 多进程版Tiny:

  首先我们先来最简单的多进程服务器模型。由于和源代码差别不大,这里我们只把需要改变的几句代码贴出来。

 1 while (1) {
 2         clientlen = sizeof(clientaddr);
 3         connfd = accept(listenfd, (SA*)&clientaddr, &clientlen);
 4         getnameinfo((SA*) &clientaddr, clientlen, hostname, MAXLINE,
 5                 port, MAXLINE, 0);
 6         if (fork() == 0)
 7         {
 8             close(listenfd);
 9             doit((void *)&connfd);
10             close(connfd);
11             exit(0);
12         }
13         close(connfd);
14         printf("Accepted connection from (%s, %s)\n", hostname, port);
15     }

  这里需要注意,在我们这个web服务器中,子进程先于父进程结束,这样子进程就形成了“僵尸进程”。设置这种状态的目的是维护子进程的信息,以便父进程在以后的某个时候获取。但这里,我们不需要获取子进程的信息,为了避免僵尸子进程消耗资源,我们可以通过信号来及时回收它们。如下代码所示:

1 void sig_chld(int signo)
2 {
3     pid_t pid;
4     int stat;
5 
6     while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
7         printf("child %d terminated\n", pid);
8     return;
9 }

    注意这里使用的是waitpid()函数,而不是wait()函数。因为信号是不排队的,可能有多个同时到达,但只会提示一次,所以这里使用while循环,确保将全部的信号处理完。只是多了这么几行代码,原来的Tiny服务器就变成了支持并发访问的进阶版服务器了。

(2) IO多路复用版Tiny
    接下来,我们进行IO多路复用的服务器实现。连续几个小时用gdb调试这个服务器,我只能说真的很酸爽。。这里我们采用select()函数,因为我个人习惯使用select()函数,其他两个poll()和epoll()在实现上有些差别,但在思想上和select()是一样的。由于IO复用改变的地方稍多一些,所以我把main()函数和doit()函数都贴出来。其中前边带红色+的代码段为改进的部分。

 1 int main(int argc, char *argv[])
 2 {
 3     int listenfd, connfd;
 4     char hostname[MAXLINE], port[MAXLINE];
 5     socklen_t clientlen;
 6     struct sockaddr_storage clientaddr;
 7     int err, maxfd, i, num;
 8     fd_set read_set, ready_set;
 9 
10     if (argc != 2) {
11         fprintf(stderr, "usage: %s <port>\n", argv[0]);
12         exit(1);
13     }
14     
15     listenfd = open_listenfd(argv[1]);
16 +    FD_ZERO(&read_set);                  //初始化read_set                                             
17 +    FD_SET(listenfd, &read_set);     //将listenfd添加到read_set                        
18 +    maxfd = listenfd;                                    
19     
20     printf("listenfd = %d\n", listenfd);
21 
22     while (1) {
23 +        ready_set = read_set;           //因为select()会改变其中set集合,所以这里用一个read_set的拷贝                                             
24 +        num = select(maxfd+1, &ready_set, NULL, NULL, NULL);                       
25 +        if (num == -1)
26 +        {
27 +            fprintf(stderr, "select error\n");
28 +            exit(1);
29 +        }
30 +        else if (num == 0)             //没有已准备好读取的文件描述符
31 +            continue;
32 
33 +       for (i = 0 ; i <= maxfd; i++)
34 +        {
35 +            if (FD_ISSET(i, &ready_set))
36 +            {
37 +                if (i == listenfd)       //有新的客户端发来连接请求
38 +                {    
39 +                    clientlen = sizeof(clientaddr);
40 +                    connfd = accept(listenfd, (SA*)&clientaddr, &clientlen);                    
41 +                    getnameinfo((SA*) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
42 +                    printf("Accepted connection from (%s, %s)\n", hostname, port);
43 +                    printf("connfd = %d\n", connfd);
44 +                    FD_SET(connfd, &read_set);
45 +                    if (connfd > maxfd)
46 +                        maxfd = connfd;
47 +                }
48                 else                   //访问数据请求
49                 {
50                     printf("message from connfd\n");
51                     doit((void *)&connfd, &read_set);
52                 }
53             }
54         }
55     }
56 }

  因为第一次忘记了将已关闭的文件描述符从read_set清除掉,害得我从头到尾调试了好几遍,在这里也提醒一个大家。
  在csapp第12章并发编程里介绍了以IO多路复用为基础的基于事件驱动的一个连接池框架。主要代码如下,其中check_clients()函数是与业务处理相联系的,这里我们把它稍加改动,使其可以应用于我们这个并发服务器的业务场景。

 1 typedef struct { //Represents a pool of connected descriptors
 2     int maxfd;   //Largest descriptor in read_set
 3     fd_set read_set; //Set of all active descriptors
 4     fd_set ready_set; //Subset of descriptors ready for reading
 5     int nready; //Number of ready descriptors from select
 6     int maxi; //High water index into client array
 7     int clientfd[FD_SETSIZE]; //Set of active descriptors
 8     rio_t clientrio[FD_SETSIZE]; // Set of active read buffers
 9 } pool;
10 
11 void init_pool(int listenfd, pool *p)
12 {//初始化客户端池
13     int i;
14     p->maxi = -1;
15     for (i = 0; i < FD_SETSIZE; i++)
16         p->clientfd[i] = -1;
17     //Initially, listenfd is only member of select read set
18     p->maxfd = listenfd;
19     p->nready = 0;
20     FD_ZERO(&p->read_set);
21     FD_SET(listenfd, &p->read_set);
22 }
23 
24 void add_client(int connfd, pool *p)
25 {//添加一个新的客户端到活动客户端池
26     int i;
27     p->nready--;
28     for (i = 0; i < FD_SETSIZE; i++)   //Find an acailable slot
29         if (p->clientfd[i] < 0) {
30             //Add connected descriptor to the pool
31             p->clientfd[i] = connfd;
32             rio_readinitb(&p->clientrio[i], connfd);
33 
34             //Add the descriptor to descriptor set
35             FD_SET(connfd, &p->read_set);
36 
37             //Update max descriptor and pool high water mark
38             if (connfd > p->maxfd)
39                 p->maxfd = connfd;
40             if (i > p->maxi)
41                 p->maxi = i;
42             break;
43         }
44         if (i == FD_SETSIZE) //Couldnot find an empty slot
45             fprintf(stderr, "add_client error: Too many clients\n");
46 }
47 
48 void check_clients(pool *p)
49 {
50     int i, connfd, n;
51     rio_t rio;
52 
53     for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) {
54         connfd = p->clientfd[i];
55         rio = p->clientrio[i];
56 
57         //If the descriptor is ready, doit
58         if ((connfd > 0) && (FD_ISSET(connfd, &p->ready_set))) {
59             p->nready--;
60             doit((void *)&connfd, p);
61         }//请求处理完毕之后的清理工作
62         close(connfd);
63         FD_CLR(connfd, &p->read_set);
64         p->clientfd[i] = -1;
65     }
66 }

   接下来,我们准备实现基于poll()函数的连接池框架和多线程版本的Tiny服务器。

posted @ 2017-07-12 10:15  liude2134  阅读(454)  评论(0编辑  收藏  举报