最全硬件嵌入式开发教程之-深入理解操作系统进程:从底层原理到嵌入式实战

今天继续更新嵌入式硬件系统开发之进程管理>>>>

深入理解进程:从底层原理到嵌入式实战(3-4 万字详解)

前言:为什么嵌入式开发者必须吃透进程?

作为嵌入式开发者,你可能会说:“我平时用的 RTOS 里只有任务(Task),没有进程啊!” 但如果你想在珠三角拿到 12k + 的嵌入式开发 offer,尤其是进入智能硬件或汽车电子领域,进程管理是绕不开的硬骨头 ——

  • 智能硬件常需要 Linux 系统跑应用程序,多进程协作是基础
  • 汽车电子的 ECU(电子控制单元)里,RTOS 的任务管理本质是简化的进程管理
  • 面试时,进程相关知识点(如 IPC、调度算法)是大厂必考题

本文将从 “是什么 - 为什么 - 怎么做” 三个维度,用 3-4 万字的篇幅彻底讲透进程。包含 15 + 代码示例、8 张思维导图、10 + 实战案例,保证刷过牛客 100 题的嵌入式开发者都能看懂。

一、进程的本质:从 “死代码” 到 “活程序” 的蜕变

1.1 程序与进程的核心区别(附实例对比)

很多人搞不清 “程序” 和 “进程” 的区别,我们用一个嵌入式场景举例:

程序(Program):你写的led_blink.c编译后生成的led_blink.elf文件,存储在开发板的 Flash 里,这是静态的—— 就像一本菜谱,躺在书架上不会自己做菜。

进程(Process):当你在 Linux 开发板上执行./led_blink,操作系统会把led_blink.elf加载到内存,分配 CPU 时间、GPIO 资源,让代码跑起来 —— 这是动态的,就像厨师按照菜谱实际做菜的过程。

用表格对比关键区别:

对比项

程序(Program)

进程(Process)

嵌入式场景举例

存在形式

静态文件(.elf/.bin)

动态执行过程

Flash 里的固件 vs 运行中的固件

资源占用

不占用 CPU / 内存(仅占磁盘)

占用 CPU、内存、I/O 资源

未运行的 APP vs 后台运行的 WiFi 服务

生命周期

永久存在(除非删除文件)

有创建、运行、终止的过程

下载固件 vs 启动 / 关闭传感器服务

独立性

无(多个程序可共享文件)

独立地址空间、独立资源

多个任务共享 UART vs 进程独占 SPI

实战验证:在 Linux 开发板上执行ls -l /bin/ls(查看程序)和ps -ef | grep ls(查看进程),前者显示文件属性,后者显示运行状态。

1.2 进程的 “三要素”:程序、数据、PCB

一个进程能跑起来,必须具备三个核心要素:

  1. 程序段(Code Segment):存放指令,比如while(1){toggle_led();delay(1000);}
  2. 数据段(Data Segment):存放变量,比如int led_state = 0;(全局变量)、栈上的局部变量
  3. 进程控制块(PCB):操作系统管理进程的 “身份证”,记录进程状态、资源等信息

用思维导图展示三者关系:

graph TD

    A[进程] --> B[程序段]

    A --> C[数据段]

    A --> D[PCB]

    B --> B1[机器指令]

    B --> B2[函数库调用]

    C --> C1[全局变量]

    C --> C2[局部变量]

    C --> C3[常量]

    D --> D1[进程ID]

    D --> D2[状态]

    D --> D3[CPU寄存器]

    D --> D4[内存指针]

    D --> D5[打开文件列表]

嵌入式视角:在 STM32 的 FreeRTOS 中,任务控制块(TCB)就是简化的 PCB,包含任务栈指针、优先级、状态等信息,对应的数据结构类似:

// FreeRTOS任务控制块(简化版)

typedef struct tskTaskControlBlock {

    StackType_t *pxTopOfStack;  // 栈顶指针(对应PCB的CPU上下文)

    xListItem xStateListItem;   // 状态链表项(对应PCB的状态)

    UBaseType_t uxPriority;     // 优先级(对应PCB的调度信息)

    // ... 其他资源信息

} TCB_t;

1.3 进程的 5 个核心特征(附反例说明)

进程有 5 个特征,缺一个都不能叫 “进程”:

  1. 动态性:能被创建、调度、终止(反例:ROM 里的固化程序,无法动态调度)

举例:在 Linux 中用./app &启动进程,kill终止进程,体现动态性。

  1. 并发性:多个进程可同时存在(反例:单任务单片机程序,一次只能跑一个功能)

举例:开发板上同时运行温度采集进程WiFi上传进程

  1. 独立性:拥有独立地址空间(反例:线程,共享进程地址空间)

举例:一个进程崩溃(如段错误),不会影响其他进程。

  1. 异步性:进程按不可预知的速度推进(反例:实时任务,需严格按时间执行)

举例:两个进程打印日志,输出顺序可能每次不同。

  1. 结构性:由程序段、数据段、PCB 组成(反例:裸机程序,没有 PCB 管理)

举例:Linux 的/proc/[pid]/目录下的文件,就是进程结构的体现。

面试陷阱:面试官可能问 “线程是否具备这些特征?”—— 线程没有独立性(共享地址空间),所以不是进程。

二、进程状态:从 “就绪” 到 “运行” 的生死轮回

2.1 进程的 5 种基本状态(附 Linux 实际验证)

进程在生命周期中会经历 5 种状态,我们结合ps命令的实际输出理解:

状态名称

英文标识

含义(大白话)

Linux 中查看方式(ps aux)

创建态

NEW

