云计算百科
云计算领域专业知识百科平台

进程与线程:进程基础

进程基础

1. 程序与进程

1.1 程序

1.1.1 程序的核心定义
  • 程序是存储在硬盘上的静态指令集合和数据集合,是完成特定任务的代码文件;
  • 程序仅占用磁盘资源,不占用 CPU、内存、进程 ID(PID)等系统运行资源;
  • 不同操作系统的可执行程序格式对比:
操作系统 / 平台典型可执行程序格式说明
Windows .exe(可执行文件) 双击可直接运行,是 Windows 最核心的可执行程序格式
Android .apk(安装包) 本质是 zip 压缩包,内含 dex 字节码、资源文件、清单文件等,安装后才能运行
Linux ELF(Executable and Linkable Format) Linux/Unix 类系统的标准二进制文件格式,是所有可执行程序 / 库文件的统一格式

易错点:

  • ✅ 在 Linux 中可执行文件的类型是 ELF 格式;
  • ❌ 在 Linux 中 ELF 就是可执行文件(ELF 包含多种文件类型,并非仅可执行文件)。
1.1.2 ELF 格式文件类型

ELF 格式包含 4 类核心文件类型:

  • 可执行文件:可被直接运行的文件;
  • 可重定位文件:编译器编译过程中生成的中间文件(后缀为 .o/.a);
  • 共享目标文件:可被多个程序共享调用的库文件(Linux 下的 .so 文件);
  • 核心转储文件:程序崩溃时生成的二进制文件。
  • 实用命令:readelf -h ELF格式文件 → 查看文件的 ELF 头信息。

    1.1.3 ELF 格式文件组成

    ELF 文件由以下核心部分构成:

    • ELF 头:位于文件最开始,记录文件基本信息(文件类型、架构、入口地址、程序头表 / 节区头表的位置和大小);
    • 程序头表:描述文件中的段信息,供加载器将文件加载到内存时使用;
    • 节区:文件核心内容,包含代码(.text)、数据(.data、.bss)、符号表(.symtab)、重定位信息等;
    • 节区头表:记录每个节区的名称、类型、大小、文件偏移量等,用于链接过程;
    • 段:由一个或多个节区组成,是加载到内存的基本单元。

    1.2 进程

    1.2.1 进程的核心定义
    • 进程是程序的一次动态执行过程,占用 CPU、内存等系统运行资源,有完整的生命周期;
    • 一个程序可以对应多个进程(如多次启动同一个软件);
    • 一个进程在运行过程中,通常只对应一个程序。
    1.2.2 程序加载为进程的核心步骤
  • 用户发起程序执行请求;
  • 内核创建进程控制块(PCB);
  • 为进程分配内存空间;
  • 加载程序运行所需的依赖资源;
  • 设置程序入口地址与运行上下文;
  • CPU 调度算法选中该进程,分配 CPU 时间片执行;
  • 进程终止后,内核回收其占用的系统资源。
  • 2. 进程相关核心信息与工具

    2.1 task_struct 结构体

    • 进程控制块(PCB)的核心实现,存储进程的所有核心信息(PID、状态、内存地址、优先级等);
    • 存储路径:/usr/src/linux-headers-4.10.0-28-generic/include/linux/sched.h。

    2.2 Linux 中查看进程的命令

    命令说明
    ps -ef / ps aux / ps axjf 静态查看进程列表(不同参数展示维度不同)
    top 动态实时监控进程资源占用情况
    htop 增强版 top(需手动安装:sudo apt-get install htop)
    pstree 以树形结构展示进程间的父子关系

    2.3 进程的父子关系

    • Linux 中除初始化进程外,其余所有进程都有父进程;
    • 若父进程先退出,子进程会被其他进程(通常是 init/systemd 进程)接管,避免成为 “孤儿进程”。

    3. 进程状态

    3.1 进程的生命周期(状态转换)

    3.1.1 诞生 → 就绪态
    • 触发条件:父进程调用 fork 函数,内核为新进程分配 PID、PCB 等资源,新进程进入到就绪态;
    • 就绪队列:进程已具备执行条件,但未被 CPU 调度,会在就绪队列中等待。
    3.1.2 就绪态 → 执行态
    • 触发条件:内核调用调度(sched)算法,选中就绪队列中的进程,为其分配 CPU 时间片,进程进入执行态。
    3.1.3 执行态 → 就绪态

    触发以下任一条件,进程会从执行态回到就绪态:

  • 进程执行时间耗尽 CPU 时间片;
  • 更高优先级的进程进入就绪队列时,CPU会优先去执行高优先级进程,当前进程被抢占。
  • 3.1.4 执行态 → 睡眠态
    • 触发条件:进程因等待资源 / 事件主动放弃 CPU(如读文件、网络请求);
    • 可中断睡眠(睡眠态):等待键盘输入等场景,可被信号唤醒,回到就绪态;
    • 不可中断睡眠(挂起态):等待磁盘 IO 完成(如写入大文件),不响应信号(避免 IO 中断导致数据损坏),IO 完成后自动回到就绪态。
    3.1.5 执行态 → 暂停态
    • 触发条件:进程收到 SIGSTOP(暂停)、SIGTSTP(终端暂停)信号。
    3.1.6 终止 → 僵尸态 → 死亡态
  • 进程通过以下 4 种方式终止,内核调用 do_exit 清理进程资源,但保留 PCB,进程进入僵尸态:
    • 在 main 函数内执行 return
    • 调用 exit() / Exit() / _exit() 函数
    • 进程中最后一个线程执行 pthread_exit()
    • 被信号(如 SIGKILL、SIGTERM)杀死
  • 父进程调用 wait / waitpid 函数,读取僵尸态进程的退出状态;
  • 内核释放该进程的 PCB 资源,进程进入死亡态。
  • 3.2 进程的状态转换图

    在这里插入图片描述

    4. fork函数

    4.1 作用

    • 用于产生一个新的子进程

    4.2 接口规范

    项目内容
    功能 创建一个新的子进程
    头文件 #include <unistd.h>
    函数原型 pid_t fork(void);
    返回值 成功:父进程返回子进程的 PID(大于 0 的正整数),子进程返回 0失败:返回 -1
    备注 函数执行成功后,会分别在父子两个进程中返回,返回值不同以区分身份

    4.3 调用触发流程

    • 为子进程分配唯一的PID,并将子进程的PPID(父进程 ID)设置为当前父进程的 PID。
    • 复制父进程的进程控制块(PCB),包含进程状态、优先级、文件描述符、信号掩码等核心信息。
    • 基于写时复制机制,建立父子进程的内存映射关系(非立即拷贝物理内存,只有当其中一方修改数据时才会实际复制,以提升效率)。
    • 将子进程加入就绪队列,等待 CPU 调度执行。

    4.4 两次返回特性

    • 在父进程中,返回子进程的PID,父进程可以通过这个返回值来管理子进程。
    • 在子进程中,返回0,子进程可以通过 getpid() 获取自身 PID,通过 getppid() 获取父进程 PID。
    • 如何fork函数调用失败,则返回-1。

    4.5 父子进程的异同点

    4.5.1 完全相同的属性(子进程是父进程的 “复制品”,在子进程创建之初时)
    • 实际 UID 和 GID,以及有效 UID 和 GID
    • 所有环境变量
    • 进程组 ID 和会话 ID
    • 当前工作路径(除非用 chdir() 修改)
    • 打开的文件描述符
    • 信号响应函数
    • 整个内存空间(栈、堆、数据段、代码段、标准 IO 缓冲区等)
    4.5.2 不同的属性
    • 进程号(PID):父子进程 PID 唯一,是进程的 “身份证号”。
    • 记录锁:父进程对文件加的锁,子进程不会继承。
    • 挂起的信号:父进程中 “悬而未决” 的信号,子进程不会继承。

    4.6 核心注意事项

  • 子进程的执行起点:子进程会从 fork() 返回值后的下一条逻辑语句开始运行,避免了无限调用 fork() 产生 “无限子孙” 的问题。
  • 并发执行的随机性:父子进程是相互平等的,执行次序是随机的(由内核调度算法决定),除非使用同步机制(如信号、管道),否则无法判断谁先运行。
  • 内存空间的独立性:子进程完整复制了父进程的内存空间,二者的内存是相互独立的,修改各自的数据不会影响对方。
  • 4.7 代码演示

    4.7.1 示例代码

    #include <stdio.h>
    #include <unistd.h>
    #include <errno.h>
    #include <stdlib.h>

    // 全局变量
    int g_num2 = 123;

    // 静态全局变量
    static int num4 = 100;

    int main()
    {
    // 局部变量
    int num1 = 10;

    // 静态局部变量
    static int num3 = 111;

    // 堆区
    int* num5 = (int*)malloc(4);
    *num5 = 1000;

    pid_t pid = fork();
    if (pid < 0)
    {
    perror("fork fail");
    return 1;
    }

    // 子进程
    if (pid == 0)
    {
    num1++;
    printf("num1=%d, &num1 = %p\\n",num1, &num1);
    g_num2++;
    printf("g_num2=%d, &g_num2 = %p\\n",g_num2, &g_num2);
    num3++;
    printf("num3=%d, &num3 = %p\\n",num3, &num3);
    num4++;
    printf("num4=%d, &num4 = %p\\n",num4, &num4);
    (*num5)++;
    printf("*num5=%d, num5 = %p, &num5 = %p\\n",*num5, num5, &num5);
    printf("我是子进程,pid=%d,ppid=%d\\n",getpid(),getppid());
    }

    // 父进程
    if (pid > 0)
    {
    sleep(1);
    printf("num1=%d, &num1 = %p\\n",num1, &num1);
    printf("g_num2=%d, &g_num2 = %p\\n",g_num2, &g_num2);
    printf("num3=%d, &num3 = %p\\n",num3, &num3);
    printf("num4=%d, &num4 = %p\\n",num4, &num4);
    printf("*num5=%d, num5 = %p, &num5 = %p\\n",*num5, num5, &num5);
    printf("我是父进程,pid=%d,ppid=%d\\n",getpid(),getppid());
    }

    while(1)
    {

    }
    }

    4.7.2 运行结果

    在这里插入图片描述

    4.7.3 结论
    • 子进程创建时,会完整拷贝父进程的栈数据、堆数据、全局数据、静态数据及代码段;
    • 父子进程的内存空间相互独立,修改各自数据不会影响对方;
    • 父子进程中变量的虚拟内存地址可以相同,但物理内存地址不同;
    • 虚拟内存与物理内存的映射规则:
    • 每个进程拥有独立的虚拟内存空间,代码中访问的变量地址均为虚拟地址,而非物理内存;
    • 写时复制机制:子进程创建后,变量在未被修改时(未进行写操作),通常父子进程共享一份物理内存的映射表,这个时候,父子进程中变量对应的实际物理内存是相同的;如果其中一方需要改变数据(进行写操作),这个时候操作系统才会真正的改变物理内存,但是其虚拟内存地址依旧是相同的。
    • 为保证父子进程代码执行逻辑一致,二者的虚拟内存地址设计为相同。

    4.8 fork和vfork的区别

    对比维度fork 函数vfork 函数
    内存分配机制 写时复制:父子进程共享代码段,数据段 / 堆栈等仅在修改时拷贝,初始共享物理内存 完全共享地址空间:父子进程共用一套数据段和堆栈,无写时复制,子进程修改数据直接影响父进程
    执行顺序 父子进程执行次序随机,由操作系统内核调度算法决定 强制子进程优先执行,父进程被阻塞,直到子进程调用 exec 函数簇或 _exit 退出
    设计目的 适用于子进程独立执行父进程代码,或后续执行任意操作 专为子进程创建后立即调用 exec 设计,减少内存复制开销,提升执行效率
    安全性 高:父子进程地址空间相互隔离,数据修改互不影响 低:子进程修改数据会覆盖父进程数据;若子进程未及时调用 exec/_exit,易引发程序异常
    返回值规则 成功:父进程返回子进程 PID,子进程返回 0;失败:返回 -1 与 fork 完全一致
    子进程退出要求 可调用 exit() 或 _exit() 退出 必须调用 _exit() 退出,禁止调用 exit()(否则会刷新父进程 IO 缓冲区,导致数据混乱)

    5. exec函数簇

    5.1 接口规范

    功能:在进程中加载新的程序文件或者脚本,覆盖原有代码,重新运行

    项目详情
    头文件 #include <unistd.h>
    原型 int execl(const char *path, const char *arg, …); int execv(const char *path, char *const argv[]); int execle(const char *path, const char *arg, …, char *const envp[]); int execlp(const char *file, const char *arg, …); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);
    参数 path:即将被加载执行的 ELF 文件或脚本的路径file:即将被加载执行的 ELF 文件或脚本的名字arg:以列表方式罗列的 ELF 文件或脚本的参数argv:以数组方式组织的 ELF 文件或脚本的参数envp:用户自定义的环境变量数组
    返回值 成功:不返回失败:-1
    备注 1,函数名字带字母 l 意味着其参数以列表(list)的方式提供。2,函数名字带字母 v 意味着其参数以矢量(vector)数组的方式提供。3,函数名字带字母 p 意味着会利用环境变量 PATH 来找寻指定的执行文件。4,函数名字带字母 e 意味着用户提供自定义的环境变量。

    5.2 参数格式要求

    被加载的文件的参数列表必须以自身名字为开始,以 NULL 为结尾。

    例如,加载当前目录下 a.out 并传参 "abcd":

    execl("./a.out", "a.out", "abcd", NULL);

    或用数组方式:

    const char *argv[3] = {"a.out", "abcd", NULL};
    execv("./a.out", argv);

    5.3 执行后行为

    exec 函数族执行成功后,原有程序代码会被新文件 / 脚本完全覆盖,后续代码不会执行,且函数不会返回。

    只有在执行失败时,才会返回 -1,继续执行后续代码。

    6. exit和_exit区别

    6.1 exit执行流程

    • 1、调用atexit注册退出处理函数。
    • 2、刷新并关闭所有打开的标准IO流。
    • 3、删除创建的临时文件(tmpfile函数)。
    • 4、调用exit进入内核态,进程终止,同时将status(子进程的退出值)传递给父进程。

    6.2 _exit执行流程:

    • 1、直接触发内核态的系统调用,跳过所有用户态下的清理步骤。
    • 2、内核负责关闭进程所有打开的文件描述符、释放内存资源等。
    • 3、向父进程发送信号,传递status值。

    6.3 函数exit和_exit的接口规范

    类别_exit() 函数exit() 函数
    核心功能 退出本进程 退出本进程
    头文件 #include <unistd.h> #include <stdlib.h>
    函数原型 void _exit(int status); void exit(int status);
    参数 status:子进程的退出值 status:子进程的退出值
    返回值 不返回 不返回
    备注1 子进程正常退出,status 一般为 0; 子进程正常退出,status 一般为 0;
    备注2 子进程异常退出,status 一般为非 0; 子进程异常退出,status 一般为非 0;
    备注3 直接退出,不冲洗标准 IO 残留数据到内核,也不执行 “退出处理函数” 退出时自动冲洗(flush)标准 IO 中残留的数据到内核;若进程注册了 “退出处理函数”,会自动执行这些函数后再退出

    7. wait和waitpid区别

    7.1 区别

    waitwaitpid
    等待任意一个子进程的退出,不可指定。 可以获取指定子进程的退出状态。
    强制阻塞,直到有子进程退出才返回。 可以通过第三个形参来设置非阻塞。

    7.2 函数wait和waitpid的接口规范

    功能:等待子进程退出,获取其退出状态;可将子进程状态切换为 EXIT_DEAD,以便系统释放资源

    项目详情
    头文件 #include <sys/wait.h>
    原型 pid_t wait(int *stat_loc);``pid_t waitpid(pid_t pid, int *stat_loc, int options);
    参数pid: ①小于 – 1:等待组 ID 的绝对值为 pid 的进程组中的任一子进程 ②-1:等待任一子进程 ③0:等待调用者所在进程组中的任一子进程 ④大于 0:等待进程组 ID 为 pid 的子进程
    参数stat_loc: 子进程退出状态
    参数option: WCONTINUED:报告任一从暂停态出来且从未报告过的子进程的状态 WNOHANG:非阻塞等待 WUNTRACED:报告任一当前处于暂停态且从未报告过的子进程的状态
    返回值wait(): 成功:退出的子进程PID 失败:-1
    返回值waitpid(): 成功:状态发生改变的子进程 PID(如果 WNOHANG 设置,且由 pid 指定的进程存在但状态未发生改变,则返回 0) 失败:-1
    备注 如果不需要获取子进程的退出状态,stat_loc 可以设置为 NULL

    7.3 子进程退出状态解析宏

    下表中的宏用于解析通过 wait()/waitpid() 获取到的子进程退出状态:

    宏含义
    WIFEXITED(status) 如果子进程正常退出,则该宏为真。
    WEXITSTATUS(status) 如果子进程正常退出,则该宏将获取子进程的退出值。
    WIFSIGNALED(status) 如果子进程被信号杀死,则该宏为真。
    WTERMSIG(status) 如果子进程被信号杀死,则该宏将获取导致他死亡的信号值。
    WCOREDUMP(status) 如果子进程被信号杀死且生成核心转储文件(core dump),则该宏为真。(注:该选项在某些 Unix 系统中无效,比如 AIX、sunOS)
    WIFSTOPPED(status) 如果子进程的被信号暂停,且 option 中 WUNTRACED 已经被设置时,则该宏为真。
    WSTOPSIG(status) 如果 WIFSTOPPED(status) 为真,则该宏将获取导致子进程暂停的信号值。
    WIFCONTINUED(status) 如果子进程被信号 SIGCONT 重新置为就绪态,该宏为真。

    7.4 重点

  • 退出状态 vs 退出值

    • 退出状态是一个包含更多信息的整数值,而退出值只是子进程通过 exit()/return 返回的具体数值。
    • 要想拿到退出值,必须先用 WIFEXITED() 判断子进程是否正常退出,再用 WEXITSTATUS() 提取。
  • 核心转储文件

    WCOREDUMP() 宏用于判断子进程是否在被信号杀死时生成了 core dump 文件,但它不是 POSIX 标准,在 AIX、sunOS 等系统中无效。

  • 非阻塞等待

    waitpid() 的 WNOHANG 选项可以让函数立即返回,避免父进程被阻塞,适合需要同时处理其他任务的场景。

  • 8. 进程组、对话期、终端

    8.1 进程组

    • 1、由一个或多个共享进程组ID(PGID)的进程组成的集合
    • 2、创建进程组的进程其PID = PGID,即使组长进程终止,进程组依然存在,直到组内所有进程全部结束。
    • 3、一个进程只能属于一个进程组。

    8.2 对话期(会话期)

    • 1、一个或多个进程组的集合,会话由首进程创建,会话内所有进程共享一个控制终端。
    • 2、创建会话的进程,其PID = 会话ID(SID),该进程不能是其他进程组的组长。
    • 3、一个会话包含多个进程组,进程组又分为前台进程组和后台进程组。
      • ①:前台进程组与控制终端关连,能够接收终端输入和信号。
      • ②:后台进程组不与控制终端直接交互,通常不受终端信号影响。
    • 4、只有非进程组组长的进程(组员进程)才能创建新会话,如果创建会话的进程是组长则创建失败。
    • 5、一个会话最多只能绑定一个控制终端,它俩是一对一的关系。

    8.3 终端

    • 1、在Linux中的终端其实就是一个字符设备文件,作为进程与用户交互的接口,分为控制终端和非控制终端。
    • 2、一个会话最多关联一个控制终端,由会话首进程打开。
    • 3、控制终端的进程组ID指向当前前台进程组,终端输入的信号会发送给前台进程组。
    • 4、当关闭控制终端时,内核会向前台进程组发送信号(SIGHUP)。
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 进程与线程:进程基础
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!