Contents

浅谈为什么进程阻塞时不占用CPU

内核接收数据流程

讨论这个问题,我们需要先大致回顾一下内核接收数据时的过程:

  1. 网卡接收数据后,通过DMA写入内核的ring buffer,然后通过软中断和硬中断通知内核有数据到达。
  2. 内核派出一个专门线程ksoftirqd 从ring buffer中获取数据帧,用sk_buff表示,然后传入网络协议栈进行处理。
  3. 协议栈按照MAC→IP→TCP/UDP的顺序逐层校验sk_buff的报文头,判断是否是发给本机用户程序的数据。
  4. 内核按照源目端口和源目IP四元组找到对应的Socket,把数据写入Socket的接收缓冲区。
  5. 用户进程对应的系统调用把内核Socket缓冲区的数据拷贝到应用层的缓冲区,然后唤醒用户进程。

Linux的进程调度

在Linux中,进程大致有7种状态:

/posts/why-blocking-proc-save-cpu/images/linux-proc-state.png

内核在进行进程切换时,只会在“可运行状态”的进程间进行。而处于等待数据而阻塞的进程会被设置为“挂起”,所以不会参加轮转,这就是所谓的不占用CPU资源。

内核如何挂起、唤醒进程

那么当进程用系统调用(如select/poll/epoll)进行阻塞时,内核有什么动作呢?

  1. 首先,进程与内核的数据交换都基于Socket,Socket的数据结构中主要包含发送缓冲区、接收缓冲区和等待队列wait_queue_head_t),其中等待队列维护了所有等待该Socket事件的进程
  2. 当进程创建Socket,对其进行阻塞等待时,内核将该进程状态改为“挂起”,并加入至等待队列。
  3. 在等待过程中,这个进程便不会被cpu调度。
  4. 当数据到达,并被写入到Socket的接收缓冲区后(也就是第一部分的步骤4),内核从Socket缓冲区拷贝数据到各用户态的缓冲区,然后把Socket等待队列的所有进程都唤醒,从等待队列移出,重新加入cpu的工作队列。
  5. 至此,此进程的阻塞结束,恢复执行。

Reference

操作系统面试题:进程如何阻塞?进程阻塞为什么不占用CPU?_我是方小磊的博客-CSDN博客_进程阻塞

从内核接收数据到EPOLL原理

2.3 Linux 系统是如何收发网络包的?