当前位置: 首页 > news >正文

linux系统中进程控制

目录

  一、进程的概述

1. 进程的定义

2. 进程的特性

3. 进程和程序的区别

4.进程的三种基本状态

5.进程各状态间的切换

二、进程相关命令

ps 查看进程

kill消灭进程

三、进程相关名词

①. 父子进程

②. 祖先进程

③. 守护进程

④. 僵尸进程

⑤. 孤儿进程

进程相关名词对比表

相关名词一句话总结

四、进程控制相关函数

        常见进程控制相关头文件说明

1.getpid() / getppid() ------ 获取进程(父)PID

2.system() ------ 运行进程

3. exec()  ------ 替换进程

4.fork()/vfork() ------ 创建进程

fork()

        三进程轮流报数

                父子孙三进程报数

        一父二儿三进程报数

vfork()

fork() 和 vfork() 的异同

5.exit()/_exit() ------ 销毁进程

进程在哪些情况下会被销毁

return、exit()、_exit() 区别

exit()

_exit()

6.wait()/waitpid() ------ 等待进程

为什么要等待进程结束?


一、进程的概述

1. 进程的定义

进程是一个正在执行的程序,是操作系统进行资源分配和调度的基本单位。
它体现了程序的动态执行过程,和静态的程序文件不同。

进一步解释
当一个进程开始运行时,就启动了一个动态的过程,主要包括两部分内容:

  1. 程序的执行过程 —— 指令的逐条运行。

  2. 所需的数据 —— 包括代码区、数据区、堆、栈以及进程运行时所占用的各种资源(如 CPU 时间、内存、文件句柄等)

2. 进程的特性

  • 1.动态性

    • 进程是程序一次执行的过程,从创建到消亡是一个动态变化的过程。

  • 2.并发性(重点)

    • 多个进程可以在宏观上“同时”运行。

    • 微观上,CPU 在某一时刻只执行一个进程,但通过快速切换(时间片轮转),让用户感觉是并行执行。

    • 示例:进程 A(QQ)、进程 B(Word)交替运行。

  • 3.独立性

    • 进程是系统资源分配和调度的最小单位

    • 每个进程都有唯一的标识符(PID,类型为 pid_t,本质是一个非负整数),类似身份证,确保唯一性。

  • 4.异步性

    • 各进程独立运行,速度不可预知,可能相互制约,导致进程的执行呈现“间隙性”。

  • 5.结构性

    • 进程由三部分组成:

      1. 程序(代码)

      2. 数据(运行所需数据)

      3. 进程控制块(PCB)

    • PCB 是操作系统为管理进程设置的核心数据结构,是系统感知进程存在的唯一标志。


PCB(进程控制块)的主要组成部分

  • 程序计数器(PC)

    • 保存将要执行的下一条指令的地址。

  • 进程状态

    • 记录进程当前所处状态(如 new、running、waiting、blocked 等)。

  • 存储器管理信息

    • 包括页表、段表等,用于管理该进程占用的内存资源。

  • 输入输出信息

    • 保存该进程所使用的 I/O 设备及文件信息,例如打印机、磁带机等。

3. 进程和程序的区别

  • 1.静态与动态

    • 程序:是一个静态的文件,存放在磁盘中(可以长期保存),仅仅是指令和数据的集合。

    • 进程:是程序在系统中的一次执行过程,是动态的,从创建到消亡都处于变化中。

  • 2.存储位置

    • 程序:可以以文件形式存放在外存(硬盘/SSD)中,不一定会被立即执行。

    • 进程:必须加载到内存中才能运行,并且占用 CPU、内存、I/O 等资源。

  • 3.对应关系

    • 一个程序 可以对应 多个进程(比如你开了多个 QQ 窗口,每个窗口对应一个进程)。

    • 一个进程 只能对应 一个特定的程序

  • 4.本质

    • 程序是资源,进程是执行者

    • 没有执行的程序只是“躺着的代码”;只有当它运行时,才成为进程。

4.进程的三种基本状态

  • 1.就绪态(Ready)

    • 定义:进程已经分配到除 CPU 以外的所有资源,只差 CPU。

    • 特点:一旦调度器把 CPU 分给它,就可以立刻运行。

    • 类比:学生已经准备好考试,只差老师发试卷。


  • 2.运行态(Running)

    • 定义:进程正在 CPU 上执行。

    • 特点:同一时刻,一个 CPU 核心只能运行一个进程。

    • 类比:学生正在答卷。


  • 3.阻塞态(Blocked / Waiting)

    • 定义:进程因为等待某个事件(而不是因为 CPU 不够)而暂时不能运行。

    • 特点:即使有 CPU 时间片,也不能运行,必须等事件完成后才能转为就绪态。

    • 常见原因:

      • 等待 I/O 完成(比如读取文件)。

      • 等待缓冲区可用。

      • 等待某个条件或信号(比如进程间同步)。

    • 类比:学生在考试时必须等老师发参考资料才能继续写题。


5.进程各状态间的切换

  • 就绪 → 运行:进程获得 CPU。

  • 运行 → 就绪:时间片用完,或被更高优先级进程抢占。

  • 运行 → 阻塞:执行过程中,等待 I/O 或条件未满足。

  • 阻塞 → 就绪:等待的事件完成(I/O 完成、信号到达)。

