目录
1、冯诺依曼体系
2、Linux下的进程概念
3、PCB结构体
4、在Linux下查看进程
5、父子进程
6、终止进程
7、操作系统的进程状态
7.1 Linux下的进程状态
8、孤儿进程
9、进程优先级
9.1 PRI和NI
结语
前言:
进程作为操作系统中最核心的知识点之一,也是最重要的一环,正因为有了进程的概念,我们在使用电脑时才可以同时打开多个软件,我们给电脑的每一个指令在操作系统的层面上几乎都可以看成是一个进程,在Linux下也如此,Linux下之所以可以处理多种指令也离不开进程的作用,本文着重介绍Linux下的进程概念。
1、冯诺依曼体系
在Linux下,当执行一个可执行程序或者执行一条指令时,都是让cpu对其进行计算的,因为在冯诺依曼体系中,cpu担任计算机系统的大脑,运行的任何文件包括指令,之所以可以在屏幕上看到运行的结果,都是因为cpu对其进行了计算。
冯诺依曼体系结构如下:
在该体系中,所有需要让cpu进行计算的任何程序或者指令,都必须加载到内存中,才能让cpu进行计算,换句话说,cpu只和内存打交道。
而以上最主要的一点是:在这些指令或者程序被加载到内存中时,就变成了所谓的进程,详细如下文。
2、Linux下的进程概念
在Linux下可以执行多条指令,原因就是指令或可执行程序在被加载至内存中变成了进程,但事实上操作系统做了更多的事情,因为Linux下的指令非常之多,比如:ls pwd cd..等等,这些指令从名称就能够看出来没有什么特别相似的地方,因此管理起来非常不方便,若连管理都无法做到,更别提让cpu去调度这些杂乱无章的进程了。
因此在指令被加载到内存中时,系统会创建一个描述该指令属性的结构体PCB,对每个指令亦是如此,这样一来就能够将指令的相同属性提取出来,然后通过管理结构体PCB间接管理这些杂乱无章的指令(比如用链表的形式管理PCB,能够更好的实现增删查改的功能),所以一个真正意义上的进程=描述该指令的PCB+该指令的具体实现内容。(用PCB来管理对应的指令,就好比学校里用学生的各项成绩来决定学生的评分,目的就是为了更好的管理庞大的群体)
详细结构图如下:
以管理PCB结构体的数据结构间接来管理各种指令,也就是说cpu调度的是PCB结构体而非指令本身。
3、PCB结构体
PCB结构体是操作系统中对描述进程属性的统一集合的名称,在Linux下PCB的结构体名称是task_struct,他们的作用都是将指令的共有属性描述出来,目的就是方便管理指令,更好的让cpu调度进程。
task_struct的具体内容如下:
1、标识符: 描述某一个进程,以区分其他进程,是进程的唯一。
2、状态: 运行状态、阻塞状态、挂起状态等等,Linux下表示状态的符号为:R、S、D、T、t、X、Z。
3、优先级: 优先级高的进程会先被cpu调度,低的相反。
4、程序计数器: 保存即将执行的下一条语句的地址。
5、内存指针: 为了能够找对PCB对应的指令或者程序。
6、上下文数据: 当一个进程被cpu切出去时,会保存该进程被切出去时的临时数据,以便该进程下一次进入cpu时无需从头开始计算。
7、I/O状态信息: 保存进程涉及到的各种I/O设备。
8、记账信息: 保存处理器的使用时间总和。
4、在Linux下查看进程
在Linux下有两种常用的方法可以查看当前系统的进程,1:用指令ps axj,2:ls /proc。用指令ps axj的运行结果如下:
从上图中可以得出一些信息,每个进程都有属于自己的进程pid,但是也会有一个父进程ppid,可以理解为每个进程都会有一个父进程,和二叉树的根结点与子结点概念相似,进程是其父进程的一个分支。
proc是一个目录,里面存放了所有进程的pid:
可以发现proc目录下的进程pid可以和上面图中的进程pid对应起来。并且可以在proc内根据进程pid找到对应的执行指令和详细信息,具体如下:
特别注意:在Linux下,我们提出的每一条指令,都会被当作是一个进程,因为指令要被cpu进行计算,而要被cpu计算则必须是进程,所以系统会自动分配一个子进程帮助我们执行各种指令,解释图如下:
5、父子进程
pid表示一个进程的pid号,这是该进程独有的编号,而该进程ppid表示其父进程的pid号,每个进程都有pid和ppid,并且可以通过系统调用接口清楚的看到一个进程的pid和ppid。
getpid()和getppid()的接口信息如下:
测试结果如下:
从该进程的父进程pid可以看到,父进程pid为21533和上面测试进程的父进程pid是一模一样的,因此当我们登录一个用户时,系统会为该用户分配一个专门的进程(bash),这个进程会分支出无数的子进程供用户执行指令,所以我们执行指令的父进程都是bash进程。
6、终止进程
终止进程的概念对应windows下的从任务管理器结束一个进程,其在Linux下的指令为kill -9 要终止的进程pid,测试如下:
7、操作系统的进程状态
在操作系统的层面上,进程分为三种状态,即运行状态、阻塞状态、挂起状态。这里涉及到等待队列的概念,等待队列即一个进程的PCB结构体在等待cpu的调度或者外部资源的输入,这时候PCB会被放入对应的等待队列中。比如PCB结构体准备让cpu调度,则PCB结构体处于cpu调度的等待队列中。
1、运行状态:即该进程被加载至内存中准备被cpu进行调度,称之为运行状态。
2、阻塞状态:即一个进程被cpu调度时,该进程需要外部的输入资源,比如cin,scanf等输入键盘资源的情况时,该进程会从cpu中抽离出来等待外部资源输入后再进入cpu,把这个等待的过程称之为阻塞状态。
3、挂起状态:即在阻塞状态的条件下,当下系统资源严重不足时,即使该进程在等待外部资源的输入时,也会将该进程的代码部分抽离等待行列,并放入磁盘中,只留下PCB结构体在等待队列中。
7.1 Linux下的进程状态
Linux下的进程状态分布如下:
R (running)运行状态
S (sleeping)睡眠状态
D (disk sleep)深度睡眠状态
T (stopped)停止状态
t (tracing stop)跟踪停止状态
X (dead)结束状态
Z (zombie)僵尸状态
R状态与S状态的测试如下(还是用上面的循环程序举例):
R表示运行,S表示睡眠(对应阻塞)。+号表示在前台运行,即此时的命令框不能输入任何指令。 该循环程序的状态是S的原因在于,cpu每次读取进程会有一个时间片的概念,即一个进程在cpu中待的时间不能够超过10ms,超过了就会把该进程从cpu拿下来,否则cpu遇到死循环的程序则不能够计算其他的进程了(事实上我们运行了一个死循环程序,还可以输入其他的指令),所以cpu会将该死循环的程序拿下来去计算其他的进程,那么该进程就会被处于一个睡眠状态,即S状态,他在等待某种特定的资源后重新被唤醒并进入运行状态。
当进程在写入数据时比如写进磁盘中(前提是处于高度IO的状态,可以理解为是S状态的深度版本),属于D状态(深度睡眠状态),D状态表示该进程会等待至I/O结束后才表示该进程结束,这个过程中该进程不能被操作系统强行中断。
t状态指的是当用gdb进行调试某个可执行程序时,对其中代码设置断点,这时候该进程就处于t状态。
而用指令kill -19 进程pid,可以使该进程的状态设置为T。
最后X状态和Z状态是对应的,即当一个进程结束了则该进程的状态为X,但是这个过程极快,无法观察捕捉到。
若当一个进程已经结束了,但是该进程的父进程没有回收他的PCB资源(即父进程还在运行),则该进程就会处于X状态的前一个状态Z(僵尸状态), 表示该进程的代码和数据可能已经释放了,但是该进程对应的PCB结构体依然存在。
测试僵尸状态下的现象:
1 #include<stdio.h>2 #include<unistd.h>3 4 int main()5 {6 pid_t in = fork();//fork创建一个子进程7 if(in==0)8 {9 while(1)10 {11 printf("这是一个子进程:PID: %d PPID:%d\n ",getpid(),getppid());12 13 sleep(5); 14 return 1;//结束子进程15 }16 }17 else18 {19 while(1)20 {21 printf("这是一个父进程:PID: %d PPID:%d\n ",getpid(),getppid());22 23 sleep(1);24 25 }26 }27 return 0;28 }
测试结果:
若进程处于僵尸状态,那么最终导致的危害就是内存泄漏,因为该进程虽然代码数据资源被释放了,但是在系统中,该进程的PCB结构体一直没有被父进程回收,而PCB结构体被创建出来也是需要占用内存资源的。
8、孤儿进程
上述的僵尸进程指的是父进程不退出,而子进程先退出了,导致僵尸进程,而孤儿进程刚好与其相反,孤儿进程表示子进程还没退出,而父进程已经退出了,这时候父进程就会被其父进程(bash)回收,但是该进程的子进程还在运行,这时候该子进程就变成了孤儿进程。
孤儿进程测试代码:
1 #include <stdio.h>2 #include <unistd.h>3 #include <stdlib.h>4 int main()5 {6 pid_t id = fork();7 if(id < 0){8 perror("fork");9 return 1;10 }11 else if(id == 0){//child12 printf("I am child, pid : %d\n", getpid());13 sleep(10);14 }else{//parent15 printf("I am parent, pid: %d\n", getpid());16 sleep(3); 17 exit(0);//父进程先退出18 }19 return 0;20 }
测试结果:
子进程的ppid从6422变成了1,因为该子进程的父进程已经结束了,所以该子进程变成了孤儿进程,但是必须得有人来“领养”这个孤儿进程,否则该孤儿进程结束后会变成僵尸进程,就会一直导致内存泄漏,于是该孤儿进程就被分配给了操作系统,操作系统的pid即为1。
注意此时的子进程的状态没有+号,表示他现在是一个后台进行的进程,前台无法使用ctrl + c结束该进程,只能使用kill -9指令将其结束。
9、进程优先级
首先cpu一次只能计算一个进程,只是cpu的计算速度非常快,以至于我们在使用操作系统的时候感觉像是多个进程同时在被计算,实际上只是cpu以极快的速度来回不断的计算内存中的进程,上文提到进程在cpu中待的时间不能超过10ms,就是为了可以让cpu处理其他进程,因为cpu一次只能计算一个进程。
所以cpu对于进程来说是稀缺资源,当然cpu已经尽力保证每个进程都可以被调度到,但还是可以通过调整进程的优先级提高某个或降低某个进程被调度的先后顺序。
优先级的标识符如下:
默认可执行程序的PRI是80,NI是0。
9.1 PRI和NI
PRI是表示进程的优先级,那么NI就是调整该优先级的一个手段,他们的关系为:PRI = PRI + NI。 比如将一个进程的优先级调高,那么需要把NI的值设为负数,比如NI = -10,那么PRI = 80+(-10)=70,则调整后的PRI的优先级比原来提高了10个等级。(注意:NI的范围是[-20,19])
调整NI的指令操作如下,首先用指令top打开进程界面,然后按“r”,要求输入需要更改优先级的进程pid:
输入进程后,输入NI的值,注意NI的范围:
这里我们输入10,表示降低8820进程的优先级,则8820的进程PRI应该变成了90,最后通过ps -al查看进程优先级,如下:
结语
以上就是关于Linux下进程的讲解,进程作为操作系统中最重要的一环,他实现了操作系统可以高效率处理多个不同指令的功能,其功劳离不开冯诺依曼体系,因为该体系让计算机性价比达到最高,当然在此体系上,进程的本质是因为指令被对应的PCB结构体所采用数据结构的方式进行管理,对指令本体的执行变成了对数据结构的增删查改,因此大大提高了效率。
最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!