在这篇文章中介绍了利用select来实现IO多路转接。在本文中,将介绍利用poll来实现IO多路转接。
poll与select的作用相同。都是一次等待多个文件描述符,当至少有一个文件描述符上的就绪事件发生时,poll返回满足就绪条件的文件描述符的个数并将发生改变的文件描述符由参数带回。只是poll在接口设计上与select有所不同。
select在接口使用时,中间的三个参数均为输入输出型参数。因此需要设置一个数组保存关心的文件描述符。在每次调用select之前,根据数组来手动设置事件集,在使用过程中这些接口的实现不仅麻烦,而且时间开销也非常大,进而导致效率低;同时由于fd_set的大小固定,所以最多能关心的文件描述符的数量有限。因此poll在接口的实现上对这两方面的问题进行了改良。下面来介绍利用poll来实现TCP服务器。
Poll
Poll的函数原型:
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
参数说明:
fds:该参数是一个结构体指针。指向一个结构体数组。数组的内容为一个个的struct pollfd结构体变量。该结构体的成员变量如上所示:
fd:表示关心的文件描述符
events:表示关心的文件描述符上的事件集合
revents:表示关心的 文件描述符上的哪些事件发生了。
比如说:关心文件描述符为4上的读事件。所以,在调用poll之前,将该结构体变量中fd的值设置为4,events设置为读事件(对应相关的常量,后面介绍)。然后调用poll,在poll返回之后,如果该文件描述符上的读事件就绪了,此时该结构体变量的成员revents的值就被设置为读事件相关的常量。
由上述可以知道,该结构体指针是一个输入输出型参数,作为输入型参数,由调用者告诉操作系统关心哪些文件描述符上的哪些事件(调用者设置fd和events)。作为输出型参数,由操作系统告诉调用者哪些文件描述符上的哪些事件就绪了(操作系统设置revents)。
nfds:表示结构体数组的大小。即表示关心的文件描述符的总个数。
timeout:表示超时时间,单位为ms(毫秒)。注意:将该处的timeout的结构与select中的做区分。
-1:表示poll阻塞等待
0:表示非阻塞等待
特定的时间值:表示阻塞规定的时间后返回。
在上述的struct pollfd结构体中,event和events均为short类型。表示的是事件集合。因为short大小为2字节16个比特位。如果用一个比特位来表示一个事件,则一个文件描述符上的能关心的事件最多为16个。将一个事件用一个宏来表示,各个事件所对应的宏只有一个比特位为1,且各个宏之间为1的比特位必然不同。以下为常用的事件所表示的宏(其余相关的宏在:/usr/include/asm-generic/poll.h 中查看):
POLLIN:表示读事件(0x001)
POLLOUT:表示写事(0x004)
返回值说明:
与select返回值类似。返回值大于0表示满足就绪条件的文件描述符的个数;返回值等于0表示超时返回;返回值小于0表示poll函数调用出错。
根据poll来实现IO多路转接。有以下几点需要注意:
1. 对poll的参数进行设定。
因为poll的参数主要是结构体数组。此时只需要设置一个结构体数组。将关心的文件描述符上的事件添加进该结构体中即可。在添加事件时,因为不同的事件用不同的宏来表示,所以,只需要将events变量与关心的事件宏按位或即可添加成功。
2. 调用poll时。判断事件是否就绪的条件与select相同。
3. 当poll返回时,要判断关心的文件描述符上的事件是否发生。所以将revent变量与相应的事件宏进行按位与。
接下来,根据poll来实现TCP服务器:
1. 绑定设置监听套接字
2. 设置上述所说的结构体数组。初始化该数组后,将监听套接字添加进该数组中(只关心读事件)。
3. 循环用poll开始等待。
4. poll返回。返回值小于0,说明poll调用失败。返回值等于0,说明超时返回。大于0,说明有就绪事件发生。
5. 返回值大于0时,遍历结构体数组,判断哪些文件描述符上的事件发生了(本程序中只关心读事件)。
如果监听套接字,则调用accept接收连接。然后将新获得的连接添加进结构体数组中;
如果是普通文件描述符,则调用read读取数据,如果read返回值大于0,表示读取成功;如果等于0,表示对端关闭连接,此时,关闭该连接,并在结构体数组中将该文件描述符删除;如果小于0,表示读取失败,此时,关闭该连接,并在结构体数组中将该文件描述符删除
根据上述流程编写的完整的TCP服务器代码见:poll实现TCP服务器。客户端代码与select相同:客户端代码
Poll的优点
1. 在select中是用三个位图来保存关心的文件描述符。因此文件描述符的个数受fd_set大小的限制。而poll中是用数组来保存,数组的大小可以进行动态申请,因此Poll没有最大文件描述符的限制;
2. poll用一个结构体来保存:文件描述符,关心的事件,满足就绪条件的事件。相对于位图来说,在接口的使用上更加方便。同时它将select中的输入输出参数分离开,在每次调用poll之前不必在重新设定关心的事件。从而节省了一定的事件。
Poll的缺点
poll在select的基础上做了一定的优化,但并没有解决根本问题,所以它还具有select的一些缺点:
1. 在调用select时,需要将关心的文件描述符从用户态拷贝到内核;
2. 在select中,需要操作系统主动的遍历关心的文件描述符,等待就绪事件的发生;
3. 在select之后,调用者需要遍历结构体数组判定哪些文件描述符上的事件发生了;
以上三点,在时间开销上都比较大,因此服务器的性能会受影响。
4. 某一时刻只有很少的连接处于就绪状态,此时随着关心的文件描述符的增多,服务器的性能会有所下降。