I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。
这句话表明,是先有可用的文件描述符,然后才有监视目标文件描述符这么个动作。
监视的目的是看被监视的文件描述符对应的设备是否有数据可以读写,其实主要是读操作。
I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取,又或者用于服务器上读取多个客户端的socket连接操作等等。
可参考这篇文章体会其好处以及实现原理:
9.2 I/O 多路复用:select/poll/epoll | 小林coding (xiaolincoding.com)
select/poll/epoll 是内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件。poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合。
select 函数
select 函数原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
函数参数和返回值含义如下:
nfds:所要监视的这三类文件描述集合中,最大文件描述符加 1。
readfds、writefds 和 exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等,这三个参数都是 fd_set 类型的,fd_set 类型变量的每一个位都代表了一个文件描述符。readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。writefds 和 readfs 类似,只是 writefs 用于监视这些文件是否可以进行写操作。exceptfds 用于监视这些文件的异常。
比如我们现在要从一个设备文件中读取数据,那么就可以定义一个 fd_set 变量,这个变量要传递给参数 readfds。当我们定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:
void FD_ZERO(fd_set *set) void FD_SET(int fd, fd_set *set) void FD_CLR(int fd, fd_set *set) int FD_ISSET(int fd, fd_set *set)
FD_ZERO 用于将 fd_set 变量的所有位都清零,FD_SET 用于将 fd_set 变量的某个位置 1,也就是向 fd_set 添加一个文件描述符,参数 fd 就是要加入的文件描述符。FD_CLR 用于将 fd_set变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符。FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符。
timeout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 timeval 表示,结构体定义如下所示:
struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微妙 */ };
当 timeout 为 NULL 的时候就表示无限期的等待。
返回值:0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作;-1,发生错误;其他值,可以进行操作的文件描述符个数。
使用 select 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
poll函数
我们先来看看poll函数。
poll()函数原型如下所示:
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数参数含义如下:
fds:
指向一个 struct pollfd 类型的数组(注意,是结构体数组,就是用来放需要监视的多个fd和其对应的事件),数组中的每个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件,稍后介绍 struct pollfd 结构体类型。 注意,都是把已经准备好的文件描述符放到epoll里面来监视,对于socket来说,是把已经建立连接的socket放进该数组中,也就是说,数组里的都是要监视的目标文件描述符。
nfds:
参数 nfds 指定了 fds 数组中的元素个数,数据类型 nfds_t 实际为无符号整形。
timeout:
该参数用于决定 poll()函数的阻塞行为,具体用法如下:
⚫ (常用)如果 timeout 等于-1,则 poll()会一直阻塞,直到 fds数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号时返回。
⚫ 如果 timeout 等于 0,poll()不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态。
⚫ 如果 timeout 大于 0,则表示设置 poll()函数阻塞时间的上限值,意味着 poll()函数最多阻塞 timeout毫秒,直到 fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止。
struct pollfd 结构体
struct pollfd 结构体如下所示:
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
fd 是一个文件描述符,struct pollfd 结构体中的 events 和 revents 都是位掩码,调用者初始化 events 来指定需要为文件描述符 fd 做检查的事件。当 poll()函数返回时,revents 变量由 poll()函数内部进行设置,用于说明文件描述符 fd 发生了哪些事件(注意,poll()没有更改 events 变量),我们可以对 revents 进行检查,判断文件描述符 fd 发生了什么事件。
这句话说明,我们在一开始需要表明要监视的文件描述符fd以及其对应的事件events,至于是否真的发生了对应的动作,则通过revents变量来指示。
应将每个数组元素的 events 成员设置为表 13.2.1 中所示的一个或几个标志,多个标志通过位或运算符( | )组合起来,通过这些值告诉内核我们关心的是该文件描述符的哪些事件。同样,返回时,revents 变量由内核设置为表13.2.1 中所示的一个或几个标志。
表 13.2.1 中第一组标志(POLLIN、POLLRDNORM、POLLRDBAND、POLLPRI、POLLRDHUP)与数据可读相关;第二组标志(POLLOUT、POLLWRNORM、POLLWRBAND)与可写数据相关;而第三组标志(POLLERR、POLLHUP、POLLNVAL)是设定在 revents 变量中用来返回有关文件描述符的附加信息,如果在 events 变量中指定了这三个标志,则会被忽略(注意,这些错误标志是文件描述符的错误信息,而不是poll函数的返回)。
如果我们对某个文件描述符上的事件不感兴趣,则可将 events 变量设置为 0;另外,将 fd 变量设置为文件描述符的负值(取文件描述符 fd 的相反数-fd),将导致对应的 events 变量被 poll()忽略,并且 revents变量将总是返回 0,这两种方法都可用来关闭对某个文件描述符的检查。
在实际应用编程中,一般用的最多的还是 POLLIN 和 POLLOUT。对于其它标志这里不再进行介绍了,后面章节内容中,如果需要使用时再给大家介绍!
poll()函数返回值
poll()函数返回值含义与 select()函数的返回值是一样的,有如下几种情况:
⚫ 返回-1 表示有错误发生,并且会设置 errno。
⚫ 返回 0 表示该调用在任意一个文件描述符成为就绪态之前就超时了。
⚫ 返回一个正整数表示有一个或多个文件描述符处于就绪态了,返回值表示 fds 数组中返回的 revents变量不为 0 的 struct pollfd 对象的数量。
poll函数实现的原理
内核将用户的fds结构体数组拷贝到内核中,当有事件发生时内核再将所有事件都返回到用户fds数组中;
polll函数只返回已就绪事件的个数,所以用户要操作就绪事件,就得用轮询的方法,
注意:poll函数是阻塞函数,当没有就绪文件描述符的时候,poll一直处于阻塞状态,直到有就绪文件描述符。
注意,poll还是需要结合轮询使用,只不过,每次轮询到poll的时候,如果没有数据,就会阻塞,有数据时才会继续执行,然后就判断返回的就绪文件描述符的数量,如果大于零,就说明有数据可读。
那么,怎么知道是哪个文件描述符发生了事件呢?
需要从头到尾一个一个地遍历!!!
因为poll只能返回就绪文件描述符的数量,而并不能反映出来是具体哪个文件描述符。
遍历时,判断结构体数据各元素变量的revents变量,当 poll()函数返回时,revents 变量由 poll()函数内部进行设置,用于说明文件描述符 fd 发生了哪些事件(注意,poll()没有更改 events 变量),我们可以对 revents 进行检查,判断文件描述符 fd 发生了什么事件。
判断某个文件描述符是否就绪之后,就需要结合read/write函数来进行数据的读写。
使用示例
示例代码 13.2.3 使用 poll 实现同时读取鼠标和键盘 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <poll.h> #define MOUSE "/dev/input/event3" int main(void) { char buf[100]; int fd, ret = 0, flag; int loops = 5; struct pollfd fds[2]; /* 打开鼠标设备文件 */ fd = open(MOUSE, O_RDONLY | O_NONBLOCK); if (-1 == fd) { perror("open error"); exit(-1); } /* 将键盘设置为非阻塞方式 */ flag = fcntl(0, F_GETFL); //先获取原来的 flag flag |= O_NONBLOCK; //将 O_NONBLOCK 标准添加到 flag fcntl(0, F_SETFL, flag); //重新设置 flag /* 同时读取键盘和鼠标 */ fds[0].fd = 0; fds[0].events = POLLIN; //只关心数据可读 fds[0].revents = 0; fds[1].fd = fd; fds[1].events = POLLIN; //只关心数据可读 fds[1].revents = 0; while (loops--) { ret = poll(fds, 2, -1); if (0 > ret) { perror("poll error"); goto out; } else if (0 == ret) { fprintf(stderr, "poll timeout.\n"); continue; } /* 检查键盘是否为就绪态 */ if(fds[0].revents & POLLIN) { ret = read(0, buf, sizeof(buf)); if (0 < ret) printf("键盘: 成功读取<%d>个字节数据\n", ret); } /* 检查鼠标是否为就绪态 */ if(fds[1].revents & POLLIN) { ret = read(fd, buf, sizeof(buf)); if (0 < ret) printf("鼠标: 成功读取<%d>个字节数据\n", ret); } } out: /* 关闭文件 */ close(fd); exit(ret); }
示例代码 13.2.3 演示了使用 poll()函数来实现 I/O 多路复用操作,同时读取键盘和鼠标。
struct pollfd 结构体的 events 变量和 revents 变量都是位掩码,所以可以使用"revents & POLLIN"按位与的方式来检查是否发生了相应的 POLLIN 事件,判断鼠标或键盘数据是否可读。
测试结果:
poll的优缺点
优点
(1)poll() 不要求开发者计算最大文件描述符加一的大小。
(2)poll() 在应付大数目的文件描述符的时候速度更快,相比于select。
(3)它没有最大连接数的限制,原因是它是基于链表来存储的。
(4)在调用函数时,只需要对参数进行一次设置就好了。
缺点
(1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义(epoll可以解决此问题)
(2)与select一样,poll需要轮询pollfd来获取就绪的描述符,这样会使性能下降
(3)同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
在使用 select()或 poll()时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的 I/O 操作(read或者write),以清除该状态,否则该状态将会一直存在。譬如示例代码 13.2.3,当 poll()成功返回时,检查文件描述符是否处于就绪态,如果文件描述符上可执行 I/O 操作时,也需要对文件描述符执行 I/O 操作,以清除就绪状态。
poll或者epoll返回之后,为什么需要进行读写操作来消除就绪状态?
poll 和 epoll 返回之后,通常需要进行读写操作来消除就绪状态,但具体情况需根据实际场景而定。以下是对这一问题的详细解释:
poll
读操作:当 poll 返回某个文件描述符可读时,说明该文件描述符对应的缓冲区有数据可读。此时应调用相应的读函数(如
read
、recv
等)将数据从内核缓冲区读取到用户空间。如果不进行读操作,下次 poll 调用时,该文件描述符可能仍会处于可读状态,因为数据还在缓冲区中未被处理。例如,在一个网络服务器中,当 poll 检测到某个客户端连接有数据可读时,服务器需要及时读取数据,以便后续对该请求进行处理,否则可能会导致数据积压或丢失。写操作:如果 poll 返回某个文件描述符可写,表示该文件描述符对应的缓冲区有空间可供写入数据。此时应调用写函数(如
write
、send
等)将数据写入缓冲区。若不及时写入数据,可能会影响数据的发送流程,导致缓冲区无法及时释放等问题。比如在向一个文件或网络连接写入数据时,如果检测到可写但不写入,后续的写入操作可能会因为缓冲区已满而阻塞。epoll
读操作:与 poll 类似,当 epoll 返回某个文件描述符可读时,也需要进行读操作来读取数据。不过,epoll 可以工作在边缘触发(ET)模式或水平触发(LT)模式下。在 ET 模式下,如果只进行了部分数据读取,那么下次 epoll 返回时,只有在缓冲区又有新的数据到达时才会再次报告该文件描述符可读;而在 LT 模式下,只要缓冲区还有未读数据,epoll 就会一直报告该文件描述符可读,直到所有数据被读完为止。
写操作:对于 epoll 返回可写的文件描述符,同样需要执行写操作。在 ET 模式下,一次写操作后,如果没有写完所有请求的数据,可能需要再次等待 epoll 通知可写才能继续写入;而在 LT 模式下,只要缓冲区有空间,就可以继续写入数据,直到写完为止。
综上所述,无论是使用poll还是epoll进行I/O多路复用监控,在它们返回文件描述符就绪状态后,一般都需要根据具体的就绪类型(读或写)进行相应的读写操作,以避免数据丢失或缓冲区溢出等问题。
epoll函数
参考:
9.2 I/O 多路复用:select/poll/epoll | 小林coding (xiaolincoding.com)
epoll一般在socket编程中用的比较多,用于服务端监控多个客户端的连接。
epoll 的API用来执行类似poll()的任务。相比poll,epoll更高效。
epoll的接口也比较简单,一共就3/4个函数:
int epoll_create(int size); int epoll_create1(int flags); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
从下图你可以看到 epoll 相关的接口作用:
epoll 通过两个方面,很好解决了 select/poll 的问题。
第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过
epoll_ctl()
函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是O(logn)
。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数来让内核将其加入到这个就绪事件列表中,当用户调用
epoll_wait()
函数时,虽然也是返回有事件发生的文件描述符的个数,但不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。接下来介绍API的详细使用
创建epoll
如下:
#include <sys/epoll.h> int epoll_create(int size); int epoll_create1(int flags);
epoll_create() 可以创建一个epoll实例。在linux 内核版本大于2.6.8 后,参数size 参数就被弃用了,但是传入的值必须大于0。
在 epoll_create () 的最初实现版本时, size参数的作用是创建epoll实例时候告诉内核需要使用多少个文件描述符。内核会使用 size 的大小去申请对应的内存(如果在使用的时候超过了给定的size, 内核会申请更多的空间)。现在,这个size参数不再使用了(内核会动态的申请需要的内存)。但要注意的是,这个size必须要大于0,为了兼容旧版的linux 内核的代码。
epoll_create() 会返回一个epoll对象的文件描述符。这个文件描述符用于后续的epoll操作。如果不需要使用这个描述符,请使用close关闭。
epoll_create1() 扩展了epoll_create() 的功能,如果参数flags的值是0,epoll_create1()等同于epoll_create();flasg也可以使用 EPOLL_CLOEXEC,EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志类似,即进程被替换时会关闭打开的文件描述符。
需要注意的是,epoll_create与epoll_create1当创建好epoll句柄后,它也会占用一个fd值,在linux下如果查看/proc/<pid>/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
设置epoll事件
如下:
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctrl是将要监视的文件描述符关联到epoll对象中。
epfd是epoll文件描述符;
op是添加事件的类型;
fd是要监视的目标文件描述符
op参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD,注册新的fd到epfd中;
EPOLL_CTL_DEL,从epfd中删除一个fd;
EPOLL_CTL_MOD,修改已经注册的fd的监听事件。
event是一个结构体指针,这个参数是用于关联指定的fd文件描述符的。它的定义如下:
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 */ };
看起来和poll的第一个参数有些像,poll的第一个参数是个结构体数组,我们把要监测的文件描述符以及事件放到结构体里,再把这个结构体放到待监视的列表数组中。
不过,epoll中是通过epoll_ctrl来添加的,放的时候,前三个参数都很好理解,最后一个结构体参数封装了对应的事件events,而且另外多了个data,data是个联合体,可以用来存放一些用户数据,比如,可以把目标fd在这里面也放一份。还可以用*ptr指针来存放更多的用户数据。
epoll_data_t是一个共用体,其4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但由于epoll_data_t是一个共用体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。也就是说,epoll中可以通过epoll_ctrl的联合体data中的ptr指针来传入除fd之外更多的用户数据(注意,这个是用户数据,并不是接收到的数据,别搞混了)。
events和poll的很像,如下是可以用的事件:
- EPOLLIN - 当关联的文件可以执行 read ()操作时。
- EPOLLOUT - 当关联的文件可以执行 write ()操作时。
- EPOLLRDHUP - (从 linux 2.6.17 开始)当socket关闭的时候,或者半关闭写段的(当使用边缘触发的时候,这个标识在写一些测试代码去检测关闭的时候特别好用)
- EPOLLPRI - 当 read ()能够读取紧急数据的时候。
- EPOLLERR - 当关联的文件发生错误的时候,epoll_wait() 总是会等待这个事件,并不是需要必须设置的标识。
- EPOLLHUP - 当指定的文件描述符被挂起的时候。epoll_wait() 总是会等待这个事件,并不是需要必须设置的标识。当socket从某一个地方读取数据的时候(管道或者socket),这个事件只是标识出这个已经读取到最后了(EOF)。所有的有效数据已经被读取完毕了,之后任何的读取都会返回0(EOF)。
- EPOLLET - 设置指定的文件描述符模式为边缘触发,默认的模式是水平触发。
- EPOLLONESHOT - (从 linux 2.6.17 开始)设置指定文件描述符为单次模式。这意味着,在设置后只会有一次从epoll_wait() 中捕获到事件,之后你必须要重新调用 epoll_ctl() 重新设置。
返回值:如果成功,返回0。如果失败,会返回-1, errno将会被设置
有以下几种错误:
EBADF - epfd 或者 fd 是无效的文件描述符。
EEXIST - op是EPOLL_CTL_ADD,同时 fd 在之前,已经被注册到epoll中了。
EINVAL - epfd不是一个epoll描述符。或者fd和epfd相同,或者op参数非法。
ENOENT - op是EPOLL_CTL_MOD或者EPOLL_CTL_DEL,但是fd还没有被注册到epoll上。
ENOMEM - 内存不足。
EPERM - 目标的fd不支持epoll。
epoll_ctl时传入用户数据有什么用
epoll_ctl时传入的用户数据主要用于保存与文件描述符相关的上下文信息,以便在事件触发后能够快速识别和处理相应的文件描述符。以下是关于用户数据作用的详细解释:
标识文件描述符:通过将文件描述符作为用户数据传入epoll_event结构体中,可以在事件触发后,通过检查epoll_event结构体中的data字段来快速识别出哪个文件描述符产生了事件。
传递额外信息:用户数据可以是指针、文件描述符、32位或64位无符号整数等类型。除了用于标识文件描述符外,还可以传递其他与文件描述符相关的业务逻辑信息,如连接状态、会话ID等,再比如传入一个回调函数,在wait到之后执行回调。
避免重复查找:在实际开发中,通常会将套接字的描述符或其他标识符作为用户数据保存。当epoll事件返回时,可以直接使用这些标识符进行操作,而无需再次通过套接字查找对象,从而提高了效率。
灵活性高:由于用户数据是一个联合体类型,可以根据实际情况灵活选择存储的数据类型,以满足不同的业务需求。
综上所述,epoll_ctl时传入的用户数据在事件驱动编程中起着至关重要的作用,它不仅用于标识文件描述符,还可以传递额外的业务逻辑信息,提高程序的效率和可维护性。
等待epoll事件
如下:
#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);
一般使用epoll_wait即可。
events:结构体指针, 一般是一个数组(注意和epoll_ctrl的最后一个参数区分,epoll_ctrl里的就是一个结构体指针,epoll_wait里的是一个用来存放已就绪的目标结构体的列表)每次epoll_wait() 返回的时候,会包含用户在epoll_ctl中设置的events,就是放到这个数组里。这个数组需要我们自行创建,然后传递到epoll_wait里面去。
maxevents:事件的最大个数, 或者说是数组的大小
timeout:超时时间, 含义与poll的timeout参数相同,设为-1表示永不超时;
返回值:有多少个IO事件已经准备就绪。如果返回0说明没有IO事件就绪,而是timeout超时。遇到错误的时候,会返回-1,并设置 errno。
有以下几种错误:
EBADF - epfd是无效的文件描述符
EFAULT - 指针events指向的内存没有访问权限
EINTR - 这个调用被信号打断。
EINVAL - epfd不是一个epoll的文件描述符,或者maxevents小于等于0
什么时候使用poll,什么时候使用epoll?
在Linux系统编程中,poll和epoll是两种常用的I/O多路复用技术,它们用于监控多个文件描述符的状态变化,以便程序可以非阻塞地等待事件的发生。以下是何时使用poll以及何时使用epoll的分析:
使用poll的场景
连接数较少时:当需要监控的文件描述符数量较少,且对性能要求不是特别高时,可以使用poll。
跨平台需求:由于poll是POSIX标准的一部分,具有更好的跨平台兼容性,因此在需要支持多种操作系统的应用场景中,poll可能是一个更合适的选择。
简单性:对于初学者或者不需要处理大量并发连接的场景,poll的使用相对简单,更容易理解和实现。
使用epoll的场景
大规模并发连接:当需要处理成千上万甚至更多的并发连接时,epoll的性能优势非常明显,因为它通过事件驱动机制避免了无效的轮询,从而大大提高了效率。
Linux特有功能:由于epoll是Linux特有的特性,如果应用只需要在Linux环境下运行,那么epoll是一个非常好的选择。
资源利用最优化:epoll的设计允许它更有效地利用系统资源,特别是在处理大量并发连接时,它可以显著减少CPU的占用和提高吞吐量。
综上所述,poll适用于连接数较少、跨平台需求或简单性要求较高的场景;而epoll则适用于需要处理大规模并发连接且追求高性能的Linux特定应用。在选择使用哪种技术时,开发者应根据具体的应用需求、预期的负载以及目标运行环境来决定。
通常在socket编程中使用epoll较多,其他情况一般使用poll就够了。
以下是一个使用
epoll
的示例代码。这个示例展示了如何创建一个 epoll 实例,将文件描述符添加到 epoll 中,并等待事件的发生。#include <stdio.h> #include <stdlib.h> #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #define MAX_EVENTS 10 int main() { int epoll_fd, nfds, fd; struct epoll_event event, events[MAX_EVENTS]; // 创建一个 epoll 实例 epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } // 打开一个文件描述符(例如标准输入) fd = STDIN_FILENO; // 设置事件类型为可读 event.events = EPOLLIN; event.data.fd = fd; // 将文件描述符添加到 epoll 实例中 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1) { perror("epoll_ctl: add"); close(epoll_fd); exit(EXIT_FAILURE); } // 等待事件发生 while (1) { nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); close(epoll_fd); exit(EXIT_FAILURE); } for (int i = 0; i < nfds; i++) { if (events[i].data.fd == fd) { char buffer[1024]; ssize_t count = read(fd, buffer, sizeof(buffer)); if (count == -1) { perror("read"); close(epoll_fd); exit(EXIT_FAILURE); } else if (count == 0) { printf("EOF\n"); close(epoll_fd); exit(EXIT_SUCCESS); } else { write(STDOUT_FILENO, buffer, count); } } } } close(epoll_fd); return 0; }
代码解释:
创建 epoll 实例:使用
epoll_create1(0)
创建一个 epoll 实例。如果失败,程序会打印错误信息并退出。打开文件描述符:在这个例子中,我们使用标准输入(
STDIN_FILENO
)作为文件描述符。设置事件类型:我们将事件类型设置为
EPOLLIN
,表示我们关心的是可读事件。添加文件描述符到 epoll 实例:使用
epoll_ctl
将文件描述符添加到 epoll 实例中。如果失败,程序会打印错误信息并退出。等待事件发生:使用
epoll_wait
等待事件发生。如果有事件发生,处理这些事件。在这个例子中,我们读取标准输入的数据并将其写入标准输出。处理事件:在循环中处理所有发生的事件。如果读取到数据,将其写入标准输出;如果遇到 EOF,则退出程序。
关闭 epoll 实例:在程序结束时关闭 epoll 实例。
这个示例展示了如何使用
epoll
来高效地管理多个文件描述符的事件,适用于需要处理大量并发连接的场景,如网络服务器等。
补充:
参考:Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_主动去触发epoll事件-CSDN博客
事件
可读事件
,当文件描述符关联的内核读缓冲区可读,则触发可读事件。 (可读:内核缓冲区非空,有数据可以读取)
可写事件
,当文件描述符关联的内核写缓冲区可写,则触发可写事件。 (可写:内核缓冲区不满,有空闲空间可以写入)epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
调用epoll_create时,内核除了帮我们在epoll文件系统里建了个
file结点(epoll_create创建的文件描述符)
,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件。不管是poll还是epoll,都是监视文件描述符是否可读或者可写,之后,都需要进行主动的数据读写。
epoll更多原理参考:
EPOLL原理详解(图文并茂) - Big_Chuan - 博客园 (cnblogs.com)
再补充一个实际用例来加深理解。
先创建一个epoll实例
然后添加要监视的fd以及要放进去的用户数据
这里就是先构造了一些用户数据,然后把这些用户数据放到了data的ptr这个指针变量里面,这样就能放入除fd外更多的用户数据了。最后调用epoll_ctrl函数来将epoll_fd加入到监控之中。
然后就是循环进行epoll_wait监控了
在循环中,先定义了一个events数组,然后传递进入epoll_wait,用来接收就绪的文件描述符的信息。
之后,遍历已就绪的文件描述符数组,执行对应的用户行为。
注意,此处并没有去读写数据。而是直接对用户数据进行操作。
注意字符设备块设备的文件描述符,以及网络设备的文件描述符的区别;
字符设备块设备的文件描述符是通过打开相应的设备文件来打开的,而网络设备的文件描述符需要用socket创建出来或者使用accept得到的,并不需要传递什么对应的设备。
什么是进程替换?
我们的可执行程序,在运行起来的时候就是一个进程,一个进程就会有他的内核数据结构+代码和数据,把一个已经成型的进程的代码和数据替换掉,这就叫进程替换,也就是可以通过系统调用把当前进程替换位我们需要的进程,那么替换后,会创建一个新进程吗?不会,只是在旧进程的壳子执行新进程;替换进程后,之前的代码不会执行,因为已经被替换了。