为什么使用多路IO复用
使用场景:
同时需要对多个低速IO进行读写操作
解决方法与问题:
- 最简单低速阻塞IO(pipe,FIFO,socket)会持续阻塞直到有数据时读入,如果有两个读fd,第二个fd只有当前一个fd完成读入操作后,才能读取自己的数据。如果第二个fd数据提前到达也无法处理。
- 对每一个阻塞读fd开辟一个线程/进程,除了开辟进程/线程需要分配/切换的资源,还需要考虑进程/线程同步的问题
- 非阻塞IO一定程度上可以解决单线程下顺序阻塞等待的问题,但是需要轮询忙等,这段时间进程不处于阻塞状态,轮询占用CPU时间片,造成CPU浪费
- 异步IO(还不太了解):asynchronous I/0存在可移植的问题。
- 多路IO复用:构造感兴趣fd列表,使用
select
/poll
/pselect
/epoll
(linux 2.6+)执行多路IO复用,进程会告知哪些fd准备好进行IO
select
函数签名
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#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);
输入参数 | |
---|---|
ndfs | 3类fd中感兴趣的最大fd值+1,避免后面对所有fd集合全部遍历,或者全都要FD_SETSIZE (1024) |
readfds | fd_set 类型,可读fd标志位集合 ,NULL 忽略 |
writefds | fd_set 类型,可写标志位集合,NULL 忽略 |
exceptdfs | fd_set 类型,处于异常标志位集合,NULL 忽略 |
timeout | 传递一个微秒精度时间结构指针,控制select 的最大等待时间 |
返回值 | |
---|---|
n | 准备好fd数目(3个fd_set 之和),不同fd_set 同一位fd都准备好计数多次,此时fd_set 已准备好的fd位置位 |
0 | 超时,没有描述符准备好,所有fd_set 清空 |
-1 | 出错,如在一个fd都没准备好捕获到信号,此时所有fd_set 都不会修改 |
timeout
timeout
由于是微秒精度的,可以当作一个定时器来使用。例:客户端socket使用connect
非阻塞建立连接时,设置自定义的超时等待时间(阻塞socket fd connect
超时等待时间75s?)。根据timeout
传入指针不同,可以指定不同的等待效果:
NULL
: 永远等待,感兴趣fd准备好返回。如果捕捉到信号,select
返回-1,errno
返回EINTR
- 全0:不等待,等价于轮询
- 不为0:在定时时间内,有一个fd准备好返回。超时没有fd准备好,返回0
fd_set
fd_set
相当于一个大数组,每一个fd对应同编号一位01标志:
UNIX中专门定义了对于fd_set
操作的函数:
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
void FD_CLR(int fd, fd_set *set); // 清除fd位上标志
int FD_ISSET(int fd, fd_set *set); // 测试fd位标志,在fd_set中返回非0,不在返回0
void FD_SET(int fd, fd_set *set); // 设置fd位上标志
void FD_ZERO(fd_set *set); // 清空整个fd,在初始化一个fd_set必做
对于select
中每一类型的fd_set
,准备好的含义是如下的:
fd_set | |
---|---|
readfds | 对应位fd进行读操作不阻塞,则是准备好的 |
writefds | 对应位fd进行写操作不阻塞,则是准备好的 |
exceptfds | 描述符有一个未决异常条件,则是准备好的(没太了解) |
注意点:
- 普通文件的IO,
readfds
,writedfs
,excepdfs
总是准备好的 - fd是否阻塞不会影响
select
的定时阻塞,如果超时5s的select
读一个非阻塞IO,select
最多阻塞5s - 文件到达尾端,
select
会认为是可读的,read
调用后返回0,可以用来判断到达尾端。
具体针对socket,下面的情况是读准备好:
- socket内核接收缓存区字节数>=低水位标记
SO_RCVLOWAT
,可以无阻塞读,recv
返回>0 - scoket对方关闭连接,scoket的
recv
返回0 - socket有新的连接请求(对于listen socket fd调用
accept
) - socket有上未处理错误,
getsockopt
读取清除该错误
具体针对socket,下面情况写准备好:
- scoket内核发送缓冲区可用字节>=低水位标记
SO_SNDLOWAT
,可以无阻塞写,send
返回>0 - socket写操作被
shutdown
,对写关闭的socket再执行send
会触发SIGPIPE
信号 - socket使用非阻塞
connect
连接成功/失败后 - socket有上未处理错误,
getsockopt
读取清除该错误
具体针对socket,下面情况异常条件准备好:
- socket接收到外带数据,即TCP中紧急数据urgent data,TCP URG控制位=1,
send
时设置MSG_OOB
标志位,接收时发送SIGURG
信号
pselect
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
select
威力加强版,主要是提供了精度更高纳秒级的超时timeout
,以及信号屏蔽字sigmask
,如果是NULL
等价于select
使用过程
以最简单的多路读IO为例:
- 创建多个低速系统调用读IO的fd
- 创建
fd_set
并且FD_ZERO
清空,fd使用FD_SET
置位 - 调用
select
阻塞等待 - 退出阻塞后,遍历前
ndfs
个fd,使用FD_ISSET
判断是否准备好,准备好的fd调用read
- 处理好所有fd后,循环回到2
书上代码实例:Github
存在问题
fd_set
默认1024位,最多就处理这么多fdfd_set
是不可重用的,就是说在一次select
后,之前设置的感兴趣fd位都被重置,只有准备好的fd被保存,下次要IO时,select
之前需要重新设置select
只知道有一个fd准备好,但不知道具体是哪些,需要O(n)遍历
poll
函数签名
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数 | |
---|---|
fds | pollfd 结构体的数组,每一个结构指定了fd,以及对应感兴趣的条件 |
nfds | pollfd 注册的事件数量 |
timeout | poll 愿意等待时间,毫秒为单位 |
pollfd
poll
不像select
为每一个fd都有提供标志位,而是构造pollfd
数组,结构如下:
struct pollfd {
int fd; /* 监控的文件描述符 */
short events; /* 请求监控的事件请求 */
short revents; /* 内核设置,说明fd发生了哪种事件 */
};
可以看到,不同于select
,设置监控的事件标志events
和返回事件标志revents
分离定义开来,poll
返回时只用设置revents
,events
保持不需要重置。
下面是具体事件的标志:
特点与问题
- 解决了
select
中固定长度fd_set
的问题 - 解决了
select
中fd_set
不可重用的问题 - 对于设置的所有
pollfd
,依然需要顺序遍历并判断感兴趣事件是否触发
参考资料
[1]. UNIX环境高级编程
[2]. Linux高性能服务器编程
[3]. 图解TCP/IP