【Linux】Linux进程状态和进程优先级

进程是操作系统进行资源分配和调度的基本单位,PCB(进程控制块)用于描述进程状态。Linux中,进程可通过fork创建,状态包括运行、睡眠、死亡等。僵尸进程是已退出但未被父进程回收的状态,可能导致内存浪费。通过系统调用如fork和kill,可以创建和管理进程。
摘要由CSDN通过智能技术生成

目录

1、什么是进程?

1.1、描述进程-PCB

2、Linux进程的一些基本操作

2.1、查看进程

2.2、结束进程

 2.3、常见进程调用

2.4、通过系统调用创建进程-fork初识 

3、进程状态

3.1、普通的操作系统层面

4、Linux操作系统的状态

5、僵尸进程和孤儿进程

5.1、Z(zombie)-僵尸进程

5.2、僵尸进程危害

5.3、孤儿进程

6、进程优先级

6.1、基本概念

6.2、查看系统进程

6.3、PRI and NI

7、进程的其他概念

8、进程切换


1、什么是进程?

进程(Process)是指计算机中已运行的程序,是系统进行资源分配和调度的基本单位,操作系统结构的基础。

我们平时在编写好一个C/C++的可执行程序时,经过编译链接后形成二进制可执行程序,而程序的本质就是文件,放置在磁盘上,根据冯诺依曼体系结构可知,我们要想把这个程序运行起来,就必须要把该程序从磁盘加载到内存中,但这时CPU还不能在内存当中出处理这个程序,因为在内存当中还有无数个程序在计算机里跑,有如此之多加载进来的程序,每一个程序内存分配多少,在哪里分配,你的代码执行到哪里了,有没有被调度过,先执行哪一个,后执行哪一个等问题该怎么解决呢?

这时操作系统就想到了一个管理思路,就是--先描述,在组织。所以为了描述这些进程,计算机里就有了PCB的概念;

1.1、描述进程-PCB

 在Linux中描述进程的结构体叫做task_struct,也叫做进程控制块(PCB)。task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程所有的信息,其内容可以分为以下几类:

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

该结构体抽象出来可以用如下来表示(假设该task_struct使用链表进行组织)

struct tack_struct
{
	//进程所有属性
	// .....
	//进程对应代码和数据地址
	// ......
	//下一个进程的地址
	struct task_struct* next;
};

Linux中task_struct的部分源代码:

所谓的对进程进行管理,就是对进程对应的PCB进行相关的管理,进而变成了对链表的增删查。也就是说,当系统中新增一个进程加载到内存时,就会把该进程的所以信息放到结构体中,然后链入到链表中;而查找进程,就是遍历该链表,找到该进程所在位置,进程对应的代码也就找到了;删除一个进程就是遍历链表找到并判断PCB中进程是死亡的,然后把他的PCB和进程控制块free掉,此时这个进程就被释放掉了

结论:进程 = 内核数据结构(task_struct) + 进程对应的磁盘代码。

补充:

为什么会有PCB(Linux中struct task_struct)这样的结构体呢?

答:因为管理的核心理念、核心思路是对数据做管理,对数据做管理就必须拿到数据,可是拿到的数据没有规矩,杂乱无章,另外数据量可能很大;所以我们要对数据做归类,按照我们面向对象的思路,把被管理对象所需要的属性抽象出来,当我们加载到内存之后,操作系统为其创造PCB的本质就是因为管理的理念是先描述,在组织,操作系统要对进程进行管理,就必须遵守自己设计的理念,因此,必须要有PCB。

2、Linux进程的一些基本操作

2.1、查看进程

我们可以通过以下两者指令来查看进程:

 (1)查看系统所以进程:

 (2)ps axj 指令配合 grep 和管道查看指定进程:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5     while(1)
  6     {
  7         printf("hello world\n");
  8         sleep(1);
  9     }
 10     return 0;                                                                          
 11 }

2.2、结束进程

对应我们编写的普通程序来说,我们可以使用[Ctrl + c]来结束;也可以使用kill命名,指定 -9选项加该进程pid来强制结束进程:

 

 2.3、常见进程调用

 我们可以通过使用操作系统给我们提供的系统调用接口 getpid() 和 getppid() 来获取进程id和父进程id(进程id是一个进程的唯一标识):

  1 #include<stdio.h>
  2 #include<unistd.h>
  3  #include<sys/types.h>
  4 
  5 int main()
  6 {
  7     while(1)
  8     {
  9         printf("我是子进程!,我的pid是:%d,父进程pid是:%d\n",getpid(),getppid());
 10         sleep(1);                                                                                                                                                                     
 11     }
 12     return 0;
 13 }

 可以看到,我们通过 getpid() 和 getppid() 函数得到的值的确是我们进程对应的id;同时,我们发现 myproc 进程的父进程是 bash,即 shell 外壳,shell 为了防止自身崩溃,并不会自己去执行指令,而是会派生子进程去执行。

另外,同一个程序重新被运行时它的进程id可能与之前不一样,因为它的代码和数据需要重新从磁盘中加载;但是它的父进程id一定是一样的,因为它们都是通过 bash 来执行;

