Linux下的IO复用机制

所谓IO复用(IO multiplexing),就是使用一个线程来同时管理多个IO流。我们“复用”的是线程。
为了实现上述功能,我们要先构造一张我们感兴趣的描述符列表,然后调用一个函数,直到这些描述符中的一个已经准备好进行IO时,该函数才返回。Linux提供了select()、poll()、epoll()三个函数来供我们进行IO复用。

(UNP上给出的I/O复用模型)

select

该函数允许进程指示内核等待多个事件中的任何一个发生,并且在有一个或多个事件发生或经历过一段指定的时间后才唤醒他。

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
//返回:若有描述符就绪则返回已就绪的描述符个数,若超时则返回0,若出错则返回-1

参数
(1) timeout:等待时间,timeval结构用于指定这段时间的秒数和微秒数,struct timeval { long tv_sec; long tv_usec; };
这个参数有三种可能:1)永远等待下去(仅在有一个描述符准备好IO时才返回):把该参数置为NULL;2)等待一段固定时间(仅在有一个描述符准备好IO时才返回,但不超过该参数所指定的时间):在timeval结构中指定秒数和微秒数;3)不等待 (检查描述符后立即返回,这称为轮询):该参数必须指向一个timeval结构,其中的定时器值设为0;
=> 前两种情形的等待通常会被进程在等待期间捕获的信号中断,并调用相应的信号处理函数。
=> 尽管timeval结构允许我们指定一个微秒级的分辨率,但内核支持的分辨率往往粗糙的多,Linux内核会把超时值向上舍入成10ms的整数倍。
(2)readset,writeset,exceptset:指定我们要让内核测试读、写和异常条件的描述符。
fd_set数据类型用来表示描述符集,通常是一个整数数组,其中每个整数的每一位对应一个描述符。例如:采用32位整数,那么该数组的第一个元素对应于描述符0-31,第二个元素对应与描述符32-63,依次类推。
有配套的四个宏帮助我们处理fd_set数据类型:

#include <sys/select.h>
void FD_ZERO(fd_set *fdset);             //清除fdset中的每一位,常用于文件描述符集设置前对其进行初始化
void FD_SET(int fd, fd_set *fdset);      //打开fdset中fd描述符对应位
void FD_CLR(int fd, fd_set *fdset);      //关闭fdset中fd描述符对应位
int  FD_ISSET(int fd, fd_set *fdset);    //select函数返回后,检测fdset中对应描述符是否准备好,当描述符fd在描述符集fdset中则返回非零值,否则返回零

=> 描述符集的初始化非常重要,因为作为自动变量分配的一个描述符集如果没有初始化,那么可能发生不可预料的后果。
=> select()函数的中间三个参数readset,writeset,execptset中,如果我们对某一个条件不感兴趣,就可以把它置为空指针。事实上,如果这三个指针都为空,我们就有了一个比Unix的sleep()函数更精确的定时器(sleep睡眠以秒为最小单位)。
=> select()函数修改由指针readset,writeset,exceptset指向的描述符集,这三个参数都是值-结果参数。调用select()函数时,我们指定所关心的描述符的值,select()函数返回时,结果将显示哪些描述符已就绪。而且描述符集内任何与未就绪描述符对应的位在返回时均被清0,因此,每次重新调用select()函数时,我们都需要再次把描述符集中所有关心的位置为1。
(3)maxfdp1:指定待测描述符的个数,它的值是待测试的最大描述符值加1(因此描述符是从0开始的),描述符0,1,2 … maxfd1-1均将被测试。例如:打开描述符1、4、5,则其maxfd1的值为6。
=> 设置这个参数是出于效率方面的考虑,内核不会复制描述符集中超过maxfdp1的部分,从而减少每次测试的描述符个数,提高效率。
=> select的最大描述符数:头文件<sys/select.h>中定义的FD_SETSIZE常值是数据类型fd_set中描述符的总数。该值通常为1024,表示select最多可同时监听1024个fd,我们可以通过修改该头文件然后重新编译内核来扩大这个数目(使用起来很不方便)。

poll

poll提供的功能与select类似,但它避免了select描述符有限的问题。

#include <poll.h>

int poll(struct pollfd fdarray[], unsigned long nfds, int timeout);
//返回:若有描述符就绪则返回已就绪描述符个数,若超时则返回0,若出错则返回-1

参数
(1)fdarray:该参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。

struct pollfd {
    int fd;
    short events;
    short revents;
};

=> 如果我们不关心某个特定的描述符,可以把与他对应的pollfd结构的fd成员设置成一个负值。poll()函数将忽略这样的pollfd结构的events成员,并在返回时将它的revents成员的值置为0
=> 要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态。(每个描述符都有两个变量,一个为调用值,另一个为返回值,从而避免使用值-结果参数)
events和revents的取值是一些指定的常值。(前四行是可读性,中间三行是可写性,最后三行是异常条件)

(图片摘录自UNP)
(2)nfds:结构数组中元素的个数(采用unsigened long类型似乎过于大了,新标准中为该参数定义了新的nfds_t数据类型)
(3)timeout:指定poll()函数返回前等待多长时间。单位是毫秒。
=> timeout的取值:INFTIM(永远等待)、0(立即返回,不阻塞进程)、>0(等待指定的毫秒数)

