You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#include<sys/time.h>#include<sys/select.h>intselect(intnfds, fd_set*readfds, fd_set*writefds, fd_set*exceptfds, structtimeval*timeout);
// Retrun number of ready file descriptors, 0 on timeout, or -1 on error.
#include<sys/epoll.h>intepoll_create(intsize);
// Return file descriptor on success, or -1 on error// size 指定想要检查的文件描述符个数,该参数并不是上限,而是告诉内核应该如何为内部数据结构划分初始大小
#include<sys/epoll.h>intepoll_wait(intepfd, structepoll_event*evlist, intmaxevents, inttimeout);
// Return number of ready file descriptor, 0 on timeout, or -1 on error
#include<sys/epoll.h>#include<fcntl.h>#include"tlpi_hdr.h"#defineMAX_BUF 1000 /* Maximum bytes fetched by a single read() */
#defineMAX_EVENTS 5 /* Maximum number of events to be returned from a signle epoll_wait() call */
inttestEpoll(intargc, char*argv[])
{
intepfd, ready, fd, s, j, numOpenFds;
structepoll_eventev;
structepoll_eventevlist[MAX_EVENTS];
charbuf[MAX_BUF];
if (argc<2||strcmp(argv[1], "--help") ==0)
usageErr("%s file ...\n", argv[0]);
// 创建一个 epoll 实例epfd=epoll_create(argc-1);
if (epfd==-1)
errExit("epoll_create");
// 打开由命令行参数指定的每个文件,以此作为输入for (intj=1; j<argc; j++) {
fd=open(argv[j], O_RDONLY);
if (fd==-1)
errExit("open");
printf("Opened \"%s\" on fd %d\n", argv[j], fd);
ev.events=EPOLLIN; /* Only interested in input events */ev.data.fd=fd;
// 将得到的文件描述符添加到 epoll 实例的兴趣列表中if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) ==-1)
errExit("epoll_ctl");
}
numOpenFds=argc-1;
// 执行一个循环while (numOpenFds>0) {
/* Fetch up to MAX_EVENTS items from the ready list */printf("About to epoll_wait()\n");
// 循环中调用 epoll_wait() 来检查 epoll 实例的兴趣列表中的文件描述符,// 并处理每个调用返回的事件ready=epoll_wait(epfd, evlist, MAX_EVENTS, -1);
if (ready==-1) {
// 被信号打断处理if (errno==EINTR)
continue;
elseerrExit("epoll_wait");
}
printf("Ready: %d\n", ready);
/* Deal with returned list of events */// 如果 epoll_wait() 调用成功,程序就再执行一个内层循环检查 evlist// 中每个已就绪的元素。for (intj=0; j<ready; j++) {
printf(" fd=%d; events: %s%s%s\n", evlist[j].data.fd,
// 对于 evlist 中的每个元素,程序不只是检查 events 字段中的 EPOLLIN 标记
(evlist[j].events&EPOLLIN) ? "EPOLLIN" : "",
// EPOLLHUP、EPOLLERR 也要检查
(evlist[j].events&EPOLLHUP) ? "EPOLLHUP" : "",
(evlist[j].events&EPOLLERR) ? "EPOLLERR" : "");
if (evlist[j].events&EPOLLIN) {
s=read(evlist[j].data.fd, buf, MAX_BUF);
if (s==-1)
errExit("read");
printf(" read %d bytes: %.*s\n", s, s, buf);
} elseif (evlist[j].events& (EPOLLHUP | EPOLLERR)) {
/* If EPOLLIN and EPOLLHUP were both set, then there might * be more than MAX_BUF bytes to read. Therefore, we close * the file descriptor only if EPOLLIN was not set. * We'll read further bytes after the next epoll_wait(). */// 当所有打开的文件描述符都关闭后,循环终止printf(" closing fd %d\n", evlist[j].data.fd);
if (close(evlist[j].data.fd) ==-1)
errExit("close");
numOpenFds--;
}
}
}
printf("All file descriptors closed; bye\n");
exit(EXIT_SUCCESS);
}
整体概览
实际上 I/O 多路复用,信号驱动 I/O 以及 epool 都是用来实现同一个目标的技术——同时检查多个文件描述符,看它们是否准备好了执行 I/O 操作(准确地说,是看 I/O 系统调用是否可以非阻塞地执行)。文件描述符就绪状态的转化是通过一些 I/O 事件来触发的,比如输入数据到达,套接字连接建立完成,或者是之前满载的套接字发送缓冲区在 TCP 将队列中的数据传送到对端之后由了剩余空间。同时检查多个文件描述符在类似网络服务器的应用中很有用处,或者是那么必须同事检查终端以及管道或套接字输入的应用程序。
需要注意的是这些技术都不会执行实际的 I/O 操作。它们只是告诉我们某个文件描述符已经处于就绪状态了,这时需要调用其他的系统调用来完成实际的 I/O 操作。
水平触发和边缘触发
当采用水平触发通知时,我们可以在任意时刻检查文件描述符的就绪状态。这表示当我们确定了文件描述符处于就绪态时(比如存在输入数据),就可以对其执行一些 I/O 操作,然后重复检查文件描述符,看看是否仍然处于就绪态(比如还有更多的输入数据),此时我们就能执行更多的 I/O,以此类推。换句话说,由于水平触发模式允许我们在任意时刻重复检查 I/O 状态,没有必要每次当文件描述符就绪后需要尽可能多地执行 I/O (也就是尽可能多地读取字节,亦或是根本不去执行任何 I/O)。
与此相反的是,当我们采用边缘触发时,只有当 I/O 事件发生时我们才会收到通知。在另一个 I/O 事件到来前我们不会收到任何新的通知。另外,当文件描述符收到 I/O 事件通知时,通常我们并不知道要处理多少 I/O(例如有多少字节可读)。因此,采用边缘触发通知的程序通常要按照如下规则来设计。
I/O 多路复用
select() 系统调用
系统调用
select()
会一直阻塞,直到一个或多个文件描述符集合成为就绪态。参数
readfds
,writefds
以及exceptfds
都是指向文件描述符集合的指针,所指向的数据类型是 fd_set,。这些参数按照如下方式使用。readfds
是用来检测输入是否就绪的文件描述符集合writefds
是用来检测输出是否就绪的文件描述符集合exceotfds
是用来检测异常情况是否发生的文件描述符集合在 Linux 上,一个异常情况只有下面两种情况下发生:
通常,数据类型
fd_set
以位掩码的形式来实现。但是是由下面四个宏来完成文件描述符集合有一个最大容量限制,由常量 FD_SETSIZE 来决定。在 Linux 上,该常量的值为1024。
参数
readfds
、writefds
和exceptfds
所指向的结构体都是保存结果值的地方。在调用select()
之前,这些参数指向的结构体必须初始化(通过FD_ZERO() 和 FD_SET()
),以包含我们感兴趣的文件描述符集合。之后select()
调用会修改这些结构体,当select()
返回时,它们包含的就是已处于就绪态的文件描述符集合了(值-结果参数)。(由于这些结构体会在调用中被修改,如果要在循环中反复调用select()
,我们必须保证每次都要重新初始化它们。)之后这些结构体可以通过FD_ISSET()
来检查。timeout 参数
参数
timeout
控制着select()
的阻塞行为。该参数可指定为NULL
,此时select()
会一直阻塞。又或者是指向一个timeval
结构体。如果结构体
timeval
的两个域都为0的话,此时select()
不会阻塞,它只是简单地轮询指定的文件描述符集合,看看其中是否有就绪的文件描述符并立即返回。否则,timeout
将为select()
指定一个等待时间的上限值。当
timeout
设为NULL
,或其指向的结构体字段非零时,select()
将阻塞直到有下列事件发生:readfds
、writefds
或exceptfds
中指定的文件描述符中至少有一个成为就绪态;timeout
中指定的时间上限已超时select()
返回所在3个集合中被标记为就绪态的文件描述符总数。如果返回 -1 则是错误发生,包括EBADF
和EINTR
。如果返回 0 则说明超时。示例程序
poll()
系统调用系统调用
poll()
执行的任务同select()
很相似。两者间主要的区别在于我们要如何制定待检查的文件描述符。在select()
中,我们提供三个集合,在每个集合中标明我们感兴趣的文件描述符。而在poll()
中我们提供一列文件描述符,并在每个文件描述符上标明我们感兴趣的事件。参数
fds
列出了我们需要poll()
来检查的文件描述符。该参数为pollfd
结构体数组,其定义如下。pollfd
结构体中的events
和revents
字段都是位掩码。调用者初始化events
来指定需要为描述符fd
做检查的事件。当poll()
返回时,revents
被设定以此来表示该文件描述符上实际发生的事件。输入事件相关位掩码
输出事件相关位掩码
返回有关文件描述符附加信息的位掩码
timeout 参数
返回值
示例程序
文件描述符何时就绪
select()
和poll()
只会告诉我们 I/O 操作是否会阻塞,而不是告诉我们到底能否成功传输数据。普通文件
代表普通文件的文件描述符总是被
select()
标记为可读和可写。对于poll()
来说,则会在revents
字段返回 POLLIN 和 POLLOUT 标志。原因如下:read()
总是会立刻返回数据、文件结尾符或者错误write()
总是会立刻传输数据或者因出现某些错误而失败终端和伪终端
在终端和伪终端上
select()
和poll()
所代表的含义select()
poll()
close()
后管道和 FIFO
select()
和poll()
在管道或 FIFO 读端上的通知select()
poll()
select()
和poll()
在管道或 FIFO 写端上的通知select()
poll()
套接字
select()
poll()
shutdown(SHUT_WR)
比较
select()
和poll()
实现细节
在 Linux 内核层面,
select()
和poll()
都使用了相同的内核poll
例程集合。这些例程有别于系统调用poll()
本身。每个例程都返回有关单个文件描述符就绪的信息。这个就绪信息以位掩码的形式返回,其值同poll()
系统调用中返回的revents
字段中的比特值相关。poll()
系统调用的实现包括为每个文件描述符调用内核poll
例程,并将结果信息填到对应的revents
字段中去。API 之间的区别
select()
有文件描述符上限select()
的参数fd_set
同事也是保存调用结果的地方,如果要在循环中重复调用select()
的话,我们必须每次都要重新初始化fd_set
。而poll()
通过独立的两个字段events
(针对输入)和revents
(针对输出)来处理,从而避免每次都要重新初始化参数。select()
提供的超时精度比较高revents
字段中设定POLLNVAL
标记,poll()
会准确高数我们是哪一个文件描述符关闭了。与之相反,select()
只会返回 -1,并设错误码为EBADF
。通过在描述符上执行 I/O 系统调用并检查错误码,让我们自己来判断哪个文件描述符关闭了。性能
当满足如下两条中任意一条时,
poll()
和select()
将具有相似的性能表现。然而,如果被检查的文件描述符集合很稀疏的话,
select()
和poll()
的性能差异将变得非常明显,在这种情况下,后者更优。select()
和poll()
存在的问题当检查大量的文件描述符时,这两个 API 都会遇到一些问题
select()
或poll()
,内核都必须检查所有被指定的文件描述符,看它们是否处于就绪态。当检查大量处于密集范围,该挫折耗费时间将大大超过接下来的操作。select()
或poll()
,程序都必须传递一个表示所有需要被检查的文件描述符的数据结构到内核,内核检查过描述符后,修改这个数据结构并返回给程序。(此外,对于select()
来说,我们还必须在每次调用前初始化这个数据结构。)对于poll()
来说,随着待检查的文件描述符数量的增加,传递给内核的数据结构大小也会随之增加。当检查大量文件描述符时,从用户控件到内核控件来回拷贝这个数据结构将占用大量的 CPU 时间。对于select()
来说,这个数据结构的大小固定为FD_SETSIZE
,与待检查的文件描述符数量无关。select()
或poll()
调用完成后,程序必须检查返回的数据结构中的每个元素,以此查明哪个文件描述符处于就绪态了。信号驱动 IO
在信号驱动 I/O 中,当文件描述符上可执行 I/O 操作时,进程请求内核为自己发送一个信号。之后进程就可以执行任何其他的任务知道 I/O 就绪为止,此时内核会发送信号给进程。要使用信号驱动 I/O,程序需要按照如下步骤来执行。
为内核发送的通知信号安装一个信号处理例程。默认情况下,这个通知信号为
SIGIO
。设定文件描述符的属主,也就是当文件描述符上可执行 I/O 时会接收到通知信号的进程或进程组。通常我们让调用进程成为属主。设定属主可通过
fcntl()
的F_SETOWN
操作来完成:fcntl(fd, F_SETOWN, pid);
通过设定
O_NONBLOCK
标志使能非阻塞 I/O。通过打开
O_ASYNC
标志使能信号驱动 I/O。这可以和上一步合并为一个操作,因为它们都需要用到fcntl()
的F_SETFL
操作。调用进程现在可以执行其他的任务了。当 I/O 操作就绪时,内核为进程发送一个信号,然后调用在第 1 步中安装好的信号处理例程。
信号驱动 I/O 提供的是边缘触发通知。这表示一旦进程被通知 I/O 就绪,它就应该尽可能多地执行 I/O (例如尽可能多地读取字节)。假设文件描述符是非阻塞式的,这表示需要在循环中执行 I/O 系统调用直到失败为止,此时错误码
EAGAIN
或EWOULDBLOCK
。示例程序
何时发送『I/O 就绪』信号
终端和伪终端
当产生新的输入时
管道和 FIFO
套接字
connect()
请求完成,也就是 TCP 连接的主动端进入ESTABLISHED
状态。showdown()
关闭了写连接(半关闭),或者通过close()
完全关闭inotify 文件描述符
当 inotify 文件描述符称为可读状态时会产生一个信号——也就是由 inotify 文件描述符监视的其中一个文件上有事件发生时。
epoll 编程接口
epoll
API 的主要优点如下:epoll
的性能延展性比select()
和poll()
高很多。epoll
API 既支持水平触发也支持边缘触发。与之相反,select()
和poll()
只支持水平触发,而信号驱动 I/O 只支持边缘触发。epoll
API 的核心数据结构称作epoll
实例,它和一个打开的文件描述符相关联。这个文件描述符不是用来做 I/O 操作的,相反,它是内核数据结构的句柄,这些内核数据结构实现了两个目的。ready list 中的成员是 interest list 的子集。
epoll
API 由以下 3 个系统调用组成:epoll_create()
创建一个epoll
实例,返回代表该实例的文件描述符。epoll_ctl()
操作同epoll
实例相关联的兴趣列表。通过epoll_ctl()
,我们可以增加新的描述符到列表中,将已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的位掩码。epoll_wait()
返回与epoll
实例相关联的就绪列表中的成员。创建
epoll
实例:epoll_create()
系统调用
epoll_create()
创建了一个新的epoll
实例,其对应的兴趣列表初始化为空。作为函数返回值,
epoll_create()
返回了代表新创建的epoll()
实例的文件描述符。这个文件描述符在其他几个epoll
系统调用中用来表示epoll
实例。当这个文件描述符不再需要时,应该通过close()
来关闭。当所有与epoll
实例相关的文件描述符都背关闭时,实例被销毁,相关的资源都返还给系统。修改
epoll
的兴趣列表:epoll_ctl()
fd
指定要修改的文件描述符,它甚至可以是另一个epoll
实例的文件描述符,但是,这里fd
不能作为普通文件或目录的文件描述符op
指定需要执行的操作:EPOLL_CTL_ADD
,加入兴趣列表中EPOLL_CTL_MOD
,修改 fd 上设定的时间,需要用到由 ev 所指向的结构体中的信息EPOLL_CTL_DEL
,移除兴趣列表ev
是指向结构体epoll_even
的指针,结构体定义如下:其中 data 的字段如下
参数
ev
为文件描述符fd
所做的设置如下:epoll_event
中的events
字段是一个位掩码,它指定了我们为待检查的描述符fd
上感兴趣的事件集合。data
字段是一个联合体,当描述符fd
稍后称为就绪态时,联合体的成员可用来指定传回给调用进程的信息。使用
epoll_create()
和epoll_ctl()
事件等待:
epoll_wait()
系统调用
epoll_wait()
返回epoll
实例中处于就绪态的文件描述符信息。单个epoll_wait()
调用能返回多个就绪态文件描述符的信息。参数
evlist
所指向的结构体数组重返回的是有关就绪态文件描述符的信息。数组evlist
的空间由调用者负责申请,所包含的袁术个数在参数maxevents
中指定。在数组
evlist
中,每个元素返回的都是单个就绪态文件描述符的信息。events
字段返回了在该描述符上一届发生的事件掩码。data
字段返回的是我们在描述符下使用epoll_ctl
注册感兴趣的事件时在ev.data
中所指定的值。注意,data
字段是唯一一个可获知同这个事件相关的文件描述符号的途径。因此,当我面调用epoll_ctl()
将文件描述符添加到兴趣列表中时,应该要么将ev.data.fd
设为文件描述符号,要么将ev.data.ptr
设为指定包含文件描述符号的结构体。参数
timeout
用来确定epoll_wait()
的阻塞行为,有如下几种:epoll
事件当我们调用
epoll_ctl()
时可以在ev.events
中指定的位掩码以及由epoll_wait()
返回的evlist[].events
中的值在下表给出:epoll
中events
字段的位掩码值epoll_ctl()
的输入?epoll_wait()
返回EPOLLONESHOT
标志默认情况下,一旦通过
epoll_ctl()
的EPOLL_CTL_ADD
操作将文件描述符添加到epoll
实例的兴趣列表中后,它会保持激活状态(即,之后对epoll_wait()
的调用会在描述符处于就绪态时通知我们) 直到我们显示地通知epoll_ctl()
的EPOLL_CTL_DEL
操作将其从列表中移除。如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给epoll_ctl()
的ev.events
中指定EPOLLONESHOT
标志。如果指定了这个标志,那么在下一个epoll_wait()
调用通知我们对应的文件描述符处于就绪态之后,这个描述符就会在兴趣列表中被标记为非激活态,之后的epoll_wait()
调用都不会再通知我们有关这个描述符的状态了。如果需要,我们可以稍后通过epoll_ctl()
的EPOLL_CTL_MOD
操作重新激活对这个文件描述符的检查。示例程序
深入探究
epoll
的语义当我面通过
epoll_create()
创建一个epoll
实例时,内核在内存中创建了一个新的i-node
并打开文件描述(文件描述符表示的是一个打开文件的上下文信息(大小、内容、编码等与文件有关的信息),这部分实际是由内核来维护的。),随后在调用进程中为打开的这个文件描述分配一个新的文件描述符。同epoll
实例的兴趣列表相关联的是打开的文件描述,而不是epoll
文件描述符。这将产生下列结果:dup()
(或类似的函数)复制一个epoll
文件描述符,那么被复制的描述符所指代的epoll
兴趣列表和就绪列表同原始的epoll
文件描述符相同。若要修改兴趣列表,在epoll_ctl()
的参数epfd
上设定文件描述符可以是原始的也可以是复制的。fork()
调用之后的情况。此时子进程通过继承复制了父进程的epoll
文件描述符,而这个复制的文件描述符所指向的epoll
数据结果同原始的描述符相同。当我们执行
epoll_ctl()
的EPOLL_CTL_ADD
操作时,内核在epoll
兴趣列表中添加了一个元素,这个元素同时记录了需要检查的文件描述符数量以及对应的打开文件描述的引用。epoll_wait()
调用的目的就是让内核负责监视打开的文件描述符。这表示我们必须对之前的观点做改进:如果一个文件描述符是epoll
兴趣列表中的成员,当关闭它后会自动从列表中删除。改进版应该是这样的:一旦所有指向打开文件描述的文件描述符都被关闭后,这个打开的文件描述符将从epoll
的兴趣列表中移除。这表示如果我们通过dup()
(或类似的函数)或者fork()
为打开的文件创建了描述符副本,那么这个打开的文件只会在原始的描述符以及所有其他的副本都被关闭时才会移除。epoll
同 I/O 多路复用的性能对比poll()
、select()
以及epoll
进行 100000 次监视操作所花费的时间poll()
所占用的 CPU 时间(秒)select()
所占用的 CPU 时间(秒)epoll
所占用的 CPU 时间(秒)为什么:
select()
和poll()
时,内核必须检查所有在调用中指定的文件描述符。与之相反,当通过epoll_ctl()
指定了需要监视的文件描述符时,内核会在与打开的文件描述上下文相关联的列表中记录该描述符。之后每当执行 I/O 操作使得文件描述符成为就绪态时,内核就在epoll
描述符的就绪列表中添加一个元素。(单个打开的文件描述上下文中一次 I/O 事件可能导致与之相关的多个文件描述符成为就绪态。)之后的epoll_wait()
调用从就绪列表中简单地取出这些元素。select()
或poll()
时,我传递一个标记了所有待监视的文件描述符的数据结构给内核,调用返回时,内核将所有标记为就绪态的文件描述符的数据结构再传回给我们。与之相反,在epoll
中我们使用epoll_ctl()
在内核控件中建立一个数据结构,该数据结构会将待监视的文件描述符都记录下来。一旦这个数据结构建立完成,稍后每次调用epoll_wait()
时就不需要再传递任何与文件描述符有关的信息给内核了,而调用返回的信息中只包含那些已经处于就绪态的描述符。边缘触发通知
epoll
API 还能以边缘触发方式进行通知——也就是说,会告诉我们自从上一次调用epoll_wait()
以来文件描述符上是否已经有 I/O 活动了(或者由于描述符被打开了,如果之前没有调用的话)。使用epoll
的边缘触发通知在语义上类似于信号驱动 I/O ,只是如果有多个 I/O 事件发生的话,epoll
会将它们合并成一次单独的通知,通过epoll_wait()
返回,而再信号驱动 I/O 中则可能会产生多个信号。采用
epoll
的边缘触发通知机制的程序基本框架如下:epoll_ctl()
构建epoll
的兴趣列表。epoll_wait()
取得处于就绪态的描述符列表read(), write(), recv(), send()
或accept()
)返回EAGAIN
或EWOULDBLOCK
错误。当采用边缘触发通知时避免出现文件描述符饥饿现象
假设其中一个就绪态文件描述符又大量输入,如果使用非阻塞式读操作将所有输入都读取,那么此时就会有使其他的文件描述符处于饥饿状态的风险存在(即,在我们再次检查这些文件描述符是否处于就绪态并执行 I/O 操作前会有很长的一段处理时间)。该问题的一个解决方案是让应用程序维护一个列表,列表中存放着已经被通知为就绪态的文件描述符。通过一个循环按照如下方式不断处理。
epoll_wait()
监视文件描述符,并将处于就绪态的描述符添加到应用程序维护的列表中。如果这个文件描述符已经注册到应用程序维护的列表中了,那么这次监视操作的超时时间应该设为较小的值或者是0。这样如果没有新的文件描述符成为就绪态,应用程序就可以迅速进行到下一步,去处理那些已经处于就绪态的文件描述符了。epoll_wait()
调用后从列表头开始处理 )。当相关的非阻塞 I/O 系统调用出现EAGAIN
或EWOULDBLOCK
错误时,文件描述符就可以在应用程序维护的列表中移除了。参考资料
《Linux/Unix 系统编程手册》
The text was updated successfully, but these errors were encountered: