一、进程创建

1.1 fork 函数

在 Linux 中 fork 函数是非常重要的系统调用,它从已存在进程中创建一个新进程。新进程称为子进程,原进程称为父进程。

#include <unistd.h>
pid_t fork(void);

返回值:子进程中返回 0,父进程中返回子进程的 PID,出错返回 -1。

进程调用 fork 后,内核会做以下工作:分配新的内存块和内核数据结构给子进程;将父进程部分数据结构内容拷贝至子进程;添加子进程到系统进程列表;fork 返回,开始调度器调度。

fork 之前父进程独立执行,fork 之后父子两个执行流分别执行。谁先执行完全由调度器决定。子进程的 task_struct 以父进程为模板。

#include <stdio.h>
#include <unistd.h>
int main() {
    int data = 100;
    printf("Before fork, data = %d\n", data);
    pid_t pid = fork();
    printf("After fork, PID = %d, fork return = %d\n", getpid(), pid);
    return 0;
}

运行结果会打印三行输出:一行 Before,两行 After。子进程没有打印 before,因为 fork 之前的代码只有父进程执行。

1.2 fork 返回值设计

fork 有两个返回值,由内核实现机制决定。fork 返回时,内核已在父子进程中分别建立各自的 task_struct,各自从返回指令处开始执行。

子进程返回 0,父进程返回子进程 PID。通过 if-else 对返回值分流,让父子进程在不同分支执行不同逻辑。

#include <stdio.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if(pid < 0) {
        perror("fork");
        return 1;
    } else if(pid == 0) {
        printf("子进程运行中, PID = %d\n", getpid());
    } else {
        printf("父进程运行中, 子进程 PID = %d\n", pid);
    }
    return 0;
}

1.3 写时拷贝

父子代码共享,数据在未写入时也共享。任意一方试图写入时,以写时拷贝方式各自拥有一份副本。这是一种延时申请技术,提高整机内存使用率,保证进程独立性。

1.4 fork 常规用法

两种典型用法:父进程复制自己处理不同任务(如服务器生成子进程处理请求);子进程从 fork 返回后调用 exec 执行全新程序。

1.5 fork 调用失败的原因

系统中有太多进程;实际用户进程数超过限制。

二、进程终止

2.1 进程退出场景

三种退出场景:代码运行完毕结果正确、结果不正确、代码异常终止。

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止(如收到信号)

2.2 退出码

退出码 0 表示成功,非 0 表示错误。可通过 echo $? 查看最近一次退出码。退出码 1 表示不被允许的操作,130/143 属于 128+n 信号。

常用退出码:

  • 退出码 0:命令执行无误
  • 退出码 1:不被允许的操作
  • 退出码 130/143:128+n 信号

return 0 的含义是退出码被系统获得用于辨别执行情况。代码出异常后退出码无意义,应关注是什么信号导致了退出。

2.3 exit 函数

#include <stdlib.h>
void exit(int status);

exit 执行用户定义的清理函数,关闭所有流并刷新缓冲区,最后调用 _exit。

#include <stdio.h>
#include <stdlib.h>
int main() {
    printf("Welcome to Linux process");
    exit(0);
}

2.4 _exit 函数

#include <unistd.h>
void _exit(int status);

_exit 直接进入内核终止,不执行清理,不刷新缓冲区。status 仅低 8 位有效。

#include <stdio.h>
#include <unistd.h>
int main() {
    printf("Welcome to Linux process");
    _exit(0);
}

2.5 return 退出

main 中 return n 等同于 exit(n)。return 会刷新缓冲区。

2.6 终止方式对比

方法

说明

刷新缓冲区

main 中 return n

n 表示退出码,自动传入 exit

会刷新

exit(n)

标准库函数,执行清理后调 _exit

会刷新

_exit(n)

系统调用,直接进入内核终止

不会刷新

三、进程等待

3.1 进程等待的必要性

子进程退出后若父进程不管不顾会造成僵尸进程,导致内存泄漏。kill -9 也无法杀死僵尸进程。父进程通过进程等待回收子进程资源并获取退出信息。另一种方式:父进程将 SIGCHLD 设为 SIG_IGN,子进程终止时自动清理。

3.2 wait 函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

成功返回被等待进程 PID,失败返回 -1。status 是输出型参数,不关心时设为 NULL。阻塞等待直到有子进程退出。

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if(pid == 0) {
        sleep(5);
        exit(42);
    } else {
        int status;
        wait(&status);
        printf("子进程已回收\n");
    }
    return 0;
}

