3. Linux进程与线程:进程与线程的区别、Linux调度器工作原理、CFS完全公平调度算法

聊到CPU亲和性绑定,咱们得先搞清楚一个基础问题——你到底是在绑进程,还是在绑线程?

很多人觉得这俩差不多,其实差远了。我在做低延迟交易系统时,就吃过这个亏。当时绑了进程的亲和性,结果线程还是到处乱跑,延迟直接飙了十几微秒。嗯,从那以后我再也不敢把进程和线程混为一谈了。

3.1 进程与线程的区别

说白了,进程是资源分配的最小单位,线程是CPU调度的最小单位。你想想看,进程有自己的独立地址空间、文件描述符表、信号处理表,而线程呢?它跟同进程的其他线程共享这些东西。

我习惯用一个比喻来理解:进程就像一家公司,有自己的办公场地、财务、法务。线程就是公司里的员工,大家共用办公场地和资源,但每个人干自己的活。

核心区别一览:

维度 进程 线程
地址空间 独立 共享
资源开销 大(fork时复制页表) 小(仅分配栈和TCB)
切换代价 高(TLB刷新、上下文切换重) 低(共享地址空间,TLB命中率高)
同步方式 IPC(管道、共享内存、消息队列) 锁、条件变量、原子操作
亲和性绑定 sched_setaffinity pthread_setaffinity_np

我在项目中遇到过一个问题:某个交易进程用fork创建了子进程,结果子进程的亲和性没继承,被调度到了另一个NUMA节点上。内存访问延迟直接翻倍。后来我改用pthread_create创建线程,亲和性就好控制多了。

实战建议:在交易系统中,我强烈建议用线程而非进程。原因很简单——共享内存不需要IPC,延迟更低。而且线程的亲和性绑定更精细,可以做到每个核心绑一个线程。

3.2 Linux调度器工作原理

Linux调度器,说白了就是决定「下一个该谁上CPU跑」。它的核心目标就三个:公平、高效、低延迟。

早期的O(1)调度器,我记得是2.6内核时代的产物。它用优先级数组,每个优先级一个队列。但有个问题——交互式进程容易饿死。后来有了CFS,才真正解决了公平性问题。

调度器的核心数据结构是runqueue(运行队列)。每个CPU核心都有自己的runqueue,这本身就是一种天然的CPU亲和性——进程尽量在同一个核心上跑,缓存命中率高。

注意:调度器默认会做负载均衡。它会定期把进程从一个核心迁移到另一个核心,以平衡各核心的负载。但这对交易系统是灾难——缓存全废了,延迟抖动巨大。所以我们要用亲和性绑定来「锁死」进程/线程到指定核心。

调度器的工作流程大致是这样的:

  1. 时钟中断触发调度(通常是1ms或4ms一次)
  2. 调度器检查当前进程的时间片是否用完
  3. 从runqueue中选出下一个最合适的进程
  4. 执行上下文切换(保存寄存器、切换页表、刷新TLB)
  5. 新进程开始运行

你想想看,每次上下文切换,TLB都要刷新,L1/L2缓存里的数据全没了。这就是为什么交易系统要尽量减少上下文切换——一次切换可能损失几十纳秒到几微秒。

3.3 CFS完全公平调度算法

CFS(Completely Fair Scheduler)是2.6.23内核引入的,一直用到现在。它的设计哲学很有意思——不是给每个进程分配时间片,而是让每个进程获得等比例的CPU时间。

怎么做到的?核心概念叫虚拟运行时间(vruntime)

每个进程都有一个vruntime,它记录了这个进程「实际消耗的CPU时间,经过优先级加权后的值」。CFS用红黑树来组织所有可运行的进程,vruntime最小的进程在最左边,优先被调度。

CFS核心公式:

vruntime += 实际运行时间 * (NICE_0_LOAD / 进程权重)

优先级高的进程(nice值小),权重更大,vruntime增长更慢,所以能获得更多CPU时间。

我在优化交易系统时,就利用了这个特性。把交易线程的nice值设低(优先级高),让它在CFS调度中占优势。但要注意——光调nice值不够,还得配合亲和性绑定,否则它可能被调度到别的核心上。

CFS的几个关键参数:

参数 默认值 说明
sched_latency_ns 6ms(桌面)/ 24ms(服务器) 调度延迟目标,所有进程在一个周期内至少运行一次
sched_min_granularity_ns 0.75ms 进程最小运行时间,防止频繁切换
sched_wakeup_granularity_ns 1ms 唤醒抢占的粒度,影响交互式响应

避坑指南:我曾经在交易服务器上调过sched_min_granularity_ns,想让它更小一点,减少调度延迟。结果反而导致上下文切换暴增,吞吐量下降。后来我明白了——调度器参数不是越小越好,要找到平衡点。

CFS还有一个重要特性叫组调度(group scheduling)。它允许你把多个进程/线程放到一个组里,组内公平分配CPU时间。这在容器化场景下特别有用——你可以保证每个容器获得等比例的CPU,而不是容器内的每个进程去抢。

嗯,这里要注意:CFS的公平是「尽力而为」的。如果系统过载,它还是会尽力保证每个进程都能运行,但延迟会变差。这就是为什么交易系统要预留核心——让交易线程独占核心,CFS根本不需要调度其他进程,延迟自然就稳定了。

Linux调度器与CFS核心逻辑 CPU Core 0 CPU Core 1 CPU Core 2 CPU Core 3 Per-CPU Runqueue(每个核心独立) P1 P2 P3 P4 P5 CFS 调度器 选择 vruntime 最小的进程 CPU亲和性绑定:sched_setaffinity / pthread_setaffinity_np 锁定进程/线程到指定核心,避免负载均衡迁移 关键概念 • vruntime • 红黑树 • 组调度 • 负载均衡 • 上下文切换 • 亲和性绑定 交易系统: 必须绑定亲和性!

总结一下:进程和线程的区别,决定了我们该用哪种亲和性绑定API。调度器的工作原理,解释了为什么默认调度对交易系统不友好。而CFS的公平性,让我们可以通过调整优先级和亲和性来「抢」到更多CPU时间。

嗯,这些基础搞清楚了,下一节我们就可以直接上手写亲和性绑定的代码了。