Linux 的 IO 模型
Posted technologyDaily
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 的 IO 模型相关的知识,希望对你有一定的参考价值。
Linux IO 模型介绍及对应 Python 代码演示。
几个基本概念
用户空间和内核空间
我们都知道,操作系统的核心就是内核,独立于普通的应用程序,可以访问受保护的内存区域,也有访问底层硬件设备的权利。操作系统将虚拟空间分成两部分,一部分是内核空间,一部分是用户空间。内核对外提供一些系统调用,内核被所有的用户进程所共享,用户进程可以通过系统调用间接地操作受保护的内存区域和底层硬件。
文件描述符
文件描述符就是一个用于表述指向文件的引用的抽象概念。当程序打开或者创建一个文件的时候,内核向进程返回一个文件描述符。
缓存IO
缓存IO的好处:
1、缓存 I/O 使用了操作系统内核缓冲区,在一定程度上分离了应用程序空间和实际的物理设备。
2、缓存 I/O 可以减少读盘的次数,从而提高性能。
IO模式
一个read操作发生的时候,会经历两个阶段:
1、 等待数据的准备
2、 将数据从内核拷贝到进程中
正是因为这两个阶段,linux系统产生了下面五种网络模式的方案:
(1)、阻塞IO (2)、非阻塞IO (3)、I/O多路复用
(4)、信号驱动IO (5)、异步IO
由于信号驱动IO使用的场景比较少,就不多做介绍。
IO 模型
阻塞 IO
阻塞IO就是当用户进程发起一个系统调用,比如read,内核接收到这个请求之后,会开始准备数据,进行读盘,将数据先拷贝到内核的缓冲区中(缓存IO,必须先将数据拷贝到内核的缓冲区,再拷贝到用户的进程中)。在内核准备数据的过程中,用户进程会一直被阻塞,直到内核准备好了数据,再将数据从内核的缓冲区拷贝到用户进程中。
Python示例代码(这里使用网络编程的例子来演示,传输层使用TCP协议):
Server.py
Client.py
非阻塞 IO
在非阻塞IO模型下,用户进程发起一个read操作,如果内核中的数据还没有准备好,内核会立刻向用户进程返回一个标识。用户进程不会被阻塞,用户进程得到数据还没准备好的标识时,可以再次发送read操作。一旦内核中的数据准备好了,并且用户进程再次发起了系统调用,它马上就将数据拷贝到用户内存,然后返回。
非阻塞IO就是用户进程不停地主动地询问内核中的数据准备好了没有。
异步IO(因IO多路复用相对复杂一些,故放到最后)
用户进程发起read操作之后,可以立刻去做别的事情,不用等待内核的回应。从kernel的角度,当它受到一个异步read之后,首先它会立刻返回,所以用户进程不会阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户进程,当这一切都完成之后,内核会给用户进程发送一个标识,告诉它read操作完成了。
IO多路复用
IO多路复用就是我们说的select,poll,epoll。IO多路复用的好处就在于单个process就可以同时处理多个网络连接的IO。下面将逐一介绍它们三个。
select
以上便是select函数,select函数会监听文件描述符,一旦有感兴趣的事件(比如读操作、写操作等等)发生,就会通知用户进程。select监听三种文件描述符,分别是readfds、writefds、exceptfds,select函数在调用的时候会把这三种类型的文件描述符从用户进程拷贝到内核空间中,之后select函数会一直阻塞,直到有描述符就位,函数返回。用户进程接收到消息之后,会去遍历fdset的集合,来找到有就绪事件的描述符。
select缺点:
单个进程能监视的文件描述符的数量存在最大的限制
得到内核的通知后,需要遍历fdset,才能知道哪些描述符有就绪事件
select函数在调用的时候,会把fdset的三个集合拷贝到内核空间,比较消耗资源
poll
以上便是poll函数,poll函数是用链表来实现的,它和select实现上不同的地方就是select是按照描述符需要监听的事件来存储,监听同一事件的描述符被存储在同一个集合中。而poll的每个节点存储一个描述符,并且会存储其需要监听的事件和其回调需要触发的事件。它和select极为相似,唯一的不同就是poll单个进程能监听的文件描述符的数量是没有限制的,但是其得到内核的通知后,依然需要遍历fd的集合来确定哪些描述符发生了感兴趣的事件。select的一些问题poll并未解决。
poll和select太相似,不再列举代码
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中
epoll操作过程共需要下面三个接口:
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是为了限制epoll所能监听的描述符的最大个数,只是对内核初始化分配内部数据结构的一个建议。创建好epoll句柄之后,就会占用一个fd,使用完epoll后,必须关闭,否则可能会导致fd值耗尽。
函数是对指定描述符fd执行op操作。
– epfd:是epoll_create()的返回值。
– op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
– fd:是需要监听的fd(文件描述符)
– epoll_event:是告诉内核需要监听什么事,
struct epoll_event结构如下:
//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。
参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。
该函数返回需要处理的事件数目,如返回0表示已超时。
epoll工作模式
LT模式
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket。
在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
epoll总结
在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。
epoll优点
1、一个进程能监视的文件描述符的数量是没有限制的
2、IO效率不会随着监视的fd的数量增长而下降
3、内核和用户进程用共享内存的方式通信,相比select或者poll在用户进程和内核空间来回拷贝fd的信息来说,效率更高。
epoll实现的一个简单的服务器(ET模式):
重要部分加了注释。ET模式下只要EPOLLIN或者EPOLLOUT事件发生了 就要一直读取或者一直写入 直到返回了EAGAIN参数,
这点要和LT模式区分开来。
以上是关于Linux 的 IO 模型的主要内容,如果未能解决你的问题,请参考以下文章