前面介绍了select处理,这一章继续介绍另外一种I/O多路服用的机制:epoll。来比较下两种机制的不同点。
select: 调用过程如下:

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间

总结:

select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

对于select的几个缺点。epoll的改进机制如下:

对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

  对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。

  对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

总结:

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销

epoll的接口函数很简单,只有3个函数

1. int epoll_create(int size);

创建一个 epoll 的句柄, size 用来告诉内核这个监听的数目一共有多大。这个参数不同于 select() 中的第一个参数,给出最大监听的 fd+1 的值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/ 进程 id/fd/ ,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll 的事件注册函数,它不同与 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

第一个 参数是 epoll_create() 的返回值,

第二个 参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD :注册新的 fd 到 epfd 中;

EPOLL_CTL_MOD :修改已经注册的 fd 的监听事件;

EPOLL_CTL_DEL :从 epfd 中删除一个 fd ;

第三个 参数是需要监听的 fd ,

第四个 参数是告诉内核需要监听什么事, struct epoll_event 结构如下:

struct epoll_event {

__uint32_t events;  /* Epoll events */

epoll_data_t data;  /* User data variable */

};

events 可以是以下几个宏的集合:

EPOLLIN :      表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);

EPOLLOUT :     表示对应的文件描述符可以写;

EPOLLPRI :       表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR :      表示对应的文件描述符发生错误;

EPOLLHUP :      表示对应的文件描述符被挂断;

EPOLLET :       将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的。

