【Linux】第十二站:进程

简介: 【Linux】第十二站:进程


一、windows和linux中的进程

一个已经加载到内存的程序,叫做进程

进程也叫做任务

比如在windows中,这个任务管理器就可以看到进程

而在linux中,我们使用

ps axj

就可以看到我们的进程了

二、先描述

我们先使用如下代码

如下所示,就可以查看到进程了

在这个过程中,我们的这个可执行程序就会从磁盘中加载到内存中,然后经过CPU,最终输出到显示器上。

这个加载到内存中的程序就是一个进程

像我们电脑的操作系统开机的时候也一样,开机的时候,就是操作系统加载到内存的时候。这也是进程

这些加载到内存,其实就是将这些二进制数据(代码和一些数据)从磁盘中搬到内存中

如下图所示,当一个程序要运行的时候,他需要先将这些二进制文件从磁盘中搬到操作系统中。

但是常识告诉我们,一个操作系统,不仅仅只能运行一个进程,可以同时运行多个进程

所以说,操作系统必须将这些进程给管理起来

那么应该如何管理进程呢?

当然还是先描述,在组织

所以任何一个进程,在加载到内存的时候,形成真正的进程时,操作系统要先创建描述进程属性的结构体对象----PCB(process ctrl block 进程控制块)

这就是类似于人是怎么样辨别认识一个事情或者对象的

当然是都是通过属性认识的。

当属性够多,这一堆属性的集合,就是目标对象

这个PCB就是进程属性的集合

而在C语言中,我们可以用struct结构体来描述这个集合。

这个结构体里面就有进程编号、进程的状态、优先级、相关的指针信息…等等信息

然后根据进程的PCB属性,就可以为该进程创建对应的PCB对象了

不过我们的操作系统除了创建一个PCB对象之外,还要去将代码和数据加载到内存中

就好比我们现在是一个学生,那么PCB就是我们的档案,代码和数据就是我们本人。只有当这两部分都在学校的时候,我们才是这个学校的学生。

所以描述进程的PCB的结构体和该进程的代码和数据合起来才称作进程

所以所谓的进程 = 内核PCB数据结构对象 + 你自己的代码和数据

内核PCB数据结构对象是描述你这个进程的的所有的属性值

所以操作系统要做管理的时候,不需要对我们自己做出管理,只需要对我们这个内核PCB数据结构对象做出管理即可

这个PCB对象里面是有相关的指针信息的,所以可以通过这个PCB对象直接找到我们的

上面这个过程就是一个进程的描述的过程,即先描述

三、在组织

可是我们操作系统经常要持续很多个进程的。所以这就需要组织起来了,从而达到对我们的进程做出管理

为了能够将这些数据组织起来,所以我们可以在每一个PCB对象中加上一个相应的指针,用来找到下一个PCB对象,即如同链表的结构

所以在操作系统中,对于进程的管理,就变成了对于这个单链表的增删查改了

这个PCB就好比我们每个人的简历一样,在我们找工作的时候,HR都是直接对我们的简历做出管理的。当我们投递简历以后,我们会看到在排队中,这个排队就指的是这个PCB在排队。而不是我们本人在排队

四、具体的Linux系统是如何做的?

1.基本概念

课本概念:程序的一个执行实例,正在执行的程序等

内核观点:担当分配系统资源(CPU时间,内存)的实体

2.描述进程-PCB

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。

课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

3.task_struct和PCB的关系

  • task_struct 是PCB具体的一种
  • 在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构类型(自定义类型),它会被装载到RAM(内存)里并且包含着进程的信息

4.task_struct内容分类

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

5.linux具体如何做的?

在linux中PCB 就是task_struct 结构体,里面包含进程的所有属性

Linux中如何组织进程呢?,linux内核中,最基本的组织进程task_struct的方式,是采用双向链表的

不过要注意的是,有可能这个task_struct既是属于一个双链表,又是属于一个队列的。他是比较复杂的

6.查看进程

我们可以使用这个命令查看进程

top

同时我们在前面所演示这个命令也是一个查看进程的命令

这里这里的&&代表的十左侧的要执行成功,右侧的也要执行成功。即两条都要执行

ps ajx | head -1 && ps ajx | grep  myprocess

除了使用&&,也可以使用;来隔开

ps ajx | head -1 ; ps ajx | grep myprocess

如下所示,我们可以看到两个进程了

