有着上一节我们对操作系统和冯诺依曼体系结构的理解,本篇我们便可以开始对 Linux 中的进程开始讲解。在本篇中对进程的基本概念进行了简单的介绍,然后通过对描述进程的 PCB,与 Linux 中的 task_struct 的详细讲解,使得对进程的概念有了一个更加详细的理解。然后接着又介绍了 task_struct 中的内部属性 pid 与 ppid(剩下的 task_ struct 内部属性将会在后面的文章给出),接着还介绍了进程的创建方式和子进程的创建,最后介绍了 proc 查看当前进程信息数据。
目录如下:
目录
1. 进程的基础概念
1.1 基本概念
1.2 描述进程 —— PCB
1.3 task_struct
2. 进程的进一步深入了解
2.1 tast_struct 的内部属性
2.2 进程创建的方式
2.3 fork 创建子进程
2.4 proc 查看进程
1. 进程的基础概念
1.1 基本概念
进程在课本中的概念为:程序的一个执行实例,正在执行的程序。
进程的内核观点:担当分配系统资源(CPU时间、内存)的实体(对于这个概念的认识,目前还不是清晰)。
对于以上的两种概念来说,其实理解起来都相对枯燥,接下来我们将从内核数据结构展开,来对进程进行更加详细的剖析。
1.2 描述进程 —— PCB
进程中的信息被放入到一个叫做进程控制块的数据结构中,我们可以理解为进程属性的集合,课本上称为 PCB。如下图:
如上图所示,当我们对电脑开机,电脑会首先将磁盘中的操作系统加载到内存中,操作系统在内存中也是需要占有一定的空间的。当我们想要使用某些程序的时候,会从磁盘中将该程序的代码和数据加载到内存中,但是目前这样还不能算是程序(进程),这仅仅只是进程的程序和数据,进程还需要被我们的操作系统所管理,所以在操作系统中还存在一个内核数据结构PCB(process control block)用来表征在加载在内存中的每个程序,这个 PCB 结构体中存在一个进程的所有属性,指向下一个 PCB 的 next 指针,以及该进程代码数据在内存中的位置,内存指针。
当我们需要加载某个进程的时候,我们将在操作系统的内核数据结构中的插入一个新的 PCB 数据结构,然后将该进程的各种属性描述进该数据结构,当我们不在需要使用该进程的时候,我们就先将该进程的代码和数据先释放掉,然后将对应的 PCB 结构体给删除掉。
所以对以上的总结一个进程一定会有一个对应的 PCB,进程 = PCB + 对应的代码和数据,我们对进程的管理并非对内存中的数据代码做管理,而是对进程的 PCB 进行管理。
1.3 task_struct
对于 PCB 的实现,在不同的操作系统下存在不同的实现方式,如我们在 Linux 系统下对于 PCB 的实现为 task_struct 结构体。接下来给出一个程序在 Linux 系统下的调度简图:
如上图所示,在操作系统中存在一个 task_queue 队列用于对 task_struct 结构体进行排队,当排到那个 task_struct 的时候,就会从内存中找到对应的代码和数据将其加载到 CPU 中进行处理,以此的排队顺序,分别分时的对每个进程进行处理。
2. 进程的进一步深入了解
在上文中,我们已经介绍了进程的基本概念,以及描述的进程的 PCB,同时还有在 Linux 下的 PCB 结构体 task_struct,在下文中,我们还会围绕 task_struct 进行更加深入的探讨。
2.1 tast_struct 的内部属性
接着我们来讨论 task_struct 中的内部属性,首先我们是关于程序的启动,如下:
当我们在程序中写出一个可执行文件,当我们开始执行的时候,其实就是启动了一个程序,也就是进程。其实不仅是我们写出的可执行文件,我们的系统命令、可执行文件,在 linux 中的大部分执行操作,本质上都是在运行进程。
现在我们开始查看 task_struct 中有关进程的属性,如下所示。
每一个进程都有自己的唯一标识符,叫做进程pid,执行如下指令就可以查看我们进行从pid:
进程之间的区分就是使用进程的唯一标识符:pid,获取 pid 的方法不仅仅只有通过外部指令能获取到进程的 pid,我们还可以在进程代码中标识出自己的 pid,此操作需要调用系统调用接口 getpid,如下:
关闭进程的方式:通常我们关闭一直运行的进程,除了使用 ctrl + c,还可以使用另一个指令,kill -9 [进程的pid],如下:
如上所示,我们使用 kill 指令将进程给终止掉了,kill -9 后面一定要跟进程的pid,当输入错误的 pid 的时候,便不能将进程的终止掉。
2.2 进程创建的方式
接下来我们将从代码层面来揭露出进程创建的方式,我们在上文中发现每个进程都有着属于自己的进程标识符 pid,每个进程不仅有自己的 pid,还有对应的 ppid,也就是父进程的 pid,父进程的唯一标识符,如下:
当我们执行如上代码之后,我们会发现,每一次运行同一个可执行文件(进程),得出的 pid 都是不一样的,而每一次进程对应的 ppid 却是一样的,其中 pid 每次不一样属于正常情况,因为每一次执行结束之后,后台也许还会运行别的进程,每一次给出进程的 pid 都是动态的。接下来,让我们看看 pid 为 12484 的进程是什么:
通过如上操作,我们发现 pid 为 12484 的进程为 bash,也就是我们的命令行解释器,只要是在当前 Linux 环境中编译运行的可执行文件,其父进程都是 bash。
那么接下来当我们想要创建一个进程的时候,我们该如何进行创建进程呢,在操作系统那篇 blog 中也已经指出,我们用户并不能直接在操作系统中创建出一个 task_struct,并不能直接使用 task_struct 创建出一个进程,但是我们可以使用系统提供的系统调用接口 fork,提供 fork 接口,我们就可以成功的在一个我们自己的进程中,创建出另一个进程,如下:
如上图所示,当我们在代码中使用 fork 接口,原本代码中只存在一个语句打印 hello linux,但是在显示的程序中,我们发现,打印了两次 hello linux,并且在左边的监视窗口中,我们发现,过了一会又多了一个 myprocess 进程,并且该进程的 ppid 就是等于前面进程的 pid,所以我们可以得出新产生的这个进程就是由原来的进程产生的。
还存在一个问题,为什么会打印两次 hello linux呢?原本在代码中只有一句打印 hello linux 的代码,这是因为使用 fork 接口之后,就会创建一个进程,本质上是系统中多出一个进程,多一个内核 task_struct,且该 task_struct 有着自己的代码和数据。在父进程的代码和数据是从磁盘中加载进来的,子进程的代码和数据又是从哪来呢?默认情况下,子进程的数据会继承父进程的代码和数据。所以会打印出两个 hello linux。
2.3 fork 创建子进程
在上文中,我们已经发现使用 fork 可以创建出一个子进程,但是创建出子进程有什么作用呢?作用就是让子进程与父进程运行不一样的程序,提高效率。以下为 fork 的介绍:
当我们使用 fork 创建子进程创建成功的时候,fork 会返回子进程的 pid 给父进程,返回 0 给子进程(自己),若创建子进程失败,就只能返回 -1。在这里我们会发现 fork 的不同,fork 的返回值有两个,我们在以往的学习中,关于 C/Cpp,其各种函数都是只返回一个返回值,而在这里 fork 却可以返回两个,关于 fork 返回值存在两个,我们之后在探讨,现在先验证 fork 是否会返回两个值,如下:
如上所示,我们写出的了一个有关 fork 的代码,使用 id 来接收的 fork 的返回值,当 id = -1 的时候,直接结束进程,当 id = 0 的时候,执行子进程的代码,当 id 不等于 0 的时候,执行原进程的代码,从运行的结果我们可以发现,确实运行了两个进程,并且确实 fork 返回给子进程的 id = 0,返回给原进程的 id 为子进程 pid。其实在 fork 之后的代码都是父子进程之间共享的代码,然后我们使用判断将其划分,让其执行不同的代码。但是,为什么对于同一个 id,它为什么会有两个值呢?
首先先探讨是如何返回两个值的,对于 fork 而言,其既是一个接口,也是一个函数,一个由操作系统提供的函数,这个函数的返回值就是一个 pid_t 类型的值,也就是 id。我们之前也提到,fork 函数之后的数据,父子进程的代码和数据都是共享的,其实准确来说,不是在 fork 函数后,而是在 fork 函数之中就已经共享了,所以关于最后一句的返回值代码来说,就确实会分别被父子进程给分别执行一次。以上仅仅只是探讨了为什么会返回两个值,并未解决为什么 id 的值会存在两种情况,如下:
首先对于进程与进程之间,他们之间要相互独立,互不影响,就好比结束这个进程不会影响到另一个进程一样,在父子进程之间同样如此,结束了父进程,子进程也可以继续运行,结束了子进程,父进程也不会受影响。这样产生的原因是,创建一个子进程其实就是在操作系统中产生一给内核 task_struct ,他们之间也仅仅只是 next 指针相连接的关系,和 pid ppid 之间的关系,只是称呼上叫做父子进程,那么对于子进程就一定存在自己独立的数据,这是在生成子进程写时拷贝的结果,则对于 id 存在两个值,和写时拷贝和虚拟地址都有关(关于写时拷贝和虚拟地址两个概念将会在后面的文章给出详细的解释)
以上便是关于如何创建子进程的知识,以下给出了一个创建多个子进程的代码,如下:
2.4 proc 查看进程
以上讲解的为进程是如何创建的,如何创建子进程。那么关于创建出的进程的信息数据是放到哪的呢,我们可以在目录 /proc 下查找到,如下:
如上所示,我们可以在 /proc 目录中通过进程的 pid 查找的对应的进程目录,在当我们进入到目录的时候,我们就可以在这里面看到有关当前进程的信息和代码,如下:
由上图片可以发现,在名为 exe 的一个可执行文件,其实是一个链接文件,链接的地址为,进程对应自己的可执行文件的程序。所以,进程的 pcb 中会记录下自己对应的可执行程序的路径。还有一个 cwd 链接文件,也进程当前路径,所以每个进程在启动的时候,会记录当前在哪个路径下启动,这样的好处有,当我们在进程中创建了一个文件(比如使用 fopen 创建一个文件),只要不指明路径,就会默认在当前进程所处路径中创建。