学科分类
目录
Linux编程

epoll的工作模式

epoll有两种工作模式,分别为边缘触发(Edge Triggered)模式和水平触发(LevelTriggered)模式。

所谓边缘触发,指只有当文件描述符就绪时会触发通知,即便此次通知后系统执行I/O操作只读取了部分数据,文件描述符中仍有数据剩余,也不会再有通知递达,直到该文件描述符从当前的就绪态变为非就绪态,再由非就绪态再次变为就绪态,才会触发第二次通知;此外,接收缓冲区大小为5字节,也就是说ET模式下若只进行一次I/O操作,每次只能接收到5字节的数据。因此系统在收到就绪通知后,应尽量多次地执行I/O操作,直到无法再读出数据为止。

而水平触发与边缘触发有所不同,即便就绪通知已发送,内核仍会多次检测文件描述符状态,只要文件描述符为就绪态,内核就会继续发送通知。

epoll的工作模式在调用注册函数epoll_ctl()时确定,由该函数中参数event的成员events指定,默认情况下epoll的工作模式为水平触发,若要将其设置为边缘触发模式,需使用宏EPOLLET对event进行设置,具体示例如下。

event.events=EPOLLIN|EPOLLET;

之后需在循环中不断调用,保证将文件描述符中的数据全部读出。

案例5中的epoll便工作在水平模式下,为帮助读者理解,下面给出具体案例,来展示epoll在边缘触发模式下如何实现双端通信。ET模式只能工作在非阻塞模式下,否则单纯使用epoll(单进程)将无法同时处理多个文件描述符,因此在实现案例之前,需先掌握设置文件描述符状态的方法,Linux系统中可使用fcntl()函数来设置文件描述符的属性。

fcntl()函数是Linux中的一个系统调用,其功能为获取或修改已打开文件的性质,该函数存在于函数库fcntl.h中,其声明如下:

int fcntl(int fd, int cmd, ... /* arg */ );

其中参数fd为被操作的文件描述符,cmd为操作fd的命令(具体取值可参见Linux的manpage),之后的arg用来接收命令cmd所需使用的参数,该值可为空。

若要通过fcntl()设置文件描述符状态,通常先使用该函数获取fd的当前状态,再对获取的值进行位操作,最后调用fcntl()将操作的结果重新写回文件描述符。如下为修改文件描述符阻塞状态的方法:

flag = fcntl(fd, F_GETFL);   //宏F_GETEL表示获取文件描述符相关属性
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);   //使用新属性设置文件描述符

下面给出ET模式下epoll服务器的实现。

案例6:搭建工作在边缘触发模式的epoll服务器,使服务器可接收并处理客户端发送的数据。

epollet_s.c //服务器

 1    #include <stdio.h>
 2    #include <string.h>
 3    #include <netinet/in.h>
 4    #include <arpa/inet.h>
 5    #include <sys/wait.h>
 6    #include <sys/types.h>
 7    #include <sys/epoll.h>
 8    #include <unistd.h>
 9    #include <fcntl.h>
 10    #define MAXLINE 10
 11    #define SERV_PORT 8000
 12    int main(void)
 13    {
 14        struct sockaddr_in servaddr, cliaddr;
 15        socklen_t cliaddr_len;
 16        int listenfd, connfd;
 17        char buf[MAXLINE];
 18        char str[INET_ADDRSTRLEN];
 19        int i, efd, flag;
 20        listenfd = socket(AF_INET, SOCK_STREAM, 0);
 21        bzero(&servaddr, sizeof(servaddr));
 22        servaddr.sin_family = AF_INET;
 23        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 24        servaddr.sin_port = htons(SERV_PORT);
 25        bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 26        listen(listenfd, 20);
 27        struct epoll_event event;
 28        struct epoll_event resevent[10];
 29        int res, len;
 30        efd = epoll_create(10);
 31        //设置epoll为ET模式
 32        event.events = EPOLLIN | EPOLLET;
 33        printf("Accepting connections ...\n");
 34        cliaddr_len = sizeof(cliaddr);
 35        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
 36        printf("received from %s at PORT %d\n",
 37            inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
 38            ntohs(cliaddr.sin_port));
 39        flag = fcntl(connfd, F_GETFL);
 40        flag |= O_NONBLOCK;
 41        fcntl(connfd, F_SETFL, flag);
 42        event.data.fd = connfd;
 43        epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
 44        //获取数据
 45        while (1) {
 46            printf("epoll_wait begin\n");
 47            res = epoll_wait(efd, resevent, 10, -1);
 48            printf("epoll_wait end res %d\n", res);
 49            if (resevent[0].data.fd == connfd) {
 50                while ((len = read(connfd, buf, MAXLINE / 2)) > 0)
 51                    write(STDOUT_FILENO, buf, len);
 52            }
 53        }
 54        return 0;
 55    }

该案例中客户端程序与案例5相同,此处不再重复给出。

编译程序,分别在不同终端打开服务器和客户端,服务器和客户端中的信息分别如下。

服务器:

Accepting connections ...
received from 127.0.0.1 at PORT 60806
epoll_wait begin
epoll_wait end res 1
hello world
epoll_wait begin

客户端:

hello world

由程序执行结果可知,案例实现成功。

需要注意的是,若将服务器端第50行代码中的while修改为if,则服务器端的打印结果如下:

Accepting connections ...
received from 127.0.0.1 at PORT 60806
epoll_wait begin
epoll_wait end res 1
helloepoll_wait begin

由此可知,ET模型的epoll服务器每次只能读取5字节的字符。

多学一招:线程池

多进程/多线程可单独使用,也可与I/O多路转接服务器结合,通过转接机制监控客户端程序状态,通过多进程/多线程处理用户请求,以期减少资源消耗,提升服务器效率。

然而大多网络端服务器都有一个特点,即单位时间内需处理的连接请求数目虽然巨大,但处理时间却是极短的,如此,若使用多进程/多线程机制结合I/O多路转接机制搭建的服务器,便需在每时每刻不停地创建、销毁进程或线程,虽说相对进程,线程消耗的资源已相当少,但诸多线程同时创建和销毁,其开销仍是不可忽视的。而Linux系统中的线程池机制便能客服这些问题。

所谓线程池(Thread Pool),简单来说,就是一个用来放置线程的“池子”。线程池的实现原理如下:当服务器程序启动后,预先在其中创建一定数量的线程,并将这些线程依次加入队列中。在没有客户端请求抵达时,线程队列中的线程都处于阻塞状态,此时这些线程只占用一些内存,但不占用cpu。若随后有用户请求到达,由线程池从线程队列中选出一个空闲线程,并将用户请求传给选出的线程,由该线程完成用户请求。用户请求处理完毕,该线程并不退出,而是再次被加入线程队列,等待下一次任务。此外,若线程队列中处于阻塞状态的线程较多,为节约资源,线程池会自动销毁一部分线程;若线程队列中所有线程都有任务执行,线程池会自动创建一定数量的新线程,以提高服务器效率。

点击此处
隐藏目录