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

【Linux】深度解析Linux进程管理:从进程PCB到创建子进程的全景指南


文章目录

  • 前言
  • 一、从任务管理器到内核源码
  • 二、进程控制块(task_struct)的体系化设计
    • 2.1 先描述
    • 2.2 再组织
    • 2.3 Linux系统下的PCB设计
  • 三、PID的奥秘:从用户空间到内核空间
    • 3.1 PID及相关指令
    • 3.2 PPID父进程
  • 四、通过系统调用fork()函数创建子进程
    • 4.1 为什么要给父进程返回PID,给子进程返回0
    • 4.2 一个函数如何做到返回两次值的?
    • 4.3 一个变量pid_d id为什么会有两个不同的内容?
  • 总结


前言

在Linux操作系统中,进程管理是操作系统核心功能模块之一,其本质是通过地址空间抽象与动态资源调度实现多任务并发执行。本文将从进程控制块(task_struct)、内存管理单元(MMU)到调度算法(CFS)的实现机制展开分析,揭示操作系统如何通过精妙的数据结构与算法设计,构建高效可靠的进程管理体系。
承上启下:
上篇最后提到了操作系统是通过先描述,再组织的方式管理资源,同样的,操作系统也是以同样的方式进行进程的管理。


一、从任务管理器到内核源码

一个已经加载到内存中的程序,就叫做进程。
在Windows系统下,当我们在Windows中按下 Ctrl+Shift+Esc 调出任务管理器时,就可以看到有着各种各样的进程:

在这里插入图片描述
在Linux系统下,我们使用指令ps axj,也能查看到当前系统中的进程,top指令来查看当前正在运行的进程
在这里插入图片描述
在这里插入图片描述

二、进程控制块(task_struct)的体系化设计

在计算机开机时,只需要先将操作系统加载进内存,再由操作系统运行其他多个进程就完成了计算机的开机工作。但此时由于每个进程的状态都不一样,操作系统是如何分辨哪些是需要被立即执行的或是不执行的进程呢?所以操作系统必须将进程管理起来。
于是,就很顺利地利用到了上一章内容提到的先描述,再组织的管理方法!

2.1 先描述

任何一个进程,在加载到内存并形成真正的进程时,操作系统都要先创建描述进程的结构体对象—— PCB(process contrl block,进程控制块) 包含该进程属性集合(进程编号、进程状态、进程优先级)的结构体,再将代码与数据加载到内存中。

每个进程都拥有两个核心要素:

  1. 有进程资源(学生个体)
  2. 有进程PCB(校教务管理系统有该学生个体信息)

PCB结构体内包含相关指针,指向了个人代码数据块。因此,操作系统管理进程并不需要管理真正的代码和数据,操作系统管理进程只需通过PCB即可找到对应的个人代码和数据进行执行。

结论: 进程 = 内核PCB结构体对象 + 个人代码数据

举例:就像校园保安虽在学校(个体),但因为没有学籍档案(PCB),永远无法成为正式学生(进程)。这解释了为什么僵尸进程会占用系统资源——它们失去了PCB却未被清理

2.2 再组织

有了对每一个进程的描述PCB块,就只需要把这些块链接起来的方法,我们可以将这些PCB都想象成一个一个的节点,再由某种算法组织,这一结构是不是很像数据结构中的链表?事实上,操作系统在这一过程中,充斥着大量的数据结构,不仅仅是某种单链表,甚至会出现各种数据结构复用的场景(这个节点即是某个单链表的节点,又是某个二叉树的节点)。

struct LinkNode
{
	int data;
	struct LinkNode* next;
};

于是,在操作系统中对进程进行管理,就变成了对某种数据结构的增删查改的操作。所谓增删查改就意味着该进程是否将要被执行、或者是否结束进程,对于不同状态的进程存在着不同的 “ 队列 ” ,按序执行的进程PCB需要去对应队列排队等待执行。

但是在不同的平台上,其PCB的实现方式也不同,那么Linux系统是怎么做的呢?

