输入输出系统

Linux下主要的I/O分为:

  • 阻塞I/O(Blocking IO):服务端返回结果之前,客户端线程会被挂起,此时线程不可被CPU调度,线程暂停运行

  • 非阻塞I/O(Non-blocking IO):在服务端返回前,函数不会阻塞客户端线程,而会立刻返回

阻塞和非阻塞的区别在于:客户端线程在调用function后是否立刻返回。

  • 同步I/O(Sync IO):客户端会一直等待服务端响应,直到返回结果

  • 异步I/O(Async IO):客户端发起调用之后立刻返回,不会等待服务端响应。服务端通过通知机制或者回调函数来通知客户端

同步和异步的区别在于:服务端在拷贝数据时是否阻塞客户端线程。

用户态和内核态

Linux系统中分为内核态(Kernel model)和用户态(User model),CPU会在两者之间切换。

  • 内核态:代码拥有完全的底层资源控制权限,可以执行任何CPU指令,访问任何内存地址,其占有的处理机是不允许被抢占的,内核态的程序崩溃会导致PC停机。

  • 用户态:是用户程序能够使用的指令,不能直接访问底层硬件和内存地址。用户态运行的程序必须委托系统调用来访问硬件和内存

内核态的指令包括:

  • 启动I/O

  • 内存清零

  • 修改程序状态字

  • 设置时钟

  • 允许/终止中断

  • 停机

用户态的指令包括:

  • 控制转移

  • 算数运算

  • 取数指令

  • 访管指令(使用户程序从用户态进入内核态)

用户态和内核态的切换

用户态切换到核心态有三种方式:

  • 系统调用

用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心是使用操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。

  • 异常

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

  • 外围设备的中断

当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

进程

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。从一个进程转到另一个进程上运行,整个过程包括:

  • 保存处理机上下文,包括程序计数器和其他寄存器

  • 更新PCB信息

  • 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列

  • 选择另一个进程执行,并更新其PCB

  • 更新内存管理的数据结构

  • 恢复处理机上下文

进程阻塞

正在执行的进程由于一些事情发生,如请求资源失败、等待某种操作完成、新数据尚未达到或者没有新工作做等,由系统自动执行阻塞原语,使进程状态变为阻塞状态。因此,进程阻塞是进程自身的一种主动行为,只有处于运行中的进程才可以将自身转化为阻塞状态。当进程被阻塞,它是不占用CPU资源的。

文件描述符

FD(File Descriptor)用于描述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。

当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

缓存I/O

缓存IO(也称标准IO),大多数文件系统的默认IO 操作都是缓存IO。

在Linux的缓存IO机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存IO的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

Liunx下五种I/O模型

  • 阻塞I/O(blocking IO)

  • 非阻塞I/O (nonblocking I/O)

  • I/O复用 (I/O multiplexing)

  • 信号驱动I/O (signal driven I/O ,SIGIO)

  • 异步I/O (asynchronous I/O)

阻塞I/O模型

进程会一直阻塞,直到数据拷贝完成。应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。数据准备好后,从内核拷贝到用户空间,IO函数返回成功指示。阻塞IO模型图如下所示:

非阻塞I/O模型

通过进程反复调用IO函数,在数据拷贝过程中,进程是阻塞的。模型图如下所示:

I/O复用模型

主要是select和epoll。一个线程可以对多个I/O端口进行监听,当socket有读写事件时分发到具体的线程进行处理。模型如下所示:

实现方式

单进程最大连接数

IO效率

消息传递方式

select

32位:1024

64位:2048

低效率

内核态将消息传递到用户态,需要内核拷贝操作

poll

无限制(基于链表存储)

低效率

内核态将消息传递到用户态,需要内核拷贝操作

epoll

有上限(2G内存支持20万连接数

有活跃的socket才调用callback,效率高

内核态与用户态共享存储实现

select

select是POISIX中规定的,一般操作系统都实现了。

select本质是通过设置或检查存放fd标志位的数据结构来进行下一步处理。缺点是:

  • 单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说和系统内存有关,内核参数/proc/sys/fs/file-max中设置。

    • 32位默认是1024个

    • 64位默认为2048个

  • 对socket进行扫描时是线性扫描,即采用轮询方法,效率低。当套接字比较多的时候,每次select()都要遍历FD_SETSIZE个socket来完成调度,不管socket是否活跃都遍历一遍,会浪费很多CPU时间。

给套接字注册某个回调函数,当活跃时,自动完成相关操作,避免轮询,这正是epoll与kqueue所优化的地方。

需要维护一个用来存放大量fd的数据结构,会使得用户空间和内核空间在传递该结构时复制开销大。

poll

poll本质和select相同。

  1. 将用户传入的数据拷贝到内核空间

  2. 然后查询每个fd对应的设备状态

    1. 如果设备就绪则在设备等待队列中加入一项并继续遍历

    2. 如果遍历所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或主动超时

  3. 被唤醒后又要再次遍历fd

它没有最大连接数的限制,原因是它是基于链表来存储的,但缺点是:

  • 大量的fd的数组被整体复制到用户态和内核空间之间,不管有无意义

  • poll还有一个特点“水平触发”,如果报告了fd后,没有被处理,那么下次poll时再次报告该fd

epoll

epoll是Linux系统特有的。

epoll支持水平触发和边缘触发,最大特点在于边缘触发,只告诉哪些fd刚刚变为就绪态,并且只通知一次。

epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll的优点:

  • 没有最大并发连接的限制

  • 效率提升,只有活跃可用的FD才会调用callback函数

  • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递

信号驱动I/O模型

开启Socket的信号驱动I/O功能,并通过sigaction系统调用安装一个信号处理函数,进程继续运行不会被阻塞。当数据准备好时,内核为该进程发送一个SIGIO信号,进程可以在信号处理函数中调用I/O操作函数处理数据。过程如下图所示:

异步I/O模型

相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。异步过程如下图所示:

五种模型对比

阻塞、非阻塞、IO复用都属于同步IO。

最后更新于

这有帮助吗?