注意:

进程处于阻塞态,如果获取了事件请求,只能回到就绪态,不能回到运行态;

只有在运行态才能进入阻塞态,就绪态不能进入阻塞态。

二、进程相关命令

ps 查看进程

1.ps -aux

  • 查看系统中所有进程及其详细状态(用户、PID、CPU 占用率、内存占用率、状态等)。

  • 常用于整体查看进程运行情况。

  • USER:进程的所属用户(谁启动的)。

  • PID:进程的唯一标识符(Process ID)。

  • %CPU:进程占用 CPU 的百分比。

  • %MEM:进程占用物理内存的百分比。

  • VSZ(Virtual Memory Size):虚拟内存大小(单位 KB),进程申请的虚拟地址空间总量。

  • RSS(Resident Set Size):常驻内存集大小(单位 KB),实际占用的物理内存。

  • TTY:进程关联的终端(控制台)。

    • ? 表示该进程没有控制终端(例如系统守护进程)。

  • STAT:进程的状态标识符。

    • 常见的有:

      • R:运行中(Running)

      • S:睡眠(Sleeping,可中断)

      • D:不可中断的睡眠(通常是等待 I/O)

      • T:暂停(Stopped)

      • Z:僵尸进程(Zombie)

    • 还可能带附加符号:

      • <:高优先级

      • N:低优先级(nice 值)

      • s:会话首进程

      • l:多线程进程

      • +:位于前台进程组

  • START:进程的启动时间。

  • TIME:进程累计使用 CPU 的时间。

  • COMMAND:启动该进程的命令。

2.ps -cf

  • 以树状结构显示进程之间的父子关系

  • 常用于分析进程是由谁启动的、层级关系如何。

  • UID:进程所属用户。

  • PID:进程 ID。

  • PPID(Parent PID):父进程 ID,说明该进程由哪个进程启动。

  • CLS(Scheduling Class):进程调度策略类别。

    • TS:Time Sharing,分时调度(大多数普通进程)。

    • FF:First in First out(实时调度)。

    • RR:Round Robin(实时调度)。

  • PRI:进程优先级(数值越小优先级越高)。

  • STIME:进程的启动时间。

  • TTY:进程对应的终端。

  • TIME:进程使用 CPU 的累计时间。

  • CMD:启动该进程的命令。

kill消灭进程
 

kill 命令的本质

  • 作用:向指定的进程 发送信号(signal)

  • 默认信号kill PID 默认发送的是 SIGTERM (15),表示“请求终止”,允许进程做清理工作后再退出。

  • 强制信号kill -9 PID 发送的是 SIGKILL (9),表示“强制杀死”,进程立即结束,不能被捕获、阻塞或忽略。


常用信号

  • SIGTERM (15):正常终止进程(默认)。

  • SIGKILL (9):强制立即杀死进程(不可拦截)。

  • SIGSTOP (19):暂停进程(类似 Ctrl+Z)。

  • SIGCONT (18):恢复被暂停的进程。

  • SIGHUP (1):让进程重新读取配置文件(常用于守护进程)。

测试killl

1.编译并运行程序:

gcc test.c -o test
./test

test.c代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc,char *argv[])
{  pid_t pid;// 获取当前进程 PIDpid = getpid();// 输出 PIDprintf("当前进程的 PID = %d\n", pid);while(1); // 让程序保持运行,方便用 ps / kill 测试return 0;
}

程序会打印出 PID,然后一直运行。

2.开另一个终端,可以找到该进程:


3.用 kill 结束它:

可以看到进程被杀死释放

也找不到改进程了

三、进程相关名词

①. 父子进程

1.定义

  • 在操作系统中,大多数进程不是凭空产生的,而是由另一个进程通过 系统调用(如 fork())创建的。

  • 创建者进程称为 父进程,被创建的进程称为 子进程

2.祖先进程

  • 在父子关系链条中,最顶层的父进程称为 祖先进程

  • 在 Linux 系统中,所有进程最终的祖先进程是 init 进程 (PID=1),它负责收养孤儿进程。

3.资源继承

  • 子进程会继承父进程的大部分资源,如:

    • 代码段

    • 数据段

    • 打开的文件描述符

    • 环境变量

    • 工作目录

  • 但子进程拥有自己独立的 PID 和运行空间。

4.子进程回收

  • 子进程运行结束后,系统会保留它的 退出状态信息(如退出码、CPU 时间等)。

  • 父进程必须调用 wait()waitpid() 来读取这些信息,并释放子进程占用的 PCB(进程控制块)等资源。

  • 如果父进程不回收,子进程就会变成 僵尸进程

②. 祖先进程

1.定义

  • 在进程链条中,最顶层的父进程称为 祖先进程

  • 在 Linux 系统中,所有进程最终的祖先进程是 init 进程 (PID=1)

2.系统启动过程与祖先进程的产生

  • 计算机加电启动时,BIOS/UEFI(BOSS 😉)会从磁盘中加载 引导程序,将 Linux 内核装入内存。

  • 内核初始化完成后,会创建 进程 0(又称为 swapper 进程 或 idle 进程)。

  • 进程 0 随后创建 进程 1,即 init 进程

