【linux】:进程状态(僵尸进程等)以及环境变量

文章目录

  • 前言
  • 一.进程状态
  •     进程的优先级
  • 二.环境变量
  • 总结

前言

  本篇文章是接着上一篇【linux】:进程概念的后续,对于有基础的同学可以直接看这篇文章,对于初学者来说强烈建议大家从上一篇的概念开始看起,上一篇主要解释了冯诺依曼体系以及操作系统的概念还有在linux系统中进程是什么样的,如何去查看一个进程,如何给一个进程多开一个子进程以及为什么fork()函数可以有两个返回值的问题。


一、进程状态

为了能更深刻的理解linux中的进程状态,我们把linux中的内核源代码拿出来看一下:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

task_struct是一个结构体,内部会包含各种属性,其中就包括状态,如下图:

3df939f7365247b194331616c0242f2d.png

我们先来讲解阻塞和挂起这两个重要的概念,阻塞就是进程因为等待某种资源就绪,而导致的一种不推进的状态。我们经常可以看到不管是手机还是电脑当打开的软件很多的时候,就有出现应用卡顿的情况,这是因为当我们打开很多的软件的时候进程也变多了,操作系统调度不过来了,这个时候卡的那个进程就是阻塞了。再比如说我们下载一个软件,下载了一半没网了,这个时候下载进度就不动了,这个时候这个进程就变成了阻塞状态,只有当网络好了能继续下载了CPU才会继续调度这个进程,所以这个进程卡住了是在等待某种资源就绪,当资源就绪了就会被CPU调度取消阻塞状态。所以进程要通过等待的方式,等具体的资源被别人用完之后,再被自己使用。那么进程等待某种资源就绪的过程中,资源指什么呢?这里的资源指软硬件资源,比如:磁盘,显卡,网卡等各种外设。下面我们用图解释一下阻塞的过程:

2ae1295df6334863ada20d33b28ce3c7.png

我们前面讲过操作系统对于软硬件的管理是先描述在组织,所以对于网卡磁盘等也是通过struct来描述的,当CPU正在跑一个进程的时候,这个进程突然没网了这个时候就将这个进程先变成阻塞状态,然后看下图:

d567697866df45b392b8e42d1f11d9b0.png 这个时候因为网络的问题进程需要等待网络恢复才能继续在CPU上运行所以这个进程就会链接在网卡的尾部等待网卡资源就绪也就是网络恢复才可以正常运行。所以PCB是可以被维护在不同的队列中的。

阻塞:阻塞就是不被CPU调度。一定是因为当前进程需要等待某种资源就绪。一定是进程task_struct结构体需要在某种被OS管理的资源下排队。

下面我们解释一下挂起的概念:

e502b206bc824aa1b851ed5293bc2b46.png

 上图是一个进程正在被CPU调度,然后突然没网了,看下图:

afbc5920d8074e9e8bba28255f603dfd.png

这个时候进程进入阻塞状态等待网卡设备就绪,由于内存中空间有限所以对于阻塞状态的进程的代码和数据来说无疑是浪费空间的,所以操作系统会先将阻塞状态的进程的代码和数据放入磁盘中,将内存中的代码和数据释放掉。

c8392f05a4c54287895a92073efe7a74.png

等过了一段时间,网卡设备就绪了,这个时候进程会继续被CPU调度,在这之前需要把磁盘中的代码和数据继续放入内存中

ca9e009b6ca941d9a5e38b34d4984b32.png 以上的将代码和数据先放入磁盘等待网卡设备就绪然后就绪后再将磁盘中的代码和数据放入内存的过程就叫做挂起。

在这里问一个问题,进程是R状态一定是在CPU上运行吗?答案是不一定。进程一般是什么状态,要看这个进程在哪里排队,看下图:

d4a02721d55540829586b381cb1827ce.png

首先我们创建一个.c文件然后写一个死循环代码,然后创建Makefile文件:

d22d81fc78f746f289140395fbc4ba63.png 接下来我们直接运行并且查看当前进程状态:

14dacd07de7f4873844a786d735f697f.png

e7bad5ae41884d2c943545a068d362a3.png 我们先用ps axj | head -n1指令调出进程属性,然后后面加上grep mytest过滤出mytest可执行程序的进程,后面加的grep -v grep是过滤掉grep自己本身的进程,最后成功的显示出这个进程,我们可以很清楚的看到这个正在运行的程序的状态并不是R而是S+,S代表休眠状态,接下来我们将打印注释掉试一下是什么状态:

7e4da7b97e9741e9b23e5b024d3d5d8f.png