2.4、通过系统调用创建进程-fork初识 

我们可以通过系统调用接口 fork 来创建子进程:

 1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 
  5 int main()
  6 {
  7     pid_t id = fork();
  8     if(id == 0)
  9     {
 10         while(1)
 11         {
 12 
 13             printf("子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
 14             sleep(1);
 15         }
 16     }
 17     else if(id >0)
 18     {
 19         while(1)                                                                                                                                                                      
 20         {
 21 
 22             printf("子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
 23             sleep(1);
 24         }
 25     }
 26     else
 27     {
 28         printf("创建子进程失败!");
 29     }
 30     return 0;
 31 }

  • 通过以上我们可以知道 fork是一个函数,在函数执行前只有一个父进程,函数执行后 ——父进程+子进程;
  • 也就是说fork() 之后,其一,会有父进程+子进程两个进程在执行后续代,其二 ,fork后续的代码,被父子进程共享!
  •  通过返回值不同,让父子进程执行后续共享的代码的一部分。

3、进程状态

3.1、普通的操作系统层面

什么是进程状态:我们知道,一个程序被加载到内存变成进程之后,操作系统要对该进程进行管理,即为其创建对应的PCB对象;而进程状态,本质上就是PCB内部的一个整形变量,不同的整形值就对应不同的进程状态。

在普适的操作系统层面,即站在操作系统学科的角度来说,进程状态可能有如下几种:运行、挂起、阻塞、新建、就绪、等待、挂机、死亡;其中最重要也是最难理解的几种状态分别是:运行、阻塞、挂起。

运行状态:

  • 操作系统为了合理分配CPU以及各种硬件资源,也为了更好的调度各个进程,会为CPU创建一个进程队列,为每一个硬件都创建一个等待队列,而此时在运行队列里的进程叫做运行状态。
  • 而让某一个进程处于运行状态本质上就是将该进程对应的PCB放入CPU的运行队列中,然后再将PCB中维护进程状态的变量修改为相应的值,比如0;
  • 因为进程PCB里面有进程的各种属性,以及进程对应的代码和数据的地址,所以CPU从运行队列中取出PCB后,可以根据该PCB来得到进程的各种数据和指令,然后执行相应运算;
  • 所以进程处于运行状态并不一定意味着该进程此刻正在被运行,只要该进程处于CPU的运行队列中即可。(注:CPU是纳秒级的芯片,运算速度非常快,所以只要进程处于CPU的运行队列中,我们就可以认为该进程正在被运行)

阻塞状态:

进程的阻塞是指使一个进程让出处理器,去等待一个事件,如等待资源、等待I/O完成、等待一个事件发等,通常进程自己调用阻塞原语阻塞自己,所以,是进程自主行为,是1个同步事件。

阻塞状态本质上就是将进程的PCB从CPU的运行队列中剥离出来,放入硬件的等待队列中,然后将PCB中维护进程状态的变量修改为相应的值,比如1;待该进程获得对应的对应的硬件资源以后,再将该进程放入CPU的运行队列中。

注:并不是只有等待硬件资源进程才会处于阻塞状态,一个进程等待另一个进程就绪、一个进程等待某种软件资源就绪等都会处于阻塞状态。
 

挂起状态:

当出现了引起挂起的事件时系统或进程利用挂起把指定进程或处于阻塞状态的进程挂起。其执行过程大致如下:检查要被挂起进程的状态,若处于活动就绪态就修改为挂起就绪,若处于阻塞态,则修改为挂起阻塞。被挂起进程PCB的非常驻部分要交换到磁盘对换区。

注:挂起进程并不是释放进程,因为该进程对应的PCB仍然处于某硬件的等待队列中,当该进程获得对应的资源以后,操作系统仍然可以将该进程对应的代码和数据从磁盘加载到内存中来继续运行,其本质是对内存数据的唤入唤出;同时阻塞不一定挂起,挂起也不一定阻塞,也可能是新建挂起、就绪挂起,甚至是运行挂起。

总结:进程状态改变的本质是进程对应的 PCB (task_struct 对象) 处于不同设备的运行队列/等待队列中。
 

4、Linux操作系统的状态

  • 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务)。

下面的状态在kernel源代码里定义:

/*
* 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 */
};
  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列 里。
  • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
  • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。 

运行状态(R):

运行状态即进程在的PCB位于CPU的运行队列中:

 睡眠状态(S):

Linux下的睡眠状态其实就是阻塞状态的一种,进程需要等待资源: 

从上面我们可以看出,明明进程一直在运行,但是进程状态却是休眠状态,这是因为printf本身是要访问显示器,而显示器是外设,相对于CPU的速度是比较慢的,而CPU要等显示器就绪需要花费比较长的时间(相对于CPU)。

我们的代码中可能有99%的时间在等IO就绪,1%的时间在执行打印程序,所以我们在查进程状态的时候,大部分都处于睡眠状态(S)。

磁盘休眠状态(D):

