由于poll是对select的改进,poll的功能和select的功能一样,只不过是参数稍微不同,poll的底层原理也和select差不多。
对多路复用select()不太熟悉的可以参考这篇博文:I/O多路复用之select
我们首先回忆一下 select接口 :
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select需要我们指定文件描述符的最大值,然后取[0,nfds)这个范围内的值查看是在集合readfds,writefds或execptfds中,也就是说这个范围内存在一些不是我们感兴趣的文件描述符,cpu做了一些无用功,poll对它进行了改进,下面就看看poll是怎么做的。
poll函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数参数:
1. fds参数:
fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读,可写和异常等事件。
pollfd结构体定义如下:
struct pollfd {
int fd; /*文件描述符 */
short events; /* 注册的事件*/
short revents; /* 实际发生的事件,由内核填充 */
};
其中,fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序的fd上实际发生了哪些事件。
events 和 revents能够设置的值都定义在<poll.h>头中,有以下几种可能:
事件 | 描述 |
---|---|
POLLIN | 普通或优先级带数据可读 |
POLLRDNORM | 普通数据可读 |
POLLRDBAND | 优先级带数据可读 |
POLLPRI | 高优先级数据可读 |
POLLOUT | 普通数据或优先级带数据可写 |
POLLWRNORM | 普通数据可写 |
POLLWRBAND | 优先级带数据可写 |
POLLERR | 发生错误 |
POLLHUP | 发生挂起 |
POLLNVAL | 文件描述符没有打开 |
需要注意的是,后三个只能作为描述字的返回结果存储在revents中,而不能作为测试条件用于events中,POLLERR,POLLHUP,POLLNVAL在events属性中是没有意义的。
常用的是POLLIN和POLLOUT。
POLLRDHUP (since Linux 2.6.17),在socket的一端关闭了连接,或者是写端关闭了连接时触发。但使用POLLRDHUP事件时,我们需要在代码最开始处定义_GNU_SOURCE。
poll返回时,我们只需要将fds数组中的每个元素中的revents成员与关心的事件进行&操作,如果值大于0,则代表该事件已就绪。例如:if(fds[0].revents & POLLIN ){//文件描述符可读}
2. nfds参数:
nfds参数指定被监听事件集合fds的大小。其类型nfds_t的定义如下:
typedef unsigned long int nfds_t;
3. timeout参数:
timeout参数指定poll的超时值,单位是毫秒。当timeout值为 0 时,poll() 函数立即返回。当timeout的值为 -1 时,则使 poll() 一直阻塞直到一个指定事件发生。
函数返回值:
成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;
失败时,poll() 返回 -1,并设置 errno 为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。
代码示例:
下面是一个使用poll同时监听一个有名管道和标准输入的代码。
#include <poll.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ret;
int fd;
struct pollfd fds[2]; // 监视文件描述符结构体,2 个元素
ret = mkfifo("test_fifo", 0666); // 创建有名管道
if(ret != 0){
perror("mkfifo:");
}
fd = open("test_fifo", O_RDWR); // 读写方式打开管道
if(fd < 0){
perror("open fifo");
return -1;
}
ret = 0;
fds[0].fd = 0; // 标准输入
fds[1].fd = fd; // 有名管道
fds[0].events = POLLIN; // 普通或优先级带数据可读
fds[1].events = POLLIN; // 普通或优先级带数据可读
fds[0].revents=0; //初始化
fds[1].revents=0;
while(1){
// 监视并等待多个文件(标准输入,有名管道)描述符的属性变化(是否可读)
// 没有属性变化,这个函数会阻塞,直到有变化才往下执行,这里没有设置超时
ret = poll(fds, 2, -1);
//ret = poll(&fd, 2, 1000);
if(ret == -1){ // 出错
perror("poll()");
}else if(ret > 0){ // 准备就绪的文件描述符
char buf[100] = {0};
if( ( fds[0].revents & POLLIN ) == POLLIN ){ // 标准输入
read(0, buf, sizeof(buf));
printf("stdin buf = %s\n", buf);
}else if( ( fds[1].revents & POLLIN ) == POLLIN ){ // 有名管道
read(fd, buf, sizeof(buf));
printf("fifo buf = %s\n", buf);
}
}else if(0 == ret){ // 超时
printf("time out\n");
}
}
return 0;
}
poll()的特点:
select() 和 poll() 系统调用的本质一样,前者在 BSD UNIX 中引入的,后者在 System V 中引入的。相比select(),poll()性能改进了很多,但是也还是存在一些缺点。
poll的优点:
- poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
- poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。
- poll() 没有最大文件描述符数量的限制。
poll的缺点:
- 虽然fd没有限制,但是事实上,同时连接的客户端在有些时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
- poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间
- 每次调用poll,都要轮询检测events数组中fd的revents是不是和其events相同,开销大
推荐文章:I/O多路复用之epoll