Linux进程概念


这篇博客,我们介绍进程的一些基础概念
首先,在提到进程之前,我打算从“管理”这个词入手,再接着引入进程的概念。

冯诺依曼体系结构

首先,大家可以想象一下,我们从键盘输入数据,接着显示器上显示出数据,是个怎样的实现过程呢?它并不是一步到位的,而是经过了下面冯诺依曼体系结构从而做到的,先上图。
在这里插入图片描述
在这上面,输入设备有我们常说的键盘,鼠标,摄像头,话筒,等等,输出设备有显示器等等。中央处理器也就是我们常说的CPU,它包含了运算器和控制器,存储器就是我们的内存部分。

我们从键盘上输入数据,到显示器上显示数据。实际上是输入设备键盘先将数据和代码给到存储器,CPU对数据进行读取后,再给到存储器,由存储器再给到输出设备显示器上显示出来。
数据要在计算机体系结构中进行流动,流动过程中,对数据进行加工处理。从一个设备到另一个设备的过程本质是一种拷贝,那么数据设备间拷贝的效率,影响计算机整机基本效率。

接着说说冯诺依曼体系结构的优势之处:
之所以数据要通过内存进行传输,是因为输入设备和输出设备的运行效率实际会很低,通过经手内存,CPU再经过快速的计算,可以提高传输的效率。

在该体系中规定,数据不直接与CPU打交道,而是先和内存进行打交道。也就是说输入和输出设备的数据一定经内存再到CPU的,而不是直接到达CPU。

操作系统

接着浅浅聊聊操作系统,并通过操作系统我们要引进“管理”的概念。
但我们先区分一下:

操作系统和CPU的区别

CPU和操作系统是密切相关的。①操作系统运行在硬件系统之上,通过CPU指令集与硬件进行通信和控制。②操作系统为应用程序提供硬件抽象层和标准接口,使得应用程序可以方便地运行。③操作系统也负责调度进程、分配内存、处理中断等任务。④操作系统有最高的特权级别,可以控制应用程序的权限和对CPU的访问。⑤CPU执行操作系统和应用程序的指令,完成各种计算和处理。
简单的说,CPU要负责执行操作系统所发出的指令。那么对于上面操作系统功能的描述,看完接下来对于操作系统的描述后会更加清晰。

操作系统的实现原理

操作系统包括操作系统的内核+操作系统的外壳周边程序,是一个进行软硬件资源“管理”的软件。
这里借用一张图给大家进行说明:在这里插入图片描述
操作系统对下面向硬件,对上面向用户。
这里注意两个点就好:
①操作系统向下对硬件的管理是通过驱动层访问到硬件的。
因为操作系统只有一个,但硬件会有很多个,很多种。 在更新硬件或操作系统时,为了使得两者之间的兼容性,也就是不造成更新其一,得两者都实现更新的情况。我们有了驱动层进行隔离。通过驱动层对硬件管理起来,每个硬件在驱动层都有一个接口,通过这个接口对接操作系统。这样就把两者都给分开了!
②操作系统向上对用户通过系统调用接口对接用户,用户不允许直接访问到操作系统。
因为用户如果直接访问操作系统的话,万一对操作系统进行了数据修改,将造成不可挽回的损失。所以有了系统调用接口,用户要实现的指令,通过这个接口连接到操作系统进行实现。

为什么要有操作系统?

操作系统的本质是向下对软硬件进行管理,向上为用户提供安全、高效、稳定的运行环境。

管理的本质

在操作系统中所谓的内存管理,进程管理等等。“管理”的概念引进:管理的本质是先描述再组织
例如,在学校校长对学生进行管理,实际上不会时常见到学生的人,对学生管理的手段是通过获取到学生的一些基本信息进行管理,学号,姓名,电话等等。对学生的成绩进行管理,也是去获取到学生成绩的数据。 通过先描述(得到数据),再实现组织(进行学生管理)。
有了管理的理念,我们就可以引进进程了。

进程

概念:进程就是程序的一个执行实例,也就是正在执行的程序,它担当的角色是分配系统资源的实体。

