欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

select poll epoll分析与区别

程序员文章站 2022-06-14 09:02:10
...

select 、poll、 epoll三者的区别

操作系统在处理io的时候,主要有两个阶段:

  • 等待数据传到io设备
  • io设备将数据复制到user space

我们一般将上述过程简化理解为:

  • 等到数据传到kernel内核space
  • kernel内核区域将数据复制到user space(理解为进程或者线程的缓冲区)
      select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select

函数原型及其参数讲解

//头文件
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
//函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);//将fd从set中清除出去

int FD_ISSET(int fd, fd_set *set);//判断fd是否在集合中

void FD_SET(int fd, fd_set *set);//将fd设置到集合中去;

void FD_ZERO(fd_set *set); //将set清空成0;
1.参数1int nfds 所监听的所有文件描述符中,最大的文件描述符+12.参数234分别对应
所监听的文件描述符“可读"事件;readfds()
所监听文件描述符"可写"事件;writefds()
所监听文件描述符"异常“事件;
timeval:轮训时间,到时select返回;
返回值:成功返回所监听的所有的监听集合中,满足条件的总数;
失败:-1 并且设置errno

fd_set *readfds, fd_set *writefds,fd_set *exceptfds这三个参数是传入传出参数;例如有lfd ,fd1 ,fd2,fd3;fd_set readfds; FD_ZERO(&readfds);//清空整个readfds; FD_SET(lfd,&readfds); FD_SET(fd1,&readfds); FD_SET(fd2,&readfds); FD_SET(fd3,&readfds); 此时readfds中lfd,fd1,fd2,fd3标志位都变成1; 然后调用select后返回,假设lfd,fd3有变化,这时readfds集合中fd1和fd2会清理变成0,lfd和fd3还是1,所以才能使用FD_ISSET判断返回的描述符是否在集合中; FD_ISSET(lfd,&readfds);因为lfd在,所以返回值是1; FD_ISSET(fd1,&readfds);因为fd1不在,所以返回值是0; select函数缺点:1文件描述符上限,因为一个进程能打开的最大文件是1024个,所以select最大能监听的的文件描述符是1024; 2.当select监听的描述符比较少的时候可以构建一个数组,把要监听的描述符放进数组,然后遍历素组中的每一个数据; 3.监听集合和满足条件的监听集合都是同一个,所以每次使用都要将原有集合保存;

  struct timeval {
    time_t         tv_sec;     /* seconds */
    suseconds_t    tv_usec;    /* microseconds */
  };

进程与kernel之间用select系统调用

单个进程就可以同时处理多个网络连接的io请求(同时阻塞多个io操作)。基本原理就是程序呼叫select,然后整个程序就阻塞状态,这时候,kernel内核就会轮询检查所有select负责的文件描述符fd,当找到其中那个的数据准备好了文件描述符,会返回给select,select通知系统调用,将数据从kernel内核复制到进程缓冲区(用户空间)。

select poll epoll分析与区别
下图为select同时从多个客户端接受数据的过程

虽然服务器进程会被select阻塞,但是select会利用内核不断轮询监听其他客户端的io操作是否完成
select poll epoll分析与区别

原理概述

select 的核心功能是调用tcp文件系统的poll函数,不停的查询,如果没有想要的数据,主动执行一次调度(防止一直占用cpu),直到有一个连接有想要的消息为止。从这里可以看出select的执行方式基本就是不同的调用poll,直到有需要的消息为止。

select的几大缺点

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

select的几大优点

(1)select的可移植性更好,在某些Unix系统上不支持poll()。

(2)select对于超时值提供了更好的精度:微秒,而poll是毫秒。

poll函数

函数参数及其参数讲解

poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息

#include <poll.h>

int poll (struct pollfd fd[], nfds_t nfds, int timeout)

参数
1)第一个参数:一个结构体数组, struct pollfd结构如下:

struct pollfd {
	int fd;//文件描述符
	short events;//请求的事件
	short revents;//返回的事件
}

