前言
在操作系统的世界里,"进程" 是一个贯穿始终的核心概念。无论是我们日常打开的浏览器、运行的代码,还是后台默默工作的服务,本质上都是一个个 "进程" 在操作系统的调度下有序运行。理解进程,是掌握操作系统工作机制、走进并发编程世界的第一步。
本文将从最基础的 "进程是什么" 讲起,带你逐层揭开进程的神秘面纱:从描述进程的核心数据结构 PCB(进程控制块),到 Linux 内核中具体的task_struct;从如何查看进程的标识符(PID)、父进程 ID(PPID),到通过ps命令和/proc文件系统窥探进程的实时状态;最终聚焦于进程创建的核心系统调用fork,解析它如何 "一分为二" 生成子进程,以及那些看似反直觉的返回值背后的底层逻辑。
无论你是刚接触操作系统的初学者,还是想夯实基础的开发者,这篇文章都将为你搭建起理解进程的 "知识骨架",为后续深入学习进程调度、通信、同步等内容铺好基石。
目录
[1. 基本概念](#1. 基本概念)
[1. 概念理解](#1. 概念理解)
[1.2 描述进程-PCB](#1.2 描述进程-PCB)
[1.3 task_ struct](#1.3 task_ struct)
[2. 进程查看](#2. 进程查看)
[2.2 ps 和/proc 获取进程信息](#2.2 ps 和/proc 获取进程信息)
[2.3 getppid()获取父进程pid](#2.3 getppid()获取父进程pid)
[3. 进程创建](#3. 进程创建)
[3.1 系统调用创建进程-fork](#3.1 系统调用创建进程-fork)
[3.2 fork的返回值](#3.2 fork的返回值)
1. 基本概念
1. 概念理解
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。

换个方式理解:
进程=内核数据结构对象+自己的代码和数据
Linux下:进程=PCB(task_struct)+代码和数据
对进程的管理就变成了对构建的数据结构进行增删查改。
1.2 描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct-PCB的一种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
进程的所有属性,就可以直接或者间接通过task_struct找到。
1.3 task_ struct
内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下⼀条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I∕O状态信息: 包括显示的I/O请求,分配给进程的I∕O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
cpp
struct task_struct {
volatile long state; // 进程状态(运行、睡眠等)
struct thread_info *thread_info; // 指向线程信息结构
pid_t pid; // 进程标识符
struct mm_struct *mm; // 指向内存描述符
struct mm_struct *active_mm; // 当前使用的内存描述符
struct list_head tasks; // 用于链接所有进程的双向循环链表节点 [^1]
struct sched_entity se; // 调度实体
unsigned int time_slice; // 时间片
// ... 其他字段省略
};
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。

2. 进程查看
我们历史上执行的所有指令,工具,自己的程序,运行起来,全部都是进程!!!
2.1getpid获取标识符
获取当前进程的唯一标识符(Process ID,简称 PID)。PID 是操作系统分配给每个正在运行的进程的一个正整数值,用于唯一标识和管理进程。

cpp
1#include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 int main(){
5 while(1){
6 sleep(1);
7 printf("我是一个进程!我的pid:%d \n",getpid());
8 }
9 return 0;
10 }

2.2 ps 和/proc 获取进程信息
ps aux:以用户为中心的详细进程快照
bash
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 168504 13080 ? Ss 08:00 0:02 /sbin/init
hu 12345 0.0 0.0 4320 720 pts/0 S+ 10:30 0:00 ./a.out
ps axj:以进程关系为中心的输出(包含进程组和会话信息)
ps axj
输出格式侧重进程间的关系,包含进程组 ID(PGID)、会话 ID(SID)、控制终端(TTY)等字段,适合分析进程的层级关系(如父子进程、进程组、会话)。
选项含义
a
:同ps aux
,显示所有用户的进程。x
:同ps aux
,显示无控制终端的进程。j
:以作业控制格式输出,增加进程组 ID(PGID)、会话 ID(SID)、控制终端 ID(TTY)等与进程关系相关的字段。
bash
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:02 /sbin/init
1234 5678 5678 5678 pts/0 5678 S+ 1000 0:00 ./a.out

ps axj | grep
是一个组合命令,用于在 ps axj
的输出中筛选包含特定关键词的进程信息。


kill - 9+进程pid 可以杀死进程,也可以用ctrl C
进程的信息可以通过 /proc 系统文件夹查看
/proc
是一个特殊的虚拟文件系统(procfs),它并非存储在磁盘上,而是动态反映系统内核和进程的实时状态。通过访问 /proc
下的文件和目录,你可以查看或修改内核参数、进程信息、硬件状态等。



进程启动,查看,着重关注cwd和exe文件 ,一般是在当前路径下生成可执行文件,cwd是当前路径。我们可以用chdir改变当前进程的工作目录。
改变进程的当前工作目录 :调用 chdir
后,进程后续的相对路径操作都将基于新的目录。
影响文件操作 :例如,若当前目录为 /home/hu
,执行 chdir("/tmp")
后,打开文件 test.txt
实际访问的是 /tmp/test.txt
。
2.3 getppid()获取父进程pid
每次重新启动进程 ,进程pid会变,但是父进程ID没变。


命令行解释器bash本身就是一个进程。
每次登录服务器时,操作系统会给每一个登录用户分配一个bash.

上面是bash打印的字符串,然后卡住等待,等待输入命令给bash
回想我们的程序,都可以先printf再scanf
3. 进程创建
3.1 系统调用创建进程-fork

cpp
#include <stdio.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 int main(){
6 printf("父进程开始执行,pid:%d\n",getpid());
7 fork();
8 printf("进程开始运行,pid:%d\n",getpid());
9 }
刚开始只有一个执行流,fork创建进程之后,有两个执行流,所以后面的printf会有两个,且结果id不一样。子进程执行父进程之后的代码。

在仅创建子进程时,子进程没有自己的代码和数据,因为目前,没有程序新加载。子进程执行父进程之后的代码。
3.2 fork的返回值

fork会有两个返回值。
子进程PID返回给父进程,0返回给子进程,失败的话-1返回给父进程
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(){
printf("父进程开始执行,pid:%d\n",getpid());
pid_t id=fork();
if(id<0){
perror("fork");
return 1;
}
else if(id==0){
//child
while(1){
sleep(1);
printf("我是一个子进程!我的pid:%d ,我的父进程pid:%d\n",getpid(),getppid());
}
}
else{
while(1){
sleep(1);
printf("我是一个父进程!我的pid:%d ,我的父进程pid:%d\n",getpid(),getppid());
}
}
// printf("进程开始运行,pid:%d\n",getpid());
return 0;
}
根据ID的判断执行了两个部分程序

不免产生一个疑惑?。
为什么fork给父子返回各自不同的返回值?
一个父进程可以有多个子进程,父:子=n:1;将子进程的pid返回给父进程方便父进程管理区分不同的子进程,用于标识新创建的子进程;
为什么一个函数会返回两次?
一个函数return xxx了,它的核心功能就完成了。fork创建子进程,申请新的pcb,拷贝父进程的pcb给子进程,子进程pcb放到进程列表中甚至放到调度队列中,return是条语句,是个函数,是共用的,最后父子进程都会执行return语句。
函数 "返回两次" 的本质:进程复制 + 指令指针共享
fork()
的核心是内核为当前进程创建了一个几乎完全相同的副本。
为什么一个变量id==0又>0? 导致 if 与else同时成立?(以后解释,当学习到虚拟地址空间会说明)
进程具有独立性,父子进程相互独立。父子进程的数据结构独立;代码是共享只读的,不可修改的;数据是写时拷贝,父子一方修改数据时,OS会把数据拷贝一份,目标进程修改这个拷贝。
cpp
#include <stdio.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 int val=520;
6 int main(){
7 printf("父进程开始执行,pid:%d\n",getpid());
8 pid_t id=fork();
9 if(id<0){
10 perror("fork");
11 return 1;
12 }
13 else if(id==0){
14 //child
15 while(1){
16 sleep(1);
17 printf("我是一个子进程!我的pid:%d ,我的父进程pid:%d,val:%d \n",getpid(),getppid(),val);
18 val+=10;
19 }
20
21 }
22 else{
23 while(1){
24 sleep(1);
25 printf("我是一个父进程!我的pid:%d ,我的父进程pid:%d,val:%d\n",getpid(),getppid(),val);
26 }
27
28 }
29 // printf("进程开始运行,pid:%d\n",getpid());
30 return 0;
31 }

结束语
到这里,我们已经走完了进程基础知识的探索之旅。从抽象的 "进程概念" 到具体task_struct结构体,从getpid、ps等工具的使用,到fork创建进程的底层逻辑,我们不仅认识了进程的 "外貌"(如何查看信息),更触摸到了它的 "骨架"(PCB 的核心作用)和 "诞生方式"(fork 的特殊机制)。
这些知识看似基础,却是理解操作系统并发能力的关键 ------ 毕竟,所有复杂的多任务场景,追根溯源都是一个个进程在 PCB 的 "记录" 下,通过调度器的协调有序运行的结果。
接下来,你可能会好奇:进程是如何被调度的?多个进程之间如何通信?fork创建的子进程为何能共享代码却拥有独立内存?这些问题,我们将在后续的内容中继续探索。