3.进程 1 (init) 的作用

  • init 是系统中第一个用户态进程,也是所有其他进程的“祖先”。

  • 主要职责:

    • 负责系统和用户进程的初始化。

    • 创建并管理各种后台服务进程。

    • 启动 shell 进程,提供用户与系统交互的接口。

    • 收养孤儿进程,防止产生僵尸进程。

③. 守护进程

1.定义

  • 守护进程(又叫 精灵进程)是一类特殊的进程。

  • 特点是 独立于控制终端,在 后台长期运行,通常会周期性地执行某些任务或等待特定事件发生。

2.特点

  • 后台运行:不直接与用户交互。

  • 无控制终端:不会随着终端的关闭而退出。

  • 生命周期长:往往自系统启动时就运行,一直持续到系统关闭。

  • 提供服务:通常为其他进程或用户提供系统级服务。

3.常见的守护进程

  • sshd:提供远程登录服务。

  • crond:定时任务调度。

  • syslogd:系统日志服务。

  • httpd/nginx:Web 服务。

4.作用

  • 保证一些系统服务长期稳定运行。

  • 提供后台支持,让用户即使不操作终端,服务也能自动执行。

④. 僵尸进程

1.定义

  • 僵尸进程是指 已经结束运行,但仍然在进程表中保留条目的进程

  • 它本身已经不再占用 CPU 和内存等运行资源,但会占用一个 PID 和少量系统内核资源。

  • 可以把它理解为系统里的“垃圾进程”。

2.产生原因

  • 子进程结束 时,内核会保留其退出状态信息(如退出码、资源使用情况)。

  • 如果 父进程还在运行,但没有调用 wait()waitpid() 来回收子进程的退出信息,那么子进程就会变成 僵尸进程

3.危害

  • 少量僵尸进程问题不大,但大量僵尸进程会导致系统的 进程号(PID)耗尽,从而影响新进程的创建。

4.解决办法

  • 在父进程中调用 wait()waitpid(),主动回收子进程。

  • 如果父进程没有正确回收,可以让父进程退出,这样子进程会变为孤儿进程,由 init 进程 接管并回收,从而避免僵尸进程长期存在。

⑤. 孤儿进程

1.定义

  • 如果 父进程先结束,而它的 子进程仍在运行,这些没有父进程的子进程就称为 孤儿进程

2.处理机制

  • 在 Linux 系统中,孤儿进程不会一直“无依无靠”。

  • 它们会被 祖先进程  收养。

  • 当孤儿进程最终结束时,由 init 负责调用 wait() 回收它们的资源。

3.特点

  • 孤儿进程 不会对系统造成危害,因为系统会自动交给 init 进程托管并正确回收。

  • 与僵尸进程不同,僵尸进程是因为父进程不回收造成的,而孤儿进程则是父进程先退出。

进程相关名词对比表

名词定义特点是否有危害典型处理方式
父子进程父进程创建子进程,二者形成父子关系子进程继承父进程的大部分资源,但有独立 PID父进程需管理、回收子进程
祖先进程最顶层的父进程,在 Linux 中是 init (PID=1)所有进程最终的祖先,负责收养孤儿进程系统启动时由内核创建
守护进程在后台长期运行、无控制终端的特殊进程周期性执行任务或等待事件,提供系统服务系统启动时创建并常驻后台
僵尸进程子进程结束但父进程未回收其资源占用 PID,不能运行,系统垃圾有(大量僵尸会耗尽 PID)父进程调用 wait()/waitpid() 回收,或交给 init 进程回收
孤儿进程父进程先结束,子进程仍在运行自动由 init 收养,运行正常由 init 回收,不会造成危害

相关名词一句话总结

父子进程是进程间最基本的关系,子进程的正常回收是避免系统出现僵尸进程的关键。

Linux 中的 祖先进程 是由内核在启动时产生的 init(PID=1),它是所有进程的最早祖先,也是系统运行的核心支撑进程。

守护进程就是在后台默默运行的“精灵”,不依赖终端,负责为系统和用户提供长期服务。

僵尸进程 = 子进程已死 + 父进程未收尸。必须由父进程调用 wait() 系统调用来清理,否则就是系统垃圾。

孤儿进程 = 父进程已死 + 子进程未结束,它们会被 init 收养,不会变成系统垃圾。

四、进程控制相关函数

相关头文件

#include <sys/types.h>   // 定义 pid_t 类型
#include <unistd.h>      // 提供 getpid()、getppid()
#include <stdlib.h>      // 提供 exit() 等
#include <sys/wait.h>    // 提供 wait() 等
#include <stdio.h>       // 提供 printf()

