程序是保存指令和数据的文件,进程是程序的一次运行实例。要观察一个进程,需要先认识它的标识,再理解父子关系及进程创建。
本文围绕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/cwdexe:指向进程对应的可执行文件。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。
- 根据返回值执行不同分支,不等于一个进程同时进入两个分支。
- 父子执行先后没有固定保证,普通变量的修改互不影响。
进程状态、退出后的资源回收及等待机制,可以在这些概念之后继续整理。