epoll 相关问题简单说明

塞、非阻塞、异步 I/O

阻塞:

read 为例,当 fd 中无数据,read 将进行等待,直到有数据为止,否则 read 将用于阻塞。

“低速”系统调用。

阻塞:

fd 中无数据可读,那么 read 不进行等待而直接返回,并且返回相应错误码(EAGAIN)

此时,怎么办呢?调用者可以以轮询的方式进行查看。如果不想轮询呢?系统提供了一套机制,帮助我们进行“轮询查看”工作,基于Linux 系统, epoll 便承担着这样的角色。

异步:

异步,基于 epoll,当某个 fd (网络fd)可读时,便进行相应的读操作,当 fd 可写时,便进行相应的写操作,而不是传统的 server read request, handle request and then write response. 传统的 C 语言编程常规实现而言,就是注册“监听”某个 fd 的事件,针对不同事件挂载不同 callback 即可。对于面向对象语言,搭建框架的时候,实现框架提供的对应的接口即可。

基于 Linux 平台,针对网络 fd 提供了 epoll 进行 异步的处理。而针对文件I/O,提供了 aio 机制。

多路复用 I/O multiplexing:

Linux epoll 内部维护了一张文件表,用户往其中添加我们感兴趣的fd。并且提供了一个函数,可以阻塞调用者,当我们感兴趣的fd有我们感兴趣的事件时,我们该函数便返回一些必要的信息。

epoll

哎,比较惨,《UNIX 环境高级编程里面高级I/O部分,里面仍旧只有 select poll,然后有没有别的相关的书。所以,首先只能借助于那个“男人”了。

The  epoll  API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them.  The epoll API can be used either as an edge-triggered or a level-triggered interface and scales well  to  large numbers of watched file descriptors.

1. epoll 可同时监控多个 fd

2. epoll 提供了两种模式 ET LT

3. epoll 监控的 fd 数量 可以是 large numbers

接口

epoll_create(2) creates  a  new  epoll  instance and returns a file descriptor referring to that instance.  (The more recent epoll_create1(2) extends the functionality of epoll_create(2).)

epoll_create 创建一个 epoll fd.

Interest in particular file descriptors is then registered via epoll_ctl(2).  The set of  file  descriptors  currently registered on an epoll instance is sometimes called an epoll set.

通过 epoll_ctl 接口,我们可以将感兴趣的 fd 注册到 epoll_create 返回的 epoll fd 中。

epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.

阻塞等待 I/O 事件,没有事件被触发就会被阻塞。

epoll 模式

Level-triggered and edge-triggered

The  epoll  event  distribution interface is able to behave both as edge-triggered (ET) and as level-triggered (LT). The difference between the two mechanisms can be described as follows.  Suppose that this scenario happens:

1. The file descriptor that represents the read side of a pipe (rfd) is registered on the epoll instance.

2. A pipe writer writes 2 kB of data on the write side of the pipe.

3. A call to epoll_wait(2) is done that will return rfd as a ready file descriptor.

4. The pipe reader reads 1 kB of data from rfd.

5. A call to epoll_wait(2) is done.

If the rfd file descriptor has been added to the epoll interface using the EPOLLET (edge-triggered)  flag,  the  call  to       epoll_wait(2)  done in step 5 will probably hang despite the available data still present in the file input buffer; mean‐       while the remote peer might be expecting a response based on the data it already sent.

The reason for this is that edge-triggered  mode delivers events only when changes occur on the monitored file descriptor.

So, in step 5 the caller might end up waiting for some data that is already present inside the input buffer.

In the above example, an event on rfd will be  generated because of the write done in 2 and the event is consumed in 3.

Since the read operation done in 4 does not consume the whole buffer data, the call to epoll_wait(2) done in step 5 might block indefinitely.

LT ET 模式

假设一个场景:

1. 读端仅监控一个 fd EPOLLIN 事件

2. 当写端 写入了 2kB 的数据

