☰
Linux内核O(1)调度器与进程优先级深度解析
2026/9/28 7:54:39 网站建设 项目流程

1. 从"越用越卡"说起:O(1)调度器要解决的真问题

很多年以前我刚接触Linux内核的时候,最喜欢问自己一个问题:为什么一台老机器跑Linux 2.4内核,一旦把CPU占满的进程拉起来几十个,整个系统就拖泥带水,连移动鼠标都费劲?后来我明白了,问题出在调度器身上。当时的调度器每次要选下一个进程运行时,都得把运行队列里的所有进程扫一遍,挨个比较优先级,复杂度是O(n)。进程一多,每次调度耗时线性上升,系统自然就卡了。所以当Linux 2.6内核把调度器重写成O(1)算法时,行业里是相当兴奋的——"不管多少个进程,调度耗时恒定为常数",这口号很提气。

但"O(1)"这三个字背后,远不止一个复杂度记号那么简单。它牵扯到进程优先级怎么定、时间片怎么给、交互式进程怎么照顾、多CPU之间负载怎么均衡,以及最重要的——进程切换时"下一个该轮到谁"这个问题怎么在常数时间内回答。这篇文章我想把进程优先级和O(1)切换调度算法揉在一起讲透,适合正在啃Linux内核源码的人、准备面试的人,以及那些在生产环境里用nice、renice、chrt调优过进程却又说不清底层原理的运维朋友。

要先说明一件事:如果你在搜索引擎里查"Linux进程优先级",大概率看到的是一堆nice值、top命令列的PR和NI列,这些当然有用,但它们只是内核调度器设计里的冰山一角。真正决定一个普通进程在O(1)调度器里何时被选中、能跑多久的,是一套完整的优先级体系。搞清楚这套体系,你再看ps -l的输出,会发现那些数字全是"翻译好的结果"而不是"原始依据"。

2.6时代的调度器由Ingo Molnár一手重写,从2.4的O(n)调度器变成每个CPU独立维护runqueue、两级优先级数组的设计。它的伟大之处在于,把"选择下一个进程"这个动作从"遍历加排序"变成了"查位图加队列出队"——前者是穷人逛菜市场逐个比价,后者是进店直接走向那个放好货的货架。理解了这个类比,你就抓住了O(1)的骨架。接下来的内容我会按这个骨架展开:优先级怎么算,活跃/过期数组怎么转,位图怎么加速,最后再看我们平时能动手的优先级调整命令和它们在内核里对应的实际效果。

2. 优先级数值体系:从nice到140个调度等级

2.1 140这个数字是怎么来的

在O(1)调度器里,每个进程都被映射到一个0到139的优先级数值,数值越小,优先级越高。其中0到99保留给实时进程,100到139给普通进程。实时进程走的是SCHED_FIFO或SCHED_RR调度策略,普通进程走的是SCHED_NORMAL(在2.6早期也叫SCHED_OTHER)。这两个区间是硬隔离的:只要有一个实时进程处于可运行状态,普通进程就基本没有机会被选中,除非实时进程主动睡眠或让出CPU。

普通进程的优先级起点不由你直接指定,而是由nice值换算而来。nice值的范围是-20到19,数值越小代表"越不友好"(对别的进程越不客气),实际上对应更高优先级。在内核里有一条经典换算公式:

static_prio = MAX_RT_PRIO + nice + 20

其中MAX_RT_PRIO等于100。也就是说,nice=-20的进程静态优先级是100,nice=0的进程静态优先级是120,nice=19的进程静态优先级是139。可以看到,普通进程无论如何都不会跌到实时区间里,这条线是不会越过的。

你可能会注意到这里用了"静态优先级"这个词。静态的意思是它主要由nice值决定,不会因为进程的运行历史而改变。真正参与调度比较的其实是另一个数——动态优先级,这东西就跟进程的睡眠和运行时间紧密挂钩了。

2.2 动态优先级:睡眠多的进程被"奖励"了

O(1)调度器最令人津津乐道的设计之一就是动态优先级。它引入了一个叫sleep_avg的平均睡眠时间字段,内核用它来估算这个进程是偏交互还是偏计算。交互式进程,比如编辑器、终端、GUI事件处理,特点是大部分时间在等输入,醒来后只跑一小会儿就又睡过去。计算型进程则相反,一旦拿到CPU就恨不得跑到天荒地老。

