《深入理解计算机系统》Tiny服务器3——poll类型IO复用版和多线程版Tiny
上次的博客,记录到了根据CSAPP里客户端池的IO多路复用版Tiny的实现。今天学习了《UNIX网络编程》的前几章。在第6章里讲述了基于select()和poll()函数的IO复用,并且给出了和CSAPP中相似的代码实现。这不过,Stevens大神没有将主要元素设为一个结构体。顺便一提,上次的基于select()函数的服务器实现过程中我遇到了很多问题,主要有以下几个方面:
1) 在上一个多路复用服务器实现中,每次进行静态页面的请求时,在服务器端都会输出一条错误信息:
*** stack smashing detected ***: ./csapp_select_tiny terminated
Aborted (core dumped)
这是由于程序中分配的缓冲区太小了。我们把缓冲区大小BUFSIZE设置为4000,即可解决问题。
2) 有时候在浏览器中获取了静态页面或动态数据之后,服务器端会不断有乱码刷出来。经过GDB跟踪调试,发现是在页面获取成功之后,相应的文件描述符没有取消掉。增加一条FD_CLR宏即可。
3)
另外认识到浏览器与web服务器之间的链接一般是短链接。以浏览器请求动态CGI程序为例。在浏览器地址栏中输入"localhost:8888/",浏览器首先向服务器发出connect()请求,获取主页面。当我们输入要相加的数据之后,此时浏览器与服务器已经断开了连接。还需要浏览器向服务器发送connect()连接请求,重新建立连接,并进行动态内容的访问。在服务器中输出的信息如下:
Request headers: GET / HTTP/1.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:54.0) Gecko/20100101 Firefox/54.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 Response headers: HTTP/1.0 200 OK Server: Tiny Web Server Connection: close Content-length: 290 Content-type: text/html Request headers: GET /cgi-bin/adder?num1=12&num2=34 HTTP/1.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:54.0) Gecko/20100101 Firefox/54.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://localhost:8888/ Connection: keep-alive Upgrade-Insecure-Requests: 1 ^C
看似很简单的思想,帮助我解决了很多调试中不容易发现的问题。
今天我还参照CSAPP的结构体和函数设计,针对poll()方法设计了相应的结构体和函数,总体比较来看变化不大。现将代码贴在下边。
1 typedef struct { 2 int maxi; //client数组当前最大下标值 3 int nready; //准备的描述符 4 struct pollfd client[OPEN_MAX]; //pollfd结构数组 5 rio_t clientrio[FD_SETSIZE]; //与描述符对应的缓冲区 6 } pool; 7 8 9 void init_pool(int listenfd, pool *p) 10 { 11 int i; 12 13 p->client[0].fd = listenfd; 14 p->client[0].events = POLLRDNORM; 15 for (i = 1; i < OPEN_MAX; i++) 16 p->client[i].fd = -1; 17 p->maxi = 0; 18 p->nready = 0; 19 } 20 21 void add_client(int connfd, pool *p) 22 { 23 int i; 24 p->nready--; 25 for (i = 0; i < OPEN_MAX; i++) 26 27 if (p->client[i].fd < 0) { 28 p->client[i].fd = connfd; 29 rio_readinitb(&p->clientrio[i], connfd); 30 31 p->client[i].events = POLLRDNORM; 32 33 if (i > p->maxi) 34 p->maxi = i; 35 break; 36 } 37 if (i == OPEN_MAX) 38 { 39 fprintf(stderr, "too many clients\n"); 40 exit(1); 41 } 42 } 43 44 void check_clients(pool *p) 45 { 46 int i, connfd, n; 47 char buf[MAXLINE]; 48 rio_t rio; 49 50 for (i = 1; (i <= p->maxi) && (p->nready > 0); i++) { 51 connfd = p->client[i].fd; 52 rio = p->clientrio[i]; 53 54 if ((connfd > 0) && (p->client[i].revents & (POLLRDNORM | POLLERR))) 55 { 56 doit(&connfd, p); 57 } 58 59 close(connfd); 60 p->client[i].fd = -1; 61 p->nready--; 62 } 63 }
在调试这个基于poll()函数的Tiny时,遇到了一个很诡异的情况。运行服务器之后,直接返回了段错误:
Segmentation fault (core dumped)
在用GDB调试时发现,程序刚刚进入main()函数的入口就出错了。经过上网搜索,得知是由于我们这个程序的结构体太大了,超过了栈的大小。所以我们在main函数里的pool结构体实例前加上static关键字,把它放到全局区。这样再运行就没事了。新程序整体上和select方式的程序一样,只不过是一些子函数和结构体的替换。由此也体会到了模块化设计的重要性。
(3) 多线程版Tiny
在经过了IO多路复用版本Tiny的摧残之后,多线程版本就显得小菜一碟了。当监听套接字接受到来自浏览器的请求之后,创建一个线程对其进行业务处理,提供静态页面服务或动态CGI服务。为了便于线程结束之后自动清理,我们利用pthread_detach(pthread_self())将子线程和主线程分离开。多线程版Tiny服务器程序的主要框架如下,为了便于观察,这里省略掉了很多和源代码一样的部分。
1 static void *doit(void *arg); 2 3 int main(int argc, char *argv[]) 4 { 5 int listenfd, *connfdp; //声明一个指针,存放connfd 6 pthread_t thread; 7 //...其余变量声明 8 9 listenfd = open_listenfd(argv[1]); 10 while (1) { 11 clientlen = sizeof(clientaddr); 12 connfdp = malloc(sizeof(int)); //动态分配一块内存给connfd,使得每个线程都有各自的已连接描述符副本 13 *connfdp = accept(listenfd, (SA*)&clientaddr, &clientlen); 14 if (pthread_create(&thread, NULL, &doit, connfdp) != 0) //创建线程 15 { 16 fprintf(stderr, "pthread_create error\n"); 17 exit(1); 18 } 19 } 20 } 21 22 static void *doit(void *arg) 23 { 24 int fd = *((int *)arg); //获取参数 25 free(arg); //释放内存空间 26 pthread_detach(pthread_self()); //线程分离,线程执行结束后自动回收资源 27 28 //...函数主体,参考CSAPP源代码 29 pthread_exit(0); //线程退出 30 } 31
注意,第5行我们不是声明了一个int型变量用来存放已连接描述符,而是声明了一个指向int型变量的指针。在12-13行,对其动态分配内存空间,这样,对于每个创建的线程,都有各自的已连接描述符的副本。这里如果不对其进行动态分配内存,由于其指向不明,冒然写入会引起"Segmentation fault"错误。详情请参考《UNIX网络编程》第3版540页。
至此,并发服务器的三种模型我们都简单的实现了一遍。这里再总结一下:
(1) 多进程和多线程版本的Tiny服务器实现起来都比较简单,只需要将主体业务处理程序放到子进程或线程里,其他地方稍加修改就行;
(2) IO多路复用模型与其他两种模型比较起来比较复杂,但通过我们构造一个基于连接池的框架,也可以像多进程和多线程一样比较容易的实现。IO复用要注意的是,在处理完一个文件描述符之后,及时将其清理掉。IO多路复用还可以使用epoll()函数,整体上和select()以及poll()函数没有什么区别,这里就不给出实现了。感兴趣的朋友可以自己实现一下基于epoll()函数的连接池模型。