本篇定位:Linux 内核的"宇宙中心"是 task_struct——所有子系统都围绕它。对于有过轻量级实时操作系统,如FreeRTOS TCB 到 port 层经验的工程师,本篇把"Linux 进程"对你 FreeRTOS 任务的增量讲清:task_struct 比 TCB 胖十倍(含 mm/fs/files/signals)、调度从"优先级抢占"跳到"CFS 公平 + EEVDF"、上下文切换从"切栈"跳到"切地址空间 + 切栈 + 切 fs/files"。读完能写内核线程、能调调度问题、能讲清 CFS vs FreeRTOS 优先级调度的本质差异。


目录

一、task_struct:宇宙中心的数据结构

1.1 task_struct 有多大

  • FreeRTOS TCB:几百字节(栈指针/优先级/状态/链表节点)
  • Linux task_struct:约 8000 字节(ARM64/RISC-V 64)

为什么这么大?因为 Linux 进程要独立地址空间 + 文件上下文 + 信号 + cgroup + namespace + 审计…全挂这里。

1.2 task_struct 核心字段(分组)

struct task_struct {
    // === 标识 ===
    pid_t pid;                  // 进程 ID(线程组里是线程 ID)
    pid_t tgid;                 // 线程组 ID(进程 ID,用户看到的)
    struct task_struct *group_leader;  // 线程组首领
    char comm[TASK_COMM_LEN];   // 可执行文件名(16 字符)

    // === 状态 ===
    volatile long state;        // 进程状态(见 §二)
    int exit_state;
    int prio, static_prio, normal_prio;  // 优先级
    unsigned int policy;        // 调度策略(SCHED_NORMAL/FIFO/RR)

    // === 调度 ===
    const struct sched_class *sched_class;  // 调度类
    struct sched_entity se;     // CFS 调度实体(vruntime 等)
    struct sched_rt_entity rt;  // RT 调度实体
    unsigned int rt_priority;
    cpumask_t cpus_allowed;     // 允许跑的 CPU

    // === 家族 ===
    struct task_struct *parent;      // 父进程
    struct list_head children;       // 子进程链表
    struct list_head sibling;

    // === 内存(★对照 FreeRTOS 最大差异)===
    struct mm_struct *mm;       // 进程地址空间(线程共享)
    struct mm_struct *active_mm; // 内核线程用(借来的 mm)

    // === 文件 ===
    struct files_struct *files; // 打开的文件表
    struct fs_struct *fs;       // 文件系统根/root

    // === 信号 ===
    struct signal_struct *signal;
    struct sighand_struct *sighand;
    sigset_t blocked, real_blocked;

    // === 栈 ===
    void *stack;                // 内核栈(union with thread_info)

    // === 时间统计 ===
    u64 utime, stime;           // 用户态/内核态时间
    u64 nvcsw, nivcsw;          // 自愿/非自愿切换次数

    // === namespace/cgroup(容器)===
    struct nsproxy *nsproxy;
    struct css_set __rcu *cgroups;

    // ... 还有几百个字段
};

1.3 对照 FreeRTOS TCB

字段FreeRTOS TCBLinux task_struct差异
栈指针pxTopOfStacksp(在 stack 里)同
优先级uxPriorityprio/static_prioLinux 更复杂(多套)
状态eTaskStatestateLinux 状态更多
链表xStateListItem 等list_head 多个同思路
地址空间❌ 无mm_struct ★Linux 最大增量
文件表❌ 无files_struct ★Linux 最大增量
信号❌ 无signal/sighand ★Linux 有
时间统计❌ 无utime/stimeLinux 有

嵌入式视角:
Linux task_struct 是"胖版 TCB"——多了 mm(地址空间)/files(文件)/signals(信号)/namespace(容器)。因为 Linux 进程要独立地址空间和文件上下文,FreeRTOS 任务全在一个地址空间共享一切。字段虽多,核心思路同(标识/状态/调度/栈),增量在 mm 和 files。