调度器怎么利用sleep_avg呢?每次进程从睡眠中醒来,sleep_avg会增加;每次进程在CPU上消耗一个tick,sleep_avg会减少。然后通过一个bonus映射把sleep_avg翻译成一个-5到+5的修正项,最终动态优先级等于静态优先级减去这个修正项:

effective_prio = static_prio - bonus

这里bonus的范围是-5到+5。一个经常睡眠、sleep_avg很高的进程,拿到的bonus接近+5,effective_prio就比静态优先级小5个级别,实际优先级被提升了。反过来,一直霸占CPU、sleep_avg被打到很低的进程,bonus可能为-5,effective_prio就比静态优先级低5个级别。

这招很聪明,它让交互式进程在竞争CPU时天然占优,界面响应就会更流畅。但聪明里也带着麻烦:一个进程可以通过每分钟sleep一小会儿来维持高sleep_avg,从而长期获得优先级优势。2003年内核邮件列表上曾经吵得不可开交的"睡眠进程欺骗"问题就是这么来的。这也是为什么后来CFS调度器彻底放弃了sleep_avg这套启发式,改用完全不同的思路。

2.3 时间片:优先级越高,一次能跑越久

在O(1)调度器里,时间片不是固定的,它跟静态优先级挂钩。优先级越高,获得的时间片越长。大致规则是:

time_slice = BASE_TIMESLICE * (MAX_PRIO - static_prio) / USER_PRIORITY

其中MAX_PRIO是140,USER_PRIORITY是40(普通进程的优先级等级数量)。BASE_TIMESLICE是一个与内核HZ相关的基准值。套进去算一下:nice=0的进程static_prio=120,时间片大约是基准值的一半;nice=-20的进程static_prio=100,时间片是最大值;nice=19的进程static_prio=139,时间片是最小值。实际数值还会受HZ配置影响,不同内核版本也有微调,但方向是一致的:高优先级进程不仅被优先选中,而且一旦选中还能跑更久。

这里有一个常见的理解误区:很多人以为动态优先级也影响时间片长度。实际上在O(1)调度器里,动态优先级只决定"队列里谁排在谁前面",时间片的计算用的是静态优先级。换句话说,sleep很多把你的动态优先级抬高了,你确实会被更早选到,但这不改变你单次运行的时间片配额。时间片在你首次进入runqueue时按静态优先级算好,用完了就移到过期队列,直到所有活跃进程都用完时间片才重新计算。

3. O(1)调度的枢纽:runqueue结构与活跃/过期数组

3.1 每个CPU一个runqueue

2.4时代的调度器是一个全局运行队列,所有CPU上的进程都在同一个队列里抢锁。这带来两个问题:一是锁竞争激烈,尤其是多核服务器上,调度本身成了瓶颈;二是CPU缓存亲和性差,一个进程可能这次在CPU0跑,下次被调度到CPU1,L1/L2 cache里的数据全白瞎了。

O(1)调度器把全局队列拆成了per-CPU的runqueue。每个CPU维护自己的运行队列,普通情况下进程只在自己所属CPU的runqueue上进进出出,不需要碰其他CPU的锁。只有负载不平衡到一定程度时,调度器才会把进程从一个CPU迁移到另一个CPU,也就是后面的负载均衡机制。

per-CPU runqueue里最关键的是两个优先级数组指针:

struct runqueue { spinlock_t lock; prio_array_t *active; // 指向活跃数组 prio_array_t *expired; // 指向过期数组 prio_array_t arrays[2]; // 实际存储两个数组 unsigned long nr_running; task_t *curr, *idle; // ... };

arrays[2]是真正的存储体,active和expired只是两个指针,通过交换指针来切换当前活跃数组,这正是O(1)调度器"切换"机制的精髓。

3.2 活跃队列和过期队列怎么配合

每个优先级数组里面,对应每一个优先级都维护一个双向链表。所有处于可运行状态的进程,按照优先级挂到相应的链表上。调度的基本流程是这样的:

