在前两篇博客中,我们讲述了常见的IO模型,并对多路转接模型中的select模型进行了细致介绍和具体的代码实现。那么接下来,我们在本篇博客中将对剩余的多路转接模型中的:poll和epoll进行介绍。
目录
1.poll
1.1操作流程
1.2代码实现
2.epoll
2.1操作流程
2.2代码实现
2.3触发方式
2.3.1水平触发
2.3.2边缘触发
1.poll
1.1操作流程
一、我们需要定义一个事件结构体数组,来存取监控事件和它们的状态,具体的结构体内容如下:
struct pollfd {
int fd;//需要进行监控的文件描述符
short events;//想要监控的事件, POLLIN--可读 POLLOUT--可写
short revents;//监控返回后,存储实际就绪的事件
};
二、我们对不同的描述符按需进行具体的监控事件,并在数组中进行设置,例如:
//定义具体结构体数据
struct pollfd fds[MAX];
//对标准输入进行可读事件监控
fds[0].fd = 0;
fds[0].events = POLLIN;
//对标准输出进行可写事件监控
fds[0].fd = 1;
fds[0].events = POLLOUT;
三、开始监控,原理和select相似,依旧是将数组中有效数据拷贝到内核当中,进行多次的轮询遍历,第一次遍历:判断有无就绪事件,没有则挂起到监控队列;第二次遍历:进程阻塞被唤醒之后,进行一次遍历,对每个元素的revents设置实际的就绪事件。
我们介绍具体的监控接口如下:
int poll(struct pollfd *fds, nfds_t maxevents, int timeout);
/*其中,
fds是定义事件结构体数组首地址
maxevents数组中有效元素个数
timeout是监控阻塞的超时时间,以毫秒为单位
*/
具体的返回值为:>0表示实际就绪的事件个数;==0表示超时;<0表示出错。
四、 调用返回之后,遍历事件结构体数组,根据revents成员确定描述符是否就绪了某个事件,进而对描述符进行操作。
1.2代码实现
我们设计简单代码,对poll模型加以使用,即对标准输入进行读端监控,得到具体代码如下:
然后编译并执行可以得到结果如下:
超时警告和读端监控都可正常运行。
2.epoll
2.1操作流程
一、在内核中创建epoll句柄即eventpoll结构,具体接口如下:
int epoll_create(int size);
/*
其中,size是所监控的描述符上线,在Linux2.6.8中被忽略,但必须大于0
*/
具体的返回值是:返回epoll的描述符,创建错误则返回-1。(我们可以用epoll监控epoll)
二、向内核的句柄中,添加/移除/修改所要监控的描述符即对应的事件结构,具体的操作接口如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
/*
其中,epfd是epoll_create所返回的epoll描述符
op是对epoll要进行的操作,EPOLL_CTL_ADD/EPOLL_CTL_DEL/EPOLL_CTL_MOD
fd是要进行操作的描述符
struct epoll_event *ev是对描述符进行操作的详细信息
*/
我们对其中结构体struct epoll_event进行介绍,其中包括一个uint32_t类型的变量,代表想要监控的事件,以及监控后存放实际就绪的事件;还存在一个联合体,用来存储额外信息。
三、开始监控,epoll的监控是一个异步阻塞操作,发起监控调用是为了告诉操作系统,可以开始监控,并且具体的监控任务由操作系统完成,而系统内容为epoll的每个描述的就绪事件挂了一个回调函数。
该回调函数的功能就是:描述符一旦就绪了指定的事件,将事件信息拷贝一份到epoll_create接口所创建的双向链表rdlist中,那么rdlist的作用便是:存放就绪的描述符对应的事件结构。
一旦系统监控中存在描述符就绪,则唤醒进程的阻塞,进程被唤醒之后,将会查看rdlist双向链表中是否由数据,便可确定是否有描述符就绪。(进程只需要判断rdlist链表是否为NULL,便可知是否存在事件就绪,不需要进行遍历)
具体的操作接口如下:
int epoll_wait(int epfd, struct epoll_event *evs, int maxevents, int timeout);
/*
其中,epfd时是epoll_create所返回的描述符
evs是epoll_event结构体数组的空间首地址,接收就绪事件
maxevents是数组的最大元素个数,也表示了当前想要获取的最大事件个数
timeout是设置的监控时间--以毫秒为单位
*/
具体的返回值为:>0表示实际就绪的事件个数;==0表示超时;<0表示出错。
2.2代码实现
我们首先设计Epoll类,便于后续具体操作,具体内容如下:
其中使用到的tcp_socket.hpp头文件内容在Linux常见IO模型-2中进行过编写和展示,并且其中的客户端代码不变,对服务端代码进行修改如下:
最后编译并执行,得到结果如下:
2.3触发方式
2.3.1水平触发
水平触发是只要存在事件满足对应触发条件,则会触发对应的事件。
- 可读:缓冲区中数据大小大于高水位标记(默认1字节),就会触发可读事件;
- 可写:缓冲区中剩余空间大小大于高水位标记,则会触发可写事件。
2.3.2边缘触发
边缘触发是尽量让用户在一次事件触发中,将所有能够的数据全部都处理完毕,尽量减少事件触发次数,减少运行态切换次数。
- 可读:每当套接字有新的数据到来的时候,则会触发一次事件;
- 可写:缓冲区剩余空间从无到有的时候,才会触发一次事件。