二、进程状态

2.1 Linux 进程状态

#define TASK_RUNNING        0   // 可运行(就绪或正在跑)
#define TASK_INTERRUPTIBLE  1   // 可中断睡眠(信号能唤醒)
#define TASK_UNINTERRUPTIBLE 2  // 不可中断睡眠(信号不唤醒,等 IO)
#define __TASK_STOPPED      4   // 停止(信号 SIGSTOP)
#define __TASK_TRACED      8   // 被跟踪(gdb)
#define EXIT_ZOMBIE       16   // 僵尸(死了等父收尸)
#define EXIT_DEAD         32   // 最终死亡
状态含义排查要点
R正在运行或在运行队列等待 CPU看 %CPU、top 是否持续占用
S可中断睡眠,等事件/IO/信号量能被信号唤醒,kill 通常有效
D不可中断睡眠,通常等磁盘/设备 IO信号唤不醒,重点查硬件/驱动/NFS
T被暂停,如 Ctrl+Z、SIGSTOP发 SIGCONT 可恢复
t被调试器/ptrace 跟踪暂停gdb 断点命中时常见
Z僵尸进程,已退出但父进程未回收不占 CPU/内存,但占 PID
X已死亡,PCB 已回收通常不会在 ps/top 里看到

2.2 状态机

       fork()
         │
         ▼
    ┌─────────┐  调度选中   ┌─────────┐
    │ RUNNING │ ─────────> │ RUNNING │
    │ (就绪)  │ <───────── │ (运行)  │
    └────┬────┘  时间片到   └────┬────┘
         │                        │
         │ 等 IO/信号量           │ 信号/IO 完成
         ▼                        ▼
    ┌──────────────┐         ┌─────────┐
    │INTERRUPTIBLE │ ───────>│ RUNNING │
    │ (可中断睡)   │  唤醒    │ (就绪)  │
    └──────────────┘         └─────────┘
         │
         │ 等 磁盘 IO(D 状态)
         ▼
    ┌────────────────┐
    │UNINTERRUPTIBLE │  信号唤不醒,只能 IO 完成
    │ (不可中断睡)   │
    └────────────────┘

2.3 关键状态解读

  • TASK_RUNNING:就绪(在运行队列)或正在跑(单 CPU 同时只有一个"跑")
  • TASK_INTERRUPTIBLE:可中断睡眠,等事件(信号量/IO/信号),信号能打断
  • TASK_UNINTERRUPTIBLE:D 状态,等磁盘 IO 等,信号唤不醒——这是"杀不掉"的进程(kill -9 也没用)
  • TASK_ZOMBIE:死了但父没 wait(),占 PID 和退出码,等收尸

2.4 ps 看状态

ps -eo pid,stat,comm
# STAT 列:
# R=running  S=interruptible sleep  D=uninterruptible sleep
# T=stopped  Z=zombie  X=dead
# + 前台  s 会话首  l 多线程  < 高优先级  N 低优先级

2.5 对照 FreeRTOS 状态

FreeRTOSLinux差异
RunningRUNNING(运行)同
ReadyRUNNING(就绪)Linux 不分就绪/运行(都 RUNNING)
BlockedINTERRUPTIBLE/UNINTERRUPTIBLELinux 分两种睡眠
SuspendedSTOPPED类似
DeletedZOMBIELinux 要父收尸