  • 当前进程的时间片还在,它就在活跃数组里继续排队等下一个tick。
  • 当前进程的时间片用完,调度器把它移到过期数组,并且重新计算一个新的时间片给它。
  • 调度器从活跃数组里按优先级从高到低找出第一个非空链表,取队首进程作为下一个要运行的进程。
  • 当活跃数组已经空了,只要交换active和expired指针,原来的过期数组就变成了新的活跃数组。

这个过程里最关键的是:进程用完了时间片不会立即被换出去,而是必须移出活跃数组进入过期数组。这保证了所有进程都能在每一轮里得到机会,避免低优先级进程被无限饿死。即使在极高的负载下,高优先级进程可以每轮都排前面,但低优先级进程至少会在过期数组里攒着,等活跃数组清零后统一释放出来。

这个设计还带来一个很有意思的副产品:进程的大多数运行开销发生在"从活跃移到过期"这个动作里,而"选下一个进程"这个动作本身非常轻。你可以理解成食堂打饭:每个人拿一个固定大小的餐盘(时间片),吃完的从窗口挪到等待区(过期数组),窗口前没人了就把等待区整体当成新的窗口区。窗口永远有人服务,服务员的挑选动作永远是"看哪个窗口队列最长、队首是谁"这么一下。

3.3 位图加速到底是怎么加速的

如果每次选下一个进程都要从priority 0到139扫描链表,那还是O(n)。所以O(1)调度器里有一个和优先级数组配套的结构——bitmap位图。

struct prio_array { unsigned int nr_active; unsigned long bitmap[BITMAP_SIZE]; struct list_head queue[MAX_PRIO]; };

这个bitmap总共覆盖140个优先级,每个优先级对应一个bit。某个优先级对应的链表非空时,bitmap里该位置1。调度器要选下一个可运行进程时,直接在这一两个字长的bitmap里查找第一个置位的bit,这个操作是用CPU位运算指令完成的,常数时间。找到bit后就能定位到对应的queue链表,取出第一个进程即可。

用位图加速的核心思路是:与其花时间比较140个优先级数值,不如让硬件帮你在一两个周期内算出"最小的非零bit"在哪个位置。这个查找操作在x86上对应BSF等指令,在ARM上有CLZ,都是单周期级别的操作。所以不管系统里是3个进程还是3000个进程,找最高优先级运行队列的时间都是一样的——这就是O(1)的底气所在。

我在这里插入一个当年看源码时的细节:双向链表queue里存的是task_struct中的run_list节点,链表操作用的是内核标准的list_add/list_del。进程在进入runqueue时按优先级挂在对应链表尾部,同等优先级的进程之间采用先来后到的轮转方式。这保证了同一优先级内部的公平性,不至于让某个进程永远排在前面。

4. 一次完整调度决策的脉络:从时钟中断到context_switch

4.1 tick中断里发生了什么

进程不会无缘无故被换走,调度决策的触发点主要有三个:时钟tick中断、进程主动睡眠/唤醒、以及抢占点的检查。我们先把时钟tick这条路走通。

每个CPU有一个周期性的时钟中断,在O(1)调度器里它最终会调用scheduler_tick()。这个函数做的事情大概是这样:

  1. 首先拿到当前CPU的runqueue和当前运行进程curr。
  2. 如果curr是 idle进程(空闲进程),看看是否有其他进程需要调度,有就直接设置need_resched。
  3. 如果curr是普通进程,减少它的时间片计数。如果时间片已经用完,就把这个进程移到过期数组,重新分配时间片;如果进程是交互式的且满足某些条件,也可能不立即移出而是继续留在活跃队列,这个细节当时有很多trick,用得好能显著改善交互延迟。
  4. 如果curr是实时进程且策略是SCHED_RR,同样检查时间片轮转。
  5. 最后判断当前运行进程是否仍然是最应该运行的进程。如果不是,就设置TIF_NEED_RESCHED标志。

TIF_NEED_RESCHED是一个进程描述符里per-thread的标志位,它本身不会立即触发切换。真正触发切换的时机是在中断返回路径上,内核会检查这个标志位,如果置位就调用schedule()。换句话说,调度器不是硬抢占式的"到点就切",而是"到点标记一下,等安全点再切"。这样能避免在中断上下文里做复杂的锁操作,减少竞态条件。

这里有一个值得注意的边界:如果当前进程是内核线程、正在持有某些关键锁,抢占可能会被推迟到更合适的时机。内核对抢占的控制分为自愿抢占和内核抢占,2.6内核默认开启CONFIG_PREEMPT的内核抢占支持,允许在安全时进行内核态抢占,但锁保护的关键临界区是绝不预抢的。

4.2 睡眠与唤醒路径上的调度插曲

进程主动睡眠时,比如调用sleep()、等待I/O、等待锁,会把自己从运行队列中摘除,然后调用schedule()选择下一个进程。这个过程相对直接:睡眠前把进程状态置为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,从runqueue里删除,调用schedule()。

当条件满足,比如I/O完成、信号到达、锁被释放,进程需要被唤醒。内核通过try_to_wake_up()把进程重新放入适当的CPU的runqueue,并增加它的sleep_avg。这里有一个关键决策:放到哪个CPU的runqueue。简单场景是放到原来所属的CPU,但如果睡眠期间负载均衡有变化,或者当前CPU空闲,调度器可能会选择放到其他CPU,这涉及唤醒迁移的逻辑。

唤醒之后还有一个抢占判断:如果唤醒的进程比当前正在运行的进程优先级还高,并且时机允许,那么当前进程会被标记need_resched,等回到用户态或安全点就切换过去。这就是为什么当你用chrt把一个进程改成实时优先级后,它几乎立刻就能抢占当前跑着的普通进程——不是因为它神奇,而是唤醒路径上的抢占检查发挥了作用。

4.3 调度器主函数schedule()的固定动作

不管从哪条路走到schedule(),这个函数的核心步骤是稳定的:

