当前位置: 首页 > news >正文

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

前言

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

目录

1. 基本概念

1. 概念理解

1.2 描述进程-PCB

1.3 task_ struct

2. 进程查看

2.1getpid获取标识符

2.2 ps 和/proc 获取进程信息

2.3  getppid()获取父进程pid

3. 进程创建

3.1 系统调用创建进程-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设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
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 是操作系统分配给每个正在运行的进程的一个正整数值,用于唯一标识和管理进程。 

  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:以用户为中心的详细进程快照

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)等与进程关系相关的字段。
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND0     1     1     1 ?           -1 Ss       0   0:02 /sbin/init1234  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

#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返回给父进程 

#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){//childwhile(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会把数据拷贝一份,目标进程修改这个拷贝。

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

http://www.lryc.cn/news/588777.html

相关文章:

  • Isaac Sim仿真赋能机器人工作流,推动具身智能在机器人领域研究
  • Web前端入门:JavaScript 由程序触发绑定事件的几种方式
  • redisson 设置了过期时间,会自动续期吗
  • [ctfshow web入门]web98 学习PHP的引用
  • LeetCode 3136.有效单词:遍历模拟
  • 21.映射字典的值
  • 连接new服务器注意事项
  • 非实时的防控场景
  • LLM面试题及讲解 4
  • 【Tauri】Tauri 2.x+Vue自动更新教程对接后端接口更新
  • C++--哈希
  • CPU寄存器、进程上下文与Linux O(1)调度器原理
  • 知识付费小程序资质全解析
  • 进程的内存映像,只读区,可读写区,堆,共享库,栈详解
  • 【机器学习基础【5】】Python数据科学三件套:从数据创建到处理再到可视化实战
  • 链表的 哑结点的本质
  • 排序算法实战(上)
  • 经典排序算法之希尔排序
  • 解锁Python爬虫:数据获取与清洗的进阶指南
  • 深入浅出 RabbitMQ-核心概念介绍与容器化部署
  • Zabbix钉钉告警
  • 如何将华为文件传输到电脑
  • C++ - 仿 RabbitMQ 实现消息队列--muduo快速上手
  • 每日钉钉API探索:chooseUserFromList灵活选取自定义联系人
  • 变更缓冲池简介
  • Git分支管理与工作流详解
  • STL的一些知识点
  • Java-特殊文件、日志技术
  • RTDETR融合CFFormer中的FeatureCorrection_s2c模块
  • 下一代防火墙-web防护