【Linux】select、poll、epoll 的区别以及他们的函数原型
目录
三、epoll(Linux独有)用户关注的文件描述符上的事件直接由内核记录
用来解决什么问题?
1、同时处理多个描述符;
2、监听多个描述符上有没有就绪事件。
I/O复用:一个进程或者一个线程能够同时对多个文件描述符(socket)提供服务
服务器上的进程或者线程,如何将多个文件描述符统一监听,当任意一个文件描述符上有事件发生,其都能及时处理。
套接字:是一种通信机制,凭借这种机制,客户/服务器系统的开发工作,既可以在本地上单击进行,又可以跨网络进行。
一、select
函数原型:
int select (int nfds,struct fd_set *readfds,struct fd_set *writefds,struct fd_set *execptfds,struct timeval* timeout);
nfds :监听的最大文件描述符值+1;(提高底层效率)
readfds,writefds ,execptfds:分别指向可读可写和可异常事件对应的文件描述符的集合;
timeout:设置select函数的超时时间;
返回值:>0,返回就绪的文件描述符数;
==0,超时;
==-1,出错。
问题:
1、如何将文件描述符分别设置到readfds,writefds,exceptfds.
按位设置。
2、select返回之后,如何知道哪些文件描述符就绪
select每次都会将所有的文件描述符返回。
描述符就绪的条件:
①满足下列条件之一,套接字准备好读
1)套接字接收缓冲区当中的数据字节数大于等于套接字接收缓冲区中设置的最小值(对于TCP和UDP默认值为1)
2)该链接的读半部关闭(也就是接收了FIN的TCP)
3)该套接字是一个监听套接字且已完成的连接数不为0,对于这样的套接字,accept通常不会阻塞;
4)其中有一个套接字错误处理;
②满足下列任意一个条件时,套接字准备好写
1)该套接字发送缓冲区中的可用空间的大小大于等于套接字发送缓冲区当中设置的最小值时,并且或者该套接字已经连接,或者套接字不需要连接(UDP);
2)该连接的写半部关闭;
3)使用非阻塞式的connect的套接字已建立连接,或者connect已经以失败告终;
4)其中有一个套接字待错误处理;
3、select的缺点
①每次调用select,都必须把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
②文件描述符就绪时,内核会修改readfds、writefds、execptfds结构,所以每次调用select之前,必须重新将文件描述符注册一遍;
③每次调用select都必须在内核遍历传递进来的所有fd,这个开销在fd很多时会很大(时间复杂度O(n));
每次都必须循环探测哪些文件描述符就绪(O(n));
调用前都必须重新设置结构体变量
④单个进程能够监视的文件描述符存在最大的限制。
二、poll
函数原型:
int poll(struct pollfd *fds,int nfds,int timeout);
struct pollfd
{
int fd; //用户关注的文件描述符
short events; //用户关注的事件类型
short revents; //由内核填充
}
fds:数组首地址,用户定义
poll和select函数相同,在一定时间内轮询关注的文件描述符,测试其是否就绪。
返回值: -1 出错;
0 超时;
>0 返回就绪文件描述符的个数。
1、优点
①将用户关注的文件描述符的事件单独表示,可关注更多的事件类型;
②将用户传递的和内核修改的分开,每次调用poll之前,不需要重新设置文件描述符;不按位表示,直接用int类型;
③poll函数没有最大文件描述符的限制。
2、缺点
①每次调用都需要将用户空间数组拷贝到内核空间;
②每次返回都需要将所有的文件描述符拷贝到用户空间数组中,无论是否就绪;
③返回的是所有的文件描述符,搜索就绪文件描述符的事件复杂度为O(n);
三、epoll(Linux独有)用户关注的文件描述符上的事件直接由内核记录
1、epoll函数的创建:
int epoll_create(int size);(创建内核事件表,由红黑树实现)
size只是给内核一个提示,告诉他需要多大的事件表。
2、epoll内核事件表的操作(设置添加、删除、修改)
int epoll_clt(int epfd,int op,struct epoll_event *event);
fd:要操作的文件描述符;
op:指定操作类型(向事件表中注册、修改、删除fd上的注册事件);
event:指定事件;
3、epoll系列调用的主要接口
int epoll_wait(int epfd,struct epoll_event*events,int maxevents,int timeout);
epfd:返回就绪文件描述符个数;
events:返回所有就绪的文件描述符;
maxevents:数组长度。
此函数如果检测到事件,就将所有的就绪事件从内核事件表中(由epfd中的参数指定)复制到它的第二个参数events指定的数组中,这个数组只输出epoll_wait检测到的就绪事件。所以搜索就绪文件描述符的时间复杂度为O(1).
特点:
①事件类型会更名;
②用户关注的事件由内核维护,每次调用epoll_wait时不需要将用户空间的数据拷贝到内核空间;
③每次epoll只会返回就绪的文件描述符;
④epoll内核实现比select、poll高效
select、poll采用轮询
epoll采用回调的方式
⑤epoll支持高效的ET模式;
⑥监视的描述符数量不受限制;
⑦IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。
epoll的LT模式(普通)和ET模式(高效)
LT模式(电平触发):在数据到达后,无论程序有没有接收,如果没有接收完,下一轮epoll_wait任然会提醒应用程序该描述符上有数据,直到数据被接收完。(一直提醒) (阻塞和非阻塞都可以)
ET模式(边沿触发)(必须使用非阻塞描述符):在数据到达后,无论程序有没有接收,如果没有接受完,都只提醒一次,下一轮不再提醒应用程序该描述符上有数据。(只能非阻塞)
为什么ET模式下只能设置非阻塞?
因为ET模式下,数据到达后,只提醒一次,想让数据读完,必须得循环读取。
ET模式下每次write或read需要循环write或read直到返回EAGAIN错误。以读操作为例,这是因为ET模式只在socket描述符状态发生变化时才触发事件,如果不一次把socket内核缓冲区的数据读完,会导致socket内核缓冲区中即使还有一部分数据,该socket的可读事件也不会被触发
若ET模式下使用阻塞IO,则程序一定会阻塞在最后一次write或read操作,因此说ET模式下一定要使用非阻塞IO
fcntl():可以设置非阻塞
三个函数的区别
解决的问题:
1、select、poll
1) select、poll每轮循环都需要从用户空间拷贝所有描述符和事件到内核空间;
2)内核以轮询的方式检查描述符上有没有事件产生,时间复杂度O(n);
3)select、poll 返回后,需要遍历所有描述符才能找到就绪的文件描述符,时间复杂度O(n).。
2、epoll
1)创建内核事件表,每个描述符只添加一次(不要每次都拷贝数据到内核中);
2)内核实现:注册回调函数方式实现,时间复杂度O(1);
3)直接返回就绪的文件描述符 ,时间复杂度O(1)。