本文包含4种 IO模型
的介绍、实现原理分层解析、驱动编写思路和代码实现、以及应用层使用方法。
IO模型的分析
IO 模型是指应用程序在调用 read
函数的时候,如果数据没有准备好,此时进程会发生什么样的状态转换,以及什么时候会返回。在Linux中有五种IO模型,分别是阻塞IO模型,非阻塞IO模型,IO多路复用IO模型,信号驱动IO模型,异步IO模型。下面是对它的分析
以下内容仅为我的个人积累,详细内容请参考官方文档和相关书籍。
阻塞IO模型
在使用open打开设备文件的时候,如果没有指定 O_NONBLOCK
,就说明使用的阻塞方式打开的文件。调用read函数想要从硬件中读取数据的时候,如果数据准备好了 read
就会立即返回,如果调用 read
的时候硬件的数据没有准备好进程休眠。当数据准备好的时候底层硬件会产生中断,内核的中断处理函数就会执行了,在中断处理函数中唤醒休眠的进程,将准备好的数据拷贝到用户空间即可。
阻塞IO模型的代码实现流程
应用层
1 | fd = open("/dev/mycdev",O_RDWR); //阻塞打开 |
驱动层
1 | /* file_operations: */ |
IO多路复用IO模型
在同一个app应用程序中如果想要同时监听多个fd对应数据。就需要使用 select/poll/epoll
来完成监听。如果所有的文件描述符的数据都没有准备好,此时进程休眠。如果有一个或者多个硬件的数据准备好就会产生硬件中断,在处理函数中唤醒休眠的进程。此时 select/poll/epoll
就会返回,从就绪的表中找到准备好数据的文件描述符,然后调用 read
将数据读取到用户空间即可。
IO多路复用IO模型的代码实现流程
应用层
1 | int fd1,fd2; |
驱动层
1 | /* file_operations:(应用层select/poll/epoll对应驱动都是poll函数) */ |
IO多路复用IO模型的实现原理
应用层
1 | select(fd2 + 1, &rfds, NULL, NULL, NULL); |
虚拟文件系统层
首先使用 vi -t sys_select
命令查看 select
函数的实现
- 对最大文件描述符的值作校验工作
- 在内核空间分配6张表的内存,其中前3张表用于保存用户传递到内核的文件描述符后三张表用于保存就绪的文件描述符(后三张表此时是空的)
- 遍历文件描述符
mask = rfds-->fd-->fd_array[fd]-->file-->f_op-->poll(file,wait);
判断mask返回的值,如果所有的文件描述对应驱动poll函数返回的值都是0,说明所有文件描述符的数据都没准备好,构造等待队列,进程休眠 - 如果一个或者多个文件描述符对应的数据准备好了,就会唤醒这个休眠的进程
- 再次遍历文件描述符
mask = rfds-->fd-->fd_array[fd]-->file-->f_op-->poll(file,wait);
找出mask不为0的文件描述符,将这个文件描述符放到就绪的文件描述符表中 - 将就绪的文件描述表拷贝到用户空间
总结
总的来说 select
poll
epoll
的实现原理都是一样的,只是在实现的时候有一些细节上的差别。
select (结构体)
- select监听的最大文件描述符限制1024
- select的内部实现又清空表的过程,需要反复构造表,从用户空间向内核空间拷贝表,效率低
- select从休眠状态被唤醒之后需要再次遍历文件描述符表,效率比较低
poll (链表)
- poll监听的文件描述符没有个数限制
- poll没有清空表的过程,效率高
- poll从休眠状态被唤醒之后需要再次遍历文件描述符表,效率比较低
epoll (红黑树+双链表)
- epoll监听的文件描述符没有个数限制
- epoll没有清空表的过程,效率高
- epoll监听的文件描述符就绪之后它能够直接拿到就绪的文件描述符,不需要遍历,效率高
epoll_ctl
支持管道,FIFO,套接字,POSIX消息队列,终端,设备等,但是就是不支持普通文件或目录的fd
异步通知IO模型
当底层硬件的数据准备好的时候会产生硬件中断,在驱动的中断处理函数中给对应的进程发送信号,当进程收到信号的时候去读取数据,当没有收到信号的时候进程可以执行任意操作。
信号和中断不同,中断是基于硬件实现的,而信号是基于软件实现的是中断的一种模拟,如果没有操作系统那么就没有信号。
异步通知IO模型的代码实现流程
应用层
首先在系统的信号中有一个 29) SIGIO
就是专门留给IO模型使用的,可以在终端通过 kill -l
命令查看
1 | // 信号处理函数 |
虚拟文件系统层
首先可以使用 vi -t sys_fcntl
命令查看 fcntl
函数的实现
1 | // 可知首先执行的是 |
驱动层
1 | /* file_operations: */ |