进程不止于可执行程序的代码和数据,它还包括一个PCB的概念。
进程 = PCB + 自己的代码和数据
PCB是进程控制块,是操作系统管理控制进程运行的信息集合。操作系统用 PCB 来描述进程的基本情况以及运行变化的过程。

接着上图,看看进程PCB的抽象图
在这里插入图片描述
上面说过,硬盘通过内存再到CPU。 再这里的PCB方便大家理解,大家可以去联想上面说的“管理”的概念。在这里,为了对内存中的这些数据进行管理,PCB相当于获取到这些数据的一些属性,从而便于了对这些数据的管理。

每个进程都有对应的一个PCB!
PCB中有一个类似于 struct PCB* next 的指针这也说明了我们对进程的管理就可以通过PCB来进行对应的管理。进一步理解,我们对进程的管理就可以理解成对PCB这个链表进行增删查改。
在Linux中,我们把这个结构体取名为task_struct

进程的动态运行

进程的动态运行,本质是通过调度运行进程,让进行控制块进行排队。
在这里插入图片描述

pid和ppid

假设有一个可执行程序myprocess
我们执行 ./myprocess 实际上就是启动运行了这个进程。
对于每一个进程都要有一个唯一的标识符,这个标识符就是进程pid
pid 是pid_t类型的,也就是unsigned int 无符号整数。

如果我们想要获取pid,我们可以使用getpid()的方式。因为我们不能直接访问操作系统,所以我们得通过系统调用接口,也就是使用getpid()的方式获取pid
一个进程除了pid,还有ppid,也就是父进程的pid值。那么我们使用的接口就是getppid()获取ppid

每次启动时,对应的pid都不一样,这属于正常的情况!

父进程的代码和数据是从磁盘中加载出来的。子进程默认继承父进程的代码和数据。
可是有父进程可以直接执行了,为啥还要创建子进程呢?

有了子进程,父子进程就可以执行不同的操作。

 1 #include<stdio.h>
 2 #include <sys/types.h>
 3 #include <unistd.h>
 4 
 5 int main()
 6 {
 7   printf("i am a father process,pid:%d,ppid:%d\n",getpid(),getppid());
 8   sleep(5);
 9   pid_t id = fork();     //这里我们使用fork()函数来实现父子进程执行不同的操作
10   if(id == 0)
11   {
12     printf("i am a child process,pid:%d,ppid:%d,我返回的值是:%d\n",getpid(),getppid(),id);
13   }
14   else{
15   printf("i am a father process,pid:%d,ppid:%d,我返回的值是:%d\n",getpid(),getppid(),id);                                                                                     
16   }
17 }

上面代码的输出结果:
在这里插入图片描述
这里,我们来一起认识一下fork()函数:
在这里插入图片描述
所以上面的代码,id 才可以有两次返回值, 不同的返回值对应父子进程使得又可以实现不同的功能。

进程状态

进程状态分为多种:
R : 称为运行状态。 这个运行状态并不表明程序一定在运行中,它可能表示进程正在运行也可能表现为进程正在运行队列里。
S : 睡眠状态,也叫可中断睡眠。此时的进程在等待事件的完成(例如接收数据时的等待)。
D : 深度睡眠状态,也叫不可中断睡眠。此时进程在等待IO的结束。
T和t : 停止状态。此时进程处于暂停的状态,此时进程在等待下一步操作。
X :死亡状态,该状态是一瞬间的,难以捕捉住这个状态。

这里就着D深度睡眠状态再进行一下理解:
Linux操作系统有权杀掉进程来释放空间。
那么在磁盘传入数据给内存中的进程时,需要一定的时间。 假如此时的操作系统因为要需要释放空间,将正在接收数据的进程给杀死,此时的磁盘就找不到这段进程也就无法传入数据了,因此可能引发数据丢失! 所以为了避免此情况,这段进程就需要深度睡眠,深度睡眠也叫不可中断睡眠,此时的进程就不能被操作系统给随意杀死了。

僵尸进程

