- 我们可以使用 fgets 方法等待标准输入,但无法同时在套接字有数据的时候读出数据;
- 我们也可以使用 read 方法等待套接字有数据返回,但无法同时在标准输入有数据的情况下,读入数据并发送给对方。
I/O 多路复用的设计初衷就是解决这样的场景。我们可以把标准输入、套接字等都看做 I/O 的一路,多路复用的意思,就是在任何一路 I/O 有“事件”发生的情况下,通知应用程序去处理相应的 I/O 事件,这样我们的程序在同一时刻就仿佛可以处理多个 I/O 事件。
select 函数
函数声明1
2
3int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
//返回:若有就绪描述符则为其数目,若超时则为 0,若出错则为 -1
n 表示的是待测试的描述符基数,它的值是待测试的最大描述符加 1。比如现在的 select 待测试的描述符集合是{0,1,4},那么 maxfd 就是 5。
三个描述符集合:
- 读描述符集合 readset ,通知内核,在哪些描述符上检测数据可以读。
- 写描述符集合 writeset ,哪些描述符可以写。
- 异常描述符集合 exceptse,哪些描述符有异常发生。
使用下面的宏设置描述符集合:1
2
3
4void FD_ZERO(fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
下面一个向量代表了一个描述符集合,其中,这个向量的每个元素都是二机制数中的 0 或者 1。1
a[maxfd-1], ..., a[1], a[0]
- FD_ZERO 用来将这个向量的所有元素都设置成 0;
- FD_SET 用来把对应套接字 fd 的元素,a[fd] 设置成 1;
- FD_CLR 用来把对应套接字 fd 的元素,a[fd] 设置成 0;
- FD_ISSET 对这个向量进行检测,判断出对应套接字的元素 a[fd] 是 0 还是 1。
其中 0 代表不需要处理,1 代表需要处理。三个描述符集合中的每一个都可以设置成空,这样就表示不需要内核进行相关的检测。
timeval 结构体时间:
1
2
3
4 struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- 设置成空 (NULL),表示如果没有 I/O 事件发生,则 select 一直等待下去。
- 设置一个非零的值,这个表示等待固定的一段时间后从 select 阻塞调用中返回,
socket_fd 可读的几种情况
- 第一种情况是套接字接收缓冲区有数据可以读,如果我们使用 read 函数去执行读操作,肯定不会被阻塞,而是会直接读到这部分数据。
- 第二种情况是对方发送了 FIN,使用 read 函数执行读操作,不会被阻塞,直接返回 0。
- 第三种情况是针对一个监听套接字而言的,有已经完成的连接建立,此时使用 accept 函数去执行不会阻塞,直接返回已经完成的连接。
- 第四种情况是套接字有错误待处理,使用 read 函数去执行读操作,不阻塞,且返回 -1。
select 检测套接字可写,完全是基于套接字本身的特性来说的,具体来说有以下几种情况:
- 第一种是套接字发送缓冲区足够大,如果我们使用非阻塞套接字进行 write 操作,将不会被阻塞,直接返回。
- 第二种是连接的写半边已经关闭,如果继续进行写操作将会产生 SIGPIPE 信号。
- 第三种是套接字上有错误待处理,使用 write 函数去执行读操作,不阻塞,且返回 -1。
总结成一句话就是,内核通知我们套接字可以往里写了,使用 write 函数就不会阻塞。
select 缺点:支持的文件描述符的个数是有限的。在 Linux 系统中,select 的默认最大值为 1024。
范例
1 | int main(){ |
poll 函数
poll 函数的原型:1
2
3int poll(struct pollfd *fds, unsigned long nfds, int timeout);
//返回值:“returned events”中非 0 的描述符个数,若超时则为 0,若出错则为 -1
第一个参数是一个 pollfd 的数组。其中 pollfd 的结构如下:1
2
3
4
5struct pollfd {
int fd; /* file descriptor */
short events; /* events to look for */
short revents; /* events returned */
};
描述符上待检测的事件类型 events 可以表示多个不同的事件,具体的实现可以通过使用二进制掩码位操作来完成。
events 类型的事件包括可读事件、可写事件和错误事件。
POLLIN
和 POLLOUT
可以表示读和写事件。1
2
3#define POLLIN 0x0001 /* any readable data available */
#define POLLPRI 0x0002 /* OOB/Urgent readable data */
#define POLLOUT 0x0004 /* file descriptor is writeable */
和 select 非常不同的地方在于,poll 每次检测之后的结果不会修改原来的传入值,而是将结果保留在 revents 字段中,这样就不需要每次检测完都得重置待检测的描述字和感兴趣的事件。我们可以把 revents 理解成“returned events”。
events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。
第二个参数 nfds
描述的是数组 fds 的大小,简单说,就是向 poll 申请的事件检测的个数。
第三个参数 timeout
,描述了 poll 的行为。
timeout<0 表示在有事件发生之前永远等待;timeout="0,表示不阻塞进程,立即返回;timeout">0 表示 poll 调用方等待指定的毫秒数后返回。
如果我们不想对某个 pollfd 结构进行事件检测,可以把它对应的 pollfd 结构的 fd 成员设置成一个负值。这样,poll 函数将忽略这样的 events 事件,检测完成以后,所对应的“returned events”的成员值也将设置为 0。
- 在 select 里,文件描述符的个数已经随着 fd_set 的实现而固定,没有办法对此进行配置;
- 在 poll 里,我们可以控制 pollfd 结构的数组大小,即突破原来 select 函数最大描述符的限制,在这种情况下,应用程序调用者需要分配 pollfd 数组并通知 poll 函数该数组的大小。