由浅入深的了解进程(2)

2024-08-02 08:22:09 浏览数 (2)

前言

上一篇文章中从冯诺依曼体系结构聊起,再到操作系统,如果忘记了之前的这两个知识,可以i回去看一看,现在我们将要重点开始进程了!

1、进程

操作系统中,进程可以同时存在多个!

那这么多的进程操作系统要不要管理啊?当然是要的啊!那要是管理的话,能不能想起来之前一篇文章写的关于管理的介绍啊?先描述在组织!

1、1、基本概念

课本概念:程序的一个执行实例,正在执行的程序等。 内核观点:担当分配系统资源(CPU时间,内存)的实体。 那我们怎么能够判断,什么是执行实例,什么叫或者说怎么样才能够称为是叫正在执行的程序呢? 程序在没有运行的时候存放的位置是磁盘。在运行的时候,需要把程序(代码和数据)加载到内存中。难道这个过程就能够叫做进程吗?其实我们可以想,如果说这就能够叫做进程的话,我们还有多进程,那多进程不就是多个程序直接加载到内存中就行了? 很显然,不能够就是这样。因为,如果是多进程的话就必须还要考虑程序运行的先后,程序占据内存的位置,程序是否被CPU运行还是其他状态。 这些问题都是需要考虑的,所以程序运行到内存中根本就不是进程,他只是进程对应的代码和数据。 通过上面的介绍和解释,我们该能够想到进程,应该必须包含哪些基本的要求。

1、2、描述进程-PCB

所以到底什么是进程呢?(我们可以通过先描述再组织来看到底什么是进程。)为了管理所有已经加载进来的进程,操作系统需要为其构建一个struct PCB结构体,来管理。其中PCB内包含进程属性。一个进程一定有一个PCB。PCB就是对进程进行描述,紧接着就是操作系统对进程的组织,通过next指针连接起来。

进程 = 自己的代码和数据 PCB 这样的话,未来CPU要运行进程的时候,对哪些进程应该被调度,应该被允许,应该被创建,应该被暂停,全部都转化成对链表的增删查改。所以对于进程的管理也就是相当于是对PCB的管理。

PCB介绍: PCB(process control block)。这样的数据结构存在于内核之中,我们一般称为内核数据结构。 小知识:我们电脑在开机的时候,通常都会有几秒钟的等待时间,这段时间就是把在磁盘上的操作系统运行到内存上,然后CPU运行malloc出空间以及PCB结构体,然后运行出操作系统所需要的时间。 操作系统对于进程的管理,本质上是对于PCB的管理,并非直接对可执行程序进行管理。

为什么要有PCB? 因为OS要对进程进行管理。方便对于不同的可执行程序进行管理。

上面讲述到进程先后顺序的问题也就能够解决。调度运行进程,不是程序的代码和数据进行queue排队等待,本质上是让进程控制块进行排队。类似于大学生投简历,不是大学生本人真正的去,而是让面试官去看简历,从前到后的看(这是排序),看到简历就能够大概知道大学生本身这个人究竟是什么样的(不需要本人来,不需要真正的程序的代码和数据来)。

1、3、Linux系统中的PCB

Linux中的PCB叫做struct task_struct。 进程=内核task_struct 程序的代码和数据

1、4、如何理解进程动态运行

相比于找工作一样,一个岗位是否需要招聘你的这个面试过程,有时候不只是一面或者二面,有时候还需要笔试等等一系列的操作,虽然你本人并没有在公司内部在不同面试官面前走动,但是你的简历一定会给不同的面试官看,一定会不断地进行流动。所以一个进程也不知是会在一个地方进行排队,肯定还会有别的动态运行过程。 进程动态运行过程体体现在, 只要进程task_struct,将来在不同的队列中,进程就可以访问不同的资源。(更多细节以后依次再谈)

1、5、task_struct本身内容分类

本身的内容包含了标示符,状态(运行/休眠/退出状态),优先级,程序计数器,内存指针,上下文数据,I/O状态信息,记账信息,其他信息。 创建一个进程其实很简单,我们只需要在Linux系统下写一个文件,然后编译器编译之后,直接

代码语言:javascript复制
./XXX:本质上就是让系统创建进程并运行

所以,我们自己写的代码的可执行就是相当于是系统的命令,相当于是可执行文件。所以在Linux上运行的大部分执行操作本质上也就是运行进程。在使用指令的时候,几乎是一瞬间执行可执行程序,一瞬间把可执行程序加载到内存,一瞬间创建PCB,一瞬间再把进程调度完毕,然后就会使我们看到的现象,指令执行成功。

1、5、1、进程查看(task_struct查看/PCB查看)

这是我们查看系统中正在进行进程的操作,同时过滤了一下,只显示我们需要的进程。 但是为什么是由两个呢,为什么还有一个什么grep上面的呢?答案就是因为grep在过滤行消息的时候也是启动了进程的啊,grep启动的进程就是查看指定文件的程序,所以也包含了关键字,所以在显示的时候也能够看到见。 可是直接grep出来的每一列的含义是什么,我么也不知道啊。这时候就需要这个命令

代码语言:javascript复制
ps axj | head -1: 显示第一行

这样我们就能够看到每一列是什么意思了,那我们能不能够直接既显示第一行也显示grep过滤出来的信息方便我们查看什么含义呢?当然也是可以的,不过要使用好管道和head命令。 两个指令并行进行的符号是&&