僵尸进程 : 当子进程退出,父进程还没退出,父进程还没有读取到子进程退出返回的代码就会出现僵尸进程,子进程进入Z状态。(Zombies)
如果父进程不对子进程进行回收,那子进程就会一直处于僵尸状态。

僵尸进程的危害: 一个父进程可能会创建出多个子进程,如果这些子进程都不被释放掉的话,就会导致多块内存被浪费。 除此之外,还会消耗进程的数量,因为操作系统的进程的数量有一定的限制,如果存在大量的僵尸进程,会消耗一定的进程ID,导致无法创建新进程。

僵尸进程的验证

 1: test.c  ⮀                                                             ⮂⮂ buffers 
  1 #include<stdio.h>
  2 #include <sys/types.h>
  3 #include <unistd.h>
  4 #include<stdlib.h>
  5                                                                                    
  6 int main()
  7 {
  8   pid_t pid=fork();
  9 
 10   if(pid==0)  //子进程
 11   {
 12       printf("child id is %d\n",getpid());
 13     printf("parent id is %d\n",getppid());
 14   }
 15   else  //父进程不退出,使子进程成为僵尸进程
 16   {
 17     while(1)
 18     {}
 19   }
 20   exit(0);
 21 }

如上代码,我们设置僵尸进程的状态。让父进程持续进行,子进程就得不到回收,子进程就会进入僵尸状态。 ps axj 查看进程状态
在这里插入图片描述
此时我们运行这个程序就可以观察到僵尸状态了!

(Ctrl+z是暂停,Ctrl+c是终止)
此时,我们使用Ctrl+z ,将程序暂停,我们可以观察到该程序依然是僵尸状态。
又或者我们执行make clean,清除这个可执行程序,它依然是僵尸状态。因为此时的进程是在内存中的,此时如果我们不手动将这个进程释放,那它就会一直保持僵尸状态!!!
在这里插入图片描述

kill -9 进程号 杀死进程

那么我们可以使用 kill -9 进程号的方式来删除该进程(由上面的图可以看到30976这个数字就是该进程的进程号!)
在这里插入图片描述

Z 和 Z+ 的差别

先看图:
在这里插入图片描述
在这里我们可以看到起初运行程序是在前台的,状态为Z+ ;当我们使用Ctrl+z之后,进程就被挂在了后台,显示的就是Z
所以我们得到结论:
Z,不带加号,表示在后台!
Z+,带加号,表示在前台!

孤儿进程

孤儿进程: 孤儿指的是父进程比子进程结束得更快,导致子进程失去了父进程,此时子进程的父进程会变成系统进程(通常是1号进程init进程)

孤儿进程的验证
代码与僵尸进程类似,将子进程设置成死循环,让父进程先结束就好了。

  1 #include<stdio.h>  
  2 #include<stdlib.h>  
  3 #include<unistd.h>  
  4 #include<sys/types.h>                                                              
  5                          
  6 int main()               
  7 {                        
  8   pid_t pid=fork();      
  9                          
 10   if(pid==0)         
 11   {           
 12     printf("child ppid is %d\n",getppid());
 13     sleep(10);     //为了让父进程先结束      
 14     printf("child ppid is %d\n",getppid());  
 15   }                                          
 16   else                                       
 17   {     
 18     printf("parent id is %d\n",getpid());
 19   }                                        
 20                                            
 21   exit(0);
 22 }           
~

在这里插入图片描述
我们可以看到,起初child的parent是9221,后来父进程先结束,子进程成为了孤儿进程,子进程的父进程变成了操作系统1号。

孤儿进程相比于僵尸进程,它会更安全一点。因为它有系统对它进行回收,而僵尸进程处理不当就会造成空间浪费了!

时间片

我们的进程在运行的过程中,会基于时间片进行轮转。也就是说,一个进程如果迟迟未响应,时间片轮转,就会接着进行下一个进程。
在这里插入图片描述

让多个进程以切换的方式进行调度,在一个时间段内推进代码,称为并发
任何时刻都有多个进程在同时运行,称为并行

阻塞态