77d11feecf4248a8b7ded3609caa15eb.png

7507eb2bd25146bd9f8a6cf3afb38c22.png 我们可以看到当将打印代码注释掉后这个进程的状态变成了R,这个时候为什么是运行状态了呢?因为printf打印需要打印到屏幕上,而屏幕就是一种外设,频繁的往屏幕打印进程会等待屏幕就绪才可以打印,而CPU的运算非常快外设的速度却很慢,所以CPU早就跑完了代码接下来将进程状态设为阻塞状态让这个进程去屏幕后面等待屏幕就绪当屏幕就绪后又会重新被CPU调度然后执行重复的操作。

所以进程是R状态不代表进程在运行,而代表该进程在运行队列中排队。

S状态是休眠状态,可中断休眠。而S状态就是一种阻塞状态。

接下里我们演示一下,首先修改一下代码:

bcbf6fd0ae7f44d08a9efadd4d41cd52.png

2a353a55f92b451490226f37241198b6.png 这个时候我们去看进程状态。

53c6773db78c48dda6a94a5dff508eac.png

为什么是S状态呢?因为进程会等待键盘资源就绪,也就是说只有键盘输入了才叫键盘资源就绪这个时候才会被CPU调度 

e040603ad4e146abb89fe3f4b56abd9d.png

e4acf87bd34842b18a9d48f3d10a3032.png 当我们终止程序后程序就结束了这个进程也结束了,对应了S状态是可中断休眠状态。

D状态也是一种休眠状态,D状态是不可休眠状态。D状态在生活中我们基本不会遇到,就是磁盘基本快满了还在往磁盘存数据,这个时候你就会发现你的电脑非常卡而且不能强制进行任何关闭操作,如果关电源会导致资料丢失所以不可中断只能等磁盘自己恢复。

T状态叫暂停状态,下面我们来演示一下T状态:

a544bbb1cb7e402583c5563d5c66b6d0.png

 我们先讲代码修改一下方便演示:

aa147b94433c422aa89cd103c6eff458.png

6978af082c6f491c8a572a9a736eb61a.png

 运行后我们可以看到这个时候的进程状态为S,这个时候我们使用一个暂停命令:

f25550a2b0ba4f72958ba82284e6faac.png

19号这个命令sigstop为暂停然后我们使用一下:

b6c9f9041b664231bfd65d69319b2099.png8d582a80ebc44450820d3447ad60d2b6.png

2f4d65e576a543cfb805521e5cb46fb7.png 这个时候我们能很清楚的看到进程变为T状态了并且程序确实暂停了,这个时候我们如何恢复运行呢?

f67fd315f1e940098782d7b7107add68.png

这里的18号代码为sigle continue的简写就是继续的意思

995f09920c694de6a1d03fa8ea89f1a2.png 0ac9404a84c5437384add93dee997e03.png

这个时候程序就又可以继续了。但是我们发现这个时候我们用ctrl+c关不掉了,并且进程状态也变了。

83ab6e9f914b498699b2c148de238f9a.png

4903185d9d2c494f9a7b72f2b33a5a7a.png 从原来的S+变成了S为什么就不能终止程序了呢,因为状态后面带+号是代表程序在前台运行,在前台运行的程序可以ctrl + c结束,没有+就变成了在后台运行,这个时候我们只能通过kill命令杀掉这个进程。

ed0135960216496ca661c0b3d6c8d7c9.png

6f502a7fc2cf458090e5ca1eb76d661f.png 7a01e949aa5b43598fd0643605c7b9c8.png

23f291c5a7784085bf1c5304d6f8b663.png 这个时候程序就结束了,不管是前台或后台我们都可以用kill杀掉。

X状态被称为死亡状态,Z状态称为僵尸状态,下面我们解释一下这两个状态:

为什么我们要创建进程呢?因为我们需要进程为我们做事,我们写过C语言代码,知道每个main函数没必须返回0,这是因为我们要知道函数的结果,如果没有返回值我们就无法确定一个函数是否允许,所以我们需要进程的返回值来确定进程的状态。那么如果一个进程退出了立马变成X状态,作为这个进程的父进程,有没有机会拿到结果呢?linux当进程退出的时候,一般进程不会立即彻底退出,而是要维持一个状态叫Z,也叫作僵尸状态,方便后续父进程(OS)读取该子进程退出的退出结果。那么如何看到僵尸状态呢?子进程退出,但是不要回收子进程。下面我们演示一下:

8f42c19aafdc4a36b0bb591279f1ae2e.png

