欢迎光临
我们一直在努力

从操作系统启动到文件句柄与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→网卡→中断确认

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

    赞(0)
    未经允许不得转载:171主机测评 » 从操作系统启动到文件句柄与Socket:完整系统运行原理深度剖析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址