标签(空格分隔): 非阻塞 IO


非阻塞I/O

描述符究竟是什么

Unix中所有的I/O的基础构建模块都是一个字节序列。而大多数程序甚至运行在更简单的抽象层次————一个字节流或I/O流上。

进程通过描述符来引用若干个I/O流,描述符又被人们熟知为文件描述符。而管道、文件、FIFO队列、POSIX的IPC机制(消息队列、信号量、共享内存)以及事件队列等都是通过描述符引用I/O流的实例。

描述符或是由诸如open、pipe、socket等系统调用显式创建,或是从父进程处继承。在以下情形描述符会被释放:

  • 进程退出
  • close系统函数被调用
  • 被标记为close on exec的情况下,在函数exec调用后隐式释放

这里注意close on exec————一个进程分叉时,它所有的描述符都会“复制”到子进程中去。如果描述符中有哪一个被标记为close on exec,那么在父进程调用fork之后而子进程执行exec之前,子进程中被标记为close on exec的描述符会被关闭,变为不可用状态

数据转换过程的发生实际上就是是read或write的系统函数被调用

第七章. I/O 系统 概述, 来自《FreeBSD操作系统的设计与实现. 第315页

每个描述符指向一个数据结构,称为内核中的文件条目(file entry),该结构以字节为单位维护一个文件偏移量(file offset),从文件条目对象的开头开始。open系统调用创建一个新的文件条目,其中包含了每单个描述符的文件偏移量。

编辑:fork系统调用致使描述符在父子进程间共享(引用语义上的)。所以父子进程实际上使用同一个描述符,在文件条目中引用同一个偏移量。dup/dup2也使用同样的语义来复制一个文件描述符

在Slack的dist-sys里可见的C示例代码确认了这一点

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(char *argv[]) {
    int fd = open("abc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    fork();
    write(fd, "xyz", 3);
    printf("%ld\n", lseek(fd, 0, SEEK_CUR));
    close(fd);
    return 0;
}

运行结果:

3
6

更有趣的是在描述符被共享的情况下close on exec标志位所做的事。我的猜测是如果该标志位的设置会将这一描述符从子进程描述符表中移除,以至于父进程可以继续使用,而子进程在exec执行之后则无法使用该描述符

因为多个描述符可能会引用同一个文件条目,文件条目的数据结构中为每个描述符维护着相应的偏移量。ReadWrite的调用操作从该偏移量处开始执行,偏移量本身也在每次数据转换后更新自身值。偏移量决定了下一个读或写操作从文件条目的什么位置开始。进程

非阻塞描述符

条件触发

边缘触发

描述符的多路I/O

  • 非阻塞型I/O
  • 信号驱动型I/O
  • 轮询型I/O
  • BSD专用内核事件轮询型I/O(kevent系统调用)

非阻塞型I/O

描述符发生了什么

进程发生了什么

内核发生了什么

缺陷是什么

  • 频繁查询确认:
  • 非频繁查询确认:

什么情况下适用?

信号驱动型I/O

描述符发生了什么

进程发生了什么

内核发生了什么

缺陷是什么

什么情况下适用?

轮询型I/O

描述符发生了什么

进程发生了什么

内核发生了什么

缺陷是什么

什么情况下适用?

BSD内核事件轮询型I/O

描述符发生了什么

进程发生了什么

内核发生了什么

缺陷是什么

什么情况下适用?

POSIX异步I/O

在Linux上,执行并行I/O的另一种方法是使用依赖于glibc基于多线程实现

These functions are part of the library with realtime functions named librt. They are not actually part of the libc binary. The implementation of these functions can be done using support in the kernel (if available) or using an implementation based on threads at user level. In the latter case it might be necessary to link applications with the thread library libpthread in addition to librt.

当然,这种方式也不是没有缺点的:

This has a number of limitations, most notably that maintaining multiple threads to perform I/O operations is expensive and scales poorly. Work has been in progress for some time on a kernel state-machine-based implementation of asynchronous I/O (see io_submit, io_setup, io_cancel, io_destroy, io_getevents), but this implementation hasn’t yet matured to the point where the POSIX AIO implementation can be completely reimplemented using the kernel system calls.

在FreeBSD上,POSIX AIO 通过aio系统调用实现。队列中的I/O操作会由一个异步的“内核进程”(又被称为“内核I/O守护进程”或“AIO守护进程”)来执行。多个AIO守护进程被分到一系列可配置池里。每个池子会根据负载情况增加或移除AIO守护进程。其中一个AIO守护进程池被用于为socket们的异步I/O请求提供服务,另一个则用于服务除对原始磁盘的异步I/O请求以外的全部异步I/O请求

执行一个异步I/O操作过程是这样的:

  • 内核创建一个异步I/O请求结构,结构内包含执行所需相关信息

  • 如果内核缓冲区无法立即容纳请求的资源,该请求结构会进入队列中

  • 如果请求创建时,AIO守护进程不可用,请求结构会进入队列等待进程和系统调用返回

  • 下一个可用的AIO守护进程使用内核的同步路径来处理该请求

  • 守护进程完成I/O后,请求结构被标记为完成,并返回相应结果或错误码

  • 如果I/O操作完成,进程使用aio_error系统调用来轮询,这个是调用通过检查之前内核创建的异步I/O请求结构状态来实现的

  • 如果进程运行到了I/O不完成就无法继续的地方,它可以调用aio_suspend等待直到I/O完成

  • 进程会在AIO请求结构处被投入睡眠,当I/O操作完成时进程会被唤醒,或进程本身设置了当I/O操作完成时发送一个信号给自己中断唤醒

  • 当标识I/O操作完成的aio_suspendaio_error返回或中断信号到达时,使用aio_return系统调用来获得其返回结果

Written with StackEdit.