嵌入式 Linux 进程
~/embedded-linux/processes

嵌入式 Linux
进程深度解析

$ 深入探讨进程创建、管理与 IPC 机制,配以完整 C 语言代码示例

Linux KernelC Languagefork()IPCEmbedded Systems

在嵌入式 Linux 系统中,进程是操作系统进行资源分配和调度的基本单位。 它是一个正在执行的程序的实例,拥有自己独立的虚拟地址空间、文件描述符、程序计数器和寄存器集。 与桌面或服务器环境相比,嵌入式系统通常资源受限(CPU、内存、存储),因此对进程的精细化管理显得尤为重要。

本文深入探讨嵌入式 Linux 环境下的进程核心概念,涵盖进程的创建与管理、多种进程间通信(IPC)机制, 并提供完整的 C 语言代码示例,帮助开发者更好地理解和应用这些技术。

01

进程的创建与执行

在 Linux 中,新的进程通常由一个已存在的进程(父进程)创建。 核心的系统调用是 fork()exec() 系列函数。

## fork():创建子进程

syscallfork#unistd.h
pid_t fork(void);
返回值父进程中返回子 PID;子进程中返回 0;出错返回 -1
说明通过复制当前进程创建子进程。子进程获得父进程数据段和堆栈的副本,与父进程共享代码段。

fork() 被调用一次,但返回两次。 父进程收到子进程的 PID,子进程收到 0,程序通过返回值区分执行路径。 Linux 使用写时复制(Copy-on-Write)技术优化内存, 只有在父子进程之一真正修改内存页时,才会进行实际复制。

## vfork():轻量级创建

syscallvfork#unistd.h
pid_t vfork(void);
返回值与 fork() 相同
说明不复制父进程地址空间,子进程与父进程共享内存。父进程被挂起,直到子进程调用 exec() 或 _exit()。
嵌入式场景注意
vfork() 在内存极为紧张的嵌入式设备上可显著减少内存占用。 但必须确保子进程在调用 exec()_exit() 之前, 不修改任何父进程数据,也不调用任何可能返回的函数。

## exec() 系列:执行新程序

exec() 系列函数用于在当前进程上下文中加载并执行新程序。 调用成功后,当前进程的地址空间被完全替换,但 PID 保持不变。

exec() 系列函数对比
函数名参数形式PATH 搜索环境变量
execl()可变参数列表继承父进程
execv()字符串数组继承父进程
execle()可变参数 + envp自定义
execve()字符串数组 + envp自定义
execlp()可变参数列表继承父进程
execvp()字符串数组继承父进程
推荐
嵌入式开发中,execvp()execlp() 因能自动搜索 PATH 而最为常用。

## 代码示例:fork() + execvp()

下面的示例展示了如何使用 fork() 创建子进程, 然后在子进程中使用 execvp() 执行 ls -l 命令。 父进程使用 waitpid() 等待子进程结束并获取退出状态。

process_creation.c
1""token-comment"">// process_creation.c
2""token-include"">#include <stdio.h>
3""token-include"">#include <stdlib.h>
4""token-include"">#include <unistd.h>
5""token-include"">#include <sys/types.h>
6""token-include"">#include <sys/wait.h>
7
8int main() {
9 pid_t pid = fork();
10
11 if (pid < 0) {
12 perror(""fork failed"");
13 exit(1);
14 } else if (pid == 0) {
15 ""token-comment"">// 子进程
16 printf(""Child process (PID: %d) is running.\n"", getpid());
17
18 char *args[] = {""ls"", ""-l"", ""/home/ubuntu"", NULL};
19 execvp(args[0], args); ""token-comment"">// 替换当前进程映像
20
21 perror(""execvp failed"");
22 exit(1);
23 } else {
24 ""token-comment"">// 父进程
25 printf(""Parent process (PID: %d) created child (PID: %d).\n"",
26 getpid(), pid);
27
28 int status;
29 waitpid(pid, &status, 0);
30
31 if (WIFEXITED(status)) {
32 printf(""Child exited with status %d.\n"", WEXITSTATUS(status));
33 }
34 printf(""Parent process is done.\n"");
35 }
36 return 0;
37}
02

进程的管理与终止

