poll的多路复用机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询(polling),根据描述符的状态进行处理,但是poll没有最大文件描述符数量上的限制。
poll函数的原型声明:
//使用:man 2 poll,查看poll函数的使用帮助信息(CentOS-7.6)
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include
int ppoll(
struct pollfd *fds,
nfds_t nfds,
const struct timespec *timeout_ts,
const sigset_t *sigmask);
【参数说明】
(1)第1个参数fds:是一个struct pollfd结构体类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd结构体的定义如下:
struct pollfd
{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件,由内核填充
};
每一个struct pollfd结构体指定了一个被监视的描述符,可以传递多个结构体,指示poll监视多个文件描述符,没有数量限制,由参数fds指针指向一个struct pollfd结构体数组来实现。要测试的I/O事件由events成员指定,poll函数在相应的revents成员中返回该描述符的状态。(每个描述符都有两个变量,一个为调用值,另一个为返回结果值,从而避免了select中使用值-结果参数,select函数的中间3个参数都是值-结果参数)。其中,events成员是监视该描述符的事件掩码,由用户自己来设置该值;revents成员是描述符的操作结果事件掩码,内核在调用返回时设置这个成员的值。
events成员中请求的任何事件都可能在revents成员中返回。下图中列出了用于指定events标志以及测试revnets标志的一些常值:
上图中分为了3个部分:第1部分是处理输入的4个常值,第二部分是处理输出的3个常值,第3部分是处理错误的3个常值。其中第3部分的3个常值不能在events中设置,但是当相应条件存在时就会在revents中返回。
<说明> 上表中列举的符号常量定义在/usr/include/bits/poll.h文件中,参考的是CentOS-7.6系统。
/* Event types that can be polled for. These bits may be set in `events'
to indicate the interesting event types; they will appear in `revents'
to indicate the status of the file descriptor. */
#define POLLIN 0x001 /* There is data to read. */
#define POLLPRI 0x002 /* There is urgent data to read. */
#define POLLOUT 0x004 /* Writing now will not block. */
#if defined __USE_XOPEN || defined __USE_XOPEN2K8
/* These values are defined in XPG4.2. */
# define POLLRDNORM 0x040 /* Normal data may be read. */
# define POLLRDBAND 0x080 /* Priority data may be read. */
# define POLLWRNORM 0x100 /* Writing now will not block. */
# define POLLWRBAND 0x200 /* Priority data may be written. */
#endif
#ifdef __USE_GNU
/* These are extensions for Linux. */
# define POLLMSG 0x400
# define POLLREMOVE 0x1000
# define POLLRDHUP 0x2000 //(since Linux 2.6.17)
#endif
/* Event types always implicitly polled for. These bits need not be set in
`events', but they will appear in `revents' to indicate the status of
the file descriptor. */
#define POLLERR 0x008 /* Error condition. */
#define POLLHUP 0x010 /* Hung up. */
#define POLLNVAL 0x020 /* Invalid polling request. */
poll识别3类数据:普通(Normal)、优先级带(Priority Band)和高优先级(High Priority)。例如,我们要同时监视一个文件描述符的可读和可写事件,可以将events设置为POLLIN | POLLOUT。当poll函数返回时,我们可以检查revents中的标志:
可读:items[i].revents & POLLIN
可写:items[i].revents & POLLOUT
如果POLLIN事件被设置,则文件描述符可以读取而不导致阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
(2)第2个参数:nfds,表示的是被监控的描述符的个数,亦即fds指针指向的struct pollfd结构体数组的元素个数。
<说明> 历史上这个参数曾被定义成功无符号整型(unsigned long),似乎过分大了,定义为无符号整型(unsigned int)可能就足够了。Unix98为该参数定义了名为nfds_t的新数据类型。在/usr/include/sys/poll.h文件中该数据类型的定义如下:
typedef unsigned long int nfds_t; //CentOS7.6中,该数据类型是被定义为无符号长整型的
(3)第3个参数:timeout,指定poll函数返回前等待超时的时间,单位是毫秒数。下表给出了它的可能取值:
<说明>
(1)如果timeout > 0 或者 为负值(一般设置为-1)时,poll函数将会被阻塞,直到被监控的描述符指定的I/O事件准备就绪或者发生错误时,poll才会返回;或者定时器到时也会返回(在timeout>0的情况下)。
(2)timeout=0时,poll函数立刻返回,不阻塞进程,无论是否有描述符准备就绪。
【返回值】
1、成功,返回已就绪的描述符个数,即返回struct pollfd结构体中revents成员值非0的描述符个数;
2、若定时器到时之前没有任何描述符就绪,则返回0。
3、当发生错误时,返回值为-1,并设置相应的错误码给errno全局变量。错误码的可能取值如下:
<说明> 如果我们不再关心某个特定描述符,那么可以把与它对应的struct pollfd结构体的fd成员设置为一个负值(一般而言设置为-1)。poll函数将忽略这样的pollfd结构的events成员,同时返回时将它的revents成员的值置为0。相比于select函数,poll函数不再有FD_SETSIZE最大描述符数目的设定,因为分配一个pollfd结构体数组并把该数组中元素个数通知内核就行了,内核不再需要知道类似fd_set的固定大小的数据类型。
事实上,传递给select函数的fd_set结构体类型变量的成员是一个整型数组,不过它的数组长度是个固定值,是由操作系统内部定义的FD_SETSIZE 和 NFDBITS 这两个符号常量决定的,无法人为修改;而传递给poll函数的pollfd结构体数组,其结构体数组的长度是可以人为设定的。
poll 与 select最大的区别就是poll没有最大描述符数量的限制,因此它仍然存在和select同样的缺陷。
(1)和select函数一样,poll同样需要维护一个用来存放描述符的数据结构,当描述符的数量比较大时,会使得用户空间和内核空间在传递该数据结构时复制开销大。
(2)poll 和 select一样,对描述符进行扫描的方式也是线性扫描,每次调用poll都需要遍历整个描述符集,不管那个描述符是不是活跃的,都需要遍历一遍。当描述符数量较多时,会占用大量CPU资源。
(3)poll 和 select一样,不是线程安全的函数。
程序描述:编写一个echo server程序,功能是客户端向服务器发送信息,服务器端接收数据后输出并原样返回给客户端,客户端接收到消息并输出到终端。代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//#define IPADDRESS "127.0.0.1"
//#define PORT 8787
#define MAXLEN 1024
#define LISTENQ 5
#define OPEN_MAX 1000
#define INFTIM -1
/**
服务器端程序
*/
#include "socket_common.h"
//函数声明
static int prepare_tcp_listen(const char *ip,int port);
static void do_poll(int listenfd);
static void handle_client(struct pollfd *connfds,int count);
int main(int argc,char *argv[])
{
int sfd;
if(argc < 2)
{
printf("usage: ./poll_server port\n");
exit(-1);
}
sfd=prepare_tcp_listen(NULL,atoi(argv[1]));
do_poll(sfd);
return 0;
}
static int prepare_tcp_listen(const char *ip,int port)
{
//创建socket套接字
int sfd=socket(AF_INET,SOCK_STREAM,0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(struct sockaddr_in));
//填充sockaddr_in结构体内容
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(port);
//server_addr.sin_addr.s_addr=inet_addr(ip);
server_addr.sin_addr.s_addr=INADDR_ANY;
//绑定IP地址和端口号
if(bind(sfd,(struct sockaddr*)&server_addr,sizeof(struct sockaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);
}
//监听客户机的连接请求
if(listen(sfd,LISTENQ) == -1)
{
perror("listen");
close(sfd);
exit(-1);
}
return sfd;
}
static void do_poll(int listenfd)
{
int new_fd;
struct pollfd clitfds[OPEN_MAX];
struct sockaddr_in client_addr;
socklen_t clitaddrlen=sizeof(client_addr);
int imax,i,nready;
//初始化客户端连接描述符
for(i=0;iimax)?i:imax;
if(--nready <= 0)
continue;
}
printf("connect success client num=%d\n",imax);
//处理与客户端的通信过程
handle_client(clitfds,imax);
}
}
static void handle_client(struct pollfd *connfds,int count)
{
int i,len;
char buf[MAXLEN];
bzero(buf,sizeof(buf));
//扫描整个文件描述符的集合状态,检测有无就绪的文件描述符
for(i=1;i<=count;i++)
{
if(connfds[i].fd<0)
continue;
//检测客户端文件描述符是否准备好
if(connfds[i].revents & POLLIN)
{
//接收客户端发送过来的消息
if((len=read(connfds[i].fd,buf,MAXLEN)) == 0)
{
close(connfds[i].fd);
connfds[i].fd=-1;
continue;
}
write(STDOUT_FILENO,buf,len); //输出到终端屏幕
//向客户端发送buf内容
write(connfds[i].fd,buf,len);
}
}
}
服务端程序说明:服务端有两个文件描述符,一个是监听客户端连接请求的文件描述符listen_fd,另一个是处理客户端读写操作的文件描述符new_fd,每当有新的客户端连接上来的时候,就将新的new_fd添加到pollfd结构体数组clientfds当中,同时受监控的文件描述符数目加1。
/**
客户端程序
*/
#include "socket_common.h"
//函数声明
int tcp_connect(const char *ip,int port);
static void handle_connection(int sockfd);
int main(int argc,char *argv[])
{
if(argc < 3)
{
printf("usage: ./poll_client ip port\n");
exit(-1);
}
int cfd=tcp_connect(argv[1],atoi(argv[2]));
//处理连接描述符
handle_connection(cfd);
return 0;
}
//用于客户端向服务器端发起连接
int tcp_connect(const char *ip,int port)
{
int cfd=socket(AF_INET,SOCK_STREAM,0);
if(cfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(struct sockaddr_in));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(port);
server_addr.sin_addr.s_addr=inet_addr(ip);
//将cfd连接到制定的服务器网络地址server_addr
if(connect(cfd,(struct sockaddr*)&server_addr,sizeof(struct sockaddr)) == -1)
{
perror("connect");
close(cfd);
exit(-1);
}
return cfd;
}
static void handle_connection(int sockfd)
{
char sendbuf[MAXLEN],recvbuf[MAXLEN];
struct pollfd pfds[2];
int len;
//添加连接描述符
pfds[0].fd=sockfd;
pfds[0].events=POLLIN;
//添加标准输入描述符
pfds[1].fd=STDIN_FILENO;
pfds[1].events=POLLIN;
for(;;) //循环处理
{
if(poll(pfds,2,-1) < 0)
{
perror("poll");
exit(-1);
}
//接收从服务器端发送过来的消息
if(pfds[0].revents & POLLIN)
{
if((len=read(sockfd,recvbuf,MAXLEN)) == 0)
{
fprintf(stderr,"client:server has closed!\n");
close(sockfd);
exit(-1);
}
write(STDOUT_FILENO,recvbuf,len); //标准输出
}
//测试标准输入是否准备好
if(pfds[1].revents & POLLIN)
{
if((len=read(STDIN_FILENO,sendbuf,MAXLEN)) == 0) //标准输入
{
shutdown(sockfd,SHUT_WR); //终止socket通信,关闭连接的写这一半
continue;
}
write(sockfd,sendbuf,len); //发送消息给服务器端
}
}
}
客户端程序说明:客户端程序设置了两个文件描述符,一个是用于监控来自服务端的可读数据;另一个是监控标准输入端的可读数据。poll函数监控这两个描述符的可读事件,可以看到,我们设置的超时条件是永久等待,在这两个描述符的可读I/O事件未就绪时,客户端进程将一直处于阻塞状态。
#第1种方式
all:poll_server poll_client
poll_server:poll_server.o
gcc poll_server.o -o poll_server
poll_client:poll_client.o
gcc poll_client.o -o poll_client
poll_server.o:poll_server.c
gcc -c poll_server.c -o poll_server.o
poll_client.o:poll_server.c
gcc -c poll_client.c -o poll_client.o
clean:
rm -rf ./*.o ./poll_server ./poll_client
该示例程序本人已经测试通过了的。
由于poll的多路复用机制仍然存在诸多问题,于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll。epoll 可以说是I/O多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题,比如:epoll 现在是线程安全的,epoll不仅会告诉描述符集中是否有描述符准备就绪,还会告诉你是哪个描述符准备就绪了,不用自己去找了。在下一篇博文中,会详细介绍epoll的用法。
IO多路复用之poll总结
《UNIX网络编程卷1:套接字联网API(第3版)》第6.10章节