I/O多路复用的实现机制 - poll 用法总结

一、基本知识

poll的多路复用机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询(polling),根据描述符的状态进行处理,但是poll没有最大文件描述符数量上的限制。

二、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标志的一些常值:

I/O多路复用的实现机制 - poll 用法总结_第1张图片

上图中分为了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函数返回前等待超时的时间,单位是毫秒数。下表给出了它的可能取值:

I/O多路复用的实现机制 - poll 用法总结_第2张图片

<说明>

(1)如果timeout > 0 或者 为负值(一般设置为-1)时,poll函数将会被阻塞,直到被监控的描述符指定的I/O事件准备就绪或者发生错误时,poll才会返回;或者定时器到时也会返回(在timeout>0的情况下)。

(2)timeout=0时,poll函数立刻返回,不阻塞进程,无论是否有描述符准备就绪。

【返回值】

1、成功,返回已就绪的描述符个数,即返回struct pollfd结构体中revents成员值非0的描述符个数;

2、若定时器到时之前没有任何描述符就绪,则返回0。

3、当发生错误时,返回值为-1,并设置相应的错误码给errno全局变量。错误码的可能取值如下:

  • EFAULT:fds指针指向的结构体数组的地址超出进程的地址空间。
  • EINTR:在请求事件发生前产生了一个信号事件。
  • EINVAL:nfds的值超出了RLIMIT_NOFILE 的值。
  • ENOMEM:没有多余的内存空间分配描述符表。

<说明> 如果我们不再关心某个特定描述符,那么可以把与它对应的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最大的区别就是poll没有最大描述符数量的限制,因此它仍然存在和select同样的缺陷。

(1)和select函数一样,poll同样需要维护一个用来存放描述符的数据结构,当描述符的数量比较大时,会使得用户空间和内核空间在传递该数据结构时复制开销大。

(2)poll 和 select一样,对描述符进行扫描的方式也是线性扫描,每次调用poll都需要遍历整个描述符集,不管那个描述符是不是活跃的,都需要遍历一遍。当描述符数量较多时,会占用大量CPU资源。

 (3)poll 和 select一样,不是线程安全的函数。

五、示例程序

程序描述:编写一个echo server程序,功能是客户端向服务器发送信息,服务器端接收数据后输出并原样返回给客户端,客户端接收到消息并输出到终端。代码如下:

  • 公共头文件:socket_common.h
#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
  • 服务端程序:poll_server.c
/**
服务器端程序
*/
#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。

  • 客户端程序:poll_client.c
/**
客户端程序
*/
#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事件未就绪时,客户端进程将一直处于阻塞状态。

  • Makefile
#第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章节

 

你可能感兴趣的:(计算机网络,#,网络I/O模型)