POSIX: Portable Operating System Interface, developed by IEEE and adopted as standards by ISO and IEC (ISO/IEC)
1. Unix下I/O模型
- 阻塞I/O(BIO)模型(默认recvfrom系统调用)
- 非阻塞I/O模型(基于轮询系统调用recvfrom)
- 基于I/O多路复用模型(select和poll)
- 基于信号的I/O模式(SIGIO信号通知)
- 异步IO模型(POSIX aio_系列函数)
输入操作的两个阶段:
- 系统内核等待数据准备(比如等待网络数据达到)。当数据到达,数据被复制到内核缓冲区;
- 系统内核将内核缓冲区数据,复制到进程缓存区。将内核缓冲区就绪数据,复制到应用程序缓冲区;
阻塞与非阻塞,是针对进程而言,是否由于I/O问题导致进程阻塞与否
1.1. 阻塞I/O模型
进程调用recvfrom系统调用在数据报到达并复制到应用程序缓冲区或者发生错误(最常见的错误是系统调用被信号中断)之前不会返回;
进程从调用recvfrom到返回的整个时间都被阻止,当recvfrom成功返回时,我们的应用程序处理的数据包。
1.2. 非阻塞I/O模型 - 轮询系统请求
- sockect: 创建一个套接字,返回一个文件描述符
- fcntl: 针对文件描述符进行控制,设置文件描述符相关状态,比如文件记录锁、文件IO读写阻塞、异步通知等设定
若套接字通过fcntl
被设置为非阻塞O_NOBLOCK
,recvfrom在内核数据未就绪情况下,会直接返回-1,以及额外的ERRNO(EAGIN)错误;
对于前三个recvfrom,没有数据要返回,内核立即返回错误EWOULDBLOCK(EAGIN),应用程序不断轮询内核以查看某些操作是否已准备就绪,这通常是浪费CPU时间。
我们第四次调用recvfrom,数据报就绪,它被复制到我们的应用程序缓冲区,并recvfrom成功返回,然后我们处理数据。
1.3. 同步I/O多路复用模型 - select和poll
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
针对以readfds、writefds和errorfds传递地址的I/O描述符集合,检测查是否他们中有IO准备就绪,返回文件描述符集合中就绪的文件描述符数量或者-1(错误发生)、0(超时),当文件描述符被中断处理时候,select可能无法修改导致调用失败;
利用两个系统调用之一中调用select或poll阻塞,而不是在实际的I/O系统调用中阻塞:
与阻塞I/O模型相比,优点是可以通过select等待多个描述符就绪,缺点是需要使用两个系统调用:select+recvfrom
另外,还有一个模型需要注意,多线程的阻塞IO模型,即不使用select模式,但可以通过多线程,每个线程负责一个系统调用(如recvfrom)阻塞IO操作
1.4. 基于信号I/O模型 - SIGIO
若套接字通过fcntl
被设置成异步IO模式O_ASYNC
,在内核I/O数据就绪的情况下,通过使SIGIO信号发送到进程组。
基于信号的I/O模式有两个阶段:
- 应用程序设置套接字文件描述符允许信号IO驱动模式,并通过
sigaction
注册一个信号处理函数,这个系统调用是直接返回的,不会阻塞 - 当数据报准备在内核准备就绪,
SIGIO
信号会通知到我们的应用进程,触发注册的信号处理函数,通知进程执行recvfrom
操作
与阻塞I/O模型相比,其优点是在内核等待数据报过程中,进程不会被阻塞。
1.5. 异步IO模型
int aio_read(struct aiocb *aiocbp);
,POSIX异步I/O (AIO)接口允许应用程序启动一个或多个异步执行的I/O操作,其他类似的有aio_cancel
、aio_write
、aio_return
、aio_suspend
、aio_error
等
异步I/O由POSIX(Portable Operating System Interface)规范定义,形成了相关标准和规范, 这些aio_*
系列函数通过告诉内核启动操作并在整个操作(包括从内核到缓冲区的数据副本)完成时通知我们来工作
在将数据复制到我们的应用程序缓冲区之前,不会生成此信号,这与信号驱动的I/O模型不同:
异步IO模型和信号驱动的I/O模型的主要区别在于,通过信号驱动的I/O,内核告诉我们何时可以启动I/O操作(在内核IO缓冲数据就绪时候触发),但是使用异步I/O,内核告诉我们I/O操作完成时。
1.6. 五类I/O模型比较
前四个模型之间的主要区别在于第一个阶段(内核数据就绪处理机制不同),因为前四个模型中的第二个阶段是相同的:recvfrom当数据从内核复制到调用者的缓冲区时,进程被阻塞。
但是,异步I/O处理两个阶段,与前四个不同(直至内核就绪数据被拷贝至应用进程缓冲区)。
2. 同步IO与异步IO
POSIX将这两个术语定义如下:
- 同步I/O操作会导致请求进程被阻塞,直到I/O操作完成。
- 异步I/O操作不会导致请求进程被阻止。
使用这些定义,前四个I/O模型(阻塞、非阻塞、I/O多路复用、信号驱动I/O)都是同步的,因为实际的I/O操作(recvfrom)会阻止进程,只有异步I/O模型匹配异步I/O定义。
3. select函数
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
3.1. select的文件描述符状态检测功能
进程通过Select函数指示内核感兴趣的描述符(用于读取,写入或异常条件)以及等待多长时间(描述符不仅限于套接字,还可以使用select测试任何描述符):
- 等待多个事件中的任何一个发生,并仅在发生一个或多个这些事件时唤醒进程;
- 经过指定的时间后,唤醒进程;
3.2. Timeout参数
Timeout参数告诉内核,等待内核数据准备的时间,支持秒和微秒的timeval
结构,有以下几类值的区别:
- 永久等待(Timeout被设置为空指针),告知内核仅在数据准备好了再通知进程
- 等待一个固定时间,设定了等待内核IO操作的超时时间
- 不做等待(设置为0),检测描述符后,立即返回,不做等待
3.3. readset,writeset和exceptset参数
三个中间参数readset,writeset和exceptset指定了我们希望内核为读取,写入和异常条件测试的描述符,如果我们不感兴趣条件可以指定为一个空指针,都指定为空,类似于sleep的定时器(但精度更高)
3.4. select返回值
select函数在返回时,结果指示哪些描述符已准备好,即在所有描述符集中准备好的总位数。
如果计时器值在任何描述符就绪(读、写、错误操作就绪)之前到期,则返回值0。返回值-1表示错误(例如,如果函数被捕获的信号中断,则可能发生错误)。
3.5. 就绪状态文件描述符
- 若满足以下四个条件中的任何一个,则套接字已准备好读取:
- 套接字接收缓冲区数据字节数 >= 低水位标记的当前大小(
SO_RCVLOWAT
- socket设定的,tcp、udp默认为1个字节) - 收到FIN的TCP连接
- 套接字是侦听套接字,已完成连接的数量非零
- 套接字错误正在等待读操作处理
- 套接字接收缓冲区数据字节数 >= 低水位标记的当前大小(
- 若满足以下四个条件中的任何一个,则套接字已准备好写入:
- 套接字发送缓冲区中可用空间的字节数 >= 发送缓冲区的低水位标记的当前大小(
SO_SNDLOWAT
设定) - 连接的写半部分已关闭,将生成套接字上的写操作SIGPIPE
- 使用非阻塞连接的套接字已完成连接,或者连接失败
- 套接字错误正在等待处理 套接字上的写操作不会阻塞
- 套接字发送缓冲区中可用空间的字节数 >= 发送缓冲区的低水位标记的当前大小(
- 若套接字的带外数据或套接字仍处于带外标记(
out-of-band
),则套接字具有待处理的异常条件
3.6. 最大描述符数select
select最初设计,操作系统通常对每个进程描述符的最大数的上限,并选择只用该相同的限制。
但是,当前版本的Unix允许每个进程几乎无限数量的描述符(通常仅限于内存量和任何管理限制),这会影响select,从可移植性的角度来看,请注意使用大型描述符集。
3.7. Select处理流程
|
|
- Call select:
- 通过描述符集(rset)来检查可读性
- 在计算两个描述符的最大值后调用select,在调用中,写集指针和异常集指针都是空指针。
- Handle readable socket
- 如果套接字是可读的,则使用readline读取回显的行并由fputs输出
- Handle readable input
- 如果标准输入是可读的,则由fgets读取一行并使用writen将其写入套接字
4. 小结
简要概述了Unix的五类I/O模型(阻塞IO、非阻塞轮询I、SELECT IO多路复用、信号IO驱动以及异步AIO模型),前面四类按POSIX描述,IO过程中对应用进程有阻塞,归类为同步IO模型,异步AIO模型归类为异步IO模型;
关于IO阻塞与非阻塞,是程序通过fcntl
设置套接字文件描述符的状态为O_NOBLOCK
,也可以直接基于socket指定SOCK_NONBLOCK
类型标识;
关于信号IO驱动,是基于程序通过fcntl
设定套接字的文件描述符状态为O_ASYNC
,当内核数据准备就绪,会通过SIGIO
信号通知应用进程;
最后,内容简要对select函数进行了说明,select函数对文件描述符设定了读、写、异常的检测,同时设定了检测的超时事项,最后,附带了一个Select的处理流程;
5. 参考
- 《Unix网络编程》: https://notes.shichao.io/unp/ch6/