磁盘休眠状态也叫做深度睡眠状态,当内存空间不足时,操作系统会把一部分进程挂起来节约资源,但当内存空间严重不足,可能会使操作系统崩溃时,系统就会主动杀死某些进程。

但是这里有个严重的问题,当磁盘写入数据失败时,磁盘会向进程返回一个结果,但恰好这个进程因为内存不足被系统杀死了,磁盘的反馈无人应答时,磁盘就会把该部分的数据丢弃,然后处理其他进程任务;此时,这部分用户数据就丢失了。

为了应对这种情况,Linux设计出了深度睡眠(D)状态,处于深度睡眠状态的进程既不能被用户杀掉,也不能被操作系统杀掉,只能通过断电,或者等待进程自己醒来。

注:深度睡眠一般只会在高IO的情况发生下,且如果操作系统中存在多个深度睡眠状态的程序,那么说明该操作系统也即将崩溃了。

暂停状态(T):

暂停状态也是阻塞状态的一种,我们可以通过 kill -l 命令查看kill命令,其中指定 - 19 选项来让一个进程从运行状态变为暂停状态:

 

 对应我们来讲,当前进程被暂停了,这个进程可以被挂起,但他也属于阻塞的一种,因为它当前没有代码在运行了。

当我们暂停一个进程后,也可以使用kill -18 指令让一个处于暂停的进程恢复运行:

 

这里如果细心可以发现一个细节,我们在暂停或继续之后,状态后面的“+”消失了;其实,进程状态后面的 + 号代表着一个进程是前台进程,没有 “+”号就代表是后台进程;对于前台进程,我们可以使用 Ctrl + c 将其终止,也可以用 kill 命令杀死它;但是对于后台进程来说,我们只能通过 kill 命令来杀死它。

死亡状态(X):

 死亡状态代表一个进程结束运行,该进程对应的PCB以及代码和数据将全部被系统回收。

僵尸(死)状态(Z):

僵尸状态就是进程在退出时等待父进程或者操作系统来读取退出状态代码,然后释放PCB的一种状态。

总结:可以看到,具体的Linux操作系统下的进程状态和普适的操作系统学科上进程的状态是不同的,比如Linux操作系统没有阻塞和挂起状态,阻塞状态通过睡眠、深度睡眠、暂停、追踪暂停等状态表现出来,而进程处于这些状态时是否会被调整为挂起状态,用户是不可得知的,因为操作系统没必要将挂起状态暴露给用户,用户也不关心一个进程是否会处于挂起状态。

5、僵尸进程和孤儿进程

5.1、Z(zombie)-僵尸进程

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

 我们可以看到,当通过kill指令kill掉子进程15591后,由于父进程中没有子进程的退出状态进行读取,所以子进程变成了Z状态,且子进程后面还提示了<defunct>(失效,不在使用),此时父进程如果不对子进程进行回收,子进程会一只进入僵尸状态。

5.2、僵尸进程危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
  • 当一个父进程创建了很多子进程,但不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,比如C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 僵尸进程还会造成内存泄漏 。

5.3、孤儿进程

父进程先于子进程退出,子进程就变成为“孤儿进程” 。子进程会被操作系统1号init进程领养,如果不领养,那么子进程退出的时候,对应的僵尸,就没人能回收了。

补充:

  1. 父进程退出后并没有变成Z状态,因为父进程的父进程是bash,bash会读取父进程的退出状态;
  2. 子进程被领养后变成了后台进程。

6、进程优先级

6.1、基本概念

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

6.2、查看系统进程

在Linux系统中,用ps –l命令则会类似输出以下几个内容:

 我们很容易注意到其中的几个重要信息,有下:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice值

6.3、PRI and NI

  • PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小 进程的优先级别越高
  • NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  • 每一个进程的PRI默认都是80,PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
  • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 所以,调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是-20至19,一共40个级别

7、进程的其他概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

8、进程切换

进程切换:CPU同一时刻只能运行一个进程,但是CPU的运算速度非常快,所以位于CPU运行队列中的每一个进程都只运行一个时间片,每个进程运行完一个时间片后被都被放到运行队列尾部,等待下次运行;这样使得在一个时间段中多个进程都能被运行。

上下文保护与上下文恢复

CPU在进行进程切换时要进行上下文保护与上下文恢复:


我们的进程在运行时会产生非常多的临时数据,同时CPU中存在一套寄存器硬件,当进程运行时,进程的PCB会被放入CPU内的寄存器中,此时CPU就可以通过进程PCB得到进程代码数据的地址;CPU在运行进程时所产生的大量的临时数据也都会被保存在寄存器中;

那么,我们在进行进程切换时需要进行进程的上下文保护与上下文恢复,即进程停止运行时将寄存器里面的数据保存起来,进程重新运行时将保存的数据再放入到寄存器中;以便我们能够接着上次运行的地方接着运行。

注:CPU寄存器硬件被所有进程共享,但是当CPU在具体运行某一进程时,CPU寄存器中的数据只属于该进程;同时,我们进行上下文保护时保存的是寄存器中的数据,而不是寄存器硬件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只睡不醒的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值