0%

IO多路复用

我们可以将标准输入、套接字等都看做IO的一路,多路复用的意思是在任何一路IO有事件的情况下,通知应用程序去处理相应的IO事件,这样程序就仿佛可以同一时刻处理多个IO事件了。

select

  • select fd_set结构,底层是一个bitmap结构,是定长的1024,不容易改,如果需要改需要重新编译内核代码。

  • 使用select函数,通知内核挂起进程,当一个或者多个IO事件发生之后,控制权限返回给应用程序,由应用程序进行IO事件的处理。

  • 当有事件发生的时候,并不知道具体是哪几条流(可能有一个也可能有多个),只能轮询一遍出所有的流,找出相应的数据,或者写入数据的流,**时间复杂度为O(n)**。

  • 需要维护一个存放大量fd的数据结构,当用户空间向内核空间传递该数据结构的时候会发生大量的拷贝,拷贝开销大,从而影响效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/select.h>
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *expectset, struct timeval *timeout);

struct timeval
{
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 */
}
timeout可以有三种情况:
timeout == NULL; /* 等待无限长的时间 */
timeout->tv_sec == 0 && timeout->tv_usec == 0 /* 不等待,直接返回。(Noblock 非阻塞)*/
timeout->tv_sec != 0 || timeout->usec != 0 /* 等待指定的时间 */

/* FD_ZERO 将一个fd_set类型的变量的所有的位都设置为0 */
int FD_ZERO(fd_set *fdset);

/* FD_CLR 可以将某一个位进行清除 */
int FD_CLR(fd_set *fdset);

/* FD_SET 用来将某一个位进行置位1 */
int FD_SET(int fd, fd_set *fdset);

/* FD_ISSET 用来检测某一个位是否被置位 */
int FD_ISSET(int fd, fd_set *fdset);

返回值(return value):做好准备的文件描述符个数,超时为0, 错误为-1。

中间的三个参数readset、writeset、exepectset,分别是读描述符集合 readset、写描述符集合 writeset 和异常描述符集合 exceptset,这些参数指明了我们关心哪一些描述符,和需要满足什么条件。一个文件描述符保存在fd_set类型之中,fd_set其实就是一个位图bitmap。

int maxfd; 指的是最大的文件描述符加1。

Linux环境下编程,系统给提供了一组宏定义,可以对我们的fd_set进行赋值等操作。

Demo:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
int main(int argc, char **argv)
{
if (argc != 2)
{
error(1, 0, "usage: select01 <IPaddress>");
}
int socket_fd = tcp_client(argv[1], SERV_PORT);

char recv_line[MAXLINE], send_line[MAXLINE];
int n;

fd_set readmask;
fd_set allreads;
FD_ZERO(&allreads);
FD_SET(0, &allreads);
FD_SET(socket_fd, &allreads);

for (;;)
{
readmask = allreads;
int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);

if (rc <= 0)
{
error(1, errno, "select failed");
}

if (FD_ISSET(socket_fd, &readmask))
{
n = read(socket_fd, recv_line, MAXLINE);
if (n < 0)
{
error(1, errno, "read error");
}
else if (n == 0)
{
error(1, 0, "server terminated \n");
}
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}

if (FD_ISSET(STDIN_FILENO, &readmask))
{
if (fgets(send_line, MAXLINE, stdin) != NULL)
{
int i = strlen(send_line);
if (send_line[i - 1] == '\n')
{
send_line[i - 1] = 0;
}

printf("now sending %s\n", send_line);
size_t rt = write(socket_fd, send_line, strlen(send_line));
if (rt < 0)
{
error(1, errno, "write failed ");
}
printf("send bytes: %zu \n", rt);
}
}
}
}

poll

poll其实本质和select一样没有太大的区别,主要有几点改变。

1
2
3
4
5
6
7
8
>struct pollfd
>{
int fd;
short events;
short revents;
>};

>int poll(struct pollfd *fds, unsigned long nfds, int timeout);
  • Events可以表示多个不同的事件,具体的实现可以通过二进制掩码位操作来完成,例如POLLINPOLLOUT表示可读可写事件。

  • 与select不同的在于,poll每次检测之后的结果不会修改元凯的传入的值,而是将结果保留在revents字段中,这样就不用每次检测完成之后都要重置待检测的描述字和感兴趣的事件。revents可以理解return events

  • 如果对某个pollfd结构进行事件检测,可以将对应的pollfd结构的fd成员设置成一个负值,poll函数将会忽略该事件。

  • 突破了select(1024)最大文件描述符的限制,原因是它是基于链表来存储

同样存在缺点

1、依旧是大量的fd数组在用户空间和内核空间之间进行拷贝。

2、poll是水平触发,如果fd就绪被报告之后没有对其进行处理,下次poll韩式会再次报告该事件。

