嵌入式 Linux
进程深度解析
$ 深入探讨进程创建、管理与 IPC 机制,配以完整 C 语言代码示例▋
在嵌入式 Linux 系统中,进程是操作系统进行资源分配和调度的基本单位。 它是一个正在执行的程序的实例,拥有自己独立的虚拟地址空间、文件描述符、程序计数器和寄存器集。 与桌面或服务器环境相比,嵌入式系统通常资源受限(CPU、内存、存储),因此对进程的精细化管理显得尤为重要。
本文深入探讨嵌入式 Linux 环境下的进程核心概念,涵盖进程的创建与管理、多种进程间通信(IPC)机制, 并提供完整的 C 语言代码示例,帮助开发者更好地理解和应用这些技术。
进程的创建与执行
在 Linux 中,新的进程通常由一个已存在的进程(父进程)创建。 核心的系统调用是 fork() 和 exec() 系列函数。
## fork():创建子进程
pid_t fork(void);fork() 被调用一次,但返回两次。 父进程收到子进程的 PID,子进程收到 0,程序通过返回值区分执行路径。 Linux 使用写时复制(Copy-on-Write)技术优化内存, 只有在父子进程之一真正修改内存页时,才会进行实际复制。
## vfork():轻量级创建
pid_t vfork(void);vfork() 在内存极为紧张的嵌入式设备上可显著减少内存占用。 但必须确保子进程在调用 exec() 或 _exit() 之前, 不修改任何父进程数据,也不调用任何可能返回的函数。## exec() 系列:执行新程序
exec() 系列函数用于在当前进程上下文中加载并执行新程序。 调用成功后,当前进程的地址空间被完全替换,但 PID 保持不变。
| 函数名 | 参数形式 | PATH 搜索 | 环境变量 |
|---|---|---|---|
execl() | 可变参数列表 | ✗ | 继承父进程 |
execv() | 字符串数组 | ✗ | 继承父进程 |
execle() | 可变参数 + envp | ✗ | 自定义 |
execve() | 字符串数组 + envp | ✗ | 自定义 |
execlp() | 可变参数列表 | ✓ | 继承父进程 |
execvp() | 字符串数组 | ✓ | 继承父进程 |
execvp() 和 execlp() 因能自动搜索 PATH 而最为常用。## 代码示例:fork() + execvp()
下面的示例展示了如何使用 fork() 创建子进程, 然后在子进程中使用 execvp() 执行 ls -l 命令。 父进程使用 waitpid() 等待子进程结束并获取退出状态。
| 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 | |
| 8 | int 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 | } |
进程的管理与终止
## wait() 和 waitpid()
当子进程结束时,它会变成僵尸进程(Zombie)—— 进程本身已终止,但内核进程表中的条目仍存在,直到父进程调用 wait() 或 waitpid() 获取其退出状态。若父进程不处理,僵尸进程会持续占用资源。
| 函数 | 等待对象 | 阻塞行为 | 灵活性 |
|---|---|---|---|
wait() | 任意一个子进程 | 始终阻塞 | 低 |
waitpid() | 指定 PID 或任意 | 可通过 WNOHANG 非阻塞 | 高 |
SIGCHLD 注册处理器,在处理器中循环调用 waitpid(-1, NULL, WNOHANG) 来非阻塞地回收所有已结束的子进程, 防止僵尸进程积累耗尽进程表空间。## 进程终止
| 方式 | 函数/机制 | 清理操作 | 说明 |
|---|---|---|---|
| 正常退出 | exit() | 执行(刷新缓冲区等) | 标准库函数,推荐使用 |
| 立即退出 | _exit() | 不执行 | 系统调用,信号处理器中使用 |
| main 返回 | return 0 | 执行 | 等同于调用 exit() |
| 异常终止 | 信号(SIGSEGV 等) | 不执行 | 由内核或其他进程触发 |
进程间通信(IPC)
由于进程拥有独立的地址空间,它们之间不能直接访问对方的内存。 操作系统提供了多种 IPC 机制来允许进程间交换数据和同步操作。
| 机制 | 数据方向 | 亲缘关系 | 内核持久 | 典型用途 |
|---|---|---|---|---|
| 匿名管道 | 单向 | 需要 | 否 | 父子进程简单数据流 |
| 命名管道 | 单向 | 不需要 | 否(文件) | 任意进程间数据流 |
| 信号 | 单向 | 不需要 | 否 | 异步事件通知 |
| 消息队列 | 双向 | 不需要 | 是 | 结构化消息传递 |
| 共享内存 | 双向 | 不需要 | 是 | 大量数据高效共享 |
| 套接字 | 双向 | 不需要 | 否 | 网络/本地通信 |
## 匿名管道(Anonymous Pipe)
管道是最简单的 IPC 形式,通过 pipe() 系统调用创建, 返回两个文件描述符:fd[0](读端)和 fd[1](写端)。 只能用于有亲缘关系的进程之间。
| 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 | |
| 8 | int 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() 创建, 任何知道路径的进程均可访问,无需亲缘关系。先运行读进程,再运行写进程。
| 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 | |
| 10 | int 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 | } |
| 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 | |
| 9 | int 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(), 因为前者行为更可预测。
| 信号名 | 编号 | 默认行为 | 说明 |
|---|---|---|---|
SIGINT | 2 | 终止 | 键盘中断(Ctrl+C) |
SIGTERM | 15 | 终止 | 软件终止请求 |
SIGKILL | 9 | 强制终止 | 不可捕获,不可忽略 |
SIGSEGV | 11 | 终止+核心转储 | 段错误(非法内存访问) |
SIGCHLD | 17 | 忽略 | 子进程状态改变 |
SIGUSR1 | 10 | 终止 | 用户自定义信号 1 |
SIGUSR2 | 12 | 终止 | 用户自定义信号 2 |
write()、_exit()。printf()、malloc() 等函数不安全, 在处理器中调用可能导致死锁或数据损坏。| 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 | |
| 7 | void 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 | |
| 14 | int 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),接收方可按类型选择性接收。 属于内核持久性资源,进程退出后不会自动释放。
| 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 | |
| 11 | struct msg_buffer { |
| 12 | long msg_type; ""token-comment"">// 必须是第一个成员,且为 long |
| 13 | char msg_text[100]; |
| 14 | }; |
| 15 | |
| 16 | int 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 机制,允许多个进程直接访问同一块物理内存, 避免内核与用户空间之间的数据拷贝。但必须配合信号量等同步机制防止数据竞争。
shmctl(id, IPC_RMID, NULL) 释放,否则会造成内核资源泄漏。| 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 | |
| 12 | int 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 | } |
嵌入式场景注意事项
SIGCHLD 注册处理器, 循环调用 waitpid(-1, NULL, WNOHANG) 非阻塞回收。vfork() 相比 fork() 可显著减少内存占用。但必须确保子进程在调用 exec() 或 _exit() 之前,不修改任何父进程数据。msgctl(id, IPC_RMID, NULL) 和 shmctl(id, IPC_RMID, NULL)。sigaction() 替代 signal(), 并通过 volatile sig_atomic_t 类型的标志位与主循环通信。结论
进程是嵌入式 Linux 系统中资源管理和并发执行的核心。 掌握 fork()、exec()、wait() 等进程管理工具,并根据应用场景选择合适的 IPC 机制, 是开发健壮、高效嵌入式应用的基础。
| 场景 | 推荐机制 | 原因 |
|---|---|---|
| 父子进程间传递少量数据 | 匿名管道 | 简单易用,无需额外配置 |
| 无亲缘关系进程间简单通信 | 命名管道(FIFO) | 通过文件系统路径访问 |
| 异步事件通知(设备状态变化) | 信号 | 轻量级,适合简单控制信号 |
| 多进程间传递结构化数据 | 消息队列 | 支持消息类型,可选择性接收 |
| 多进程间共享大量数据(图像帧) | 共享内存 | 零拷贝,性能最高,需配合信号量 |
| 跨主机或跨网络通信 | 套接字 | 通用性强,支持 TCP/IP 协议栈 |