  1. 关闭内核抢占,获取当前CPU的runqueue锁。
  2. 找到下一个可运行的进程next,就是通过bitmap和链表取出来的那个最高优先级进程。
  3. 处理prev_mm:如果当前进程是内核线程,它没有自己的用户态地址空间,它借用了上一个用户进程的mm。切换时要把这个借用关系处理好。
  4. 执行context_switch(),完成地址空间切换和寄存器切换。地址空间切换会写CR3或对应体系结构的页表基址寄存器,寄存器切换保存当前进程上下文、恢复新进程上下文。
  5. 更新runqueue统计信息,释放锁,重新开启抢占。

整个过程里最重的是context_switch,它要面对TLB清零、cache失效等开销。O(1)调度器虽然让"选择进程"变快了,但真正切换一个进程的代价是固定的、不可压缩的。这也解释了为什么调度器倾向于让时间片较长的高优先级进程少被切走——切换本身有成本,尽量避免频繁切换才是高性能的关键。

谈到切换,很多人会问:同一优先级内的进程怎么轮转?答案是时间片用完后进程会进入过期数组,等活跃数组空了整体交换数组,这时过期数组里的同一优先级进程会按链表顺序重新进入活跃数组,队首就是之前最早到期的那个。因此同一优先级内的轮转粒度实际上是"一轮时间片的长度",而不是严格的FIFO。

5. 实测观察:用工具调整优先级时你到底在改什么

5.1 ps -l和top里的PRI是怎么来的

了解了内核里的优先级体系之后,再看用户态工具就清晰多了。ps -l会输出两列:PRI和NI。NI是nice值,你直接能看到,默认是0。PRI就值得说一说了,ps命令显示的PRI是经过映射的——它把内核里的动态优先级做了某种偏移,不同ps版本计算方法还不完全一样,但大致可以理解为"100 + 动态优先级档次修正"之类的展示值。所以同一时间,你用top看到的PR和ps看到的PRI可能数值不一样,别慌,这是不同工具做映射的差异,不是系统出错了。

如果你在2.6内核上运行top,普通进程的PR超过30、NI为0是常见的,因为top显示时也做了偏移处理。关键是看NI列的变化:你把一个进程的nice从0调到10,它的静态优先级会从120变成130,动态优先级大约也会下降10个级别。时间片会缩短,被选中概率也会降低。如果你调整后立即观察top,它的排序位置会往后挪,但这个变化不是瞬时的,因为sleep_avg还会影响动态优先级,有时nice调了10却没有想象中的明显效果,就是被sleep_avg对冲了一部分。

5.2 nice、renice的边界限制

启动进程时用nice -n 5 ./myapp,给它一个较低的优先级。运行中可以用renice -n 10 -p PID调整。这里有三个限制值得记住:

  1. 普通用户只能提高nice值(让自己更友好),不能降低nice值(让自己更不友好)。只有root或具备CAP_SYS_NICE能力的进程才能往负数调。这个限制是安全设计,否则任何用户都能把自己进程的nice调到-20,把所有CPU抢走。