epoll

epoll接口

1
2
3
4
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epoll_create 函数是一个系统调用函数,函数将会在内核空间开辟一块新的空间,可以理解为epoll结构空间,返回值为epoll的文件描述符编号,方便后续操作使用。

  • epoll_ctl是epoll事件的注册函数,epoll与select不同,select函数是调用时指定的需要监听的描述符和事件,epoll先将用户感兴趣的描述符事件注册到epoll空间内,此函数是非阻塞函数,作用仅仅是增删改epoll空间内的描述符信息。
    参数1:epfd,epoll结构的进程fd编号,函数将依靠该编号找到对应的epoll结构。
    参数2:op,表示当前的请求的类型,由三个定义:

    EPOLL_CTL_ADD:注册新的fd到epoll中。

    EPOLL_CTL_MOD:修改已经注册的fd的监听事件。

EPOLL_CTL_DEL:从epfd中删除一个fd。
参数3:fd,需要监听的文件描述符,一般指的是sockfd。
参数4:event,告诉内核该fd资源感兴趣的事件。

  • epoll_wait函数,等待事件的发生,类似于select()函数调用。根据参数timeout,决定是否处于阻塞模式。
    参数1:epfd,指定一个感兴趣事件列表。
    参数2:*events,是一个指针,必须指向一个epoll_event结构数组,当函数返回的时候,内核会将就绪状态的的数据拷贝到该数组中。
    参数3:maxevents,标明参数2epoll_event结构数组最多能接收的数据量,即本次操作做多能获取多少就绪数据。
    参数4: timeout,单位为毫秒。

    0:表示立即返回。

    -1:阻塞调用,直到有用户感兴趣事件就绪为止。

epoll_create函数

  • epoll_create 函数是一个系统调用函数,函数将会在内核空间开辟一块新的空间,可以理解为epoll结构空间,返回值为epoll的文件描述符编号,方便后续操作使用。

epoll_ctl函数

epoll_ctl是epoll事件的注册函数,epoll与select不同,select函数是调用时指定的需要监听的描述符和事件,epoll先将用户感兴趣的描述符事件注册到epoll空间内,此函数是非阻塞函数,作用仅仅是增删改epoll空间内的描述符信息。

  • 参数1:epfd,epoll结构的进程fd编号,函数将依靠该编号找到对应的epoll结构。

  • 参数2:op,表示当前的请求的类型,由三个定义:

EPOLL_CTL_ADD:注册新的fd到epoll中。

EPOLL_CTL_MOD:修改已经注册的fd的监听事件。

EPOLL_CTL_DEL:从epfd中删除一个fd。

  • 参数3:fd,需要监听的文件描述符,一般指的是sockfd。

  • 参数4:event,告诉内核该fd资源感兴趣的事件。

epoll_wait函数

epoll_wait函数,等待事件的发生,类似于select()函数调用。根据参数timeout,决定是否处于阻塞模式。

  • 参数1:epfd,指定一个感兴趣事件列表.。

  • 参数2:*events,是一个指针,必须指向一个epoll_event结构数组,当函数返回的时候,内核会将就绪状态的的数据拷贝到该数组中。

  • 参数3:maxevents,标明参数2epoll_event结构数组最多能接收的数据量,即本次操作做多能获取多少就绪数据。

  • 参数4: timeout,单位为毫秒。

0 表示立即返回。

-1 阻塞调用,直到有用户感兴趣事件就绪为止。

>0 阻塞调用,阻塞指定的时间内如果有时间就绪则是提前返回,否则就等待指定的事件后返回。
返回值(return value):本次就绪的fd的个数。

工作模式

epoll对文件描述符的操作有两种模式:LT(水平触发)和ET(边缘触发)。LT是默认模式,两者的区别如下:

LT(level 水平触发):事件就绪之后,用户可以选择处理或者不处理,如果用户本次未进行处理,那么下次调用epoll_wait时候仍然会将未出的事件打包给你。

ET(edge 边缘触发): 事件就绪之后,用户必须处理,因为内核不给给你兜底,内核把就绪的事件打包给用户态之后,就会把对应的就绪事件清理掉,不会再次通知用户。

ET模式在很大的程度上减少了epoll事件被重复触发的次数,因此被认为效率比LT模式高,也是epoll重要的杀手锏。

epoll优点

1、没有最大文件描述符限制

2、使用mmap文件映射内存加速与内核空间的消息传递,不再是大量的fd从用户空间拷贝到内核空间,大大减少了拷贝的开销。

3、效率提升,不再使用轮询

Reference

[1] https://blog.csdn.net/qq_35976351/article/details/85228002

[2] https://time.geekbang.org/column/article/138948

小主,路过打个赏再走呗~