进程状态与孤儿进程

🙊 进程的理解 🙊

进程 = 内核数据结构 + 自己的代码和数据

操作系统会新增或者退出进程,在操作系统的一个时间段内存在很多进程,这些进程都由操作系统进行管理,管理的方式就是先描述再组织,操作系统为了管理进程会创建内核数据结构,然后将内核数据结构定义出来的进程对象用特定的方式进行管理。

一般在 windows 中双击程序、在 linux./ 运行程序都算是运行了一个进程。双击或者 ./ 就会将可执行的二进制文件加载到内存,冯诺依曼体系规定,把软件或自己编写的代码和数据加载到内存后才可以被操作系统调度运行,操作系统会管理这些进程。

进程就是内核数据结构 + 该进程对应的代码和数据。操作系统为了管理进程,就需要从进程中提取进程相关的属性集合,构建出对应的数据结构对象加上从外设中加载进来的该进程匹配的代码和数据。

操作系统对进程的管理并不是对加载到内存中的二进制文件进行管理,而是抽象出进程的数据结构,这个数据结构是一个结构体,包含了进程的内部属性,结构体也可以指向进程加载到内存中所对应的代码和数据。

以后提到进程只关心一个进程所创建的内核数据结构,而它的代码和数据在哪里并不重要。

将所有加载到内存的进程创建出来特定的描述进程的数据结构结构体对象,叫做该进程的进程控制块,简称 PCBlinux 中叫做 task_struct,在内核中 task_struct 就是一个结构体,当加载一个进程的时候,操作系统就会用 task_struct 结构体定义出来一个对象,这个对象含有进程的属性,和加载到内存中的代码和数据的位置。

如果进程太多,操作系统会以链表等数据结构对这些进程进行管理。对进程做管理就变成了对链表的增删查改,这就是对进程的理解。

🙊 进程状态 🙊

💖 进程状态介绍

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 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 */
};

一般进程在运行的时候,是否在一直运行呢?

答: 程序不是在 cpu 中一直运行的,是一个进程都在 cpu 中运行一段时间,然后操作系统再将这个进程从 cpu 卸掉,再将另一个进程放入 cpu,周而复始重复这个工作,直到所有进程都运行完。

这种方式叫基于进程切换的分时操作系统。说直接点就是让每个进程都运行一点,通过切换的方式在同一时间段内让所有进程都得以推进。

因为 cpu 运行速度极快,人们的感官完全感受不到这种快速切换的时间差,所以在我们看来特定的时间段内好像多个进程同时被运行。

那么操作系统通过何种方式进行这样的切换呢?

这就需要进程状态来控制,每个进程都都会有不同的状态,操作系统通过进程的各种状态来决定进程调度与否。

💖 阻塞和挂起的概念

阻塞: 进程因为等待某种条件就绪,而导致不推进的一种状态。

当我们自己写一个程序并编译完成运行,程序被加载到内存变成进程,但是这个进程始终没有被 cpu 执行。windows 下启动很多软件后发现电脑非常卡顿,因为操作系统在用 cpu 调度的时候,正在调度的进程在运行,没有被调度的进程就卡住。

所谓的阻塞就是进程卡住了,这种阻塞或进程卡顿一定是在等待某种资源。
比如下载一个软件的时候突然断网或者下载速度为 0 的时候,进程就卡住了,此时这个进程在等待网络资源。

总结:

1、进程要通过等待的方式,等具体的资源被其他进程使用完成后再被自己使用。

2、阻塞是进程等待某种资源就绪的过程。

💖 如何理解等待的过程

在操作系统层面如何理解进程等待某种资源的就绪?

操作系统是一款管理软硬件资源的软件,管理的本质就是先描述再组织,操作系统要管理硬件资源如磁盘、显卡、网卡等设备,就要先描述、再组织,先将这些硬件设备描述起来,再将这些设备用链表的形式组织起来。这样操作系统对设备的管理就变成了对链表的增删查改。
操作系统内可以存在大量的进程,而且通过先描述、再组织的方式对这些进程进行管理。如一个进程被 cpu 运行时需要等待网卡支持。

在这里插入图片描述
cpu 不再调度此进程,进程阻塞等待网络资源。

在这里插入图片描述
此时进程的 task_struct 链接到描述网卡结构体指针后,等待网卡响应。

在这里插入图片描述
因为对应的资源也需要被管理,所有的外设都被操作系统通过先描述再组织抽象成了特定的数据结构,比如进程的起始地址、维护一个队列等。当进程被调度的时候,就是通过描述进程的结构体对象 task_struct 找到进程的代码和数据去运行

如果发现代码中有些资源没有就绪,只需要将进程控制块从 cpu 某些特定的队列中拿出来放到等待资源处进行排队,就叫该进程在等待某种资源。

如果该进程被连接到外设或某种资源的 task_struct ,不会被 cpu 调度,此时就是进程阻塞。

