写在开始:本文回答的五个核心问题

  1. 操作系统最小自洽内核需要哪些功能?没有它们为什么无法运转?

  2. 调度模块和系统模块如何协同工作?中断在其中扮演什么角色?

  3. 用户态读文件和读Socket的完整路径是什么?区别在哪里?

  4. 内核态或调度执行期间被中断会怎样?系统为什么不会崩溃?

  5. 第一个进程何时创建?进程树和线程组在内核中如何组织?

作者介绍

CodeStats。资深底层技术爱好者,专注计算机体系结构、操作系统内核与Java虚拟机实现原理。

目录

  • 一、核心基石:操作系统最小自洽内核需要哪些功能?

  • 二、启动流程:从裸机到第一个进程

  • 三、中断机制:三种中断类型与物理触发流程

  • 四、调度核心:调度模块与系统模块如何协同

  • 五、进程树:第一个进程的创建与进程/线程组织结构

  • 六、安全边界:内核态被中断为什么不崩溃

  • 七、文件读取:从read()到磁盘的完整数据路径

  • 八、Socket原理:网络数据收发完整流程

  • 总结:操作系统完整执行循环

一、核心基石:操作系统最小自洽内核需要哪些功能?

1.1 什么是“自洽”?

一个能“自洽运转”的内核,指不依赖任何外部程序,仅凭自身代码和硬件交互,就能无限稳定运行的核心模块集合。

1.2 五个核心模块(缺一不可)

模块 物理存在形式 核心职责 缺了会怎样
中断服务程序 IDT(中断描述符表)+ 处理函数机器码 响应硬件(时钟/网卡/键盘)和软件(syscall)信号 CPU无法感知外部事件,系统静态死亡
调度程序 schedule()函数 + 运行队列 决定下一个进程谁上CPU,实现并发假象 多进程无法共享CPU,系统无法处理多任务
内存管理 页表 + 伙伴系统 + slub分配器 管理物理内存分配、虚拟地址映射、内存保护 进程无法拥有独立地址空间,相互覆盖崩溃
进程管理 task_struct数组 + 进程链表 创建/销毁进程、维护进程状态、管理进程树 无法创建新进程,永远无法运行用户程序
系统调用接口 syscall入口 + 系统调用表 提供用户态进入内核态的“门” 用户态程序无法请求内核服务,无法读写文件/网络

1.3 五个模块如何构成闭环(自洽循环)

中断 → 唤醒调度器 → 调度器选择进程 → 进程需要内存 → 进程通过系统调用请求服务 → 服务中又依赖中断感知硬件事件 → 回到中断

第一推动力:硬件时钟芯片向CPU引脚发送电信号。没有这个电信号,IDT中的中断服务程序永远不会被执行,调度器只是一段静态内存数据。

二、启动流程:从裸机到第一个进程

2.1 五个阶段(物理时序)

