持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情
前言
学到这,感觉没什么可说的,poll 是对select 的改进,但是它是个半成品,相对 select 提升不大。更多的是把poll当成一个过渡,大家并没有给poll过多的关注,而是直接epoll了。所以着重解释了从select到poll改变了什么,思路的变化,到如何使用上其实就是调函数,也没有太多细节要注意。
回顾上文select
上节通过解释和代码,我们可以总结select的缺点主要有:
- select函数的监听事件的文件描述符集合参数,是传入传出参数,导致每次调用select都要重新设置。
- 由于传入传出,fdset的位图bitmap反复产生用户态和内核态之间的拷贝,这需要大量的开销。
- 传出的位图内容改变了,但是大小没有变化。select函数返回后,是通过遍历fdset,找到就绪的描述符fd。
- 最主要是select关心的文件描述符是有上限的(底层是位图,有固定大小,在linux下,bitmap的长度不能超过1024,虽然可以调整,但是描述符数量越大,效率越低,调整的意义不大。)
poll的改进
解决了select的两件事情:
- poll以链表的形式存储文件描述符,解决了文件描述符的数量限制。
- poll的数据结构,将监听事件集合和返回事件集合分离,所以每次循环都不需要重新设置。 其实就是指struct pollfd结构体里的events和revents。
poll
poll也是操作系统提供的系统调用函数,机制与select类似,管理多个描述符也是进行轮询,即每次调用都要扫描整个注册文件描述符的集合,并将其中就绪的文件描述符返回给用户程序,因此他们检测就绪事件的算法的时间复杂度为O(n)。
pollfd结构体
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
- fd:待监听的文件描述符
- events:用户告诉内核,需要监听该文件描述符的哪些事件。
- revents:内核告诉用户,监听到该文件描述符的哪些事件就绪。
poll支持的事件类型如下表,其中,可以在events中填充的事件类型,如果关注多种事件类型,可以使用按位或将这些事件合到一起:
poll函数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
- fds:struct pollfd类型的数组,用来传递用户关注的文件描述符以及事件类型,每一个元素中, 包含了三部分内容:
文件描述符
,监听的事件集合
,返回的事件集合
。 - nfds:表示fds数组的长度
- timeout:poll函数的超时时间, 单位是毫秒(ms)。设置为0,表示poll非阻塞等待;设置为-1,表示阻塞等待;设置大于0,则表示阻塞等待一次的时间。超过时间后,会超时返回,跟select一样。
返回值
返回值和select的完全一样。
- 返回0:超时,表示超出设置的timeout时间后还没有文件描述符就绪。
- 返回-1:发生错误,并设置 errno 为下列值之一。
- EBADF:一个或多个结构体中指定的文件描述符无效。
- EFAULT:fds 指针指向的地址超出进程的地址空间。
- EINTR:请求的事件之前产生一个信号,调用可以重新发起。
- EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
- ENOMEM:可用内存不足,无法完成请求。
- 返回大于0的值:表示poll由于监听的文件描述符就绪而返回,返回就绪文件描述符的总数。
poll服务器实现思路
- 首先
lfd=socket()创建TCP套接字
,bind()绑定端口号
,对TCP套接字进行listen()
,这时就会得到一个监听套接字lfd
; - 创建
pollfd
结构体数组cfds[OPEN_MAX] ,把对lfd
的读事件监听添加到数组的第一个元素
,并用-1初始化cfds[OPEN_MAX]里剩下元素的fd; - 进行poll()函数阻塞轮询,根据
不同的返回值
,进行不同的处理:- 0:timeout超时,继续循环回去轮询监听;
- -1:出错返回,根据errno设置处理方式,或exit退出,或continue;
- 大于0:说明这时有事件就绪或者有新连接到来。
- 如果返回值大于0,对cfds[]数组进行判断操作:
- 先判断
cfds[0].revents & POLLIN
,为真则处理客户端链接请求,cfd=accept(),此时进行accept不会被阻塞,并将cfd添加到cfds[]中的未使用位置,并设置监听读事件; - 再判断其他文件描述符
cfds[i].revents & POLLIN
,为真则为读事件,有客户端发来了数据,这时就能对客户端发来的数据进行read,read(cfds[i].fd, buf, sizeof(buf))),这时的read也不会阻塞。
- 先判断
poll总结
优点
(1)poll()的nfds参数,不要求像select那样计算最大文件描述符加一的大小。
(2)poll没有最大连接数的限制,原因是它是基于链表来存储的(但是数量过大后性能也是会下降)。
(3)pollfd结构包含了要监视的event和就绪的revent,不再使用select"参数-值"传递的方式,在调用函数时,只需要对参数进行一次设置就好了。
(4)优化了编程接口。select()函数有5个参数,而poll()减少到了3个参数。
缺点
- 内存拷贝开销,仍然如select一样的,避免不了调用poll时用户到内核,poll返回时内核到用户的数据拷贝。包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
系统调用函数的执行是发生在内核区的,而用户程序的执行是发生在用户区的,所以会存在内核区与用户区之间的内存复制的系统开销。
- 遍历问题,与select一样,poll返回后,需要
需要通过遍历获取就绪事件的文件描述符
,它的开销随着文件描述符数量的增加而线性增大。 - 时间复杂度,虽然没有数量上限,但同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
与select一样,每次poll系统调用时,需要在内核遍历传入的整个文件描述符集合,逐个检测,查看是否有就绪的文件描述符,然后返回就绪文件描述符的个数。也就是说,poll也是线性扫描的方式,当注册的文件描述符fd的数量很多时,效率会较低,时间复杂度为O(n)。
- 工作模式,poll 只能工作在水平触发(LT)模式下。
poll代码
//
// Created by 11406 on 2022/5/26.
//
#include <string.h>
#include <strings.h>
#include<netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <poll.h>
#include "wrap.h"
#define PORT 6266
#define OPEN_MAX 1024
int main(int argc,char *argv[]){
int lfd,cfd;
int maxi,i,nready,ret;
char buf[BUFSIZ],clie_ip[INET_ADDRSTRLEN];
struct pollfd client[OPEN_MAX];
struct sockaddr_in srv_addr,clt_addr;
socklen_t clt_addr_len;
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family=AF_INET;
srv_addr.sin_port= htons(PORT);
srv_addr.sin_addr.s_addr= htonl(INADDR_ANY);
lfd= Socket(AF_INET,SOCK_STREAM,0);
int opt=1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt, sizeof(opt));
Bind(lfd,(struct sockaddr*)&srv_addr, sizeof(srv_addr));
Listen(lfd,128);
client[0].fd=lfd;
client[0].events=POLLIN;
//client[0].revents=0;
maxi=0; /* client[]数组有效元素中最大元素下标 */
for(i=1;i<OPEN_MAX;i++){
client[i].fd=-1;
}
while(1){
nready= poll(client,maxi+1,-1);
/*if(nready==-1){
if(errno == EINTR){
continue;;
} else{
sys_err("poll error");
}
}*/
if(client[0].revents & POLLIN) {
clt_addr_len = sizeof(clt_addr);
cfd = Accept(lfd, (struct sockaddr *) &clt_addr, &clt_addr_len);
printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &(clt_addr.sin_addr.s_addr), clie_ip, sizeof(clie_ip)), ntohs(clt_addr.sin_port));
for (i = 1; i < OPEN_MAX; i++) {
if (client[i].fd == -1) {
client[i].fd = cfd;
client[i].events = POLLIN;
client[i].revents = 0;
break;
}
}
if (i == OPEN_MAX) {
sys_err("too many clients");
}
if (i > maxi) {
maxi = i;
}
if (--nready == 0) {
continue;
}
}
for (i = 1; i <= maxi; i++) {
if (client[i].fd == -1) {
continue;
}
if (client[i].revents & POLLIN) {
ret = read(client[i].fd, buf, sizeof(buf));
printf("----------%d\n", ret);
if (ret == 0) {
Close(client[i].fd);
client[i].fd == -1;
} else if (ret < 0) { //ECONNRESET
sys_err("read");
} else {
//write(STDOUT_FILENO,buf,ret);
for (int j = 0; j < ret; j++) {
buf[j] = toupper(buf[j]);
}
write(STDOUT_FILENO, buf, ret);
write(client[i].fd, buf, ret);
}
if (--nready <= 0) { //有事件,且处理了事件才减1!!!
break;
}
}
}
}
Close(lfd);
return 0;
}