5ec9051a3b7b43ceb60ab8a4ce8c9e1c.png

893c404472b841888f799d563970654e.png 这个时候两个进程都处于S状态,根据我们刚刚所说的只要让子进程先退出就能看到僵尸状态。

59e025d4c1d44a36baddafcf0282dbae.png

370a95e14a794840b2d23bb85aa64cfa.png

03b920da86e44ef28d32b0df02ba37bf.png 我们可以清楚的看到,当我们将子进程杀掉后子进程的状态变成了Z也就是僵尸状态,在mytest后面的单词<defunct>这个单词就有死人的意思,那么僵尸状态在这会占用资源吗?答案是会的,维持僵尸状态会占用资源,僵尸状态必须释放,如果这样的僵尸状态很多那么机器就很容易卡死。而维护僵尸状态的意义就是让父进程能够读取子进程退出的信息。

僵尸状态的危害:

僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护。那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间! 内存泄漏?是的

刚刚我们看到子进程先退就变成僵尸了,下面我们看看父进程先退会怎么样。 

99fd9a5c77c549098a0bce7551ae7744.png

 我们将代码修改一下方便观察,下面我们用命令观察一下:

a96f149c0b7b40d890c2a156d719976b.png

 我们先用shell编程每隔1秒监测一下进程。然后将进程跑起来:

961767d5a92f49b0873bb469384b1a06.png f3034cf04323476eaa0907e833b9b05d.png

 我们可以看到一开始有两个进程,当父进程结束后只剩下了子进程,这个时候我们发现子进程的状态从S+变成S了,也就是说从前台变成后台了。

451607d1174940e0808b443b39a592a1.png4d68ca3353174cb7a3060059716b962b.png

 我们可以看到这个时候已经不能用ctrl+c终止程序了,只能用kill杀掉子进程。那么为什么父进程先退出的时候没有变成僵尸呢?这是因为这个先退出的父进程被他的父进程回收了,他的父进程就是bash。怎么证明呢?在上图中我们发现pid为15529的进程的父进程一开始为15528,当15528退出的时候,15529这个子进程又重新给自己找了个爹pid为1,pid为1的进程我们都知道,这就是操作系统,也就是说,父进程退出,子进程会被OS自动领养(通过让1号进程成为新的父进程),那么这个被领养的进程就被称为孤儿进程。那么为什么我们上面演示子进程退出的时候子进程变成僵尸状态了呢?因为上面我们为了演示出僵尸状态故意没有将代码写完,因为没有等待,所以子进程变成了僵尸状态。那么为什么子进程会被自动领养呢?因为如果不领养就导致没人能找到子进程,一旦子进程退出就没人回收这个进程了,那么这个子进程就是一种游离状态,这样就会造成资源浪费,也就是内存泄漏。

进程的优先级

cpu资源分配的先后顺序,就是指进程的优先权。优先权高的进程有优先执行权力,配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

优先级和权限有什么区别呢?答案是权限代表了能不能的问题,而优先级是你执行的先后顺序,优先级已经确定了你可以干某件事只是取决于先后问题。那么为什么会有优先级呢?因为CPU的资源有限。

我们可以用ps -l命令查看进程中的优先级,如下:

df1fffff63724d0bbbe2924ba3f2d4a4.png

在上面的图片中,PRI代表程序的优先级,NI代表进程优先级的修正数据。PRI的值越小进程的优先级越高,而NI值可以理解为是改变PRI的值从而修改进程的优先值。PRI(新) = PRI(旧)+NI

而在linux系统中,旧的PRI值一定为80。下面来演示一下:

79ffc6fdadad455d8c6fdeeb61458ee9.png 我们先随便写一个死循环程序,然后运行起来。

e4981ebd63204f0e895d8fea845a44b7.png

 可以看到程序已经跑起来了,这个时候用top命令去修改优先级。top进入后输入R,R就是renice的意思,然后输入pid

3d7fa8cbfb9e49199e4739b87de3e0be.png

8e8cdad160a24639bc449b07c165ec7b.png

 接下里让我们输入nice值,我们就调整为-20

f5df746543964949bd26d5abdafac521.png

 我们可以看到确实成功修改了这个时候我们再修改为100

155128a64bc24b899284c2a0d3f9c908.png

c92ae562786a4c9cba1bb673cfc4a9b6.png为什么是不是180呢?因为我们优先级的调整范围是-20到19这个级别,也就是说最小是-20,最大是19。 进程的优先级在我们平时使用中都不会调整,一般都会使用默认的优先级,大家知道该怎么修改就可以了。