## wait() 和 waitpid()

当子进程结束时,它会变成僵尸进程(Zombie)—— 进程本身已终止,但内核进程表中的条目仍存在,直到父进程调用 wait()waitpid() 获取其退出状态。若父进程不处理,僵尸进程会持续占用资源。

wait() vs waitpid()
函数等待对象阻塞行为灵活性
wait()任意一个子进程始终阻塞
waitpid()指定 PID 或任意可通过 WNOHANG 非阻塞
嵌入式守护进程最佳实践
SIGCHLD 注册处理器,在处理器中循环调用 waitpid(-1, NULL, WNOHANG) 来非阻塞地回收所有已结束的子进程, 防止僵尸进程积累耗尽进程表空间。

## 进程终止

进程终止方式对比
方式函数/机制清理操作说明
正常退出exit()执行(刷新缓冲区等)标准库函数,推荐使用
立即退出_exit()不执行系统调用,信号处理器中使用
main 返回return 0执行等同于调用 exit()
异常终止信号(SIGSEGV 等)不执行由内核或其他进程触发
03

进程间通信(IPC)

由于进程拥有独立的地址空间,它们之间不能直接访问对方的内存。 操作系统提供了多种 IPC 机制来允许进程间交换数据和同步操作。

嵌入式 Linux 常用 IPC 机制对比
机制数据方向亲缘关系内核持久典型用途
匿名管道单向需要父子进程简单数据流
命名管道单向不需要否(文件)任意进程间数据流
信号单向不需要异步事件通知
消息队列双向不需要结构化消息传递
共享内存双向不需要大量数据高效共享
套接字双向不需要网络/本地通信

## 匿名管道(Anonymous Pipe)

管道是最简单的 IPC 形式,通过 pipe() 系统调用创建, 返回两个文件描述符:fd[0](读端)和 fd[1](写端)。 只能用于有亲缘关系的进程之间。

anonymous_pipe.c
1""token-comment"">// anonymous_pipe.c
2""token-include"">#include <stdio.h>
3""token-include"">#include <stdlib.h>
4""token-include"">#include <unistd.h>
5""token-include"">#include <string.h>
6""token-include"">#include <sys/wait.h>
7
8int main() {
9 int pipefd[2];
10 char buffer[100];
11 const char *message = ""Hello from parent!"";
12
13 if (pipe(pipefd) == -1) {
14 perror(""pipe failed"");
15 exit(1);
16 }
17
18 pid_t pid = fork();
19
20 if (pid == 0) {
21 close(pipefd[1]); ""token-comment"">// 关闭写端
22 read(pipefd[0], buffer, sizeof(buffer));
23 printf(""Child received: '%s'\n"", buffer);
24 close(pipefd[0]);
25 exit(0);
26 } else {
27 close(pipefd[0]); ""token-comment"">// 关闭读端
28 write(pipefd[1], message, strlen(message) + 1);
29 printf(""Parent sent: '%s'\n"", message);
30 close(pipefd[1]);
31 wait(NULL);
32 printf(""Parent is done.\n"");
33 }
34 return 0;
35}

## 命名管道(FIFO)

命名管道(FIFO)以文件形式存在于文件系统中,通过 mkfifo() 创建, 任何知道路径的进程均可访问,无需亲缘关系。先运行读进程,再运行写进程。

fifo_writer.c
1""token-comment"">// fifo_writer.c
2""token-include"">#include <stdio.h>
3""token-include"">#include <stdlib.h>
4""token-include"">#include <fcntl.h>
5""token-include"">#include <sys/stat.h>
6""token-include"">#include <string.h>
7
8""token-include"">#define FIFO_PATH ""/tmp/myfifo""
9
10int main() {
11 mkfifo(FIFO_PATH, 0666); ""token-comment"">// 创建命名管道
12
13 int fd = open(FIFO_PATH, O_WRONLY);
14 if (fd == -1) { perror(""open""); exit(1); }
15
16 const char *msg = ""Hello via FIFO!"";
17 write(fd, msg, strlen(msg) + 1);
18 close(fd);
19 printf(""Writer: Done.\n"");
20 return 0;
21}
fifo_reader.c
1""token-comment"">// fifo_reader.c
2""token-include"">#include <stdio.h>
3""token-include"">#include <stdlib.h>
4""token-include"">#include <fcntl.h>
5""token-include"">#include <sys/stat.h>
6
7""token-include"">#define FIFO_PATH ""/tmp/myfifo""
8
9int main() {
10 char buffer[100];
11
12 int fd = open(FIFO_PATH, O_RDONLY);
13 if (fd == -1) { perror(""open""); exit(1); }
14
15 read(fd, buffer, sizeof(buffer));
16 printf(""Reader received: '%s'\n"", buffer);
17 close(fd);
18 unlink(FIFO_PATH); ""token-comment"">// 删除 FIFO 文件
19 return 0;
20}

