I/O多路复用

I/O 多路复用

I/O多路复用支持应用同时在多个文件描述符上阻塞,并在其中某个可以读写的时候收到通知,在设计上遵循以下原则:

  1. I/O多路复用:当任何一个文件描述符I/O就绪时进行通知
  2. 都不可用?在有可用的文件描述符之前一直处于睡眠状态
  3. 唤醒:哪个文件描述符可用
  4. 处理所有准备就绪的文件描述符,没有阻塞
  5. 返回第一步,重新开始

select()

select()系统调用提供了一种实现同步I/O多路复用的机制:

1
2
3
4
5
6
7
8
#include <sys/select.h>

int select (int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
FD_CLR (int fd, fd_set *set);
FD_ISSET (int fd, fd_set *set);
FD_SET (int fd, fd_set *set);
FD_ZERO (fd_set *set);

在给定的文件描述符I/O准备就绪之前并且还没超出指定的时间限制,select()调用就会阻塞

监视的文件描述符分为3类,分别等待不同的事件:

  • readfds:是否有数据可读
  • writefds:是否有写操作可以无阻塞完成
  • exceptfds:是否发生异常,或者出现带外数据

指定的集合可以是NULL,表示不监听该事件

参数timeout是指向timeval结构体的指针,定义如下:

1
2
3
4
5
6
7
#include <sys/time.h>

struct timeval
{
long tv_sec; /*seconds*/
log tv_usec; /*microseconds*/
}

宏管理

不是直接操作文件描述符,而是同过辅助宏管理

FD_ZERO从指定集合中删除所有文件描述符,每次调用select()之前,都应该调用

FD_SET向指定机中添加一个文件描述符

FD_CLR从指定集中删除一个文件描述符

FD_ISSET检查一个文件描述符是否在给定的集合中,如果在返回非0,不在返回0

返回值和错误码

select()调用成功,返回三个集合中I/O就绪的文件描述符总数。如果给出了超时设置,返回值可能是0.出错时,返回-1,并把errno值设置成如下值之一:

EBADF: 某个集合中存在非法文件描述符

EINTR: 等待时捕获了一个信号,可以重新发起调用

EINVAL: 参数n是负数,或设置超时时间值非法

ENOMEM: 没有足够的内存来完成请求

使用实例

监听stdin的输入,5s超时,虽然这个例子不是I/O复用,但是也能理解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
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
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define TIMEOUT 5
#define BUF_LEN 1024

int main (void)
{
struct timeval tv;
fd_set readfds;
int ret;

FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);

tv.tv_sec = TIMEOUT;
tv.tv_usec = 0;

ret = select (STDIN_FILENO+1,
&readfds,
NULL,
NULL,
&tv);
if (ret == -1)
{
perror ("select");
return 1;
}
else if (!ret)
{
printf ("%d seconds elapsed.\n",TIMEOUT);
return 0;
}

if (FD_ISSET(STDIN_FILENO, &readfds))
{
char buf[BUF_LEN+1];
int len;

len = read (STDIN_FILENO, buf, BUF_LEN);
if (len == -1)
{
perror ("read");
return 1;
}

if (len)
{
buf[len] = '\0';
printf ("read:%s\n",buf);
}

return 0;
}

fprintf(stderr, "This should not happen!\n");
return 1;
}

poll()

poll()解决了一些select()的不足,不过select()还是被频繁使用(出于习惯或可移植性的考虑)

1
2
3
#include <poll.h>

int poll (struct pollfd *fds, nfds_t nfds, int timeout);

select()使用了基于文件描述符的三位掩码的解决方案,其效率不高;和它不同,poll()使用了由nfds个pollfd结构体构成的数组,fds指针指向该数组。pollfd结构体定义如下:

1
2
3
4
5
6
7
8
#include <poll.h>

struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /*returned events witnessed */
}