二、环境变量

环境变量一般是指操作系统中用来指定操作系统运行环境的一些参数,比如我们在编写c/c++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找,环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。比如我们在linux中写的可执行程序,要想这个程序先运行起来必须在前面就是 .   /   ,  . 是在当前路径,/ 是路径分隔符,而同样为可执行程序的 ls指令等却不需要在前面加上./,难道就因为一个是被纳入系统的程序一个是我们自己写的吗?其实并不是这样,系统的指令之所以不需要在前面加./是因为有环境变量的帮助,这个环境变量会帮我们去搜索系统中的ls命令,而这个环境变量叫PATH,下面我们查看一下这个环境变量:

eef2400857064d3984f83907a6e6973a.png

 echo是打印一个字符串,PATH是环境变量,前面加上$符号就是获取环境变量的内容,这里与指针解引用相似。

和环境变量相关的命令:

1.echo:显示某个环境变量值

2.export:设置一个新的环境变量

3.env:显示所有环境变量

4.unset:清除环境变量

5.set:显示本地定义的shell变量和环境变量

fa6597d954d5457a9cbf4f001828b532.png

 我们之前讲过linux的指令,which可以查看指令的路径,通过查看我们发现ls指令在usr/bin中,而PATH环境变量是根据冒号一个一个路径进行查找,当找到usr/bin这个路径的时候就不需要我们在使用的时候加./了。那么如何将我们写的可执行程序添加到环境变量中呢?看下图:

db99d84aed4d48f88c3223e9c1526bbf.png

 我们写了一个程序用来演示。接下来我们用命令将我们写的这个程序加入到环境变量中

9179e0db8aa74d2196d602781ee889d4.png

 可以看到我们成功添加,然后我们试试可以直接运行吗?

c3f9e490a15a4b9593cea23c8f8b8584.png我们看到是可以运行的并且不用在输入前面的./了

827ead7ebe764ca78c953512cf28bc88.png

 这个时候我们的其他指令不能使用该怎么办呢?这时只需要重新登录xshell即可。

那么我们怎么样才能既使用系统的指令又用自己的呢?

69c775a1039445efa5443b330d36f084.png

 我们将刚刚的命令修改一下就可以既使用系统的指令又用自己的。当然除了这一种方式我们还可以直接将要添加的可执行程序的目录拷贝到PATH中,而这种方式在linux中相当于软件的安装。

下面我们用env指令查看系统中的环境变量:

d4dbfd76edcf4344be89b8c9c2551257.png

 我们可以看到系统中的环境变量很多,当然我们也可以用history指令查看以往我们用过的指令:

10c64bfd8c7d49a7880dd3db7da4d4da.png

 为什么从10开始呢,因为history只会保留最新的1000条指令,一旦超过就会删掉原来旧的指令。

下面我们用C语言来获取系统中的环境变量:

1053a65ec3f145e0ac903e548dfecbb6.png

 我们再写c/c++的时候从来没有写过main函数的参数,而main函数实际上有3个参数,这三个参数不需要我们手动去写编译器会默认给我们传参,envp这个指针数组中每一个指针都指向一个有效的字符串,而最后一个指针必须以NULL结尾。

437592c9d81447f69511ffb70f3eb002.png

 然后我们将代码写完整如下图:

517d42ddc8ed4c258f8552a19429ef7a.png

 为什么for循环中envp没有写判断呢?因为我们刚刚说过,envp这个指针数组中最后一个指针指向的一定是NULL,而NULL在for循环中对应为假,所以不需要写判断语句。

91984d5470944dd896117db9c15d9955.png

 由于使用main函数的三个参数是c99标准下的,所以我们在编译后面加上c99。

0493d41210ec4fc88805e0a966a6dec6.png

 然后我们直接运行程序发现这里的环境变量与我们用env命令显示的一致。

总结:环境变量本质就是内存级的一张表,这张表由用户在登录系统的时候,进行给特定用户形成属于自己的环境变量表。环境变量中的每一个都有自己的用途,有的是进行路径查找的,有的是进行身份认证的,有的是进行动态库查找的,有的是用来进行确认当前路径等等,每一个环境变量都有自己特定的应用场景。那么环境变量对应的数据都是从哪来的呢?是从系统的相关配置文件中读取进来的。下面我们验证一下:

我们先用ctrl + ~进入家目录,然后输入指令ls -al查找文件:

e0a14d674ac247ba9d4c4c1b727fb701.png

 我们可以看到bash的两个shell脚本,然后我们用vim打开这个脚本:

