详解Linux进程状态(进程排队,运行状态,睡眠状态,停止状态,僵尸状态,孤儿进程)

您所在的位置:网站首页 在线编辑如何停止工作状态 详解Linux进程状态(进程排队,运行状态,睡眠状态,停止状态,僵尸状态,孤儿进程)

详解Linux进程状态(进程排队,运行状态,睡眠状态,停止状态,僵尸状态,孤儿进程)

2024-07-11 20:34| 来源: 网络整理| 查看: 265

进程排队 概念

进程排队是指在计算机系统中,当进程等待某种软硬件资源(如CPU、键盘、磁盘、网卡等)时,其进程控制块(PCB)会在特定的队列中等待。进程控制块(PCB)是描述进程状态的重要数据结构,包含了进程的各种信息,如进程标识符、进程状态、程序计数器、内存指针等。

在进程排队时,PCB内部的各个结构体通过指针连接起各个进程,形成队列。这样,操作系统就可以根据队列中PCB的状态,对进程进行管理和调度。当所等待的资源可用时,进程就会从队列中被取出,并继续执行。

进程不是一直在运行的

下面是一段C语言的代码,在这个代码程序需要等待用户输入内容,当用户没有输入时,进程会一直等待并不会运行。

所以进程可能在等待某种软硬件资源。

int main() { int a; scanf("%d",&a); printf("%d\n",a); return 0; }

进程放在CPU上,也不是一直会运行的

首先理解上面这句话我们要先理解时间片这个概念

时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。

宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

当我们写了一个死循环时,当程序放在CPU上运行起来,因为这个程序存在死循环会一直占用CPU资源。但是实际情况是CPU不会让这个程序一直占用,而是给对应的程序分配相应的时间片。

理解排队

首先进程在排队那么一定是在等待某种资源。我们都知道进程 = task_struct + 可执行程序,进程排队并不是可执行程序在排队,而是task_struct 在进行排队!!!

一个task_struct可以被链入多种数据结构中

task_struct在linux中是被链入到双链表当中,但是linux内核中的双链表和普通的双链表存在差异。linux内核中的双链表结构是下图这种状况连接的。

在这里插入图片描述

但是我们还存在疑问,我们只能同过链式结构找到listnode对象,但是我们怎么找到task_struct对象开头的地址。

我们都知道结构体中的 成员变量的地址 = 结构体的首元素地址 + 偏移量,现在我们知道结构体的成员变量的开始的地址,那么 结构体的首元素地址 = 成员变量的地址 - 偏移量。

我们可以通过先求出偏移量

&((task_struct*)0->n)

然后就可以求出来结构体的首元素地址

(task_struct*)&n - &((task_struct*)0->n)

所以当我们知道task_struct中的某个成员变量的地址就可以找到task_struct的地址,那么在task_struct中可以存放多个类似于struct listnode结构。

所以进程的排队根本就是,对task_struct中的各种数据结构的操作!!!

进程状态表述 教材中的进程状态表述

在这里插入图片描述

就绪状态

就绪状态指的是进程已经准备好执行,但由于其他进程正在使用CPU或者由于某种原因(如优先级、调度策略等),它尚未被调度器选中以获得CPU资源进行执行。在此状态下,进程已经通过了必要的检查和资源分配,只需等待调度器的调度即可。

阻塞状态

阻塞状态通常发生在进程需要等待某个外部事件(如I/O操作完成、信号量释放等)时。当进程处于阻塞状态时,它无法继续执行,必须等待该事件完成后才能进入就绪状态并等待调度执行。阻塞状态是一种非执行状态,CPU不会分配给处于阻塞状态的进程。

当我们的进程在等待软硬件资源的时候,如果没有就绪,我们的进程task_struct只能将自己的设置为阻塞状态,将自己的pcb链入等待的资源提供的等待队列当中。

所以我们可以知道进程状态的变迁,引起的是PCB会被OS变迁到不同的队列中。

创建状态

创建状态是进程生命周期的开始阶段。在创建状态中,操作系统为新进程分配了必要的资源(如内存空间、文件句柄等),并初始化了进程控制块(PCB)。此时,进程尚未进入就绪状态,也不能被执行。

执行状态

执行状态是指进程正在占用CPU并执行指令的状态。当一个进程从就绪状态被调度器选中后,它会进入执行状态,并开始使用CPU资源执行其代码。在执行过程中,进程可能会因为时间片用完或遇到I/O操作等原因而进入其他状态。

终止状态

终止状态是进程生命周期的结束阶段。当进程完成了所有任务或遇到无法恢复的错误时,它会进入终止状态。在终止状态中,操作系统会释放进程所占用的所有资源,并结束进程的执行。终止状态是进程的最终状态,一旦进入该状态,进程就无法再回到其他状态。

挂起状态