常见进程控制相关头文件说明

  • #include <sys/types.h>

    • 作用:定义一些系统调用会用到的基本数据类型。

    • 常见类型:

      • pid_t —— 进程ID类型

      • uid_t / gid_t —— 用户ID/组ID

      • off_t —— 文件偏移量

    • 示例:pid_t pid = fork();


  • #include <unistd.h>

    • 作用:POSIX 标准 API 函数原型。

    • 常见函数:

      • fork() / vfork() —— 创建子进程

      • getpid() / getppid() —— 获取进程/父进程 ID

      • exec*() 系列函数 —— 执行新程序

      • sleep() / usleep() —— 进程睡眠

    • 示例:pid_t pid = vfork();


  • #include <stdlib.h>

    • 作用:提供通用的工具函数。

    • 常见函数:

      • exit() / _exit() —— 退出进程

      • malloc() / free() —— 内存管理

      • atoi() / atof() —— 字符串转数值

    • 示例:exit(0);


  • #include <sys/wait.h>

    • 作用:提供与 进程等待 相关的函数与宏。

    • 常见函数:

      • wait() —— 等待任意子进程结束

      • waitpid() —— 等待指定子进程结束

    • 常见宏:

      • WIFEXITED(status) —— 判断子进程是否正常退出

      • WEXITSTATUS(status) —— 获取子进程的退出码


  • #include <stdio.h>

    • 作用:标准 I/O 函数库。

    • 常见函数:

      • printf() / scanf() —— 格式化输入输出

      • fprintf() —— 文件流输出

    • 示例:printf("pid=%d\n", getpid());

1.getpid() / getppid() ------ 获取进程(父)PID

1. 头文件

#include <sys/types.h>
#include <unistd.h>


2. 作用

  • getpid():获取当前进程的 进程 ID (PID)

  • getppid():获取当前进程的 父进程 ID (PPID)


3. 函数原型

pid_t getpid(void);
pid_t getppid(void);


4. 参数

  • 无参数


5. 返回值

  • getpid():返回当前进程的 PID。

  • getppid():返回父进程的 PID。

  • 这两个函数调用总是成功,不会返回错误(errno 不会被设置)。


6. 示例代码

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>int main(void) {pid_t pid, ppid;// 获取当前进程IDpid = getpid();// 获取父进程IDppid = getppid();printf("当前进程ID (pid) = %d\n", pid);printf("父进程ID (ppid) = %d\n", ppid);return 0;
}

现象:


7. 注意事项

  • PID 命名空间:如果父进程在不同的 PID namespace,getppid() 可能返回 0

  • glibc 缓存机制

    • 从 glibc 2.3.4 起,getpid() 内部会缓存 PID。

    • 如果通过 syscall() 直接调用 fork()/clone(),而不是 glibc 的封装函数,子进程中 getpid() 可能返回错误的值(父进程 PID)。

  • 孤儿进程:如果父进程先退出,子进程的 ppid 会变为收养它的进程 ID(通常是 initsystemd,PID=1)。


📌 总结口诀:

  • getpid() → 自己是谁。

  • getppid() → 父亲是谁(但父亲没了就会变成“被 init/systemd 收养”)。

2.system() ------ 运行进程

1. 头文件

#include <stdlib.h>


2. 作用

  • 用来在 C 程序中执行一个 shell 命令

  • 内部会调用 fork()execl("/bin/sh", "sh", "-c", command, …)waitpid() 来完成命令的执行和回收子进程。

  • 相当于在程序中直接运行 sh -c "command"


3. 函数原型

int system(const char *command);


4. 参数

  • const char *command:要执行的 shell 命令字符串。

    • 如果为 NULL,函数只检查系统是否有可用的 /bin/sh,返回结果表示 shell 是否存在。


5. 返回值

  • command == NULL

    • 返回 非零值 → 系统有可用 shell;

    • 返回 0 → 系统没有可用 shell。

  • 出错时

    • 如果子进程无法创建 / 无法获取状态 → 返回 -1

    • 如果无法执行 shell → 返回值等效于子进程调用 _exit(127)

  • 正常执行时

    • 返回值是 子 shell 的退出状态(可用 WIFEXITED(status)WEXITSTATUS(status) 等宏分析)。

    • 即命令本身的最后一条语句的退出码。

6.运行机制

system("ls -l");
相当于:

  1. fork() —— 创建子进程。

  2. 子进程调用 exec() —— 执行命令(由 /bin/sh 解释执行)。

  3. 父进程 wait() —— 等待子进程结束。

  4. 父进程继续运行。

👉 特点:执行完成后会回到原进程,继续往下执行。


7.示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>int main(void) {pid_t pid, ppid;// 第一步:打印当前进程的PID和PPIDpid = getpid();ppid = getppid();printf("当前进程 PID = %d\n", pid);printf("父进程 PPID = %d\n", ppid);// 第二步:利用 system() 查看本进程信息printf("\n=== 使用 system() 查看进程信息 ===\n");system("ps -o pid,ppid,cmd | grep main");// 第三步:利用 system() 执行 pwd 和 ls -lprintf("\n=== 当前路径 ===\n");system("pwd");printf("\n=== 当前目录内容 ===\n");system("ls -l");return 0;
}

现象:

8. 注意事项

  • 简便但低效:调用 system() 会多启动一个 shell,执行效率比 exec() 低。

  • 信号处理:在父进程执行命令期间,SIGCHLD 被阻塞,SIGINTSIGQUIT 被忽略。

  • 安全性问题

    • 不要在 setuid/setgid 程序中使用 system(),因为环境变量可能被利用,导致安全漏洞。

    • 更安全的方式是使用 exec() 系列函数。

  • 区别返回值 127 的情况

    • 命令本身返回 127;

    • 或 shell 无法执行(两种情况返回值一样,不可区分)。


📌 总结口诀:

  • 简单场景system("ls"); 最方便。

  • 需要控制/更安全 → 用 fork() + exec()