3. 读端 epoll_wait 调用结束并返回该 fd 可读状态

4. 读端读取了 1kB 的数据

5. 读端继续调用 epoll_wait

如果是 ET 模式:

读端第 5 步的 epoll_wait 调用会 hang 住,即使监控的 fd 仍旧有数据可读。然而此时远端即写端,由于发送了一些数据,很可能是一个完整的请求,此时的远端即写端不继续写数据,而是等待当前我们读端的回复。由于 ET 模式下,EPOLLIN 事件只会在“新”的数据到来那一刻才会触发一次,而我们在这一次触发过程中,只读取了部分数据,读端暂且无法根据这部分数据进行处理,也就无法针对该请求进行回复。

那么就造成了远端即写端收不到回复,陷入等待,而不会继续写数据,而读端读取部分数据而没有继续读数据,无法进行回复,进入了 epoll_wait 调用,此时这个 epoll_wait 调用将会被无限期的 block

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:

i with nonblocking file descriptors; and

ii  by waiting for an event only after read(2) or write(2) return EAGAIN.

个应用程序如果使用了 ET 模式,应该使用非阻塞的fd,避免在阻塞的 read 或者 write 时,造成一个数据消费任务被“饿死”。

所以 ET 模式的用法要点:

1. 使用 非阻塞的 fd

2. read 或者 write 操作返回 EAGAIN 时,才进入 epoll_wait 的调用

PS:

nonblock fd read 返回 EAGAIN:表示缓冲区数据被读完(不会遗留数据)

nonblock fd write 返回 EAGAIN:表示缓冲区被写满(待数据发送出去之后,缓冲重新进入可写状态,会触发 EPOLLOUT 事件,不会造成 epoll_wait hang)

epoll 默认采用 LT 模式。

LT 模式下,前文所述场景,无需担心 epoll_wait 会被 hang 住,因为只要监控的 fd 缓存中仍旧有数据可读,那么 epoll_wait 就会返回,触发 EPOLLIN 事件。

Since even with edge-triggered epoll, multiple events can be generated upon receipt of multiple chunks of data, the call‐er has the option to specify the EPOLLONESHOT flag, to tell epoll to disable the associated  file  descriptor  after  the receipt  of  an  event with epoll_wait(2).  When the EPOLLONESHOT flag is specified, it is the caller's responsibility to rearm the file descriptor using epoll_ctl(2) with EPOLL_CTL_MOD.

即便使用 ET 模式,多路复用时,分段数据的提取,都会导致 epoll 事件的重复触发,(比如发送端发送2kB,读取1kB,发送端继续发送数据2kB,此时缓冲区遗留了3kB数据,而调用者是知道缓冲区数据没有被读完的,而即便 ET 模式下,EPOLLIN 事件依旧会被触发两次,可能没有必要)当这种情况不是我们所需要的时候,我们可以设置 EPOLLONESHOT flag,同一个 fd 的同一个事件只触发一次,后续不再继续触发,此时需要调用者自己去调用 epoll_ctl 接口接口 EPOLL_CTL_MOD 去设置继续监听事件。

man 手册中关于 epoll 的问答

Q0  What is the key used to distinguish the file descriptors registered in an epoll set?

