简介:这是一份面向嵌入式开发初学者与RTOS兴趣者的轻量级实践资源,聚焦C语言实现的简易实时操作系统内核,帮助理解任务调度、中断响应、双链表管理等核心机制。压缩包共7个文件(6KB),含2个C源文件(main.c、cx_rtos.c)实现主循环与RTOS核心逻辑,2个头文件(cx_rtos.h、list.h)定义数据结构与接口,另有项目配置文件(.pro)、用户配置(.user)及VS Code调试配置(c_cpp_properties.json),结构紧凑、便于编译调试。已有551人学习下载,适合在STM32等裸机平台快速部署验证。读者可完整掌握基于双链表的任务优先级调度流程、上下文切换实现细节、定时器驱动的时间片管理逻辑,并通过源码直观体会C语言在系统级编程中的内存控制与确定性执行特性。 手写一套C语言实时操作系统(RTOS)内核这件事,听起来像是典型的“重复造轮子”,但如果真把工程压缩包发出去,别人看到的绝不是几KB代码,而是对操作系统底层机制的一次完整拆解。这次我在《手写c语言实时操作系统代码.rar》里实现了一个不依赖板级厂商库、不靠移植FreeRTOS的纯C微型内核,包含了基于优先级的抢占式调度、任务状态管理、信号量、互斥锁、消息队列,以及基于系统节拍的时间片轮转。对于一直用现成RTOS做开发、却说不清“任务切换那一刻CPU到底发生了什么”的嵌入式工程师来说,这篇文章可能是很适合的参考。我会把自己设计TCB、构造上下文切换、调试同步原语时踩过的坑一并聊清楚,而不是只贴一段能跑的效果代码。
1. 手写RTOS的价值边界:不是什么项目都适合从零造内核
很多开发者一听说“手写RTOS”,第一反应就是“FreeRTOS都开源了,你花几个月写个不如它的东西,图什么?”这种质疑有道理,但忽视了手写内核真正能解决的问题。我这次手写的动机很简单:产品里用到的RTOS虽然稳定,但很多行为对应用层来说是黑盒,遇到任务莫名其妙卡死、优先级反转、栈溢出的问题,只能靠猜。与其继续在黑盒外打转,不如自己写一个小而完整的内核,把每个环节都握在手里。
1.1 手写内核算不算是“重复造轮子”
从工程效率角度讲,如果公司已经深度依赖FreeRTOS或者RT-Thread,产品交付期限又紧,那确实不该从零写内核。但手写RTOS的价值集中在两条线上:一是学习路径,二是深度定制场景。学习路径不用多说,内核里的调度算法、临界区保护、任务栈切换这些概念,只要是看书和看源码,永远有一种“隔了一层”的感觉;自己动手写一遍,哪怕只跑通三个任务调度,对关中断、栈指针、异常返回机制的理解都会深一个台阶。
深度定制场景则是另一回事。在一些对实时性要求极端的场景里,通用RTOS的某些默认策略可能并不合适,比如中断底半部的延迟处理、tick周期与低功耗唤醒的耦合、信号量的超时机制等。自己写内核,可以把调度器和底层硬件绑定得更紧,做出来一个“只有自己需要的功能”的最小系统,少了那些永远不会用到的模块,代码体积和运行开销都能压得很低。手写的微型内核代码量一般只有一千到三千行,审查起来非常轻松,这在安全敏感场景里也是个不小的优势。
1.2 手写内核的适用边界
但必须泼一盆冷水:如果项目需要完整的文件系统、网络协议栈、动态加载模块,手写RTOS就不合适了,这些组件的工程量远超内核本身,从零造轮子根本不现实。手写内核最适合的是学习、实验、原型验证,或者是极简控制器的裸核场景。我在实践中给自己定的边界是:内核只管理任务调度、同步、通信和必要的时间控制,文件系统和驱动全部放任务层去实现,不往里塞无关功能。
这种“小而专”的定位会让内核的可维护性直线上升。每增加一个特性,我都会先问自己:这个功能是核心调度必须的,还是可以在任务层做?能下放到任务层的,就绝不让内核介入。比如设备互斥访问,直接用一个互斥锁就解决了,没必要在内核里做专门的资源管理模块。
2. 从零定义TCB:任务控制块是内核的一等公民
任何一个RTOS,核心数据结构就是任务控制块(Task Control Block,TCB)。所有关于任务的信息,包括栈指针、优先级、状态、延迟计数、等待的内核对象,都必须沉淀在TCB里。调度器的每一行代码,本质上都是在对TCB列表做操作。所以TCB字段设计得是否合理,直接决定后续代码的复杂度。
2.1 TCB结构体设计的关键字段
我先给出一个典型的TCB定义,这就是我在代码包里最基础的结构体之一:
typedef enum task_state { TASK_READY = 0, TASK_RUNNING, TASK_BLOCKED, TASK_SUSPENDED } task_state_t; typedef struct tcb { char name[16]; void (*entry)(void *arg); void *arg; uint8_t prio; uint8_t state; uint32_t *sp; /* 当前栈指针(由切换代码维护) */ uint32_t *stack_base; uint32_t stack_size; uint32_t delay_ticks; /* 阻塞计数,tick中断里递减 */ struct tcb *next; /* 链表指针 */ } tcb_t;设计的时候有几点值得特别说明。sp字段是整个TCB里最重要的,它保存的是该任务被切出去时的栈指针值;下一次被调度进来时,切换代码会从这个值恢复寄存器。stack_base和stack_size用来做栈边界检查,对调试任务栈溢出非常有用。delay_ticks是给delay_until()这类接口用的,每次系统节拍中断进入时递减,减到0那就说明阻塞时间到了,任务重新进入就绪态。next指针让TCB可以串成不同的链表,比如就绪链表、阻塞链表、信号量等待链表。
2.2 就绪表与优先级调度策略的取舍
TCB只是数据容器,真正决定调度行为的是就绪表的组织和查找算法。我这次实现的是固定优先级抢占式调度,共16个优先级(0最高,15最低),采用“位图标记+链表”的结构:用一个uint16_t ready_group的每一bit表示对应优先级上是否有任务就绪,每个优先级再挂一个单向链表。找最高优先级就绪任务时,只需要对ready_group做个前导零计数,复杂度是O(1),非常快。
static uint16_t ready_group; static tcb_t *ready_list[16]; /* 每个优先级一个链表头 */ static tcb_t *get_highest_ready_task(void) { uint32_t bit = __builtin_clz(ready_group); /* 专为ARM等平台优化 */ tcb_t *task = ready_list[bit]; /* 如果有同优先级轮转需求,从队头取然后移到队尾 */ return task; }这里有两个策略可以选:同优先级任务用时间片轮转,还是严格按FIFO排队?我最后选择了时间片轮转,每个tick会检查当前运行任务的已用时间片,达到阈值后强制移到队尾,让同优先级的兄弟任务有机会运行。这样对交互式任务更友好,不会出现一个任务死循环把同优先级其他任务饿死的情况。需要说明的是,__builtin_clz在x86上会被编译成BSR指令,在ARM上会生成CLZ指令,都用不到循环扫描,实时性上有保障。
2.3 任务创建时的栈初始化“伪装术”
任务创建时最容易被忽略的一个点是:一个新任务被第一次调度时,它根本没有“上一次被切出”时保存的上下文,那么切换代码怎么恢复寄存器?答案是创建任务时就要在任务栈里“伪造”一份初始上下文。可以把这个过程理解成给一张白纸提前画好“应填写的初始数据”,这样内核第一次切换时,看到的栈结构和老任务完全一致,恢复寄存器的代码不用分辨新老任务。
void task_stack_init(tcb_t *task, uint32_t *stack_top) { uint32_t *st = stack_top; /* 按 Cortex-M 的异常栈帧手动压栈 */ *--st = 0x01000000; /* xPSR,初始为Thumb状态 */ *--st = (uint32_t)task->entry; /* PC */ *--st = 0x0000000E; /* LR,初始返回后不回来 */ *--st = 0; /* R12 */ *--st = 0; /* R3 */ *--st = 0; /* R2 */ *--st = 0; /* R1 */ *--st = (uint32_t)task->arg; /* R0,作为入口参数 */ /* 剩余寄存器按需补0 */ for (int i = 0; i < 8; i++) *--st = 0; task->sp = st; }这段“伪寄存器”逻辑针对的是Cortex-M架构,不同类型架构的异常栈帧布局不一样,ARM7或RISC-V需要各自调整。但核心思想是通用的:新任务的初始栈必须具备一个合法、完整的异常返回帧,让CPU第一次恢复时直接跳进任务入口函数。
3. 上下文切换:纯C写不出调度器,关键还是要碰汇编
如果说TCB是内核的心脏,那么上下文切换就是心脏的搏动。可惜的是,C语言标准里没有“保存r4到r11寄存器”这种能力,要想完成精确的寄存器级切换,只能通过编译器内嵌汇编或独立的汇编函数来完成。这也是手写RTOS里最绕不开、也最容易出bug的部分。
3.1 为什么选PendSV做任务切换
Cortex-M系列有一个专门为RTOS设计的异常,叫PendSV(可挂起的系统服务)。它在所有其他中断处理完之后才执行,且可以被任意高优先级中断抢占。用PendSV做上下文切换的好处是:当一个中断正在处理时,如果内核触发了任务切换,这个切换不会立即打断中断流程,而是等中断返回前才执行,保证中断的实时响应不被内核调度“插队”。
实际做法是:在SysTick中断里扫描就绪表,如果发现需要切换任务,就挂起PendSV(把PendSV的挂起位置1),然后返回;CPU在退出中断上下文后,如果PendSV挂起且优先级最低,会立即进入PendSV处理器,在这里完成真正的寄存器保存与恢复。
3.2 PendSV切换的核心过程拆解
先看这段模拟Cortex-M的汇编切换代码,我在代码包中有近似实现:
__asm volatile ( " mrs r0, psp\n" /* R0 = 任务栈指针(线程模式下的PSP) */ " ldr r3, =current_tcb\n" " ldr r2, [r3]\n" /* R2 = 当前TCB地址 */ " stmdb r0!, {r4-r11, lr}\n" /* 保存当前任务剩余寄存器 */ " str r0, [r2]\n" /* 将最新栈指针写回TCB.sp */ " ldr r3, =next_tcb\n" " ldr r2, [r3]\n" /* R2 = 下一个TCB */ " ldr r0, [r2]\n" /* 取出新任务的栈指针 */ " ldmia r0!, {r4-r11, lr}\n" /* 恢复下一个任务的寄存器 */ " msr psp, r0\n" /* 更新PSP为新任务栈指针 */ " bx lr\n" /* 触发异常返回,硬件自动恢复r0-r3,r12,pc,xPSR */ );这段代码前半部分是保存现场,后半部分是恢复现场。注意stmdb r0!, {r4-r11, lr}会把当前任务的r4到r11和lr压进自己的任务栈,同时更新栈指针;然后str r0, [r2]把更新后的栈指针写回TCB。新任务恢复时反过来,先从新TCB拿到sp,再从栈里弹出对应寄存器。中间那一步“把最新栈指针写回TCB”是重中之重,少写这一句,第二次切换就会把栈搞乱。
3.3 任务进中断和进异常的区别
很多人学到上下文切换时会有一个疑问:为什么PendSV只保存了r4到r11和lr,r0到r3、r12、pc、xPSR去哪里了?这就要说到Cortex-M的硬件机制了。当一个异常或中断触发时,CPU硬件会自动把r0、r1、r2、r3、r12、LR、PC、xPSR这8个寄存器压到当前栈上;异常返回时再自动弹回来。也就是说,这部分上下文由硬件免费搞定,软件不用管。而r4到r11是“调用者不必保存”的寄存器,内核必须自己管理,所以PendSV代码里才手动保存它们。
这里会引出一个新手常见错误:中断服务函数如果在处理中修改了r4-r11,内核又不保存,那么从中断返回后,被打断的任务寄存器状态就被破坏了。解决办法是:要么中断服务函数保持极简,不触碰复杂逻辑;要么在中断入口显式保存现场。我这次的设计是中断里只做信号量释放、消息队列插入这类内核原语操作,通过原语内部的临界区保护,避免裸操作寄存器导致上下文污染。
4. 同步与通信机制:信号量、互斥锁和消息队列的完整封装
调度器让多个任务具备了“并发运行”的假象,但没有同步机制,多个任务一旦竞争共享资源,系统马上乱套。手写RTOS里的同步原语,是最能体现“用简单代码解决复杂问题”的部分。
4.1 信号量的实现与等待队列
信号量本质上就是一个计数器加一个等待链表。P操作(take)把计数减一,如果计数小于0就把当前任务挂到该信号量的等待链表,并触发调度;V操作(give)把计数加一,如果有任务在等待就唤醒一个。具体实现如下:
typedef struct semaphore { uint32_t count; tcb_t *wait_head; } sem_t; void sem_give(sem_t *sem) { uint32_t primask = enter_critical(); if (sem->wait_head != NULL) { tcb_t *task = sem->wait_head; /* 从等待链表摘除 */ sem->wait_head = task->next; task->next = NULL; task->state = TASK_READY; add_ready_task(task); } else { sem->count++; } exit_critical(primask); /* 如果当前任务优先级低于刚唤醒的任务,触发调度 */ schedule_if_needed(); } int sem_take(sem_t *sem, uint32_t timeout) { uint32_t primask = enter_critical(); if (sem->count > 0) { sem->count--; exit_critical(primask); return 0; } if (timeout == 0) { exit_critical(primask); return -1; /* 不等待,直接超时返回 */ } /* 将当前任务挂到等待链表 */ current_tcb->state = TASK_BLOCKED; current_tcb->delay_ticks = timeout; insert_wait_list(&sem->wait_head, current_tcb); exit_critical(primask); schedule(); return 0; }这段代码有两点值得注意。sem_give里先关中断再做链表操作,是因为信号量的take/give既可能发生在任务上下文,也可能发生在中断上下文。如果只是在take/give里铺一个位图作为“伪临界区”,中断和任务同时操作就会出问题。其次,schedule_if_needed()不是每次give都切换,只在高优先级任务被唤醒时才切换;这个优化对减少不必要的上下文切换很有帮助。
4.2 互斥锁与优先级继承
信号量虽然能实现互斥,但存在一个经典问题:优先级反转。假设低优先级任务持有一把锁,高优先级任务正在等这把锁,此时中优先级任务抢占了低优先级任务,高优先级任务反而被中优先级任务间接“卡住”,实时性根本无法保证。解决这个问题最常用的方案是优先级继承:当高优先级任务因等锁阻塞时,把当前持锁任务的优先级临时提升到高优先级任务的级别,等释放锁后恢复原优先级。
我这次没有实现完整的优先级继承,而是在互斥锁里加了一个简化版:持锁任务的TCB保存原始优先级,并记录哪个高优先级任务在等锁;锁释放时再恢复原优先级。这个简化版用十几行代码就能实现,但对教学和中小型嵌入式系统来说,收益非常明显。如果不想做优先级继承,最直接的替代方案是规定所有任务按固定顺序加锁,避免锁的交叉持有,从而根除死锁与部分反转问题。
4.3 消息队列:用环形缓冲区承载任务间数据流传
任务间通信除了共享内存,最常用的就是消息队列。这次实现的是一个定长消息队列,底层用环形缓冲区加信号量组合而成:
typedef struct msg_queue { uint8_t *pool; /* 定长消息池 */ uint32_t msg_size; uint32_t capacity; uint32_t head; uint32_t tail; uint32_t count; tcb_t *rx_wait; /* 等待接收的任务 */ tcb_t *tx_wait; /* 等待发送的任务 */ } msg_queue_t;发送和接收操作都在临界区里做入队/出队,操作完成后根据等待队列决定是否唤醒任务。消息队列一个很实用的点是:它天然解耦了生产者和消费者的时序。高优先级任务往队列里塞数据后马上去忙别的事,低优先级任务在空闲时从队列取数据处理,不会再因为一个慢任务阻塞整个系统。实测下来,这个定长队列在中断服务函数里也能安全使用,因为发送端只要关中断做入队操作,耗时极短,可以满足中断实时性要求。
5. 从main函数到第一个任务运行:启动流程里最容易出错的三步
很多手写RTOS的项目在调度器代码上花了很多精力,结果跑起来第一个任务就hardfault,问题往往不出在调度器本身,而是启动顺序不对。启动流程里的坑,我这次基本上都踩了一遍。
5.1 启动流程的标准顺序
一个微型RTOS从main函数开始,一般经历以下流程:
int main(void) { /* 1. 初始化系统时钟和外设 */ system_clock_init(); uart_init(); /* 2. 创建信号量、消息队列等内核对象 */ sem_init(&key_sem, 0); mq_init(&uart_queue, 4, 64, uart_queue_pool); /* 3. 创建用户任务,分配栈 */ task_create("led_task", led_task, NULL, 5, 512); task_create("uart_task", uart_task, NULL, 3, 1024); task_create("idle_task", idle_task, NULL, 15, 256); /* 4. 启动调度器,不再返回 */ rtos_start(); return 0; }rtos_start()里要做三件事:把当前运行环境的栈指针当作“第一个任务切换的起点”来初始化;给每个任务的TCB挂上初始状态;从就绪表里挑出最高优先级任务,触发PendSV进入该任务。这三个环节的顺序不能乱,尤其不能在任务创建完成前就开启SysTick,否则SysTick中断一进来,扫描的是未初始化的就绪表,立刻崩溃。
5.2 第一个任务切换的特殊性
第一个任务的切换和后续任务切换有着本质不同:第一个任务切换时,当前上下文其实是rtos_start()函数所在的main上下文,不是任何任务。如果直接把main的上下文保存进某个TCB,这个TCB就变成“伪任务”,后续调度可能产生奇怪行为。稳妥做法是:在启动调度器时不做保存,只做“恢复”,直接把第一个任务的初始上下文加载进CPU。换句话说,启动调度器是一个单向门,只恢复、不保存。
void rtos_start(void) { tcb_t *first = get_highest_ready_task(); current_tcb = first; /* 直接加载第一个任务的初始栈 */ __asm volatile ( " ldr r0, [%0]\n" /* 取first->sp */ " ldr sp, r0\n" " pop {r4-r11, lr}\n" " pop {r0-r3}\n" " add sp, sp, #8\n" " bx lr\n" :: "r" (first) ); }这句“只恢复不保存”看着简单,但如果你在rtos_start里做了很多初始化操作,main函数栈上可能残留了杂乱数据,加载新任务后会直接覆盖它们,只要新任务不回退到main就没事。为了保险,rtos_start之后一般不会允许返回,这也是这个函数用for(;;);兜底的原因。
5.3 时基配置与tick中断的细节
系统时基(tick)是整个调度器的时间基准。我使用的是ARM Cortex-M自带的SysTick定时器,通常配置成1ms或10ms中断一次。tick中断里主要做两件事:递减每个阻塞任务的delay_ticks,如果到0就重新加入就绪表;判断当前任务的运行时间片是否用完,如果是就标记需要切换。
这里有个容易被人忽略的细节:SysTick中断本身也会抢占任务,所以在SysTick里调用schedule()时,不能直接做完整上下文切换,只能标记“需要切换”,真正的切换在PendSV里进行。如果直接在SysTick里做上下文切换,SysTick返回时的异常栈帧和PendSV返回时的异常栈帧会纠缠在一起,轻则丢数据,重则直接hardfault。这是我在调试早期遇到的最隐蔽问题之一。
6. 实际调试中的三个崩溃Bug:从现象到根因的完整排查链路
手写内核最刺激的部分是调试。这里记录的是我在验证这套RTOS过程中遇到的最典型的三个问题,每个都值得单独拿出来说说排查思路。
6.1 问题一:任务运行几次后随机HardFault
现象是LED任务能闪几分钟,但一旦串口任务开始收发数据,系统就随机跳进HardFault。第一反应是消息队列代码写错了,但逐行检查队列代码并无明显问题。后来我意识到问题可能出在栈上:串口任务栈分配了1024字节,但串口协议解析用了大数组,加上printf类函数的栈消耗,在复杂路径下栈可能溢出到下一块内存区域。
排查方法是给每个任务栈尾部填充固定魔数(比如0xDEADBEEF),系统跑一段时间后检查所有任务栈的魔数区是否被破坏。结果串口任务栈顶部有大概200字节被覆盖,基本坐实栈溢出。解决办法是从两个方向处理:一方面把任务栈加大到2048,同时把printf的缓冲区移到全局;另一方面在任务创建函数里增加栈溢出检测逻辑,每次切换时检查栈指针是否越界。从此hardfault再没出现。
这个问题的经验是:栈溢出不会以“直接报错”的形式出现,它总是先破坏相邻数据,然后在一个毫不相关的地方炸开。所以RTOS项目里,栈边界魔数检查应该是标配,而不是可选项。
6.2 问题二:高优先级任务被低优先级任务“卡死”
现象是串口任务(优先级3)发送一条指令后,无法再接收数据,LED任务(优先级5)却在正常工作。通过LED闪烁频率做“简易示波器”观察,发现串口任务并非完全阻塞,而是周期性卡顿几秒。进一步在调试器里挂上Watchpoint,发现串口任务实际是在一个信号量take上长时间等待。
再追根因,发现是缓冲区互斥锁被低优先级的数据解析任务持有,而一个中优先级的空闲任务一直占着CPU不放,导致数据解析任务得不到执行、锁无法释放,高优先级串口任务被反向“卡”住。这就是标准的优先级反转。
修复方案是给互斥锁加上前半节提到的优先级继承。加完之后用相同场景复测,串口任务的响应时间从数千毫秒恢复到几十毫秒,效果立竿见影。如果你使用的现成RTOS没有提供优先级继承选项,也可以通过在应用层约定锁的顺序来规避反转,但最可靠的还是在内核层面解决。
6.3 问题三:tick中断里切换任务导致“幽灵优先级”
这个问题更加隐蔽。现象是所有任务运行正常,但偶尔会出现一个低优先级任务连续运行很长时间,高优先级任务虽然处于就绪态却迟迟不切换。通过日志打印每个任务的TCB状态,发现低优先级任务的state始终是TASK_RUNNING,而高优先级任务的state已经是TASK_READY,但调度器仿佛没看到他。
追查调度器代码后发现问题出在SysTick中断的处理逻辑:我在tick中断里直接调用了schedule(),并且没有关中断保护就绪表的操作。两个中断嵌套时,外层中断还没完成,内层调度器已经修改了ready_group,返回后再恢复了一个过期的就绪表状态,导致最高优先级任务丢失。
修复就是前面提到的规则:tick中断里只负责标记切换请求,全部就绪表的修改都放在临界区内做,实际的上下文切换工作交给PendSV。这个改动之后,“幽灵优先级”问题消失。这次的教训我认为最值得记下来:在中断处理函数里修改内核数据结构,哪怕只是读一下,也必须有明确的临界区保护策略,否则中断嵌套就会成为定时炸弹。
7. 如果从头再来一次,我会在动手前先想清楚的几件事
这套微型RTOS调试稳定之后,我回头审视整个开发过程,还是有很多可以做得更好的地方。如果你也想尝试手写RTOS,我认为有几个前置功课值得先做。
首先是把目标CPU的异常模型彻底搞懂。以Cortex-M为例,你至少要知道:哪些寄存器由硬件自动保存,哪些由软件保存;异常返回的EXC_RETURN机制是什么;PendSV和SysTick的优先级关系怎么设置。我最早就是在这里偷懒,导致后面花了大量时间排查上下文损坏。建议先读一遍ARM官方《Cortex-M3 Devices Generic User Guide》的异常模型章节,再动手写汇编,比自己一边试错一边查手册高效得多。
其次是调试工具的准备。手写RTOS的调试难度远超普通裸机程序,因为程序运行轨迹不按线性执行。我强烈建议至少准备三种工具:支持断点的调试器(可以看每个任务的调用栈)、能实时观察内存区域的监视窗口(检查栈魔数)、以及一个逻辑分析仪或示波器(观察GPIO翻转时序)。很多人觉得LED闪烁法土,但在判断任务是否按预期时间片轮转时,LED波形比任何printf都直观。
最后是要给内核写一套轻量级自检机制。我后来在代码里加了一个简单的心跳任务,每100ms翻转一次GPIO,并在每个任务入口做一次栈边界检查;同时用一个数组记录最近50次调度的任务ID和时间戳。这套“软件跟踪器”在出现异常时能快速回放出真实运行轨迹,比单纯看代码高效得多。你可以在自己的内核里直接抄这个思路,本质上就是给内核加个黑匣子。
手写RTOS不是为了证明自己比FreeRTOS作者厉害,而是为了让你在面对“任务调度”“中断嵌套”“优先级反转”这些术语时,脑子里能浮现出寄存器、栈指针和链表节点的具体图像。亲手把dispatch循环写出来、跑通、再看着它发生bug、再修复,这套过程带给人的收益,远超读十遍别人写好的源码。如果你决定试试,我建议从最小的调度器跑通三个LED任务开始,再一步步加入信号量和队列。等你亲手写出第一套稳定运行的上下文切换代码时,你会对“操作系统到底在干什么”这件事产生完全不同的理解。
本文还有配套的精品资源,点击获取