在上一篇文章中,我们详细的介绍了I/O复用技术中的select使用。这篇文章我们来主要介绍一下poll. poll系统调用和select类似,也是在指定事件内轮询一定数量的文件描述符,以测试其中是否有就绪的 本质都是统一监听,如果任意一个文件描述符上有关注的事件发生。则这个文件描述符以及发生的事件通知给应用程序。
1、函数原型
int poll(struct pollfd fds[],int len,int timeout);参数1:struct pollfd fds[]参数 1.1结构体描述 fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件,pollfd结构体的定义如下:
Struct pollfd { int fd;//用户设置关注的文件描述符(用户填充),不是二进制了,是int类型 Short events;//用户关注的事件类型(用户填充) Short revents;//由内核填充,就绪的事件类型 }我们每次调用poll之前要定义这个结构体,并且用户事件描述符对fd,events进行初始化。返回时就绪事件就在revents里面存储。具体的对结构体的操作如下图所示: 从这个结构中我们可以看到用户初始描述符和内核返回的就绪描述符是分开的,所以不用每次初始化它,初始化一次就可。但我们仍然不知道哪几个描述符就绪,只知道有几个就绪。
1.2用户关注的事件类型 我们知道Select通过三个参数传递只能关注三种事件类型。而poll能够关注的时间类型就较多。 每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件。
其中,POLLIN、POLLOUT和POLLRDHUP是常用的。POLLRDHUP是专门捕捉客户端断开异常的,我们在select时通过recv<=0来判断客户端是否关闭,在这专门提供了函数。
参数2:int len参数 主要指参数一pollfd结构类型的数组的长度。poll没有空间限制,可以存放很多很多个,不像select只能存储1024个。
参数3:timeout参数 定时时间,在一定时间内没有检测到就绪事件,返回0,为NULL表示永久阻塞。
优点:
在pollfd结构体中我们可以明确的看到。poll的文件描述符是通过整型值去记录的。所以其能处理的文件描述符的个数就比select要大得多。而且是由用户自己指定的而select中的fd_set就只有系统内核提供的1024位。所以在应付大数目的文件描述符的时候相比于select速度更快。因为poll将用户关注的和内核填充的分开可以,用户关注的事件类型是不会变的,所以初始化一次就可以。相比于select关注的事件类型更多缺点:
大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义,效率低下。与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符代码的实现和select类似,但是主要有以下值得注意的点。
select每次要初始化结构体,因为用户和内核共用一个空间。poll不用,一次初始化,每次只用对结构体中的fd,events成员进行一次初始化,记录用户事件描述符,有新连接,添加到结构体中,连接断开,从结构体中删除。内核返回的就绪事件在revents中放着。select在处理事件时比较简单,只需要判断是连接(添加进去)还是数据连接(处理数据)。而poll比较复杂,它需要判断监听,数据。数据连接时需要判断三种情况:没有事件发生;客户端自动断开连接事件发生;可读事件发生那我们现在实现客户端连接服务器,发送数据,服务器收到给客户端回复ok。程序流程图参见博客poll详解
#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 SIZE 100 int CreateSocket() { int listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listenfd == -1) return -1; struct sockaddr_in ser_addr; memset(&ser_addr, 0, sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(6000); ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr)); if(res == -1) return -1; res = listen(listenfd, 5); if(res == -1) return -1; return listenfd; } void InitFds(struct pollfd *fds) { int i = 0; for(; i < SIZE; ++i) { fds[i].fd = -1; } } void AddFd(struct pollfd *fds, int fd) { int i = 0; for(; i < SIZE; ++i) { if(fds[i].fd == -1) { fds[i].fd = fd; fds[i].events = POLLIN | POLLRDHUP;//事件类型设置上去 但是针对于listenfd是不要POLLRDHUP这个宏。所以在主函数中要进行处理 break; } } } void DelFd(struct pollfd *fds, int fd) { int i = 0; for(; i < SIZE; ++i) { if(fds[i].fd == fd) { fds[i].fd = -1; break; } } } void GetNewLink(struct pollfd *fds, int listenfd) { struct sockaddr_in cli_addr; socklen_t len = sizeof(cli_addr); int c = accept(listenfd, (struct sockaddr*)&cli_addr, &len); if(c == -1) return; printf("%d link success\n", c); AddFd(fds, c); } void DealOneClientData(struct pollfd *fds, int fd) { char buff[128] = {0}; int n = recv(fd, buff, 127, 0); if(n <= 0) { close(fd); DelFd(fds, fd); return; } printf("%d: %s\n", fd, buff); send(fd, "OK", 2, 0); } void DealReadyEvent(struct pollfd *fds, int listenfd) { int i = 0; for(; i < SIZE; ++i) { if(fds[i].fd == -1) continue;//不是有效的文件描述符 //看这个文件描述符所关注的事件有没有发生。一般POLLRDHUP事件发生则POLLIN事件也一定发生 //策略:先判断文件描述符再去判断事件类型 if(fds[i].fd == listenfd && fds[i].revents & POLLIN) { GetNewLink(fds, listenfd); } else if(fds[i].revents & POLLRDHUP)//这种事件类型发生代表是普通的第二类文件描述符 { close(fds[i].fd); fds[i].fd = -1; printf("one client unlink\n"); } else if(fds[i].revents & POLLIN)//有数据到达 { DealOneClientData(fds, fds[i].fd); } } } int main() { int listenfd = CreateSocket(); assert(listenfd != -1); struct pollfd fds[SIZE]; InitFds(fds);//初始化的时候只有一个listendfd在里面 //针对listenfd文件类型要进行单独设置 fds[0].fd = listenfd; fds[0].events = POLLIN; while(1) { int n = poll(fds, SIZE, -1); assert(n != -1); if(n == 0)//超时的 { printf("time out\n"); continue;//继续监听 } DealReadyEvent(fds, listenfd);//处理就绪事件 } close(listenfd); exit(0); }