目录
前言:
进程的状态
直接谈论进程的状态
僵尸进程和孤儿进程
纯理论部分
运行态:
阻塞态:
挂起态:
进程的优先级以及切换问题
切换:
优先级:
前言:
承接上文,进程1到3我们分别介绍了从操作系统层面认识进程,什么是进程,进程的相关属性有哪些,如何创建进程,以及颠覆三观的函数fork,最后介绍了从哪里看进程的部分详细信息,以及深化了一下Linux中一切皆文件的思想->因为进程本质上也是目录!
本文作为进程的收尾工作,要介绍的是进程的状态,什么是僵尸进程,什么是孤儿进程,简单描述进程的调度问题,调度问题会在地址空间详细介绍,以及进程的优先级问题,进程的切换问题等。
更详细的进程介绍会在环境变量以及地址空间介绍完之后,介绍进程控制以及进程替换等,到时候进程才算完结。
好了,废话不多讲,开始今天的第一个话题,进程的状态。
进程的状态
进程的状态分为如下三个部分进行介绍,第一个是直接谈论进程的状态问题,第二个是僵尸进程以及孤儿进程,最后则是进程状态的纯理论,例如挂起态 阻塞态等。
直接谈论进程的状态
我们首先来用ps -xaj引入这个话题:
进程3中创建5个子进程的代码我们作为例子来介绍,我们已经介绍了ppid pid ,状态就是STAT,STAT就是state的简称,state翻译过来就是状态的意思。
/*
* 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 */
};
Linux的内核源代码中对于状态有一个数组来描述,一共有R S D T t X Z ,共7种状态,其中部分状态我们无法通过代码运行来观察,比如X,dead,表示进程死亡,就是被操作系统回收了,这是一瞬间的事情,我们很难观察。
5 int main()
6 {
7 while(1)
8 {
9 printf("Hello world!\n");
10 }
11
12 return 0;
13 }
我们使用这段代码进入主题,这段代码没有啥问题吧?那么当我们运行起来这段代码的时候,按照常识来说状态应该是R,那来看看:
这边已经开始运行了。
可是为什么我们看运行状态是R和S混在一起,大多数时候还是S居多,难道一个进程可以同时拥有多个状态吗?当然不是的,那么为什么该代码的结果是S和R混一起?我们注释掉printf再看看:
可以发现此时的状态又变成了R,难道因为printf?确实是。
你想,cpu用来执行printf,是不是需要很短很短的时间,甚至超出了我们的想象,但是打印出来是在显示器上打印出来的,所以根据冯诺依曼体系,我们知道,cpu不是和外设直接打交道,那么cpu执行了printf之后,会先将打印结果输入到缓冲区,然后缓冲区刷新到显示器上,那么请问,这个死循环的过程,显示器是否时刻准备就绪呢?
当然不是,cpu一瞬间可以给很多很多的printf,显示器打印出来的速度是远远不及cpu运行的速度的,那么把显示器当作是人的话,它指定是会抱怨说:太多了太多了,打印不过来了。
所以,显示器的状态并不是准备就绪的,当我们运气好点,能偶尔看到显示器就绪的状态,此时cpu刚好执行printf,所以状态是R,但是大多数时候显示器可忙不过来。
这就是为什么大多数状态是S的原因,休眠状态,而这种休眠状态还被成为可中断休眠,实际上是一个浅休眠的状态。
这是状态S。
肯定是有细心的同学发现了,这上面的不都是带有+的吗?为什么都说是S啊R啊什么的,是错了吗?当然不是,这是分为进程是前台运行还是后台运行的。
我们可以输入:
./test &
就是代表的指定进程在后台运行,此时,+就不见了,所以,+号有代表的是进程是运行在前台,没有加号代表的是进程运行在后台,此时进程有一个特点就是进程是无法被ctrl + c干掉的,只能通过我们kill指令来杀死:
那么前台和后台的具体参照物是什么这里先不介绍,我们主要还是侧重状态。
那么什么是kil指令呢?kill指令翻译过来也就是杀死的意思,但是并不是完全的杀死,我们可以看看kill指令有哪些子指令:
可以看到不同的数字对应了不同的英文,我们在C语言阶段可以看到的是,一般宏定义就是全英,这里也差不多,不同的数字代表不同的意思而已,我们刚才使用的kill -9,SIGKILL,信号杀死,关于信号后面会另开一篇文章介绍,但是KILL我们是熟悉的,杀死!也就是杀死进程。
从kill我们可以引入第二个点了,T。
T表示的状态是stopped,即暂停,那么kill指令中的-19代表的就是暂停,所以我们看看:
此时就变成了T,至于为什么没有+号了,因为暂停了的进程就变成了后台运行的,这点不用太在意,这就是T,那么暂停了的进程我们想要它跑起来该怎么做呢?
18号指令,SIGCONT,也就是信号继续的意思,CONT就是continue的意思,所以就是信号继续,那么试试:
此时就变化了,所以T的意思就是暂停。
那么什么是t呢?
可以看到源码的意思是trace stop,代表追踪暂停,实际上就是当我们使用断点的时候,代表程序被暂停,所以就是t:
此时通过编译-g,生成二进制的可调试文件,我们开始调试,并打几个断点:
打了断点之后,进程信息里面并没有出现我们想要的东西,因为我们还没有r我们的程序:
r之后进程信息就变成了t,这就是T和t的不同之处,区别不大,总而言之都是暂停的意思。
接着我们介绍D,disk sleep:
这个点我们无法通过演示来观察,所以以文字叙述的方式来了解即可。D状态是Linux中的一个独特的状态,即深度睡眠,比如在内存里面,一个进程要给磁盘写入1GB的数据,那么磁盘写入需要时间吧?写完了还需要给进程说我写好了或者我没有写好,此时进程就等着了,那它也没事干,就睡觉呗,此时操作系统来了,操作系统有特权,即杀死进程或者回收进程的特权,它一来,看到这个进程在睡觉,就气不打一处来,一下子给人回收了,结果磁盘没有写入成功,就导致了1GB的数据丢失,那么老板为了不造成如上的数据丢失的情况,就将进程的状态设置为了D,深度睡眠,深度睡眠的一个特点就是不能被操作,杀死 回收都不可以,必须等磁盘来报告了,状态才改变,这就是D的由来。
好了,状态就介绍完了,接下来是僵尸进程和孤儿进程。
僵尸进程和孤儿进程
僵尸进程和孤儿进程,就可以“顾名思义”了,一个是僵尸,代表杀不死的,那么僵尸怎么形成的呢?即进程运行结束了,但需要维持自己的推出信息,直到父进程来读取,僵尸进程才会被回收,那么我们怎么看僵尸进程呢?
因为僵尸进程是没有父进程回收的子进程,所以我们只需要让父进程一直死循环即可:
7 pid_t id = fork();
8 if(id == 0)
9 {
10 int cnt = 5;
11 while(cnt--)
12 {
13 printf("I am a child process,pid is %d\n",getpid());
14 sleep(1);
15 }
16 }
17 else
18 {
19 while(1)
20 {
21 printf("I am a parent process,pid is %d\n",getppid());
22 sleep(1);
23 }
24 }
当5秒计数过去之后,此时子进程的状态就变成了Z+,也就是僵尸,那么后面跟着的defunct翻译过来就是无效的意思,本质上就是子进程的工作已经完成了,但是要等待父进程完成之后来回收自己,所以它要维护自己的退出信息,退出信息在task_struct里面,那么进程 = task_struct + 自己的代码和数据,变成了僵尸进程之后,代码和数据就用不到了,只需要管结构体就行了,那么如果没有父进程来回来这个僵尸进程,僵尸进程就会一直保持僵尸进程的状态。
那么僵尸进程的危害是什么呢?
内存泄漏!
你要知道,task_struct里面的变量可是很多很多的,根据结构体的内存对齐的规则,自然知道该结构体的大小是很大的,本来操作系统的资源就紧张,万一不小心给父进程杀死了,就没人管它,所以内存就泄露了。
孤儿进程,就是没有父进程的,我们将原来的代码改动一下,使得父进程先退出,子进程一直循环,因为父进程先退出是不用管什么东西的,那么子进程一旦没在父进程之前退出,就会变成了孤儿进程,但是并不是真正意义上的孤儿:
此时,孤儿进程的ppid变成了1,也就是1号进程操作系统来回收,虽说是孤儿进程,但是不能没有人来回收吧?
并且,孤儿进程是不能通过ctrl + c干掉的,只能被kill掉:
纯理论部分
这个章节主要介绍的是运行态,阻塞态,挂起态。
运行态:
对于运行态来说,我们常认为的,进程只要是被cpu执行了,那么就被成为运行态,但是在不同的操作系统的教材上,常常称进程如果在运行队列中也是运行态
head指针后面跟的就是不同的PCB,那么有个问题了,如果一个进程被cpu执行了,是否会等该进程执行完了才会到下一个进程?显然不是,如果是这样,那其他进程也别活了,我就来个死循环,其他进程全部完蛋,所以存在时间片的概念,简单来说就是给定时间,如果该时间内进程没有执行完毕,那这个进程重新到后面排队去,务必要保证其他进程的执行,那么现在埋一个伏笔,进程之间是如何切换的?
这是OS中的基本调度算法,但是Linux中并不是。具体的会在后面介绍。
那么就有了并行和并发的概念,多个进程在cpu这里来回切换以保证多进程的持续推进,叫做并发,一个进程也是可以在多个cpu之间运行的,这种情况叫做并行。
阻塞态:
阻塞?什么是阻塞?等待呗,例如我们调用了scanf函数,但是呢,我们为了作乐,就是不输入,那么操作系统总不能让一个一直等待资源的进程来骚扰自己吧?并且我们知道这种等待资源的进程可以用状态S来表示,即睡眠。
那么OS中是如何操作的呢?
我们首先需要认识一个问题,是只有Cpu才有运行队列吗?或者说只有cpu才有队列吗?
当然不是。
如果一个进程等待资源,比如键盘,那么在硬件中,驱动程序也会存在队列,叫做wait_queue。
注:该介绍都是基于Linux的原理部分,但是不代表是Linux的源代码。
wait_queue指向的是什么呢?同进程的链表一样,指向的是device,也就是硬件设备的PCB,源码中可以用define定义数字,表示该PCB是哪种硬件,也可以使用int stat,表示状态,比如定义是否在等待,数据是否输入完成。
那么这个过程,就是叫做阻塞态,即等待资源,有了数据,操作系统就会将对应数据重新拿过来排队,此时进程就从阻塞态变成运行态。
挂起态:
挂起态也是很有意思的,计算机中在磁盘会有一个分区叫做swap,交换的意思,用处是什么?
都知道操作系统的压力一般是比较大的,如果发现内存中的空间不够了,就会从内存中换出一部分资源到磁盘中的swap中分区中,此时进程就变成了挂起态,当然,不是说什么进程都是可以变成挂起态的,肯定OS层面会有一定的分析。
如果内存空间够了,就会从swap分区中拿过来对应的进程,当然,拿过去的只是进程的代码和数据,进程的PCB是不会拿过去的,因为进程的PCB需要记录信息。
swap的空间大小一般都是内存的1.5倍左右,肯定不会太大的,太大了操作系统一依赖这个分区,效率就会下降,本来换入换出就会导致效率下降了,如果更加频繁的换入换出,效率更低了,同算法不同的是,挂起态这里是效率换空间的做法。
挂起态也可以称为阻塞挂起态,因为进程本质上也是没有被调度的。
进程的优先级以及切换问题
切换:
进程的切换问题,在运行态中,进程的切换是肯定会有的,那么OS如何保证进程的数据不被丢失呢?
进程的PCB里面可以保存各种信息,那么在时间片到了的时候,task_struct里面会记录cpu中相应的寄存器的值,在下次轮到该进程的时候,就能接着进程的上次运行的结果接着来。
这是因为寄存器本身具有数据存储的能力,寄存器不等于寄存器中的内容是我们要知道的,官方一点来说,这是存储上下文数据,cpu寄存器存储的临时数据,就是进程对应的上下文,我们不必担心寄存器不够,寄存器可多着呢。
优先级:
优先级相信不用过多介绍同学们也知道为什么存在优先级,优先级如何判断。
优先级的存在就是为了公平,比如我们去排队,总得有个先后顺序吧,这就是优先级的作用,为了保证公平,那么优先级VS权限呢?
一个是已经拥有访问的权限,不过是时间问题,一个是不知道能不能访问,所以它两之间的比较就不多说了。
在PCB里面,总会有字段是用来描述优先级的,比如int prio,这个数字越小,代表优先级越大,具体参考进程1,代表的是操作系统,这优先级能不大嘛?
查看优先级我们需要top一下,ps -al也可以的,top之后,打开了一个类似于任务管理器的东西,其中PR代表的就是优先级的数字,Ni代表的是nice值,我们可以通过nice值来修改优先级的大小,当我们是普通用户的时候,优先级修改第二次就会报警告,除非我们是root用户才会让我们修改。
所有优先级 = PR + NI,这里的NI是有一定的取值范围的,是[-20,19],如果我们修改NI为100,只会取19,不会超出去。
其中修改nice值也很简单,只需要nice或者是renice指令,因为很简单这里不介绍。
我们一般可以通过top进入到了任务管理器,然后按r,输入对应的pid,回车再输入对应的nice值即可。
这就是优先级我们要注意的内容。
感谢阅读!