EPOLLONESHOT : 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待事件的产生,类似于 select() 调用。参数 events 用来从内核得到事件的集合, maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的 size ,参数 timeout 是超时时间(毫秒, 0 会立即返回, -1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。 epoll有两种工作方式:

LT level triggered 水平触发模式,

同时支持阻塞和非阻塞的socket。在这种模式中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行I/O操作,如果你不做任何操作,还是会继续通知你。(没处理这个流还是一直通知你)

ET edge triggered 边缘触发模式

只支持非阻塞的socket。效率比LT高。这种工作模式下,当从epoll_wait调用获取到事件后,如果没有把这次事件对应的套接字处理完,那么在这个套接字中没有心的时间再次到来时,ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT只要有数据就总可以获取。

参考下面这个图:

实现代码如下:

//网络编程服务端

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <errno.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <arpa/inet.h>//htons()函数头文件

#include <netinet/in.h>//inet_addr()头文件

#include <fcntl.h>

#include <sys/epoll.h>

#include "pub.h"

#define MAXSOCKET 20

int main(int arg, char *args[])

{

if (arg < 2)

{

printf("please print one param!\n");

return -1;

}

//create server socket

int listen_st = server_socket(atoi(args[1]));

if (listen_st < 0)

{

return -1;

}

/*

* 声明epoll_event结构体变量ev,变量ev用于注册事件,

* 数组events用于回传需要处理的事件

*/

struct epoll_event ev, events[100];

//生成用于处理accept的epoll专用文件描述符

int epfd = epoll_create(MAXSOCKET);

//把socket设置成非阻塞方式    setnonblock(listen_st);

//设置需要放到epoll池里的文件描述符

ev.data.fd = listen_st;

//设置这个文件描述符需要epoll监控的事件

/*

* EPOLLIN代表文件描述符读事件

*accept,recv都是读事件

*/

ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;

/*

* 注册epoll事件

* 函数epoll_ctl中&ev参数表示需要epoll监视的listen_st这个socket中的一些事件

*/

epoll_ctl(epfd, EPOLL_CTL_ADD, listen_st, &ev);

while (1)

{

/*

* 等待epoll池中的socket发生事件,这里一般设置为阻塞的

* events这个参数的类型是epoll_event类型的数组

* 如果epoll池中的一个或者多个socket发生事件,

* epoll_wait就会返回,参数events中存放了发生事件的socket和这个socket所发生的事件

* 这里强调一点,epoll池存放的是一个个socket,不是一个个socket事件

* 一个socket可能有多个事件,epoll_wait返回的是有消息的socket的数目

* 如果epoll_wait返回事件数组后,下面的程序代码却没有处理当前socket发生的事件

* 那么epoll_wait将不会再次阻塞,而是直接返回,参数events里面的就是刚才那个socket没有被处理的事件

*/

int nfds = epoll_wait(epfd, events, MAXSOCKET, -1);

if (nfds == -1)

{

printf("epoll_wait failed ! error message :%s \n", strerror(errno));

break;

}

int i = 0;

for (; i < nfds; i++)

{

if (events[i].data.fd < 0)

continue;

if (events[i].data.fd == listen_st)

{

//接收客户端socket

int client_st = server_accept(listen_st);

/*

* 监测到一个用户的socket连接到服务器listen_st绑定的端口

*

*/

if (client_st < 0)

{

continue;

}

//设置客户端socket非阻塞                setnonblock(client_st);

//将客户端socket加入到epoll池中

struct epoll_event client_ev;

client_ev.data.fd = client_st;

client_ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;

epoll_ctl(epfd, EPOLL_CTL_ADD, client_st, &client_ev);

/*

* 注释:当epoll池中listen_st这个服务器socket有消息的时候

* 只可能是来自客户端的连接消息

* recv,send使用的都是客户端的socket,不会向listen_st发送消息的

*/

continue;

}

//客户端有事件到达

if (events[i].events & EPOLLIN)

{

//表示服务器这边的client_st接收到消息

if (socket_recv(events[i].data.fd) < 0)

{

close_socket(events[i].data.fd);

//接收数据出错或者客户端已经关闭

events[i].data.fd = -1;

/*这里continue是因为客户端socket已经被关闭了,

* 但是这个socket可能还有其他的事件,会继续执行其他的事件,

* 但是这个socket已经被设置成-1

* 所以后面的close_socket()函数都会报错

*/

continue;

}

/*

* 此处不能continue,因为每个socket都可能有多个事件同时发送到服务器端

* 这也是下面语句用if而不是if-else的原因,

*/

}

//客户端有事件到达

if (events[i].events & EPOLLERR)

{

printf("EPOLLERR\n");

//返回出错事件,关闭socket,清理epoll池,当关闭socket并且events[i].data.fd=-1,epoll会自动将该socket从池中清除                close_socket(events[i].data.fd);

events[i].data.fd = -1;

continue;

}

//客户端有事件到达

if (events[i].events & EPOLLHUP)

{

printf("EPOLLHUP\n");

//返回挂起事件,关闭socket,清理epoll池                close_socket(events[i].data.fd);

events[i].data.fd = -1;

continue;

}

}

}

//close epoll    close(epfd);

//close server socket    close_socket(listen_st);

return 0;

}

最新文章

  1. 【译】java.lang.ThreadLocal
  2. Win2012R2的一个Bug---安装群集后可能引发的软件崩溃问题及相应补丁
  3. 不自动生成Android Dependencies的解决方式
  4. hdu5441(2015长春赛区网络赛1005)类最小生成树、并查集
  5. LPC1768之ISP
  6. 初识 Asp.Net内置对象之Request对象
  7. service structure flowchart [mobile to server via TCP/IP protocol]
  8. AO之Addins开发[杂谈1] Toolbar中添加一条分割线
  9. FZU 2234
  10. Spring Boot 2.x 编写 RESTful API (六) 事务
  11. 资源中心的ES 服务的COM.IFLYTEK.ERSP.API.RESOURCEAPI 接口注册ZOOKEEPER失败,解决记录
  12. 安装win10 和win中的一些杂项问题
  13. RabbitMQ(2) 一般介绍
  14. [原]CentOS7.2部署KVM虚拟机
  15. M2阶段测试报告
  16. 【emWin】例程十四:xbf外置字体
  17. 托管博客到coding或者github
  18. [python]如何理解uiautomator里面的 right,left,up,down 及使用场景
  19. ELK学习笔记之ELK架构与介绍
  20. 禁止一个click事件执行的方法

热门文章

  1. 【Scala-ML】怎样利用Scala构建并行机器学习系统
  2. hadoop2.4 支持snappy
  3. Restful风格的前后端分离
  4. 【HTML5】元素&lt;head&gt;的使用
  5. Linux 查看某个程序所在端口的 PID
  6. JSON Web Token (JWT) 实现与使用方法
  7. select * from A.B.C.D sqlserver 中 select * from .Literary_PuDong.dbo.Users
  8. Memcache针对不同场景数据应用缓存策略
  9. 征信报告页面的input验证收集
  10. Swagger跨域访问