从操作系统启动到文件句柄与Socket:完整系统运行原理深度剖析
写在开始:本文回答的五个核心问题
-
操作系统最小自洽内核需要哪些功能?没有它们为什么无法运转?
-
调度模块和系统模块如何协同工作?中断在其中扮演什么角色?
-
用户态读文件和读Socket的完整路径是什么?区别在哪里?
-
内核态或调度执行期间被中断会怎样?系统为什么不会崩溃?
-
第一个进程何时创建?进程树和线程组在内核中如何组织?
作者介绍
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指令时发生的物理事实:
-
CPU自动将当前RIP存入RCX,将RFLAGS存入R11
-
CPU从IA32_LSTAR MSR寄存器(内核启动时预设)读取入口地址,加载到RIP
-
CPU将特权级从Ring 3切换到Ring 0
-
跳转到
entry_SYSCALL_64内核入口 -
内核根据
rax寄存器中的系统调用号查sys_call_table,执行对应函数 -
执行
sysret指令,恢复RIP/RFLAGS,特权级切回Ring 3
四、调度核心:调度模块与系统模块如何协同
4.1 调度器两种触发方式
| 触发方式 | 触发源 | 频率/时机 | 执行动作 |
|---|---|---|---|
| 周期性调度 | 时钟中断→timer_interrupt→scheduler_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 第一个进程的创建
内核初始化最后阶段,手工执行以下操作:
-
分配
task_struct结构体 -
手工设置初始寄存器值:RIP =
/sbin/init入口地址,RSP = 用户态栈 -
分配用户态页表(
mm_struct) -
加入运行队列
-
执行
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()时时钟中断发生:
-
内核正在复制
task_struct(RIP指向内核代码) -
时钟中断电信号到达CPU INTR引脚
-
CPU暂停
memcpy,查IDT跳转到timer_interrupt -
中断处理程序更新
jiffies++,置TIF_NEED_RESCHED标志 -
检查
preempt_count > 0(禁止抢占标志) -
中断处理程序不调用
schedule(),直接执行iret -
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_struct中fdtable数组对应位置的file对象指针。
每个file对象包含:
-
f_inode:指向底层文件的inode节点 -
f_pos:当前文件读写偏移量 -
f_op:指向file_operations结构体(read/write/llseek等函数指针)
八、Socket原理:网络数据收发完整流程
8.1 Socket为什么是文件
socket()系统调用时内核执行:
-
分配socket结构体
-
分配
file对象,file->f_op = &socket_file_ops -
分配文件描述符,挂到进程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→网卡→中断确认 |
📢 如果本文对您有帮助,欢迎点赞、收藏、关注。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)