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亲和性——进程尽量在同一个核心上跑,缓存命中率高。
注意:调度器默认会做负载均衡。它会定期把进程从一个核心迁移到另一个核心,以平衡各核心的负载。但这对交易系统是灾难——缓存全废了,延迟抖动巨大。所以我们要用亲和性绑定来「锁死」进程/线程到指定核心。
调度器的工作流程大致是这样的:
- 时钟中断触发调度(通常是1ms或4ms一次)
- 调度器检查当前进程的时间片是否用完
- 从runqueue中选出下一个最合适的进程
- 执行上下文切换(保存寄存器、切换页表、刷新TLB)
- 新进程开始运行
你想想看,每次上下文切换,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根本不需要调度其他进程,延迟自然就稳定了。
总结一下:进程和线程的区别,决定了我们该用哪种亲和性绑定API。调度器的工作原理,解释了为什么默认调度对交易系统不友好。而CFS的公平性,让我们可以通过调整优先级和亲和性来「抢」到更多CPU时间。
嗯,这些基础搞清楚了,下一节我们就可以直接上手写亲和性绑定的代码了。