1. OS基本概念

基本概念区分

  1. 内核(kernel)和用户(user):内核是操作系统的一部分,以较高的权限级别运行;而用户(空间)通常指的是那些以较低权限级别运行的应用程序。
  2. 用户模式(User Mode)和内核模式(Kernel Mode):指处理器执行模式的专业术语。在内核模式下,代码能够完全 [1] 控制 CPU,拥有最高权限;而用户模式下的代码则受到限制。例如,只有在内核模式下,才能启用或禁用 CPU 的本地中断;如果在用户模式下尝试这样的操作,则会触发异常,此时内核会介入处理。
  3. 用户空间(User Space)和内核空间(Kernel Space):专指与内存保护相关的概念,或者是与内核及用户应用程序相关联的虚拟地址空间。内核空间是专门为操作系统内核保留的内存区域,而用户空间则是分配给各个用户进程的内存区域。内核空间具有访问保护,用户应用程序无法直接访问这部分空间;相对地,用户空间则可以被运行在内核模式下的代码直接访问。

1.1 典型操作系统架构

操作系统内核负责安全且公平地管理多个应用程序对硬件资源的访问和共享。

1_1.jpg

系统调用: 内核为应用程序提供了一组API,这些 API 与普通的库 API 有所不同,它们标志着执行模式从用户态切换到内核态的界限。为了确保应用程序的兼容性,系统调用的变动非常少

内核代码可以逻辑上划分为核心内核代码和设备驱动程序代码。设备驱动程序负责操作特定的设备,而核心内核代码则是通用的。此外,核心内核代码还可以细分为多个逻辑子系统,如文件访问、网络和进程管理等。

1.单体内核
单体内核(也称为宏内核或巨内核)是一种内核设计,其中各个内核子系统之间的访问没有特别的保护措施,允许各个子系统互相直接调用公共函数。

2.微内核
其中大部分功能以受保护的方式相互作用,并通常作为用户空间中的服务来运行。因为内核的关键功能现在在用户模式下运行,导致在内核模式下运行的代码量大幅减少,微内核由此得名。
在微内核架构中,内核只包含最基本代码(允许不同运行进程间进行消息传递)。在实际应用中,这意味着内核仅实现调度程序和进程间通信(IPC)机制,以及基础内存管理,从而在应用程序和服务之间建立了保护层。
这种架构的优点之一是服务被隔离,因此某一个服务中的错误不会影响其他服务。

1.2 地址空间

“物理地址空间”指的是内存总线上可见的 RAM 和设备内存。例如,在 32 位的 Intel 架构中,通常会将 RAM 映射到较低的物理地址空间,而显卡内存则映射到较高的物理地址空间。
“虚拟地址空间”(有时简称为地址空间)是指启用虚拟内存模块时,CPU 所感知的内存布局(有时也称为保护模式或开启分页)。内核负责建立映射,创建虚拟地址空间,其中某些区域会映射到特定的物理内存区域。
“进程空间”是与单个进程相关联的虚拟地址空间的一部分,它构成了进程的“内存视图”,从零开始并连续延伸。进程地址空间的结束位置取决于具体实现和系统架构。
“内核空间”是运行在内核模式下代码的内存视图。

非对称多处理(ASMP): 一种内核支持多处理器(核心)的模式。在这种模式下,有一个处理器被专门分配给内核,而其他处理器则负责运行用户空间的程序。
这种方法的一个缺点是,内核的吞吐量(如系统调用和中断处理等)并不会随着处理器数量的增加而线性扩展,尽管典型的进程频繁地进行系统调用。因此,这种方法主要局限于特定类型的系统,如科学计算应用。

对称多处理(SMP): 与 ASMP 相比,在 SMP 模式下,内核能够在任何可用的处理器上运行,这与用户进程相似。这种方法实现起来更为复杂,因为如果两个进程同时运行并访问相同内存位置的内核函数,就会在内核中引发竞态条件。
为了实现 SMP 支持,内核必须采用同步机制(例如自旋锁)来确保在任何时刻只有一个处理器进入临界区。

1.2 系统调用

概念: 系统调用是内核向用户应用程序提供的“服务”,它们类似于库 API,因为它们被描述为具有名称、参数和返回值的函数调用。

+-------------+           +-------------+
|   应用程序   |           |   应用程序   |
+-------------+           +-------------+
  |                           |
  |read(fd, buff, len)        |fork()
  |                           |
  v                           v
+---------------------------------------+
|                 内核                  |
+---------------------------------------+

然而,从底层视角看的话,我们会发现系统调用实际上并不是函数调用,而是特定的汇编指令
在 Linux 中,系统调用使用数字进行标识,系统调用的参数为机器字大小(32 位或 64 位)。最多可以有 6 个系统调用参数。系统调用编号和参数都存储在特定的寄存器中

2. 内存寻址

首先区分三个概念
逻辑地址: 机器语言指令中用来指定一个操作数火一条指令的地址。跟分段机制有关,一个逻辑地址由一个段和偏移量构成,但在linux中,分段很少用,因此可以等同于虚拟地址。
虚拟地址: 无符号整数,程序员在代码中用取址符号看到的地址
物理地址: 内存芯片级内存单元,是底层处理器用到的真实地址