注: 队列里面放的不是进程的代码和数据,而是描述进程的 task_struct 结构体对象放在队列里。

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

💖 挂起

进程被调度的时候,需要等待某种资源的响应,等待过程中进程不再被 cpu 调度,需要链接到设备的 task_struct 队列中等待响应,这个过程叫做进程的阻塞。

在这里插入图片描述
而当某一时刻内存资源紧张,操作系统将不被调度,进程的代码和数据放入磁盘中。

在这里插入图片描述
当进程等待的资源就绪时,cpu 再次调度进程前将代码和数据放入内存,再将进程放入 cpu 中运行。

在这里插入图片描述
将进程的代码和数据由操作系统放入到磁盘,就叫做进程的挂起。

🙊 Linux 进程状态 🙊

💖 进程状态介绍

task_struct 是一个结构体,其内部会包含各种属性,其中就包括进程状态。
所谓的状态在操作系统的 task_struct 中的伪代码如下:

struct task_struct
{
	//一个进程
	int status;
	//进程的其他属性
	...
}

所谓的进程状态就是用一个数组表示,如 0 表示 running1 表示 sleeping。各字母代表的进程状态如下:

/*
* 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 状态

当一个进程被加载运行时的状态就是 R 状态。那么进程只要是 R 状态,就一定在 cpu 运行吗?

一个进程在 R 状态,并不代表一定在 cpu 运行,系统里可能存在很多进程都在 R 状态,但是可能只有一部分正在运行,所以进程一定在调度运行时维护一个运行队列。不同的进程对应自己的 task_struct,这些运行的进程都会在运行队列中,cpu 在调度进程的时候,只需在运行队列中挑选指定的进程运行就可以了。

在这里插入图片描述
进程是什么状态也看进程在哪里排队。一个进程在 cpu 的运行队列排队,就是 R 状态,在其他设备排队,就是阻塞状态。

💖 进程运行状态验证

接下来我们写一段代码:

#include<stdio.h>
int main()
{
	whhile(1)
	{
		printf("我是否在运行呢?"\n);
	}
	return 0;
}

将程序运行并执行查看指令 ps axj | head -n1 && ps ajx | grep mytest | grep -v grep

在这里插入图片描述
可以看到当前 mytestSTAT 状态为 S+,并不是 R 状态。
将代码做出以下修改:

#include<stdio.h>
int main()
{
	whhile(1)
	{
	}
	return 0;
}

再次将程序运行并查看。

在这里插入图片描述
发现此时进程的状态为 R 状态。因为 printf 为打印函数,需要频繁访问显示器设备打印消息。当进行频繁打印的时候,外设可能没有就绪,此时当前进程并不在 cpu 中排队而是在外设中排队等待外设就绪。当外设就绪才将信息打印到外设。

刚刚的 S 休眠状态是阻塞状态中的一种,当前进程并没有真的一直在 cpu 的运行队列中等,而是在等待显示器资源。

cpu 比外设的速度快,不允许进程在 cpu 中等待外设,因为在等待外设的时间内,cpu 可以执行成千上万行代码,所以需要将其放入到外设的队列中等待。
while 循环是在不断地执行循环体里的内容,执行的时候由于代码只有 printf 一行,一瞬间就执行完毕,而 printf 需要等待,即将进程从 cpu 运行队列放入外设的队列进行等待,相对执行较慢,在用 ps 命令查看的时候,可能很多次才能出现一个 R 状态,大多数只能查看到 S 状态。

为什么注释掉就变成了 R

因为注释掉以后,就没有任何的访问资源的代码,只有 while 循环判断,而 while 只是单纯的计算代码,在整个进程调度的生命周期中,只会用 cpu 资源。

进程在 R 状态并不直接代表进程正在运行,而是该进程在运行队列中排队,这个队列由操作系统维护。由于操作系统在内存中,所以此运行队列也在内存中。

💖 进程的 S(休眠) 状态

S 状态是可休眠状态,本质就是一种阻塞状态,看如下代码:

#include<stdio.h>
int main()
{
	while(1)
	{
		int a = 0;
		scanf(%d,&a);
		printf("%d\n"a);
	}
	return 0;
}

将程序运行并查看进程状态如下:

在这里插入图片描述
可以看到此时进程的状态是 S 状态,当前进程就没有被 cpu 调度,因为此时进程被 scanf 阻塞等待输入,进程等待的资源并没有就绪。所以此时进程没有在运行队列中等待,而是等待键盘有数据后将当前进程 pcb 放入到运行队列中被调度。

所以此种情况 S 状态在等待键盘资源。比如输入 10,按回车键时键盘设备就绪,就绪后进程就会放入到运行队列中,cpu 调度进程执行 scanf 的代码从键盘中读取数据。

💖 进程的 D(不可中断休眠) 状态

D 状态也是一种阻塞休眠状态,在这个状态的进程通常会等待 IO 的结束。Linux 中如果一个进程处在 D 状态,该进程无法被杀死,即使是操作系统也不行。

💖 进程的 T(暂停) 状态

T 停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。如写一段代码:

#include <stdio.h>    
#include <unistd.h>    
                                                                                           
int main()    
{    
  int count = 0;    
  while(1)    
  {    
   printf("我在运行吗???%d\n",count++);    
   sleep(1);      	
  }                                                                                      
  return 0;                                                 
}         

如果想让进程暂停,可以使用 kill 命令向指定的进程发送暂停信号。将代码运行并查看进程状态,使用 kill -19 30603 暂停进程。

在这里插入图片描述
此时查看进程状态,发现此进程由 S+ 状态变成了 T 状态,表示当前进程处于暂停状态

在这里插入图片描述
此时再输入 kill + 18 + PID,就可以继续运行。

在这里插入图片描述
此时会发现 ctrl + c 无法退出进程。

在这里插入图片描述
因为进程状态带 + 是代表进程在前台运行,如果没有 + ,代表程序在后台运行,此时可以使用 kill -9 PID 指令结束进程。

在这里插入图片描述进程如果在前台运行,就可以使用 CTRL c 来结束,使用 kill 命令可以结束任何进程。查状态的时候带 + 的代表在前台运行,不带 + 的就代表在后台运行。

💖 进程的 X(死亡) 状态 和 Z(僵尸)状态

X 状态只是一个返回状态,不会在任务列表里看到这个状态。

为什么创建进程?

因为要让进程帮我们完成某些任务,我们有时会关心任务是否完成的结果。这个结果通过退出码拿到。下面介绍一下什么是退出码

当写一个 c 代码的时候,main 函数的返回值为 int,这个返回值叫做进程退出码,可以根据退出码判断程序执行结果是否正确。

如果一个进程退出了,立刻就 X 状态,父进程有没有机会拿到退出结果?

Linux 当进程退出的时候,一般程序不会立即彻底退出,而是维持一个 Z(僵尸) 状态,方便操作系统后续读取该子进程退出的结果。

在这里插入图片描述
使用命令查看进程,可以看到两个进程是父子关系,父进程的父进程是 bash。如果此时子进程先退出,就会进入到 Z 状态。

在这里插入图片描述
僵尸进程并不会立即释放。维护僵尸进程的意义就是父进程会读取到子进程退出的信息。

危害:

进程的退出状态必须被维持下去,因为他要告诉父进程,你交给我的任务,我办的怎么样了。父进程如果一直不读取,子进程就一直处于 Z 状态。

维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct ( PCB ) 中,换句话说,Z 状态一直不退出,PCB 一直都要维护。

一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想 C 中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

🙊 孤儿进程 🙊

如果父子两个进程中的父进程先退出,会出现什么现象呢?看以下代码:

#include <stdio.h>                                                     
#include <unistd.h>                                                    
                                                                       
int main()                                                             
{                                                                      
  pid_t id = fork();                                                   
  if(id == 0)                                                          
  {                                                                    
    //子进程                                                           
    while(1)                                                           
    {                                                                  
      printf("我是子进程,我在运行,pid:%d,ppid:%d\n",getpid(),getppid());    
      sleep(1);                                                        
    }                                                                  
  }                                                                    
  if(id > 0)                                                           
  {                                                                    
    //父进程                                                           
    int cnt = 10;                                                      
    while(1)                                                           
    {                                                                  
    printf("我是父进程,我在运行,pid:%d,ppid:%d\n",getpid(),getppid());    
    sleep(1);                                                          
    if(cnt-- <= 0)                                                     
      break;                                                                               
    }                                                                
  }                                                                  
  return 0;                                                     
}             

使用 ps ajx | grep mytest 过滤当前进程

ps ajx | grep mytest | grep -v grep 不显示 grep 本身

使用 while :; do ps ajx | grep mytest | grep -v grep; sleep 1;echo “-----------”; done 进行周而复始运行。

在这里插入图片描述
使用 while :; do ps ajx | head -1 && ps axj | grep mytest | grep -v grep; sleep 1;echo “-----------”; done 显示进程属性链。

在这里插入图片描述
此时运行进程

在这里插入图片描述

10s 后父进程退出

在这里插入图片描述
但是根据前面讲到的,父进程退出后应该处于僵尸状态,但是为什么这里没有看到父进程的僵尸状态?

父进程也有自己的父进程 bash,父进程退出的时候,bash 会自动回收退出父进程的僵尸状态。而父进程退出后,子进程的 PPID 变成了 1

父进程退出,子进程会被操作系统领养 (通过让 1 号进程成为新的父进程),被领养的进程叫做孤儿进程

为什么操作系统要领养子进程呢?
如果操作系统不领养孤儿进程,将来孤儿进程退出后,就无法对其回收。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值