eventsrevents是通过对代表各种事件的标志进行逻辑或运算构建而成的。events包括要监视的事件,poll用已经发生的事件填充revents。poll函数通过在revents中设置标志POLLHUPPOLLERRPOLLNVAL来反映相关条件的存在。不需要在events中对于这些标志符相关的比特位进行设置。如果fd小于0, 则events字段被忽略,而revents被置为0.标准中没有说明如何处理文件结束。文件结束可以通过revents的标识符POLLHUN或返回0字节的常规读操作来传达。即使POLLINPOLLRDNORM指出还有数据要读,POLLHUP也可能会被设置。因此,应该在错误检验之前处理正常的读操作。

		**poll函数的事件标志符值**

常量 说明
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件

注意:后三个只能作为描述字的返回结果存储在revents中,而不能作为测试条件用于events中。
  2)第二个参数nfds:要监视的描述符的数目。
  3)最后一个参数timeout:是一个用毫秒表示的时间,是指定poll在返回前没有接收事件时应该等待的时间。如果 它的值为-1,poll就永远都不会超时。如果整数值为32个比特,那么最大的超时周期大约是30分钟。

timeout值 说明
INFTIM 永远等待
0 立即返回,不阻塞进程
>0 等待指定数目的毫秒数

区别

poll的原理与select非常相似,差别如下:

  1. 描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制
  2. poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。

细谈事件驱动–>epoll

epoll_create

函数原型
int epoll_create(int size);//创建epoll例程
参数解析

size:创建例程的数量,只作为操作系统参考的数值,最终创建例程的数量取决于操作系统

  返回值:创建成功-----返回epoll例程的文件描述符; 失败------返回-1

epoll_ctl

函数原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //epfd例程对文件描述符进行操作
参数解析

epfd:注册监视epoll例程的文件描述符,即epoll_create的返回值

  op:监视对象的增加、删除或更改等操作

1。 EPOLL_CTL_ADD //将文件描述符注册到epoll例程
2.EPOLL_CTL_DEL //从例程中删除文件描述符
3. EPOLL_CTL_MOD //更改注册的文件描述符

  fd:需要注册的套接字文件描述符

  event:监视对象的事件类型

struct epoll_event { 
    uint32_t events; //注册监听事件的类型 EPOLLIN--读取数据   EPOLLET--边缘触发得到事件通知
    epoll_data_t data; //存储监听文件描述符的相关信息
}; 
 
typedef union epoll_data { 
    void *ptr; 
    int fd; //套接字文件描述符
    uint32_t u32; 
    uint64_t u64; 
} epoll_data_t;

epoll_wait

函数原型

```c
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数解析

epfd:创建例程的文件描述符

events:保存发生事件的文件描述符集合的地址,跟epoll_ctl函数的参数event不是一个值

maxevents:保存最大事件数

timeout:毫秒为单位,传递-1代表一直等待注册事件的发生

返回值:成功----返回0; 失败—返回0

I/O复用模型epoll执行流程

创建例程create_poll——>在例程中注册套接字文件描述符的操作epoll_ctl——>等待监听事件的发生epoll_wait

epoll解决的问题

  1. epoll没有fd数量限制。
  2. epoll没有这个限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右。
  3. epoll不需要每次都从用户空间将fd_set复制到内核kernel,epoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次。
  4. select 和 poll 都是主动轮询机制,需要遍历每一个人fd;
    epoll是被动触发方式,给fd注册了相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。
    虽然epoll。poll。epoll都需要查看是否有fd就绪,但是epoll之所以是被动触发,就在于它只要去查找就绪队列中有没有fd,就绪的fd是主动加到队列中,epoll不需要一个个轮询确认。
    换一句话讲,就是select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动轮询一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用轮询。

总结

  1. select, poll是为了解決同时大量IO的情況(尤其网络服务器),但是随着连接数越多,性能越差
  2. epoll是select和poll的改进方案,在 linux 上可以取代 select 和 poll,可以处理大量连接的性能问题

借鉴
https://blog.csdn.net/czyaun/article/details/102842012

https://www.cnblogs.com/sunweiye/p/11172751.html