select poll epoll分析与区别
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.参数1:int nfds 所监听的所有文件描述符中,最大的文件描述符+1;
2.参数2,3,4分别对应
所监听的文件描述符“可读"事件;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同时从多个客户端接受数据的过程
虽然服务器进程会被select阻塞,但是select会利用内核不断轮询监听其他客户端的io操作是否完成
原理概述
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;//返回的事件
}
events
和revents
是通过对代表各种事件的标志进行逻辑或运算构建而成的。events
包括要监视的事件,poll
用已经发生的事件填充revents
。poll函数通过在revents
中设置标志POLLHUP
、POLLERR
和POLLNVAL
来反映相关条件的存在。不需要在events
中对于这些标志符相关的比特位进行设置。如果fd
小于0, 则events
字段被忽略,而revents
被置为0.标准中没有说明如何处理文件结束。文件结束可以通过revents
的标识符POLLHUN
或返回0字节的常规读操作来传达。即使POLLIN
或POLLRDNORM
指出还有数据要读,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非常相似,差别如下:
- 描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制
- 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解决的问题
- epoll没有fd数量限制。
- epoll没有这个限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右。
- epoll不需要每次都从用户空间将fd_set复制到内核kernel,epoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次。
- 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的编号,所以直接找到就可以,不用轮询。
总结
- select, poll是为了解決同时大量IO的情況(尤其网络服务器),但是随着连接数越多,性能越差
- epoll是select和poll的改进方案,在 linux 上可以取代 select 和 poll,可以处理大量连接的性能问题
借鉴
https://blog.csdn.net/czyaun/article/details/102842012
https://www.cnblogs.com/sunweiye/p/11172751.html