《深入理解计算机系统》Tiny服务器4——epoll类型IO复用版Tiny
前几篇博客分别讲了基于多进程、select类型的IO复用、poll类型的IO复用以及多线程版本的Tiny服务器模型,并给出了主要的代码。至于剩下的epoll类型的IO复用版,本来打算草草带过,毕竟和其他两种IO复用模型差不太多。但今天在看Michael Kerrisk的《Linux/UNIX系统编程手册》时,看到了一章专门用来讲解epoll函数,及其IO复用模型。于是,自己也就动手把Tiny改版了一下。感兴趣的同学可以参考上述手册的下册1113页,有对于epoll比较详细的讲解。
前边针对IO多路复用,我们已经有了很相似的select()函数和poll()函数,那为什么还需要一个epoll()函数呢?肯定是因为前两个在某些情况下不能满足人们的要求吧。我们首先就来分析一下前两种IO多路复用模型所存在的问题:
(1) 每次调用select()或poll(),内核都必须检查所有被指定的文件描述符,看它们是否处于就绪态。当检查大量处于密集范围内的文件描述符时,该操作耗费的时间将大大超过接下来的操作。
(2) 每次调用select()或poll()时,程序都必须传递一个表示所有需要被检查的文件描述符的数据结构到内核,内核检查过描述符后,修改这个数据结构并返回给程序。
(3) select()或poll()调用结束后,程序必须检查返回的数据结构中的每个元素,以此查明哪个文件描述符处于就绪态了。
所以,随着带检查的文件描述符数量的增加,select()和poll()所占用的CPU时间也会随之增加,所以才出现了适合于大量文件描述符处理的epoll()。在书中有一张表,记录了三种IO复用模型随着处理文件描述符的增多,其花费时间的比较:
epoll类型的IO复用模型主要由三个相关函数组成,分别为
- epoll_create():创建一个epoll实例,返回代表该实例的文件描述符
- epoll_ctl():操作同epoll实例相关联的兴趣列表,通过这个函数,我们可以增加新的描述符到列表中,将已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的位掩码
- epoll_wait():返回与epoll实例相关联的就绪列表中的成员
下面我们依次简单介绍一下,更具体的论述请参考manpage。
首先是epoll_create()函数,其原型为:
int epoll_create(int size) //成功返回创建的文件描述符,失败返回-1
这个函数只有一个参数size,但自从Linux2.6.8以来,这个参数就被忽略不用。只要我们输入一个正值就行。在程序结束时,可以通过调用close()函数,将返回的这个描述符关闭。
第二个函数epoll_ctl()比较复杂,其原型为:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev) //成功返回0,失败返回-1
这个函数有四个参数,分别为
- epfd——与epoll实例相关联的文件描述符,通过epoll_create()产生
- op ——需要执行的操作,可以是EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL,分别是将第三个参数fd加入epfd、修改fd上设定的事件和移除fd
- fd ——指明了要修改兴趣列表中哪一个文件描述符的设定
- ev ——指向结构体epoll_event的指针
结构体epoll_event的定义为
struct epoll_event { uint32_t events; //epoll事件,位掩码 epoll_data_t data; //用户数据 }
其中,epoll_data_t是一个联合,其定义为
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
最后一个相关函数是epoll_wait(),它与select()和poll()类似,用来得到监视结果。其原型为
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout) //成功时返回已就绪的描述符的个数,失败 返回-1
其中,evlist也是指向结构体epoll_event链表的指针,需要通过动态申请内存获得。
在简单介绍了三个函数之后,我们就可以开始改进我们的Tiny了。首先介绍我们的连接池的结构体声明:
typedef struct epoll_event SE; //为了缩短代码的长度 typedef struct { int epfd; //epoll的文件描述符 SE ev; //event结构 SE *ev_list; //指向event结构链表的头指针 } pool;
接下来就是几个相关操作的参数,如初始化、析构函数、添加客户端、移除客户端:
1 void init_pool(int listenfd, pool *p) //初始化连接池 2 { 3 p->epfd = epoll_create(EPOLL_SIZE); //建立epoll描述符 4 p->ev_list = malloc(sizeof(SE)*EPOLL_SIZE); //为链表动态分配内存 5 p->ev.data.fd = listenfd; //设置监听套件字 6 p->ev.events = EPOLLIN; //设置感兴趣的监视类型为输入 7 if (epoll_ctl(p->epfd, EPOLL_CTL_ADD, listenfd, &(p->ev)) != 0) //将listenfd写入epoll描述符 8 { 9 fprintf(stderr, "epoll_ctl error\n"); 10 exit(1); 11 } 12 } 13 14 void free_pool(pool *p) //清空连接池 15 { 16 free(p->ev_list); 17 close(p->epfd); 18 } 19 20 void add_client(int connfd, pool *p) //添加客户端描述符 21 { 22 p->ev.data.fd = connfd; 23 p->ev.events = EPOLLIN; 24 if (epoll_ctl(p->epfd, EPOLL_CTL_ADD, connfd, &(p->ev)) != 0) 25 { 26 fprintf(stderr, "epoll_ctl error\n"); 27 exit(1); 28 } 29 } 30 31 void del_client(int connfd, pool *p) //删除客户端描述符 32 { 33 epoll_ctl(p->epfd, EPOLL_CTL_DEL, connfd, NULL); 34 close(connfd); 35 }
最后,给出主函数的框架:
1 int main(int argc, char *argv[]) 2 { 3 int listenfd, connfd; 4 int err, i, ev_cnt; 5 static pool mypool; 6 //...其余参数声明 7 8 listenfd = open_listenfd(argv[1]); 9 init_pool(listenfd, &mypool); //初始化连接池 10 11 while (1) { 12 //Wait for listening/connected descriptor(s) to become ready 13 ev_cnt = epoll_wait(mypool.epfd, mypool.ev_list, EPOLL_SIZE, -1); 14 if (ev_cnt == -1) 15 { 16 fprintf(stderr, "epoll_wait error\n"); 17 exit(1); 18 } 19 for (i = 0; i < ev_cnt; i++) 20 { 21 if (mypool.ev_list[i].data.fd == listenfd) { //客户端请求建立连接 22 clientlen = sizeof(clientaddr); 23 connfd = accept(listenfd, (SA *)&clientaddr, &clientlen); 24 add_client(connfd, &mypool); //将新建立连接的套接字加入epoll实例描述符 25 } 26 else { //已连接的客户端请求数据 27 doit(&(mypool.ev_list[i].data.fd), &mypool); 28 del_client(mypool.ev_list[i].data.fd, &mypool); //将已处理完的描述符清除 29 } 30 31 } 32 } 33 close(listenfd); 34 free_pool(&mypool); 35 return 0; 36 } 37
对比前边的基于select()函数和poll()函数的IO复用模型,我们可以看到,epoll()的IO复用模型也没有复杂多少,三者的大体框架都是一样的。但epoll在处理高并发的业务时有比另外两个更好的性能,我们要继续掌握它更深层次的使用。这里只是举例说明了epoll最简单的一个用法,感兴趣的同学请自行钻研。