进程在等待的状态,我们可以理解为阻塞态。
例如,计算机等待我们输入数据的时候,就会进入阻塞态。

我们的应用设备中,都有自己的wait_queue。 比方说上面说的等待我们键盘输入数据,那么如果我们不进行输入操作,此时进程的task_struct就会被放到设备的wait_queue中去。当输入好了,又会把该数据移动到对应的运行队列中去!
在这里插入图片描述

挂起态

由于OS中内存有时会很紧张,所以为了更合理的使用内存资源,就有了挂起态的概念。
挂起态表示的是暂时不使用的进程会堆放到磁盘中去。
在磁盘中有个swap分区,当数据再次需要时,会交换回去这块空间。

这个过程会造成时间的消耗,这属于用时间换空间的做法,但这样可以减轻内存的压力!
在这里插入图片描述

进程切换

进程在切换的过程中,有一个非常重要的事情要做:对进程上下文数据进行保护和恢复

CPU的寄存器硬件只有一套,但进程会有多个。也就意味着,多个进程会争先使用到CPU。
CPU对进程属于一对多的情况! 我们的进程在切换的过程中,CPU需要将进程上下文保存起来,如果不保存起来,下一次再调度到该进程时,就会导致该进程的数据已经找不到了!

有个小例子,例如在写ADD函数时,最后返回值传给某个值时,这里就相当于通过寄存器返回!

优先级

我们上面说了进程切换,那么思考一个问题,进程切换的顺序是怎么样的呢?有没有规律可言?
这是就有了优先级的概念。优先级,顾名思义,优先级决定着进程调度,切换的顺序。
Linux中,进程优先级数字越小,进程优先级越高!!
有了优先级,就可以分时操作系统,实现基本的公平。

在这里插入图片描述
PRI :进程优先级
NI : 进程优先级的修正数据
新的优先值由两部分组成: PRI = 默认的优先级值(一般是80) + NI
NI 可以进行修改,但不推荐!
要修改的话,可以先输入top,接着进入top后按“r”–>输入进程PID–>输入nice值

NI也是有修改范围的! NI的范围是[-20,19],共40个数字。(至于为什么是40个数字,与活动队列、过期队列有关,这个后面再提!)
在这里插入图片描述
例如将NI修改成-10,PRI就被修改成了70。

命令行参数

命令行参数本质是交给程序,通过不同的选型,制定不同的程序功能。

例如我们使用ls,后面还可以跟各种选项实现不同的功能,这本质就是命令行的一部分!

命令行中启动的程序,实际是一个进程,是属于bash的子进程!

环境变量

环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。

Linux中,存在一些全局的变量。这些全局的变量告诉命令行解释器,去对应的路径寻找可执行程序,环境变量就是这样!

PATH 是环境变量, $PATH是打印环境变量的内容
echo $PATH 可以查看自己的环境变量内容
环境变量在系统对应的配置文件中!

环境变量指令

env —用来查看所有的环境变量(系统和本地的都显示出来)
echo xxx — 用来查看指定的环境变量
export name = value —用来创建环境变量
unset name —用来取消环境变量
set —用来显示本地定义的环境变量

在这里插入图片描述
这里要注意的是: export 创建的环境变量也只是临时性的,重新启动服务器时,又会消失该环境变量。因为每次启动服务器都是去调用系统中的环境变量。除非在系统环境变量文件中进行修改,但不推荐这样做。

环境变量有系统环境变量和我们本地环境变量之分

例如 pwd,ls等等指令都是在系统中的
我们假设想运行可执行程序使用 ./myprocess 这里就不行,因为这属于我们本地的环境变量
如果想把它变成系统环境变量,需要去修改系统环境变量的配置文件,但不推荐修改!

环境变量很多,bash内部为了组织起来,会类似形成一张表
在这里插入图片描述

命令行参数也与其类似,在bash进程启动时会自动形成两张表。一张是命令行参数表,一张是环境变量表。 bash通过各种方式交给子进程。

这里最后再提及一个小点,echo和export属于内建命令。80%的命令都是由bash创建子命令执行的,而echo和export由bash亲自执行。