D 状态(不可中断睡眠 TASK_UNINTERRUPTIBLE)是 Linux 的"杀不掉"
FreeRTOS 任务阻塞 = 等 signal/queue,能被删。Linux D 状态(等磁盘 IO)信号唤不醒,kill -9 也杀不掉——因为杀它要它响应信号,它在 D 不响应。这是 Linux 比 FreeRTOS 复杂的点😄 状态进程正在内核里等待,比如:
磁盘 I/O 没返回;
NFS/网络存储断联;
驱动/硬件卡住;
持有内核锁或等待关键资源。
这时信号会被标记到 task 上,但进程不会立刻被唤醒去处理它。只有等它等待的资源返回、内核主动唤醒它,它才有机会退出。
它之所以“杀不掉”,不是信号没发出去,而是进程在内核态等待关键资源时,内核不会立刻让它处理信号。
Linux 这样设计
主要是保护内核数据一致性。
如果进程正在写磁盘、持有锁、操作硬件寄存器,中途被信号强行打断,可能导致:
数据写到一半,文件系统不一致;
锁没释放,其他进程永远拿不到;
硬件/DMA 状态混乱。
所以内核把这类等待设为“不可中断”。
你调"进程杀不掉"先看是不是 D 状态。
ps -eo pid,ppid,stat,wchan,comm | grep D
cat /proc//stack
lsof -p
dmesg -T | tail


三、进程 vs 线程 vs 内核线程

3.1 Linux 不区分进程线程(都是 task_struct)

概念mm 是否共享files 是否共享创建方式
进程不共享(独立)不共享fork()
线程共享共享clone(CLONE_VM|CLONE_FILES|…)
内核线程无 mm(借 active_mm)共享 init_fileskthread_create()

3.2 fork / clone / vfork

Linux 进程创建底层都是 clone 系统调用,靠 flag 控制共享什么:

// fork:不共享(子独立)
clone(SIGCHLD);

// pthread_create:共享 mm/files/信号等
clone(CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | ...);

// vfork:共享 mm,父阻塞到子 exec/exit
clone(CLONE_VM | CLONE_VFORK);

3.3 内核线程(kthread)

内核线程:只在内核态跑,没用户态地址空间。用途:后台活(kworker/ksoftirqd/kthreadd/migration)。

// 创建内核线程
struct task_struct *k = kthread_create(my_thread_fn, arg, "mythread");
wake_up_process(k);  // 唤醒让它跑

// 或一步到位
struct task_struct *k = kthread_run(my_thread_fn, arg, "mythread");

内核线程的 mm 为 NULL,跑时 active_mm 借前一个进程的 mm(省切页表)。

嵌入式视角:内核线程 = FreeRTOS 任务
你 FreeRTOS 任务 = 函数死循环。Linux 内核线程最像 FreeRTOS 任务——一个函数在内核态循环跑。你写驱动要后台活(轮询/处理),用 kthread_run 起一个,和 FreeRTOS xTaskCreate 同构。区别:内核线程在内核态(全权),FreeRTOS 任务在"内核态"(FreeRTOS 无分层)。用户态线程( pthread)是另一回事,在 U 态跑。

3.4 进程 / 线程 / 内核线程总结

Linux 里最核心的一点是:内核不区分“进程”和“线程”,它们都是 task_struct。
差别只在于:创建时共享了哪些资源。

3.4.1 用户态进程

fork() / vfork() / clone()
    └─> sys_fork / sys_clone
        └─> kernel_clone() / copy_process()
  • fork():复制父进程,地址空间独立,COW。
  • vfork():子进程暂时共享父进程地址空间,通常用于 exec 前。
  • clone():最灵活,可以通过 flags 控制共享程度。
    用户态进程通常有:
  • 独立 PID;
  • 独立地址空间;
  • 独立文件描述符表;
  • 独立信号处理;
  • 父子关系。

3.4.2 用户态线程

pthread_create()
    └─> glibc clone()
        └─> kernel_clone()

pthread_create() 底层会调用 clone(),并带上类似这些 flag:

CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD

同一进程里的线程:

  • 共享地址空间;
  • 共享文件描述符表;
  • 共享信号处理;
  • 共享 TGID;
  • 各自有独立 TID;
  • 各自有独立内核栈和寄存器上下文。
    也就是说:线程不是比进程更轻的“另一种对象”,而是共享更多资源的 task_struct。

3.4.3 内核线程