ae34742d84d447578034c3d119f5a4f0.png

 我们用vim进入etc/bashrc,注意在etc目录下bashrc是全局的。

5463d7bebacc4306bfd430955fc36dfb.png

 比如我们命令行上的# 或者 $提示符就是这样编写的。  环境变量是通常具有全局属性的,当我们写了一个环境变量val=100,然后这个环境变量就会shell的表中,当我们给这个进程在开一个子进程的时候,shell中的这张表也会交给子进程,这样子进程中也就有了环境变量val=100,下面我们来证明一下:

946aac7ad9fc4dce8ad5b3cdc71bf470.png

 我们先自己导入一个环境变量,然后输入env命令查看:

97d7963b289b40c0badbd1e5bb70f892.png 我们看到确实将这个环境变量添加到系统中,下面我们修改一下代码进行演示:

17dd266e3a7b436f9bee421adfb364cc.png

getenv()函数是获取一个环境变量并打印 

02d0eb73320c493e99461da6c3318923.png 我们从上图中可以看到成功获取到了我们自己设的环境变量,我们前面讲过当我们运行一个进程的时候这个进程的父进程是bash,刚刚我们的环境变量是保存在系统中也就是说只有bash可以访问,但是现在这个子进程也可以使用就说明了环境变量是全局的,会由父进程传给子进程。

d9aed7534c1c46f2a3093c444c92f572.png

 我们定义了一个变量前面没有加export,然后我们在前面加$符号打印其内容发现也能正常打印

721859772cf748c09ecbc161b9eacd02.png

 下面我们用getenv获取一下这个环境变量:

a815b9ad3e4d4774a0e5ddc509c16cff.png

3886220543ad459d90366fcce00653ea.png 这个时候我们发现用函数获取环境变量获取不到我们刚刚定义的hello1,也就是说不带export定义的环境变量是不可以被getenv()获取到的,那么也就不可以被子进程继承,那么为什么加了export就能被继承呢?因为不加export定义的环境变量是不会被添加到环境变量表中,这样的变量被称为shell的本地变量,这种变量只能在shell内部有效。

b01629f725ca4cdba22c8b2362c99dd0.png

 因为hello1已经在shell本地了,所以前面加export可以直接将hello1添加到环境变量表,也就可以正常被子进程继承了。

接下来我们继续解释刚刚main函数的三个参数中的另外两个。

6919d0e5236a4f24b8c0fd25e610f666.png

 argv的使用与argc都有一个共同点,就是不需要在判断结束条件。

6e4188fc28384bdb91e521796a593cc6.png

如下图所示:

f97e681055f84c0492bef8c8f721e5be.png-a -b其实是参数选项,my2.24是可执行。那么这个操作有什么作用呢?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
//int main()
//{
//  int cnt = 10;
//  while (cnt--)
//  {
//    printf("这里在倒计时:%d\n",cnt);
//  }
//  return 0;
//}
void Usage(const char* name)
{
  printf("\nUsage:%s -[a|b|c]\n",name);
  exit(0);
}
int main(int argc,char* argv[])
{
  if (argc!=2) Usage(argv[0]);
  if (strcmp(argv[1],"-a")==0) printf("打印当前目录下的文件名\n");
  else if(strcmp(argv[1],"-b")==0) printf("打印当前目录下的详细信息\n");
  else if(strcmp(argv[1],"-c")==0) printf("打印当前目录下的文件名(包含隐藏文件)\n");
  else printf("其他功能,待开发\n");
//{
//  for (int i = 0;argv[i];i++)
//  {
//    printf("argv[%d]->%s\n",i,argv[i]);
//  }
  return 0;
}
//int main()
//{
//  printf("myenv:%s\n",getenv("hello1"));
//  return 0;
//}

 我们重新写一段代码,然后我们运行起来。

b4680554930146ad93724193fd4aab62.png

 启动软件后这个软件告诉我们使用方法是./my2.24 + abc任意一个字符

6ad89f683c6240bdbbe80480f81ad57f.png

 这样就能完成一个类似于打印目录的操作,这只是简单的演示实际上可以实现一些有用的东西。

 


总结

本篇文章相较于上一篇进程的概念多了很多需要实践的东西,比如测试进程的优先级,理解孤儿进程,学会理解环境变量并且可以自己添加环境变量,环境变量的获取,环境变量的修改等。下一篇继续更深入的学习linux的进程,即使进程这部分概念多也希望大家可以多多练习才能更深入的理解进程。

猜你喜欢

转载自blog.csdn.net/Sxy_wspsby/article/details/129177383