## 信号(Signal)

信号是一种异步通知机制,用于处理系统事件或进程间简单通信。 推荐使用 sigaction() 而非 signal(), 因为前者行为更可预测。

常用信号一览
信号名编号默认行为说明
SIGINT2终止键盘中断(Ctrl+C)
SIGTERM15终止软件终止请求
SIGKILL9强制终止不可捕获,不可忽略
SIGSEGV11终止+核心转储段错误(非法内存访问)
SIGCHLD17忽略子进程状态改变
SIGUSR110终止用户自定义信号 1
SIGUSR212终止用户自定义信号 2
信号处理器安全性
信号处理器中只能调用异步信号安全(async-signal-safe)函数, 如 write()_exit()printf()malloc() 等函数不安全, 在处理器中调用可能导致死锁或数据损坏。
signal_handler.c
1""token-comment"">// signal_handler.c
2""token-include"">#include <stdio.h>
3""token-include"">#include <stdlib.h>
4""token-include"">#include <unistd.h>
5""token-include"">#include <signal.h>
6
7void handle_sigint(int sig) {
8 ""token-comment"">// 注意:此处只能调用异步信号安全函数
9 const char msg[] = ""\nCaught SIGINT. Exiting gracefully.\n"";
10 write(STDOUT_FILENO, msg, sizeof(msg) - 1);
11 _exit(0);
12}
13
14int main() {
15 struct sigaction sa;
16 sa.sa_handler = handle_sigint;
17 sigemptyset(&sa.sa_mask);
18 sa.sa_flags = SA_RESTART;
19 sigaction(SIGINT, &sa, NULL); ""token-comment"">// 推荐使用 sigaction
20
21 printf(""PID: %d. Press Ctrl+C to send SIGINT.\n"", getpid());
22
23 while (1) {
24 sleep(1);
25 }
26 return 0;
27}

## 消息队列(Message Queue)

消息队列存储在内核中,克服了管道只能承载无格式字节流的缺点。 消息具有类型(msg_type),接收方可按类型选择性接收。 属于内核持久性资源,进程退出后不会自动释放。

message_queue.c
1""token-comment"">// message_queue.c
2""token-include"">#include <stdio.h>
3""token-include"">#include <stdlib.h>
4""token-include"">#include <string.h>
5""token-include"">#include <sys/ipc.h>
6""token-include"">#include <sys/msg.h>
7""token-include"">#include <sys/wait.h>
8
9""token-include"">#define MSG_KEY 1234
10
11struct msg_buffer {
12 long msg_type; ""token-comment"">// 必须是第一个成员,且为 long
13 char msg_text[100];
14};
15
16int main() {
17 int msgid = msgget(MSG_KEY, 0666 | IPC_CREAT);
18 if (msgid == -1) { perror(""msgget""); exit(1); }
19
20 struct msg_buffer message;
21 pid_t pid = fork();
22
23 if (pid == 0) {
24 ""token-comment"">// 子进程:接收类型为 1 的消息
25 printf(""Child: Waiting for message...\n"");
26 msgrcv(msgid, &message, sizeof(message.msg_text), 1, 0);
27 printf(""Child received: '%s'\n"", message.msg_text);
28 exit(0);
29 } else {
30 ""token-comment"">// 父进程:发送消息
31 message.msg_type = 1;
32 strcpy(message.msg_text, ""Hello from message queue!"");
33 msgsnd(msgid, &message, sizeof(message.msg_text), 0);
34 printf(""Parent sent message.\n"");
35
36 wait(NULL);
37 msgctl(msgid, IPC_RMID, NULL); ""token-comment"">// 释放内核资源
38 printf(""Parent: Done.\n"");
39 }
40 return 0;
41}

