Linux IO模式及 select、poll、epoll详解
一概念说明:
用户空间和内存空间
- 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。
- 操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
- 为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
- 针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间
- 将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间
内核空间: 可以操作底层硬件设备的权限,用户空间只能操作该进程的权限
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换
任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
- 保存处理机上下文,包括程序计数器和其他寄存器。
- 更新PCB信息。
- 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
- 选择另一个进程执行,并更新其PCB。
- 更新内存管理的数据结构。
- 恢复处理机上下文。
进程的阻塞
- 正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。
- 进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。
BIO 就是因为进程阻塞,导致升级成NIO
当进程进入阻塞状态,是不占用CPU资源的。
-
当CPU在工作队列中获取到进程A
-
执行A操作根据文件列表
-
当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中(如下图)。
-
由于工作队列只剩下了进程B和C,cpu会轮流执行这两个进程的程序,不会执行进程A的程序。
-
所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。
-
当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,此行为: 唤醒进程
文件描述符fd
- 文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
- 文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
- 当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
Linux 一切皆文件
缓存IO
- 缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。
- 在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中
- 数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
- 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
IO模式
- 对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 信号驱动 I/O( signal driven IO)
- 异步 I/O(asynchronous IO)
注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。
阻塞IO
- 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据
- 对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来
- 这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。
- 当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
用户进程会等两阶段 : 1 等待数据到达内核 2 等待kernel 把数据从内核copy到用户空间
- 当进程8调用kernel时,两阶段数据没有准备好
- kernel阻塞等待
- 当进程8数据准备完成,调用client8
- 得到client8 数据返回
- 进程9才会调用到kernel
非阻塞IO
- 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。
- 从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。
- 用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。
- 一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
采用轮训的机制
IO多路复用
- IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。
- select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
- 它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
多路复用执行流程:
- 当用户进程调用了select,那么整个进程会被block
- 而同时,kernel会“监视”所有select负责的socket
- 当任何一个socket中的数据准备好了,select就会返回。
- 这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
select
-
调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。
-
当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。
-
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
-
select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。
poll
- pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。
- 同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
- 从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
poll 跟 select 的区别在于 select 有数量限制,poll 没有
epoll
- epoll是在2.6内核中提出的,是之前的select和poll的增强版本。
- 相对于select和poll来说,epoll更加灵活,没有描述符限制。
- epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll 内核空间+用户空间 有一个共享空间,减少数据的copy