图解IO多路复用之Poll的实现原理

IO多路复用在Linux上还有一种poll的实现方案,其实poll和select一样,它们都是函数,但是poll针对select的底层使用的bit数组的文件描述符列表做了优化,下面分析poll的原理。

1、认识poll函数

poll的原型函数如下所示:

struct pollfd myfd[500];    
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

(1)pollfd结构体

函数的结构形式如下所示:

struct pollfd { 
# 需要内核检测的文件描述符 
    int fd; 
# 文件描述符的事件 
    short events;      
# 返回就绪的事件 
    short revents; 
};

在 pollfd 结构体中events和revents的事件主要是读事件、写事件和读写异常的事件,事件的整理如下表所示:

事件 做events值 做revents值
读事件 POLLIN 可以 可以
POLLRDNORM 可以 可以
POLLRDBAND 可以 可以
POLLRI 可以 可以
写事件 POLLOUT 可以 可以
POLLWRNORM 可以 可以
POLLWRBAND 可以 可以
读写异常事件 POLLERR 可以
POLLHUP 可以
POLLNVAL 可以

events可以单独的传读事件或写事件(如events= POLLIN ),也可以同时传入读事件和写事件(如 ev ents=POLLIN | POLLOUT )

revents是内核写出的值,其值是根据events传入的事件决定的,如果events传入的是读事件,那么revents传出的就是就绪的读文件描述符。

poll相比于select方式最大的改进是增加了 pollfd 结构体,正是由于pollfd结构体的出现,那么如32位系统就不会出现1024个文件描述符的限制,相对于select来说可以承受更多客户端连接并且 pollfd 结构体也是可以重复使用的。

(2)nfds

本参数用来表示在struct pollfd myfd[500]中有效的文件描述符的最大值加1(如在数组中有效的文件描述是0-300,那么nfds = 300 + 1 = 301),设置此参数的目的是给用户态的fds遍历就绪文件描述符的时候指定一个循环的上限范围值。

(3)timeout

设置poll函数的阻塞时长,单位是毫秒,设置不同的值会有不同的效果,如下表所示:

设置的值 含义
> 0 如果超过指定的时间后,无论是否有就绪的文件描述符都退出阻塞。
= 0 不管检测集合中有没有已就绪的文件描述符,函数立刻返回
= -1 一直阻塞,直到检测的集合中有就绪的文件描述符之后退出阻塞

(4)poll函数的返回值

poll返回一个int类型的数据,我们可以根据返回的不同结果来分析是否有就绪的文件描述符,其返回值表如下所示:

返回值 含义
> 0 本次检测到就绪文件描述符的个数
= -1 检测失败

2、poll的原理

以客户端读取数据为案例介绍poll的工作原理,下图展示了客户端读取数据的流程图:

(1)客户端设置的文件描述符信息,如下图设置的读事件:

(2)通过poll函数将设置的文件描述符信息加载到内核中,此时内核中的文件描述符信息如下所示:

(3)内核开始监听文件描述符对应的事件,此时有数据通过网卡发送过的的时候,如下所示:

数据在内核的环形缓冲区中使用DMA复制技术将数据复制到对应的读缓冲区中,然后修改文件描述符信息(更新revents上对应的数据),poll经过一轮监听之后将就绪的文件描述符数据返回到用户态。

(4)poll返回数据到用户态,用户态根据poll函数的返回判断是否有数据达到(返回值大于0表示有数据到达),此时用户态开始遍历文件描述符数组来分析是哪个文件描述符就绪并做后续的相关的业务,如下图所示:

假设poll遍历出来是第1个文件描述符就绪了(也就是fd[0]),那么它开始接收数据,数据接收完毕之后还会将第1个文件描述符的revents位置的1修改成0,这样也是为什么pollfd可以重复的原因。

通过以上的步骤就完成了读数据的读取过程。写数据的过程也是类似的,这里就不展开描述了。

总结:

(1)poll相对于select方式,增加了pollfd的结构体,其工作原理的seclet基本类似。

(2)select具有跨平台的特点,但是poll只能在Linux上使用。

1