【I/O多路复用】poll系统调用

文章目录

【1】前言

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它再次遍历fd。

【2】poll系统调用函数原型

# include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

fds参数:一个pollfd结构类型的数组,指定所有我们希望监听的所有文件描述符上发生的可读、可写和异常等事件。

nfds参数:指定指定被监听事件集合fds的大小

timeout参数:指定poll的超时值,单位是毫秒。
timeout 为-1时: poll调用将永远阻塞,直到某个事件发生
timeout为0时:poll调用将立即返回

 typedef  unsigned  long int  nfds_t;

pollfd结构体定义如下:

struct pollfd 
{
	int fd;         	/* 文件描述符 */
	short events;       /* 注册的事件 */
	short revents;      /* 实际发生了的事件,由内核填充*/
}; 

其中fd成员指定文件描述符;
events成员告诉poll监听fd上那些事件,他是一系列事件的按位或;
revents成员由内核修改,用来通知应用程序fd上哪些事件真实发生了。
如果events上的事件发生了,revents就会将自己与events的对应位置为1

poll支持的时间类型如下:

事件 描述
POLLIN 有数据可读
POLLRDNORM 有普通数据可读
POLLRDBAND 有优先数据可读
POLLPRI 有紧迫数据可读
POLLOUT 写数据不会导致阻塞
POLLWRNORM 写普通数据不会导致阻塞
POLLWRBAND 写优先数据不会导致阻塞
POLLMSGSIGPOLL 消息可用

revents域中可能返回下列事件:

事件 描述
POLLER 指定的文件描述符发生错误
POLLHUP 指定的文件描述符挂起事件
POLLNVAL 指定的文件描述符非法

返回值

  • 大于0:表示数组fds中有socket描述符的状态发生变化,或可以读取、或可以写入、或出错。并且返回的值表示这些状态有变化的socket描述符的总数量;此时可以对fds数组进行遍历,以寻找那些revents不空的socket描述符,然后判断这个里面有哪些事件以读取数据。
  • 等于0:表示没有socket描述符有状态变化,并且调用超时。
  • 小于0:此时表示有错误发生,此时全局变量errno保存错误码。

【3】程序示例

下面是一个基于TCP的多客户端服务器交互的程序:

【1】建立三次连接后若client客户端不发送数据,则服务器端会在超时时间5秒内轮询遍历fds数组等待client客户端发送数据,超时时间timeout设置为5秒,若5秒内没有数据发送到服务器则会打印出"time out"。
【2】直到client发送数据,内核会将该事件的fds[i].revevts修改,poll则会进行遍历fds数组找到fds[i].revents异或有数据可读POLLIN为真的事件进行处理。
【3】这时有两种情况,若fds[i].fd == sockfd说明监听队列中有连接待处理,则使用accept拿出一个连接。否则,没有新连接产生,是有客端发来了数据,我们直接使用recv接收客端数据,并打印收到的数据。

server.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<poll.h>

#define MAXFD 10
int create_sockfd();