前面两个是这个可执行程序由于跑了两份,所以的两份进程。

下面的第三个进程是grep本身的进程,因为它本身也带有myprocess,所以就把它自己的进程过滤出来了。而之前的那些指令的进程早就已经结束了,所以不会显示出来。这里是因为正好grep执行的时候把他自己给过滤出来了

注意看

注意这个ID值,虽然这两个程序是一样的,但是他们的ID是不一样的,虽然将同一个程序执行了两遍,但是它还是两个进程,因为他们创建了两个不同的PCB

除了上面这种方法还有这样一种方法

ls /proc

这个是linux系统中比较重要的,也比较奇怪的一个目录

它在关机的时候数据就全没了,在开机的时候就又会创建这个目录文件

这个其实因为因为操作系统用文件系统的方式把内存当中的文件,包括进程全部可视化出来了

它这上面的数据都是内存级的

我们可以来证明一下

我们先关掉一个进程

当我们再次使用这个

ls /proc

这个指令的时候

对于这些蓝色的数字,我们知道它一定是目录,而对于黑色的我们先不管

而这些蓝色的数字就是当前进程的PID,所以在创建进程的时候,操作系统会创建一个与PID一样名字的文件夹,这个目录里面保存这个进程的大部分属性

我们可以通过这个找到我们这个进程的目录

然后我们可以查看一下这里面的内容

如果我们将我们这个进程之间结束掉,那么我们这里就用这种方法找不到这个进程了,这个目录就自动删除了

如果我们重启一个进程,那么就又出来了

不过这个PID也被改变了。

所以说每一个程序结束以后在重新启动,PID就会变化了,所以他是系统当中动态运行的相关信息。

我们可以进入到它里面就可以看到这个东西,这就足以证明这个就是我们的这个可执行程序的进程

对于这个cwd,就是我们当前进程的工作目录(current work dir)

对于这个cwd,这个是很有用的

比如说当我们使用touch命令创建一个test.c文件的时候,它是如何找到当前的目录呢?

就是进程启动的时候已经记录了当前的目录。所以才找到的

就好比我们之前的文件操作的时候,为什么文件可以创建在当前目录呢?

就是因为进程有这个cwd,记录了当前工作目录,所以可以直接拼接上去。这就是当前路径

7.kill进程

我们先写出如下代码

如下所示,当我们运行了这个代码以后,它就会有它对应的进程

并且其中我们发现最下面的这个proc其实是因为grep这条指令也要带上proc,所以也把他过滤出来了,可见指令在运行时也是要有进程的

那么我们也可以看到这个PID,有了这个PID有什么用呢?在前面我们用它可以找到一个对应的文件。那么还有什么作用呢?其实我们还可以直接杀掉这个进程

kill -9 13388 //13388是当前程序对应的PID

我们可以看到,进程确实被杀死了

8.进程的PID

我们知道对于我们的操作系统,它的内部一定是这样的

内部有task_struct结构体,然后指向对应的代码块和数据。而pid一定是在它的内部存储着的

而我们前面的ps命令其实就是在遍历这个链表。从而获取每个值的

如果我们想要获得某个进程的pid,由于我们无法直接拿去,所以我们只能通过系统调用来获取

getpid()

如下是man手册中的getpid

我们可以知道,它的作用是返回调用这个函数的进程的PID

然后我们可以去应用一下:

我们可以用这段指令来进行检测进程

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

它一开始的效果是这样的

如下所示,我们就可以检测到进程了

我们也可以看到这个进程的PID,并且如果我们在中间终止掉了这个进程,然后再重启的时候,它的PID已经被改变了

相应的,除了PID以外,还有一个PPID,它的意思是父进程

同样的,我们可以去获取他的进程

如下是运行结果

如果我们将这个进程杀掉,然后,我们继续重启这个进程

我们会发现,父进程没有变化,我们自己的进程的ID变化了

那么我们可能会好奇这个13200里面到底是什么呢?

我们可以看到有很多的13200,但是我们先只关心一下PID为13200的这个

我们可以看到它后面跟的是bash,也就是命令行解释器

所以说,运行一个进程时,命令行解释器会把我们这条命令的进程变成bash的子进程,由子进程实行对应的命令,这样的话,当子进程执行命令的时候,一旦这个进程出问题了,就不会影响bash进程

但是如果我们直接将我们的这个Xshell断开链接了,然后重启,我们就会发现这个PPID也变了

