《深入理解计算机系统》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最简单的一个用法,感兴趣的同学请自行钻研。

时间: 2024-12-19 07:47:53

《深入理解计算机系统》Tiny服务器4——epoll类型IO复用版Tiny的相关文章

epoll实现IO复用,TCP通信

函数原型: 函数说明:该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它. 参数说明: fds:是一个struct pollfd结构类型的数组,用于存放需要检测其状态的Socket描述符: 每当调用这个函数之后,系统不会清空这个数组,操作起来比较方便:特别是对于socket连接比较多的情况下,在一定程度上可以提高处理的效率:这一点与select()函数不同,调用select()函数之后,select()函数会清空它所检测的socket描述

TCP编程:select提高服务器处理能力 [socket多路IO复用]

服务器: #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/wait.h>

IO复用之——epoll

一. 关于epoll 对于IO复用模型,前面谈论过了关于select和poll函数的使用,select提供给用户一个关于存储事件的数据结构fd_set来统一监测等待事件的就绪,分为读.写和异常事件集:而poll则是用一个个的pollfd类型的结构体管理事件的文件描述符和事件所关心的events,并通过结构体里面的输出型参数revents来通知用户事件的就绪状态: 但是对于上述两种函数,都是需要用户遍历所有的事件集合来确定到底是哪一个或者是哪些事件已经就绪可以进行数据的处理了,因此当要处理等待的事

【转】《深入理解计算机系统》C程序中常见的内存操作有关的典型编程错误

原文地址:http://blog.csdn.net/slvher/article/details/9150597 对C/C++程序员来说,内存管理是个不小的挑战,绝对值得慎之又慎,否则让由上万行代码构成的模块跑起来后才出现内存崩溃,是很让人痛苦的.因为崩溃的位置在时间和空间上,通常是在距真正的错误源一段距离之后才表现出来.前几天线上模块因堆内存写越界1个字节引起各种诡异崩溃,定位问题过程中的折腾仍历历在目,今天读到<深入理解计算机系统>第9章-虚拟存储器,发现书中总结了C程序中常见的内存操作有

《深入理解计算机系统》读书笔记第七章——链接

<深入理解计算机系统>第七章 链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或拷贝)到存储器并执行. 链接的时机 编译时,也就是在源代码被翻译成机器代码时 加载时,也就是在程序被加载器加载到存储器并执行时. 运行时,由应用程序执行. 在现代系统中,链接是由链接器自动执行的. 7.1 编译器驱动程序 编译系统提供编译驱动程序——调用语言预处理器.编译器.汇编器和链接器. (1)运行C预处理器:源程序main.c->ASCII码中间文件main.i (2)

《深入理解计算机系统》第七章 链接

<深入理解计算机系统>第七章 链接 链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(货被拷贝)到存储器并执行. 链接的时机 编译时,也就是在源代码被翻译成机器代码时 加载时,也就是在程序被加载器加载到存储器并执行时 运行时,由应用程序执行 链接器使分离编译称为可能. 链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或拷贝)到存储器并执行. 链接可以执行于编译时,也就是在源代码被翻译成机器代码时:也可以执行于加载时,也就是在程序

让Tiny服务器运行起来

让Tiny服务器运行起来 之前写了<深入理解计算机系统>一书中的Tiny服务器的源码解析,但是书中并没有介绍如何运行Tiny,下面就介绍下怎么让Tiny运行起来. Tiny的源文件有tiny.c.csapp.c和csapp.h三个.另外要在根目录下新建cgi-bin目录用于存放CGI程序. 方法一: 1.将所有源文件tiny.c.csapp.c和csapp.h放在同一个目录下.另外在同一目录下放置photo.jpg作为测试文件.使用命令: $gcc -o tiny tiny.c csapp.c

深入理解计算机系统9个重点笔记

引言 深入理解计算机系统,对我来说是部大块头.说实话,我没有从头到尾完完整整的全部看完,而是选择性的看了一些我自认为重要的或感兴趣的章节,也从中获益良多,看清楚了计算机系统的一些本质东西或原理性的内容,这对每个想要深入学习编程的程序员来说都是至关重要的.只有很好的理解了系统到底是如何运行我们代码的,我们才能针对系统的特点写出高质量.高效率的代码来.这本书我以后还需要多研究几遍,今天就先总结下书中我已学到的几点知识. 重点笔记 编写高效的程序需要下面几类活动: 选择一组合适的算法和数据结构.这是很

《深入理解计算机系统》 Chapter 7 读书笔记

<深入理解计算机系统>Chapter 7 读书笔记 链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(货被拷贝)到存储器并执行. 链接的时机 编译时,也就是在源代码被翻译成机器代码时 加载时,也就是在程序被加载器加载到存储器并执行时 运行时,由应用程序执行 链接器使分离编译称为可能. 一.编译器驱动程序 大部分编译系统提供编译驱动程序:代表用户在需要时调用语言预处理器.编译器.汇编器和链接器. 1.将示例程序从ASCⅡ码源文件翻译成可执行目标文件的步骤 (1)运