基础状态
计算机程序在运行过程中会不断"变化",这些变化就可以用程序的状态(state) 来描述。不同领域对"状态"的划分略有差别,但总体可以从 生命周期状态 和 运行时状态 两个角度来理解。这是最常见的一种"状态划分",从操作系统角度看一个程序/进程通常会经历:
1)新建状态(New / Created)
当用户启动一个程序时,操作系统会先创建一个进程,这时进程处于"新建状态"。在这个阶段,系统正在为它分配必要的资源,比如进程控制块(PCB)、进程号(PID)、初始内存空间等,但它还没有进入真正可执行的队列,也还没有开始占用 CPU 执行指令。
2)就绪状态(Ready)
当进程创建完成并且运行所需的基本条件都具备后,它会进入"就绪状态"。就绪状态的含义是:进程已经准备好运行了,但此刻 CPU 可能正在运行别的进程,所以它只能在就绪队列中等待调度,一旦轮到它,它就能马上开始执行。
3)运行状态(Running)
当操作系统把 CPU 分配给某个就绪进程后,这个进程就进入"运行状态"。运行状态表示进程正在 CPU 上执行自己的指令、进行计算或处理任务,是进程真正"在跑"的阶段。一个 CPU 核心同一时刻只能运行一个进程(更准确说是一个线程),其他进程则处于就绪或等待状态。
4)阻塞/等待状态(Blocked / Waiting)
如果进程在运行过程中遇到必须等待的事情,就会从运行状态转为"阻塞/等待状态"。常见情况包括等待磁盘读写、网络数据到达、键盘输入、或者等待某个锁资源等。处于阻塞状态的进程即使有 CPU 也无法继续执行,它必须等到等待的事件发生(比如 I/O 完成、锁被释放)才会重新回到就绪状态。
5)终止状态(Terminated / Exit)
当进程完成任务正常结束,或者因为错误崩溃退出,或者被用户/系统强制杀死时,它就进入"终止状态"。终止状态表示进程已经不再执行,操作系统会回收它占用的资源(内存、打开的文件、各种句柄等),最终彻底从系统中消失。
小结:这套状态是操作系统调度中最经典的:新建→就绪→运行→阻塞→终止。
linux中如何管理程序
在 Linux 内核中,task_struct 是进程/线程的核心描述结构 ,相当于内核里的"任务控制块",用于表示并管理一个正在运行的任务 ;它保存了该任务的关键信息,比如进程状态、调度信息、优先级、CPU 上下文、内存管理、打开的文件、信号处理以及父子进程关系等,因此 task_struct 承担着内核对进程(线程)进行调度与资源管理的主要责任。所以,linux中通过对task_struct的操作,就可以让进程处于各种位置,发挥出更好的效果。
1)task_struct 为什么能被"自由组织"
Linux 的链表实现是这样的:
-
双链表的节点类型是
struct list_head { struct list_head *next, *prev; } -
Linux 不搞"链表节点单独分配",而是把
list_head直接作为字段嵌进业务结构体里 (这里就是task_struct) -
需要把 task 放进哪个"集合/队列",就用对应的那个
list_head字段去挂链
直觉上就是:task_struct 身上带了很多"挂钩",每个挂钩都能把它挂到一个链表/队列/树里,所以组织方式很灵活。
2)一个 task 可以同时在很多"链/队列"里
task_struct 里典型的"组织用字段"大致是这几类(名字可能随内核版本略有变化,但思想一致):
-
全局任务链 :
tasks(list_head)让系统能遍历所有进程(比如
for_each_process()),这更多用于管理/统计,而不是调度的主结构。 -
父子关系与兄弟链 :
children、sibling(list_head)父进程有孩子链表,每个孩子在"兄弟链"里排队,从而能实现进程树。
-
线程组链 :
thread_group(list_head)同一个线程组(同一个 TGID)下的多个线程可以串起来。
-
PID 相关的哈希/链 :常见是
pid_links[](内部会用到哈希链hlist_node等)用于通过 pid 快速查找任务,而不是全表扫描。
-
调度相关结构 :通常不是简单"一个调度链表",而是按调度类(CFS/RT/DL)分别组织
例如 CFS 更像是"红黑树/队列 + 辅助链",RT 更像是"按优先级的队列数组"等。也就是说:调度主要挂在每 CPU 的 runqueue 上,不是挂在全局
tasks链上。
重点:同一个 task_struct 可以同时出现在多个集合里,因为它有多个不同的挂钩字段,互不冲突。
3)怎么从链表节点"访问回 task_struct",再访问里面的数据
用语言描述:为什么能"从成员指针回到结构体指针"
1)关键前提:成员在结构体里有固定"偏移量"
在 C 里,一个结构体 struct task_struct 的每个字段(比如 tasks 这个 struct list_head)在内存中的位置是固定的:
-
结构体起始地址是
&p(p是task_struct指针) -
成员
tasks的地址是&p->tasks -
二者之间相差一个固定的字节数,这个字节数叫 offset(偏移量) ,可以用宏
offsetof(struct task_struct, tasks)算出来
也就是说永远成立:
&p->tasks == (char*)p + offsetof(struct task_struct, tasks)
2)你在遍历链表时拿到的是"成员地址"
Linux 的链表是侵入式的:链表节点 list_head 直接嵌在 task_struct 里。
所以链表里存的/连的是 &p->tasks 这种"成员节点地址",而不是 p 本人。
遍历 tasks 链表时,pos 通常是 struct list_head *,它实际上指向某个进程的 p->tasks。
3)如何从 &p->tasks 反推 p
既然:
-
member_ptr = (char*)p + offset那反过来就是:
-
p = (char*)member_ptr - offset
这就是 container_of 的本质:知道成员指针 + 知道成员在结构体里的偏移量 → 推回结构体起始地址。
4)为什么要 (char*) 强转
因为指针做加减,单位是"指向类型的大小"。
-
task_struct* + 1会跳过一个task_struct的大小 -
我们要按"字节"精确地减去 offset,所以把指针转成
char*(1 字节单位)最合适。
例子:用一个小结构体模拟 task_struct + 链表节点
假设我们有个"任务"结构体:
-
pid是数据 -
tasks是挂到全局链表用的钩子(list node)struct list_head {
struct list_head *next, *prev;
};struct my_task {
int pid;
struct list_head tasks; // 侵入式链表节点:嵌在结构体内部
int state;
};
内存布局可以想象成这样(简化):
p (my_task 起始地址)
+0 pid
+4 (对齐/填充可能存在)
+8 tasks (list_head)
+... state
如果你现在只有 &p->tasks,想回到 p,你就做:
p = (char*)(&p->tasks) - offsetof(struct my_task, tasks)
为什么这对 Linux 的 task_struct 特别重要
因为 task_struct 里有很多"挂钩字段",比如(示意):
-
struct list_head tasks;(全局进程遍历) -
struct list_head thread_group;(线程组) -
还有调度相关的
sched_entity(内部也会被挂进调度结构)
不同链表拿到的"节点指针"可能是不同字段的地址:
-
从
tasks链表遍历 →list_entry(pos, task_struct, tasks) -
从
thread_group链表遍历 →list_entry(pos, task_struct, thread_group)
同一个 task 能被不同组织结构"同时管理",靠的就是:每个组织结构对应 task_struct 里的一个成员节点;需要哪个结构就用哪个成员节点反推出 task。
Linux 之所以用 container_of / list_entry 这种"从成员指针反推出外层结构体"的方式,本质原因是它采用了侵入式数据结构 :链表节点(list_head)不是单独分配的,而是直接嵌在 task_struct 这种业务对象内部。这样设计后,内核在管理进程时不需要额外创建"链表节点对象",而是让每个进程天然自带多个"挂钩",可以随时把它挂到不同的组织结构里(全局进程链、父子链、线程组链、调度队列等)。而遍历这些结构时拿到的只是挂钩指针,利用成员偏移量就能准确回到所属的 task_struct,从而访问进程的各种信息。
这样做的好处非常明显:效率高、内存省、结构灵活 。首先链表操作只改指针,不涉及频繁的动态内存分配/释放,减少碎片和开销,适合内核这种高性能场景;其次同一个 task_struct 可以同时挂到多种链/队列中,实现"自由组织",调度器和进程管理模块都能快速插入、删除、移动任务;最后 container_of 是纯编译期偏移计算,运行时成本极低,使得遍历链表时既能保持数据结构通用(只认 list_head),又能拿回具体类型(task_struct*)进行业务处理。
linux中如何管理设备
1)、struct device 是干嘛用的(一句话版)
它把"硬件设备"从各自为政,统一纳入一套内核可管理、可匹配、可观察的体系里
在它出现之前,Linux 是驱动自己找设备,设备关系混乱,没有统一生命周期
有了 struct device 之后:设备有"身份",驱动不再主动找设备,而是被动匹配,内核能统一管理设备
struct device 是 Linux 内核中对"一个设备"的统一抽象,它的作用不是描述寄存器细节,而是给每个硬件或逻辑设备一个被内核识别、管理和追踪的身份,使内核能够以一致的方式对待各种来源的设备(USB、PCI、SoC 内设等)。
2)、它改变了什么?(解决的核心问题)
1️⃣ 从"驱动中心" → "设备中心"
以前(老模型)
驱动:我去扫描硬件
驱动:我决定怎么初始化
现在(device model)
内核:发现一个设备
内核:给它建一个 device
内核:看看谁能驱动它
👉 struct device 把 Linux 从"驱动自己找硬件"的模式,转变为"内核先表示设备、再让驱动来匹配"的模式,设备成为系统中的一等公民,驱动不再主导流程,只负责在匹配成功后提供功能实现。
2️⃣ 不同总线,不同设备 → 统一规则
不管你是:
-
USB
-
PCI
-
I2C
-
SoC 内部外设
统一流程:
发现设备
→ 创建 device
→ 挂到某条 bus
→ bus 负责匹配 driver
→ 调用 driver->probe()
驱动只关心:
"你是不是我能处理的 device?"
不管设备来自哪种总线或固件描述方式,Linux 都会先创建一个 struct device 并把它挂到对应的总线上,再由总线负责按照统一规则匹配驱动,这让 USB、PCI、platform 等子系统在宏观逻辑上高度一致,然后由总线尝试为它匹配合适的驱动,匹配成功后调用驱动的 probe,设备才真正进入可工作状态。
3️⃣ 设备生命周期被标准化
以前:
- 设备什么时候生、什么时候死,全靠驱动自觉
现在:
-
插入 → 注册
-
拔出 → 注销
-
统一引用计数
-
统一释放时机
👉 当设备被移除或失效时,内核会先解除设备与驱动的绑定,调用驱动的移除逻辑,然后将该 struct device 从设备模型中移除,最终在引用计数归零后统一释放资源。减少内核崩溃和内存泄漏
三、运行逻辑(超简流程)
从设备出现到可用
硬件出现(插入 / 枚举 / 启动时发现)
↓
内核识别(USB/PCI/DT/ACPI)
↓
创建 struct device
↓
加入设备模型(device_add)
↓
挂到 bus
↓
bus 尝试匹配 driver
↓
匹配成功 → driver->probe()
↓
设备开始工作
设备消失时
设备移除
↓
解除 driver 绑定
↓
driver->remove()
↓
device 从模型中移除
↓
引用计数归零
↓
真正释放
四、你平时"看见"的变化
struct device 带来的直接可感知结果 :/sys/devices 能看到完整硬件拓扑,插 USB 不用重启,suspend / resume 自动级联,用户态能通过 udev 感知设备变化,驱动写法高度一致
👉 struct device 使 Linux 能够支持热插拔、自动加载驱动、统一的电源管理和清晰的硬件拓扑展示,这些能力并非单个驱动实现,而是设备模型集中提供的系统级能力。
五、和 task_struct 的本质对比(帮助理解)
| 维度 | task_struct | struct device |
|---|---|---|
| 管理对象 | 执行实体 | 硬件实体 |
| 谁创建 | fork | 总线 / 枚举 |
| 核心能力 | 调度 | 发现、匹配、生命周期 |
| 面向谁 | CPU | 驱动 |
一句话类比:
task_struct让 CPU 有秩序地"用人",
struct device让内核有秩序地"用硬件"。在内核整体中的定位
在内核架构上,
struct device扮演的是"硬件管理核心节点"的角色,所有设备相关的子系统都围绕它展开,而进程结构如task_struct只是设备的使用者,并不参与设备的创建和管理。