A0  The  key  is the combination of the file descriptor number and the open file description (also known as an "open file handle", the kernel's internal representation of an open file).

这个问题,就是表示 epoll 监控的是文件 fd.

Q1  What happens if you register the same file descriptor on an epoll instance twice?

A1  You will probably get EEXIST.  However, it is possible to add a duplicate (dup(2), dup2(2),  fcntl(2)  F_DUPFD)  file descriptor  to  the  same epoll instance.  This can be a useful technique for filtering events, if the duplicate file descriptors are registered with different events masks.

重复添加同一个 fd 会怎么样?返回 EEXIST 错误

如果 fd_dup 是 从 fd dup过来的,那么可以重复添加吗?

可以,并且,推荐用法是:fd fd_dup 分别注册不同的事件(使用不同的 events masks)

Q2  Can two epoll instances wait for the same file descriptor?  If so, are events reported to both  epoll  file  descriptors?

A2  Yes, and events would be reported to both.  However, careful programming may be needed to do this correctly.

如果两个 epoll fd,可以将同一个 fd 添加入两个 epoll 中吗?可以,并且,同一个事件会被分别触发,但是,要小心操作。

Q3  Is the epoll file descriptor itself poll/epoll/selectable?

A3  Yes.  If an epoll file descriptor has events waiting, then it will indicate as being readable. 

epoll fd 可以放到另外一个 epoll 里面吗?可以,并且,当epoll fd内部有事件被触发,那么第二个epoll会返回第一个 epoll fd 的 EPOLLIN 事件。

Q4  What happens if one attempts to put an epoll file descriptor into its own file descriptor set?      

A4  The  epoll_ctl(2)  call  fails  (EINVAL).   However,  you  can add an epoll file descriptor inside another epoll file descriptor set.

如果把 epoll fd 添加入自己epoll 中,添加接口 epoll_ctl 会直接返回错误,错误码 EINVAL

Q5  Can I send an epoll file descriptor over a UNIX domain socket to another process?

A5  Yes, but it does not make sense to do this, since the receiving process would not have copies of the file descriptors in the epoll set.

是否可以把 UNIX 域的 socket 添加入 epoll 呢?可以,但是这样做没有意义,epoll 中不会 copy 这个 socket 到自己的 set 中。

Q6  Will closing a file descriptor cause it to be removed from all epoll sets automatically?

A6  Yes,  but  be  aware  of  the  following  point.  A  file descriptor is a reference to an open file description (see open(2)).  Whenever a file descriptor is duplicated via dup(2), dup2(2), fcntl(2) F_DUPFD, or  fork(2),  a  new  file descriptor referring to the same open file description is created.  An open file description continues to exist until all file descriptors referring to it have been closed.  A file descriptor is removed from an epoll set only after all the  file  descriptors  referring  to  the  underlying  open file description have been closed (or before if the file descriptor is explicitly removed using epoll_ctl(2) EPOLL_CTL_DEL).  This means that even  after  a  file  descriptor that  is part of an epoll set has been closed, events may be reported for that file descriptor if other file descriptors referring to the same underlying file description remain open.

close了的 fd 会从 epoll中被自动删除吗?会!但是fd被dup了之后,dup的没被close,但是fd被close了,fd依旧会被触发。如果fd是被 epoll_ctl +EPOLL_CTL_DEL被删除了,即便被dup了,该fd也不会被触发

Q7  If more than one event occurs between epoll_wait(2) calls, are they combined or reported separately?

A7  They will be combined.

两次epoll_wait 中间触发了多个事件,那么这些事件会被合并。

Q8 Does an operation on a file descriptor affect the already collected but not yet reported events?

A8 You can do two operations on an existing file descriptor.  Remove would be meaningless for this case. Modify will reread available I/O.

(我确认我看懂了英文,但是不明白说的是啥,实验的结果如下:)

如果有一个 fd epoll 收集了其需要触发的事件,但是没有被上报,即我们没有及时调用 epoll_wait 去查看到这个事件,那么我们操作这个 fd 的时候,会不会影响到 epoll 已经收集到的事件呢?

针对一个已经存在的 fd 你有两个操作(我猜是 read write),问题描述的场景中,我们将该 fd epoll 中移除,是没有意义的,因为,我们针对这个 fd 的操作,epoll 会及时捕捉到,并且同步更新该 fd 的读写状态

Q9 Do I need to continuously read/write a file descriptor until EAGAIN when using the EPOLLET flag (edge-triggered behavior) ?

A9  Receiving an event from epoll_wait(2) should suggest to you that such file descriptor is ready for the requested  I/O operation. You must consider it ready until the next (nonblocking) read/write yields EAGAIN.  When and how you will use the file descriptor is entirely up to you. For packet/token-oriented files (e.g., datagram socket, terminal in canonical mode), the only way to detect the end of the read/write I/O space is to continue to read/write until EAGAIN.

For stream-oriented files (e.g., pipe, FIFO, stream socket), the condition that the read/write I/O space is exhausted can also be detected by checking the amount of data read from / written to the target file descriptor.  For  example, if  you  call read(2) by asking to read a certain amount of data and read(2) returns a lower number of bytes, you can be sure of having exhausted the read I/O space for the  file  descriptor.  The same is true when writing using write(2).

(Avoid this latter technique if you cannot guarantee that the monitored file descriptor always refers to a stream-oriented file.)

ET 模式下:针对一个fd, 是否需要一直 read 直到返回 EAGAIN,或者 一直 write 直到返回 EAGAIN呢?

针对数据报形式的fd,如果 updfd,建议去读完(写也是类似,只是write如果数据全发送完了就没有必要EAGAIN)。因为udp报文,不读完,可能拿不到完整的报文。

对于流式的传输,缓冲区内部的状态会被read或者write检测到,通过 read write 的返回值,与实际想要获取的 read 的接收数据的buf大小或者需要 write 的数据量进行对比,即可知道,I/O的当前状态,此时,你没有必要进行下一次的 read 或者 write 尝试,去获取 EAGAIN 错误码才做处理。

(你要小心的是,这个 fd 的传输形式到底是不是数据报形式还是流形式,如果拿不准,那还是一直 read/write 直到获取到 EAGAIN)

流式传输时,在缓冲区临界状态下,最后一笔的可读或可写数据,实际 读或写 的量,都是小于实际期望读取到或者写入的数据量大小的。

epoll ET 模式 遇到大 缓冲区的问题:

1. read:

read 缓冲区很大,而远端一次性发送了很多数据。

当 我们尝试将该缓冲区数据读完,会耗时很久(不读完,是不行的,因为,前文提到 epoll_wait hang 的风险存在,所以此时最好读完)。这个耗时很久的操作,可能就会其他任务的执行。

2. write:

write 此时与 read 类似。write 场景下的技术问题还有很多,后续深入讨论。

使用者,应该记录 fd 的状态(redis就是这么做的),但是会引入一个新的问题,这样的记录,缓存存在延迟,那么有些 fd 已经 close,我们需要及时同步这个操作到我们的状态缓存中。

API

int epoll_create(int size);

int epoll_create1(int flags);

两个接口功能差不多,都是返回一个 epoll 实例

size: 给个大于0的整数即可,可以假模假式的1024,实际这个参数已经 deprecated 了。

flags: 可以给一个参数  EPOLL_CLOEXEC ,具体作用如上图红线中所示。

dup ,主要是被动发生 fd dup 的时候,比如 fork,这些 fd 也会被“文件共享”的方式被共享继承。比如 redis 里面的 rdb 或者 aof rewrite 子进程,子进程启动第一件事儿,就 close 掉了用于监听的 fd

exec 函数族接口,都是另起一个独立进程的,在exec之前,我们也可以手动 close 掉通过 fork 来的 fd

成功返回 epollfd

失败返回 -1,系统 errno 被置为对应错误码:

EINVAL: 表示入参错误

EMFILE: 针对每个用户,可打开的 epollfd 个数有限,达到限制的情况下,返回该错误

ENFILE: 整个系统的 open fd 达到上限

ENOMEM: 系统内存不足

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

epfd: epoll_create 成功的返回值

fd: 需要操作的 fd

op: 操作类型

  EPOLL_CTL_ADD: fd 添加入 epoll

  EPOLL_CTL_MOD: 修改 fd 感兴趣的事件

  EPOLL_CTL_DEL: fd epoll 中移除

event->events:

EPOLLIN: for read

EPOLLOUT: for write

EPOLLRDHUP: (stream socket)远端 close 或者 shutdown write 端口,那么我们监控的 fd 会上报该事件。TCP 全双工,所以可以针对一个 TCP socket 可以单独关闭 读或者写端。

EPOLLPRI: fd 出现内部异常。(比较复杂,没遇到过)

EPOLLERR: fd 出现错误,比如,read 端已经关闭,write 端即收到该事件,表示我们无法继续 write

EPOLLHUP: fd 远端被关闭,跟着 EPOLLIN 一起过来的事件。流式 socket 无需考虑,因为 EPOLLIN 事件中,我们会 read 然后 返回值为 0,即表示对端已经关闭了。

EPOLLET: ET 模式下使用 epoll ,默认采用 LT 模式。

EPOLLONESHOT: 某个关联 fd 的某个事件一次触发,后续需要手动设置 MOD

EPOLLWAKEUP: 太复杂,还没搞明白,等着问高手

EPOLLEXCLUSIVE: 主要是内核为了解决“惊群”问题,引入的。多进程下,fd 如果被共享了,一定要使用该 flag 去避免 epoll 的惊群问题。否则,同一个 fd 的同一个事件,会把多进程的 epoll wait 同时都唤醒。请自行以 epoll 惊群 为关键字网络搜索查看。

 

EPOLLRDHUP:

(stream socket)远端 close 或者 shutdown write 端口,那么我们监控的 fd 会上报该事件。TCP 全双工,所以可以针对一个 TCP socket 可以单独关闭 读或者写端。

经过测试实验,当客户端 close ,如果该 client socket fd 注册了该事件,那么此时,会上报该事件,否则,不会上报该事件。

EPOLLHUP:

Hang up happened on the associated file descriptor.  epoll_wait(2) will always wait for this event; it is not necessary to set it in events.

Note that when reading from a channel such as a pipe or a stream socket, this event merely indicates that the peer closed  its end of the channel.  Subsequent reads from the channel will return 0 (end of file) only after all outstanding data in the channel has been consumed.

无需注册添加的事件,总会被上报。

字:远端 close 了,会上报该事件。但是实际编程测试,并未捕捉到。

网上有人说是自己这一端 close,如果如此,那么问题在于此时该 fd 会被移除出 epoll,也不会上报事件。

经过测试发现,本端 close fd ,不会上报该 fd 的 EPOLLHUP 事件,但是,采用 shutdown(fd, SHUT_WR);即本端关闭该 fd 的写端,即便采用了 ET 模式,依旧会一直触发 EPOLLHUP 事件

另外,单独的客户端 shutdown 读,或者 close ,server端都稳定触发 RDHUP事件。。。。

返回值:成功返回0,失败返回-1

errno

EBADF: epfd 或者 fd 是无效的,比如已经被 close 了。

EEXIST: EPOLL_CTL_ADD 操作时,重复 EPOLL_CTL_ADD 同一个 fd

EINVAL: 主要是针对 events参出错,比如 EPOLL_CTL_MOD 操作下带 EPOLLEXCLUSIVE(flag只能在 ADD 时添加)

ELOOP: 两个 epoll fd 彼此互相添加。

ENOENT: MOD 或者 DEL 没有被添加的 fd

ENOMEM: 内存不足

ENOSPC: 当前用户 ADD 某个 fd 时,该用户的注册到 epoll fd 已经数量已经超标

EPERM: 支持 epoll fd epoll 里面添加,比如 文件 fd

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);