3. exec()  ------ 替换进程

1. 头文件

#include <unistd.h>


2. 作用

  • 功能:用新程序替换当前进程的执行代码,但 进程ID (PID) 不变

  • 注意:

    • 进程的 代码段、数据段会被新程序替换

    • 进程 ID 不变,父进程ID也保持不变。

    • 成功执行后,execl() 之后的代码不会被执行


3.函数族(exec 系列)

exec 函数家族名字里通常包含 l/v/e/p

函数名参数形式是否自动查找PATH是否可指定环境变量
execl列表形式 (arg1, arg2, …, NULL)❌ 不会❌ 不行
execv数组形式 (argv[])❌ 不会❌ 不行
execlp列表形式✅ 会查找 PATH 环境变量❌ 不行
execvp数组形式✅ 会查找 PATH 环境变量❌ 不行
execle列表形式❌ 不会✅ 可以指定 envp
execvpe数组形式✅ 会查找 PATH✅ 可以指定 envp

解释

  1. l (list):参数逐个列出,最后以 NULL 结尾。

    execl("/bin/ls", "ls", "-l", NULL);
  2. v (vector):参数打包成 char *argv[] 数组。

    char *argv[] = {"ls", "-l", NULL};
    execv("/bin/ls", argv);
    
  3. p (path):会在 PATH 环境变量中搜索程序,而不需要写绝对路径。

    execlp("ls", "ls", "-l", NULL);   // 会在 PATH 中找 ls
    
  4. e (environment):允许传入自定义环境变量 envp[]

    char *argv[] = {"ls", "-l", NULL};
    char *envp[] = {"PATH=/usr/bin", NULL};
    execvpe("ls", argv, envp);
    

关键点

  • exec不会返回(除非失败返回 -1),因为它会用新程序替换掉当前进程映像

  • PID 不变,只是代码和数据段换了。

  • 常用的其实就 2 个:

    • execlp(方便直接写命令,查 PATH)。

    • execvp(一般配合 argv[],更灵活)。


4. 函数原型

int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /* (char *) NULL, char * const envp[] */);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);


5. 参数说明

  • path / file:要执行的文件名或路径。

    • 如果是 execlp/execvp/execvpe,则 file 中不包含 / 时,会在 PATH 环境变量指定的目录中搜索。

  • arg / argv[]:新程序的参数列表。

    • arg0 一般约定为程序名本身。

    • execl/execlp/execle:参数用可变参数列出,最后必须以 (char *)NULL 结尾。

    • execv/execvp/execvpe:参数通过字符串数组 argv[] 传递,以 NULL 结尾。

  • envp[]:新程序的环境变量表(仅 execleexecvpe 可指定)。


6. 返回值

  • 成功:不返回(原进程映像完全被替换)。

  • 失败:返回 -1,同时设置 errno

7.示例

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main(void)
{pid_t pid, ppid;// 执行前,打印自身进程号和父进程号pid = getpid();ppid = getppid();printf("执行前:PID = %d, PPID = %d\n", pid, ppid);// 提示即将执行新程序printf("即将用 sleep 10 替换当前进程...\n");// 替换进程映像:把当前程序替换为 "sleep 10"execlp("sleep", "sleep", "10", NULL);// 如果 exec 成功,下面的 printf 永远不会执行// exec 失败时才会执行这里printf("exec 执行失败");return 1;
}

现象:

编译并运行

gcc exec_demo.c -o exec
./exec

进程的PID为8457

在程序运行时,打开另一个终端,输入:

ps -ef | grep sleep

会看到

PID 8457 和刚才 exec 打印的完全一样;

说明进程号没变,只是进程映像从 exec变成了 sleep

4.fork()/vfork() ------ 创建进程

fork()

1. 头文件

#include <unistd.h>

2. 作用

  • 在当前进程中创建一个 子进程

  • 子进程几乎是父进程的副本,拥有独立的 PID,独立的地址空间。

  • 父子进程会从 fork() 调用处继续往下执行


3. 函数原型

pid_t fork(void);

4. 参数

  • 无参数。


5. 返回值

  • 父进程中:fork() 返回子进程的 PID(正整数)

  • 子进程中:fork() 返回 0

  • 出错时:返回 负数(-1),表示创建失败。

6.特点

  • fork() 调用时,操作系统会把当前进程 复制一份,生成一个几乎一模一样的子进程。

  • 子进程会继承父进程的代码段、数据段、堆、栈、文件描述符等

  • 父子进程拥有各自独立的内存空间,所以变量值最初相同,但之后各自修改互不影响。

  • fork() 前的代码只执行一次(因为那时还没分叉)。

  • fork() 后的代码会被执行两次(父子进程各一次)。

  • 子进程不是从 main 开始重新运行,而是从 fork() 调用点继续执行

7.示例:

#include <unistd.h>
#include <stdio.h>int main() {printf("创建子进程\r\n");pid_t ret = fork();if(ret < 0) {printf("创建子进程失败\r\n");}else if(ret == 0) {printf("fork()的返回值在子进程为0 :ret = %d\r\n", ret);printf("我是子进程 pid = %d  ppid = %d\r\n", getpid(), getppid());}else {printf("fork()的返回值在父进程为子进程的PID :ret = %d\r\n", ret);printf("我是父进程 pid = %d  ppid = %d\r\n", getpid(), getppid());}}