内存控制单元(MMU)分段单元的硬件电路把逻辑地址转换为虚拟地址;分页单元的硬件电路把虚拟地址转换为物理地址
如下是X86中一个虚拟地址转换成真实物理地址的示例:

#include <iostream>
#include <unistd.h>     // getpagesize()
#include <fcntl.h>      // open()
#include <cstdint>      // uint64_t
#include <sys/types.h>
#include <sys/stat.h>

using namespace std;

/*
    函数作用:
    给定一个虚拟地址,查询它对应的物理地址。

    原理:
    1. 通过 /proc/self/pagemap 读取页表信息
    2. 找到该虚拟页对应的物理页框号 (PFN)
    3. 计算物理地址 = 页框号 * 页大小 + 页内偏移
*/
uint64_t get_physical_address(uint64_t virtual_addr)
{
    // -------------------------------
    // 第 1 步:获取系统页大小
    // -------------------------------
    // Linux 一般是 4096 字节 (4KB)
    uint64_t page_size = getpagesize();
    cout<<page_size<<endl; //显示的是16进制“1000”,也就是十进制的4096
    // -------------------------------
    // 第 2 步:计算页内偏移
    // -------------------------------
    // 虚拟地址的低 12 位就是页内偏移
    // 例如:4096 = 2^12
    uint64_t page_offset = virtual_addr % page_size;

    // -------------------------------
    // 第 3 步:计算虚拟页号
    // -------------------------------
    // 虚拟地址 / 页大小 = 第几个虚拟页
    uint64_t page_number = virtual_addr / page_size;

    // -------------------------------
    // 第 4 步:打开 pagemap 文件
    // -------------------------------
    // /proc/self/pagemap 保存当前进程的页表信息
    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0)
    {
        perror("open pagemap failed");
        return 0;
    }

    // -------------------------------
    // 第 5 步:计算在 pagemap 中的偏移量
    // -------------------------------
    // pagemap 中:
    // 每个虚拟页对应一个 64 位(8 字节)的条目
    //
    // 所以:
    // 第 N 个虚拟页 对应文件偏移:
    // N * 8 字节
    off_t offset = page_number * sizeof(uint64_t);

    // -------------------------------
    // 第 6 步:移动文件指针
    // -------------------------------
    if (lseek(fd, offset, SEEK_SET) == (off_t)-1)
    {
        perror("lseek failed");
        close(fd);
        return 0;
    }

    // -------------------------------
    // 第 7 步:读取页表项
    // -------------------------------
    uint64_t entry;
    if (read(fd, &entry, sizeof(uint64_t)) != sizeof(uint64_t))
    {
        perror("read failed");
        close(fd);
        return 0;
    }

    close(fd);

    // -------------------------------
    // 第 8 步:检查页面是否存在
    // -------------------------------
    // pagemap 第 63 位是 Present 位
    // 如果为 0,说明这个页还没有映射到物理内存
    if (!(entry & (1ULL << 63)))
    {
        cout << "Page not present in memory!" << endl;
        return 0;
    }

    // -------------------------------
    // 第 9 步:提取物理页框号 (PFN)
    // -------------------------------
    // pagemap 低 55 位是 PFN
    // 用掩码提取出来
    uint64_t pfn = entry & ((1ULL << 55) - 1);

    // -------------------------------
    // 第 10 步:计算物理地址
    // -------------------------------
    // 物理地址 = 页框号 * 页大小 + 页内偏移
    uint64_t physical_addr = (pfn * page_size) + page_offset;

    return physical_addr;
}


int main()
{
    cout << "===== 虚拟地址 -> 物理地址 演示 =====" << endl;

    int x = 123;

    // 打印变量的虚拟地址(逻辑地址≈虚拟地址)
    uint64_t virtual_addr = reinterpret_cast<uint64_t>(&x);

    cout << "变量值: " << x << endl;
    cout << "虚拟地址: 0x" << hex << virtual_addr << endl;

    // 查询物理地址
    uint64_t physical_addr = get_physical_address(virtual_addr);

    if (physical_addr != 0)
    {
        cout << "物理地址: 0x" << hex << physical_addr << endl;
    }
    else
    {
        cout << "无法获取物理地址 (需要 root 权限)" << endl;
    }

    return 0;
}

2.1 逻辑地址

段选择符和段寄存器
逻辑地址由一个段标识符(16位)和一个指定段内相对地址的偏移量(32位)构成;
为了快速找到段选择符,处理器提供段寄存器,用于存放段选择符:cs,ss,ds,es,fs和gs。
其中cs是代码段寄存器,指向包含程序指令的段。除此之外,含有一个两位的字段,表示CPU的特权级(CPL),0表示最高级,3表示最低优先级。linux只有0和3,分别表示内核态和用户态

段描述符
每个段由一个8位的段描述符表示,表示段的特征。通常放在全局描述符表(GDT)中,或者局部描述符中(LDT)。
除了GDT和LDT外,处理器提供一种附加的非编程寄存器(不能被程序员编辑)。每当一个段选择符被装入段寄存器中,相应的段描述符就由内存装入到对应的非编程CPU寄存器。针对该段的逻辑地址转换就可以不访问主存中的GDT,处理器直接引用存放段描述符的CPU寄存器即可。仅当段寄存器的内容改变时,才访问GDT/LDT。