嵌入式Linux核心课程课
文章目录
- 嵌入式Linux核心课程课
-
- 一、课程目标
- 二、进程调度核心概述
-
- [2.1 调度核心目的](#2.1 调度核心目的)
- [2.2 调度器核心概念](#2.2 调度器核心概念)
- 三、Linux核心调度策略(详解+样例代码)
-
- [3.1 调度策略一:CFS调度(Completely Fair Scheduler,完全公平调度)](#3.1 调度策略一:CFS调度(Completely Fair Scheduler,完全公平调度))
- [3.2 调度策略二:RT调度(Real-Time Scheduler,实时调度)](#3.2 调度策略二:RT调度(Real-Time Scheduler,实时调度))
- [3.3 样例代码1:查看进程调度策略与优先级(用户态)](#3.3 样例代码1:查看进程调度策略与优先级(用户态))
- [3.4 样例代码2:内核态查看调度类(验证调度机制)](#3.4 样例代码2:内核态查看调度类(验证调度机制))
- 四、进程调度触发时机
- 五、进程销毁机制(详解+样例代码)
-
- [5.1 进程销毁的两种方式](#5.1 进程销毁的两种方式)
- [5.2 进程销毁完整流程](#5.2 进程销毁完整流程)
- [5.3 关键概念:僵尸进程与孤儿进程](#5.3 关键概念:僵尸进程与孤儿进程)
- [5.4 样例代码3:进程主动退出与父进程回收(用户态)](#5.4 样例代码3:进程主动退出与父进程回收(用户态))
- [5.5 样例代码4:内核态释放进程资源(简化版)](#5.5 样例代码4:内核态释放进程资源(简化版))
- 六、课堂练习
- 七、课后作业
- 八、本章总结
- 九、核心关键词
- [第10课 课程回顾总结](#第10课 课程回顾总结)
- [上一节课答案:第9课 进程管理子系统(一):进程概念与创建 实战作业代码](#上一节课答案:第9课 进程管理子系统(一):进程概念与创建 实战作业代码)
一、课程目标
-
理解进程调度的核心意义与设计原则,掌握调度器的作用
-
掌握Linux内核两种核心调度策略(CFS、RT)的原理与适用场景
-
理解进程优先级、时间片的分配逻辑,以及调度触发时机
-
掌握进程销毁的完整流程,理解僵尸进程、孤儿进程的产生与处理
-
能通过代码分析进程调度机制,排查调度相关异常
-
衔接进程创建知识,形成完整的进程生命周期管理认知
二、进程调度核心概述
进程调度是内核的核心功能之一,本质是合理分配CPU资源,决定多个就绪进程中哪个能获得CPU执行权,确保系统高效、公平、实时地运行。
2.1 调度核心目的
-
提高CPU利用率:避免CPU空闲,让CPU始终处于有效工作状态
-
保证系统公平性:为每个进程分配合理的CPU时间,避免饥饿
-
满足实时性需求:嵌入式场景中,确保高优先级实时进程快速响应
-
优化系统响应速度:缩短进程等待时间,提升用户体验
2.2 调度器核心概念
Linux内核通过**调度器类(sched_class)**管理不同类型的调度策略,每个进程都属于某一个调度类,调度器根据调度类的优先级决定调度顺序。
核心调度器:Linux 2.6.23后引入CFS调度器,替代传统O(1)调度器,成为默认调度器。
三、Linux核心调度策略(详解+样例代码)
Linux内核支持多种调度策略,核心分为两大类:普通进程调度(CFS)和实时进程调度(RT),分别对应不同的应用场景。
3.1 调度策略一:CFS调度(Completely Fair Scheduler,完全公平调度)
核心原理
CFS是Linux默认调度策略,适用于普通用户进程(非实时),核心思想是"公平分配CPU时间"------为每个进程分配一个"虚拟运行时间",调度器始终选择虚拟运行时间最少的进程执行。
关键机制:
-
虚拟运行时间(vruntime):根据进程优先级动态调整,优先级越高,vruntime增长越慢,获得CPU的机会越多。
-
时间片:CFS不固定时间片大小,而是根据就绪进程数量动态调整,避免短进程等待过长时间。
-
调度触发:进程时间片耗尽、进程主动放弃CPU、高优先级进程进入就绪态时触发。
适用场景
普通应用程序(如Shell、浏览器、后台服务),对实时性要求不高,追求公平性和CPU利用率。
3.2 调度策略二:RT调度(Real-Time Scheduler,实时调度)
核心原理
RT调度适用于实时进程,采用"优先级抢占式调度"------实时进程优先级高于所有普通进程,高优先级实时进程可抢占低优先级进程的CPU执行权。
RT调度的两种子策略:
-
SCHED_FIFO(先进先出):同优先级实时进程,按就绪顺序执行,一旦执行,直到主动放弃CPU或被更高优先级进程抢占。
-
SCHED_RR(时间片轮转):同优先级实时进程,按时间片轮转执行,时间片耗尽后切换到下一个同优先级进程。
适用场景
嵌入式实时场景(如工业控制、车载系统、医疗设备),对响应时间有严格要求(如毫秒级响应)。
3.3 样例代码1:查看进程调度策略与优先级(用户态)
C
查看当前进程的调度策略、优先级,验证CFS调度#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sched.h>
int main() {
pid_t pid = getpid();
int policy;
struct sched_param param;
// 获取当前进程的调度策略
policy = sched_getscheduler(pid);
if (policy == -1) {
perror("sched_getscheduler failed");
return -1;
}
// 获取当前进程的调度参数(优先级)
if (sched_getparam(pid, ¶m) == -1) {
perror("sched_getparam failed");
return -1;
}
// 打印调度策略
printf("当前进程PID:%d\n", pid);
switch (policy) {
case SCHED_OTHER:
printf("调度策略:CFS(SCHED_OTHER)\n");
break;
case SCHED_FIFO:
printf("调度策略:RT-FIFO(SCHED_FIFO)\n");
break;
case SCHED_RR:
printf("调度策略:RT-RR(SCHED_RR)\n");
break;
default:
printf("调度策略:未知\n");
}
// 打印优先级(CFS优先级范围0-139,RT优先级范围1-99)
printf("进程优先级:%d\n", param.sched_priority);
return 0;
}
运行结果:
当前进程PID:1234
调度策略:CFS(SCHED_OTHER)
进程优先级:0
说明:普通用户进程默认使用CFS调度策略,优先级为0(CFS优先级0对应nice值0,nice值范围-20~19,值越小优先级越高)。
3.4 样例代码2:内核态查看调度类(验证调度机制)
C
内核模块,查看当前进程的调度类,区分CFS与RT调度
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/init.h>
static int __init sched_class_demo_init(void)
{
// 判断当前进程的调度类
if (current->sched_class == &fair_sched_class) {
printk("当前进程使用 CFS 公平调度\n");
} else if (current->sched_class == &rt_sched_class) {
printk("当前进程使用 RT 实时调度\n");
} else {
printk("当前进程使用其他调度类\n");
}
// 打印进程优先级(prio为动态优先级,static_prio为静态优先级)
printk("进程静态优先级:%d,动态优先级:%d\n", current->static_prio, current->prio);
return 0;
}
static void __exit sched_class_demo_exit(void)
{
printk("调度类演示模块卸载\n");
}
module_init(sched_class_demo_init);
module_exit(sched_class_demo_exit);
MODULE_LICENSE("GPL");
运行结果:
当前进程使用 CFS 公平调度
进程静态优先级:120,动态优先级:120
说明:CFS进程的静态优先级默认120(对应nice值0),动态优先级与静态优先级一致;RT进程的静态优先级范围为1~99,高于CFS进程。
四、进程调度触发时机
进程调度不会随机触发,内核在特定时机触发调度,确保系统稳定高效,核心触发时机分为以下4种:
-
进程时间片耗尽:CFS调度中,进程的虚拟运行时间达到阈值,调度器触发切换。
-
进程主动放弃CPU:进程调用sleep()、wait()等函数,进入睡眠状态,主动释放CPU。
-
高优先级进程就绪:高优先级进程(尤其是RT进程)从睡眠或创建状态进入就绪态,抢占当前运行进程的CPU。
-
内核态返回用户态时:内核处理完系统调用、中断后,返回用户态前,检查是否需要调度。
五、进程销毁机制(详解+样例代码)
进程销毁是进程生命周期的最后一步,核心是释放进程占用的所有资源(内存、文件描述符、PCB等),分为"主动退出"和"被动终止"两种方式。
5.1 进程销毁的两种方式
方式1:主动退出(正常销毁)
进程通过调用exit()、_exit()系统调用主动退出,释放资源,常见场景:
-
用户程序执行完毕,main函数return,底层调用exit();
-
进程主动调用exit(),终止自身运行。
方式2:被动终止(异常销毁)
进程被其他进程或内核终止,常见场景:
-
其他进程调用kill()系统调用,发送终止信号(如SIGKILL);
-
进程执行非法操作(如除以0、访问非法内存),内核触发异常,终止进程;
-
父进程终止,子进程被init进程(PID=1)收养,若子进程退出后父进程未回收,成为僵尸进程。
5.2 进程销毁完整流程
-
触发退出:进程调用exit()或被信号终止,进入退出流程;
-
关闭资源:关闭进程打开的所有文件描述符、网络连接、信号处理等;
-
释放内存:释放进程地址空间、页表、堆栈等内存资源;
-
更新PCB:将进程状态设置为EXIT_ZOMBIE(僵尸态),保存退出状态码;
-
通知父进程:向父进程发送SIGCHLD信号,告知父进程自身已退出;
-
资源回收:父进程调用wait()/waitpid(),读取子进程退出状态,释放子进程PCB,进程彻底销毁;若父进程未回收,子进程保持僵尸态,直到父进程退出后被init进程回收。
5.3 关键概念:僵尸进程与孤儿进程
-
僵尸进程:子进程退出,父进程未调用wait()/waitpid()回收,子进程PCB未释放,状态为EXIT_ZOMBIE。危害:占用PID资源,长期积累会导致系统PID耗尽。
-
孤儿进程:父进程先于子进程退出,子进程被init进程(PID=1)收养,子进程退出后由init进程回收,不会成为僵尸进程。
5.4 样例代码3:进程主动退出与父进程回收(用户态)
C
创建子进程,子进程主动退出,父进程调用waitpid()回收,避免僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t pid, wpid;
int status;
// 创建子进程
pid = fork();
if (pid < 0) {
perror("fork failed");
return -1;
}
if (pid == 0) {
// 子进程:执行任务后主动退出
printf("子进程(PID:%d):执行任务完毕,主动退出\n", getpid());
exit(0); // 主动退出,退出状态码0
} else {
// 父进程:等待子进程退出并回收
printf("父进程(PID:%d):等待子进程退出...\n", getpid());
// waitpid() 等待指定子进程退出,获取退出状态
wpid = waitpid(pid, &status, 0);
if (wpid == -1) {
perror("waitpid failed");
return -1;
}
// 判断子进程退出状态
if (WIFEXITED(status)) {
printf("父进程:子进程(PID:%d)正常退出,退出状态码:%d\n", wpid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("父进程:子进程(PID:%d)被信号终止,终止信号:%d\n", wpid, WTERMSIG(status));
}
}
return 0;
}
运行结果:
父进程(PID:1234):等待子进程退出...
子进程(PID:1235):执行任务完毕,主动退出
父进程:子进程(PID:1235)正常退出,退出状态码:0
说明:父进程通过waitpid()回收子进程,避免子进程成为僵尸进程;WIFEXITED(status)判断子进程是否正常退出,WEXITSTATUS(status)获取退出状态码。
5.5 样例代码4:内核态释放进程资源(简化版)
C
内核模块,模拟进程退出时释放资源的核心逻辑
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/slab.h>
static int __init proc_exit_demo_init(void)
{
struct task_struct *task = current;
printk("当前进程(PID:%d):模拟退出前释放资源\n", task->pid);
// 模拟释放进程资源(实际内核由do_exit()函数完成)
if (task->mm) {
printk("释放进程地址空间(mm_struct)\n");
// 内核中实际调用mmput()释放mm_struct
}
printk("模拟进程状态设置为僵尸态(EXIT_ZOMBIE)\n");
printk("通知父进程,等待回收\n");
return 0;
}
static void __exit proc_exit_demo_exit(void)
{
printk("进程退出演示模块卸载\n");
}
module_init(proc_exit_demo_init);
module_exit(proc_exit_demo_exit);
MODULE_LICENSE("GPL");
运行结果:
当前进程(PID:1236):模拟退出前释放资源
释放进程地址空间(mm_struct)
模拟进程状态设置为僵尸态(EXIT_ZOMBIE)
通知父进程,等待回收
说明:内核中进程退出的核心函数是do_exit(),负责释放进程资源、设置僵尸态、通知父进程,本代码模拟了该过程的核心逻辑。
六、课堂练习
-
简述CFS调度与RT调度的核心区别,以及各自的适用场景。
-
Linux进程调度的触发时机有哪些?
-
什么是僵尸进程?如何避免僵尸进程产生?
-
CFS调度中,虚拟运行时间(vruntime)的作用是什么?
-
进程主动退出与被动终止的区别是什么?各自的触发场景有哪些?
七、课后作业
-
画图描述进程调度的完整流程,标注CFS与RT调度的优先级关系。
-
编写程序:创建两个RT进程(SCHED_RR策略),设置不同优先级,验证高优先级进程抢占低优先级进程。
-
编写程序:创建子进程后,父进程不调用wait(),观察子进程是否成为僵尸进程,并用ps命令查看。
-
简述进程销毁的完整流程,说明init进程在进程回收中的作用。
-
查阅资料,说明Linux内核中do_fork()(创建进程)与do_exit()(销毁进程)的核心关联。
八、本章总结
本章承接上一课进程创建的内容,详细讲解了进程管理子系统的核心后续流程------进程调度与销毁。重点介绍了CFS完全公平调度和RT实时调度两种核心策略,明确了两者的原理、适用场景及优先级差异;解析了进程调度的触发时机,说明内核如何合理分配CPU资源;同时讲解了进程销毁的两种方式、完整流程,以及僵尸进程、孤儿进程的产生与处理方法。
进程调度是保障系统高效、实时运行的关键,进程销毁是释放系统资源、避免资源泄漏的核心环节。通过本课学习,形成了完整的进程生命周期(创建→调度→运行→销毁)认知,掌握了进程调度与销毁的底层逻辑,为后续学习进程通信、内核调试、嵌入式实时系统优化打下坚实基础。
九、核心关键词
进程调度、CFS调度、RT调度、调度器、虚拟运行时间、时间片、进程销毁、僵尸进程、孤儿进程、exit()、waitpid()
第10课 课程回顾总结
本课作为进程管理子系统的第二部分,重点讲解了进程调度与销毁的核心机制,衔接上一课进程创建的内容,形成了完整的进程生命周期管理体系。课程首先明确了进程调度的核心目的的是合理分配CPU资源,保障系统公平性、高效性和实时性,介绍了调度器类的核心概念。随后详细解析了CFS完全公平调度和RT实时调度两种核心策略,对比了两者的原理、关键机制和适用场景,通过用户态与内核态样例代码,直观展示了调度策略的应用与验证方法。
课程还讲解了进程调度的四大触发时机,明确了调度器何时触发进程切换。在进程销毁部分,详细说明了主动退出与被动终止两种方式,拆解了进程销毁的完整流程,重点解释了僵尸进程与孤儿进程的产生原因、危害及处理方法,通过代码演示了父进程回收子进程的核心操作。
通过本课学习,掌握了进程调度与销毁的底层逻辑,理解了CFS与RT调度的区别,能通过代码分析调度机制、避免僵尸进程。本课知识是嵌入式Linux内核开发的重要基础,对理解系统资源管理、优化实时性、排查进程异常具有重要意义,为后续进程通信、驱动开发等内容奠定了基础。
上一节课答案:第9课 进程管理子系统(一):进程概念与创建 实战作业代码
C
进程创建综合实战(fork+写时复制验证+进程信息打印)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
int main() {
pid_t pid;
char buf[32] = "父进程初始数据";
printf("【父进程】PID:%d,准备创建子进程,buf:%s\n", getpid(), buf);
// 调用fork创建子进程,验证写时复制
pid = fork();
if (pid < 0) {
perror("fork创建子进程失败");
return -1;
}
if (pid == 0) {
// 子进程:修改buf,验证写时复制(修改后才复制页)
strcpy(buf, "子进程修改后数据");
printf("【子进程】PID:%d,PPID:%d,buf:%s\n", getpid(), getppid(), buf);
// 子进程执行完毕,主动退出
sleep(2);
printf("【子进程】执行完毕,退出\n");
return 0;
} else {
// 父进程:不修改buf,查看数据是否被影响(验证COW)
sleep(1); // 等待子进程修改数据
printf("【父进程】PID:%d,子进程PID:%d,buf:%s\n", getpid(), pid, buf);
// 等待子进程退出,避免僵尸进程
wait(NULL);
printf("【父进程】子进程已退出,父进程执行完毕\n");
}
return 0;
}
代码功能说明
该程序是第9课进程概念与创建的实战作业,核心验证fork创建子进程及写时复制(COW)机制。程序中父进程创建子进程后,子进程修改共享数据缓冲区,父进程不修改,通过打印缓冲区内容,验证写时复制的核心逻辑------只有当子进程修改数据时,内核才会复制对应内存页,父进程数据不受影响。同时程序打印父子进程PID、PPID,展示进程关系,父进程通过wait()回收子进程,避免僵尸进程。代码贴合课程重点,巩固进程创建、写时复制、进程关系等核心知识点,为后续进程调度、销毁学习打基础。
注意事项
-
编译命令:gcc fork_cow_demo.c -o fork_cow_demo
-
运行时需确保父进程等待子进程执行(sleep()函数),否则可能出现打印顺序混乱。
-
写时复制验证关键:子进程修改数据后,父进程数据应保持初始值,若两者数据一致,说明COW机制未生效(需检查内核配置)。
-
父进程必须调用wait()/waitpid()回收子进程,否则子进程会成为僵尸进程,可通过ps -ef | grep defunct查看。
-
fork创建子进程后,父子进程执行顺序由调度器决定,sleep()函数仅用于控制打印顺序,不影响进程执行逻辑。
-
若fork失败,大概率是系统PID资源耗尽或权限不足,可通过ps -ef查看进程数量,结束无用进程后重试。
-
可使用strace ./fork_cow_demo 查看fork系统调用的执行过程,观察COW机制的底层调用。