Basic Concepts

  • 用户空间 VS 内核空间

    • 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)

    • 操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。

    • 为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。

    • 针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

  • 进程切换:为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。

    • 过程:

      1. 保存处理机上下文,包括PC和其他寄存器

      2. 更新PCB信息

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

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

      5. 更新内存管理的数据结构

      6. 恢复处理机上下文

  • 进程阻塞:

    • 正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。 [进程主动行为]

    • 不占用CPU资源

  • 文件描述符fd

    • File descriptor:用于表述指向文件的引用的抽象化概念。

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

  • 缓存IO

    • 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。

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

    • 缺点:

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

 

 

Linux IO模型

  • 网络IO本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为读流的操作。

  • 根据上面介绍到的缓存IO,我们知道对于一次IO访问,要经历两个阶段。下面以read为例

    1. 第一阶段:等待数据准备(waiting for the data to be ready)

    2. 第二阶段:将数据从内核拷贝到进程中。

  • 对socket流而言:

    1. 第一步:图层设计等待网络上的数据分组到达,然后被复制到内核的某个缓冲区

    2. 第二步:将数据从内核缓冲区复制到应用程序缓冲区。

  • 网络IO模型大致有如下几种

    • 同步模型(synchronous IO)

    • 阻塞IO(bloking IO)

    • 非阻塞IO(non-blocking IO)

    • 多路复用IO(multiplexing IO)

    • 信号驱动式IO(signal-driven IO)

    • 异步IO(asynchronous IO)

 

同步阻塞IO

  • blocking io

  • linux默认情况下所有socket都是blocking

  • 应用程序执行系统调用recvform后,整个过程都是阻塞的。 [处于一种不再消费CPU而是简单等待响应的状态]

  • 流程

    1. 进程调用recv()/recvform()系统调用

    2. kernel开始IO的第一个阶段:准备数据 [需要等待网络IO数据的到来]

    3. 第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

  • 优点:

    • 及时返回数据,无延迟

    • 内核开发者比较省事

  • 缺点:

    • 对用户而言有性能代价

 

同步非阻塞IO

  • nonblocking io

  • 同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。 [ 非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error]

  • polling: 进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。

  • 拷贝数据整个过程,进程仍然是属于阻塞的状态。

  • 优点:

    • 能在等待任务完成的时间里干其他活

  • 缺点:

    • 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低

IO多路复用

  • 由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间。

  • “希望有人帮忙做polling”: UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。 [系统调用]

  • select轮询相对非阻塞的轮询的区别在于---前者可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。select或poll调用之后,会阻塞进程,与blocking IO阻塞不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。

  • I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。

  • 多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了,当然也可以针对不同的编号。

  • select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO

  • 它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

  • 流程:

    • 用户进程调用select,整个进程被block

    • kernel“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。

    • 用户进程再调用read操作,将数据从kernel拷贝到用户进程。

  • 优缺点:如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

  • IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。

Part-Summary

  • 对于上述三个IO模型(同步阻塞,同步非阻塞,IO多路复用),它们从整个IO过程来看,他们都是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。

异步非阻塞IO(AIO)

  • 过程

    1. 用户进行aio_read系统调用

    2. 无论内核数据是否准备好,都会直接返回给用户进程

    3. 用户进程可以去做别的事情 (非阻塞)

    4. 等socket数据准备好,内核直接复制数据给进程

    5. 然后从内核向进程发送通知

    6. [IO两个阶段,进程都是非阻塞的]

  • 通知

    • 在linux中,通知的方式是信号

      • 处理信号的方式: [取决于进程当前状态]

        • 如果是在用户态:

          • 强行打断,调用实现注册的信号处理函数

          • 但该函数可以决定何时以及如何处理该异步任务

          • [一般来说,一般是把事件”登记“一下放进队列,然后返回该进程原来在做的事]

        • 如果是在内核态:

          • 当前通知被挂起,等到进程回到用户态再触发信号通知。

        • 如果正在被挂起:

          • 就把该进程唤醒,等到下次被CPU调度时再触发信号通知。

关于异步阻塞

  • 异步+阻塞的存在理由有两方面

    • 为了在异步环境里模拟”顺序执行“的效果,就需要把同步代码转换成异步形式,这称为CPS(Continuation Passing Style)变换。 [这里也可以知道异步非阻塞方式是不保证顺序执行的]

    • 降低响应延迟。 (如果采用非阻塞方式,一个任务 A 被提交到后台,就开始做另一件事 B,但 B 还没做完,A 就完成了,这时要想让 A 的完成事件被尽快处理(比如 A 是个紧急事务),要么丢弃做到一半的 B,要么保存 B 的中间状态并切换回 A,任务的切换是需要时间的(不管是从磁盘载入到内存,还是从内存载入到高速缓存),这势必降低 A 的响应速度。因此,对实时系统或者延迟敏感的事务,有时采用阻塞方式比非阻塞方式更好。)

相关系统调用

  • recv/recvform

select, poll, epoll

  • reference:大话select、poll、epoll
  • 这三个都是IO多路复用的机制,可以监视多个描述符的读/写等事件,一旦某个fd就绪,就能将发生的时间通知给关心的应用程序去处理该事件。
  • Linux的socket事件wakeup callback机制
    • linux(2.6+)内核的事件wakeup callback机制,是IO多路复用机制存在的本质。
    • 即Linux通过socket睡眠队列来管理所有等待socket的某个事件的process,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的process,通知process相关事件发生。
    • 通常情况,socket的事件发生的时候,其会顺序遍历socket睡眠队列上的每个process节点,调用每个process节点挂载的callback函数。
    • e.g: select、poll、epoll_wait陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前process构建一个wait_entry节点,然后插入到监控socket的sleep_list。 
    •       -->  socket的事件发生了,然后socket顺序遍历其睡眠队列,依次调用每个wait_entry节点的callback函数

Select

  • select


    • 根据上述socket wakeup callback机制,process需要同时插入到这N个socket的sleep_list上,等待任意一个socket可读事件发生而被唤醒。当被唤醒时,其callback里面应该有逻辑去检查具体哪些socket可读。
    • 过程:
      1. select将需要监控的fd集合拷贝到内核空间
      2. 挨个遍历,检查是否有socket有可读事件
      3. 若遍历完后,没有任何一个socket可读,那么select会调用schedule_timeout进入schedule循环,使得process进入睡眠
      4. 如果timeout内有数据可读了or timeout了,则调用select的进程会被唤醒
      5. select遍历监控的socket集合,挨个收集可读事件并返回给用户。
    • select的几个缺点:
      • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
      • select支持的fd数量太小,默认是1024
      • select返回时,只是返回data available,但并不返回具体是哪个socket有数据。  [只要有一个数据可读,整个socket集合就会被遍历一次,并调用socket的poll函数收集可读事件。]

POLL

  • poll其实是略鸡肋的存在,它只针对fd数量限制做了优化。

  • 并未解决大量描述符数组被整体复制于用户态和内核态的地址空间之间,以及个别描述符就绪触发整体描述符集合的遍历的低效问题。

EPOLL

  • 显然,epoll的优化就是针对上述提到的select的问题。  [注意下面两种方法是计算机领域中解决问题的两种基本思想:添加中间层;变集中(中央)处理为分散(分布式)处理]

  • fd set的拷贝问题:共享内存
    • epoll通过内核与用户空间mmap(内存映射)同一块内存来解决
  • 按需遍历就绪的fd set:epoll引入一个中间层:ready_list

Summary

  • blocking VS non-blocking

    • 调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还在准备数据的情况下会立刻返回。

  • synchronous VS asynchronous