前言
本文从基本的原理上了解用户空间、内核空间、进程上下文、及系统的五种常用I/O模型,加深对Linux系统的理解。
1. 概念说明
1.1 用户空间与内核空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。空间分配如下图所示:
有了用户空间和内核空间,整个linux内部结构可以分为三部分,从最底层到最上层依次是:硬件-->内核空间-->用户空间。如下图所示:
需要注意的细节问题:
(1) 内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。
(2) Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。
内核态与用户态:
(1)当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
(2)当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。
参考资料:http://www.cnblogs.com/Anker/p/3269106.html
1.2 进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
- 保存处理器上下文,包括程序计数器和其他寄存器。
- 更新PCB信息。
- 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
- 选择另一个进程执行,并更新其PCB。
- 更新内存管理的数据结构。
- 恢复处理器上下文。
1.3 阻塞与非阻塞
关注的是调用者等待被调用者返回调用结果时的状态。
- 阻塞:调用结果返回之前,调用者会被挂起(不可中断睡眠态),调用者只有在得到返回结果之后才能继续;
- 非阻塞:调用者在结果返回之前,不会被挂起;即调用不会阻塞调用者,调用者可以继续处理其他的工作;
1.4 同步与异步
关注的是消息通知机制、状态;
- 同步:调用发出之后不会立即返回,但一旦返回则是最终结果;
- 异步:调用发出之后,被调用方立即返回消息,但返回的并非最终结果;被调用者通过状态、通知机制等来通知调用者,会通过回调函数处理;
1.5 文件描述符
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
1.6 缓存I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
2. 常用模型介绍
- 阻塞式I/O:blocking I/O
- 非阻塞式I/O:non-blocking I/O
- I/O复用: I/O multiplexing
- 信号驱动式I/O:signal driven I/O
- 异步I/O:asynchronous I/O
从磁盘上的一次read操作;用户发起IO调用
用户空间进程无法直接访问硬件;
进程无法直接访问内核缓冲区;
1)用户空间的进程向linux内核发起IO调用(读取文件)请求;
2)内核从磁盘中读取数据;(等待数据完成阶段。)
3)内核将数据加载至内核缓冲区;(等待数据完成阶段。)
4)内核在将内核缓冲区中的数据复制到用户空间中的进程内存中(真正执行IO过程的阶段)
2.1 阻塞式I/O:blocking I/O
阻塞式I/O模型是最常用的一个模型,也是最简单的模型。在linux中,所有的套接字默认情况下都是阻塞的。
进程在向内核调用执行recvfrom操作时阻塞,只有当内核将磁盘中的数据复制到内核缓冲区(内核内存空间),并实时复制到进程的缓存区完毕后返回;或者发生错误时(系统调用信号被中断)返回。在加载数据到数据复制完成,整个进程都是被阻塞的,不能处理的别的I/O,此时的进程不再消费CPU时间,而是等待响应的状态,从处理的角度来看,这是非常有效的。
阻塞式I/O整个过程图如下:
第一阶段(阻塞):
- ①:进程向内核发起系统调用(recvfrom);当进程发起调用后,进程开始挂起(进程进入不可中断睡眠状态),进程一直处于等待内核处理结果的状态,此时的进程不能处理其他I/O,亦被阻塞。
- ②:内核收到进程的系统调用请求后,此时的数据包并未准备好,此时内核亦不会给进程发送任何消息,直到磁盘中的数据加载至内核缓冲区;
第二阶段(阻塞):
- ③:内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成。
- ④:内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作。
特点:I/O执行的两个阶段进程都是阻塞的。
优点:
? 1)能够及时的返回数据,无延迟;
? 2)程序简单,进程挂起基本不会消耗CPU时间;
缺点:
? 1)I/O等待对性能影响较大;
? 2)每个连接需要独立的一个进程/线程处理,当并发请求量较大时为了维护程序,内存、线程和CPU上下文切换开销较大,因此较少在开发环境中使用。
2.2 非阻塞式I/O:non-blocking I/O
进程在向内核调用函数recvfrom执行I/O操作时,socket是以非阻塞的形式打开的。也就是说,进程进行系统调用后,内核没有准备好数据的情况下,会立即返回一个错误码,说明进程的系统调用请求不会立即满足(WAGAIN或EWOULDBLOCK)。
该模型在进程发起recvfrom系统调用时,进程并没有被阻塞,内核马上返回了一个error。进程在收到error,可以处理其他的事物,过一段时间在次发起recvfrom系统调用;其不断的重复发起recvfrom系统调用,这个过程即为进程轮询(polling)。轮询的方式向内核请求数据,直到数据准备好,再复制到用户空间缓冲区,进行数据处理。需要注意的是,复制过程中进程还是阻塞的。
一般情况下,进程采用轮询(polling)的机制检测I/O调用的操作结果是否已完成,会消耗大量的CPU时钟周期,性能上并不一定比阻塞式I/O高;
非阻塞式I/O模型的整个过程图如下:
第二阶段(非阻塞):
- ①:进程向内核发起IO调用请求,内核接收到进程的I/O调用后准备处理并返回“error”的信息给进程;此后每隔一段时间进程都会想内核发起询问是否已处理完,即轮询,此过程称为为忙等待;
- ②:内核收到进程的系统调用请求后,此时的数据包并未准备好,此时内核会给进程发送error信息,直到磁盘中的数据加载至内核缓冲区;
第二阶段(阻塞):
- ③:内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段,进程阻塞),直到数据复制完成。
- ④:内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;
特点:non-blocking I/O模式需要不断的主动询问kernel数据是否已准备好。
优点:进程在等待当前任务完成时,可以同时执行其他任务;进程不会被阻塞在内核等待数据过程,每次发起的I/O请求会立即返回,具有较好的实时性;
缺点:不断的轮询将占用大量的CPU时间,系统资源利用率大打折扣,影响性能,整体数据的吞吐量下降;该模型不适用web服务器;
非阻塞式IO相对于阻塞式IO,性能并没有提升;因为轮询是很耗资源的。不断的轮询意味着内核需要腾出更多的时间来反馈当前的处理情况,很可能没办法处理第②步了。
2.3 I/O 复用:I/O multiplexing
I/O multiplexing 模型也成为事件驱动式I/O模型(event driven I/O)。
在该模型中,每一个socket,一般都会设置成non-blocking;进程通过调用内核中的select()、poll()、epoll()函数发起系统调用请求。selec/poll/epoll相当于内核中的代理,进程所有的请求都会先请求这几个函数中的某一个;此时,一个进程可以同时处理多个网络连接的I/O;select/poll/epoll这个函数会不断的轮询(polling)所负责的socket,当某个socket有数据报准备好了(意味着socket可读),就会返回可读的通知信号给进程。
用户进程调用select/poll/epoll后,进程实际上是被阻塞的,同时,内核会监视所有select/poll/epoll所负责的socket,当其中任意一个数据准备好了,就会通知进程。只不过进程是阻塞在select/poll/epoll之上,而不是被内核准备数据过程中阻塞。此时,进程再发起recvfrom系统调用,将数据中内核缓冲区拷贝到内核进程,这个过程是阻塞的。
虽然select/poll/epoll可以使得进程看起来是非阻塞的,因为进程可以处理多个连接,但是最多只有1024个网络连接的I/O;本质上进程还是阻塞的,只不过它可以处理更多的网络连接的I/O而已。
I/O复用模型的整个过程图如下:
第一阶段(阻塞在select/poll之上):
- ①:进程向内核发起select/poll的系统调用,select将该调用通知内核开始准备数据,而内核不会返回任何通知消息给进程,但进程可以继续处理更多的网络连接I/O;
- ②:内核收到进程的系统调用请求后,此时的数据包并未准备好,此时内核亦不会给进程发送任何消息,直到磁盘中的数据加载至内核缓冲区;而后通过select()/poll()函数将socket的可读条件返回给进程
第二阶段(阻塞):
- ③:进程在收到SIGIO信号程序之后,进程向内核发起系统调用(recvfrom);
- ④:内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成。
- ⑤:内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作。
特点:通过一种机制能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个变为可读就绪状态,select()/poll()函数就会返回。
优点:可以基于一个阻塞对象,同时在多个描述符上可读就绪,而不是使用多个线程(每个描述符一个线程),即能处理更多的连接;这样可以节省更多的系统资源。
缺点:如果处理的连接数不是很多的话,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好;反而可能延迟还更大;原因在于,处理一个连接数需要发起两次system call;
2.4 信号驱动式I/O:signal driven I/O
信号驱动式I/O是指进程预先告知内核,使得某个文件描述符上发生了变化时,内核使用信号通知该进程。
在信号驱动式I/O模型,进程使用socket进行信号驱动I/O,并建立一个SIGIO信号处理函数,当进程通过该信号处理函数向内核发起I/O调用时,内核并没有准备好数据报,而是返回一个信号给进程,此时进程可以继续发起其他I/O调用。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。当数据报准备好之后,内核会递交SIGIO信号,通知用户空间的信号处理程序,数据已准备好;此时进程会发起recvfrom的系统调用,这一个阶段与阻塞式I/O无异。也就是说,在第二阶段内核复制数据到用户空间的过程中,进程同样是被阻塞的。
信号驱动式I/O的整个过程图如下:
第一阶段(非阻塞):
- ①:进程使用socket进行信号驱动I/O,建立SIGIO信号处理函数,向内核发起系统调用,内核在未准备好数据报的情况下返回一个信号给进程,此时进程可以继续做其他事情;
- ②:内核将磁盘中的数据加载至内核缓冲区完成后,会递交SIGIO信号给用户空间的信号处理程序;
第二阶段(阻塞):
- ③:进程在收到SIGIO信号程序之后,进程向内核发起系统调用(recvfrom);
- ④:内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成。
- ⑤:内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作。
特点:借助socket进行信号驱动I/O并建立SIGIO信号处理函数
优点:线程并没有在第一阶段(数据等待)时被阻塞,提高了资源利用率;
缺点:
? 1)在程序的实现上比较困难;
? 2)信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失。
信号通知机制:
水平触发:指数据报到内核缓冲区准备好之后,内核通知进程后,进程因繁忙未发起recvfrom系统调用;内核会再次发送通知信号,循环往复,直到进程来请求recvfrom系统调用。很明显,这种方式会频繁消耗过多的系统资源。
边缘触发:内核只会发送一次通知信号。
2.5 异步I/O:asynchronous I/O
异步I/O可以说是在信号驱动式I/O模型上改进而来。
在异步I/O模型中,进程会向内核请求air_read(异步读)的系统调用操作,会把套接字描述符、缓冲区指针、缓冲区大小和文件偏移一起发给内核,当内核收到后会返回“已收到”的消息给进程,此时进程可以继续处理其他I/O任务。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。第二阶段,当数据报准备好之后,内核会负责将数据报复制到用户进程缓冲区,这个过程也是由内核完成,进程不会被阻塞。复制完成后,内核向进程递交aio_read的指定信号,进程在收到信号后进行处理并处理数据报向外发送。
在进程发起I/O调用到收到结果的过程,进程都是非阻塞的。
异步I/O模型的整个过程图如下:
第一阶段(非阻塞):
- ①:进程向内核请求air_read(异步读)的系统调用操作,会把套接字描述符、缓冲区指针、缓冲区大小和文件偏移一起发给内核,当内核收到后会返回“已收到”的消息给进程
- ②:内核将磁盘中的数据加载至内核缓冲区,直到数据报准备好;
第二阶段(非阻塞):
- ③:内核开始复制数据,将准备好的数据报复制到进程内存空间,知道数据报复制完成
- ④:内核向进程递交aio_read的返回指令信号,通知进程数据已复制到进程内存中;
特点:第一阶段和第二阶段都是有内核完成
优点:能充分利用DMA的特性,将I/O操作与计算重叠,提高性能、资源利用率与并发能力
缺点:
? 1)在程序的实现上比较困难;
? 2)要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 复用式I/O模型为主。
3. 五种I/O模型总结
阻塞式I/O与非阻塞式I/O的区别:
阻塞式I/O会一直阻塞对应的进程,直至操作完成;
非阻塞式I/O在内核还在准备数据的情况下,会立即返回“error”给对应的进程;
同步I/O和异步I/O的区别:
根据POSIX中的定义:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- 同步I / O操作会导致请求进程被阻塞,直到I / O操作完成为止;
An asynchronous I/O operation does not cause the requesting process to be blocked;
- 异步I / O操作不会导致请求进程被阻止;
两者的区别就在于,同步I/O在“I/O operation” 会将进程阻塞。按照这个定义,并根据图-7和图-8所示,阻塞式I/O、非阻塞式 I/O、I/O 复用、信号驱动式I/O都属于同步I/O;
这里需要注意的是,非阻塞式I/O并没有在第一阶段并没有阻塞,但是POSIX中定义的“I/O operation” 只指的整个I/O操作,即recvfrom这个system call的时候。如果数据报没准备好,不会阻塞进程,一旦数据准备好,recvfrom将内核缓冲区中将数据报拷贝到用户进程时,这个时候进程是被阻塞的;因此,阻塞式I/O属于同步I/O
异步I/O则不一样,当进程发起I/O调用请求后,内核立即返回,进程也不会管这个I/O操作了。直到内核发送通知信号给进程,通知进程数据报已复制到用户进程,进程直接进进程缓冲区处理数据;因此,进程整个过程一直没有被阻塞。
如图-7、图-8所示
<center>图-7</center>
图-8
这五种 I/O 模型中,前四种属于同步 I/O,因为其中真正的 I/O 操作(recvfrom)将阻塞进程/线程,只有异步 I/O 模型才与 POSIX 定义的异步 I/O 相匹配。
4. select/poll/epoll对比
select/poll/epoll对比,如下图所示
其中,遍历相当于查看所有的位置,回调相当于查看对应的位置
Select
POSIX所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理
缺点:
- 单个进程可监视的fd数量被限制,即能监听端口的数量有限,数值存在如下文件里
cat /proc/sys/fs/file-max
- 对socket是线性扫描,即采用轮询的方法,效率较低
- select采取了内存拷贝方法来实现内核将FD消息通知给用户空间,这样一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll
本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态
- 其没有最大连接数的限制,原因是它是基于链表来存储的
- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义
- poll特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd
- 边缘触发:只通知一次,epoll用的就是边缘触发
epoll
在Linux2.6内核中提出的select和poll的增强版本
- 支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次
- 使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
优点:
- 没有最大并发连接的限制:能打开的FD的上限远大于1024(1G的内存能监听约10万个端口)
- 效率提升:非轮询的方式,不会随着FD数目的增加而效率下降;只有活跃可用的FD才会调用callback函数,即epoll最大的优点就在于它只管理“活跃”的连接,而跟连接总数无关
- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销
- 文件映射内存直接通过地址空间访问,效率更高,把文件映射到内存中。
5. httpd和nginx使用的模型?
5.1 httpd使用的模型:
prefork模型是复用性I/O模型,以多进程的方式处理请求;
worker模型是复用性I/O模型,只能不过是以多进程多线程的方式处理请求。
event模型是使用的信号驱动式I/O模型,httpd2.4也实现异步I/O模型。
5.2 nginx使用的模型:
nginx一开始设计就是基于信号驱动式I/O模型,使用边缘触发来实现,所以并发能力强、性能好,并且支持异步I/O
能完成基于mmap内存映射机制完成数据的发放。
完!
原文地址:https://blog.51cto.com/ccschan/2357207