kthread_create() / kthread_run()
    └─> kthread()
        └─> kernel_thread()
            └─> _do_fork() / kernel_clone()

内核线程的特点:

  • 没有用户态地址空间;
  • 不执行用户程序;
  • 父进程通常是 kthreadd(PID 2);
  • 运行在内核态;
  • 常用于后台任务、调度器、内存回收、flush 线程等。

3.4.4 记忆

类型创建方式地址空间PID/TGID典型用途
进程fork() / clone()独立独立 PID运行用户程序
线程pthread_create() → clone()共享同 TGID,不同 TID多线程应用
内核线程kthread_run()无用户空间独立 task内核后台工作
  • 进程 = 独立资源的 task_struct
  • 线程 = 共享资源的 task_struct
  • 内核线程 = 没有用户空间的 task_struct
  • fork/clone/kthread 最后都走同一套 copy_process() / kernel_clone()

嵌入式 / BSP 视角
做驱动和底层开发时,通常会遇到两类选择:
用户态程序里想并发 → 用 pthread。
内核模块里想开后台任务 → 用 kthread_run。
如果是短任务、中断下半部 → 优先用 workqueue,不要随便开内核线程。

3.5 PID vs TGID vs TID

  • PID(Process ID):用户看到的进程号,实际是 TGID(线程组 ID)
  • TID(Thread ID):线程 ID,内核 task_struct->pid(每个 task_struct 唯一)
  • 单线程进程:PID == TID
  • 多线程进程:同组 task_struct 共享 TGID,各 TID 不同
ps -eLf   # 看 PID(=TGID)/ TID
# 多线程进程:N 行同 PID,不同 TID(LWP)

四、上下文切换(对照 RISC-V trap)

4.1 两种"切换"

切换是什么频率
用户态↔内核态syscall/中断,同进程内高(每次 syscall)
进程上下文切换从进程 A 切到进程 B中(调度时)

4.2 进程上下文切换做什么

context_switch()(kernel/sched/core.c):

static void context_switch(struct rq *rq, struct task_struct *prev,
                           struct task_struct *next) {
    struct mm_struct *mm = next->mm;
    struct mm_struct *oldmm = prev->active_mm;

    // 1. 切地址空间(若不同)
    if (unlikely(!mm)) {           // next 是内核线程
        next->active_mm = oldmm;   // 借 prev 的 mm
    } else if (prev->mm != mm) {   // 不同进程
        switch_mm(oldmm, mm, next);  // 切页表(写 satp + flush TLB)
    }

    // 2. 切寄存器(栈指针/ra/...)-> switch_to()
    switch_to(prev, next, prev);
}

4.3 switch_to:切寄存器(架构相关)

switch_to 最终调 __switch_to(arch/riscv/kernel/entry.S 或类似):

// 简化的 RISC-V 版
__switch_to:
    # 1. 保存 prev 的 callee-saved 寄存器到 prev 栈
    addi sp, sp, -FRAME
    sd ra,  0(sp)
    sd s0,  8(sp)
    sd s1,  16(sp)
    ... # s2-s11
    sd sp, TASK_THREAD_SP(a0)   # 存 prev->thread.sp

    # 2. 切栈:加载 next 的 sp
    ld sp, TASK_THREAD_SP(a1)   # next->thread.sp

    # 3. 切 TP(percpu 指针)
    ld tp, TASK_THREAD_TP(a1)

    # 4. 切 CSR(thread.fcsr 浮点 CSR 等)
    ...

    # 5. 恢复 next 的 callee-saved
    ld s1, 16(sp)
    ld s0, 8(sp)
    ld ra, 0(sp)
    addi sp, sp, FRAME

    ret   # 跳到 next 上次被切走的地方(ra)

4.4 对照你 RISC-V trap 切栈

