目录html
Linux NIO 系列(04-3) epolllinux
Netty 系列目录(http://www.javashuo.com/article/p-hskusway-em.html)编程
句柄限制:单个进程可以监视的文件描述符的数量存在最大限制,一般是 1024,固然能够更改数量,但因为select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在 linux 内核头文件中,有这样的定义: #define __FD_SETSIZE 1024
)api
数据拷贝:内核 / 用户空间内存拷贝问题,select 须要复制大量的句柄数据结构,产生巨大的开销;数组
轮询机制:select 返回的是含有整个句柄的数组,应用程序须要遍历整个数组才能发现哪些句柄发生了事件;缓存
select 的触发方式是水平触发,应用程序若是没有完成对一个已经就绪的文件描述符进行 IO 操做,那么以后每次 select 调用仍是会将这些文件描述符通知进程。服务器
设想一下以下场景:有 100 万个客户端同时与一个服务器进程保持着 TCP 链接。而每一时刻,一般只有几百上千个 TCP 链接是活跃的(事实上大部分场景都是这种状况)。如何实现这样的高并发?网络
粗略计算一下,一个进程最多有 1024 个文件描述符,那么咱们须要开 1000 个进程来处理 100 万个客户链接。若是咱们使用 select 模型,这 1000 个进程里某一段时间内只有数个客户链接须要数据的接收,那么咱们就不得不轮询 1024 个文件描述符以肯定到底是哪一个客户有数据可读,想一想若是 1000 个进程都有相似的行为,那系统资源消耗可有多大啊!数据结构
针对 select 模型的缺点,epoll 模型被提出来了!并发
epoll 在 Linux2.6 内核正式提出,是基于事件驱动的 I/O 方式,相对于 select 来讲,epoll 没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。
Linux 中提供的 epoll 相关函数以下:
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
函数建立一个 epoll 句柄,参数 size 代表内核要监听的描述符数量。调用成功时返回一个 epoll 句柄描述符,失败时返回 -1。
函数注册要监听的事件类型。四个参数解释以下:
epfd 表示 epoll 句柄
op 表示 fd 操做类型,有以下 3 种
EPOLL_CTL_ADD 注册新的fd到epfd中 EPOLL_CTL_MOD 修改已注册的fd的监听事件 EPOLL_CTL_DEL 从epfd中删除一个fd
fd 是要监听的描述符
event 表示要监听的事件。epoll_event 结构体定义以下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;
函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。
epoll 是 Linux 内核为处理大批量文件描述符而做了改进的 poll,是 Linux 下多路复用 IO 接口 select/poll 的加强版本,它能显著提升程序在大量并发链接中只有少许活跃的状况下的系统 CPU 利用率。缘由就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入 Ready 队列的描述符集合就好了。
epoll 除了提供 select/poll 那种 IO 事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存 IO 状态,减小 epoll_wait/epoll_pwait 的调用,提升应用程序效率。
水平触发(LT):默认工做模式,即当 epoll_wait 检测到某描述符事件就绪并通知应用程序时,应用程序能够不当即处理该事件;下次调用 epoll_wait 时,会再次通知此事件。
水平触发同时支持 block 和 non-block socket。在这种作法中,内核告诉你一个文件描述符是否就绪了,而后你能够对这个就绪的 fd 进行 IO 操做。若是你不做任何操做,内核仍是会继续通知你的,因此,这种模式编程出错误可能性要小一点。好比内核通知你其中一个fd能够读数据了,你赶忙去读。你仍是懒懒散散,不去读这个数据,下一次循环的时候内核发现你还没读刚才的数据,就又通知你赶忙把刚才的数据读了。这种机制能够比较好的保证每一个数据用户都处理掉了。
边缘触发(ET): 当 epoll_wait 检测到某描述符事件就绪并通知应用程序时,应用程序必须当即处理该事件。若是不处理,下次调用 epoll_wait 时,不会再次通知此事件。(直到你作了某些操做致使该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。
边缘触发是高速工做方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核经过 epoll 告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。简而言之,就是内核通知过的事情不会再说第二遍,数据错过没读,你本身负责。这种机制确实速度提升了,可是风险相伴而行。
LT 和 ET 本来应该是用于脉冲信号的,可能用它来解释更加形象。Level 和 Edge 指的就是触发点,Level 为只要处于水平,那么就一直触发,而 Edge 则为上升沿和降低沿的时候触发。好比:0->1 就是 Edge,1->1 就是 Level。
ET 模式很大程度上减小了 epoll 事件的触发次数,所以效率比 LT 模式下高。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/epoll.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #define SERVER_PORT 8888 #define OPEN_MAX 3000 #define BACKLOG 10 #define BUF_SIZE 1024 int main() { int ret, i; int listenfd, connfd, epollfd; int nready; int recvbytes, sendbytes; char* recv_buf; struct epoll_event ev; struct epoll_event* ep; ep = (struct epoll_event*) malloc(sizeof(struct epoll_event) * OPEN_MAX); recv_buf = (char*) malloc(sizeof(char) * BUF_SIZE); struct sockaddr_in seraddr; struct sockaddr_in cliaddr; int addr_len; memset(&seraddr, 0, sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(SERVER_PORT); seraddr.sin_addr.s_addr = htonl(INADDR_ANY); listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listenfd == -1) { perror("create socket failed.\n"); return 1; } ret = bind(listenfd, (struct sockaddr*)&seraddr, sizeof(seraddr)); if(ret == -1) { perror("bind failed.\n"); return 1; } ret = listen(listenfd, BACKLOG); if(ret == -1) { perror("listen failed.\n"); return 1; } epollfd = epoll_create(1); if(epollfd == -1) { perror("epoll_create failed.\n"); return 1; } ev.events = EPOLLIN; ev.data.fd = listenfd; ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev); if(ret == -1) { perror("epoll_ctl failed.\n"); return 1; } while(1) { nready = epoll_wait(epollfd, ep, OPEN_MAX, -1); if(nready == -1) { perror("epoll_wait failed.\n"); return 1; } for(i = 0; i < nready; i++) { if(ep[i].data.fd == listenfd) { addr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &addr_len); printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); if(connfd == -1) { perror("accept failed.\n"); return 1; } ev.events = EPOLLIN; ev.data.fd = connfd; ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev); if(ret == -1) { perror("epoll_ctl failed.\n"); return 1; } } else { recvbytes = recv(ep[i].data.fd, recv_buf, BUF_SIZE, 0); if(recvbytes <= 0) { close(ep[i].data.fd); epoll_ctl(epollfd, EPOLL_CTL_DEL, ep[i].data.fd, &ev); continue; } printf("receive %s\n", recv_buf); sendbytes = send(ep[i].data.fd, recv_buf, (size_t)recvbytes, 0); if(sendbytes == -1) { perror("send failed.\n"); } } } // for each ev } // while(1) close(epollfd); close(listenfd); free(ep); free(recv_buf); return 0; }
参考:
天天用心记录一点点。内容也许不重要,但习惯很重要!