进程探秘:从 PCB 到 fork 的核心原理之旅

前言

在操作系统的世界里,"进程" 是一个贯穿始终的核心概念。无论是我们日常打开的浏览器、运行的代码,还是后台默默工作的服务,本质上都是一个个 "进程" 在操作系统的调度下有序运行。理解进程,是掌握操作系统工作机制、走进并发编程世界的第一步。

本文将从最基础的 "进程是什么" 讲起,带你逐层揭开进程的神秘面纱:从描述进程的核心数据结构 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.1getpid获取标识符

[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创建的子进程为何能共享代码却拥有独立内存?这些问题,我们将在后续的内容中继续探索。

相关推荐
A小辣椒5 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒9 小时前
TShark:基础知识
linux
AlfredZhao11 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式