unix环境高级编程:高级I/O理解

在网络通信中,数据的传输大部分基于socket(socket位于TCP/UDP与HTTP协议之间,当然socket也可以利用SOCK_RAW套接字提供一个数据报接口,用于直接访问下面的网络层,当使用SOCKT_RAW套接字时,应用程序应该构造自己的协议头部)。在unix系统中,将socket的操作抽象为文件的读,写,异常,即一个socket对象可以对应为一个文件描述符(fd)。所以网络I/O与磁盘读写O/I可以属于同一层次上要考虑的问题。

在考虑具体问题之前,首先要明确一个概念,阻塞与非阻塞是对于进程而言:当一个进程想要read一个文件描述符(fd)时,若fd的读缓冲区没有数据,此时进程将会阻塞,直到缓冲区中有数据可以使用,此时进程才会继续向下运行,这种情况便是阻塞I/O。对非阻塞I/O而言,当fd的读缓冲区中没有数据时,进程并不会阻塞在此处,而是立即返回错误,表示该操作如继续执行将会阻塞。下图是一个非阻塞I/O的例子(a.out)

   
upload successful

执行以下代码:

./a.out < /etc/services 2>stderr.out

可以得到如下结果:

upload successful

其中35对应的是EAGAIN信号,显然一次输出到终端的数据是有限的,当数据不能write到终端时,进程并没有阻塞,而是返回信号EAGAIN,表明当前写缓冲区已满, 以上代码便是一种轮询操作。

考虑如下一种场景:当该进程必须要从两个或多个fd中同时读取数据时,在这种情况下,我们不能将进程阻塞在任何一个fd的read方法上,原因是,另一个fd可能已经准备好read了,但进程阻塞,无法对其进行任何操作。为了能够使进程同时处理多个fd,我们的第一个想是fork子进程,或者用多线程同时阻塞处理。但这在实现上较为繁琐,且占用资源。另外,也可以用轮询的方法来遍历fd的状态。第三种方法是用异步I/O:进程告诉内核,当fd准备好可以进行I/O时,用一个信号通知进程,这种方法存在的问题在于:每个进程只能有一个信号,如果信号对多个fd都起作用,那么进程在收到该信号时,无法判断是哪个fd准备就绪。以上三种方法或多或少都有问题,我们主要考虑I/O多路转接技术。
I/O多路转接技术想要实现的作用是:将想监听的fd加入一个列表,用某个函数来监听这个列表,当列表中有fd准备就绪时,返回准备就绪的fd给进程使用。select函数和poll函数便是实现该功能。select函数如下:
upload successful

传给select的参数告诉内核:

我们所关心的描述符

对于每个描述符,我们所关心的条件

愿意等待多长时间

select将会返回:

已经准备好的描述符的总数量

对于读,写,异常,哪些描述符已经准备好了。

该函数返回的fd一定是准备就绪的,调用对应的read,write函数一定不会发生阻塞。select函数的具体实现过程:

upload successful

1
2
3
4
5
6
7
8
9
10
11

1. 从用户态copy fd_set到内核空间
2. 注册回调函数pollwait(将进程挂到每个socket的等待队列中,
当socket准备好后(执行mask状态码进行判断,再唤醒进程))
3. 内核遍历fd,调用每一个fd的poll方法,返回socket的mask状态掩码,即
现在准备好了没有,给fd_set赋值
4.当无可读写mask码时,select睡眠(On Linux, select() modifies
timeout to reflect the amount of time not slept),等睡眠时间到,
再次醒来轮询fd_set(内核态轮询)
5. 有值时返回fd_set,将其copy到用户空间
6. 用户进程变为运行态,循环fd_set,得到准备好的fd。

内核创建一个epoll对象,epoll_ctl函数向epoll对象中添加需要监听的fd,当fd准备就绪时,中断程序会操作epoll对象,而不是操作进程。当进程执行到epoll_wait时,如果就绪列表存在已经准备好的fd,进程将会被唤醒。

upload successful

epoll的作用是提升事件循环查询“io事件”的效率,他允许用户进程同时监听多个文件描述符的io事件即io多路复用。同时epoll在实现上是十分高效的,相对于poll/select,epoll使得进程可以监控更多的文件描述符。epoll采用注册机制,在内核中保存用户关注的文件描述符(红黑树保存),不像select一样每次都需要传入所有的文件描述符,效率更高(减少了从用户空间向内核空间的拷贝)。epoll不像select一样会在内核中轮询所有的文件描述符(低效的),epoll会在不同文件描述符对应的设备的等待队列中添加一个回调函数,在回掉函数中将对应的文件描述符添加至epoll的rdlist队列(高效的),当进程调用epoll_wait时,内核检查该队列是否为空,如果是空则阻塞进程,如果不为空则将该队列拷贝至用户空间,并返回该函数。epoll的两种触发模式:

1.水平触发:只要fd仍然可读或写,每次调用epoll_wait都会返回该文件描述符;

2.边缘触发:只fd仍然可读或写,每次调用epoll_wait只会返回一次该文件描述符;

注意异步I/O与非阻塞I/O的区别,异步I/O,需要用回调函数来处理,fd准备好之后的情况,比较复杂。而非阻塞则是在fd的read,write操作时,一定可以立即得到结果,不会阻塞进程。