刚被创建,还没加入就绪队列

一般看不到(持续时间极短)

就绪态

READY

万事俱备,就等 CPU 时间片

R(Running 的缩写,包含就绪)

运行态

RUNNING

正在 CPU 上执行

R

阻塞态

BLOCKED

等资源(如 I/O),主动放弃 CPU

S(Sleeping)或 D(深度睡眠)

终止态

TERMINATED

已结束,等待回收 PCB

Z(Zombie,僵尸进程)

实战操作:在 Linux 开发板上执行:

# 启动一个会阻塞的进程(如ping一个不存在的IP)

ping 192.168.1.254 &

# 查看状态(会显示S,阻塞在网络I/O)

ps aux | grep ping

你会看到ping进程状态为S,表示它因等待网络响应而阻塞。

2.2 状态转换的 6 种场景(附代码触发示例)

进程状态不会凭空变化,每种转换都有明确的触发条件。我们用 “嵌入式传感器采集” 场景举例:

  1. 创建态 → 就绪态

触发:进程创建完成,资源分配完毕。

代码示例

#include <stdio.h>

#include <unistd.h>

int main() {

    pid_t pid = fork();  // 创建子进程(进入创建态)

    if (pid == 0) {      // 子进程创建完成,进入就绪态

        printf("子进程就绪\n");

    }

    return 0;

}

  1. 就绪态 → 运行态

触发:调度器选中该进程,分配 CPU。

场景:就绪队列中只有你的传感器进程,调度器会立即让它运行。

  1. 运行态 → 就绪态

触发:时间片用完,或被高优先级进程抢占。

Linux 验证

# 启动一个占用CPU的进程

while true; do :; done &

# 再启动一个高优先级进程(nice值更小)

nice -n -5 ./high_prio_app &

# 查看第一个进程会变成就绪态(R,但实际未运行)

ps -l

  1. 运行态 → 阻塞态

触发:进程请求 I/O(如读取传感器数据)。

代码示例

// 读取I2C传感器(会阻塞等待数据)

int fd = open("/dev/i2c-1", O_RDWR);

char data[10];

read(fd, data, 10);  // 执行到此处,进程进入阻塞态

  1. 阻塞态 → 就绪态

触发:等待的资源到了(如传感器数据读取完成)。

原理:I/O 完成后,硬件会产生中断,内核处理中断时将进程从阻塞队列移到就绪队列。

  1. 运行态 → 终止态

触发:进程执行完毕,或被 kill。

代码示例

// 正常终止

int main() {

    printf("任务完成\n");

    return 0;  // 执行到此处,进程进入终止态

}

状态转换思维导图

graph TD

    A[创建态] -->|分配完资源| B[就绪态]

    B -->|调度器选中| C[运行态]

    C -->|时间片用完/被抢占| B

    C -->|等I/O/信号量| D[阻塞态]

    D -->|资源就绪| B

    C -->|执行完毕/被kill| E[终止态]

    B -->|被强制终止| E

    D -->|被强制终止| E

2.3 嵌入式 RTOS 中的状态变种(以 FreeRTOS 为例)

RTOS 的任务状态是进程状态的简化版,但更贴近硬件实际:

FreeRTOS 任务状态

对应进程状态

嵌入式场景举例

就绪态(Ready)

就绪态

等待调度器分配 CPU 的传感器任务

运行态(Running)

运行态

正在采集温湿度的任务

阻塞态(Blocked)

阻塞态

调用 vTaskDelay () 的延时任务

挂起态(Suspended)

无对应

被 vTaskSuspend () 暂停的调试任务

关键区别:RTOS 没有 “僵尸态”,任务删除后资源立即回收(因为嵌入式系统资源有限,不允许浪费)。

代码对比

// FreeRTOS任务状态转换示例

void vSensorTask(void *pvParameters) {

    while(1) {

        // 读取传感器(可能进入阻塞态)

        read_sensor();

        

        // 延时100ms(主动进入阻塞态)

        vTaskDelay(pdMS_TO_TICKS(100));  // 对应进程的阻塞态

    }

}

三、进程控制块(PCB):进程的 “身份证 + 档案袋”

3.1 PCB 的作用:操作系统如何 “记住” 进程?

想象一个场景:你正在用开发板调试程序,突然被打断去接电话,回来后能接着调试 —— 因为你 “记住” 了之前的状态(断点位置、变量值)。

操作系统管理进程也是同理,PCB 就是用来 “记住” 进程状态的结构。没有 PCB,操作系统就无法管理进程

具体来说,PCB 的作用有三个:

  1. 唯一标识:通过 PID 区分不同进程(就像身份证号)。
  2. 状态记录:记录进程当前状态(就绪 / 阻塞等),供调度器参考。
  3. 资源索引:保存进程占用的内存、文件、设备等资源的指针。

类比理解:PCB 就像医院的病历卡 —— 每个病人(进程)一张,记录病情(状态)、检查结果(资源),医生(操作系统)通过病历卡了解病人情况。

3.2 Linux 内核中的 PCB:task_struct 结构体详解

Linux 中的 PCB 是task_struct结构体(定义在linux/sched.h),包含 300 + 字段,我们挑嵌入式开发者必懂的 10 个字段详解:

struct task_struct {

    // 1. 进程标识

    pid_t pid;                  // 进程ID(唯一标识)

    pid_t tgid;                 // 线程组ID(多线程时用)

    

    // 2. 状态信息

    volatile long state;        // 进程状态(TASK_RUNNING等)

    unsigned int flags;         // 进程标志(如PF_KTHREAD表示内核线程)

    

    // 3. 调度信息