由于 epoll_wait 会阻塞当前调用线程,那么 epoll 提供了 pwait 接口,设置信号屏蔽字,这样即便信号来了,被屏蔽了的信号,会被 PEND

一般的,用 wait 即可,特殊的信号,就交给进行默认或者注册 signal callback 去处理。

maxevents: 当底层被触发的 fd 太多,超过了 maxevents,那么,就会被分批上报,积压的那一批 fd 等着下次 epoll_wait 调用再返回。

events maxevents 是对应的,events 是个向量指针,内部的 event holder size 要大于等于 maxevents

API 其实还有一个 就是 close。

epoll 内部数据结构

seclet 内部其实是一个存储“感兴趣”的fd 的表格,实际是一个向量,而 epoll 内部,有人说是 红黑树,有人说是 hash 表。无论是哪一种,检索速度都相对向量而言都很快,如果是红黑树检索操作时间复杂度为 O(log(N)),所以说,epoll 可以监控 large numbers of fd。

就绪 fd 存储在 一个双向链表里面。epoll_wait 之后,就绪 fd 的状态就会被转储到调用者传入的events 向量中。

编程模型

LT 与 ET

LT 为默认模式,无对应的 flag 去显式设置,与 ET模式的flag 共用一个 bit 位,当不显式的 置 ET 模式,那么就是 LT 模式。