你 mscratch 切栈(中断栈)Linux switch_to(进程切换)
触发中断/trap调度器决定
切什么sp(中断栈↔任务栈)sp + 页表 + percpu + CSR
保存啥全部通用寄存器(中断可能破坏)只存 callee-saved(s0-s11/ra)
返回mretret
频率每次中断每次调度

你的 mscratch 切栈是 Linux switch_to 的简化版
你 [[04-trap 机制详解]] 的 mscratch 切栈:trap 时 swap sp 和 mscratch。Linux switch_to 同构——切 prev/next 的 sp。增量是:① 还切页表(mm);② 只存 callee-saved(调度点已知 caller-saved 不重要);③ 还切 percpu(tp)。你已经懂"切栈",Linux 是放大版,加了切地址空间。

4.5 为什么只存 callee-saved

调度点是自愿的(调度器在明确位置调用 schedule),不是中断打断。所以调度点之后,编译器只保证 callee-saved 寄存器(s0-s11/ra/sp)跨函数调用保留,caller-saved(t0-t6/a0-a7)编译器已处理。只存 callee-saved 足够——比中断保存全部寄存器省。


五、CFS 调度器(Completely Fair Scheduler)⭐ 核心

5.1 CFS 的核心思想

不是按优先级抢,是按"谁跑得少"轮。

  • 每个进程有 vruntime(虚拟运行时间):表示它"已经跑了多少"
  • 调度器选 vruntime 最小的跑(跑得最少的优先)
  • 跑一会儿,vruntime 增加,变成不是最小,换别人跑
  • 结果:每个进程公平分享 CPU

5.2 vruntime 怎么算

vruntime += 实际运行时间 × (NICE_0_LOAD / 进程权重)
  • 权重(weight):由 nice 值算出,nice 0 权重 1024,nice -1 权重 1171(高 10%),nice +1 权重 920(低 10%)
  • 高权重进程(nice 低):vruntime 增长慢 → 更久保持"跑得少" → 更多 CPU
  • 低权重进程(nice 高):vruntime 增长快 → 很快被换下 → 少 CPU

5.3 nice 值与权重

nice权重CPU 占比(nice 0 为基准)
-2088761极高
-109548~10x
010241x(基准)
+10110~0.1x
+1915极低
  • nice 范围 -20 ~ +19,默认 0
  • nice 不是优先级,是权重——影响 vruntime 增长率,进而影响 CPU 份额
  • 普通用户只能调高 nice(降权),root 能调低(升权)

5.4 CFS 数据结构:红黑树

  • 每个 CPU 一个运行队列 struct rq
  • CFS 部分用红黑树(rbtree),按 vruntime 排序
  • 最左节点 = vruntime 最小 = 下一个跑
  • 进程入队:插入红黑树(O(log n))
  • 进程出队:取最左(O(log n))
        CPU0 rq.cfs
            │
         红黑树(按 vruntime)
            │
      [vruntime=100] ← 最左,下一个跑
       /          \
   [200]        [150]
    / \          /
 [180][210]  [130]

5.5 调度时序

1. tick 中断 → scheduler_tick()
2. 当前进程 vruntime 更新
3. 检查:当前 vruntime 是否 > 红黑树最左 + sched_latency?
   - 否:继续跑
   - 是:设 need_resched,稍后 schedule()
4. schedule():
   - 当前进程入队(回红黑树)
   - 取最左 = next
   - context_switch(prev, next)

5.6 sched_latency 与 min_granularity

  • sched_latency(目标延迟,~6ms):在这个窗口内,所有进程都应跑一遍
  • min_granularity(最小粒度,~0.75ms):每个进程至少跑这么久,避免频繁切换
  • 时间片 = sched_latency / 进程数,但不小于 min_granularity
4 个进程,sched_latency=6ms:
  每个时间片 = 6ms / 4 = 1.5ms(> min_granularity 0.75ms,OK)
  每 6ms 每进程跑 1.5ms,公平

5.7 对照 FreeRTOS 优先级调度

