# 架构

# 系统架构

  • 一个 Linux 计算机包含大量软件、硬件。可以从上到下分为三层:

    • User Space
      • :用户空间,用于运行除内核以外的所有软件。
      • 在用户空间运行的软件,其进程默认处于用户态(user mode)。但该进程可以向下调用系统接口,执行内核代码,此时该进程变为内核态(kernel mode)。
      • 例如执行 glibc 的库函数时,进程处于用户态。当库函数调用系统接口时,进程变为内核态。
    • Kernel Space
      • :内核空间,用于运行内核。
      • 内核是操作系统中的核心软件。职责如下:
        • 向上,提供一组系统调用接口(System Call Interface,SCI),供用户态进程调用。
        • 向下,控制硬件层。例如 CPU 调度、内存分配、磁盘读写。
    • Hardware Layer
      • :计算机的硬件层,包括 CPU、内存、磁盘等硬件设备。
  • Linux 中,每个进程都运行在一个独立的虚拟内存空间中,并将该空间分成两部分:

    • 少部分空间为内核空间,用于存储内核的代码、数据。
    • 大部分空间为用户空间。
  • 内核包含多个功能模块,包括但不限于:

    • SCI
    • Memory Management
    • Process Management
    • IPC(Inter-process Communication)
    • VFS(Virtual File System):一个抽象的文件系统层。向下管理不同类型的文件系统,向上提供统一的文件系统接口。
    • General Block Device Layer :一个抽象的块设备层。向下管理不同类型的硬件设备,向上提供统一的 Block IO 接口。
    • Device Driver :设备驱动程序,有很多种,用于管理各种硬件设备。

# 内核态进程

  • x86 架构的 CPU 设计了 0~3 四种权限。而 Linux 只使用了最高权限 ring 0 和最低权限 ring 3 。

  • 对比用户态进程、内核态进程:

    • CPU 执行内核态进程时,处于最高权限 ring 0 ,因此:
      • 有权直接控制计算机硬件。
      • 有权访问所有内存空间。
    • CPU 执行用户态进程时,处于最低权限 ring 3 ,因此:
      • 无权直接控制计算机硬件。需要调用系统接口,才能间接控制计算机硬件。
      • 只有权访问当前进程的用户内存空间,不能访问其它进程的用户内存空间,不能访问内核内存空间。
  • 进程从用户态切换到内核态的过程:

    1. CPU 进行上下文切换,原本执行用户空间的代码,改为执行内核空间的代码。
    2. 等内核代码执行完毕,CPU 再次上下文切换,继续执行用户空间的代码。
  • 进程从用户态切换到内核态的几种方式:

    • 进程主动调用系统接口,触发一次软中断
    • CPU 遇到异常
      • 例如遇到缺页异常时,CPU 会执行内核中的缺页异常处理程序。
    • CPU 遇到硬中断

# 内核线程

  • Linux 开机时,kthreadd 进程会创建一些内核线程(kernel thread),用于执行一些内核任务。

    • 内核线程是一种特殊的进程。每个内核线程拥有一个不同的 PID ,父进程为 kthreadd 。
  • 对比普通线程、内核线程:

    • 用户可以调用 glibc 库函数,创建普通线程。而内核线程只能由 kthreadd 进程创建。
    • 普通线程的祖先是 init 进程,PID 为 1 。而内核线程的祖先是 kthreadd 进程,PID 为 2 。所有线程的最初祖先都是 idle 进程,PID 为 0 。
    • 普通线程默认处于用户态,可以切换到内核态。而内核线程仅处于内核态,没有虚拟内存空间。
    • 普通线程会共用所属进程的 PID 。而每个内核线程会分配一个唯一的 PID 。
  • 内核线程举例:

    • kthreadd
      • 系统开机时创建的第三个进程,PID 为 2 ,负责创建、管理所有内核线程。
    • kworker
      • 负责并行执行内核 Workqueue 中的任务,比如定时器、中断、IO 。
      • 在 CPU 每个核上运行一个实例。
    • migration
      • 负责在多个 CPU 核之间迁移运行中的进程,从而避免单核故障、实现负载均衡。
      • 在 CPU 每个核上运行一个实例。
    • watchdog
      • 负责避免系统死机。进程启动之后会打开 /dev/watchdog 文件,不断对它进行写操作。如果在一定时间内(默认为 1 分钟)没有进行写操作,就会通过硬件或软件重启系统。
      • 在 CPU 每个核上运行一个实例。
    • kauditd
      • 负责记录各种系统调用、文件访问的审计日志,保存到 /var/log/audit/* 。
    • kblockd
      • 负责管理块设备。
      • 在系统中只运行一个实例。
    • kswapd0
      • 负责管理 Swap 分区。

# eBPF

Linux 的内存分为内核空间、用户空间两部分,前者只允许内核态进程使用。想在内核空间执行自定义程序时,有几种方案:

  • LKM(Linux Kernel Module,Linux 内核模块)
    • 原理:用户用 C 语言开发一个 Linux 内核模块,编译成扩展名为 .ko 的文件,然后用 insmod 命令加载到内核中。
      • 执行命令 lsmod 查看已加载的所有内核模块。
    • 优点:可以在 Linux 内核运行时,加载新模块,而无需重新编译内核。
    • 缺点:模块运行出错时,可能导致 Linux 内核崩溃。
  • eBPF(extended Berkeley Packet Filter,扩展伯克利包过滤器)
    • 2014 年,Linux kernel 3.15 添加了 eBPF 功能。
    • 原理:用户用 C 语言开发一个 eBPF 程序,用 BCC(BPF编译器集合)编译成字节码文件,然后加载到内核中,采用 JIT 技术即时编译成机器码。
      • 当 Linux 内核发生系统调用、网络包等 event 时,会通过 hook 机制自动执行 eBPF 程序。
      • 执行命令 bpftool prog list 查看已加载的所有 eBPF 程序。
    • 常见用途:
      • 用于网络包过滤,比用户态进程的速度更快、吞吐量更高。
      • 用于监控,在内核中收集、生成监控指标。
      • 用于 debug ,跟踪某些程序的运行。
    • 优点:
      • 允许内核执行自定义代码,而无需重新编译内核,或加载新模块。
      • 加载 eBPF 程序到内核时,会进行多种检查,比如确保该程序没有死循环,不会导致内核崩溃。