目录
📖1.什么是进程?
📖2.自己写一个进程
📖3.操作系统与内存的关系
📖4.PCB(操作系统对进程的管理)
📖5.真正进程的组成
📖6.形成进程的过程
📖7、Linux环境下的进程知识
7.1 task_struct
7.2 task_struct内容分类标识符:描述一个进程的唯一标识符,用来区分不同进程。
7.3 操作系统如何组织进程
7.4 查看进程属性(先学会命令即可,具体用法下面讲)
7.5 proc目录
📖8.什么是进程PID
📖9.通过系统调用接口查看进程PID
📖10.父进程PPID
📖11.fork函数 (也叫做fork系统接口)
📖12.fork函数如何实现
📖13.最后来理解bash
📖14.完结
我从不放下,我偏要偏执!
📖1.什么是进程?
上一篇文章中,我们提到过任何一个程序想要运行,必须先加载到内存中。
一个已经加载到内存中的程序并正在由CPU开始运行,就叫进程。
在Windows操作系统下,我们可以通过任务管理器来查看计算机当前正在运行的进程。
在Linux操作系统下,可以通过下面的指令来查看当前正在运行的进程信息
ps axj
top
//这两条指令都可以查看进程
📖2.自己写一个进程
我们上面提到过,进程就是加载到内存中并正在运行的程序,因此我们可以自己写一个.c源代码,通过编译来获得一个正在运行的程序
//process1.c
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process\n");
sleep(1);
}
return 0;
}
我们写的这个源代码是存储在磁盘上的,编译得到的可执行程序也是存储在磁盘上的,只有通过./mycode
去执行 mycode 这个可执行文件,才能把它加载到内存中。加载到内存之后并由CPU开始运行,此时就会得到一个名为 mycode 的进程,可以通过下面这条指令来查看该进程的属性信息
ps axj | grep 进程名称
//ps axj 是查看当前的所有进程
//通过管道和grep将mycode进程的信息过滤出来,方便查找
可以发现有两个pro.exe,但是这里我们先不用管第一个,我们现在只需要知道,第一个的pro.exe是我们这个进程的属性信息即可
结束当前进程使用ctrl+c终止进程
📖3.操作系统与内存的关系
我们写的源代码,最终一定是由CPU来运行,而CPU只能从内存中拿数据,这就决定了我们的可执行程序和代码一定要先加载到内存当中。
此外,CPU作为计算机中的大脑去传达命令控制一切,又因为CPU只能与内存打交道,CPU为了控制一切,必须能让CPU控制操作系统,因为操作系统可以管理软硬件干什么,这就导致操作系统必须在内存中(因此,操作系统是位于内存中的)
简单回顾一下
操作系统->: 管理每一个软硬件该干什么
CPU->: 下达命令,处理数据
操作系统的内存管理包含将内存中的部分不常用数据转移到磁盘中,因为内存是高速临时存储,它在断电后可能丢失数据,而硬盘是低速持久存储,断电后数据保留
小Tips:一个可执行程序本质上就是一个二进制文件,我们将一个可执行程序加载到内存,本质上就是将一个二进制文件加载到内存,这个二进制文件是由代码和数据两部分组成,无论是数据还是代码归根结底都可以被叫做数据,代码最终交给控制器去执行,数据最终交给运算器去运行。
一个计算机可以同时存在多个进程,给用户的主观感受就是,可以同时使用多个软件,即在我们的计算机上,聊天、听歌、打游戏可以同时进行。现在我们把这种可以同时运行多个进程的操作系统叫做多道操作系统。
📖4.PCB(操作系统对进程的管理)
生成PCB的过程被定义为操作系统对进程的管理
任何可执行程序,在加载到内存前,操作系统要先创建PCB,即先描述出一个进程(可执行程序)的结构体。然后每一个进程创建一个结构体对象,这个结构体对象中存储了进程的各类基本信息(但是不包括可执行程序的代码和数据),这个结构体对象也被叫做进程控制块,本质上就是进程属性的集合。在课本上称之为 PCB,PCB 本质上一定是一个 struct 结构体,Linux操作系统下的PCB叫做:task_struct。
(如果阅读完产生悖论思想可看一下目录五)
生成PCB的过程:
操作系统对软硬件的管理都是"先描述,再组织",最后都转化为数据结构的形式只要增删查改就可以
操作系统对进程管理就是如此,先描述,再组织
先描述: 每个进程都是要有一定的基础属性(所有进程的基础属性都相同),而操作系统了解到这一点后,就用一个结构体来管理这些属性
再组织: 由于每一个进程的基础属性都相同,所以操作系统就用结构体创建出对象,每一个进程代表一个结构体对象,这些结构体对象用数据结构的形式存储起来,在进程管理中是链表的形式,但无论是什么,最终都变成了对某个对象的增删改查
这里需要注意的是,PCB中有一个指针是可以指向代码和数据的,但是并不存储,只是指向,这一点很重要! ! ! ! ! ! ! !
小Tips:同一操作系统下的所有进程,其 PCB 的属性类型是完全相同的。
📖5.真正进程的组成
我们上面提到过进程就是内存中正在运行的可执行程序
因此,这个进程的组成一定是有可执行程序,因此,它一定有代码和数据
但是进程不单单由代码和数据组成
进程的组成为: PCB + 代码和数据
(你可能思考这样的问题,为什么生成PCB的过程叫做对进程的管理,没有PCB就不是进程怎么能叫做对进程的管理呢?
这里解答一下->:
进程管理不仅是 “管理已存在的进程”,更是对进程从无到有的全生命周期控制。)
如果说你是一个进程,那你就必须具备这两点,不能是单单的可执行程序就是进程
我们上面提到过操作系统如何管理进程,我们可以得知,操作系统管理进程并没有对代码和数据进行处理,可以看如下图->:
可执行程序中的代码和数据是由内存管理的,而我们的操作系统只需要生成PCB,并用PCB对进程进行更好地管理,这就是操作系统要干的事
PCB只存储进程的基本信息,不存储它的代码和数据
📖6.形成进程的过程
至此我们总结一下形成进程的过程
1️⃣ 先生成 PCB → 2️⃣ 再加载可执行程序到内存 → 3️⃣ 最后运行程序,成为进程。
📖7、Linux环境下的进程知识
7.1 task_struct
task_struct 是 Linux操作系统 下的 PCB 结构,它是 Linux 内核的一种类型,会被装载到 RAM(内存)里,并且包含着进程的信息。
7.2 task_struct内容分类
标识符:描述一个进程的唯一标识符,用来区分不同进程。
7.3 操作系统如何组织进程
Linux 内核中,最基本的组织进程 task_struct 的方式是采用双向链表组织。但是一个 task_struct 对象不仅仅属于一个双链表,它可能存在多个数据结构中。对进程的管理,本质上就是将 task_struct 对象放到某个数据结构中。
7.4 查看进程属性(先学会命令即可,具体用法下面讲)
ps axj | head -1 && ps axj | grep 进程名称
//&&用来连接两个指令操作
//左边是只打印“PCB”信息
//右边是将mycode进程的相关信息过滤出来
我们的 ps axj | head -1 就是展示出来我们前面所讲的PCB属性名单,每一个进程的PCB属性都是相同的,但是属性里面的具体内容是不同的
小Tips:最后一行显示的其实是 grep 命令的进程,这里先留一个空,下面再讲grep
7.5 proc目录
ls /proc
ls展开proc目录,那么什么是proc目录?
我们先看一看展开proc目录的效果->:
(声明 : PID为PCB的一个属性,具体PID请看目录8,这里只是知道有pid这个东西即可)
这里一看确实乱码七糟,但是别急,听我讲解一下
proc
是一个 虚拟文件系统,它不占用实际磁盘空间,数据存储在内存中,主要用于动态展示系统运行时的信息,包括进程、硬件、内核状态等。以下是其核心特点和作用如下:
1.进程信息
(声明 : PID为PCB的一个属性,具体PID请看目录8,这里只是知道有pid这个东西即可)
每个运行的进程在 /proc
下都有一个以进程 PID 命名的目录
所以我们的进程pid是141405,那么在proc目录下其实就会有一个141405的目录,这个目录中储存着我们进程的各种信息(即PCB)
我们可以通过以下指令来查看PCB属性
ll /proc/进程的pid,如图中就是141405
2.系统硬件与内核信息
我们在上图中的右下角发现了一堆英文,这其实就是硬件,里面存放的是硬件的基础信息
需要注意的是,硬件可没有PCB这一说,PCB 聚焦于 进程个体的运行管理,而硬件属性是 系统层面的全局信息
小Tips:当我们终止掉 mycode 进程 proc 目录下的1624目录也会跟着被清理。其次,一个进程终止后再启动,它的 PID 大概率是会变化的。(为什么变化?下面讲)
📖8.什么是进程PID
之前我们提到了PID,但是PID究竟是什么,我们还只限于知道,PID是PCB的其中一个属性
现在我们来彻底认识一下PID
PID 是用来唯一标识一个进程的属性,我们知道,进程的属性是PCB,而PCB是由操作系统生成的,操作系统管理着这么多的属性,怕有人乱搞,因此它们封装起来不让你直接接触,但是又不能彻底与世隔绝,因此操作系统设定了一个接口,用户可以通过这个接口去看到进程的属性,而这个接口就是PID
总结一下->:PID是一个接口,可以让用户访问到进程的属性(PCB)
📖9.通过系统调用接口查看进程PID
要获取进程的 PID 需要用到系统调用接口 getpid()
。该函数会返回调用这个函数的进程的 PID。返回值类型是 pid_t。
#include <stdio.h>
#include <unistd.h> //sleep函数头文件
#include <sys/types.h>//getpid函数头文件
int main()
{
while(1)
{
printf("我的PID是:%d\n",getpid());
sleep(1);
}
return 0;
}
📖10.父进程PPID
如果我们展开PCB的属性,我们可以看到一个PPID的东西,如下图->:
(命令在7.4查看进程属性)
这个PPID叫做父进程,既然叫做父进程肯定和子进程有关系,我们再看下面内容
📖11.fork函数 (也叫做fork系统接口)
我们先来看一张图->:
诶?我们只写了一个hello world,怎么打印出两个了?难道是,bug!
不,这并不是bug,而是正常的,这是因为fork函数的功能
当我们的程序成功运行时,到fork函数执行时,操作系统会复制当前进程(父进程),生成一个子进程。因此,就存在了两个进程,而我们就看到了两个hello world
但是这是如何实现的?请看下文
📖12.fork函数如何实现
知识点1:
我们知道,fork函数会导致复制出一个子进程,而这个子进程的代码实际上是与父进程共享的,也就说,父子进程共用一个代码
我们先总结一下->:1.父子进程共用一个代码
知识点2:
其次,虽然父子进程共用一个代码,但是数据是不同的,什么意思?
父子进程虽然代码相同,但是里面的变量是不同的,可以理解为子进程的变量是新开的一片空间,与父进程的变量不冲突(我们后面会演示,这里先记住)
再次总结一下->:2.父子进程变量的数据不冲突,因为子进程的数据单独开辟了空间
知识点3:
fork函数的返回值给父进程返回父进程的pid,给子进程返回0
返回不同的返回值,是为了区分不同的执行流,让不同的执行流去执行不同的代码块。简单来说就是为了区分父进程和子进程
让父进程和子进程去执行不同的任务,子进程创建出来的目的就是帮助父进程完成一些工作,如果返回值相同那 fork 函数后面的代码父进程和子进程都会执行,那么我们创建子进程就没有了意义
总结一下->:3.fork函数给父进程返回值是子进程的pid,给子进程返回的是0
问题1: fork函数是如何做到返回两次的?
首先,父子进程一共只会调用fork一次,不存在父进程调用一次,子进程调用一次,这也说明fork函数返回两次不是因为执行了两次
父进程的fork在被执行后,内核从 PID 池中找到一个未使用的整数作为子进程的 PID,我们知道PID是一个PCB的属性,也是一个目录。但是子进程最开始没有PCB,这就导致没有PID的存在,所以它会新开辟一段内存空间,这个内存空间会复制父进程的PCB,也就是为了创建子进程的PCB,接着父进程给这个PID返回给子进程,所以子进程就出现了(任何进程PID的分配都早于PCB的创建)
fork函数实际上执行了一次,返回了两次,因为子进程只会从fork函数的下一句开始执行,所以子进程的fork函数并没有被执行
但是子进程的fork函数被强制规定会返回,并且返回值为0
问题2:一个变量怎么会有不同的值?
我们看这样一段代码,后面是运行截图
可以发现,居然运行了两次,这就是因为我们的知识点2和知识点3,父子进程的数据是不同的,并且fork函数给父进程返回子进程的pid,子进程返回0
fork函数总结
-
fork 有两个返回值。
-
父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
问题3: 子进程创建后,父子进程谁先运行?
一般而言,一旦一个进程被创建出来,以打开QQ音乐为例,在操作系统层面该进程什么时候被运行我们作为用户是管不了的,我们只负责使用即可,操作系统在底层会去调度每一个进程,至于如何调度我们作为用户也无需关心。因此子进程被创建出来后,父进程和子进程究竟谁先执行是由调度器来决定的,所以谁先谁后并不一定。
小Tips:所有的进程在操作系统中会以链表的形式被组织起来,对进程的管理就变成了对链表的增删查改。挑选一个进程放到 CPU 中去运行,这个工作是由调度器去做的,调度器一般会做到公平调度。一般的计算机中只有一个 CPU,而进程却可能有很多个,这就注定了 CPU 是一个少量的资源,对所有的进程来说,运行的本质,就是把它放到 CPU 上,所以所有的进程,对 CPU 资源本质上是一种竞争关系,此时就必须要有调度器的存取,去保证每个进程被公平的调度。(具体可以看以后的文章,会讲到进程的优先级)
📖13.最后来理解bash
我们看这样一张图->:
我们可以看到父进程与子进程的pid,父进程的pid要早于子进程这是一个知识点
bash本质:一个能理解并执行用户输入命令的程序
(重要->:)
bash 的源代码实现中会调用 fork 这个函数去创建子进程,让自己继续去执行命令行解释。让创建出来的子进程去执行我们输入的指令。所以我们在 bash 命令行输入的所有指令,最终执行起来加载到内存变成进程后,都是 bash 进程的子进程。
因此->:(不太重要了解即可)
1. 如果一个进程是直接由bash启动的,那么bash的pid就是fork的父进程pid
2. bash的子进程去调用fork,是由fork的子进程去生成另一个子进程
📖14.完结
创作不易,留下你的印记!为自己的努力点个赞吧!