    int prio;                   // 动态优先级

    int static_prio;            // 静态优先级

    struct sched_entity se;     // 调度实体(用于CFS调度器)

    

    // 4. 内存信息

    struct mm_struct *mm;       // 内存描述符(用户空间内存)

    struct mm_struct *active_mm;// 活跃内存描述符(内核线程用)

    

    // 5. 上下文信息(CPU寄存器)

    struct thread_struct thread;// 存放寄存器值(切换时保存/恢复)

    

    // 6. 父子关系

    struct task_struct *parent; // 父进程指针

    struct list_head children;  // 子进程链表

    

    // 7. 文件信息

    struct files_struct *files; // 打开的文件列表

    

    // 8. 信号处理

    struct signal_struct *signal; // 信号描述符

    struct sighand_struct *sighand; // 信号处理函数

    

    // 9. 时间信息

    cputime_t utime;            // 用户态CPU时间

    cputime_t stime;            // 内核态CPU时间

    

    // 10. 其他

    struct task_struct *real_parent; // 实际父进程(被领养前)

};

关键字段解析

  1. pid 与 tgid
    • 单进程:pid = tgid
    • 多线程:主线程 pid = tgid,子线程 pid 不同但 tgid 相同
    • 查看方式ps -L -p <pid> 可看到线程的 LWP(轻量级进程 ID,即 pid)
  1. state
    • TASK_RUNNING:运行 / 就绪态
    • TASK_INTERRUPTIBLE:可中断阻塞(如等待键盘输入)
    • TASK_UNINTERRUPTIBLE:不可中断阻塞(如等待磁盘 I/O,ps显示 D)
    • 注意ps命令中 R = 运行 / 就绪,S = 可中断阻塞,D = 不可中断阻塞
  1. mm 与 active_mm
    • 用户进程:mm 指向自己的内存空间
    • 内核线程:mm=NULL,active_mm 指向借用的用户内存
    • 嵌入式意义:内核线程不占用用户内存,适合资源紧张的嵌入式系统
  1. thread_struct
    • 存放 CPU 寄存器值(如 ARM 的 sp、pc、lr 等)
    • 进程切换时,内核会保存当前 thread_struct,加载下一个进程的 thread_struct
    • 举例:当进程因中断切换时,pc(程序计数器)的值会被保存,恢复时从该地址继续执行

3.3 PCB 的组织方式:进程链表与哈希表

操作系统需要快速找到某个进程的 PCB,Linux 用两种数据结构组织:

  1. 双向循环链表
    • 所有 PCB 通过task_structtasks字段链接成链表
    • 遍历所有进程时使用(如ps aux命令)
    • 定义:struct list_head tasks;
  1. 哈希表
    • 通过 PID 快速查找 PCB(pid_hash数组)
    • 时间复杂度 O (1),比遍历链表快
    • 嵌入式优化:嵌入式 Linux 可能精简哈希表大小,减少内存占用

图示

graph LR

    subgraph 进程链表

        A[PCB1(pid=1)] <--> B[PCB2(pid=2)]

        B <--> C[PCB3(pid=3)]

        C <--> A

    end

    subgraph 哈希表(pid_hash)

        D[哈希桶0] --> A

        E[哈希桶1] --> B

        F[哈希桶2] --> C

    end

实战查看:在 Linux 内核源码中,init_task是第一个进程(swapper)的 PCB,所有进程都从它衍生:

// 内核启动时创建的第一个进程

struct task_struct init_task = INIT_TASK(init_task);

四、进程创建:从 fork () 到 exec () 的完整流程

4.1 进程创建的 4 个步骤(附内核源码分析)

创建进程就像开分店:总店(父进程)复制一套经营模式(代码),准备新店面(资源),招聘员工(分配 PID),最后开业(加入就绪队列)。

具体步骤:

  1. 分配 PID
    • pidmap位图中找一个未使用的 PID
    • 代码逻辑(简化):