前提:计算机资源已经比较吃紧了

阻塞挂起:当有进程在阻塞状态时,这个进程短时间内不会被放到CPU上,内存又比较吃紧,那么就把进程的相关的代码和数据交换到磁盘中(换出),磁盘的这部分区域叫做swap分区。

当内存不在吃紧的时候再讲磁盘的代码和数据放入到内存,这一步叫做换入。

当我进行换出的时候进程的task_struct是不会被换出的,仍在内存当中。由上面挂起的知识我们可以知道,当我们创建一个进程的时候是先创建这个进程的task_struct,现将管理字段创建,这样哪怕这个进程的对应代码和数据被换出了,操作系统仍能根据task_struct中的信息方便再次将进程换入。

那么根据上面的讲述是不是swap分区设置的越大越好呢?内存与磁盘进行换入,换出操作其实就是一种IO操作,IO操作是一种十分低效的操作,那么操作系统的速度就会变得比较慢了。所以并不是swap分区设置的越大越好!!!

Linux中进程状态表述

要点

所谓状态,本质就是一个整型变量,在task_struct中的一个整型变量。状态决定的是后续的动作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 */ };

Linux下进程状态

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

在这里插入图片描述

进程状态查看

Linux进程状态详解 运行状态(running)

情况1

#include #include int main() { while(1) { printf("is running? pid: %d\n",getpid()); sleep(1); } return 0; }

运行结果

在这里插入图片描述

我们发现当前进程正在运行,但是我们发现我们查出来的状态是S,因为代码只有一行sleep,CPU执行printf这句代码非常的快,所以大部分时间都是处于休眠状态,几乎查不到运行状态。

情况2

接下来我们将sleep这行代码删去

int main() { while(1) { printf("is running? pid: %d\n",getpid()); } return 0; }

运行结果

在这里插入图片描述

我们去掉了sleep后程序不应该一直执行printf语句吗,然而为什么还是S状态呢?

因为printf的本质还是访问外设,CPU的速度是要比外设快的多的,CPU现将结果放入到显示器的缓冲区当中,然而不一定能够及时刷新,所以printf里面存在很多使我们进程处于等待设备就绪的过程。

情况3

我们去掉printf语句使进程一直运行。

int main() { while(1) {} return 0; }

运行结果

在这里插入图片描述

这时我们发现进程处于R状态了

然而我们发现在R状态的后面还有一个“ + ”,这个加号代表什么意思?

在Linux中,当您在命令行运行一个程序并在命令的末尾加上&符号时,这表示你希望该程序在后台运行。这意味着即使你关闭了终端或输入了其他命令,该程序也会继续运行。

具体来说:

前台运行:默认情况下,当您在终端中运行一个程序时,该程序会在前台运行。这意味着该程序会占用终端窗口,并且您必须等待它完成或手动停止它才能继续在终端中输入其他命令。后台运行:通过在命令的末尾添加&符号,您可以使程序在后台运行。这样,程序会继续执行,但您可以在终端中输入其他命令,而不会受到该程序的影响。 睡眠状态(sleeping)

睡眠状态又称为,可中断睡眠(interruptible sleep) ,也叫做浅度睡眠。

进程正在等待某件事情(如等待I/O完成、等待条件变量为真等)的完成而进入的一种状态。此时,进程会从CPU的运行队列上移除,并放入到休眠队列中等待被唤醒。

可中断睡眠状态的进程具有TASK_INTERRUPTIBLE的状态标志位,并且其特点在于即使等待的特定事件没有发生,进程也可以被其他因素唤醒,如用户空间等待条件变量时,在条件满足之前,进程也可以被信号唤醒。此外,产生一个中断、释放进程正在等待的系统资源或是传递一个信号都可以唤醒处于可中断休眠状态的进程。

进入可中断睡眠的情况、

1、等待资源或者条件

当进程需要等待某个资源(如磁盘I/O、网络I/O、锁等)变得可用时,它可能会进入可中断睡眠状态。一旦资源变得可用或等待的条件满足,进程就会被唤醒并继续执行。

用户空间中的条件变量和互斥锁(mutex)也是常见的导致进程进入可中断睡眠状态的原因。当进程尝试获取一个已经被其他进程持有的锁时,它可能会选择进入睡眠状态,直到锁被释放。

2、等待信号

进程可以显式地调用pause()或sigsuspend()等系统调用来进入可中断睡眠状态,并等待某个信号的到来。一旦进程收到该信号,它就会被唤醒并继续执行。

某些系统调用(如select()、poll()、epoll_wait()等)也会使进程进入可中断睡眠状态,以等待文件描述符上的某些事件(如可读、可写、异常等)发生。

情况1

下面的代码就是一个简单的C语言输入输出的代码,在等待用户的输入过程中,进程其实就处于可中断睡眠状态。