FreeRTOS 优先级抢占Linux CFS
选谁跑最高优先级vruntime 最小
同优先级轮转(time slicing)自然公平(vruntime 趋同)
高优先级抢占低优先级(可能饿死低)权重高 CPU 多,但不饿死
饥饿低优先级可能饿死不会(都跑)
实时性高(优先级绝对)低(公平,延迟不定)
适合实时控制交互/吞吐

CFS 不会饿死进程,但实时性弱
FreeRTOS 高优先级任务永远先跑,低优先级可能饿死——对实时控制是优点(关键任务必须先跑)。CFS 不让任何进程饿死——对交互/吞吐是优点(都响应),但实时性弱。Linux 实时需求用 SCHED_FIFO/RR(见 §六),或 RT-Linux 补丁。这是你 MCU 转 Linux 要适应的:Linux 默认不硬实时。


六、EEVDF(新调度器,Linux 6.6+)

6.1 为什么 CFS 升级到 EEVDF

CFS 用了十几年,问题:

  • 公平性在某些场景不完美(特别是延迟敏感任务)
  • 启发式多,难调

EEVDF(Earliest Eligible Virtual Deadline First)是 6.6 引入的新算法,理论更扎实。

6.2 EEVDF 核心思想

  • 每个进程有 lag(滞后量):应得 CPU - 实得 CPU
  • Eligible(合格):lag ≤ 0(没多占)的进程才能跑
  • 在合格进程里,选 虚拟截止时间(vdeadline) 最早的跑
  • 结果:比 CFS 更精确的公平 + 更好的延迟控制

6.3 对你影响

  • 接口不变(还是 nice/policy)
  • 行为更公平,延迟敏感任务响应更好
  • 你看 6.6+ 内核代码,kernel/sched/fair.c 是 EEVDF 实现了

嵌入式视角:EEVDF 是 CFS 的演进,不是革命
CFS → EEVDF ——核心思路(公平/vruntime)保留,机制优化(加 lag/vdeadline)。


七、调度策略(Scheduling Policy)

7.1 三类调度策略

策略调度类适合行为
SCHED_NORMAL(SCHED_OTHER)CFS/EEVDF普通进程公平调度,nice 加权
SCHED_BATCHCFS/EEVDF批处理CPU 密集,少交互,降优先级
SCHED_IDLECFS/EEVDF极低优先级只在空闲跑(nice 比所有都低)
SCHED_FIFORT实时优先级抢占,无时间片,跑到让/阻塞
SCHED_RRRT实时优先级抢占 + 同优先级轮转
SCHED_DEADLINEDL硬实时EDF(最早截止期优先),指定周期/运行时间/截止期

7.2 RT 优先级

  • RT 优先级范围 1-99(99 最高)
  • RT 优先级 > 普通进程(任何 RT 都抢普通)
  • SCHED_FIFO/RR 的优先级 = rt_priority
  • RT 进程能饿死普通进程(全占 CPU)→ 内核有 rt_throttling 默认让 RT 最多占 95%,留 5% 给普通

7.3 chrt 改策略

chrt -f 80 ./my_realtime_app   # SCHED_FIFO 优先级 80
chrt -r 50 ./my_rr_app          # SCHED_RR 优先级 50
chrt -o 0 ./my_normal_app       # SCHED_OTHER
chrt -p $(pidof myapp)          # 查看策略

7.4 SCHED_DEADLINE(硬实时)

struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime = 10000000,   // 每周期需 10ms
    .sched_period = 100000000,   // 周期 100ms
    .sched_deadline = 20000000,  // 截止期 20ms
};
sched_setattr(0, &attr, 0);
  • 内核保证每 period 内给 runtime,在 deadline 前完成
  • 用 EDF 算法调度
  • 比 FIFO/RR 更精确的实时保证