## 共享内存(Shared Memory)

共享内存是最高效的 IPC 机制,允许多个进程直接访问同一块物理内存, 避免内核与用户空间之间的数据拷贝。但必须配合信号量等同步机制防止数据竞争。

资源清理
System V 共享内存是内核持久性的,必须在程序退出前显式调用 shmctl(id, IPC_RMID, NULL) 释放,否则会造成内核资源泄漏。
shared_memory.c
1""token-comment"">// shared_memory.c
2""token-include"">#include <stdio.h>
3""token-include"">#include <stdlib.h>
4""token-include"">#include <string.h>
5""token-include"">#include <sys/ipc.h>
6""token-include"">#include <sys/shm.h>
7""token-include"">#include <sys/wait.h>
8
9""token-include"">#define SHM_KEY 5678
10""token-include"">#define SHM_SIZE 1024
11
12int main() {
13 int shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
14 if (shmid == -1) { perror(""shmget""); exit(1); }
15
16 pid_t pid = fork();
17 char *shm_ptr;
18
19 if (pid == 0) {
20 ""token-comment"">// 子进程:附加、读取、修改
21 shm_ptr = (char *)shmat(shmid, NULL, 0);
22 printf(""Child reads: '%s'\n"", shm_ptr);
23 strcpy(shm_ptr, ""Hello back from child!"");
24 shmdt(shm_ptr);
25 exit(0);
26 } else {
27 ""token-comment"">// 父进程:附加、写入、等待、读取
28 shm_ptr = (char *)shmat(shmid, NULL, 0);
29 strcpy(shm_ptr, ""Hello from parent via shared memory!"");
30 printf(""Parent wrote to shared memory.\n"");
31
32 wait(NULL);
33
34 printf(""Parent reads after child: '%s'\n"", shm_ptr);
35 shmdt(shm_ptr);
36 shmctl(shmid, IPC_RMID, NULL); ""token-comment"">// 删除共享内存段
37 printf(""Parent: Done.\n"");
38 }
39 return 0;
40}
04

嵌入式场景注意事项

僵尸进程防范
长期运行的守护进程若频繁创建子进程而不及时回收,会导致大量僵尸进程积累, 最终耗尽进程表空间。推荐为 SIGCHLD 注册处理器, 循环调用 waitpid(-1, NULL, WNOHANG) 非阻塞回收。
vfork() 的正确使用
在内存极为紧张的嵌入式设备上,vfork() 相比 fork() 可显著减少内存占用。但必须确保子进程在调用 exec()_exit() 之前,不修改任何父进程数据。
IPC 资源清理
System V 消息队列和共享内存是内核持久性的,进程退出后不会自动释放。 必须在程序退出前显式调用 msgctl(id, IPC_RMID, NULL)shmctl(id, IPC_RMID, NULL)
信号处理器异步安全性
信号处理器只能调用异步信号安全函数(POSIX 定义了约 180 个)。 推荐使用 sigaction() 替代 signal(), 并通过 volatile sig_atomic_t 类型的标志位与主循环通信。
05

结论

进程是嵌入式 Linux 系统中资源管理和并发执行的核心。 掌握 fork()exec()wait() 等进程管理工具,并根据应用场景选择合适的 IPC 机制, 是开发健壮、高效嵌入式应用的基础。

IPC 机制选型指南
场景推荐机制原因
父子进程间传递少量数据匿名管道简单易用,无需额外配置
无亲缘关系进程间简单通信命名管道(FIFO)通过文件系统路径访问
异步事件通知(设备状态变化)信号轻量级,适合简单控制信号
多进程间传递结构化数据消息队列支持消息类型,可选择性接收
多进程间共享大量数据(图像帧)共享内存零拷贝,性能最高,需配合信号量
跨主机或跨网络通信套接字通用性强,支持 TCP/IP 协议栈
$ echo "Made with Manus AI" | cowsay
Linux Kernel · POSIX · C99