阻塞 I/O
如上图, 我们写的应用程序使用数据流 (UDP)Socket 接收数据, 调用 recvfrom() 系统函数接收网卡上的数据. 在阻塞 I/O 下, 调用 recvfrom()将造成进程阻塞, 直到数据接收完毕. 内存空间有系统空间和用户空间, 并且都有相应的缓冲区:
第一阶段(wait for data 等待数据准备): 进程阻塞, 内核接管, 复制数据到系统空间里的数据缓冲区(当然是能够从网卡上接收到数据). 接收完毕后, 则数据准备完毕(datagram ready).
第二阶段(copy data from kernel to user 从内核拷贝数据到用户缓冲区): 这时候进程仍然阻塞, 内核将系统空间内缓冲区 ready 的数据复制到用户空间里的用户自己定义的缓冲区. 拷贝完毕则唤醒进程. 整个阶段, 进程都是阻塞的, 直到数据读取到用户缓冲区内.
非阻塞 I/O
如上图, 同样应用程序调用数据流 (UDP)Socket 接收数据, 调用 recvfrom() 系统函数, 并且设置了 Socket 为 non-blocking 非阻塞. 这时我们调用系统函数 recvfrom()则不阻塞进程, 如果数据没有在系统空间的缓冲区内准备好, 则立即返回一个 error, 直接得到一个结果, 这时候进程可以做点其他任务而不用阻塞在这里. 但是如果数据在系统空间内的缓冲区准备好了, 则将开始从系统空间拷贝数据到用户空间.
I/O 多路复用(IO multiplexing)
I/O 多路复用大致情况如上图. I/O 多路复用也就是我们常说的 select,poll 以及 epoll, 它能够使一个进程能够同时处理更多的连接. 这里就以 select 为例: select()也是一个系统调用, 如果一个进程调用了 select()那么这个进程也会被阻塞. 第一阶段: 应用程序进程调用 select(), 这时该进程阻塞, 不过内核会监视应用程序向 select 注册的所有 Socket. 如果注册的 Socket 中有数据以及在系统空间缓冲区里准备好了的, 则该进程被唤醒. 第二阶段: 从 select()的阻塞中返回, 应用程序进程只能知道向 select 注册的 Socket 里有数据准备好的 Socket, 但是并不知道具体是哪个 Socket 数据准备好了. 所以, 应用程序需要自己遍历 Socket, 找出数据准备好的 Socket, 然后调用 recvfrom()函数将系统空间缓冲区内数据复制到用户空间缓冲区内.
信号驱动 I/O(signal driven IO)
Linux 内核定义了很多信号, 如 SIGUSR1 和 SIGUSR2 都是可以用户自定义发送 / 处理的信号. 应用程序首先需要安装信号处理器 (即提供一个回调函数给内核), 当相应事件发生的时候, 操作系统会激活该信号, 处理权交给进程, 由进程调用处理该信号的回调函数. 在信号驱动 I/O 中, 应用程序先安装信号处理器, 返回结果, 进程可以继续执行. 当数据在系统空间内准备好了后, 内核会激活信号, 进程则会调用相应的回调函数(这里数据准备好了则直接调用 recvfrom() 函数), 然后数据则直接拷贝到用户空间缓冲区内.
异步 I/O(asynchronous IO)
异步 I/O 则是调用 aio_read()系统函数, 传入了用户空间缓冲区的地址指针. 调用后进程不会被阻塞, 则可以做其他事情, 内核会在将数据复制到系统空间缓冲区, 然后再将数据复制到用户提供的用户缓冲区地址中. 都做完了后, 则内核发送一个 signal 通知应用程序 read 完成了. 这时应用程序之前提供的回调函数, 由该进程来执行.
来源: https://juejin.im/post/5c42afc36fb9a04a09565746