但是如果我们只是普通的杀掉这个进程,然后再运行的时候,父进程不改变

然后我们可以像前面一样,调出这个父进程所对应的文件

我们可以看到,以前的13200也就不见了

所以说,我们每次重新登录的时候,系统会为我们重新创建一个bash进程

我们在命令行中输入的所有进程都是bash的子进程

bash只负责命令行的解释,具体执行出问题的时候,只会影响它的子进程

这就是为什么当一个子进程中断的时候,父进程不变的原因

9.fork

我们前面一直在说进程,那么如果我们想创建一个进程,该如何做呢?

一个方法就是我们前面使用的直接./Proc,这样的话就是操作系统为我们创建进程了

如果我们想要自己创建一个进程,那么我们可以用fork函数

我们先用起来这个函数

当我们用如下代码,不包括fork的时候

毫无疑问,打印两行

当我们将这个fork加上以后

我们会发现打印出来了两次after

为什么呢?这个其实就是因为,fork之前只有一个执行流,forK之后就有两个执行流了。

不过我们还是先看一下fork函数

这是它的返回值,意思是如果成功,那么返回一个子进程的PID给父进程,返回一个0给子进程。如果失败,返回-1给父进程,不创建子进程,并适当地设置errno。

我们发现它好像有两个返回值?

我们先看一下这个代码

如下是我们的运行结果

我们发现这个运行结果很奇怪。出现了很诡异的现象。

这其实就是fork的作用,有了它就有两个进程了

我们可以关注一下一组pid和ppid的值

我们会发现,这两个刚好就是一组父子关系。

因为父进程的pid等于子进程的ppid

而前面这个29958是bash命令行的进程,从下图可以看到

我们也可以在前面的这个图看到,这个父进程其实就是我们原来的进程,而子进程就是这个父进程所创建的一个新的分支

所以这样的话,我们就也可以创建一个进程了

所以

./xxxx------------指令层面创建进程

fork()-------------代码层面创建进程

10.对fork的深刻理解

fork的英文意思是叉的意思,也就是代码一分为二

如下图所示,当我们使用fork以后,代码就变成了两个执行流,父进程的执行流执行id值大于0的部分,子进程的执行流执行等于0的部分

在这里我们就会有如下几个疑问

  1. 为什么fork要给子进程返回0,给父进程返回子进程的pid
  2. 一个函数是如何做到返回两次的?如何理解
  3. 一个变量怎么会有不同的内容?如何理解
  4. fork函数究竟在干什么?干了什么?
  1. 为什么fork要给子进程返回0,给父进程返回子进程的pid?

返回不同的返回值,是为了区分让不同的执行流,执行不同的代码块

一般而言,fork之后的代码父子共享

以上回答了为什么要不同的值,下面回答为什么父进程要返回子进程的pid

这是因为,一个父亲可以有多个孩子,而一个孩子只有一个父亲

所以未来父亲需要对孩子做出区分,所以返回一个pid就可以区分各个子进程

而对于一个孩子,由于父亲是唯一的,所以返回0即可

所以必须返回子进程的pid,用来标定子进程的唯一性

子进程只需要调用getppid就能直接获取父进程的pid,所以它直接返回0标记成功即可

  1. fork函数究竟在干什么?干了什么?(上)

我们知道

进程 = 内核数据结构(PCB) + 代码和数据

如下是我们fork之前的样子,只有当前的一个进程,CPU去调用这个进程

而创建子进程其实就是:系统里面多了一个进程

即如下图所示,首先,会先照着原来的task_struct创建一个新的task_struct。不过这个新的tash_struct会将它的pid修改为一个新的pid,将它的ppid改为前面的pid

现在子进程有了自己的内核数据结构,也就是PCB,那么它还没有它自己的代码和数据,它应该访问什么呢?

而父进程是可以访问自己的代码和数据的。子进程没有,所以只能去访问父进程的代码

所以说,他们的代码是共享的

所以CPU在执行的时候,如果是父进程,跑的是它的代码,如果是子进程,跑的也依旧是这个代码

即fork之后,父子进程代码共享,因为代码是不可以被修改的

那么既然代码是共享的,那么之前的数据呢?我们先不考虑这个问题

我们先来看一下这个,既然代码是共享的,那么我们为什么要创建进程呢?

这是为了让父和子执行不同的事情,所以需要想办法让父和子执行不同的代码块,所以我们就需要让fork具有不同的返回值

  1. 一个函数是如何做到返回两次的?如何理解?