补充:select()与poll()的区别
select()的fd_set是一个位掩码(bit mask),因此fd_set有固定的长度,该长度在内核编译时由FD_SETSIZE指定。
然而,使用者在调用poll()时需要自定义pollfd结构体数组并指定数组的大小,因此poll()函数的描述符个数没有限制。

epoll

新的Linux内核提供了epoll机制,相对于select和poll,epoll最大的好处在于它不会随着要监控的fd数目增长而降低效率。(在内核中select/poll的实现是采用轮询的方法检测就绪事件,轮询的fd数目越多,自然耗时越多,效率也就越低)
不同于select和poll,epoll提供了3个接口函数:

#include <sys/epoll.h>

int epoll_create(int size);
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);

函数说明:
(1)epoll_create()
功能:生成epoll专用的文件描述符epfd,他其实是在内核申请了一块空间,用来存放你关注的socket fd上所发生的事件。
参数:size 表示生成的epoll fd上能关注的最大socket fd数(注意:它与select()第一个参数maxfd1不同,它不是待测最大描述符值加一)
返回值:文件描述符epfd(注意:epfd会占用一个fd值,所以在使用完epoll()后,必须调用close()关闭,否则可能导致fd被耗尽)
(2)epoll_ctl()
功能:epoll的事件注册函数,用于控制epoll文件描述符上的事件,可以注册事件、修改事件或删除事件。(注意:不同于select()是在监听事件时才告诉内核要监听什么类型的事件,epoll要先注册想监听的事件类型)
参数
epfd epoll_create()的返回值
op 指定操作类型,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已注册的fd上的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
fd 要操作的socket文件描述符
event 要监听的事件,它是epoll_event结构指针类型,epoll_event结构如下:

struct epoll_event {
    _uint_32_t events;    //epoll事件
    epoll_data_t data;    //用户数据
};

typedef union epoll_data {
    void *ptr;     //指向与fd相关的用户数据
    int fd;        //指定事件所从属的目标文件描述符
    _uint32_t u32;
    _uint64_t u64;
} epoll_data_t;

结构体成员events表示事件类型,取值与poll()中events的取值基本相同。两个额外事件:EPOLLET和EPOLLONESHOT,它们是高效运作的关键
EPOLLET:将EPOLL设为边沿触发方式(Edge Trigger),这是相对于水平触发(Level Trigger)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这个事件后,如果还要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
返回值:成功返回0,失败返回-1

补充:epoll事件有两种模型
Edge Trigger(ET) 高速工作方式,错误率较大,只支持no_block socket,在这种方式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你,然后它会假设你已经知道文件描述符已就绪,并且不会为那个描述符发送更多的通知,直到你做了某些操作导致那个文件描述符变为不再就绪状态。
Level Trigger(LT) 默认工作方式,错误率较小,支持block socket和no_block socket,在这种方式下,内核告诉你一个文件描述符就绪了,然后你就可以对这个就绪的fd进行IO操作。如果你不做任何操作,内核还是会继续通知你。因此,这种方式出错的可能性较小。

(3)epoll_wait()
功能:等待事件触发,如果在规定时间内没有事件触发,则超时。类似于select()调用。
参数
events 检测到的事件 (参数events用来从内核得到事件集合,epoll_wait()函数将所有就绪的事件从内核事件表复制到本参数所指向的数组中)
maxevents 指定最多监听多少个事件(告知内核events有多大)
timeout 指定epoll的超时时间(当timeout为-1,epoll_wait调用将永远阻塞,直到某个事件发生;timeout为0,epoll_wait调用将立即返回)
返回值:成功返回就绪文件描述符的个数,失败返回-1并设置errno

(图片来源https://www.cnblogs.com/lojunren/p/3856290.html)

补充:epoll()与select()的区别:

  1. select的可用描述符数目受限。在linux/posix_types.h头文件中有这样的声明:#define __FD_SETSIZE 1024 表示select最多可用1024个fd。而epoll没有这个限制,它的限制是最大打开文件描述符个数。
  2. epoll的最大好处是不会随着fd数目的增长而降低效率,在select中采用轮询处理,其中的数据结构类似一个数组。而epoll则是维护一个队列,epoll只会对活跃的socket进行操作,这是因为在内核实现中epoll是根据每个fd上面的callback()函数实现的,只有活跃的socket才会主动去调用callback()函数(把相应描述符加入队列),不活跃的socket则不会调用。注意,如果大部分的socket都是活跃的,则epoll效率不一定比select高。
  3. epoll使用mmap加速内核与用户空间的消息传递。epoll将用户空间的一块地址和内核空间的一块地址映射到相同的一块物理内存地址,使得这块物理内存对内核和用户均可见,减少用户态和内核态之间的数据交换,提高效率。

参考:
《UNIX网络编程卷1:套接字联网API》 第3版
Linux epoll机制 - HenryLiuY - 博客园 https://www.cnblogs.com/henryliublog/p/9645562.html
Linux下的I/O复用与epoll详解 - junren - 博客园 https://www.cnblogs.com/lojunren/p/3856290.html

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值