文章目录
前言
之前对进程进行了初步的介绍,谈及了进程的创建。本文将围绕进程状态和环境变量进行讲解,更深入一点了解进程。
1.进程状态
1.阻塞与挂起
阻塞和挂起是操作系统中一组常见的概念。
先说阻塞,进程因为等待某种就绪条件而不推进的姿状态称为阻塞。
这样说概念可能显得干巴巴的,我们平时有没有遇到过这样这一种情况:当我们用电脑打开很软件是不是有事时候会卡顿呢?这种卡顿就是进程被阻塞了不能正常工作。为什么会阻塞呢?我们知道当打开很多软件的时候,就相当于在内存中有很多进程,进程需要被cpu调度处理进程才能真正运转起来。这个时候cpu就可能忙不过来了,没有被cpu处理的进程就需要等待cpu忙完有空闲的时候来处理它。也就是说进程通过等待的方式,等需要的资源被被其他进程用完后,在被自己使用。
从而推出:进程等待某种资源就绪的过程就被称为阻塞。
操作系统管理着软硬件资源,进程也是被操作系统管理着的。那么操作系统怎么知道每个进程需不需被阻塞等待资源呢?还是之前谈到的
管理:先描述,再组织。
各种外设也是被操作系统管理的,为了便于管理可将这些设备抽象成一个一个的Dev结构体。
当某个进程需要某种外设资源的时候就把该进程pcb与所需的对应资源的结构体连接起来。
我们知道内存中可能很多进程,很多进程看需要着相同的资源。比如100个进程需要网络资源,我们可将网卡设备的结构体中加入一个 struct taks * Oueue 指针成员变量,将这些需要网络资源的进程pcb依次连接起来形成一个等待队列,便于操作系统进行管理。
这些被阻塞的进程相当于在等待队列进行排队,这就是进程被阻塞后所干的事情。
什么是挂起呢?举个例子:内存中有个进程是进行下载任务的,这个进程被cpu调度处理了,但是下载到一半突然没网了,这个时候该进程只能去网卡的等待队列中进行排队等待网络资源。
我们知道进程是由两部分组成:pcb+代码和数据 ,当该进程在等待的时候,这个代码和数据其实没啥用,一直在内存中还占据空间,如果这个时候内存资源不足,操作系统就会将该进程的数据和代码和磁盘进行交换释放,数据和代码移动到外设上。
这种进程状态就是挂起.也就是说进程pcb还处在等待队列中,但是进程对应的代码和数据在内存外就是挂起。
挂起是一种特殊的阻塞。
2.Linux下的进程状态
1.概念知识
一般在学校上操作系统这门课的时候都看见过这张图或者和这张图类似的图。课本上的操作系统理论放在任何一个具体的操作系统中都是对的,包括上面的图,但是这是抽象的概念。好像什么都说了,又好像什么都没说。我们要学习一款具体的操作系统来认识操作系统。我将Linux为准介绍进程状态。
我们知道进程pcb里面有进程所有属性,那么进程状态也肯定里面。pcb是一个struct_task结构体,这个结构体中有一个status字段表示进程状态,进程所有状态表示就在上图中的字符数组中。下面我就来学习几种比较重要的进程状态。
2.R状态
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里.
进程状态,一般也看进程在哪里排队等待资源。
我写下了如图所示的代码编译运行起来了,通过查询我们看到该进程的状态是S+并不是R状态,S是休眠状态,关于这个+和S状态等下会介绍。我们现在可以判断出该进程一定不是R状态,为啥会产生这样的现象呢?
我们知道上面的代码是向显示屏打印数据的,也就是说该进程需要像显示屏这样的外设资源的。cpu的执行速度是远远大于外设的,假如该进程被cpu调度,这个时候外设有一定的响应时间,操作系统不允许该进程在持有cpu的时候,而等待外设。这个时候就会将该进程添加到等待队列中。
那我们可能会有个疑问,我们确确实实看到运行结果了啊,为啥还是s+呢?我们看到这段代码很短基本上很快就执行了。可能1秒的时间内,有 99.9%在进行等待,剩下的0.01%的时间被执行了。
这种微小的时间的变化人是察觉不到的,同时你可能输上上万次指令才可能刚好捕捉到这个运行态。
我们在看这样的代码
查看进程状态
这次我们发现该进程是R状态了 ,因为代码执行过程不需要其他外设资源,只要该进程被cpu调度就能运行。
2.休眠状态(S/D)
S是S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
休眠状态本质是一种阻塞状态,进行因为等待某种资源而进行休眠等待
。
关于这个D状态我来画图讲故事来说明一下
但是一般来说只要出现一个D状态的进程就说明机器快宕机了。因为出现D状态主要是因为像磁盘这样的外设来不及进行数据的写入,IO请求过多,磁盘压力过大。这个时候内存也好不到哪去。
3.T状态
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行.
我们来执行一下面的代码
补充知识
kill指令先前的博客用过一次,这个kill指令可以给进程发送不同的信号达到不一样的效果。
kill -l
可以查看kill指令相关选项。
kill - 19 进程pid
#发送停止信号
通过指令我们发现这个进程经过kill -19后状态就是T了。右边也可以看到程序确实是停止了。
我们通过18号信号指令来让程序继续运行。
确实是继续运行了,但是我们发现S+的+号消失了
,我们试着来终止一下这个程序。
我们发现按下ctrl +c终止不了,
为啥呢?当没有+号表示在后台运行
,这个时候想要终止程序可以使用kill -9 指令来杀死进程。
关于这个进程在后台运行就像:我们手机上一次打开很多app 哪怕我从某个app界面退出了 其实它还在后台挂着的。除非我手动的将它的后台给关掉,它才算真正意义上的退出。
T状态一般是由用户自己对进程设定的,当然某些进程如果进行非法权限访问,操作系统不想或者不能杀死该进程就会让该进程进入T状态。
T状态和S状态是有区别的,S本质就是一种阻塞状态。我们在调试代码的时候让程序在断点处停下来本质就是让进程暂停,也就是让进程进入T状态。
补充一点
当我用gdb进行调试的时候在第4行处打了一个断点,按下r运行至断点处的时候,查询到该进程的状态由S变为了t,这个t是追踪式断点,也是暂停状态的一种,当我们打断点进行调试的时候gdb就会像该进程发送暂停信号。
4.Z状态(僵尸进程)和X状态
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
它一个瞬时状态,很难捕捉到它。
Z:僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后续会说明)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就会进入Z状态。
对僵尸状态的理解
谈及僵尸状态之前,我们先思考一个问题,为啥要创建进程呢?
我们创建进程就是让进程帮我们来办事的,既然进程是办事的,那我们肯定是关心事情处理的结果的,这件事办没办成?于是就有了进程退出码的概念,进程退出码就是进程在退出消失之前告诉我们事情办没办好,这样的好处是,无须看到程序结果,程序自己就可以判断最后的结果正确与否。
代码示例
通过echo $?可以查询到进程退出码。
有了进程退出码的概念后,我们再来谈谈僵尸进程。
当某个进程被父进程创建后,在进程退出消失的之前需要告诉父进程事情办得怎么样了。这个时候该进程没有立即死亡,而是维持着一种特殊的状态等待父进程(或者OS)读取最后执行的结果正确与否。
这种状态就是僵尸进程。
我们来看个代码示例
从图中我们可以看出当子进程被杀死后,不是X状态,而是僵尸状态。如果父进程一直不读取状态码结果,子进程就会一直维持着僵尸状态。
僵尸状态的危害
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,
换句话说,Z状态一直不退出,PCB一直都要维护!那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费.因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象)是要在内存的某个位置进行开辟空间的!这样就会出现内存泄漏。
5.孤儿进程
Liun下进程一般是以父子的形式出现的,我们知道子进程退出前会返回给父进程执行结果,父进程接收后子进程才会被回收。那如果父进程先退出会发生什么呢?我们看看如下代码。
从上图中我们可以看到父进程退出后,子进程的ppid发发生了改变。也就是说子进程找了新爸爸,被1号进程给领养了。
在被领养前,子进程就没有了爸爸,这样的进程就被称为孤儿进程。
当进程退出后一般由该进程的父进程读取退出码并且回收该进程。一旦子进程没有爸爸就必须找个新爸爸,这样才能有人给子进程收尸。这个1号进程叫nit进程。我们不禁有个疑问:为啥没有看见父进程的Z状态呢?
根据前面的知识,既然没有看到父进程的Z状态,那么肯定父进程被它的父进程读取退出码后回收了。
补充小知识
除了kill -9 进程pid能杀掉进程外,killall 进程对应的程序文件名 也可以杀掉进程
这个Makefile文件中的$@ 是指目标文件 $^是文件依赖列表,不管:后面的文件有多个文件依赖,$^表示所有的问文件依赖
3.环境变量
1.概念
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
常见的几种环境变量:PATH : 指定命令的搜索路径;HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录);SHELL : 当前Shell,它的值通常是/bin/bash
我们知道在linux下运行自己编译好的可执行程序需要加./也就是在当前路径找到这个程序。但是为啥我们使用Liunx中的系统指令的时候就不用呢,比如ls.它们本质也是可执行程序啊和我们自己写的程序差不多,无非就是前者被加入到了系统库中。
其实这因为环境变量提前将这些指令的路径保存好了,在使用的时候,系统通过环境变量来匹配程序路径。
环境变量既然是个变量里面肯定是被写入东西了的,我们可以通过
echo $环境变量名
来查看该环境变量里的内容
USER环境变量保存着当前用户信息
不同的环境变量有不同的功能,输入env可以查看当前系统下的所有环境变量。
我们知道通过history指令可以查看历史指令,这个HISTSIZE环境变量表示最多可以记录的历史命令条数,这里最大记录容量是3000条指令。这个PWD环境变量记录我们当前所处的路径,我们的pwd指令就是根据这个实现的。
我们可以将自己写的程序路径添加到环境变量中像系统指令一样操作
export PATH=$PATH:路径
在PATH环境变量中添加路径。
当我们没有向PATH添加路径,也可以将程序文件添加到PATH的路径中也是一样的效果,这个就相当于在Liunx中安装软件,将指定程序和相关配置文件移动到特定的路径中,卸载程序不就是在将程序相关的文件从特定路径下移除。
2.获取环境变量
1.环境变量表
在谈及获取环境变之前,我们思考一个问题C语言中的main函数最多有几个参数呢?答案是3个,这些参数都和环境变量有关。
main(int argc,char *argv[],char* envp[])
这里面的第3个参数表示的是环境变量表。我们学过C语言的都知道envp是指针数组每个元素都是一个指针(地址)。环境变量都是都是由字符构成的字符串,将这些环境变量以表结构组织起来就形成了环境变量表。envp接收的就是这个表的地址。
代码示例
确实将所有的环境变量打印出来了,但是我们平时写main函数的时候没有写参数啊,其实这些都是编译器在背后偷偷做的。
当我们不使用这个参数的时候也能直接访问这个环境变量表,系统提供了一个全局变量environ指向的就是环境变量表的地址。我们可以通过 man environ查询这个变量
从图中可以看出,两种方式效果是一样的。从中我们也可以知道进程内部本身具体环境变量表,这个表是别人传过来的,虽然暂时不知道这个表是由谁传过来的,但是我们可以通过遍历这张表来访问环境变量,但是这样的访问方式不太好,主流的方式是通过函数来访问。
2.函数获取环境变量
我们可以像获取进程pid一样通过系统提供的函数来获取指定的环境变量。这个函数就是getenv,我们可以通过man函数查询这个函数。
代码示例
通过先前的铺垫我们已经知道这个环境变量本质是字符串,虽然我们不知道这个getevn函数是如何实现的,但是可以大致猜到getenv应该就是通过传入的参数去环境变量表中查找匹配的字符串,如果没找到就返回null。
那么获取环境变量有什么用呢?我们可以简单写一个自己的pwd。
这里确实实现了和pwd一样的功能,我们很多指令都是直接或者间接通过环境变量来实现的。再来看一份代码。
从图中可以看出哪怕是root也不能访问。我们可以根据环境变量写出于系统强相关的代码。
3.关于环境变量的理解和main函数中的两个参数
1.环境变量的理解
环境变量本质是一张内存级别的表,在用户登录系统的时候会给特定的用户形成属于自己的环境变量表。
环境变量变量中的每个变量都有特定的作用,比如:有的是查找路径的,有的是身份认证等等。那么环境变量是从哪里来的呢?其实,是从系统相关的配置文件中读取的。
我们来一一验证图中的说法
从图中可以看出,确实是获取到myval,打印出了100.
myval=100这个整体作为字符串被添加到环境变量表中,在获取的时候之会读到myval=后面的100.
这里就看到hell没有被getenv获取,这个hell就是个shell本地变量。
可能有人会有疑问,echo也是在命令行上输入的指令应该是shell创建的子进程才对,作为子进程怎么可能会读到hell呢?
其实这种指令叫内建指令,这里先不做过多赘述。
2.关于main函数两个参数
之前提到过main函数有3个参数,最后一个参数是接收环境变量表。那么前2个参数是干啥的呢?其实这两个参数是传递命令行参数的。我们来看这样的一段代码。
从上面的图我们可以看出命令行输入的可执行程序和命令行参数选项都会被转化成一个表,由shell将这个表传递给子进程,实现了同一个程序可以有不同的执行效果。基于此我们自己可以写个小代码。
我们之前学习C语言的时候main函数没有带上参数主要是因为没有使用场景,在Liunx下主要使用的命令行,通过选项参数让同一个程序实现不同的效果。
4.进程优先级
1.概念
优先级与权限的区别,前者是已经被允许,但是谁先谁后;后者是是否被允许,能不能的问题。为啥会出现优先级呢?是资源太少了,不够完全平分。进程之所以会有优先级主要是因为cpu资源有限,就会出现多进程竞争cpu资源,就会产生优先级的问题。
2.指令查看进程优先级以及更改进程优先级
输入指令ps -al 查看进程优先级
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小 进程的优先级别越高.
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值 PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice.
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值nice其取值范围是-20至19,一共40个级别.但是并不是说可以随意设置优先级,调度器是会加以限制的。
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据。同时这个PR(old)是80,并不是那之前的PRI.
修改进程优先级进入top后按“r”–>输入进程PID–>输入nice值
这个改变进程优先级一般用不到,基本上都是使用的默认进程优先级。
5.补充知识
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。简单来说并发就是你和同学去网吧打游戏,但是只有一台机器,于是你俩隔1个小时,换着来玩。在两个小时这个时间段你俩看起来好像同时拥有电脑的,cpu并发处理进程也是这样的,只不过这个时间间隔特别小,用户察觉不到。并行就是网吧里有两台电脑,一人可以拥有一台电脑打游戏。
以上内容如有问题,欢迎指正!谢谢!