int main() { int a = 0; scanf("%d",&a); printf("a = %d",a); return 0; }

运行结果

在这里插入图片描述

其中在运行时,用户可以输入CTRL + c来终止这个进程。

所以根据上面所讲我们可以得出结论睡眠状态S,其实也是阻塞状态!!!

磁盘休眠状态(Disk sleep)

磁盘休眠状态又叫做不可中断睡眠,深度睡眠

在Linux操作系统中,磁盘休眠状态(通常称为Disk Sleep或D状态)是一种特殊的进程状态,表示进程正在等待I/O操作完成,但此时进程处于一种“不可中断”的休眠模式。这种状态通常与磁盘或其他块设备的I/O操作相关。

具体来说,当Linux进程尝试进行磁盘I/O操作(如读写文件)时,如果磁盘或其他相关硬件无法立即完成请求(可能是因为硬件正忙于其他操作、等待旋转到正确的位置、或其他任何硬件级别的原因),那么进程就会进入磁盘休眠状态。

在磁盘休眠状态下,进程会暂停执行,等待I/O操作完成。与其他休眠状态(如可中断睡眠状态,通常称为Sleep或S状态)不同,磁盘休眠状态是“不可中断”的。这意味着即使收到信号(如中断信号),进程也不会从休眠状态中唤醒,而是会继续等待I/O操作完成。

例子

当一个进程需要向磁盘写入数据的时候,此时磁盘的写入速度较慢,那么进程会在内存中等待,我们假设在等待的过程中,操作系统非常的忙碌甚至已经达到了吃紧的状态了。

此时假如操作系统将这个进程直接杀掉,那么当磁盘还在写入的时候突然发现磁盘的空间不足了,回去找进程的时候发现进程已经不见了,这时磁盘就会将数据直接清理掉,那么数据直接丢失。

那么到底是操作系统,进程还是磁盘的问题,操作系统为了避免上述现象就给这种进程添加了一种D状态,表示这个进程不能被中断,这就是深度睡眠状态。

总结

这种状态一般无法看见,因为当这种状态被看见了,就说明IO请求无法被立即完成了,此时的操作系统已经负载了,随时可能出现崩溃。

磁盘休眠状态也是一种阻塞状态!!!

停止状态(stopped)

在Linux中,进程的停止状态(Stopped State)通常指的是进程接收到某种信号后被暂停执行的状态。具体来说,当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU等信号时,它可能会进入停止状态。

kill命令

在讲解停止状态用例的时候先讲解一个命令kill

在Linux系统中,kill命令用于向进程发送信号。这些信号可以是要求进程终止、停止、继续运行或其他类型的通知。kill命令通常用于管理在Linux系统上运行的进程,特别是当某个进程变得无法响应或需要被强制终止时。

kill -信号 进程pid

所以想要使用kill命令,必须知道信号。

kill -l //查看所有信号

在这里插入图片描述

这里先讲解两个信号

kill -9 进程pid //杀死一个进程 kill -19 进程pid //给一个进程发送暂停信号,使一个进程暂停 kill -18 进程pid //让暂停的进程继续运行

情况1

下面我们将让下面的代码进入暂停状态

int main() { while(1) { printf("is running? pid: %d\n",getpid()); sleep(1); } return 0; }

在进程运行中输入

kill -19 进程pid

就可以让进程处于暂停状态

在这里插入图片描述

这时我们在输入

kill -18 进程pid

就可以让进程在次处于运行状态

在这里插入图片描述

但是我们发现在进程在次运行的时候,我们在进程运行时输入 ctrl + c,进程无法被终止。

因为我们将一个进程暂停了,就必须由前台转为后台!!!我们只能运用

kill -9 进程pid

杀死进程。

t (tracing stop)跟踪停止状态

停止状态的进程通常会有"T"或"t"的标志。其中,"T"可能表示一般的停止状态,而"t"可能更具体地表示跟踪停止状态(TASK_TRACED),即进程被调试器或跟踪工具暂停。

首先我们先运行下面这个程序

int main() { printf("hello Linux!\n"); sleep(10); int a = 0; scanf("%d",&a); printf("a = %d\n",a); return 0; }

当我们不打开gdb调试的时候,我们发现这个进程正处于S状态。

在这里插入图片描述

当我们执行gdb调试信息时,程序在运行后再断点处停下来,程序需要等待下一步调试指令,这时进程的状态就是t。

注:下面这个是持续监测进程信息

while :; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep; sleep 1; done

暂停状态其实也是一种阻塞状态!!!

僵尸状态(zombie)

概念

在Linux系统中,**僵尸进程(Zombie Process)**是一个已经终止但其父进程尚未通过调用wait()或waitpid()等系统调用来获取其终止状态的进程。这样的进程在进程表中仍然占据一个位置,但其代码、数据等已经被内核释放,只剩下进程描述符(task_struct)的一些信息,用于向父进程提供信息。