嵌入式视角:Linux 实时性
FreeRTOS 是硬实时(优先级抢占,us 级响应)。Linux 默认(SCHED_NORMAL)不是实时,ms 级延迟。要用 Linux 做实时:

  • SCHED_FIFO/RR:内核抢占,软实时,但可能被中断/驱动阻塞
  • SCHED_DEADLINE:更精确,但仍非硬实时
  • PREEMPT_RT 补丁(主线化中):全内核抢占,接近硬实时
  • Xenomai:双核(Linux + 实时核),硬实时
    边缘 AI/控制若要硬实时,选 PREEMPT_RT 或 Xenomai。

八、抢占(Preemption)

8.1 抢占点

Linux 内核可抢占(CONFIG_PREEMPT),在安全点检查 need_resched,调 schedule():

抢占点何时
中断返回内核态IRQ 返回,检查 need_resched
系统调用返回syscall 返回用户态前
显式 schedule()主动睡眠/阻塞
preempt_enable() 后临界区退出
互斥锁释放mutex_unlock 后

8.2 抢占等级(CONFIG)

配置抢占性用途
PREEMPT_NONE不抢占(除非阻塞)服务器(吞吐)
PREEMPT_VOLUNTARY自愿抢占(显式点)桌面(平衡)
PREEMPT全内核抢占低延迟(嵌入式/桌面)
PREEMPT_RT实时(几乎全可抢占)硬实时

8.3 关抢占 vs 关中断

preempt_disable();      // 关抢占(本 CPU 不切走)
// 临界区(不能睡!)
preempt_enable();       // 开抢占(检查 need_resched)

local_irq_save(flags);  // 关中断(本 CPU 中断不来)
// 临界区
local_irq_restore(flags);

spinlock_t lock;
spin_lock(&lock);       // 自旋锁隐含关抢占(SMP)+ 可能关中断
spin_unlock(&lock);
  • 关抢占:本核不切走,但中断还来(中断里可能 schedule)
  • 关中断:中断不来,绝对安静
  • 自旋锁:隐含关抢占(07 篇详讲)

关抢占/持锁时不能睡眠
FreeRTOS 临界区里能调阻塞 API(会切走)。Linux 关抢占/持自旋锁时绝对不能睡眠——睡眠会 schedule(),但你关了抢占,schedule 切不出去,死锁/panic。这是 Linux 比 FreeRTOS 严格的点。能睡的临界区用 mutex(07 篇详讲)。


九、fork / exec / wait(进程生命周期)

9.1 fork()

pid_t pid = fork();
if (pid == 0) {
    // 子进程
} else if (pid > 0) {
    // 父进程
}
  • fork() 创建子进程,复制父的 task_struct
  • 用 COW(Copy-On-Write):页表复制,物理页共享,写时才复制(惰性,01 篇哲学)
  • 子继承父的 mm/files/signals 副本

9.2 exec()

execl("/bin/ls", "ls", "-l", NULL);
  • 替换当前进程的代码/数据/堆/栈(丢弃旧 mm,建新 mm)
  • PID 不变,但变成新程序
  • file_operations 保持(打开的 fd 默认保留,除 FD_CLOEXEC 标记的)

9.3 wait()

pid_t pid = wait(&status);  // 等任一子进程
  • 父 wait 收尸子进程
  • 子 exit 后变 ZOMBIE,父 wait 才彻底释放
  • 父不 wait → 子变僵尸,占 PID;父死了 → init 收养收尸

9.4 孤儿进程与僵尸

  • 孤儿:父死了,子被 init(PID1)收养,init wait 收尸
  • 僵尸:子死了父没 wait,占 PID 和退出码,父 wait 或父死后 init 收尸

嵌入式视角:fork 比 FreeRTOS 创建任务重
FreeRTOS xTaskCreate 立即分配栈和 TCB。Linux fork 用 COW,看似复制 mm,实际只复制页表(快),物理页写时才复制。但 fork 仍比 xTaskCreate 重——要复制 files/signals/页表。所以 Linux 服务器用进程池/线程,不频繁 fork。你写服务别无脑 fork。