每个pollfd结构体指定一个被监视的文件描述符,可以给poll()传递多个pollfd结构体,使它能够监视多个文件描述符

events和revents

每个结构体的events变量是要监视的文件描述符的时间的位掩码,用户可以设置该变量。revents变量时该文件描述符的结果的事件的位掩码。以下是合法的events值:

变量 描述
POLLIN 有数据可读
POLLRDNORM 有普通数据可读
POLLRDBAND 有优先数据可读
POLLPRI 有高优先数据可读
POLLOUT 写操作不会阻塞
POLLWRNORM 写普通数据不会阻塞
POLLBAND 写优先数据不会阻塞
POLLMSG 有SIGPOLL消息可用

除了场面events的位掩码外,revents还会返回如下事件:

变量 描述
POLLER 给定的文件描述符出现错误
POLLHUP 给定的文件描述符有挂起事件
POLLNVAL 给定的文件描述符非法

例如,设置某个文件描述符是否可读写,需要把events设置成POLLIN | POLLOUT。返回时,会检查revents中是否有相应的标志位。如果设置了POLLIN,文件描述符可非阻塞读。如果设置了POLLOUT,文件描述符可非阻塞写

timeout 参数指定等待的时间长度,单位是毫秒,如果为负数,表示永远等待;如果为0,则立即返回,并给出所有I/O未就绪的文件描述符列表,不会等待更多事件

返回值和错误码

poll()调用成功时,返回revents变量不为0的所有文件描述符个数;如果没有任何事件发生且未超时,返回0.失败时返回-1,并相应设置errno值如下:

EBADF: 某个集合中存在非法文件描述符

EFAULT: fds指针指向额地址超出了进程地址空间

EINTR: 等待时捕获了一个信号,可以重新发起调用

EINVAL: 参数n是负数,或设置超时时间值非法

ENOMEM: 没有足够的内存来完成请求

实列

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
#include <stdio.h>
#include <unistd.h>
#include <poll.h>

#define TIMEOUT 5

int main()
{
struct pollfd fds[2];
int ret;

fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
fds[1].fd = STDOUT_FILENO;
fds[1].events = POLLOUT;

ret = poll (fds, 2, TIMEOUT*1000);
if (ret == -1)
{
perror ("poll");
return 1;
}

if (!ret)
{
printf("%d seconds elapsed.\n",TIMEOUT);
return 0;
}

if (fds[0].revents & POLLIN)
{
printf("stdin is readable\n");
}

if (fds[1].revents & POLLOUT)
{
printf("stdout is writable\n");
}

return 0;
}

运行后。结果如下:

1
2
3
4
5
6
$ ./a.out
stdout is writable

$ ./a.out <test.txt
stdin is readable
stdout is writable

在实际应用中,不用每次调用时都重新构建pollfd结构体。该结构体可能会被重复传递多次,内核会在必要时把revents清空。

poll()和select()的区别

虽然poll()和select()完成相同的工作,但poll()调用在很多方面仍然优于select()调用

  • poll()不需要用户计算最大文件描述符值加1作为参数传递给它
  • poll()对于值很大的文件描述符,效率更高
  • select()的文件描述符集合是静态的,需要对大小设置进行权衡:如果值很小,会限制可监视的最大文件描述符值;如果很大,效率会很低
  • 对于select()调用,返回时会重新创建文件描述符集,因此每次调用都必须重新初始化。poll()调用会把输入(events)和输出(revents)分离开,支持无需改变数组就可以重新使用
  • select()调用的timeout参数在返回时是未定义的,代码要支持可移植,需要重新初始化

不过,select()调用也有些有点:

  • select()可移植性更好,因为有些UNIX系统不支持poll()
  • select()提供了更高的超时精度:select()支持微秒级,poll()支持毫秒级

I/O多路复用
https://carl-5535.github.io/2021/02/01/Linux系统编程/I-O多路复用/
作者
Carl Chen
发布于
2021年2月1日
许可协议