☰
Linux(13):进程初识——PID、PPID 与 fork 创建子进程
2026/10/16 9:36:56 网站建设 项目流程

程序是保存指令和数据的文件,进程是程序的一次运行实例。要观察一个进程,需要先认识它的标识,再理解父子关系及进程创建。

本文围绕ps、getpid()、getppid()、/proc和fork()整理基础用法,最后用全局变量说明父子进程的数据独立性,暂不展开进程状态、等待和程序替换。

一、PID 与 PPID

PID 是进程 ID,用于标识进程;PPID 是父进程的 PID,用于表达父子关系。

名称含义C 接口
PID当前进程的标识getpid()
PPID当前父进程的标识getppid()

在普通的终端环境中,通过 Bash 启动一个外部程序时,新进程的父进程通常是这个 Shell。Bash 本身也在作为进程运行。不过,Shell 内建命令不一定创建新进程,不能把每次输入命令都等同于创建进程。

同一个程序多次启动,得到的 PID 可能不同。PID 也不是永久编号:进程结束后,其编号可以被后续进程重新使用。

二、用代码查看进程标识

下面的程序每秒打印一次 PID 与 PPID,方便在另一个终端观察:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(void){while(1){printf("PID=%ld, PPID=%ld\n",(long)getpid(),(long)getppid());fflush(stdout);sleep(1);}}

保存为pid_demo.c,编译并运行:

gcc-Wall-Wextrapid_demo.c-opid_demo ./pid_demo

接口返回值类型是pid_t。这里转换为long后使用%ld输出,避免直接假定它一定适合%d。

一次运行期间,只要进程仍然存活,其 PID 不会因为循环打印而变化。PPID 表示当前的父进程关系;父进程退出后可能发生重新托管,因此不能认为 PPID 永远不变。

接口定义可参考 Linux man-pages:getpid、getppid。

三、用 ps 对照程序输出

保持程序运行,在同一台 Linux 主机的另一个终端执行:

psaxj

这类输出包含 PID、PPID 等信息。为了减少干扰,可以按程序名筛选:

psaxj|grep'[p]id_demo'

[p]id_demo仍然可以匹配pid_demo,同时通常避免把 grep 自身的命令行匹配出来。

也可以按程序打印出的 PID 精确查看。例如 PID 是 22576 时:

ps-p22576-opid,ppid,comm

上面的数字只是示例,需要换成实际 PID。程序输出与ps中的对应字段应当一致;如果进程已经结束,则可能查不到对应记录。

四、通过 /proc 查看 exe 与 cwd

/proc是 Linux 提供进程等运行信息的虚拟文件系统,不是普通的磁盘数据目录。运行中的进程通常有对应的/proc/PID目录。

继续使用实际 PID,可以查看:

ls-l/proc/22576/exe /proc/22576/cwd
  • exe:指向进程对应的可执行文件。
  • cwd:指向进程当前的工作目录。

可执行文件所在目录与当前工作目录不一定相同。程序使用相对路径打开文件时,通常以自己的工作目录为基准,而不是自动以可执行文件所在目录为基准。

访问这些信息可能受到权限限制;进程退出后,对应目录也会消失。查看自己的进程通常不需要 root 权限,但不能据此认为所有进程信息都可随意读取。

五、fork:创建一个子进程

fork()根据调用它的进程创建子进程:调用者成为父进程,新创建的进程是子进程。

#include<unistd.h>pid_tfork(void);

创建成功后,父子进程都从fork()返回的位置继续执行,并不会让子进程重新从main的第一行开始运行。

1. 一次调用,两条执行路径

情况fork 返回值后续逻辑
创建失败父进程中为-1处理错误,没有子进程产生
创建成功,父进程子进程的 PID,通常大于 0执行父进程分支
创建成功,子进程0执行子进程分支

成功时的两次返回,发生在两个不同进程中,不是在一个进程里把同一个变量先改成 0、再改成正数。返回值规则可参考 Linux man-pages:fork。

父进程得到子进程的 PID,便于识别子进程;子进程也可以用getppid()获取父进程的 PID。子进程中的 fork 返回值 0,不代表子进程自己的 PID 是 0。

2. 用返回值区分父子进程

下面的练习只调用一次fork()。为了让现象更清楚,子进程只修改一次全局变量,随后父子进程持续打印各自的信息:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intgval=100;intmain(void){printf("before fork: PID=%ld\n",(long)getpid());fflush(stdout);pid_tid=fork();if(id<0){perror("fork");return1;}if(id==0){gval+=10;while(1){printf("child: PID=%ld, PPID=%ld, gval=%d\n",(long)getpid(),(long)getppid(),gval);fflush(stdout);sleep(1);}}else{while(1){printf("parent: PID=%ld, child PID=%ld, gval=%d\n",(long)getpid(),(long)id,gval);fflush(stdout);sleep(1);}}}

保存为fork_demo.c:

gcc-Wall-Wextrafork_demo.c-ofork_demo ./fork_demo

在常见的交互式 Shell 前台运行方式下,按 Ctrl+C 通常向前台进程组发送 SIGINT。本例没有更改进程组或信号处理,因此通常可以同时结束父子进程;它不是对任何环境都成立的“结束所有子进程”指令。

练习中不要把fork()放入无限循环,否则可能持续创建大量进程。

六、怎样理解输出

在父子进程都存活的情况下,预期可以观察到:

  • fork 前的输出执行一次;示例提前刷新缓冲区,避免遗留输出被带入子进程。
  • 父子进程拥有不同的 PID。
  • 子进程的 PPID 对应父进程的 PID。
  • 父进程中的id对应子进程的 PID。
  • 子进程的gval变为 110,父进程的gval仍为 100。

父子进程哪一个先输出,由实际调度决定。不能根据代码书写顺序推断父进程一定先运行,也不能把交替打印理解为严格的一人一次。

以上是根据代码分析得到的预期现象,不是某次运行日志。实际 PID、输出顺序和节奏以运行环境为准。

七、为什么修改全局变量不会影响父进程

fork 后,父子进程具有各自的地址空间。子进程继承父进程原有的普通内存内容,但它修改自己的gval,不会把父进程中的同名变量一起改掉。

Linux 使用写时复制等机制实现 fork,因此不能简单理解为创建时立即把所有物理内存完整复制一遍。入门阶段先记住:初始内容可以相同,普通变量的后续修改彼此独立。

这里的结论针对普通进程内存,不表示父子进程之间的所有资源都完全没有联系。共享内存、文件等资源的具体行为需要分别讨论,本篇不展开。

八、复习要点

  • PID 标识进程,PPID 表示当前父进程的标识。
  • 用程序输出、ps和/proc可以从不同角度观察同一个进程。
  • fork 成功后,父子进程都从调用返回处继续执行。
  • 父进程收到子进程 PID,子进程收到 0;失败时返回 -1。
  • 根据返回值执行不同分支,不等于一个进程同时进入两个分支。
  • 父子执行先后没有固定保证,普通变量的修改互不影响。

进程状态、退出后的资源回收及等待机制,可以在这些概念之后继续整理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询