现象:

为什么子进程的 ppid 不是 8932?

这其实是运行环境导致的:

  • 当父进程结束得比子进程快时,子进程会成为“孤儿进程”。

  • 孤儿进程会被操作系统的 init/systemd(PID 一般很小,比如 1 或 1378)收养。

  • 所以看到子进程的 ppid 不是 8932,而是 1378(系统收养它的进程)。

  • #include <unistd.h>
    #include <stdio.h>int main() {printf("创建子进程\r\n");pid_t ret = fork();if(ret < 0) {printf("创建子进程失败\r\n");}else if(ret == 0) {printf("fork()的返回值在子进程为0 :ret = %d\r\n", ret);printf("我是子进程 pid = %d  ppid = %d\r\n", getpid(), getppid());}else {printf("fork()的返回值在父进程为子进程的PID :ret = %d\r\n", ret);printf("我是父进程 pid = %d  ppid = %d\r\n", getpid(), getppid());sleep(2);}}

        三进程轮流报数
                父子孙三进程报数

代码

#include <unistd.h>
#include <stdio.h>int main() {pid_t ret1 = fork();if(ret1 < 0) {printf("创建子进程失败\r\n");}else if(ret1 == 0) {      //子进程pid_t  ret2 = fork();if(ret2 < 0) {printf("创建孙进程失败\r\n");}else if(ret2 == 0) {    //孙进程int c = 3;while (1){sleep(2);printf("grandson num is %d\r\n",c);c += 3;sleep(1);}   }else {  //子进程int b = 2;while (1){sleep(1);printf("son num is %d\r\n",b);b += 3;sleep(2);}}         }   else {   //父进程int a = 1;while (1){printf("parent num is %d\r\n", a);a += 3;sleep(3);}}
}

现象:

一父二儿三进程报数

代码:

#include <unistd.h>
#include <stdio.h>int main() {pid_t ret1 = fork();if(ret1 < 0) {printf("创建子进程失败\r\n");}else if(ret1 == 0) {      //子进程1   int b = 2;while (1){sleep(1);printf("son1 num is %d\r\n",b);b += 3;sleep(2);}}         else {   //父进程pid_t  ret2 = fork();if(ret2 < 0) {printf("创建子进程2失败\r\n");}else if(ret2 == 0) {    //子进程2int c = 3;while (1){sleep(2);printf("son2 num is %d\r\n",c);c += 3;sleep(1);}   }else {  //父进程int a = 1;while (1){printf("parent num is %d\r\n", a);a += 3;sleep(3);}}}
}

现象:

vfork()

1.头文件

#include <unistd.h>

2.作用

创建一个子进程,并让子进程先运行,父进程挂起,直到子进程结束(调用 _exit()exec())之后父进程才恢复运行。

3.函数原型

 pid_t vfork(void);

4.参数

  • 无参数。


5. 返回值(和fork一样)

  • 父进程中:fork() 返回子进程的 PID(正整数)

  • 子进程中:fork() 返回 0

  • 出错时:返回 负数(-1),表示创建失败。

6.示例:

#include <sys/types.h>   // 定义 pid_t 类型
#include <unistd.h>      // 提供 vfork()、getpid()、getppid()
#include <stdio.h>       // 提供 printf()
#include <stdlib.h>      // 提供 exit() / _exit()int main(void)
{pid_t pid;int num = 10;printf("父进程开始运行 (pid=%d, ppid=%d), num=%d\n", getpid(), getppid(), num);// 创建子进程pid = vfork();if (pid < 0) {perror("vfork error");exit(1);}else if (pid == 0) {   // 子进程printf("子进程运行中 (pid=%d, ppid=%d), num=%d\n", getpid(), getppid(), num);num += 5;printf("子进程修改 num=%d\n", num);// 子进程必须用 _exit() 或 exec() 结束,避免破坏父进程栈_exit(0);}else {   // 父进程// 注意:父进程会等子进程 _exit() 后才继续执行num += 10;printf("父进程继续运行 (pid=%d), num=%d\n", getpid(), num);}return 0;
}

现象

fork() 和 vfork() 的异同

✅ 共同点

  • 1.都可以用来 创建子进程

  • 2.都有相同的 返回值规则

    • 父进程返回子进程 PID(正整数)

    • 子进程返回 0

    • 出错返回 -1


❌ 不同点

  • 1.执行顺序

    • fork():父子进程独立调度,执行顺序不确定,可以交替执行。

    • vfork():父进程会阻塞,必须等待子进程调用 _exit()exec() 之后,父进程才能继续执行。

  • 2.内存空间

    • fork():父子进程拥有各自独立的虚拟地址空间(写时拷贝机制)。

    • vfork():父子进程共享同一块地址空间(栈、数据段、堆),子进程的修改会影响父进程。

  • 3.使用场景

    • fork():通用,适用于所有情况。

    • vfork():主要用于子进程 立即调用 exec() 启动新程序的场景,效率更高。

总结一句话

  • fork → 父子进程“各自一份”,并行独立。

  • vfork → 父子进程“共用一份”,子进程先跑完,父进程再继续。