  2. nice值调整直接改静态优先级,但时间片不会立即重算。进程当前正在运行的话,已经用完的时间片计数不会因为nice改变而延长,要等它进入过期数组后才会按新静态优先级重新分配时间片。所以"改完立刻变快/变慢"多半是错觉。

  3. renice对一个进程组或整个用户生效也是可以的,但内核实际实施的是每个线程的优先级,进程组/用户只是工具帮你批量操作一堆线程。

5.3 实时优先级:chrt与SCHED_FIFO/SCHED_RR

普通进程再怎么调,优先级都在100到139这个区间里。要让进程进入0到99的实时区间,必须用chrt命令或直接调用sched_setscheduler系统调用。chrt -f 99 ./realtime_task会把进程设为SCHED_FIFO策略、优先级99,这个进程会成为系统里"最优先"的普通线程,能把所有普通进程全部压在身下。

如果是SCHED_FIFO,进程不主动让出CPU的话,可以一直运行到结束,其它进程(包括同等实时优先级的进程)都没办法抢占它,除非内核启动PREEMPT_RT相关机制遇到某些强制点。SCHED_RR则是在实时优先级上加了时间片轮转,同等优先级的实时进程之间轮流运行,每次一个时间片,用完切给下一个。

这条规则对初学者是个常见的"坑":你要是手滑把一个死循环进程设成SCHED_FIFO优先级99,它能把整个系统的交互性打到冰点,鼠标都动弹不得,只能ssh到机器上用root把它kill掉或者切换回普通策略。所以生产环境里要用chrt,先想清楚这个进程是不是真的需要硬实时特性,以及它是否足够短小精悍。

顺带一提,实时进程也有自己的时间片逻辑。SCHED_RR用的时间片和普通进程的时间片计算不完全一样,它更接近"固定的实时时间片轮转",保证同一优先级的RT进程之间公平交替。SCHED_FIFO则没有时间片概念,进程自己放权才轮到下一个。这些策略在Linux的POSIX调度接口里都有对应操作:sched_getscheduler/sched_setscheduler可以查询和设置,chrt只是它们的一个壳。

6. O(1)调度器为什么被CFS取代:成就与软肋

6.1 启发式的两难

O(1)调度器在2.6早期版本里效果确实惊艳,但它身上最受诟病的是那套sleep_avg和bonus启发式。交互式进程判定的逻辑本质上是在猜"这个进程是不是交互的",猜对了就流畅,猜错了就翻车。比如一个进程频繁sleep但每次醒来做大量计算,它会被误判为交互式从而获得优先级奖励,挤压了真正需要CPU的计算进程。

更麻烦的是,sleep_avg的增量与减量调参很微妙。参数调得太激进,交互式进程能获得过高优先级并长期霸占CPU;调得太温和,GUI又感觉卡顿。内核开发者花了很多精力去修补sleep_avg计算的边界条件,比如限制最大bonus、引入交互等级动态调整,但始终只是在缓解症状,没有消除启发式本身的不确定性。

还有多CPU负载均衡方面的复杂度。O(1)的per-CPU runqueue虽然避免了大锁竞争,但负载均衡需要定期扫描各个CPU的runqueue,把任务从忙的CPU迁到闲的CPU。早期实现用的是启发式均衡,负载比较粗糙,在NUMA架构上表现一般。后来的sched_domain、sched_group体系就复杂得多,这部分是O(1)调度器最不好维护的地方之一。

6.2 CFS的虚拟时间新思路

2.6.23内核引入了CFS调度器,彻底换了个玩法。CFS不再有"优先级数组+时间片"这套物理模型,而是给每个进程维护一个虚拟运行时间vruntime。进程每运行一个tick,vruntime就按某种速率增长;谁的vruntime最小,谁就是下一个运行对象。调度器用红黑树管理所有可运行进程,查找最小vruntime进程是O(log n),理论上不是O(1),但实际效果更好、更公平。

CFS里优先级体现为权重,权重直接决定vruntime的增长速率。高优先级进程的vruntime增长得慢,于是它更容易"保持最小",自然获得更多CPU时间。低优先级进程的vruntime增长快,虽然也会被选到,但运行时间占比明显降低,而且不会出现固定时间片一刀切带来的边界颠簸。

CFS还引入了目标延迟和最小粒度等参数,取代了"一个进程一次跑几十毫秒"的固定时间片思路。目标延迟保证在一段时间内所有可运行进程至少被调度一轮,最小粒度防止切换过于频繁。这些参数可以通过/sys/kernel/debug/sched/或者内核命令行调整,相比O(1)时代的启发式参数更直观。

6.3 今天还能看到的O(1)痕跡

虽然主体调度器换成了CFS,但O(1)调度器的很多结构性设计仍然活在今天的内核里。per-CPU runqueue这个基本架构保留了下来,只是内部从"两个优先级数组"变成了"红黑树+队列"。位图这种快速查找思想在CFS里也有体现,比如CFS就使用位图来管理空闲CPU和调度域状态。

另外,如果你去翻当前内核源码,仍然能在sched_rt.c里看到"active/expired"类似的分层思想——实时调度器至今仍然维护着优先级数组和对应的位图,因为实时进程数量通常很少,优先级数组+位图在这种场景下既简单又高效。换句话说,O(1)调度器没有完全消失,它只是缩小了自己的地盘,把普通进程的调度交给了更公平、更灵活的CFS,自己则在实时领域继续发光。

这段演进历史对我们做实际性能调优也很有启发:当你说"Linux调度不公平"或者"这个进程响应慢"时,最好先搞清楚自己面对的是CFS还是RT调度器,它们的行为模型完全不同。针对CFS去调nice值,考察的是权重比例;针对RT调度去调优先级,考察的是绝对顺序。用错模型去理解问题,排错方向大概率跑偏。

7. 踩坑经验与优先级调优的零散窍门

文章最后分享几个我在实际生产环境和学习过程中积累的零散经验,不算严谨教程,但每一句都是从问题里爬出来的。

第一个坑:在负载很高但CPU还没跑满的时候,不要只盯nice值。很多人在单机压测时发现某个后台任务慢了,第一反应是把它的nice调低。但在CFS下,如果CPU没跑满,任务并没有在和其他进程抢CPU,它慢的原因可能是I/O等待、锁竞争、NUMA远端内存访问。这时候调nice意义很小,倒不如用pidstat、perf sched、iostat把瓶颈找出来。nice调节只在CPU成为真正的竞争瓶颈时才有明显效果。

第二个坑:把交互式判断自动化。早年很多"低延迟优化指南"教人写一个循环,每隔几十毫秒sleep一下,从而让sleep_avg维持在高位,获得O(1)调度器的优先级奖励。在2.6早期这招是有效的,但内核很快加了recalc_task_prio的补偿逻辑,到了CFS就彻底失效。现在还这么做的人,只是白白浪费睡眠开销,不会拿到任何调度收益。

第三个坑:实时优先级不是越快越好。我见过有人嫌数据库线程响应慢,直接把核心线程设成SCHED_FIFO优先级90以上,结果是数据库线程把CPU占死,其他内核线程和网络中断处理被挤压,整体吞吐量反而下降,还伴随着诡异的延迟毛刺。实时调度的价值在于"确定性的截止时间",而不是"优先跑每秒",如果你的业务没有硬实时要求,没有必要碰chrt。

第四个经验:进程的PRI数值波动不一定代表优先级乱跳。在O(1)调度器时代,sleep_avg涨落会让动态优先级来回变化,top和ps上看到的PR跟着震荡是正常的。到了CFS时代,top显示的PR更多是对nice值加了一个固定偏移,变化不再剧烈。所以你在老内核上看到PR剧烈抖动,可以淡定一点,先确认内核版本再判断是异常还是正常。

最后一个技巧:如果你要在多核机器上给进程做优先级配合,记得把CPU亲和性一起考虑。taskset -c 0,1 myapp限制进程只能跑在指定CPU上,再配合nice调整,效果往往比单纯调nice更可控。因为CFS的负载均衡会在CPU之间搬移进程,搬移本身有成本,而且会破坏缓存亲和性。与其让内核反复均衡,不如在数据分片上把CPU分配好。

我一直觉得,理解调度器最好的方式不是背结论,而是亲手压一场实验:开几十个while :; do :; done烧CPU,然后互动调整其中一个进程的nice,观察它对其他进程的影响。跑完后你一定会对"优先级"三个字产生新的、更实在的体会。内核把调度的复杂性藏在runqueue和bitmap背后,但最终呈现给用户的,就是一个个进程在CPU上的起起落落,理解它的运行逻辑,是玩转Linux性能调优这盘大棋的必修课。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询