2.3 Linux系统下的PCB设计

在Linux内核中,最基本的进程描述块采用 task_struct 的方式(PCB),并使用双向链表组织。

注意,操作系统中的某个PCB节点并不只有一种数据结构类型,一个PCB节点可能即属于某个双向链表的节点,同时也属于某个二叉树的节点(内部实现其实是引入不同的数据结构的结构体指针),链入不同的结构体组织中,会进入不同的算法,从而影响着不同的应用背景。

task_struct是Linux内核的一种数据结构,是PCB的一种,它会被装载到RAM(内存)里并且包含着进程的信息。

// 精简版task_struct(基于Linux 6.x内核)
struct task_struct {
    volatile long state;        // 进程状态(运行/睡眠/停止)
    void *stack;                // 内核栈指针
    pid_t pid;                  // 进程唯一ID
    struct mm_struct *mm;       // 内存管理信息
    struct files_struct *files; // 打开文件表
    struct list_head tasks;     // 进程链表节点
    // ...(更多字段见内核源码sched.h)
};

task_ struct更多内容分类

  • 标示符(PID等): 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等
  • 优先级: 相对于其他进程的优先级
  • 程序计数器(PC指针): 程序中即将被执行的下一条指令的地址
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
  • 其他信息(信号、网络等)

三、PID的奥秘:从用户空间到内核空间

Linux内核通过task_struct结构体实现进程的元数据管理,其核心字段包括:标识符体系:PID(进程唯一标识)、PPID(父进程ID)

3.1 PID及相关指令

每个程序拥有唯一的标识符PID,如何查看?

  • 方法一 ps pjx (使用grep与管道操作查询指定进程信息)
ps ajx | head -1 && ps ajx | grep proccess

grep本身也是一个过滤进程,所以也会被加载到进程中,并且它也有proc名称的任务
在这里插入图片描述
在这里插入图片描述

  • 方法二 ls /proc/[PID](查看指定PID的进程信息)
  1. exe(可执行文件存储地址)

在这里插入图片描述

  1. cwd(current work dir,当前工作目录)

在这里插入图片描述

c语言文件部分,我们知道fopen函数如果不指定路径,仅有文件名的情况下,会在当前目录下查找;touch指令创建一个文件时,也是默认在当前目录下创建,何为“当前目录”?
cwd存放的是当前进程的工作目录,操作(fopen、touch)默认都是在这个路径下展开的。

  • 补充

杀进程:kill -9 [PID]在这里插入图片描述循环查看进程
第一个系统调用接口:getpid()
man手册查看getpipd()函数介绍以及头文件包含

while :; do ps ajx | head -1 ; ps ajx | grep proc |grep - v grep ; echo "----------------------" ; sleep 1 ; done

3.2 PPID父进程

在这里插入图片描述

可以看到子进程的PID随着每次加载的过程也跟着变化,但父进程PPID不变,那么这个PPID是什么呢?
我们使用ps ajx | head -1; ps ajx | grep [PPID]查看一下COMMAND,我们发现是bash解释器进程。
在Linux当中,每次进入xshell环境,系统都会给分配命令行解释bash进程,作为后面所有可执行文件的父进程,换言之,所有的可执行文件的进程都是bash的子进程,执行过程中出现了问题只会影响到子进程。
在这里插入图片描述

四、通过系统调用fork()函数创建子进程

区别:
./ 创建子进程,指令层面

fork() 创建子进程,代码层面

  6 int main(){
  7     printf("进程,PID = %d, PPID = %d\n",getpid(),getppid());
  8     pid_t id = fork();
  9     if(id == 0){
 10         while(1)
 11         {
 12             printf("子进程:PID = %d, PPID = %d\n",
 13                     getpid(),getppid());
 14             sleep(1);
 15         }
 16     }else if(id > 0){
 17         while(1)
 18         {
 19             printf("父进程:PID = %d, PPID = %d\n",
 20                     getpid(),getppid());
 21             sleep(1);
 22         }
 23     }else{
 24         //
 25     }                                                         
 26     return 0;
 27 }