阶段 执行者 物理动作
通电复位 硬件电路 CPU复位,RIP强制置为0xFFFFFFF0,指向BIOS固件
固件执行 BIOS/UEFI 初始化内存控制器,枚举PCIe设备,加载Bootloader
加载内核 Bootloader(GRUB) 加载vmlinuz到内存,切换CPU到64位长模式,跳转内核入口
内核初始化 内核代码(start_kernel 线性执行:填IDT→建页表→创idle进程→创PID=1→挂载根文件系统
跳转用户态 iret指令 CPU特权级从Ring 0切到Ring 3,RIP指向/sbin/init入口

2.2 关键事实

  • start_kernel执行期间,中断关闭,无进程切换,无调度。

  • 调度器代码虽在内存,但从未被执行过——只是静态机器码。

  • PID=1的初始寄存器值(RIP、RSP、CR3)是内核手工写入task_struct的,不是调度器“调度”出来的。

三、中断机制:三种中断类型与物理触发流程

3.1 三种中断类型

类型 触发源 典型例子 是否可屏蔽
硬件中断 外部硬件电信号 时钟中断、网卡收到数据包、键盘输入 部分可屏蔽
异常 CPU执行指令检测到异常 缺页异常、除零错误、调试断点 不可屏蔽
软件中断 程序执行特殊指令 syscall指令(系统调用) 不可屏蔽

3.2 硬件中断物理流程(以时钟中断为例)

text

时钟芯片 → CPU的INTR引脚电信号
    ↓
CPU完成当前指令后暂停执行流
    ↓
CPU查IDTR → 找到IDT表基址 → 读时钟中断表项 → 加载处理函数地址到RIP
    ↓
CPU自动将当前RIP/RSP/RFLAGS压入内核栈
    ↓
执行中断处理程序(内核态)
    ↓
执行iret指令 → CPU从内核栈弹出RIP/RSP/RFLAGS → 恢复原执行流

3.3 系统调用本质(软件中断)

用户态执行syscall指令时发生的物理事实:

  1. CPU自动将当前RIP存入RCX,将RFLAGS存入R11

  2. CPU从IA32_LSTAR MSR寄存器(内核启动时预设)读取入口地址,加载到RIP

  3. CPU将特权级从Ring 3切换到Ring 0

  4. 跳转到entry_SYSCALL_64内核入口

  5. 内核根据rax寄存器中的系统调用号查sys_call_table,执行对应函数

  6. 执行sysret指令,恢复RIP/RFLAGS,特权级切回Ring 3

四、调度核心:调度模块与系统模块如何协同

4.1 调度器两种触发方式

触发方式 触发源 频率/时机 执行动作
周期性调度 时钟中断→timer_interruptscheduler_tick 1000Hz(每1ms) 更新当前进程时间片,若耗尽则置TIF_NEED_RESCHED标志
主动调度 进程调用schedule() 进程主动让出CPU时(sleep/wait/yield) 立即执行上下文切换

4.2 CFS调度器核心算法

  • 每个进程维护vruntime(虚拟运行时间)——已运行时间的加权值

  • 运行队列是一棵红黑树,以vruntime为键值排序,最左节点vruntime最小

  • 调度器每次选择红黑树最左节点运行

  • 时钟中断触发时,当前进程vruntime增加,若不再是最左节点,置TIF_NEED_RESCHED标志

4.3 三方协同物理流程

text

时钟中断 → 置TIF_NEED_RESCHED标志
    ↓
中断返回 → 检查标志 → 调用schedule()
    ↓
schedule() → 从红黑树选vruntime最小的进程
    ↓
context_switch → 切换CR3(内存映射)+ 切换RSP(栈)+ 切换RIP(指令流)
    ↓
iret返回用户态 → 新进程开始执行

文件系统/网络与调度器的交互:进程执行read()陷入内核→数据未就绪→内核将进程状态置为TASK_INTERRUPTIBLE→调用schedule()切走CPU→硬件DMA完成后触发中断→中断处理程序将进程状态恢复为TASK_RUNNING→重新加入运行队列→下次调度时进程获得CPU→系统调用返回。

五、进程树:第一个进程的创建与进程/线程组织结构

5.1 第一个进程的创建

内核初始化最后阶段,手工执行以下操作:

  1. 分配task_struct结构体

  2. 手工设置初始寄存器值:RIP = /sbin/init入口地址,RSP = 用户态栈

  3. 分配用户态页表(mm_struct

  4. 加入运行队列

  5. 执行iret指令,CPU从内核态切到用户态,RIP指向/sbin/init

5.2 进程树结构

text

systemd (PID=1) ← 根节点,由内核手工创建
    ├── sshd
    │    └── bash
    │         ├── java (fork子进程)
    │         │    ├── [JVM线程1] ← 平级,共享内存,不是子进程
    │         │    ├── [JVM线程2]
    │         │    └── python (fork子进程,独立内存)
    │         └── python
    ├── getty
    └── crond

5.3 进程与线程的内核区别

进程(fork) 线程(clone)
内存 独立(复制页表,COW) 共享(CLONE_VM标志)
文件描述符 独立 共享(CLONE_FILES标志)
内核数据结构 独立task_struct + 独立mm_struct 独立task_struct + 共享mm_struct
进程关系 严格父子树状 平级兄弟团(通过tgid关联)

六、安全边界:内核态被中断为什么不崩溃

6.1 中断 vs 抢占(必须区分)

概念 定义 内核态是否允许
中断(Interrupt) 暂停当前指令流,响应硬件事件,回来继续 ✅ 允许
抢占(Preemption) 暂停当前进程,不再回来,切换到另一个进程 ❌ 临界区禁止

6.2 内核态被中断的物理事实

内核执行do_fork()时时钟中断发生:

  1. 内核正在复制task_struct(RIP指向内核代码)

  2. 时钟中断电信号到达CPU INTR引脚

  3. CPU暂停memcpy,查IDT跳转到timer_interrupt

  4. 中断处理程序更新jiffies++,置TIF_NEED_RESCHED标志

  5. 检查preempt_count > 0(禁止抢占标志)

  6. 中断处理程序不调用schedule(),直接执行iret

  7. CPU恢复被中断的memcpy继续执行

6.3 核心原则

  • 中断处理程序永远不主动调用schedule()

  • schedule()永远不嵌套schedule()

  • 临界区通过preempt_disable()禁止抢占

  • 内核态代码可以被打断,但一定在中断返回后继续执行原内核函数

七、文件读取:从read()到磁盘的完整数据路径

7.1 完整路径(11个步骤)

步骤 执行环境 物理动作
1 用户态 Java执行fileInputStream.read()
2 用户态 glibc包裹函数:系统调用号入rax,执行syscall
3 CPU硬件 查IA32_LSTAR MSR → 跳转到entry_SYSCALL_64
4 内核态 sys_read() → VFS层根据fd找到file对象
5 内核态 具体文件系统(ext4)计算逻辑块号
6 内核态 通用块层:组装BIO(块I/O请求)
7 内核态 I/O调度层:BIO合并与排序
8 内核态 块设备驱动:BIO转SCSI/NVMe命令
9 硬件 磁盘控制器通过DMA将数据读到内存缓冲区
10 硬件→内核 DMA完成 → 磁盘控制器发送中断 → 中断处理程序唤醒等待进程
11 内核态→用户态 数据从内核缓冲区拷贝到用户态缓冲区 → sysret返回

7.2 文件描述符本质

文件描述符是整数索引,指向进程files_structfdtable数组对应位置的file对象指针。

每个file对象包含:

  • f_inode:指向底层文件的inode节点

  • f_pos:当前文件读写偏移量

  • f_op:指向file_operations结构体(read/write/llseek等函数指针)

八、Socket原理:网络数据收发完整流程

8.1 Socket为什么是文件

socket()系统调用时内核执行:

  1. 分配socket结构体

  2. 分配file对象,file->f_op = &socket_file_ops

  3. 分配文件描述符,挂到进程fd表

8.2 发送数据完整路径

text

用户态socket.write() → syscall陷入内核
    ↓
内核根据fd找到socket结构体
    ↓
数据从用户态缓冲区拷贝到内核态sk_buff
    ↓
TCP层:封装TCP头(seq/ack/端口),计算校验和
    ↓
IP层:封装IP头(源IP/目标IP)
    ↓
链路层:封装以太网帧(MAC地址)
    ↓
网卡驱动:sk_buff加入发送环(Tx Ring,DMA共享内存)
    ↓
网卡通过DMA从内存搬运数据到物理层 → 电信号发出
    ↓
发送完成 → 网卡发送中断 → 释放sk_buff内存

8.3 接收数据完整路径

text

网线电信号 → 网卡硬件还原为数据帧
    ↓
网卡通过DMA将数据帧写入接收环(Rx Ring)
    ↓
DMA完成 → 网卡发送硬件中断(IRQ)
    ↓
硬中断处理程序:屏蔽网卡中断,标记DMA内存已更新
    ↓
软中断(ksoftirqd):
    ├── 剥开以太网帧头 → IP包
    ├── 剥开IP头 → TCP段
    └── TCP层:根据4元组(源IP/目标IP/源端口/目标端口)查找socket
    ↓
有效负载追加到socket接收缓冲区(Recv Queue)
    ↓
如果进程在recv()中阻塞 → 唤醒进程(TASK_RUNNING)
    ↓
进程获得CPU后,数据从内核缓冲区拷贝到用户态缓冲区
    ↓
recv()返回

8.4 I/O模型对比

I/O模型 系统调用行为 典型场景
阻塞I/O recv()不返回直到数据到达 简单客户端
非阻塞I/O recv()立即返回-EAGAIN 超时控制场景
I/O多路复用 select/poll/epoll监控多个fd Nginx、Netty

总结:操作系统完整执行循环与核心物理事实

完整三层循环结构

text

【启动阶段】一次性线性执行
通电 → BIOS → Bootloader → 内核初始化 → 手工创建PID=1 → iret跳转

【运行阶段】永恒的硬件驱动循环(1000次/秒)
时钟中断(硬件电信号)
    ↓
CPU查IDT跳转到中断处理程序(内核态)
    ↓
保存当前进程寄存器 → task_struct
    ↓
调度器决策(CFS选vruntime最小的进程)
    ↓
上下文切换:换CR3(页表)+ 换RSP(栈)+ 换RIP(指令流)
    ↓
iret返回用户态 → 新进程执行
    ↓
回到顶部,等待下一次时钟中断

【用户态请求阶段】系统调用触发,进程可能阻塞
用户态进程 → syscall → 内核执行服务(文件/网络/创建进程)
    ↓
数据未就绪?→ 进程阻塞(TASK_INTERRUPTIBLE) → 切走CPU
    ↓
硬件中断唤醒 → 进程恢复 → 系统调用返回

十个核心物理事实

# 物理事实
1 操作系统不是软件循环。空闲时CPU执行hlt指令,物理上暂停流水线,等待电信号
2 中断是唯一引擎。没有时钟芯片的INTR引脚电信号,调度器永远不会被执行
3 进程切换 = 换CR3(内存地图)+ 换RSP/RIP(执行流)
4 内核态被中断 ≠ 被抢占。中断可发生,但临界区禁止抢占
5 Socket就是文件。通过统一的文件描述符接口访问网络
6 内核态 vs 用户态 = CPU特权级(Ring 0 vs Ring 3)的切换
7 进程树是严格树状结构。根节点PID=1(systemd),fork()创建子节点
8 线程是平级兄弟团。通过clone(CLONE_VM)共享内存,tgid关联
9 文件读取路径:read()→VFS→ext4→块层→驱动→磁盘→中断→唤醒→返回
10 Socket收发路径:send()→TCP/IP协议栈→网卡驱动→DMA→网卡→中断确认

📢 如果本文对您有帮助,欢迎点赞、收藏、关注。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