LT 模式下:

只要是 缓冲区中有数据 那么 EPOLLIN 事件将一直被触发。编程层面意义就是只要调用 epoll_wait 就会直接返回,直到缓冲区数据被读完,缓冲区为空,才不再继续触发。

对于 write 事件,一直处于可 write 状态,那么一样也一直会被触发,直到缓冲区被写满。

这种情况下,server 端 accept client socket 之后,不要直接注册 OUT flag

一般场景下,此时应该是 epoll_wait 阻塞等待客户端发送请求,然后触发 EPOLLIN 事件,但是,此时的 client socket fd 是可写状态,server 端没有读取并处理数据,无回复可写,更别说把缓冲区写满,此时的 epoll_wait 调用会不断返回,狂触发 OUT 事件,这是非预期的情况,应当予以避免,所以 LT 模式下,在 accept client socket 的时候,一般此时只注册 IN 事件,而不注册 OUT 事件。当需要 write 的时候,怎么办呢?

write 的返回值小于实际需要发送的数据量,说明缓冲区满,此时,可以 MOD 之前的 fd,添加 OUT 事件的监听。然后,OUT事件被触发的时候,发送完数据,根据需要,再 MOD,将 OUT 事件的监听需求从 epoll 中摘除。

所以 LT 模式下,当 accpet 到 client 端 fd 之后,不要直接注册 OUT 事件,try write 返回 EAGAIN,可以阻塞写,可以监听OUT。