地址空间

在我们之前讲的task_struct到内存之间其实是有地址空间这个概念的,地址空间存放的都是虚拟地址,利用虚拟地址通过页表对内存进行访问。接下来详细说说:
在这里插入图片描述
这是一个基本的图况,地址空间不同的区域上的数据都会有对应的虚拟空间。这些虚拟空间会传给页表,页表转换成物理地址后就可以去访问内存了。当然页表还有的列会存放一些数据,这些我们暂时先不去了解,主要关注前两项。

那么设置地址空间以及虚拟地址的好处是什么呢?
实际地址空间内部的属性都被划分出来了,就如上面的图一样,把它划分成类似的样子。因为在内存中,不同的区域的数据并一定是连续在一起的。所以看起来其实是很乱的,也很难找。
但有了地址空间,①可以让无序变成有序,至少我们可以以表为标尺,因为这些数据都可以通过页表去找到对应的地址空间。②可以使得进程管理模块和内存管理模块进行解耦。还可以拦截非法请求,变相保护内存空间。 好比一个虚拟地址传给页表,页表发现压根没有这个地址的位置,那么就可以进行拦截了。 如果虚拟空间在页表中有位置,但没有对应内存空间的地址,可能已经将对应数据挂起给硬盘,这就叫作缺页中断(这个暂时不作了解)。

我们来看一组现象:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 
  6 int main()
  7 {
  8   int g_val = 100;
  9   pid_t pid=fork();
 10 
 11   if(pid==0)
 12   {
 13     int cnt = 0;
 14     while(1)
 15     {
 16       printf("i am child process,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
 17       sleep(1);
 18       cnt++;
 19       if(cnt == 3)
 20       {
 21         g_val = 200;
 22         printf("the g_val is changed!\n");
 23       }
 24     }
 25   }
 26   else
 27   {
 28     while(1)
 29     {
 30       printf("i am parent process,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);                                                                      
 31       sleep(1);
 32     }
 33   }
 34   exit(0);
 35 }

我们设置循环,起初父子进程指向同一块空间,值也相等。 循环3次以后,对子进程中的g_val值进行修改,父进程的g_val值不变。看看现象:
在这里插入图片描述

我们改变了子进程中g_val的值,却发现父子进程指向的g_val的空间依然是一样的,但值又不一样了。这是否有些反常?
在这里插入图片描述

假定左边为父进程,右边为子进程。起初他们指向同一块空间。 当我们要修改子进程中g_val的值时,由于要修改值,此时会发现写时拷贝!!
在这里插入图片描述
在这里子进程就指向了一块新空间。但上面不是显示的父子进程地址还是一样的吗?这是因为我们这里实际修改的是对应页表物理空间的那一块,页表当中虚拟地址并没有变! 因此显示出来,父子进程指向的空间一样但是值不相同。

还记得我们曾经写过的fork()函数吗,它有两个返回值对吧!
这里实际就是发生了写时拷贝,两个不同的值使得子进程发生写时拷贝,有了对应页表不同的物理空间。

进程调度队列

runqueue,活动队列和过期队列

这个我们浅浅聊一下。
一个CPU拥有一个runqueue。
活动队列和过期队列都有这样重要的3个参数。
在这里插入图片描述
nr_active: 总共有多少个运行状态的进程。
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级。

我们将时间片还没有结束的所有进程都按照优先级放在活动队列中,将时间片耗尽的进程放在过期队列中。

active指针和expired指针

active指针永远指向活动队列。
expired指针永远指向过期队列。

活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为时间片耗尽的进程都放到了过期队列上。 那么当活动队列被清空时,就会对活动队列与过期队列进行交换!!相对于又可以重新循环使用起来了,就相当于有具有了一批新的活动进程!但是active和expired指针还是分别指向活动队列和过期队列,这个不改变!

在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!

补充的内容

进程和作业的差别

在这里插入图片描述

名词解释

在这里插入图片描述

xshell,shell,bash的区别

在这里插入图片描述

家目录和根目录的区别

在这里插入图片描述

  • 22
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值