在上一个问题中,我们讨论到了代码块一定是共享的,为了让其可以执行不同的代码块,就需要具有不同的返回值。

那么这个是如何做到的呢?

我们要注意,fork本身也是一个函数,即它也有自己实现的代码

当一个函数已经return的时候,它的核心工作已经做完了。这就意味着。在返回之前已经创建好了子进程了。

而在return之前函数体内部一定会发生下面这些事情

  1. 创建子进程PCB
  2. 填充PCB对应的内容
  3. 让子进程和父进程指向同样的代码
  4. 父子进程都是有独立的task_struct,可以被CPU同时调用运行了

而创建好子进程后,由于父子进程代码共享,而return也是代码,所以return也是会被共享的

所以父进程调度执行的时候,返回一次,子进程调度执行的时候,也返回一次

所以这个fork函数最终返回了两次

  1. fork函数究竟在干什么?干了什么?(续)

前面说过,子进程创建好PCB以后,会共享同一块代码,那么数据呢?

首先在任何平台,进程在运行的时候,是具有独立性的,也就说当我们的qq崩了以后,并不影响其他软件

所以,父子进程既然是两个进程,那么他们就不能同时访问同样的空间。

因为数据可能会被修改,所以不能让父进程和子进程共享同一份数据。

而代码是不可被修改的,所以可以共享同一份代码,因为并不影响独立性。

所以说,子进程要将这份数据单独拷贝一份

可是有可能父进程创建出来的变量,子进程并不会去使用。这样使得子进程的利用率很低

所以操作系统变决定不这样直接去拷贝,而是一开始先共享代码和数据,但是当子进程要对某个数据进行修改的时候,采取拷贝一份这个这个数据。

所以只有当子进程要尝试修改某个数据的时候,才会去拷贝一份。这也叫做数据层面的写时拷贝

  1. 一个变量怎么会有不同的内容?

当我们理解了fork函数究竟在做了什么,我们也就随之理解了一个变量怎么会有不同的内容

return就是写入的过程,id就是父进程的数据。

所以对于子进程的id就是就会专门创建一个空间用来存储,要发生写时拷贝

所以最终返回值是不同的

所以说,我们在看我们之前的代码,我们也就能理解这些现象了

  1. 当父子进程被创建好,fork以后,谁先运行呢?

其实谁先运行是不确定的,是由调度器来决定的

也就是说,现在有很多个进程,CPU应该挑选哪一个进程来运行呢?这个是由调度器来决定的

所谓调度器,就是一套执行数据结构查找的相关算法,它会在这些进程中找到一个合适的进程放到CPU中。

像我们现在的计算机,调度器要尽可能保证公平,时间都比较均衡。

11.bash用fork创建子进程

我们还是看下面的运行结果,代码同上

我们现在已经差不多了解了这些PID和PPID这些信息了。

那么对于原来的那一个进程而言,它也是一个子进程为11129,它的父进程8464

而这个8464正好就是bash命令行解释器的进程。我们也知道这是为了保证该进程不会影响其他进程的

那么既然这里是通过创建子进程的方式完成的,那么bash如何创建子进程呢?

其实就是利用fork,让这个fork出来的子进程去执行解释新的命令。而它自己就继续接收用户的输入。

相关文章
|
2天前
|
消息中间件 算法 Linux
【Linux】详解如何利用共享内存实现进程间通信
【Linux】详解如何利用共享内存实现进程间通信
|
2天前
|
Linux
【Linux】命名管道的创建方法&&基于命名管道的两个进程通信的实现
【Linux】命名管道的创建方法&&基于命名管道的两个进程通信的实现
|
2天前
|
Linux
【Linux】匿名管道实现简单进程池
【Linux】匿名管道实现简单进程池
|
2天前
|
Linux
【Linux】进程通信之匿名管道通信
【Linux】进程通信之匿名管道通信
|
3天前
|
存储 Linux Shell
Linux:进程等待 & 进程替换
Linux:进程等待 & 进程替换
29 9
|
3天前
|
存储 Linux C语言
Linux:进程创建 & 进程终止
Linux:进程创建 & 进程终止
24 6
|
1天前
|
Linux 数据库
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
|
1天前
|
Unix Linux 调度
linux线程与进程的区别及线程的优势
linux线程与进程的区别及线程的优势
|
1天前
|
Linux 调度 C语言