5.exit()/_exit() ------ 销毁进程

进程在哪些情况下会被销毁

  • 1.进程代码执行结束 —— 程序从 main() 正常返回(return),进程自然退出。

  • 2.调用退出函数 —— 显式调用 exit()_exit() 等结束当前进程。

  • 3.外部信号终止 ——

    • 用户使用 Ctrl + C(向进程发送 SIGINT 信号)。

    • 使用 kill 命令向进程发送终止信号(如 SIGKILL)。

  • 4.异常或错误 —— 进程运行过程中出现严重错误(例如非法内存访问、除零错误),操作系统会向进程发送信号(如 SIGSEGV),导致进程被销毁。

return、exit()、_exit() 区别

  • return

    • 用于结束函数。

    • main() 中的 return 实际上等价于调用 exit(),但 return 本身只会结束函数,不是结束进程的专用方式。

  • exit(int status)

    • 功能:结束进程,会执行清理工作

  • _exit(int status)

    • 功能:立即结束进程,不会执行清理工作

exit()

1.头文件

#include <stdlib.h>

exit() 是一个高级封装,属于 C 标准库,由 glibc(或其他 C 库)实现,需要包含 C 标准库的头文件 <stdlib.h>

2.原型

void exit(int status);

3.参数

  • int status:退出状态码,通常 0 表示正常退出,非 0 表示异常退出。

  • 这个值会传递给父进程(父进程通过 wait()waitpid() 可以获取)。

4.示例代码

#include <stdio.h>
#include <stdlib.h>int main(void) {printf("Hello");   // 没有换行,存在缓冲区exit(0);           // 会刷新缓冲区,所以 "Hello" 会被打印出来printf("World\n"); // 永远不会执行
}

现象:

exit() 会刷新标准 I/O 缓冲区,把 "Hello" 打印出来。

_exit()

1.头文件

#include <unistd.h>

_exit()是系统调用的一部分,直接和内核交互,所以声明在 <unistd.h> 里。

2.原型

void _exit(int status);

3.参数

  • 和exit()一样
  • int status:退出状态码,通常 0 表示正常退出,非 0 表示异常退出。

  • 这个值会传递给父进程(父进程通过 wait()waitpid() 可以获取)。

4.示例代码

#include <stdio.h>
#include <unistd.h>int main(void) {printf("Hello");   // 没有换行,内容在缓冲区_exit(0);          // 不会刷新缓冲区printf("World\n"); // 永远不会执行
}

现象:

_exit() 不会刷新缓冲区,所以 "Hello" 没有机会打印出来。

(空输出,什么都没有)

6.wait()/waitpid() ------ 等待进程

为什么要等待进程结束?

1. 避免产生 僵尸进程

  • 子进程结束时,操作系统并不会立刻把它的所有资源回收。

  • 内核会保留一部分信息(退出状态、PID 等),让父进程可以查询。

  • 在父进程调用 wait()waitpid() 之前,这个子进程会处于 僵尸状态 (Zombie)

  • 如果父进程不去等待,僵尸进程会一直留在系统里,浪费系统资源

👉 等待子进程 = 回收子进程资源


2. 避免 孤儿进程 混乱

  • 如果父进程不管子进程直接退出,那么子进程会变成 孤儿进程,被 init(PID=1)收养。

  • 虽然系统会替它收尸(不会长期占资源),但逻辑上可能导致业务混乱,比如:

    • 父进程要处理子进程的计算结果。

    • 父进程需要知道子进程是否执行成功。

👉 等待子进程可以保持 父子逻辑关系清晰


3. 获取子进程的退出状态

  • 父进程可以通过 wait() / waitpid() 拿到子进程 exit() 返回的值。

  • 这样父进程就能判断子进程是:

    • 正常退出WIFEXITED(status))。

    • 非正常退出(信号终止 WIFSIGNALED(status))。

  • 在很多场景下,父进程需要根据子进程的执行结果做后续处理。

👉 等待子进程 = 获取子进程的运行结果


4. 保证父子进程的执行顺序

  • 有时业务需要父进程必须等子进程结束之后才能继续。

  • wait() 就是一个 同步机制

例如:
父进程让子进程计算一个结果,父进程必须等子进程退出并返回值,再继续执行。

1.头文件

#include <sys/wait.h>

2.作用

  • 1.父进程等待子进程结束

    • 通过 wait() / waitpid(),父进程能够阻塞等待子进程运行结束,避免子进程成为 孤儿进程

  • 2.回收子进程资源

    • 子进程结束后,内核仍会保留其退出状态等信息。

    • 如果父进程不调用 wait() / waitpid() 来读取这些信息,子进程就会进入 僵尸状态,占用系统资源。

    • 调用等待函数后,系统会清理(释放 PCB 等资源),避免僵尸进程的产生。

  • 3.同步与结果获取

    • wait() 是一个阻塞函数,会让父进程挂起,直到某个子进程退出。

    • waitpid() 功能更灵活,可以指定等待某个子进程,甚至支持非阻塞模式(WNOHANG),父进程能及时获取子进程的退出状态。

3.wait() 的函数原型

pid_t wait(int *status);
  • 参数

    • int *status:用于保存子进程的退出状态。

      • 如果不关心退出状态,可以传 NULL

  • 返回值

    • 成功:返回已结束子进程的 PID。

    • 失败:返回 -1,并设置 errno