在这里插入图片描述

4.1 为什么要给父进程返回PID,给子进程返回0

返回不同的返回值,是为了让不同的执行流区分,以便执行不同的代码段。fork()执行完毕之后的代码共享。
在系统中,往往是单一父进程,诸多子进程的形式,父进程需要区分并控制子进程,所以需要给父进程返回子进程的pid,防止找不到属于自己的子进程;而子进程找父进程显然更容易,因此只需要返回0

4.2 一个函数如何做到返回两次值的?

从调用fork函数开始,就已经将子进程创建了出来(创建PCB、分配代码块与数据块),但此时fork函数还未执行到最后一行return语句,所以该行return语句会被父子进程执行两次,所以fork函数是可以返回两次甚至多次的返回值的。

4.3 一个变量pid_d id为什么会有两个不同的内容?

引入概念:在任何平台,进程在运行时是具有独立性的,a软件挂掉了不影响b软件的进程。

既然是独立的,那么父进程与子进程不会共用同一块数据(注意,只共享代码(以为代码一般不会被修改),但不能共享数据(防止互相被修改)),那么子进程的数据块从何而来呢?答案是将父进程的数据块给自己“拷贝”一份(但完全拷贝时,子进程会浪费父进程数据块资源,实际上刚开始的时候父子进程的数据块与代码块全是共享状态,只有当子进程需要修改数据块的数据时,系统会单独开辟一块空间用以存放子进程修改后的数据,而后子进程再次访问时,只会访问到自己修改的那一块数据而不会污染父进程原有的数据块。这一概念,被称之为 数据层面的写时拷贝),这样一来,父子进程的关系就是,代码块是共享的,数据块是割裂的。

所以在fork函数return返回值时,就已经发生了写时拷贝,所以 pid_t id的值在被父子进程分别调用时,看到的就是不同的值。

类比字符串的深浅拷贝:浅拷贝:两个指针指向同一块空间,深拷贝,两个指针指向不同空间,就是为了防止互相干扰。

至于接收返回值的pid_t id变量是如何存储两个值这个问题,会在后面的进程地址空间章节详细解答!


总结

本章涉及到的知识点比较多,具体指令验证细节并没有列出来,但文字上尽量做出了阐述说明,希望大家看完仍有所收获。
👍 ​感谢各位大佬观看。如果本文有帮助,请点赞收藏支持~

相关文章:

  • 常见的前端安全问题
  • 探索HTML5 Canvas:创造动态与交互性网页内容的强大工具
  • vim在连续多行行首插入相同的字符
  • 3.18学习总结java
  • 2025/3.17 郭院安排会议与南京银行参访
  • JMeter基本介绍
  • SpringCloud 学习笔记3(OpenFeign)
  • springboot实现调用百度ocr实现身份识别
  • 【实习经历Two:参与开源项目,学习并应用Git】
  • Ubuntu togo系统读写性能与原生系统测试
  • 【leetcode hot 100 124】二叉树中的最大路径和
  • OSG简介
  • 2025 ubuntu系统安装docker并迁移docker,docker安装到指定的目录以及文件迁移,docker迁移文件
  • 滚动元素的新api
  • 大模型GGUF和LLaMA的区别
  • 整体二分算法讲解及例题
  • vue中ref解析
  • 1.排序算法(学习自用)
  • 第13章贪心算法
  • 深度学习框架PyTorch——从入门到精通(6.1)自动微分
  • 民生访谈|支持外贸企业拓内销,上海正抓紧制定便利措施
  • 为治理商家“卷款跑路”“退卡难”,预付式消费司法解释5月起实施
  • 史学巨擘的思想地图与学术路径——王汎森解析梁启超、陈寅恪、傅斯年
  • 解放日报:持续拿出排头兵姿态先行者担当
  • 光明日报:回应辅警“转正”呼声,是一门政民互动公开课
  • 这座“蚌埠住了”的城市不仅会接流量,也在努力成为文旅实力派