文章目录
- 前言
- 一、从任务管理器到内核源码
- 二、进程控制块(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,进程控制块) 包含该进程属性集合(进程编号、进程状态、进程优先级)的结构体,再将代码与数据加载到内存中。
每个进程都拥有两个核心要素:
- 有进程资源(学生个体)
- 有进程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; // 进程唯一IDstruct 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的进程信息)
- exe(可执行文件存储地址)
- 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变量是如何存储两个值这个问题,会在后面的进程地址空间章节详细解答!
总结
本章涉及到的知识点比较多,具体指令验证细节并没有列出来,但文字上尽量做出了阐述说明,希望大家看完仍有所收获。
👍 感谢各位大佬观看。如果本文有帮助,请点赞收藏支持~