一、poll概述
poll是另一种I/O复用,和select类似,也是在指定事件内轮询一定数量的文件描述符,判断其中是否有就绪的。本质都是统一监听,如果任意一个文件描述符上有关注的事件发生。则这个文件描述符以及发生的事件通知给应用程序。但某些机制又比select优秀很多。
二、poll函数详解
poll的函数原型:
int poll(struct pollfd fds[],int len,int timeout);
//成功返回就绪文件描述符个数,失败返回-1,0表示超时
参数:
1. struct pollfd fds[]参数:
Struct pollfd
{
int fd;//用户设置关注的文件描述符(用户填充),不是二进制了,是int类型
short events;//用户关注的事件类型(用户填充)
short revents;//由内核填充,就绪的事件类型
}
所以我们每次调用poll之前要先定义这个结构体,并用用户事件描述符对fd,events进行初始化,返回时就绪事件就在revents里面存储。可以看到 用户初始描述符和内核返回的就绪描述符是分开的,所以不用每次初始化它,初始化一次就可。 但我们仍然不知道是哪几个描述符就绪,只知道有几个就绪,还是需要轮询查找判断。
2. int len参数
表示结构体的大小,所以可以看到poll没有空间限制,可以存放很多很多个,不像select只能存储1024个。
3. timeout参数
定时时间,在一定时间内没有检测到就绪事件,返回0,为NULL表示永久阻塞。
三、poll的事件类型
通过poll的函数原型我们可以知道poll有很多事件类型,不像select只有可读,可写,异常事件,我们将这些事件列举出来,并标注几个常用的,如下图:
我们圈起来的是比较常用的三个。POLLRDHUP是专门捕捉客户端断开异常的,我们在select时通过recv<=0来判断客户端是否关闭,在这专门提供了函数。
四、poll的特点
【1.优点】
- 在pollfd结构体中我们可以明确的看到。poll的文件描述符是通过整型值去记录的。所以其能处理的文件描述符的个数就比select要大得多。而且是由用户自己指定的而select中的fd_set就只有系统内核提供的1024位。所以在应付大数目的文件描述符的时候相比于select速度更快。
- 因为poll将用户关注的和内核填充的分开可以,用户关注的事件类型是不会变的,所以初始化一次就可以。
- 相比于select关注的事件类型更多
【2.缺点】
- 大量的pollfd类型的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义,效率低下。
- 与select一样,poll返回后,需要轮询pollfd类型的数组来获取就绪的描述符。
五、poll的代码实现
我们实现一个简单的代码,它和select的实现差不多,但还是有区别如下:
- select每次要初始化结构体,因为用户和内核共用一个空间。poll不用,一次初始化,每次只用对结构体中的fd,events成员进行一次初始化,记录用户事件描述符,有新连接,添加到结构体中,连接断开,从结构体中删除。内核返回的就绪事件在revents中放着。
- select在处理事件时比较简单,只需要判断是连接(添加进去)还是数据连接(处理数据)。而poll比较复杂,它需要判断监听,数据。数据连接时需要判断三种情况:没有事件发生;客户端自动断开连接事件发生;可读事件发生。
那我们现在实现客户端连接服务器,发送数据,服务器收到给客户端回复ok,我们以程序流程图来说明整个逻辑思路:
我们就根据这个思路写出代码:
poll.c
# define _GNU_SOURCE//用来引入客户端断开链接事件,要放到最上面
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<assert.h>
# include<sys/types.h>
# include<sys/socket.h>
# include<arpa/inet.h>
# include<netinet/in.h>
# include<poll.h>
# define LEN 100
int InitSocket()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1) return -1;
struct sockaddr_in ser;
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET;
ser.sin_port