0%

io多路复用

水平触发(Level Triggered):将就绪的fd告诉进程后,若进程没有对其进行IO操作,则下次执行检测时会再次报告这些fd,select、poll、epoll均支持;

边缘触发(Edge Triggered):只告诉一遍,下次就不在告诉了,仅epoll支持;

select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <sys/time.h>  
#include <unistd.h>
/*
参数:
maxfd:一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
rdset、wrset、exset:要监视的读、写、异常文件描述符的集合
timeout:超时时间
NULL:将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
0:不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
>0:等待的超时时间,即在timeout时间内阻塞,之内有事件到来就返回了,否则在超时后返回,返回值同上述
返回值:
<0:错误,并设置errorno
0:等待超时
>0:可读写或异常的文件描述符的个数
fd_set
是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读,其中每一bit对应一个文件描述符id,
FD_SET(int fd, fd_set *fdset); //将fd加入set集合
FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何fd
FD_SETSIZE,它是数据类型fd_set的描述字数量,其值通常是1024
fd_set的大小决定了select可操作fd的大小;
fd的最大值必须<FD_SETSIZE
为方便理解,去fd_set为1字节长度即8bit:
1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
3)若再加入fd=2,fd=1,则set变为0001,0011
4)执行select(6,&set,0,0,0)阻塞等待
5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。


*/
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
//select返回时会将处理rdset、wrset、exset,即将未有时间发生的fd从fd_set中清除;

思路

对所有的fd进行线性扫描,即轮询一次

缺点

  1. 单个进程可监视的fd数量被限制,即能监听端口的大小有限

    一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max查看。32位机默认是1024个。64位机默认是2048

  2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低

    当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间

  3. 每次调用select,需要把fd集合从用户态拷贝到内核态,然后在内核遍历fd集合,当fd集合很大时,开销会比较大

poll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <poll.h>
/*
参数:
fds[]:要监视的文件描述符数组
nfds:数组fds中结构体元素的总数量
timeouts,同select
pollfd:
struct pollfd {
int fd; //文件描述符
short events; // 等待的需要测试事件
short revents; // 实际发生了的事件,也就是返回结果
};
events、revents:
POLLIN 有数据可读。
POLLRDNORM 有普通数据可读。
POLLRDBAND  有优先数据可读。
POLLPRI 有紧迫数据可读。
POLLOUT 写数据不会导致阻塞。
POLLWRNORM  写普通数据不会导致阻塞。
POLLWRBAND  写优先数据不会导致阻塞。
POLLMSGSIGPOLL 消息可用。
revents域中还可能返回下列事件:
POLLER 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起事件。
POLLNVAL指定的文件描述符非法。
返回值:同select
*/
int poll(struct pollfd fds[], nfds_t nfds, int timeout)
//poll执行后,当指定fd的有事件发生时,会设置其对应的结构体的revents

思路

poll本质上和select没有区别,但它没有最大连接数的限制,原因是它是基于链表来存储的

缺点

  1. 它没有最大连接数的限制,原因是它是基于链表来存储的
  2. poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd

epoll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<sys/epoll.h>
/*创建内核事件表
参数:
size:并不起作用,只是给内核一个提示,告诉它事件表需要多大
返回值:
>0,文件描述符,指定要访问的内核事件表,是其他所有epoll系统调用的句柄。
-1,失败
*/
int epoll_create(int size);
/*操作内核事件表
参数:
epfd:epoll_create返回值
op:操作类型
EPOLL_CTL_ADD, 往事件表中注册fd上的事件;
EPOLL_CTL_MOD, 修改fd上注册的事件;
EPOLL_CTL_DEL, 删除fd上注册的事件
fd:需要监控的fd
event:指定事件数组
epoll_event:
struct epoll_event
{
__int32_t events; //epoll事件
epoll_data_t data; //用户数据
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data;
返回值:
0,成功
-1,失败
在使用epoll_ctl时,是把fd添加、修改到内核事件表中,或从内核事件表中删除fd的事件。如果是添加事件到事件表中,可以往data中的fd上添加事件events,或者不用data中的fd,而把fd放到用户数据ptr所指的内存中(因为epoll_data是一个联合体,只能使用其中一个数据),再设置events。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*一段时间内等待一个组文件描述符上的事件
参数:
epfd:epoll_create返回值
events:一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件
maxevents:当前需要监听的所有fd数
timeout:
0,马上返回
-1,阻塞,直到有事件发生
>0,超时时间
返回值:
>0,成功,就绪文件fd的个数
-1:失败
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

思路

epoll支持水平触发和边缘触发,会把监控的fd放在内核中的一个事件表中,使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核将触发回调函数,该回调函数将就绪的文件描述符和事件拷贝到用户空间events所管理的内存,epoll_wait便可以收到通知

优点

  1. 没有最大并发连接的限制
  2. 效率提升
  3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销

三种比较

比较点 select poll epoll
一个进程最大连接数 由FD_SETSIZE宏定义,其大小是32个整数的大小,可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试 基于链接存储,无限制 上限很大,1G内存的可以打开10万左右的连接
效率 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 同select 不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll
消息传递方式 内核需要将消息传递到用户空间,都需要内核拷贝动作 同select 内核需要将消息传递到用户空间,都需要内核拷贝动作
事件集合 内核会修改fd_set,五哦一每次调用都需要重置fd_set events传入revents反馈 使用内核事件表管理;epoll_eait的events仅用来保存就绪事件
索引就绪fd的时间复杂度 O(n)O(n) O(n)O(n) O(1)O(1)
内核实现 轮询 轮询 回调
工作模式 LT LT LT和ET