操作系统的内核(Kernel)和中央处理器(CPU,Central Processing Unit)区别:

  1. 操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件。
  2. CPU 主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作。

下图清晰说明了应用程序、内核、CPU 这三者的关系。

Kernel_Layout


用户态和内核态

  • 用户态:
    • 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限,当需要执行某些特殊权限的时候,例如读写磁盘,网络通信,就需要向操作系统申请资源权限。
  • 内核态:
    • 内核态的权限很大,几乎可以完成对系统的所有操作
    • 操作系统得到指令后,会从用户态转移到内核态,执行对磁盘的操作后将结果返回进程,后转回用户态
    • 虽然拥有更高的权限,但是由于进入内核态时候的上下游调用,开销花费较大,所以要尽量减少进入内核态的次数

用户态: 应用程序,库函数
内核态: 进程管理,网络管理,数据管理,内核管理, 设备管理

为什么要有用户态,只要一个内核态不行吗?

答:

  • 内核态:CPU 拥有最高权限,可以访问所有硬件资源(内存、磁盘、网络、IO设备)。操作系统内核在这个模式下运行。
  • 用户态:权限受限,只能运行用户程序的指令,不能直接操作硬件,必须通过系统调用进入内核态。

    (1) 安全性

  • 如果没有用户态,所有程序都在内核态运行 → 意味着任何一个普通应用都能直接操作磁盘、内存、网络、甚至关机/重启。
  • 一旦一个程序写错了代码或带有恶意行为,它就能直接破坏系统。
  • 用户态限制了应用程序的权限,避免了用户程序影响系统稳定性

👉 类比:

  • 内核态 = 大楼的管理员,有钥匙可以进所有房间、控制电源。
  • 用户态 = 普通住户,只能进自己房间,不能随便动配电箱。

    (2) 稳定性

  • 应用程序可能会崩溃,但操作系统必须保证稳定。
  • 如果所有程序都在内核态运行,一个用户程序崩溃可能导致整个系统蓝屏。
  • 用户态 + 内核态的隔离让 “程序挂了 ≠ 系统挂了”

    (3) 多用户/多任务管理

  • 现代操作系统是多用户、多任务的。
  • 内核态负责:
    • 进程/线程调度
    • 内存保护
    • 文件系统管理
    • 网络与外设访问
  • 用户程序必须通过 系统调用(syscall) 请求内核服务 → 这样操作系统才能管理资源分配,保证不同用户之间互不干扰

3. 如果只有内核态会怎样?

  • 缺点
    • 安全隐患巨大(任何应用都有最高权限)。
    • 容错性差(一个程序出 bug,全系统崩溃)。
    • 难以支持多用户、多任务的隔离。
  • 优点
    • 切换开销小(因为没有用户态和内核态的切换)。
    • 性能可能更高。

👉 实际上,早期的单任务操作系统(DOS、CP/M) 就基本只有“内核态”,应用程序能直接访问硬件。
但这在多用户、多任务和联网时代是无法接受的。


用户态和内核态是怎么切换的?

系统调用(System Call)

  • 最常见方式
  • 用户程序无法直接访问硬件,需要通过内核提供的“服务接口”。
  • 用户程序执行一条特殊指令(如 int 0x80syscallsysenter),触发软中断/陷入内核,CPU 切换到内核态。
  • 内核完成操作后,再返回用户态。

👉 举例:

1
write(1, "hello", 5);  // 用户态调用

底层会触发系统调用号 → 内核进入内核态 → 写入文件描述符 → 返回结果。

异常(Exception / Trap)

  • 用户态执行过程中出现错误/特殊情况,由 CPU 硬件触发异常,自动进入内核态的异常处理程序。
  • 常见异常:
    • 除零错误(Divide by zero)
    • 缺页异常(Page Fault,用于虚拟内存机制)
    • 非法指令(Illegal Instruction)
  • 操作系统在内核态捕获并处理这些异常,比如缺页异常时分配物理页,然后再恢复用户态执行

👉 举例:
访问一个未在内存中的虚拟地址 → 触发缺页异常 → 内核处理 → 返回用户态。

外部中断(Hardware Interrupt)

  • 外部设备向 CPU 发中断信号时,CPU 暂停当前用户态执行,切换到内核态中断服务程序。
  • 常见外设中断:
    • 键盘输入
    • 网络数据到达
    • 硬盘读写完成
  • 内核态中断处理完毕后,再恢复到用户态继续执行。

👉 举例:
你在打字时 → 键盘发出中断 → CPU 切换到内核态,执行键盘中断处理程序 → 把输入放入缓冲区 → 返回用户态。

总结举例:

  • 系统调用是程序主动请求,就像是 ”我打电话找警察“
  • 异常是程序被动请求,就像是“ 我犯罪了警察打电话找我 ”
  • 中断是外部硬件设备打断CPU,就像是“ 有人按门铃,事件是警察来了(外部事件)”

系统调用的过程

  1. 用户态调用

比如用户程序调用:
write(1, "hello\n", 6);
这是一个 库函数调用(glibc 的 write()),但本质上它需要内核帮忙往文件描述符 1(标准输出)写数据。

  1. C 库封装 → 触发陷入指令
  • glibc 中的 write() 会准备好系统调用号和参数(比如 fd=1,buf=”hello”,len=6)。
  • 然后执行一条特殊的 CPU 指令,使 CPU 从 用户态切换到内核态
    • 早期 Linux 用 int 0x80(触发软件中断)。
    • 新的 CPU 用更高效的 syscallsysenter 指令。

👉 这一步叫 陷入(trap)

  1. 内核态入口(系统调用处理器)
  • CPU 自动切换到 内核态,并跳到 系统调用入口(内核预先注册好的中断向量表/IDT)。
  • 内核从寄存器里取出系统调用号(比如 SYS_write),找到对应的内核函数。

    1. 内核处理
  • 内核执行对应的系统调用实现函数(比如 sys_write)。

  • 在这个过程中,内核可以直接操作硬件驱动(比如把字符串写到终端设备)。
  • 这里的操作都是内核态代码,有最高权限。

    1. 返回用户态
  • 内核处理完后,把结果(比如写了多少字节)放到寄存器里作为返回值。

  • 然后通过 iretsysret 指令返回到用户态,继续执行用户程序。

    流程总结(简化版)

用户态调用 → C库封装 → 触发陷入指令 → CPU切换到内核态 → 内核查系统调用号 → 执行内核服务 → 返回结果 → 回到用户态


图示类比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[用户态]
应用程序 (write)
|
v
C库封装 (glibc)
|
v
特殊指令 (syscall/int 0x80)
|
------------------- CPU 切换 -------------------
[内核态]
系统调用入口
|
系统调用分发 (根据号找到 sys_write)
|
内核函数执行 (写文件/设备)
|
v
返回用户态 (sysret)