[操作系统] 进程状态详解

[操作系统] 进程状态详解

在操作系统中,进程是程序执行的基本单位,操作系统负责管理进程的生命周期。为了高效地管理进程,操作系统通过定义不同的进程状态来表示进程在不同时间点的行为。本文将详细介绍常见的进程状态及其相互之间的转换过程。进程状态概述在kernel进程有时候也叫事件。进程在执行过程中,可能会处于不同的状态。每个进程状态代表了进程在生命周期中的一种阶段,操作系统根据进程的当前状态采取不同的调度策略。

进程状态在kernel源代码里的进程状态定义如下:

代码语言:javascript代码运行次数:0运行复制/*

*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):并不意味着进程一定在实际运行中。它表明进程要么正在运行,要么处于运行队列中(Ready),等待操作系统调度器将其调度到CPU上执行。换句话说,处于运行状态的进程已经具备了运行的条件,只是可能还在等待CPU资源的分配。睡眠状态(S):意味着进程正在等待某个事件的完成。这种状态有时也被称为可中断睡眠(Interruptible Sleep)。在可中断睡眠状态下,进程可以被外部信号唤醒。例如,当进程等待某个I/O操作完成时,它会进入睡眠状态,但如果在此期间接收到一个信号,进程可以被唤醒并继续执行。磁盘休眠状态(D):也称为不可中断睡眠状态(Uninterruptible Sleep)。处于这个状态的进程通常在等待I/O操作的完成,如磁盘读写操作。与可中断睡眠状态不同,不可中断睡眠状态的进程不能被外部信号唤醒。这是因为I/O操作的完成是进程继续执行的必要条件,任何外部信号都不能中断这一过程。例如,当进程正在从磁盘读取数据时,它会进入不可中断睡眠状态,直到数据读取完成。停止状态(T)表示进程被暂停执行。可以通过发送SIGSTOP信号给进程来将其置于停止状态。被暂停的进程可以通过发送SIGCONT信号让其继续运行。例如,在调试过程中,开发者可能会发送SIGSTOP信号来暂停进程,以便检查其状态和变量,然后通过SIGCONT信号恢复进程的执行。死亡状态(X)是一个特殊的返回状态,表示进程已经完成其任务并退出。你不会在任务列表中看到处于死亡状态的进程。当进程完成执行后,操作系统会回收其资源,并将其从进程表中删除。这个状态主要用于表示进程的生命周期已经结束。进程状态转换进程状态之间的转换并非是线性的,实际上,进程的生命周期充满了状态的切换。系统中的进程调度策略和资源分配机制都会影响这些转换的频率和时机。

运行: 运行和就绪可以看做一种方式。

一个CPU对应一个调度队列(runqueue),队列。

进程创建:进程创建时,会创建一个 task_struct 数据结构,其中包含进程的所有信息。每个进程在创建时都会被放入一个 就绪队列(runqueue),该队列是一个先进先出(FIFO)队列。进程就绪:当进程处于就绪状态时,它会被加入到 CPU 的就绪队列(runqueue)中,等待 CPU 调度器的分配。这个状态是由操作系统管理的,表示进程已经可以运行,但还没有分配到 CPU。进程运行:CPU 调度器从就绪队列中选择一个进程并开始执行它。此时,进程进入 运行状态,并且会根据时间片来轮流使用 CPU。阻塞:添加和移除等待队列的示意图

从运行队列变成阻塞的本质就是把PCB链入不同的状态队列之中。当调度当前进程的时候,硬件设备未响应,该进程被链入等待队列。当硬件响应后,该进程被重新链回运行调度队列,当调度的时候再将响应的硬件信息读取。

进程等待:如果进程在运行过程中需要等待某些事件(例如等待硬件设备操作完成,程序有scanf时需要等待键盘输入),它会被移到 等待队列(wait queue) 中。在等待队列中,进程会处于阻塞状态,直到所需事件发生。事件包括,键盘,显示器,网卡,磁盘,摄像头,话筒… OS管理系统中的各种硬件资源同样遵循:先描述,在组织!

进程阻塞:例如,当进程等待硬件设备时,设备会进入 device 阻塞队列,这个队列是一个结构体 device 组织的链表,存储着等待的进程。硬件设备在准备好后,会唤醒相关的进程。 挂起就是把内存资源换到磁盘上,挂起后的资源在恢复时进行换出。

阻塞挂起(极端条件):操作系统内存资源紧张时,将没有被调度的进程相关的内存块(代码、数据等)交换到到磁盘的swap交换分区上,只在原先队列保留PCB。

当操作系统感知到恢复,操作系统就会把交换到磁盘的内存块重新加载回内存,重新构建指针映射等,然后将内存块代码等信息重新加载进内存。

就绪挂起(更极端条件):当内存空间及其短缺时,就会把就绪状态的进程也挂起。

进程结束后task_struct被销毁再次理解:内核中连接管理进程的数据结构是双向链表通过进程状态是如何转换得知,每个状态都有一对应的队列,当进程进入对应状态时就会将该进程连接到对应的队列中去。但我们又在前文所提,kernel中管理进程的数据结构是双向链表。那么进程是如何保证即不违背内核管理制度,又可以进行不同状态的转换呢?

在了解整体结构之前可以先了解管理进程的双向链表是怎么形成的。

普通的双向链表是由prev和next进行管理连接,但是进程是由task_struct和数据组成的,那应该怎么将双向链表的管理方式引入呢?

实际上在task_struct中维护了结构体list_head,该结构体组成如下:

代码语言:javascript代码运行次数:0运行复制struct list_head

{

struct list_head *next, *prev;

}task_struct(伪):

代码语言:javascript代码运行次数:0运行复制struct task_struct

{

int x;

int y;

list_head links;

...

...

}有了list_head,每个进程之间就可以通过task_struct中的list_head进行双向连接。前节点的next指向下一个进程的list_head,下一个进程的prev指向前一个进程的list_head。

既然是通过嵌套的双向链表来管理,那么又引入了新问题:通过list_head连接管理的双向链表在遍历的时候我们只能遍历到每一个list_head的地址,那么我们要如何通过一个进程task_struct中的list_head得到task_struct的地址,从而对进程进行访问、修改或者删除?

在C语言结构体的知识中,结构体中关于变量和各种类型的存储是依赖偏移量的,所以我们只要得到在当前task_struct中list_head的偏移量,那么就可以通过以下计算公式得到task_struct:

代码语言:javascript代码运行次数:0运行复制(struct task_struct *)((char *)ptr - (char *)&(((struct task_struct *)0)->links))这个公式的关键在于理解为什么可以通过假设 task_struct 起始地址为 0 来计算出 links 的偏移量,以及这种假设背后的原理。

假设 ptr 是指向list_head节点 links 的实际地址,我们通过以下计算反推出 task_struct 的起始地址。

(((struct task_struct *)0)->links): 编译期计算 links 在 task_struct 中的偏移量(例如:8 字节)。这段代码的确会得到 links 在 struct task_struct 类型的结构体中的偏移量,这是编译器通过结构体的定义和内存布局规则计算出来的,而不是通过访问实际内存得到的。(char *)&(((struct task_struct *)0)->links): 将 links 的偏移量转换为字节地址(方便后续指针运算)。(char *)ptr - 偏移量: 从 ptr(links 的实际地址)减去偏移量,回退到结构体的起始地址。(struct task_struct *): 将起始地址转换为 struct task_struct * 类型。以上就是如何通过一个进程task_struct中的list_head得到task_struct的地址的方法理解。

在解决了内核中全局管理PCB的双链表构成和使用后,又回到了之前的问题,如何在保证PCB既在全局双链表中,又可以在多个状态转换,在对应的状态队列连接和断开?

实际上,在Linux中许多数据结构都是网状的。task_struct中list_head links并不是只存在一个,而是每一种状态都对应一个links(包括全局双向链表)。

只不过各个状态的links遵循FIFO原则,与队列的管理方法保持一致,所以我们叫这些状态管理的双链表数据结构叫做队列。

一个PCB在内核中只存在一个,不是拷贝在许多个队列中进行管理,而是通过上述方法,可以隶属于多个数据结构。即使这个PCB在不同的状态中来回转换,也会始终保持在全局双链表中连接。

这就是为什么PCB即使在各种状态队列中,但我们认为PCB是通过双链表管理的。

代码语言:javascript代码运行次数:0运行复制+-----------------+ +-----------------+ +-----------------+ +-----------------+

| task_struct | | task_struct | | task_struct | | task_struct |

+-----------------+ +-----------------+ +-----------------+ +-----------------+

| next | | next | | next | | next |

| prev | | prev | | prev | | prev |

+-----------------+ +-----------------+ +-----------------+ +-----------------+

| next | | next | | next | | next |

| prev | | prev | | prev | | prev |

+-----------------+ +-----------------+ +-----------------+ +-----------------+

| next | | next | | next | | next |

| prev | | prev | | prev | | prev |

+-----------------+ +-----------------+ +-----------------+ +-----------------+

| next | | next | | next | | next |

| prev | | prev | | prev | | prev |

+-----------------+ +-----------------+ +-----------------+ +-----------------+如何查看进程的状态ps命令ps 显示当前快照中的进程信息。它不会动态更新,因此适合用来获取某一时刻的进程状态。

代码语言:javascript代码运行次数:0运行复制ps aux / ps axj 命令

ps ajx | head -1; ps ajx | grep myprocess // head -1 用来显示第一行的列信息展示,方便辨认信息

// ; 用来分割,使一次执行多个命令

// grep myprocess 只看myprocess这个程序的进程信息a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。 x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。 j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息 u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等top 命令top 提供了实时的、动态更新的进程视图,默认情况下每三秒钟刷新一次。它非常适合监控系统的性能和进程活动。

常见交互式命令(在top运行时按下):h:帮助页面,显示可用的交互命令。k:杀死一个或多个进程,需要输入PID。r:调整进程的优先级(nice值),同样需要输入PID。fF:进入字段管理器,选择要显示的列。oO:设置显示的排序方式。M:按照内存使用排序。P:按照CPU使用排序。T:按照运行时间排序。q:退出top。常见管道搭配:虽然top本身是一个交互式的工具,不常与管道直接搭配使用,但可以通过一些技巧来结合使用:

top -b -n 1 | grep :使用批处理模式(-b)运行top一次(-n 1),然后通过grep筛选出特定的进程。top -b -n 1 | head -n 8:获取top输出的前几行,通常包含系统负载和资源使用概览。状态的详细讲解在 Linux 操作系统中,每个进程都有一个状态,用于反映进程在特定时刻的执行情况。常见的进程状态包括运行、睡眠、磁盘睡眠、停止、追踪停止、死亡和僵尸状态。下面将逐一解释这些状态。

运行/就绪状态R (running)表示进程正在 CPU 上运行或准备运行。进程被调度程序选中并获得 CPU 时间片。

解释:处于“运行”状态的进程正在执行程序代码,或者正在等待 CPU 执行。因为一个CPU对应一个运行队列,所以单核CPU中只有一个进程可以处于运行状态,多核 CPU 可以并行运行多个进程。模拟代码:代码语言:javascript代码运行次数:0运行复制#include

#include

int main() {

printf("Process is running...\n");

sleep(10); // 模拟进程正在运行,等待10秒

return 0;

}查看进程状态:代码语言:javascript代码运行次数:0运行复制ps -eo pid,state,cmd | grep 如果进程处于运行状态,state 列会显示为 R。

S (sleeping) – 睡眠状态(浅睡眠)表示进程处于可中断睡眠状态,通常是进程正在等待某些事件(如 I/O 操作完成)或信号。

解释:进程被挂起并等待某些外部事件(例如文件 I/O 操作)或等待信号中断。这种睡眠是可中断的,意味着如果有外部事件(如信号)发生,进程可以被唤醒。模拟代码:代码语言:javascript代码运行次数:0运行复制#include

#include

int main() {

printf("Process is sleeping...\n");

sleep(10); // 进程进入睡眠状态,等待10秒

return 0;

}查看进程状态:代码语言:javascript代码运行次数:0运行复制ps -eo pid,state,cmd | grep 如果进程处于睡眠状态,state 列会显示为 S。

D (disk sleep) – 磁盘睡眠状态(深度睡眠)表示进程处于不可中断睡眠状态,通常是由于等待某些硬件操作完成,凡是涉及到对磁盘这种关键存储设备进行访问,进程进行高IO的时候,不设置为S,设置为D,防止数据丢失。操作系统无权kill进程(区别于S的可中断进程),只能等待自己结束进程。也就是说磁盘 I/O 操作时,进程无法被中断,必须等到 I/O 操作完成。

解释:进程正在等待某些硬件资源(如磁盘读写)完成,且无法被外部信号打断。这种状态可能会导致系统响应较慢。模拟代码:代码语言:javascript代码运行次数:0运行复制#include

#include

#include

int main() {

int fd = open("/path/to/large/file", O_RDONLY); // 触发磁盘 I/O 操作

if (fd < 0) {

perror("Failed to open file");

return 1;

}

sleep(10); // 进程会因等待磁盘 I/O 完成而进入不可中断睡眠状态

close(fd);

return 0;

}查看进程状态:代码语言:javascript代码运行次数:0运行复制ps -eo pid,state,cmd | grep 如果进程处于磁盘睡眠状态,state 列会显示为 D。

T (stopped) – 停止状态表示进程已被暂停,通常是由于接收到 SIGSTOP 信号或在调试过程中被暂停。

解释:进程不再运行,但仍保留其资源和状态,直到恢复。调试程序时,进程也会进入停止状态。模拟代码:代码语言:javascript代码运行次数:0运行复制sleep 10 # 启动一个进程

kill -STOP # 发送 SIGSTOP 信号暂停进程查看进程状态:代码语言:javascript代码运行次数:0运行复制ps -eo pid,state,cmd | grep 如果进程处于停止状态,state 列会显示为 T。

t (tracing stop) – 追踪停止状态表示进程因调试而被暂停,通常发生在进程被调试器(如 gdb)附加时。

解释:进程处于调试过程中,打断点后进行run,遇到断点时执行被暂停,以允许调试器检查进程状态。模拟代码:代码语言:javascript代码运行次数:0运行复制sleep 10 # 启动一个进程

gdb -p # 附加调试器查看进程状态:代码语言:javascript代码运行次数:0运行复制ps -eo pid,state,cmd | grep 如果进程处于追踪停止状态,state 列会显示为 t。

X (dead) – 死亡状态表示进程已终止或死亡,通常是进程已完全退出,但其进程控制块(PCB)仍未被操作系统回收。

解释:进程已终止并退出,但操作系统尚未完全清理与该进程相关的资源。查看进程状态:代码语言:javascript代码运行次数:0运行复制ps -eo pid,state,cmd | grep 如果进程处于死亡状态,state 列会显示为 X,但是一般死亡状态会将PCB数据进行删除,所以一般情况下无法查看到。

僵尸进程进行单独讲解。

僵尸进程 (zombie)什么是僵尸进程?僵尸进程(Zombie Process)是指一个已经终止的进程,但其父进程尚未调用 wait() 或 waitpid() 来读取该进程的退出状态,从而使得操作系统无法回收该进程的资源。简单来说,原因是,⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,虽然子进程已经结束了,但是父进程没有宣告子进程的死亡,父进程需要调用函数来获取子进程的退出状态,所以⼦进程进⼊Z状态。结果是,僵尸进程的进程号(PID)仍然被占用,但其内存空间和其他资源已被释放。

子进程的结果相关信息存放在task_struct中。

用代码进行模拟僵尸进程出现的情况:

代码语言:javascript代码运行次数:0运行复制#include

#include

#include

#include

#include

int main()

{

printf("我是fork前的进程!我的pid是:%d\n", getpid());

pid_t id = fork();

if (id < 0)

{

perror("fork");

return 1;

}

else if (id == 0)

{

// child process

int i = 5;

while (i--)

{

sleep(1);

}

printf("子进程即将结束,我将变成僵尸进程。\n");

// 子进程在这里结束,但父进程不会等待它,所以它会变成僵尸进程。

}

else

{

// father process

// 父进程进入无限循环,不调用wait或waitpid

while (1)

{

sleep(1);

printf("我是父进程!我的pid:%d,我的父进程id:%d\n", getpid(), getppid());

}

}

return 0;

}僵尸进程的生成过程:子进程结束: 当一个进程结束时,操作系统会保留它的退出状态信息,直到父进程能够读取该状态信息(通过调用 wait() 或 waitpid())。进程结束后,操作系统会将其状态标记为“僵尸”,这时它会占用一个 PID,但没有实际的运行资源。父进程未读取退出状态: 如果父进程在子进程终止后没有读取它的退出状态,操作系统就无法回收这个子进程的 PID 和一些控制信息,因此该子进程仍然存在,成为僵尸进程。僵尸进程的危害占用系统资源: 每个进程都占用一个进程号(PID)。即使僵尸进程不再运行,但由于它仍然存在于进程表中,它占用了系统资源,特别是有限的 PID 资源。如果僵尸进程没有被清除,系统最终会耗尽 PID,导致无法启动新的进程。导致系统性能下降: 如果有大量的僵尸进程存在,它们仍然占用一些系统内存和资源,这可能导致系统性能下降,尤其是当系统负载较高时。父进程可能变得不稳定: 如果父进程不能正确地回收子进程(调用 wait() 或 waitpid()),可能会导致父进程的逻辑出现问题,甚至可能会在某些情况下导致父进程的崩溃。系统管理上的难题: 系统管理员通常需要手动查找并清理僵尸进程。过多的僵尸进程可能增加系统管理的复杂度,尤其是在长时间运行的系统上。如何避免或清理僵尸进程?父进程调用 wait() 或 waitpid(): 父进程在子进程结束后应该及时调用 wait() 或 waitpid() 来回收子进程的退出状态,防止僵尸进程的产生。采用 signal 机制: 父进程可以捕捉到子进程结束的信号(如 SIGCHLD),并在接收到信号时及时清理子进程。通过 init 进程回收: 如果父进程在子进程结束后还没有回收它,init 进程(PID 1)会自动回收这些孤儿进程,防止它们变成僵尸进程。使用 nohup 或 disown: 对于后台运行的任务,使用 nohup 或 disown 可以确保进程结束后不会留下僵尸进程。 程序结束后,程序运行时的所有进程都会全部删除,不会永久保留,僵尸进程只是在程序运行时的不规范编码造成的进程问题。但是像常驻内存进程,那些被设计为在系统启动时自动启动,并且在整个系统运行期间持续存在于内存中的进程,造成的内存泄漏会十分严重!

slab机制在 Linux 中,为了提高内存的分配和回收效率,特别是针对内核使用的频繁的数据结构,采用了 Slab 分配器(Slab Allocator)来进行内存管理。这种机制有助于提高性能,并减少内存碎片。

Slab 机制的基本概念Slab 分配器是一种用于内核内存管理的高效机制,它通过预分配内存块(称为 slab)来管理频繁使用的数据结构对象。这样做可以有效减少频繁的内存分配和释放操作带来的性能开销。

Slab 的工作原理Slab 缓存: Slab 分配器为内核中常用的数据结构(如 task_struct 等)预分配内存,称为 slab 缓存。每个缓存分配了一定数量的内存块,内存块大小与数据结构大小匹配,内存块是可重用的。Slab 分配与复用: 当内核需要某个数据结构时,Slab 分配器会从已有的 slab 缓存中分配内存块,如果没有空闲的内存块,则会分配新的 slab。内存块分配完毕后,不再需要时会返回给 Slab 分配器,进行复用。这减少了对内存的频繁分配和释放操作。内存块状态: 每个 slab 的内存块有 3 种状态: 空闲:内存块没有被分配,可以用来存储数据。已分配:内存块被使用,存储了有效数据。被释放:内存块被释放,处于空闲状态,准备重新使用。Slab 分配器的优点: 提高性能:通过缓存机制,Slab 分配器避免了频繁的内存分配和释放,提高了性能。减少内存碎片:由于内存块大小与数据结构大小一致,Slab 分配器减少了内存碎片的产生。优化内存使用:内存块的复用保证了相同数据结构在整个内核中的一致性,减少了内存浪费。如何复用内存块?task_struct** 的示例**: 假设内核有一个 task_struct 数据结构,它在进程管理中非常常见。Slab 分配器为 task_struct 创建了一个缓存,并分配了一些内存块来存储多个 task_struct 对象。分配内存块: 当创建新进程时,Slab 分配器会从 task_struct 缓存中分配一个内存块,填充进程信息。释放内存块: 当进程结束时,task_struct 对象会被释放。此时,Slab 分配器会将该内存块标记为“空闲”,以便在需要时重新使用。避免重复分配: 通过复用已分配的内存块,Slab 分配器避免了内存的碎片化,也减少了分配和释放内存的开销。总结Slab 分配器是一种用于优化内存分配和回收的机制,它通过缓存和复用内存块来提高内存管理的效率。内核通过为常用数据结构(如 task_struct)创建 Slab 缓存来减少内存碎片和提升性能。这使得 Linux 内核在内存使用上更加高效,同时减少了内存分配和释放的复杂度。

相关推荐

痤疮多长时间能好
36365

痤疮多长时间能好

📅 07-23 👁️ 3293
带过滤空烟管
36365

带过滤空烟管

📅 07-27 👁️ 9720
最有价值的坐骑!十小时即可入手水黾坐骑攻略
det365娱乐场所官方网

最有价值的坐骑!十小时即可入手水黾坐骑攻略

📅 07-27 👁️ 5016