//向fds数组中添加文件描述符fd,指定关注的事件
void fds_add(struct pollfd fds[],int fd)
{
	int i=0;
	for(;i<MAXFD;++i)
	{
		if(fds[i].fd==-1)
		{
			fds[i].fd=fd;
			fds[i].events=POLLIN;
			fds[i].revents=0;
			break;
		}
	}

}
//在fds数组中删除指定文件描述符fd及其相关事件信息
void fds_del(struct pollfd fds[],int fd)
{
	int i=0;
	for(;i<MAXFD;++i)
	{
		if(fds[i].fd==fd)
		{
			fds[i].fd=-1;
			fds[i].events=0;
			fds[i].revents=0;
			break;
		}
	}
}
//初始化fds数组,其中文件描述符fd置为-1,其余都置为0
void fds_init(struct pollfd fds[])
{
	int i=0;
	for(;i<MAXFD;++i)
	{
		fds[i].fd=-1;
		fds[i].events=0;
		fds[i].revents=0;
	}
}
int main()
{
	int sockfd=create_sockfd();
	assert(sockfd!=-1);
	
	//初始化pollfd类型的结构体数组fds
	struct pollfd fds[MAXFD];

	//初始化fds数组
    fds_init(fds);
	
	//将sockfd添加到fds数组中
	fds_add(fds,sockfd);

	while(1)
	{
		//使用poll系统调用轮询,测试其中是否有就绪者
		int n=poll(fds,MAXFD,5000);
		if(n==-1)
		{
			perror("poll error");
		}
		else if(n==0)	//返回0表示超时,没有就绪者
		{
			printf("time out\n");
		}
		else	//fds数组存在就绪的文件描述符
		{
			int i=0;
			for(;i<MAXFD;++i)
			{
				if(fds[i].fd==-1)
				{
					continue;
				}
				
				//可获得是哪个文件描述符上就绪, fds[i]中的成员revents由系统内核修改
				if(fds[i].revents &POLLIN)
				{
					//这时候分两种情况
					//若fds[i].fd == sockfd说明监听队列中有连接待处理
					//则使用accept接收连接
					//否则,没有新连接产生,是已有客户端发来了数据
					//我们直接使用recv接收客端数据,并打印收到的数据
					if(fds[i].fd==sockfd)
					{
						struct sockaddr_in caddr;
						int len=sizeof(caddr);
						
						//接收一个套接字已建立的连接,得到连接套接字c
						int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
						if(c<0)
						{
							continue;
						}
						printf("accept c=%d\n",c);
						//将新的连接套接字c,添加到pollfd结构类型的数组fds
						fds_add(fds,c);//join
					}
					else
					{
						char buff[128]={0};
						
						//使用recv来接收客端数据
						int res=recv(fds[i].fd,buff,127,0);
						
						//接收服务器端的数据是零,即n返回0,说明客户端已经关闭
						if(res<=0)
						{
							//关闭文件描述符fds[i].fd
							close(fds[i].fd);
							//从fds数组中删除此文件描述符 
							fds_del(fds,fds[i].fd);
							printf("one client over\n");
						}
						else
						{
							//n不为0,即接收到了数据,那么打印数据,并向客端回复一条信息
							printf("recv(%d)=%s\n",fds[i].fd,buff);
							send(fds[i].fd,"ok",2,0);
						}
					}
				}
			}
		}
	}
}

int create_sockfd()
{
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(sockfd==-1)
	{
		return -1;
	}

	struct sockaddr_in saddr;
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family=AF_INET;
	saddr.sin_port=htons(6000);
	saddr.sin_addr.s_addr=inet_addr("127.0.0.1");

	int res=bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
	if(res==-1)
	{
		return -1;
	}

	listen(sockfd,5);

	return sockfd;
}



client.c

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main()
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1 );

	struct sockaddr_in saddr;
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);
	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//向服务器发起链接
	assert(res != -1);

	while(1)
	{
		char buff[128] = {0};
		printf("Please Input:");
		fgets(buff,128,stdin);
		if(strncmp(buff,"end",3) ==0 )
		{
			break;
		}
		send(sockfd,buff,strlen(buff),0);
		memset(buff,0,128);
		recv(sockfd,buff,127,0);
		printf("RecvBuff:%s\n",buff);
        printf("\n");
	}
	close(sockfd);
}

运行结果如下:
当程序没有输入时,每过5秒超时时间就会打印time out。
【I/O多路复用】poll系统调用【I/O多路复用】poll系统调用

【4】poll的优缺点

poll的优点:

  • poll() 不要求开发者计算最大文件描述符加一的大小。
  • poll() 在应付大数目的文件描述符的时候相比于select速度更快
  • 它没有最大连接数的限制,原因是它是基于链表来存储的

poll的缺点:

  • 与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符这样会使性能下降 。

  • 虽然poll突破了最大监听1024个文件描述符的限制,但它还是存在缺点就是当大量连接上只有少量活跃的连接时,它也是采用轮询的机制每次还要遍历整个数组

上一篇:使用读写管道作为事件通知的C++实现


下一篇:基于PostGIS的高级应用(5)-- Polygon Spliting