其实这些在UNP上都讲过,只是用自己的话把自己的一些理解写下来加深一些印象。
I/O复用的主要思想就是先构造一张描述符的列表,然后调用一个函数直到这些描述符中的一个已经准备好I/O操作时,该函数就返回,同时告诉进程那些描述符准备好I/O操作了。
在linux系统上支持I/O复用的系统调用有select, pselect, poll, epoll, 而epoll是在linux 2.6才加入的,相较与传统的方法,效率有了很大的提升。首先是传统方法。
- select和pselect
#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);
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
分析select会让我们对该函数有更多的认识。传向该函数的参数通知内核一些信息,我们关心哪些描述符以及这些描述符的状态,还有愿意等待的时间。
返回时内核则会告诉我们哪些描述符已就绪和已就绪描述符的数量。
这两个函数功能差不多,但仔细看两个函数最后的参数可以看出区别来。首先select和peslect超时值类型不同,并且pselect的时间值不能够被修改,同时pselect指定更加精准的定时。同时pselect添加了信号屏蔽字,执行该函数时会以原子操作安装信号屏蔽字,函数返回时恢复之前的信号屏蔽字。
同时这两个函数会根据超时值等待不同的时间:
- 一直等待:在有一个描述符准备好或信号中断返回,否则一直阻塞,时间值为NULL。
- 等待固定时间:在有一个描述符准备好或超时返回,时间值为具体值。
- 不等待:检查描述符后马上返回,时间值为0.
同时还有四个与描述符相关的宏:
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
这几个宏比较简单,在man 文档上大家可以自行查看。
2. poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
poll的第一个参数指向一个pollfd数组的指针,我觉得用fds[]应该更好,用指针会给人误解为一个指向pollfd结构体的指针,其实是指向一个pollfd结构体数组,每个数组元素包含描述符,描述符状态,其中ndfs是pollfd数组元素的个数,在函数返回时,内核会根据描述符的状态改变设置revents值,并且返回已就绪的描述符数目。
另外一个就是时间值,它不同于select采用结构体,而就是用一个整型数来表示poll等待多长时间,其他操作与select没什么不同:
- timeout == -1:一直等待
- timeout == 0:不等待
- timeout > 0:等待固定时间
对此,我们可以发现select和poll函数实现的方式不同,poll不是为每种状态设置描述符集,而是设置pollfd结构数组,每个元素包含描述符及我们所关心的状态。到此,传统的I/O复用就这些了,有些具体例子可以参考UNP第6章。另外就是epoll了,很重要的,毕竟现在用的较多嘛,下回再详细说吧。