十、进程关系与会话

10.1 进程组 / 会话

  • 进程组:相关进程集合(如 pipeline),组 ID = PGID
  • 会话:进程组集合,会话首控制终端,会话 ID = SID
  • 用途:信号广播(kill -PGID)、作业控制

10.2 守护进程(daemon)

// daemon 化典型步骤
pid = fork();
if (pid > 0) exit(0);          // 父退出
setsid();                      // 子建新会话,脱离控制终端
fork();                        // 再 fork,防再获控制终端
if (pid > 0) exit(0);
chdir("/");                    // 改根
umask(0);
// 关标准 fd,重定向到 /dev/null
  • 守护进程:后台运行,无控制终端
  • BSP 启动后台服务常 daemon 化

十一、常用进程调试

11.1 工具

工具看什么
ps aux / ps -ef进程列表
top / htop实时进程状态/CPU
pstree进程树
cat /proc/<pid>/status进程详情(状态/内存/信号)
cat /proc/<pid>/sched调度信息(vruntime/policy)
cat /proc/<pid>/maps地址空间布局
cat /proc/<pid>/stack内核栈(WARNING)
strace -p <pid>跟踪系统调用
perf top热点函数
chrt / nice / renice改调度策略/优先级

11.2 关键 /proc 字段

# /proc/<pid>/status
State:  R (running)
Pid:    1234
PPid:   1
Uid:    0
voluntary_ctxt_switches:    1500   # 主动睡眠切换
nonvoluntary_ctxt_switches: 30     # 被抢占切换
  • voluntary 多 = 频繁等 IO/睡眠(正常)
  • nonvoluntary 多 = 频繁被抢占(可能 CPU 紧张)

嵌入式视角:
Linux 的 perf 统计任意函数/CPU 周期。/proc/<pid>/sched 看 vruntime 判断进程是否被公平调度。16 篇详讲 perf/ftrace。


十二、本篇小结

  • task_struct 是宇宙中心,比 FreeRTOS TCB 胖十倍(mm/files/signals/namespace)
  • 进程状态:RUNNING/INTERRUPTIBLE/UNINTERRUPTIBLE(D,杀不掉)/ZOMBIE 等
  • Linux 不区分进程线程(都 task_struct,clone flag 控制共享);内核线程无 mm
  • 上下文切换 context_switch:切页表(switch_mm)+ 切寄存器(switch_to,只存 callee-saved),对照你 mscratch 切栈是放大版
  • CFS:vruntime 最小优先跑,红黑树,权重由 nice 决定;不会饿死进程但实时性弱
  • EEVDF(6.6+):CFS 演进,加 lag/vdeadline,更公平
  • 调度策略:SCHED_NORMAL(CFS)/SCHED_FIFO/RR(RT)/SCHED_DEADLINE(硬实时)
  • 抢占:关抢占/持自旋锁时不能睡眠(FreeRTOS 临界区能睡,Linux 不能,严格)
  • fork 用 COW(惰性),exec 换程序,wait 收尸;孤儿归 init,僵尸等收尸
  • PREEMPT_RT 补丁让 Linux 接近硬实时

速查表

想干啥怎么做
看进程列表ps aux / top / htop
看进程详情cat /proc//status
看地址空间cat /proc//maps
看调度信息cat /proc//sched
改优先级nice -n 10 ./app / renice -n -5 -p PID
改调度策略chrt -f 80 ./app
创建内核线程kthread_run(fn, arg, “name”)
跟踪 syscallstrace -p PID
看进程树pstree -p
杀进程kill -9 PID(D 状态杀不掉)
看僵尸ps aux | grep Z
实时性SCHED_FIFO/RR/DEADLINE 或 PREEMPT_RT
关抢占preempt_disable(不能睡)
睡眠等待TASK_INTERRUPTIBLE(可被信号唤醒)

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。


Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