ET 模式下:

缓冲区如果为空,然后此时有客户端写入数据,即缓冲区此时由空变成非空,有数据可读,那么此时会触发一次 IN 事件。不论 server 端此时读取数据与否,客户端一旦往 server 端发送数据,每发送一次,server 端都会触发一次 IN 事件。如果一直没有新数据到来,那么 epoll 后续不会继续触发 IN 事件。

此时:如果server端没有把缓冲区数据读完,而客户端即便是正常断开连接,server 端也会产生 SIGPIPE 信号,并且,会触发 EPOLLHUP 事件

写缓冲区如果为满,然后,客户端读取了一部分数据,由满变成非满,那么此时,会触发一次 OUT 事件。与读不同的是,后续客户端继续读取数据,写缓冲区数据量减少,并不会重复触发 OUT 事件。除非被写满,否则后续将不再“有机会”触发 OUT 事件。

但是,每次 IN 事件到来的时候,会携带 OUT 信息。

ET 模式下,可以无脑去写,报错,错误码 EAGAIN 时,如果没有注册 OUT 事件,此时可以注册,如果注册了,就等下一次触发 OUT事件继续写。

Read 注意,要读完,无论是非流式的 EAGAIN,或者是流式 read 量为 0,close fd。反正就会触发那么一次,捕捉到了,应当及时去处理。

write 的处理,就涉及到不同的编程模型了。

编程模型

1. 回射模型

2. 反应堆模型

这两个不写了,因为CSDN上有个帖子,说得很清楚。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值