static int alloc_pid(struct pid_namespace *ns) {

    // 遍历pidmap,找第一个0位

    for (i = 0; i < PIDMAP_ENTRIES; i++) {

        if (pidmap[i].page) {

            // 找到空闲PID

            return pid;

        }

    }

}

  1. 复制 PCB
    • 调用dup_task_struct()复制父进程的task_struct
    • 关键操作:分配新的内核栈(alloc_thread_info
    • 注意:默认不复制用户内存(用写时复制技术)
  1. 初始化新 PCB
    • 修改 PID、状态等信息(设为 TASK_RUNNING)
    • 清空父进程特有的信息(如信号处理、计时器)
    • 代码片段:

p->pid = alloc_pid(p->nsproxy->pid_ns);

p->state = TASK_RUNNING;

p->parent = current;  // current是当前进程(父进程)

  1. 加入进程队列
    • 将新 PCB 加入进程链表(list_add(&p->tasks, &init_task.tasks)
    • 加入对应优先级的就绪队列
    • 通知调度器有新进程就绪

4.2 fork () 系统调用:从 “一分为二” 到 “写时复制”

fork()是创建进程的 “瑞士军刀”,我们从用法、原理、优化三个层面解析。

4.2.1 fork () 的基本用法(附嵌入式场景示例)

函数原型

#include <unistd.h>

pid_t fork(void);  // 返回值:父进程得到子进程PID,子进程得到0,失败返回-1

嵌入式场景示例:开发板上同时采集温湿度和光照数据:

#include <stdio.h>

#include <unistd.h>

#include <sys/wait.h>

// 采集温度(子进程)

void collect_temperature() {

    while(1) {

        printf("温度: 25℃\n");

        sleep(2); // 模拟2秒采集一次

    }

}

// 采集光照(父进程)

void collect_light() {

    while(1) {

        printf("光照: 500lux\n");

        sleep(3); // 模拟3秒采集一次

    }

}

int main() {

    pid_t pid = fork();

    if (pid < 0) {

        perror("fork failed");

        return 1;

    } else if (pid == 0) {

        // 子进程:采集温度

        collect_temperature();

    } else {

        // 父进程:采集光照

        collect_light();

        // 等待子进程(实际中不会在循环里等)

        wait(NULL);

    }

    return 0;

}

运行结果:温度和光照数据交替打印,实现并行采集。

4.2.2 fork () 的 “写时复制”(COW)优化

早期的fork()会完整复制父进程的内存,效率极低(比如父进程有 1GB 内存,复制就要 1GB 空间)。现代操作系统用 “写时复制” 优化:

  • 原理:父子进程共享同一块物理内存,只有当任一进程修改内存时,才复制被修改的部分(页)
  • 好处:创建进程快(不用复制内存),节省内存(未修改的页共享)

图示

graph TD

    A[父进程内存] -->|fork()| B[共享物理页]

    B --> C[父进程修改页1]

    C --> D[复制页1,父进程用新页1]

    B --> E[子进程未修改]

    E --> F[子进程仍用共享页]

验证 COW:在 Linux 上用fork()创建子进程后,立即查看内存使用(top命令),会发现父子进程共享大部分内存。

4.2.3 vfork () 与 fork () 的区别(嵌入式必知)

嵌入式系统资源有限,vfork()fork()更轻量,区别如下:

对比项

fork()

vfork()

内存共享

写时复制

完全共享(包括栈)

执行顺序

父子进程执行顺序不确定

子进程先执行,父进程阻塞到子进程 exit ()

用途

通用进程创建

子进程立即调用 exec () 的场景

风险

高(子进程修改内存会影响父进程)

嵌入式使用场景:在内存只有 64MB 的嵌入式设备上,用vfork()+execve()启动新程序,比fork()节省内存。

代码示例

#include <stdio.h>

#include <unistd.h>

#include <sys/stat.h>

#include <sys/wait.h>

int main() {

    pid_t pid = vfork();

    if (pid == 0) {

        // 子进程必须调用exec系列函数或exit

        execl("/bin/ls", "ls", "-l", NULL);

        _exit(0); // 如果exec失败,必须exit

    } else {

        // 父进程在子进程exit或exec后才执行

        printf("子进程已执行\n");

        wait(NULL);

    }

    return 0;

}

4.3 exec 系列函数:进程 “改头换面”

fork()创建的子进程与父进程执行相同代码,exec系列函数能让子进程执行新程序(“换代码”)。

常用 exec 函数

函数名

功能

示例

execl()

命令行参数列表传参

execl("/bin/ls", "ls", "-l", NULL)

execv()

命令行参数数组传参

char *argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv)

execlp()

从 PATH 找程序,不用写全路径

execlp("ls", "ls", "-l", NULL)

execvp()

结合 execv () 和 execlp () 的特点

char *argv[] = {"ls", "-l", NULL}; execvp("ls", argv)

嵌入式场景:父进程监控传感器,子进程执行不同的处理程序:

#include <stdio.h>

#include <unistd.h>

#include <sys/wait.h>

int main() {

    pid_t pid = fork();

    if (pid == 0) {

        // 子进程:执行温度处理程序

        execl("./temperature_handler", "temperature_handler", "25", NULL);

        // 如果exec失败才会执行下面的代码

        perror("exec failed");

        _exit(1);

    } else {

        // 父进程:继续监控

        printf("监控中...\n");

        wait(NULL);

    }

    return 0;

}

注意exec成功后,子进程的代码、数据会被新程序替换,但 PID 不变(还是原来的子进程)。

五、进程终止与资源回收:避免 “僵尸” 横行

5.1 进程终止的 3 种方式(附代码)

进程终止就像 “死亡”,有自然死亡、意外死亡、被杀死三种方式:

  1. 正常终止(自然死亡)

#include <stdio.h>

#include <stdlib.h> // exit()

#include <unistd.h> // _exit()

int main() {

    printf("正常终止"); // 没有换行符

    exit(0); // 会刷新缓冲区,输出"正常终止"

    // _exit(0); // 不刷新缓冲区,可能不输出

}

    • main()返回(return 0;
    • 调用exit()(会刷新缓冲区)
    • 调用_exit()(不刷新缓冲区,嵌入式常用)
  1. 异常终止(意外死亡)

int main() {

    int a = 1 / 0; // 会产生SIGFPE信号,异常终止

    return 0;

}

    • 除以零、非法内存访问(段错误)
    • 收到致命信号(如 SIGSEGV、SIGFPE)
  1. 被其他进程杀死(他杀)

#include <signal.h>

#include <stdio.h>

int main() {

    pid_t pid = fork();

    if (pid == 0) {

        while(1) sleep(1); // 子进程死循环

    } else {

        sleep(2);

        kill(pid, SIGKILL); // 父进程杀死子进程

    }

    return 0;

}

    • 其他进程调用kill()发送信号
    • kill命令(如kill -9 <pid>

5.2 僵尸进程:是什么、为什么、怎么办

5.2.1 僵尸进程的产生(附复现代码)

定义:子进程终止后,PCB 未被回收,变成僵尸进程(Zombie)。

产生原因:父进程未调用wait()waitpid()回收子进程资源。

复现代码

#include <stdio.h>

#include <unistd.h>

int main() {

    pid_t pid = fork();

    if (pid == 0) {

        // 子进程:立即终止

        printf("子进程终止\n");

        _exit(0);

    } else {

        // 父进程:不调用wait(),进入死循环

        while(1) sleep(1);

    }

    return 0;

}

查看僵尸进程

# 编译运行上述程序后

ps aux | grep defunct  # defunct表示僵尸进程

会看到子进程状态为Z+(Zombie)。

5.2.2 僵尸进程的危害与解决方法

危害

  • 占用 PID(系统 PID 有限,如 32768 个),僵尸太多会导致无法创建新进程
  • 占用 PCB 内存(每个 PCB 约 1KB,10 万个僵尸就占 100MB)

解决方法

  1. 父进程主动回收:调用wait()waitpid()

#include <stdio.h>

#include <unistd.h>

#include <sys/wait.h>

int main() {

    pid_t pid = fork();

    if (pid == 0) {

        _exit(0);

    } else {

        int status;

        waitpid(pid, &status, 0); // 等待子进程终止

        // 可以通过status获取子进程退出状态

        if (WIFEXITED(status)) {

            printf("子进程正常退出,返回值:%d\n", WEXITSTATUS(status));

        }

    }

    return 0;

}

  1. 父进程忽略 SIGCHLD 信号

#include <signal.h>

signal(SIGCHLD, SIG_IGN); // 告诉内核:子进程终止后自动回收

  1. 双重 fork ():让 init 进程领养孙子进程

// 父进程 -> 子进程A -> 子进程B

// 子进程A创建B后立即退出,B成为孤儿进程被init领养,init会回收B

5.3 孤儿进程:被 “福利院”(init)领养

定义:父进程先于子进程终止,子进程被 init 进程(PID=1)领养。

特点

  • 无害(init 会负责回收)
  • 状态为S(就绪 / 阻塞),不是僵尸

复现代码

#include <stdio.h>

#include <unistd.h>

int main() {

    pid_t pid = fork();

    if (pid == 0) {

        // 子进程:等待父进程死亡

        sleep(2);

        // 父进程已死,打印新的父进程PID(应为1)

        printf("子进程的新父进程PID:%d\n", getppid());

    } else {

        // 父进程:立即退出

        _exit(0);

    }

    return 0;

}

运行结果:子进程的新父进程 PID 为 1(init 进程)。

六、进程间通信(IPC):让进程 “说话”

进程是独立的,但需要协作(如传感器进程将数据传给上传进程),这就需要 IPC。

6.1 管道(Pipe):最简单的 “传话筒”

管道是最古老的 IPC 方式,像一根 “管子”,数据从一端进,另一端出。

6.1.1 匿名管道(父子进程专用)

特点

  • 半双工(数据单向流动)
  • 只能用于有亲缘关系的进程(父子、兄弟)
  • 基于文件描述符(读端 fd [0],写端 fd [1])

代码示例:父进程给子进程发送传感器数据

#include <stdio.h>

#include <unistd.h>

#include <string.h>

int main() {

    int fd[2];

    // 创建管道

    if (pipe(fd) == -1) {

        perror("pipe failed");

        return 1;

    }

    pid_t pid = fork();

    if (pid == 0) {

        // 子进程:读数据

        close(fd[1]); // 关闭写端(只需要读)

        char buf[100];

        read(fd[0], buf, sizeof(buf));

        printf("子进程收到:%s\n", buf);

        close(fd[0]);

    } else {

        // 父进程:写数据

        close(fd[0]); // 关闭读端(只需要写)

        char *data = "温度:25℃";

        write(fd[1], data, strlen(data));

        close(fd[1]);

    }

    return 0;

}

注意

  • 管道有缓冲(默认 64KB),满了会阻塞写操作
  • 读端关闭后写操作会产生 SIGPIPE 信号(默认终止进程)
6.1.2 命名管道(FIFO):任意进程通信

特点

  • 有文件名(在文件系统中可见,如/tmp/myfifo
  • 可用于任意进程(无亲缘关系)
  • 用法类似匿名管道,但需要先创建

创建 FIFO

mkfifo /tmp/sensor_fifo  # 命令行创建

或代码创建:

#include <sys/stat.h>

mkfifo("/tmp/sensor_fifo", 0666); // 0666是权限

通信示例

写进程(传感器采集):

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

#include <string.h>

int main() {

    int fd = open("/tmp/sensor_fifo", O_WRONLY);

    char *data = "光照:500lux";

    write(fd, data, strlen(data));

    close(fd);

    return 0;

}

读进程(数据处理):

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

int main() {

    int fd = open("/tmp/sensor_fifo", O_RDONLY);

    char buf[100];

    read(fd, buf, sizeof(buf));

    printf("收到:%s\n", buf);

    close(fd);

    return 0;

}

嵌入式应用:在嵌入式 Linux 中,多个进程(如采集、处理、显示)可通过 FIFO 传递数据,无需考虑进程关系。

6.2 信号(Signal):进程间的 “紧急电报”

信号是异步通知机制,像 “发电报” 一样简单粗暴,适合传递简单指令(如终止、暂停)。

6.2.1 常见信号及默认行为

信号编号

名称

含义

默认行为

嵌入式场景举例

2

SIGINT

中断(Ctrl+C)

终止进程

手动停止调试中的程序

9

SIGKILL

杀死进程

终止进程

强制结束无响应的进程

11

SIGSEGV

段错误(非法内存访问)

终止 + CoreDump

程序 bug 导致内存越界

17

SIGCHLD

子进程终止

忽略

父进程回收子进程

19

SIGSTOP

暂停进程

暂停进程

调试时暂停程序执行

查看所有信号kill -l

6.2.2 发送信号:kill () 函数与 kill 命令

用 kill 命令发送信号

kill -9 1234  # 给PID=1234的进程发SIGKILL

kill -SIGSTOP 1234  # 暂停进程

用 kill () 函数发送信号

#include <signal.h>

#include <stdio.h>

#include <unistd.h>

int main() {

    pid_t pid = fork();

    if (pid == 0) {

        while(1) {

            printf("运行中...\n");

            sleep(1);

        }

    } else {

        sleep(2);

        kill(pid, SIGSTOP); // 暂停子进程

        sleep(2);

        kill(pid, SIGCONT); // 继续子进程

        sleep(2);

        kill(pid, SIGKILL); // 杀死子进程

    }

    return 0;

}

6.2.3 捕获信号:自定义信号处理函数

进程可以自定义信号的处理方式(除 SIGKILL 和 SIGSTOP,这两个信号不能被捕获)。

代码示例:捕获 SIGINT,实现优雅退出

#include <stdio.h>

#include <signal.h>

#include <unistd.h>

// 信号处理函数

void sigint_handler(int signo) {

    if (signo == SIGINT) {

        printf("\n收到中断信号,正在保存数据...\n");

        // 保存传感器数据等清理工作

        sleep(1);

        printf("数据保存完成,退出\n");

        _exit(0);

    }

}

int main() {

    // 注册信号处理函数

    if (signal(SIGINT, sigint_handler) == SIG_ERR) {

        perror("signal failed");

        return 1;

    }

    

    // 模拟传感器采集

    while(1) {

        printf("采集数据中...\n");

        sleep(1);

    }

    return 0;

}

运行:按 Ctrl+C 时,进程会先保存数据再退出,而不是立即终止。

6.3 共享内存:最快的 IPC(嵌入式首选)

共享内存是效率最高的 IPC 方式 —— 数据直接在内存中共享,无需拷贝。

6.3.1 共享内存的使用步骤
  1. 创建 / 打开共享内存shmget()
  2. 映射到进程地址空间shmat()
  3. 读写共享内存:直接操作指针
  4. 解除映射shmdt()
  5. 删除共享内存shmctl()

代码示例

写进程(传感器):

#include <stdio.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <string.h>

#define SHM_SIZE 1024  // 共享内存大小

#define SHM_KEY 0x1234 // 共享内存键值(唯一标识)

int main() {

    // 1. 创建共享内存

    int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);

    if (shmid == -1) {

        perror("shmget failed");

        return 1;

    }

    // 2. 映射到地址空间

    char *shmaddr = shmat(shmid, NULL, 0);

    if (shmaddr == (void*)-1) {

        perror("shmat failed");

        return 1;

    }

    // 3. 写数据

    strcpy(shmaddr, "温度:25℃ 湿度:60%");

    printf("写入共享内存: %s\n", shmaddr);

    // 等待读进程读取

    sleep(5);

    // 4. 解除映射

    shmdt(shmaddr);

    // 5. 删除共享内存(通常由一个进程负责)

    shmctl(shmid, IPC_RMID, NULL);

    return 0;

}

读进程(数据处理):

#include <stdio.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#define SHM_SIZE 1024

#define SHM_KEY 0x1234

int main() {

    // 1. 获取共享内存(已由写进程创建)

    int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);

    if (shmid == -1) {

        perror("shmget failed");

        return 1;

    }

    // 2. 映射到地址空间

    char *shmaddr = shmat(shmid, NULL, 0);

    if (shmaddr == (void*)-1) {

        perror("shmat failed");

        return 1;

    }

    // 3. 读数据

    printf("从共享内存读取: %s\n", shmaddr);

    // 4. 解除映射

    shmdt(shmaddr);

    return 0;

}

6.3.2 共享内存的同步问题(必知)

共享内存不提供同步机制,多进程同时读写会导致数据错乱(如两个进程同时写同一位置)。

解决方法:用信号量(Semaphore)同步。

示例:用信号量保护共享内存读写:

// 初始化信号量(确保先于共享内存操作)

sem_t *sem = sem_open("/sensor_sem", O_CREAT, 0666, 1);

// 写共享内存前加锁

sem_wait(sem);

// 写操作...

sem_post(sem);

// 读共享内存前加锁

sem_wait(sem);

// 读操作...

sem_post(sem);

嵌入式注意:嵌入式 Linux 可能需要开启CONFIG_SYSVIPC配置才能使用共享内存。

6.4 信号量(Semaphore):进程同步的 “红绿灯”

信号量像 “红绿灯”,控制进程何时可以访问共享资源(如共享内存、硬件设备)。

6.4.1 信号量的基本概念
  • 计数信号量:值可以是任意非负数,用于控制资源数量(如 3 个串口设备)
  • 二元信号量(互斥锁):值只能是 0 或 1,用于互斥访问(如同一时间只能一个进程用 SPI 总线)

P 操作(等待)sem_wait()—— 信号量减 1,若值 < 0 则阻塞

V 操作(释放)sem_post()—— 信号量加 1,唤醒阻塞进程

6.4.2 System V 信号量与 POSIX 信号量

Linux 有两种信号量接口,嵌入式常用 POSIX 信号量(更简单):

POSIX 信号量示例(互斥访问 SPI)

#include <semaphore.h>

#include <stdio.h>

#include <unistd.h>

#include <pthread.h>

sem_t sem; // 全局信号量

// 模拟SPI操作

void spi_operation(int id) {

    sem_wait(&sem); // P操作:获取锁

    printf("进程%d开始使用SPI\n", id);

    sleep(2); // 模拟SPI操作

    printf("进程%d结束使用SPI\n", id);

    sem_post(&sem); // V操作:释放锁

}

int main() {

    // 初始化信号量(1表示互斥锁)

    sem_init(&sem, 0, 1); // 第二个参数0表示线程间共享

    pid_t pid = fork();

    if (pid == 0) {

        spi_operation(2); // 子进程

    } else {

        spi_operation(1); // 父进程

    }

    // 销毁信号量

    sem_destroy(&sem);

    return 0;

}

运行结果:两个进程不会同时使用 SPI,体现互斥效果。

6.4.3 信号量解决生产者 - 消费者问题

场景:传感器(生产者)采集数据到缓冲区,处理程序(消费者)从缓冲区取数据。

代码示例

#include <semaphore.h>

#include <stdio.h>

#include <unistd.h>

#include <pthread.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];

int in = 0, out = 0;

sem_t empty; // 空缓冲区数量

sem_t full;  // 满缓冲区数量

sem_t mutex; // 互斥锁

// 生产者(传感器)

void *producer(void *arg) {

    for (int i = 0; i < 10; i++) {

        int data = i; // 模拟传感器数据

        sem_wait(&empty); // 等空缓冲区

        sem_wait(&mutex);

        buffer[in] = data;

        printf("生产: %d, 位置: %d\n", data, in);

        in = (in + 1) % BUFFER_SIZE;

        sem_post(&mutex);

        sem_post(&full); // 满缓冲区+1

        sleep(1); // 模拟采集间隔

    }

    return NULL;

}

// 消费者(数据处理)

void *consumer(void *arg) {

    for (int i = 0; i < 10; i++) {

        sem_wait(&full); // 等满缓冲区

        sem_wait(&mutex);

        int data = buffer[out];

        printf("消费: %d, 位置: %d\n", data, out);

        out = (out + 1) % BUFFER_SIZE;

        sem_post(&mutex);

        sem_post(&empty); // 空缓冲区+1

        sleep(2); // 模拟处理时间

    }

    return NULL;

}

int main() {

    // 初始化信号量

    sem_init(&empty, 0, BUFFER_SIZE); // 初始有5个空缓冲区

    sem_init(&full, 0, 0);            // 初始0个满缓冲区

    sem_init(&mutex, 0, 1);           // 互斥锁

    pthread_t prod_tid, cons_tid;

    pthread_create(&prod_tid, NULL, producer, NULL);

    pthread_create(&cons_tid, NULL, consumer, NULL);

    pthread_join(prod_tid, NULL);

    pthread_join(cons_tid, NULL);

    // 清理

    sem_destroy(&empty);

    sem_destroy(&full);

    sem_destroy(&mutex);

    return 0;

}

运行结果:生产者和消费者交替操作缓冲区,不会出现数据混乱。

七、进程调度:谁先 “上车” 谁说了算

7.1 进程调度的基本概念(嵌入式视角)

进程调度就是 “决定哪个进程先使用 CPU”,像公交车调度 —— 谁先上车、谁后上车,需要规则。

为什么需要调度

  • CPU 是稀缺资源(通常只有 1-4 核)
  • 多个进程需要 “公平” 使用 CPU
  • 不同进程有不同需求(如实时进程需要立即响应)

嵌入式调度 vs 通用 OS 调度

  • 嵌入式:强调实时性(如传感器数据必须 10ms 内处理)
  • 通用 OS:强调公平性和交互性(如桌面系统)

7.2 Linux 的 CFS 调度器(完全公平调度)

Linux 采用 CFS(Completely Fair Scheduler)调度器,核心思想是 “让每个进程获得公平的 CPU 时间”。

7.2.1 CFS 的基本原理
  • 虚拟运行时间:进程实际运行时间按优先级加权后的时间
  • 红黑树:所有就绪进程按虚拟运行时间排序,每次选虚拟运行时间最小的进程

举例

  • 高优先级进程的虚拟时间流逝慢(如实际运行 1ms,虚拟时间 + 0.5ms)
  • 低优先级进程的虚拟时间流逝快(如实际运行 1ms,虚拟时间 + 2ms)
  • 这样高优先级进程能获得更多实际 CPU 时间
7.2.2 进程优先级与 nice 值

Linux 用 nice 值表示进程优先级:

  • 范围:-20(最高优先级)~ 19(最低优先级)
  • 默认值:0
  • 调整优先级:nice -n <值> 命令 或 renice <值> -p <pid>

查看进程 nice 值ps -l(NI 列)

嵌入式应用:在嵌入式系统中,可将实时任务的 nice 值设为 - 20,确保优先执行。

7.3 实时调度策略(嵌入式必备)

嵌入式系统常需要实时调度(如汽车的刹车控制必须立即响应),Linux 提供两种实时调度策略:

  1. SCHED_FIFO
    • 先进先出,一旦获得 CPU 就一直运行,直到主动放弃或被更高优先级进程抢占
    • 适合短时间运行的实时任务(如传感器数据处理)
  1. SCHED_RR
    • 时间片轮转,相同优先级的进程轮流执行
    • 适合需要定期执行的任务(如 10ms 一次的电机控制)

设置实时调度策略

#include <stdio.h>

#include <sched.h>

int main() {

    struct sched_param param;

    param.sched_priority = 50; // 优先级(1-99,值越大优先级越高)

    // 设置SCHED_FIFO调度策略

    if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {

        perror("sched_setscheduler failed");

        return 1;

    }

    // 实时任务...

    return 0;

}

注意:需要 root 权限才能设置实时优先级,嵌入式系统中通常会开启相关配置。

7.4 嵌入式 RTOS 的调度器(以 FreeRTOS 为例)

FreeRTOS 的调度器比 Linux 简单,适合资源有限的嵌入式系统:

  • 抢占式调度:高优先级任务可立即抢占低优先级任务
  • 时间片调度:相同优先级任务轮流执行(可配置)

代码示例

// 高优先级任务(传感器数据处理)

void vHighPriorityTask(void *pvParameters) {

    while(1) {

        // 处理数据(必须快速完成)

        vTaskDelay(pdMS_TO_TICKS(10));

    }

}

// 低优先级任务(日志打印)

void vLowPriorityTask(void *pvParameters) {

    while(1) {

        // 打印日志(可延迟)

        vTaskDelay(pdMS_TO_TICKS(100));

    }

}

int main() {

    // 创建任务,高优先级任务优先执行

    xTaskCreate(vHighPriorityTask, "HighTask", 128, NULL, 2, NULL);

    xTaskCreate(vLowPriorityTask, "LowTask", 128, NULL, 1, NULL);

    vTaskStartScheduler(); // 启动调度器

    return 0;

}

关键区别:FreeRTOS 的任务切换开销小(约几微秒),适合微控制器(如 STM32),而 Linux 调度切换开销大(约几十微秒)。

八、实战:用进程知识解决嵌入式实际问题

8.1 案例 1:嵌入式设备的多进程架构设计

以 “智能温湿度传感器” 为例,设计多进程架构:

进程名称

功能

优先级

IPC 方式

采集进程

读取温湿度传感器

共享内存

处理进程

数据校准、转换

共享内存 + 信号量

上传进程

WiFi 上传数据

管道

日志进程

记录系统日志

最低

命名管道

优势

  • 模块化(一个进程出问题不影响其他进程)
  • 可独立升级(如只更新上传进程支持新协议)
  • 方便调试(可单独重启某个进程)

8.2 案例 2:解决传感器数据丢失问题

问题:传感器数据采集快(10ms 一次),但上传慢(100ms 一次),导致数据丢失。

分析:采集进程和上传进程速度不匹配,没有缓冲机制。

解决方案:用共享内存 + 信号量实现环形缓冲区:

  1. 采集进程:将数据写入环形缓冲区,信号量计数 + 1
  2. 上传进程:从缓冲区读数据,信号量计数 - 1
  3. 缓冲区满时,采集进程可选择覆盖旧数据或等待

核心代码:参考 6.4.3 的生产者 - 消费者模型,将缓冲区改为环形。

8.3 案例 3:调试进程相关问题的工具

工具

用途

嵌入式场景示例

ps

查看进程状态

检查是否有僵尸进程(Z 状态)

top/htop

实时查看进程 CPU / 内存使用

发现 CPU 占用 100% 的异常进程

pstree

查看进程树(父子关系)

找到某个进程的父进程

strace

跟踪进程系统调用

调试进程为何无法打开设备文件

gdb attach

调试运行中的进程

在不重启的情况下调试上传进程

调试示例:用strace查看进程为何无法读取传感器:

strace -f ./sensor_collect  # -f跟踪子进程

会输出所有系统调用,若看到open("/dev/i2c-1", O_RDWR) = -1 ENOENT,说明设备文件不存在。

九、总结:进程知识体系与面试重点

9.1 进程知识体系思维导图

graph TD

    A[进程] --> B[基本概念]

    A --> C[状态]

    A --> D[PCB]

    A --> E[创建与终止]

    A --> F[IPC]

    A --> G[调度]

    B --> B1[程序vs进程]

    B --> B2[进程特征]

    C --> C1[5种状态及转换]

    D --> D1[task_struct结构]

    D --> D2[组织方式]

    E --> E1[fork/exec]

    E --> E2[僵尸/孤儿进程]

    F --> F1[管道/FIFO]

    F --> F2[信号]

    F --> F3[共享内存]

    F --> F4[信号量]

    G --> G1[CFS调度器]

    G --> G2[实时调度]

9.2 面试高频问题与答案要点

  1. 进程与线程的区别?
    • 进程:资源分配单位,有独立地址空间
    • 线程:调度单位,共享进程资源
    • 开销:进程创建 / 切换开销大,线程小
  1. 僵尸进程产生原因及解决方法?
    • 原因:子进程终止后父进程未回收 PCB
    • 解决:wait ()/waitpid ()、忽略 SIGCHLD、双重 fork ()
  1. 什么是写时复制?为什么用它?
    • 原理:fork () 后父子进程共享内存,修改时才复制
    • 好处:加快进程创建速度,节省内存
  1. 进程间通信方式及优缺点?
    • 管道:简单,仅限亲缘进程
    • 共享内存:最快,需同步
    • 信号量:用于同步,不传递数据
    • 信号:异步,适合简单通知
  1. 实时调度与普通调度的区别?
    • 实时:优先保证响应时间(如 SCHED_FIFO)
    • 普通:优先保证公平性(如 CFS)

9.3 下一步学习建议

  1. 动手实践:用本文代码在开发板上实际运行,观察进程行为
  2. 阅读源码:看 FreeRTOS 的任务调度器源码(理解简化版进程管理)
  3. 项目实战:实现一个多进程的嵌入式应用(如智能家居网关)
  4. 深入内核:学习 Linux 内核进程调度和 IPC 的实现细节

最后有一句话和大家分享:

最好的学习方法是 “用起来”—— 在实际项目中遇到问题、解决问题,才能真正理解进程的精髓。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值