3.3 waitpid 函数

waitpid 是进程等待的最佳实践,本质是获取子进程 task_struct 内的属性数据。

pid_t waitpid(pid_t pid, int *status, int options);

pid 参数取值

pid 值

含义

> 0

等待指定 PID 的特定子进程

-1

等待任意子进程(最常用)

0

等待同进程组的任意子进程

< -1

等待 PGID 为该绝对值的任意子进程

options 参数

含义

0

阻塞等待

WNOHANG

非阻塞等待,无子进程退出则返回 0

3.4 获取子进程 status

status 低 16 位:0-6 位退出信号,8-15 位退出码。

退出信号

退出码

含义

0

0

运行正常,结果正确

0

非 0

运行正常,结果错误

非 0

无意义

代码异常终止

WIFEXITED(status): 正常终止则为真
WEXITSTATUS(status): 提取子进程退出码

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if(pid == 0) {
        sleep(20);
        exit(10);
    } else {
        int status;
        wait(&status);
        if((status & 0x7F) == 0)
            printf("正常退出, 退出码 = %d\n", (status >> 8) & 0xFF);
        else
            printf("异常退出, 信号编号 = %d\n", status & 0x7F);
    }
    return 0;
}

3.5 阻塞与非阻塞等待

阻塞等待传 0,父进程等待直到子进程退出。非阻塞等待传 WNOHANG,父进程立即返回,可轮询子进程状态。

// 阻塞等待
pid_t ret = waitpid(-1, &status, 0);

// 非阻塞轮询
do {
    ret = waitpid(-1, &status, WNOHANG);
    if(ret == 0)
        printf("子进程仍在运行...\n");
} while(ret == 0);

四、进程程序替换

4.1 替换原理

fork 创建子进程后执行的是和父进程相同的程序。若需执行全新程序,通过程序替换实现。exec 函数将用户空间代码和数据完全替换,从新程序启动例程开始执行,进程 ID 不变。

程序替换成功后后续代码不再执行,exec 只有出错返回值(-1)。子进程替换不影响父进程(写时拷贝保证数据独立性)。

4.2 exec 函数族

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

4.3 命名规律

字母

含义

说明

l

list

参数采用列表形式

v

vector

参数用数组形式

p

path

自动搜索 PATH 环境变量

e

env

自己维护环境变量

只有 execve 是真正的系统调用(man 第 2 节),其余五个最终调用 execve(man 第 3 节)。

4.4 调用示例

char *const args[] = {"ls", "-l", "/home", NULL};
char *const env[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

execl("/bin/ls", "ls", "-l", "/home", NULL);
execlp("ls", "ls", "-l", "/home", NULL);
execle("/bin/ls", "ls", "-l", "/home", NULL, env);
execv("/bin/ls", args);
execvp("ls", args);
execve("/bin/ls", args, env);

五、自主 Shell 命令行解释器

5.1 设计目标

通过自主实现微型 Shell 理解命令行解释器运行原理。要求处理普通命令和内建命令。

5.2 实现原理

Shell 核心循环:获取命令行、解析命令行、fork 创建子进程、execvp 替换子进程、父进程 wait 等待。内建命令(cd、export、env、echo)由 Shell 自身执行,因为子进程无法改变父进程环境。

5.3 main 函数的三个参数

在实现 Shell 时,main 函数的三个参数非常重要:

int main(int argc, char *argv[], char *envp[]);

参数

说明

argc

命令行参数个数(包含程序名,至少为 1)

argv

命令行参数字符串数组,argv[0] 是程序名

envp

环境变量字符串数组,格式 KEY=VALUE,以 NULL 结尾

fork/exec 相当于 call,exit/wait 相当于 return。这种将函数调用模式扩展到进程间通信的设计思想是结构化程序设计的基础。

六、总结速查表

操作

命令或函数

创建进程

fork()

正常终止进程

exit(status) / return status

直接终止进程

_exit(status)

查看退出码

echo $?

等待任意子进程

wait(&status)

等待指定子进程

waitpid(pid, &status, 0)

非阻塞等待

waitpid(pid, &status, WNOHANG)

程序替换(列表)

execl / execlp / execle

程序替换(数组)

execv / execvp / execve

判断正常退出

WIFEXITED(status)

提取退出码

WEXITSTATUS(status)

Logo

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

更多推荐