epoll同上篇博客中的select一样,都是用于多路转接,但epoll被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
一、epoll相关系统调用
epoll只有三个系统调用函数:
epoll_create:创建epoll模型
epoll_ctl:管理epoll模型
epoll_wait:等待I/O时间就绪
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
二、epoll工作原理
内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式,在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
关于内存映射技术mmap:
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset );
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它 从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射 到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。
三、epoll模型
(1)调用epoll_create创建epoll模型的时候,实际上是在内核区创建了一棵空的红黑树和一个空的队列;
(2)调用epoll_ctl的时候,实际上是在往红黑树中添加结点,结点描述的是文件描述符及其上的对应事件;
(3)当某文件描述符上的某事件就绪的时候操作系统会创造一个结点放在队列中(此结点表示此文件描述符上的此事件就绪),这个队列通过内存映射机制让用户看到。
四、epoll的优点
1.支持一个进程打开无限数目的fd
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过epoll没有这个限制,它所支持的fd上限是最大可以打来文件的数目,这个数字一般远大于1024,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2.I/O效率不随fd数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
3.使用mmap加速内核与用户控件的消息传递
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把fd消息通知给用户空间都需要内核把fd信息传递给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。
五、epoll服务器
epoll_server.c:
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <unistd.h>
4. #include <sys/types.h>
5. #include <sys/socket.h>
6. #include <arpa/inet.h>
7. #include <netinet/in.h>
8. #include <sys/epoll.h>
9. #include <string.h>
10.
11. static void usage(const char* proc)
12. {
13. "Usage: [local_ip] [local_port] %s\n", proc);
14. }
15.
16. int startup(const char* _ip, int _port)
17. {
18. int sock = socket(AF_INET, SOCK_STREAM, 0);
19. if(sock < 0)
20. {
21. "socket");
22. exit(2);
23. }
24. struct sockaddr_in local;
25. local.sin_family = AF_INET;
26. local.sin_port = htons(_port);
27. local.sin_addr.s_addr = inet_addr(_ip);
28. if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
29. {
30. "bind");
31. exit(3);
32. }
33. if(listen(sock, 10) < 0)
34. {
35. "listen");
36. exit(4);
37. }
38. return sock;
39. }
40.
41. int main(int argc, char* argv[])
42. {
43. if(argc != 3)
44. {
45. usage(argv[0]);
46. return 1;
47. }
48.
49. int listen_sock = startup(argv[1], atoi(argv[2]));
50.
51. int epfd = epoll_create(256);
52. struct epoll_event ev;
53. ev.events = EPOLLIN;
54. ev.data.fd = listen_sock;
55. epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
56. int nums = -1;
57. struct epoll_event revs[64];
58. int timeout = 1000;
59.
60. while(1)
61. {
62. switch(nums = epoll_wait(epfd, revs, 64, timeout))
63. {
64. case -1:
65. "epoll_wait");
66. break;
67. case 0:
68. "timeout...\n");
69. default:
70. {
71. int i = 0;
72. for(; i<nums; ++i)
73. {
74. int sock = revs[i].data.fd;
75. if(sock==listen_sock && (revs[i].events&EPOLLIN))
76. {
77. //listen_sock ready!!!
78. struct sockaddr_in client;
79. socklen_t len = sizeof(client);
80. int new_sock = accept(listen_sock,
81. (struct sockaddr*)&client, &len);
82. if(new_sock < 0)
83. {
84. "accept");
85. continue;
86. }
87. ev.events = EPOLLIN;
88. ev.data.fd = new_sock;
89. epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev);
90. }
91. else if(sock != listen_sock)
92. {
93. if(revs[i].events & EPOLLIN)
94. {
95. //read event ready!!!
96. char buf[1024];
97. ssize_t s = read(sock, buf, sizeof(buf)-1);
98. if(s >0)
99. {
100. buf[s] = 0;
101. "client# %s\n", buf);
102. ev.events = EPOLLOUT;
103. epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);
104. }
105. else if(s == 0)
106. {
107. "client is quit...\n");
108. close(sock);
109. epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
110. }
111. else
112. {
113. "read");
114. continue;
115. }
116. }
117. else if(revs[i].events & EPOLLOUT)
118. {
119. const char* msg = "HTTP/1.0 OK 200\r\n\r\n
120. <html><h1>hello epoll!</h1></html>";
121. write(sock, msg, strlen(msg));
122. close(sock);
123. epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
124. }
125. }
126. }
127. }
128. }
129. }
130. }
运行结果:
浏览器运行结果:
EPOLLLT——水平触发
EPOLLET——边缘触发
epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。
首先介绍一下LT工作模式:
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
优点:当进行socket通信的时候,保证了数据的完整输出,进行IO操作的时候,如果还有数据,就会一直的通知你。
缺点:由于只要还有数据,内核就会不停的从内核空间转到用户空间,所有占用了大量内核资源,试想一下当有大量数据到来的时候,每次读取一个字节,这样就会不停的进行切换。内核资源的浪费严重。效率来讲也是很低的。
ET:
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).
优点:每次内核只会通知一次,大大减少了内核资源的浪费,提高效率。
缺点:不能保证数据的完整。不能及时的取出所有的数据。
应用场景: 处理大数据。使用non-block模式的socket。
epoll_create
从slab缓存中创建一个eventpoll对象,并且创建一个匿名的fd跟fd对应的file对象,
而eventpoll对象保存在struct file结构的private指针中,并且返回,
该fd对应的file operations只是实现了poll跟release操作
创建eventpoll对象的初始化操作
获取当前用户信息,是不是root,最大监听fd数目等并且保存到eventpoll对象中
初始化等待队列,初始化就绪链表,初始化红黑树的头结点
epoll_ctl操作
将epoll_event结构拷贝到内核空间中
并且判断加入的fd是否支持poll结构(epoll,poll,selectI/O多路复用必须支持poll操作).
并且从epfd->file->privatedata获取event_poll对象,根据op区分是添加删除还是修改,
首先在eventpoll结构中的红黑树查找是否已经存在了相对应的fd,没找到就支持插入操作,否则报重复的错误.
相对应的修改,删除比较简单就不啰嗦了
插入操作时,会创建一个与fd对应的epitem结构,并且初始化相关成员,比如保存监听的fd跟file结构之类的
重要的是指定了调用poll_wait时的回调函数用于数据就绪时唤醒进程,(其内部,初始化设备的等待队列,将该进程注册到等待队列)完成这一步, 我们的epitem就跟这个socket关联起来了, 当它有状态变化时,
会通过ep_poll_callback()来通知.
最后调用加入的fd的file operation->poll函数(最后会调用poll_wait操作)用于完成注册操作.
最后将epitem结构添加到红黑树中
epoll_wait操作
计算睡眠时间(如果有),判断eventpoll对象的链表是否为空,不为空那就干活不睡明.并且初始化一个等待队列,把自己挂上去,设置自己的进程状态
为可睡眠状态.判断是否有信号到来(有的话直接被中断醒来,),如果啥事都没有那就调用schedule_timeout进行睡眠,如果超时或者被唤醒,首先从自己初始化的等待队列删除
,然后开始拷贝资源给用户空间了
拷贝资源则是先把就绪事件链表转移到中间链表,然后挨个遍历拷贝到用户空间,
并且挨个判断其是否为水平触发,是的话再次插入到就绪链表
标签:socket,epoll,描述符,fd,内核,服务器,poll From: https://blog.51cto.com/u_16147764/6397247