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。若随后有用户请求到达,由线程池从线程队列中选出一个空闲线程,并将用户请求传给选出的线程,由该线程完成用户请求。用户请求处理完毕,该线程并不退出,而是再次被加入线程队列,等待下一次任务。此外,若线程队列中处于阻塞状态的线程较多,为节约资源,线程池会自动销毁一部分线程;若线程队列中所有线程都有任务执行,线程池会自动创建一定数量的新线程,以提高服务器效率。