直到下次有新的数据进来的时候才会再次触发就绪事件。
使用epoll的一个服务端程序(不完善):
//:epollServer.c #include <sys/epoll.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <errno.h> #define SERV_PORT 6000 #define MAX_FD 1024 #define BUF_SIZE 1024 #define EVENTS_NUM 20 #define TIMEOUT 1000 #define MAX_BACK 20 void setnonblocking(int sock) { int opts; opts = fcntl(sock, F_GETFL); if (opts < 0) { perror("fcntl(sock, GETFL)"); exit(1); } opts = opts | O_NONBLOCK; if (fcntl(sock, F_SETFL, opts) < 0) { perror("fcntl(sock, SETFL, opts)"); exit(1); } } int main(int argc, char** argv) { int listenfd, connfd, sockfd, epfd, nfds, n = 0, i = 0; char line[BUF_SIZE+1]; socklen_t clilen; struct epoll_event ev, events[EVENTS_NUM]; epfd = epoll_create(MAX_FD); struct sockaddr_in serveraddr, clientaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); setnonblocking(listenfd); ev.data.fd = listenfd; ev.events = EPOLLIN | EPOLLET; //注册epoll事件 epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); bzero(&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(SERV_PORT); bzero(line, BUF_SIZE); bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); listen(listenfd, MAX_BACK); for (; ;) { //等待epoll事件的发生 nfds = epoll_wait(epfd, events, EVENTS_NUM, TIMEOUT); for (i = 0; i < nfds; i ++) { // 在listenfd上发生读事件,说明有新的客户端连接 if (events[i].data.fd == listenfd) { //sockaddr 和sockaddr_in的区别:二者占用的内存空间一样大可互相转化,前者在socket.h中后者在in.h中, //sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。 //而sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作。使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。 connfd = accept(listenfd, (struct sockaddr * )&clientaddr, &clilen); if (connfd < 0) { perror("accept error, connfd < 0"); } setnonblocking(connfd); char *str = inet_ntoa(clientaddr.sin_addr); printf("Accept a connection from %s\n", str); //注册这个connfd的ev ev.data.fd = connfd; ev.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); } //已连接的客户,收到数据 else if (events[i].events & EPOLLIN) { printf("EPOLLIN\n"); if ((sockfd = events[i].data.fd) < 0) continue; //注意这里,边缘触发应该保证读完缓冲区 //可以用循环来反复读取,当n==0的时候表示读完 if ((n = read(sockfd, line, BUF_SIZE)) < 0) { if (errno == ECONNRESET) { close(sockfd); events[i].data.fd = -1; } else { printf("Readline error\n"); } } else if (n == 0) { printf("Null message\n"); close(sockfd); events[i].data.fd = -1; } else { line[n] = '\0'; printf("Read from %d: %s\n", sockfd, line); //注册写操作事件 //ev.data.fd = sockfd; //ev.events = EPOLLOUT | EPOLLET; //epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); //这里直接写回给客户端,没有再使用epoll调度写事件 write(sockfd, line, n); } } //如果有数据要发送,本程序中没什么用。。。 else if (events[i].events & EPOLLOUT) { sockfd = events[i].data.fd; write(sockfd, line, n); ev.data.fd = sockfd; ev.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); } //有错的话是否需要close else { printf("Other events!\n"); //close(events[i].data.fd); events[i].data.fd = -1; } } } //epoll fd 会占用一个文件句柄 close(epfd); return 0; }
①从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
② 此外,内核使用了slab机制,为epoll提供了快速的数据结构:
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
③ epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。
如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。
1.执行epoll_create时,创建了红黑树和就绪list链表。
2.执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。