epoll笔记
基础概念
epoll - I/O event notification facility,一种I/O事件通知机制
I/O事件
我们知道,内核有缓冲区。假设有两个进程A,B,进程B想读进程A写入的东西(即进程A做写操作,B做读操作)。进程A需要先写入到内核缓冲区中,然后B从内核缓冲区中读取,如图:
进程B会监听内核缓冲区的变化。
I/O事件的阻塞与同步
- 当内核缓冲区为空的时候,进程B会阻塞住
- 当A往内核缓冲区写入时,内核缓冲区就不是空状态了,这时候就会唤醒进程B
- 如果缓冲区满了,但是进程B没有被唤醒,就会通知进程A,告诉A不要再写入数据了,也就是进程A被阻塞
- 当进程B被唤醒后,B就从缓冲区读取数据,由于B在读数据,缓冲区就不会是满的状态了,这时候就会通知A继续写数据,也就是进程A被唤醒
- 如果进程A还没有唤醒,而缓冲区被B读完了(缓冲区为空),这时候就会阻塞进程B
阻塞I/O的缺点
在阻塞I/O情况下,一个线程只能处理一个流的I/O事件。也就是说,如果想处理多个流的I/O事件,就必须使用多进程或者多线程——效率低
非阻塞I/O
非阻塞I/O涉及事件通知机制及轮询机制。
- 通知机制——就是当事件发生的时候,去通知他
- 轮询机制——通知机制的反面,工作过程类似枚举,效率很低
最开始能想到的就是用轮询的方法:依次询问每个流,如果缓冲区不为空,就进行操作;否则,询问下一个流
但是这种方法效率很低,会白白浪费掉CPU资源。于是便引入了代理——poll
poll
poll代理可以同时观察很多I/O流事件,在空闲的时候(即没有I/O事件的时候),会阻塞当前线程;当有I/O事件的时候,会被唤醒,然后把所有流轮询一遍。这样就能通过减少盲目的轮询来减少对CPU资源的浪费。但是,缺点也很明显:由于每次唤醒都需要把所有流都轮询一遍,当流很多的时候,轮询的时间会很长
poll进化版——epoll
epoll是基于事件的轮询,它会记录是哪个流产生了I/O事件,然后针对这个流来进行操作,大大降低了复杂度。
水平触发与边缘触发
水平触发(level-trggered)
- 只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,
- 当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知
边缘触发(edge-triggered)
- 当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知,
- 当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知
两者的区别?
水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次,举个例子:
1. 读缓冲区刚开始是空的
2. 读缓冲区写入2KB数据
3. 水平触发和边缘触发模式此时都会发出可读信号
4. 收到信号通知后,读取了1kb的数据,读缓冲区还剩余1KB数据
5. 水平触发会再次进行通知,而边缘触发不会再进行通知
所以边缘触发需要一次性的把缓冲区的数据读完为止,也就是一直读,直到读到EGAIN(EGAIN说明缓冲区已经空了)为止,因为这一点,边缘触发需要设置文件句柄为非阻塞。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
An application that employs the EPOLLET flag should use nonblocking file descriptors to avoid having a blocking read or write starve a task that is handling multiple file descriptors. The suggested way to use epoll as an edge-triggered (EPOLLET) interface is as follows:
- with nonblocking file descriptors; and
- by waiting for an event only after read(2) or write(2) return EAGAIN.
//水平触发
ret = read(fd, buf, sizeof(buf));
//边缘触发
while(true) {
ret = read(fd, buf, sizeof(buf);
if (ret == EAGAIN) break;
}
Linux中的EAGAIN含义
Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。
例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
epoll接口介绍
epoll_create
创建epoll实例,会创建所需要的红黑树,以及就绪链表,以及代表epoll实例的文件句柄。
//epoll_create, epoll_create1 - open an epoll file descriptor
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
更多参考EPOLL_CREATE
epoll_ctl
添加,修改,或者删除注册到epoll实例中的文件描述符上的监控事件。
//epoll_ctl - control interface for an epoll file descriptor
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd: epoll实例file descriptor,epoll_create函数返回值
- op:操作类型:
op | description |
---|---|
EPOLL_CTL_ADD | 添加事件 |
EPOLL_CTL_MOD | 修改事件 |
EPOLL_CTL_DEL | 移除事件 |
- fd: 操作的目标文件描述符
- event: 要操作的事件
// The event argument describes the object linked to the file descriptor fd. The struct epoll_event is defined as:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
更多参考EPOLL_CTL
epoll_wait
等待epoll实例中注册的事件触发。
//epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
- epfd: epoll实例文件描述符
- events: 数组出参,用来记录被触发的events,其大小应该和maxevents一致
- maxevents: 返回的events的最大个数,如果最大个数大于实际触发的个数,则下次epoll_wait的时候仍然可以返回
- timeout: 等待事件,毫秒为单位 -1:无限等待 0:立即返回
更多参考EPOLL_WAIT
代码示例
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock,(struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}
参考文档:
[1]. EPOLL Linux Man官方文档
[2]. epoll事件通知机制详解,水平触发和边沿触发的区别
[4]. epoll内核源码分析
[5]. The C10K problem
[6]. IO多路复用之epoll总结