上篇文章:Linux:进程

创建多个进程

while :; do ps ajx | head -1 && ps ajx | grep myproc.exe; sleep 1; done
此时,只有一个父进程,而有多个子进程:

进程状态查看
ps aux / ps axj 命令
a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等
目录
1.进程状态的深层逻辑
1.1 创建状态
1.2运行状态
2.内核如何高效管理进程队列
2.1task_struct不是双链表吗?为什么会属于调度队列?
2.2求得一个变量的起始地址
上述总结
3.阻塞状态
3.1操作系统管理底层硬件
4.挂起状态
4.1阻塞挂起
4.2运行挂起
5.Linux的进程状态
5.1运行状态
5.2睡眠状态(可中断状态)
1.进程状态的深层逻辑
进程状态本质就是task_struct内部的一个整形变量。
站在操作系统原理角度,解释进程状态:运行,阻塞,挂起

1.1 创建状态
定义:进程正在被创建,但尚未完成初始化的状态。此时,操作系统正在为该进程分配必要的资源
内核视角:当一个父进程调用 fork() 时,内核开始通过“写时拷贝”(Copy On Write)技术复制父进程的资源。此时,进程的“身份证”——PCB (Process Control Block),在 Linux 中task_struct 结构体,已经被分配出来了。
关键点:此时 task_struct 已经存在,但该进程尚未被挂入运行队列 (Run Queue)。它就像一个刚填好入职表的新员工,工位(PCB)有了,但还没有正式进入部门排班表(运行队列),因此调度器(Scheduler)还“看”不到它,它不具备被 CPU 调度的资格。
1.2运行状态
定义:进程已获得 CPU 资源,其指令正在被执行。
内核视角: 在 Linux 内核中,为了管理的统一性,通常将就绪 (Ready) 和 运行 (Running) 统称为 TASK_RUNNING 状态。
逻辑状态和物理状态:虽然在逻辑图上我们将它们分开,但在内核结构中,只要进程在运行队列 (Run Queue) 中排队,或者正在 CPU 上跑,它都被标记为 R 状态。
调度机制:一个进程并不能永远霸占 CPU。当它的时间片 (Time Slice) 耗尽,或者发生了更高优先级的抢占,它会被剥夺 CPU 使用权。
链表:当进程处于运行状态(或就绪)时,它的 task_struct 必须存在于内核的运行队列中。这引出了一个核心问题:庞大的 task_struct 是如何高效地在队列中进出的?
2.内核如何高效管理进程队列
2.1task_struct不是双链表吗?为什么会属于调度队列?
举例,int a = 10;中,有4个字节,&a时只标识一个地址,因为&只取最小数字的地址,再配合类型,来访问合法的大小。
C语言对于任何类型,开辟空间时,变量的地址在数字上等于开辟的众多字节中地址最小的那个数字。
在基础数据结构中运行队列是这样实现的:定义一个链表节点,节点里包含一个指向 task_struct 的指针。
而在Linux中,链表结构体中没有数据,是将链式信息嵌入到结构体属性中,next指针,指向的是下一个struct task_struct内部属性link

struct list_head {
struct list_head *next;
struct list_head *prev;
};
struct task_struct {
// … 其他进程信息 …
struct list_head run_list; // 嵌入的链表节点
// … 其他进程信息 …
};
而task_struct 本身是一个巨大的结构体,它包含了很多成员。其中,run_list 只是它众多成员中的一个
灵活性:一个 task_struct 可以同时包含多个 list_head 成员。
它可以通过成员 A 挂在运行队列里。它可以同时通过成员 B 挂在父子进程链表里。它可以同时通过成员 C 挂在等待队列里。
这就解释了为什么进程可以灵活地在各种状态间切换——本质上就是将 task_struct 中的某个 list_head 节点从一个队列(如运行队列)摘下,再挂到另一个队列(如等待队列)上。
2.2求得一个变量的起始地址
通过struct内部元素的地址,反求结构体变量的起始地址->求在结构体变量中的偏移量->再通过原本就知道的内部地址减去偏移量即可。



得到正确结果:

而这样的结构设计可以对内核对象采用同一种通用的方式完成,使其更具有通用性,并且在struct task_struct{ // 进程其他属性 }中,就算多插入几个链表,一个task_struct,就既可以属于双链表,也可以属于调度队列,未来还可以属于任何结构。
上述总结
进程状态的本质:是进程 PCB (task_struct) 在内核不同队列(运行队列、等待队列)之间的流转。
创建状态:是 task_struct 被分配并初始化,但尚未挂入运行队列的阶段。(是指在内核中有全局链表,如果将所有的PCB链接到链表里,但有的进程中的PCB没有放在运行状态的队列里,它也从来没有被调度过,单独的属于全局链表,并没有主动的被放到任何一个结构中的状态。)
运行状态:是 task_struct 被挂入 CPU 运行队列(Runqueue),并被调度器选中的阶段。
内核的高明之处:通过“侵入式链表”和“偏移量计算”,Linux 实现了让同一个进程极其灵活地属于多个不同的集合(队列),而无需反复申请和释放内存。这正是操作系统高效管理成千上万个进程的秘诀。
3.阻塞状态

此时程序在等待输入

进程等待某种资源,但是资源没有就绪,CPU就会让当前进程阻塞住,直到对应的资源就绪,进程等待键盘就绪,但是键盘没有被摁下,CPU阻塞进程,直到键盘被摁下,进程继续。
阻塞的本质是不调度
3.1操作系统管理底层硬件
操作系统的管理逻辑是先描述再组织

task_struct因为等待设备,而导致进程不被调度的情况,叫做进程阻塞。
运行和阻塞的本质,就看task_struct被放在了哪一个队列->把对进程的管理工作,转换为数据结构的增删查改
4.挂起状态
当系统物理内存耗尽时,操作系统必须采取紧急措施来释放内存,Linux 会将一部分进程的代码和数据从内存移动到磁盘的 Swap 分区中,这个过程称为 “换出”。Swap 分区是磁盘上预留的特殊区域,用于扩展虚拟内存。当一个进程的部分或全部内存被换出到磁盘时,该进程就会被置于 “挂起状态”。此时虽然进程的 task_struct(进程控制块)仍在内存中,但其实际的代码和数据已被移走。
对于swap分区,其大小不能太大,否则OS会依赖swap分区,swap in 和 swap out的频率增加,会导致系统速度降低

4.1阻塞挂起
进程在等待队列中被挂起,属于阻塞挂起
特点:
进程本身就在等待事件,不占用CPU
因内存紧张,被操作系统“顺便”换出到磁盘
即使等待的事件就绪,也必须先换入内存才能被唤醒
4.2运行挂起
进程在调度队列中,但还没有轮到此进程时,将此进程挂起,就属于运行挂起
特点:
进程本该能运行,但因内存不足被换出
即使CPU空闲,也必须先换入内存才能被调度
典型场景:后台低优先级进程在内存紧张时被换出
5.Linux的进程状态
Linux的进程状态是通用操作系统进程状态理论在具体实现上的扩展和细化
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),在这个 状态的进程通常会等待10的结束。
T停止状态(stopped):可以通过发送SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
5.1运行状态


5.2睡眠状态(可中断状态)


操作系统中的阻塞状态,在Linux中表现为睡眠状态
并且在这种情况下的休眠状态,为可中断休眠状态:


本章完。
网硕互联帮助中心




评论前必须登录!
注册