浅谈为什么进程阻塞时不占用CPU
Contents
内核接收数据流程
讨论这个问题,我们需要先大致回顾一下内核接收数据时的过程:
- 网卡接收数据后,通过DMA写入内核的ring buffer,然后通过软中断和硬中断通知内核有数据到达。
- 内核派出一个专门线程
ksoftirqd
从ring buffer中获取数据帧,用sk_buff
表示,然后传入网络协议栈进行处理。 - 协议栈按照MAC→IP→TCP/UDP的顺序逐层校验
sk_buff
的报文头,判断是否是发给本机用户程序的数据。 - 内核按照源目端口和源目IP四元组找到对应的Socket,把数据写入Socket的接收缓冲区。
- 用户进程对应的系统调用把内核Socket缓冲区的数据拷贝到应用层的缓冲区,然后唤醒用户进程。
Linux的进程调度
在Linux中,进程大致有7种状态:
内核在进行进程切换时,只会在“可运行状态”的进程间进行。而处于等待数据而阻塞的进程会被设置为“挂起”,所以不会参加轮转,这就是所谓的不占用CPU资源。
内核如何挂起、唤醒进程
那么当进程用系统调用(如select
/poll
/epoll
)进行阻塞时,内核有什么动作呢?
- 首先,进程与内核的数据交换都基于Socket,Socket的数据结构中主要包含发送缓冲区、接收缓冲区和等待队列(
wait_queue_head_t
),其中等待队列维护了所有等待该Socket事件的进程。 - 当进程创建Socket,对其进行阻塞等待时,内核将该进程状态改为“挂起”,并加入至等待队列。
- 在等待过程中,这个进程便不会被cpu调度。
- 当数据到达,并被写入到Socket的接收缓冲区后(也就是第一部分的步骤4),内核从Socket缓冲区拷贝数据到各用户态的缓冲区,然后把Socket等待队列的所有进程都唤醒,从等待队列移出,重新加入cpu的工作队列。
- 至此,此进程的阻塞结束,恢复执行。