前篇介绍的是关于I/O复用的方式之一select。见select
本文将及诶少第二种方式poll;
一、poll
1、函数原型
Int poll(struct pollfd *fds, int nfds, int timeout);
Fds:指定被监听的事件的集合;传的是数组首地址,由用户定义;大小是由nfds来确定;
nfds:指定被监听事件集合fds的大小,
Timeout:超时时间;
>0 就绪文件描述符的个数
-1 出错
0 超时
2、Poll和select的区别:
(1)用户关注的事件类型更多:select中只能利用readfds、writefd和excefds来记录这对应的三种事件,而poll可以利用pollfd中的events来设置事件;
(2)内核修改的和用户关注的分开表示,每次调用不需要重新设置;
(3)poll中文件描述符不再是按位设置,而是直接用int来设置;
用户关注的文件描述符的值可以更大;
用户关注的文件描述符的个数由用户数组决定,所以个数会更多,65535;
(4)Poll将用户关注的事件和内核修改的事件分开,每次调用都不需要重置;
(5)Poll和select返回的都是就绪和未就绪的,遍历就绪的文件描述符时间复杂度为O(n)。用户程序依旧需要循环检测哪些文件描述符就绪。
3、poll中用户关注的事件类型
红色部分是我们常用的几种事件。
4、poll的优缺点:
优点:
将用户关注的文件描述符的事件单独表示,可关注更多的事件类型;
将用户传递的和内核修改的分开,每次调用poll之前,不需要重新设置;
Poll函数没有最大文件描述符的限制;
缺点:
每次调用都需要将用户空间数组拷贝到内核中;
每次返回都需要将所有的文件描述符拷贝到用户空间数组中,无论是否就绪;
返回的是所有的文件描述符,搜索就绪文件描述符的时间复杂度为O(n);
二、poll服务器端的简单实现
1、代码
#define _GNU_SOURCE
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<poll.h>
#define SIZE 100
void Init_fds(struct pollfd *fds)//初始化fds结构
{
int i = 0;
for(;i < SIZE;++i)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
void Insert_fd(struct pollfd*fds,int fd,short event)//添加文件描述符以及对应的事件
{
int i = 0;
for(;i < SIZE;++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = event;
break;
}
}
}
void Delete_fd(struct pollfd *fds,int fd)//删除文件描述符以及对应的事件
{
int i = 0;
for(;i < SIZE;++i)
{
if(fds[i].fd == fd)
{
fds[i].fd = -1;
fds[i].events = 0;
break;
}
}
}
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
listen(sockfd,5);
struct pollfd fds[SIZE];
Init_fds(fds);
Insert_fd(fds,sockfd,POLLIN);
while(1)
{
int n = poll(fds,SIZE,-1);
if(n <= 0)
{
printf("poll error\n");
continue;
}
//有文件描述符就绪
int i = 0;
for(;i < SIZE;++i)//遍历数组
{
if(fds[i].fd != -1) //判断就绪
{
int fd = fds[i].fd;
if(fds[i].revents & POLLRDHUP)
{
printf("%d will close\n",fd);
close(fd);
Delete_fd(fds,fd);
}
else if(fds[i].revents & POLLIN)
{
if(fd == sockfd)//链接事件
{
int len = sizeof(cli);
int c = accept(fd,(struct sockaddr*)&cli,&len);
if(c < 0)
{
continue;
}
Insert_fd(fds,c,POLLIN | POLLRDHUP);
}
else//接收数据
{
char buff[128] = {0};
recv(fd,buff,127,0);
printf("%d,%s\n",fd,buff);
send(fd,"OK",2,0);
}
}
}
}
}
return 0;
}
2、结果展示:
小结poll:
poll相比select它能够处理的事件类型更多,范围更大,并且poll是将用户关注的事件和内核修改的事件分开。每次调用都不需要重置。
当有事件来临时,poll在用户态将事件的fd以及用户关注的事件插入到fds结构体数组中的某一个元素下。然后将数组fds(其中包括就绪的和未就绪的事件)拷贝到内核态中,内核识别具体所作的事件并且进行处理,处理完成后内核对对应fd中的revent进行修改,然后将修改过的所有的fds(包括就绪的和未就绪的事件)拷贝到用户态中。用户程序检测就绪文件描述符的时间复杂度为O(N)。这一次事件的执行就到此结束。