僵尸进程的特点

已终止:僵尸进程已经完成了其执行,但父进程还没有读取其退出状态。

占用资源:虽然僵尸进程不占用CPU或内存资源来执行代码,但它在进程表中仍占有一个位置,这可能导致系统进程表溢出。

由父进程管理:只有父进程才能读取僵尸进程的退出状态,并释放其占用的进程表位置。

我们把一个进程创建出来目的就是,让进程帮助做事情,进程最终死掉了,操作系统要获取进程退出的数据,给上层进行交代。

所以当一个进程退出的时候,代码和数据可以直接释放,但是当前进程的PCB不应该立即释放。

我们把一个进程已经执行完毕,但是我们当前没有获取它的退出相关数据时,此时进程的状态就是Z状态。

当我们父进程创建了一个子进程时,当子进程先退出的时候,父进程就必须读取子进程的状态。

情况1

下面的代码将模拟一个进程退出,看到僵尸进程

int main() { //创建子进程 pid_t id = fork(); if(id == 0) { //子进程进行工作 int cnt = 5; while(cnt) { printf("I am child, pid: %d, ppid:%d\n",getpid(),getppid()); sleep(1); cnt--; } //子进程退出没有回收 exit(0); } //父进程可以执行到这里 while(1) { printf("I am father, pid: %d, ppid:%d\n",getpid(),getppid()); sleep(1); } return 0; }

运行结果

在这里插入图片描述

我们可以发现当子进程退出后,进程的task_struct并没有销毁,进成变为僵尸进程(Z状态)。如何消除僵尸进程将在进程控制中详细讲解。下面只是浅谈。

如何消除僵尸进程

使用wait()或waitpid()系统调用:父进程可以通过wait()或waitpid()等系统调用来等待子进程的终止,并获取子进程的终止状态。这会使子进程的状态信息被完全清除,从而不再是僵尸进程。改写父进程,接管SIGCHLD信号:子进程死后,会发送SIGCHLD信号给父进程。父进程收到此信号后,执行waitpid()函数为子进程收尸。这是因为,即使父进程没有主动调用wait,内核也会向它发送SIGCHLD消息,虽然默认处理是忽略。但如果我们为SIGCHLD设置了一个处理函数,就可以在这个函数中调用wait或waitpid来处理子进程的终止状态。杀死父进程:如果父进程不再需要,也可以考虑直接杀死它。父进程死后,僵尸进程会成为“孤儿进程”,过继给1号进程init。init进程始终会负责清理僵尸进程,因此它产生的所有僵尸进程也会跟着消失。

为什么要创建一个僵尸进程

因为创建进程是希望这个进程给用户完成工作,那么子进程必须要有结果,这个结果一般是保留在子进程的task_struct中的。一个进程退出时,进程的代码和数据可以退出,但是task_struct不能退出。

只有父进程读取了子进程中的相关退出信息子进程才能由Z状态变为X状态。

如果父进程不读取,僵尸状态的进程会一直存在,task_struct对象也要一直存在。都是要占据内存的,这种情况成为内存泄漏。

所有的进程在退出的时候必须先到僵尸进程状态,然后别人读取后才会变成X状态。

父进程的退出状态是由谁读取的

一般来说父进程的退出状态直接就由bash进程读取了。

僵尸进程的危害

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

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

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

孤儿进程

概念

在Linux系统中,孤儿进程是指一个父进程退出,但它的一个或多个子进程还在运行,这些子进程就被称为孤儿进程。

情况1

int main() { //创建子进程 pid_t id = fork(); if(id == 0) { //子进程进行工作 int cnt = 500; while(cnt) { printf("I am child, pid: %d, ppid:%d\n",getpid(),getppid()); sleep(1); cnt--; } //子进程退出没有回收 exit(0); } int cnt = 5; //父进程可以执行到这里 while(cnt) { cnt--; printf("I am father, pid: %d, ppid:%d\n",getpid(),getppid()); sleep(1); } return 0; }

在这个代码中父进程先退出,子进程后退出,那么父进程都退出了,子进程的相关退出信息该由谁回收?

运行结果

在这里插入图片描述

通过上述代码的运行结果我们发信,最后子进程的父进程变成了1号进程。

所以一个进程的父进程先挂掉那么这个进程就会被1号进程所领养,那么1号进程是什么呢,我们通过top指令进行查看。

在这里插入图片描述

1号进程就是操作系统!!!

我们还会发现当一个进程变成孤儿进程的时候我们输入ctrl + c无法将进程杀掉,说明孤儿进程还会从前台进程变为后台进程!!!

本专栏为“小菜”linux学习之路 该文章仅供学习参考,如有问题,欢迎在评论区指出。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3