4.waitpid() 的函数原型

pid_t waitpid(pid_t pid, int *status, int options);
  • 参数

    • pid

      • pid > 0 → 等待指定子进程 PID。

      • pid = 0 → 等待同进程组的任意子进程。

      • pid = -1 → 等待任意子进程(相当于 wait())。

      • pid < -1 → 等待进程组 ID 为 |pid| 的任意子进程。

    • int *status:保存子进程退出状态。

    • int options:选项参数

      • 0 → 阻塞等待。

      • WNOHANG → 非阻塞等待,没有子进程退出时立即返回 0。

  • 返回值

    • 成功:返回已结束子进程的 PID。

    • 0:在 WNOHANG 下,没有子进程退出。

    • 失败:返回 -1,并设置 errno

5.相关的宏定义

在使用 wait() / waitpid() 时,配合 一些宏定义 来解析 status,这些宏定义都在:

#include <sys/wait.h>

1.判断子进程退出方式

  • WIFEXITED(status)

    • 若子进程 正常退出(调用 exit() 或从 main 返回),返回 非零

  • WIFSIGNALED(status)

    • 若子进程是被 信号终止 的(如 SIGKILL),返回 非零

  • WIFSTOPPED(status)

    • 若子进程被 信号暂停(如 SIGSTOP),返回 非零

  • WIFCONTINUED(status)(POSIX.1-2001 标准后增加)

    • 若子进程被 SIGCONT 信号 恢复运行,返回 非零


2. 获取退出码 / 信号编号

  • WEXITSTATUS(status)

    • WIFEXITED(status) 为真时使用,得到子进程调用 exit() 传递的 退出码(0~255)。

  • WTERMSIG(status)

    • WIFSIGNALED(status) 为真时使用,得到 导致子进程终止的信号编号

  • WSTOPSIG(status)

    • WIFSTOPPED(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));
}

6.示例代码

示例 1:wait() 回收子进程

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main(void) {pid_t ret;int status;ret = fork();if (ret < 0) {perror("fork error");return -1;}if (ret == 0) {  // 子进程printf("Child: pid=%d, ppid=%d\n", getpid(), getppid());sleep(2);exit(5);   // 子进程退出码 5} else {      // 父进程printf("Parent: waiting child...\n");pid_t cpid = wait(&status);  // 阻塞等待printf("Parent: child pid=%d ended\n", cpid);if (WIFEXITED(status)) {printf("Child exited normally, return code=%d\n", WEXITSTATUS(status));} else {printf("Child exited abnormally\n");}}return 0;
}

现象:

示例 2:waitpid() 等待指定子进程

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main(void) {pid_t pid;int status;pid = fork();if (pid < 0) {perror("fork error");return -1;}if (pid == 0) {  // 子进程printf("Child running... pid=%d\n", getpid());sleep(3);exit(7);  // 子进程退出码 7} else {// 父进程printf("Parent waiting for child %d...\n", pid);pid_t wpid = waitpid(pid, &status, 0);  // 阻塞等待指定 pidprintf("Parent: child %d finished\n", wpid);if (WIFEXITED(status)) {printf("Child exit code=%d\n", WEXITSTATUS(status));}}return 0;
}

现象:

http://www.dtcms.com/a/503761.html

相关文章:

  • 网站域名注册证书是什么视频拍摄设备
  • 下载 LibreCAD 并创建个人分支的 Git 操作指南
  • 石家庄大型网站建站网站开发能从事那些职业
  • JavaScript性能优化实战大纲性能优化的核心目标
  • 【Android】布局优化:include、merge、ViewStub的使用及注意事项
  • PHP 桌面端框架NativePHP for Desktop v2 发布!
  • 第7章 muduo编程示例(2)
  • 哪里有放网站的免费空间无锡市政务服务网站建设项目
  • 为什么Unity修改过物体,物体的位移和旋转还是会被改变
  • Dify 平台从 x86_64 迁移至 ARM64 架构完整指南
  • 站嗨建站适合小白的室内设计软件
  • 基于车速预测的燃料电池混合动力汽车能量管理策略:一种自适应ECMS方法及其Python实现
  • [嵌入式系统-149]:新能源汽车的三电系统以及其功能、硬件架构、嵌入式操作系统
  • 在iStoreOS系统中安装Docker:从基础到高级应用的完整指南
  • 建设银行 网站查询密码杭州清风室内设计学校
  • 专业的个人网站建设商务网站建设与维护 ppt
  • 《Python红队攻防零基础脚本编写:入门篇(二)》
  • 装修设计网站免费婚庆网站建设策划案费用预算
  • 限制GIT提交大文件
  • STM32F103C8T6_IIC协议详解
  • 几个好用的在线网站南京网络营销
  • 乐学LangChain(1):总体架构和核心组件
  • CAN 总线物理层介绍
  • yolo介绍
  • 中国建站公司网站电线电话图怎么做
  • 新华路街道网站建设企业为什么要建设电子商务网站
  • UVa 1630 Folding
  • 基于AT89C52的智能温控风扇设计
  • 32HAL——IIC温度传感器AHT20
  • 站群wordpress宣武深圳网站建设公司