代码语言:javascript复制
ps axj | head -1&& ps axj |grep myprocess:既想把头部信息,拿到也想拿到过滤的信息

除了通过ps来查看进程的运行,我们也能够直接查看目录->/proc/(PID) -d直接查看是否存在进程。 同时想要查看这个文件夹内部,也能够发现软链接链接的就是我们刚刚创建的进程文件所在的位置。如果此时我们在停止进程运行前删除文件,发现此时的进程还在继续,查看/proc目录下还是能够查看到进程。

不过此时虽然没有问题,但是如果是大一点的文件或者是软件直接这样,可能运行运行着就会出现错误,然后运行失败。

其中exe说明PCB中会记录自己对于的可执行文件的路径。 其中cwd=current work dir ,进程的当前工作目录。 当前目录是可以修改的,默认情况下的cwd是在可执行程序的目录。但是我们能够通过系统调用函数来帮助调整cwd的位置。

证明的话还可以看看们是不是cwd目录下,有我们创建的log.txt的文件。

ok,是存在的,也就是说cwd修改成功,并且能够实现"在当前目录下创建文件"

1、5、2、PID

每一个进程都有自己唯一的标识符,叫做进程的PID。(process id)。 一个进程想要知道自己的PID,不利用ps查看的话还有什么别的方法吗?没有办法!那是因为一个用户不能够直接访问操作系统内的对应的内核数据结构的PID。所以必须要有系统调用才能够得到PID。

通过库函数,利用系统调用实现操作系统内内核数据结构的PID访问。 证明方法也简单,当直接运行成的时候,再利用ps看看,两个的PID是否是一样的。

很显然,是一样的,所以这样确实能够实现访问一个PID不用ps指令。

1、5、3、终止进程
代码语言:javascript复制
ctrl c:能够直接终止进程,前面介绍过
kill -9 (PID):也可以用来直接杀掉进程

1、6、进程创建代码方式

现在主要重点讲述一下办法,后面再重新回到这个问题的时候再讲解原理。 首先讲解一下前提知识。

代码语言:javascript复制
pid_t getppid(void):获得当前进程的父进程

每次启动的时候的PID都不一样这正常吗?当然是正常的啊。这相当于是你高考考上大学之后,觉得高考的结果不太行,直接反手一个ctrl c,直接退出,然后重新回到高三重新在学,再考,可是很不如意,通常都是一个学校,但是每一年到这个学校的时候你的学好都是不一样的,这就像是进程一样。这是很正常的,每次都有不同的PID。这里的PPID每次都一样的,说明每次进程的父进程都是相同的一个。那这究竟是谁啊,怎么每次都会是一样的呢,,我们可以查看一下。

发现每次的父进程都是bash,这个bash在前面讲过,这相当于是一个shell外壳程序,称为命令行解释器。 现在开始真正的讲解到底怎么用代码创建进程。 首先我们要知道的是,一个进程是操作系统层面的事情,我们作为用户是不能直接进行创建的,所以为了能够创建进程,我们就需要有相对应的接口调用。

代码语言:javascript复制
fork: 创建子进程

对于这样的一个fork来说,当开始之后还是会出现两个hello world的,因为此时还有了一个子进程(fork之后,父子代码共享),当我们运行的时候,也能够通过ps观察到,子进程的父进程就是一开始本来进行的进程。

创建一个进程,本质上就是系统中多了一个进程,多了一个进程也就是多了一个内核task_struct和自己的代码和数据。父进程的代码和数据是从磁盘上加载进来的,所以子进程的代码和数据是从哪里来的? 子进程的代码和数据默认情况下是继承父进程的代码和数据。这就是为什么fork之后的代码共享。 凭什么能这么说?得证明是两个进程来打印的。

这可以证明!

这张图能够说明两句hello world不是一个进程中的,也能够说明fork生成的子进程的父进程就是原来的那个进程。

能够说明一开始的时候还没有子进程,直到过了几秒之后才出现的子进程。

1、7、子进程的使用

子进程的使用肯定不能是专注于和父进程使用相同代码的用处啊,既然有子进程,那就说明子进程应该能够做到和父进程运行不一样的代码啊。那我们怎么去像这样去使用子进程呢?怎么去让子进程和父进程使用不一样的代码呢? fork的返回值返回了两次!如果是父进程的程序的话,返回的就是fork就是子进程的PID,如果是子进程的话,就返回0。当然,如果返回失败的话,就返回-1。(无法创建子进程的时候)

这样的话,我们能够利用fork返回值来进行条件语句的运行。这样我们就能够实现子进程于父进程实现不同的代码。

这样的话make一下,就能够看到父子进程执行不同代码了。还能够看到是fork的返回值是否是0或者是子进程的PID。

疑点: 1、同一个id怎么能够同时拥有两个值(与父子写时拷贝和虚拟地址空间有关,现在没法说明白,以后说) 2、fork有两个返回值,会返回两次?fork也是一个函数,只不过是OS提供的。首先我们想,当一个函数的时候真正的返回了也就是说明,fork在运行的时候已经创建结束了一个子进程了,并且子进程也已经可以被调度了。那return不也是代码吗,其实fork后的代码共享从宏观上的理解是再main函数中运行完了才共享,但是其实实在fork函数内部构建结束子进程就已经共享了,所以最后返回的时候才是返回两个值。 进程一定要做到的特点 1、进程具有独立性。所以父子进程都有独立的task_struct 代码和数据。所以各自拥有的是独立的数据,这也就是id在两个进程中展示不同选择的原因。

0 人点赞