我正在参加「掘金·启航计划」。
想要更好的理解linux的IO模型,首先对于IO的整个过程有一个大概的认识。
IO,Input/Output。本质,是计算机的核心-cpu和内存 与外部设备进行数据交换的过程。比如 数据从磁盘写入内存,或内存的数据写回到磁盘等。
那么,如何交互来达到数据的交换呢?
- 用户进程通过系统调用,用户态进入内核态。
- 而划分内核空间和用户空间,是为了保护内核
- 数据会先被操作系统拷贝到内核缓冲区,再从内核缓冲区拷贝到操作系统的地址空间。
阻塞和非阻塞
阻塞是指io操作需要彻底完成才返回用户空间,过程中用户进程(发起请求io的一方)一直处于阻塞状态。
非阻塞io是指io操作被调用后先返回一个状态值,不需要等io彻底完成。
阻塞io
当用户进程发起系统调用进入内核态,系统内核此时需要准备数据
- 当数据还没到达时,系统内核需要等待足够的数据到来。此时用户进程处于阻塞状态。
当数据准备完成后,系统内核会将数据从内存空间拷贝到用户空间。
非阻塞io
当用户进程发起read操作,如果此时内核中的数据还没准备就绪,就会返回一个错误,而不是block用户进程。用户接收到错误,知道数据未准备就绪,会再次发送read操作。
多路复用io
多路复用也是事件驱动,有时也称异步阻塞io。
为了提高性能,新增一种系统调用,该系统调用可以检测IO文件描述符的状态。在linux中一般为epoll/poll函数。
通过该系统调用,一个用户进程可以监听多个文件描述符。当文件描述符准备就绪时(内核缓冲区数据准备就绪),就会向用户进程返回文件描述符的状态,用户进程根据所返回的文件描述符去进行相应的io调用。
发起新的io系统调用时,内核等待数据过程中可以立即返回,用户线程不会阻塞。
举例来说,发起一个多路复用io的read读操作的系统调用流程如下:
- 选择器注册。将需要read操作的目标socket网络连接提前注册到选择器中,才可以开启整个io多路复用模型的轮训。
- 用户线程发起io系统调用,内核此时数据尚未就绪,因此会立即返回。当用户线程的选择器中所注册的内核缓冲区数据准备就绪后,内核会将socket加入对应的就绪列表。select/epoll是采用轮询的方法来查找达到io操作就绪的socket连接。
- 用户线程获取就绪列表后,根据其中的socket连接进行read。此时用户进程阻塞,内核复制数据从内核缓冲区到用户缓冲区。
- 复制完成后,内核返回结果,用户结束阻塞。
select
io系统调用使用文件描述符来指代打开的文件。 socket是一种常见的文件类型,用来和另一个进程进行跨网络通信的文件。
select方法监听服务端socket是否可读,当所监听的所有socket都不可读,用户线程会阻塞,直到存在可读的socket,select唤醒用户线程。 对于select来说,所处理的fd_set是一个比特位结构(bitmap),每个比特位表示所监听的fd。使用一个bitmap数组来存储所有监听的fd。
- select和poll大致相同,不同在于poll使用的底层结构是链表,而select使用的是数组。
缺点
- 每次调用select(),都需要把文件描述符从用户态拷贝到内核态
- 用户程序会把需要监听的文件描述符从用户空间拷贝到内核空间
- select支持的文件描述符 默认最大为1024
epoll
- 执行epoll_create(),创建eventpoll的结构体。涉及到了红黑树和双向链表。其中红黑树保存和管理所有相关的文件描述符,而双向链表保存和管理处于就绪状态的文件描述符。
struct eventpoll{ ...
// 红黑树的根节点,该树存储所有添加到epoll的需要监控的socket
// o(logn) 对socket进行增删改查
struct rb_root rbr;
// 双向链表存放已就绪的socket
struct list_head rdlist;
}
- 执行epoll_ctl(),将要监控的文件描述符从用户空间拷贝到内核空间,将被监听的fd作为一个节点加入红黑树。
- 当一个socket的读事件或写时间就绪,就将该socket的文件描述符添加到双向链表。
- 调用epoll_wait(),检查就绪链表是否存在节点,并将就绪链表中的fd拷贝到用户空间,如果就绪链表为空,epoll_wait()会一直阻塞。
对比
- epoll没有可监控的最大文件描述符的限制
- select的每次调用都需要将fd从用户态拷贝到内核态,但是epoll只需要在刚开始确定监听文件的时候拷贝到内核空间,并使用红黑树管理。
- 当socket轮询响应就绪fd后,还需要遍历所有的文件描述符来返回。而epoll通过双向链表会直接返回就绪的fd。
Level trigger
水平触发通知时,用户进程可以随时检查文件描述符的状态,因此可以重复检查io状态,执行更多的io。
Edge Trigger
边缘触发只有在io事件发生时用户进程才会收到通知
异步IO
按照之前的io模型,是由用户线程发起io请求,调用内核缓存区。但在异步io中,用户线程成为了被调用者,用户线程发起io请求后就可以进行其他操作,当内核缓冲区中数据准备就绪后,会主动通知用户进程。
参考
juejin.cn/post/701206…
blog.csdn.net/liyifan687/…
www.zbpblog.com/blog-211.ht…