进程是程序的一次动态执行实例,是操作系统资源分配的基本单位。在Linux系统中,每个进程都有唯一的进程ID(PID),进程之间通过进程控制块(PCB)来管理。
Linux采用进程树的组织方式:
fork()是Linux/Unix系统中最核心的进程创建系统调用,它的特殊之处在于调用一次,返回两次。
工作原理:
写时复制(Copy-On-Write, COW)机制:
传统fork实现会立即复制整个父进程地址空间,效率极低。现代Linux采用写时复制技术:
优势:大幅减少fork的开销,特别是对于大型进程
// COW机制示意(伪代码)
pid_t pid = fork();
if (pid == 0) {
// 子进程修改变量时,内核才会真正复制该页
variable = 100; // 触发COW,复制该页
}
| 特性 | 父进程 | 子进程 |
|---|---|---|
| PID | 原PID不变 | 新分配的唯一PID |
| PPID | 父进程的PID | 父进程的PID(两者相同) |
| fork返回值 | 子进程的PID(>0) | 0 |
| 文件描述符 | 保持原有FD | 复制父进程的FD表 |
| 信号处理 | 原有设置 | 复制父进程的设置 |
| 资源限制 | 原有限制 | 继承父进程的限制 |
文件描述符共享的关键点:
exec函数族用于替换当前进程的映像,加载并执行另一个程序。调用exec后,进程的代码段、数据段、堆栈都会被新程序替换,但PID保持不变。
exec函数族分类:
| 函数 | 参数形式 | 搜索路径 | 使用场景 |
|---|---|---|---|
| execl | 列表参数 | 需指定完整路径 | 固定路径执行 |
| execv | 字符串数组 | 需指定完整路径 | 动态构建参数 |
| execlp | 列表参数 | 自动搜索PATH | 执行常见命令 |
| execvp | 字符串数组 | 自动搜索PATH | 动态构建+PATH搜索 |
| execle | 列表参数+环境变量 | 需指定完整路径 | 自定义环境变量 |
| execve | 字符串数组+环境变量 | 需指定完整路径 | 系统调用原语 |
exec返回值:
wait()和waitpid()用于回收子进程资源,获取子进程退出状态。
僵尸进程(Zombie Process):
wait与waitpid的区别:
| 特性 | wait | waitpid |
|---|---|---|
| 阻塞行为 | 一直阻塞直到有子进程退出 | 可设置WNOHANG非阻塞 |
| 目标 | 任意子进程 | 可指定特定PID的子进程 |
| 并发处理 | 一次只能处理一个 | 可处理多个子进程 |
退出状态宏:
WIFEXITED(status):子进程正常退出时返回非零WEXITSTATUS(status):获取子进程的退出码(0-255)WIFSIGNALED(status):子进程被信号杀死时返回非零WTERMSIG(status):获取导致子进程终止的信号Linux系统中进程有以下主要状态:
| 状态 | 符号 | 描述 |
|---|---|---|
| 运行/就绪 | R (Running/Runnable) | 正在运行或在运行队列等待CPU |
| 可中断睡眠 | S (Sleeping) | 等待事件(如I/O完成),可被信号唤醒 |
| 不可中断睡眠 | D (Disk Sleep) | 等待I/O完成,不可被信号唤醒 |
| 停止 | T (Stopped) | 被信号(SIGSTOP/SIGTSTP)暂停 |
| 僵尸 | Z (Zombie) | 已退出但资源未被父进程回收 |
查看进程状态:
# 使用ps命令查看
ps -eo pid,stat,cmd | head -20
# 使用top命令实时查看
top
典型模式:fork + exec + wait
这是Unix/Linux系统中最经典的进程创建模式:
父进程中调用wait()等待子进程结束
pid_t pid = fork();
if (pid == 0) {
// 子进程
execl("/bin/ls", "ls", "-l", NULL);
perror("execl failed");
exit(1);
} else if (pid > 0) {
// 父进程
int status;
wait(&status);
// 处理退出状态
}
在嵌入式Linux应用开发中,多进程架构广泛应用于:
#include <unistd.h>
pid_t fork(void);
参数说明:
返回值:
errno常见值:
EAGAIN:系统进程表已满或用户进程数达到上限ENOMEM:内核内存不足#include <unistd.h>
// execl - 执行文件(列表参数)
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
// execv - 执行文件(字符串数组)
int execv(const char *path, char *const argv[]);
// execlp - 按PATH搜索执行(列表参数)
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
// execvp - 按PATH搜索执行(字符串数组)
int execvp(const char *file, char *const argv[]);
// execle - 执行文件(带环境变量)
int execle(const char *path, const char *arg, ... /* (char *) NULL, char *const envp[] */);
// execve - 执行文件(系统调用原语)
int execve(const char *path, char *const argv[], char *const envp[]);
参数说明:
path:要执行的程序的完整路径file:程序名,execvp/execle会自动搜索PATH环境变量arg:第一个参数通常是程序名argv:字符串数组,以NULL结尾envp:环境变量数组,以NULL结尾返回值:
#include <sys/wait.h>
// wait - 等待任意子进程退出
pid_t wait(int *wstatus);
// waitpid - 等待指定子进程退出
pid_t waitpid(pid_t pid, int *wstatus, int options);
waitpid参数说明:
pid:指定要等待的子进程PID
pid > 0:等待特定PID的子进程pid = -1:等待任意子进程(等同于wait)pid = 0:等待与调用进程同组ID的子进程pid < -1:等待组ID等于|pid|的子进程options:选项标志
WNOHANG:非阻塞模式,无子进程退出立即返回WUNTRACED:报告停止的子进程状态WCONTINUED:报告继续的子进程状态返回值:
#include <sys/wait.h>
// 判断子进程是否正常退出
int WIFEXITED(int status);
// 获取子进程退出码(仅当WIFEXITED非零时有效)
int WEXITSTATUS(int status);
// 判断子进程是否被信号杀死
int WIFSIGNALED(int status);
// 获取导致终止的信号编号
int WTERMSIG(int status);
// 判断子进程是否被信号停止
int WIFSTOPPED(int status);
// 获取导致停止的信号编号
int WSTOPSIG(int status);
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main() {
pid_t pid;
int counter = 100; // 共享变量
printf("Before fork: pid=%d, counter=%d\n", getpid(), counter);
pid = fork();
if (pid < 0) {
// fork失败
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程
counter += 10;
printf("[Child] pid=%d, ppid=%d, counter=%d\n",
getpid(), getppid(), counter);
exit(0);
} else {
// 父进程
counter += 20;
printf("[Parent] pid=%d, child_pid=%d, counter=%d\n",
getpid(), pid, counter);
wait(NULL); // 等待子进程结束
}
return 0;
}
编译运行:
gcc -o fork_example fork_example.c
./fork_example
输出示例:
Before fork: pid=12345, counter=100
[Child] pid=12346, ppid=12345, counter=110
[Parent] pid=12345, child_pid=12346, counter=120
说明: fork后,父子进程各有一份counter的副本,互不影响。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#define FILENAME "shared_file.txt"
int main() {
pid_t pid;
int fd;
char buf[128];
// 创建并写入文件
fd = open(FILENAME, O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "Hello", 5);
close(fd);
// 以读写方式打开文件,fork后父子进程共享此fd
fd = open(FILENAME, O_RDWR);
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程:在文件末尾追加写入
lseek(fd, 0, SEEK_END); // 移动到文件末尾
write(fd, " World", 6);
printf("[Child] wrote ' World'\n");
close(fd);
exit(0);
} else {
// 父进程:等待子进程写入后读取
sleep(1); // 等待子进程写入
lseek(fd, 0, SEEK_SET); // 移动到文件开头
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf) - 1);
printf("[Parent] read: '%s'\n", buf);
close(fd);
wait(NULL);
}
return 0;
}
编译运行:
gcc -o fd_share fd_share.c
./fd_share
输出:
[Child] wrote ' World'
[Parent] read: 'Hello World'
说明: fork后父子进程共享文件偏移量,因此子进程写入后父进程能读取到完整内容。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
pid_t pid;
// 示例1:execl执行ls命令
printf("=== Example 1: execl ===\n");
pid = fork();
if (pid == 0) {
// execl: 需要指定完整路径,参数逐个传递,以NULL结尾
execl("/bin/ls", "ls", "-l", "-a", NULL);
perror("execl failed"); // 只有execl失败才会执行到这里
exit(1);
}
wait(NULL);
// 示例2:execv执行ls命令(参数通过数组传递)
printf("\n=== Example 2: execv ===\n");
pid = fork();
if (pid == 0) {
char *args[] = {"ls", "-l", "-h", NULL};
execv("/bin/ls", args);
perror("execv failed");
exit(1);
}
wait(NULL);
// 示例3:execlp按PATH搜索执行
printf("\n=== Example 3: execlp ===\n");
pid = fork();
if (pid == 0) {
// execlp: 自动搜索PATH环境变量
execlp("ls", "ls", "-l", "/tmp", NULL);
perror("execlp failed");
exit(1);
}
wait(NULL);
// 示例4:execvp按PATH搜索执行
printf("\n=== Example 4: execvp ===\n");
pid = fork();
if (pid == 0) {
char *args[] = {"ls", "-l", "-t", NULL};
// execvp: 自动搜索PATH,参数通过数组传递
execvp("ls", args);
perror("execvp failed");
exit(1);
}
wait(NULL);
// 示例5:execle设置环境变量
printf("\n=== Example 5: execle ===\n");
pid = fork();
if (pid == 0) {
char *env[] = {"MY_VAR=hello_world", NULL};
execle("/bin/env", "env", NULL, env);
perror("execle failed");
exit(1);
}
wait(NULL);
printf("\nAll exec examples completed.\n");
return 0;
}
编译运行:
gcc -o exec_example exec_example.c
./exec_example
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
void print_exit_status(int status) {
if (WIFEXITED(status)) {
printf(" 正常退出,退出码: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf(" 被信号杀死,信号: %d\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf(" 被信号停止,信号: %d\n", WSTOPSIG(status));
}
}
int main() {
pid_t pid1, pid2;
int status;
// 创建第一个子进程:正常退出
pid1 = fork();
if (pid1 == 0) {
printf("[Child1] PID=%d, PPID=%d\n", getpid(), getppid());
sleep(1);
printf("[Child1] exiting with code 42\n");
exit(42); // 退出码42
}
// 创建第二个子进程:被信号杀死
pid2 = fork();
if (pid2 == 0) {
printf("[Child2] PID=%d, PPID=%d\n", getpid(), getppid());
sleep(2);
printf("[Child2] being killed by SIGSEGV\n");
char *p = NULL;
*p = 'x'; // 故意触发段错误
exit(1);
}
// 父进程使用waitpid等待特定子进程
printf("[Parent] Waiting for child1 (PID=%d)...\n", pid1);
waitpid(pid1, &status, 0); // 阻塞等待pid1
printf("[Parent] Child1 status:");
print_exit_status(status);
printf("[Parent] Waiting for child2 (PID=%d)...\n", pid2);
waitpid(pid2, &status, 0); // 阻塞等待pid2
printf("[Parent] Child2 status:");
print_exit_status(status);
// 非阻塞等待示例
printf("\n[Parent] Non-blocking wait example:\n");
pid_t pid3 = fork();
if (pid3 == 0) {
printf("[Child3] sleeping 3 seconds\n");
sleep(3);
exit(0);
}
// 轮询等待
while (1) {
pid_t result = waitpid(pid3, &status, WNOHANG);
if (result == 0) {
printf(" [Parent] Child3 still running...\n");
sleep(1);
} else if (result > 0) {
printf(" [Parent] Child3 exited with code %d\n", WEXITSTATUS(status));
break;
} else {
perror("waitpid failed");
break;
}
}
return 0;
}
编译运行:
gcc -o waitpid_example waitpid_example.c
./waitpid_example
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#define MAX_CLIENTS 5
// 信号处理函数:回收子进程
void sigchld_handler(int sig) {
int saved_errno = errno;
// 使用WNOHANG非阻塞回收,避免阻塞
while (waitpid(-1, NULL, WNOHANG) > 0) {
continue;
}
errno = saved_errno;
}
void handle_client(int client_id) {
printf("[Client Handler %d] PID=%d, handling client %d\n",
client_id, getpid(), client_id);
// 模拟处理客户端请求
for (int i = 0; i < 3; i++) {
printf(" [Client %d] Processing request %d...\n", client_id, i + 1);
sleep(1);
}
printf(" [Client %d] Done\n", client_id);
exit(0);
}
int main() {
pid_t pids[MAX_CLIENTS];
// 注册SIGCHLD信号处理函数
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
printf("[Server] PID=%d, starting multi-process server\n", getpid());
// 模拟接受5个客户端连接,创建子进程处理
for (int i = 0; i < MAX_CLIENTS; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork failed");
exit(1);
} else if (pids[i] == 0) {
// 子进程:处理客户端
handle_client(i);
} else {
// 父进程:记录子进程PID
printf("[Server] Forked child process PID=%d for client %d\n",
pids[i], i);
}
}
// 父进程:处理其他任务(如接受新连接)
printf("[Server] Main server running, waiting for children...\n");
// 等待一段时间让子进程完成
sleep(8);
printf("[Server] Server shutting down\n");
return 0;
}
编译运行:
gcc -o multiproc_server multiproc_server.c
./multiproc_server
| 问题 | 描述 | 解决方案 |
|---|---|---|
| fork后未检查返回值 | 无法区分父进程和子进程 | 必须检查返回值:pid<0失败,pid=0子进程,pid>0父进程 |
| fork后未处理错误 | 可能导致程序行为异常 | 失败时及时exit或错误处理 |
| fork后子进程未调用exec或exit | 子进程继续执行父进程代码 | 明确子进程的执行路径 |
| fork后未wait子进程 | 产生僵尸进程 | 使用wait/waitpid或SIGCHLD处理 |
| fork后全局变量不确定 | 输出顺序与调度顺序有关 | 不要依赖父子进程的执行顺序 |
| 问题 | 描述 | 解决方案 |
|---|---|---|
| exec失败未处理 | 后续代码会继续执行 | exec成功不返回,失败需检查errno |
| 参数传递错误 | 最后一个参数必须是NULL | 列表形式:execl(..., NULL) |
| 路径错误 | 找不到可执行文件 | 使用execlp/execvp自动搜索PATH |
| 权限问题 | 可执行文件无执行权限 | 检查文件权限:chmod +x |
| 环境变量问题 | execle/execve需要手动传递环境 | 使用environ或自定义env数组 |
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 忘记调用wait | 僵尸进程堆积,占用进程表 | 及时回收子进程 |
| wait阻塞问题 | 多个子进程时阻塞等待 | 使用waitpid+WNOHANG非阻塞 |
| 状态宏使用错误 | WIFEXITED为假时使用WEXITSTATUS | 先判断WIFEXITED再获取退出码 |
| 信号竞争 | SIGCHLD信号可能在wait前到达 | 使用SA_RESTART或sigwait |
| 僵尸进程处理 | 未注册SIGCHLD处理函数 | 使用sigaction注册信号处理 |
// 错误示例1:未检查fork返回值
pid_t pid = fork();
// 危险!pid可能是-1
// 正确做法
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
// 错误示例2:exec失败后继续执行
if (pid == 0) {
execl("/bin/ls", "ls", NULL);
// 如果execl失败,这里会继续执行
printf("This will execute if execl fails\n");
exit(1); // 必须exit,否则子进程会继续执行父进程代码
}
// 错误示例3:僵尸进程未回收
if (pid == 0) {
exit(0); // 子进程退出
}
// 父进程未调用wait,子进程变成僵尸
sleep(10);
// 僵尸进程占用进程表项
答:
答:
答:
| 区别 | 父进程 | 子进程 |
|---|---|---|
| PID | 原PID | 新分配的唯一PID |
| fork返回值 | 子进程PID | 0 |
| 文件描述符 | 独立的FD表 | 复制父进程的FD表(共享文件偏移量) |
| 信号处理 | 原有设置 | 复制父进程的设置 |
| 资源限制 | 原有限制 | 继承父进程的限制 |
答:
答:
signal(SIGCHLD, SIG_IGN)答:
| 特性 | wait | waitpid |
|---|---|---|
| 阻塞 | 一直阻塞 | 可设置WNOHANG非阻塞 |
| 目标 | 任意子进程 | 可指定特定PID |
| 并发 | 一次处理一个 | 可处理多个子进程 |
| 使用场景 | 简单的单子进程 | 复杂的多子进程管理 |