用户空间和内核空间有什么区别?

用户空间和内核空间有什么区别?

当内核代表用户程序(即系统调用)执行时,是否使用内核空间?或者它是所有内核线程(例如调度程序)的地址空间?

如果是第一个,那么是否意味着普通用户程序不能拥有超过3GB的内存(如果划分为3GB + 1GB)?另外,在这种情况下,内核如何使用高内存,因为高内存中的页面将映射到哪个虚拟内存地址,因为 1GB 的内核空间将被逻辑映射?

答案1

当内核代表用户程序(即系统调用)执行时,是否使用内核空间?或者它是所有内核线程(例如调度程序)的地址空间?

是的,是的。

在我们进一步讨论之前,我们应该先说明一下关于内存的问题。

内存分为两个不同的区域:

  • 用户空间,这是正常用户进程运行的一组位置(即除内核之外的所有位置)。内核的作用是管理在此空间中运行的应用程序,以免相互干扰以及与机器干扰。
  • 内核空间,这是内核的代码和数据存储的位置,并在其下执行。

运行在用户空间下的进程只能访问内存的有限部分,而内核可以访问所有内存。进程也运行在用户空间可以访问内核空间。用户空间进程可以只访问内核的一小部分通过内核公开的接口 -系统调用。如果进程执行系统调用,则会将软件中断发送到内核,然后内核调度适当的中断处理程序,并在处理程序完成后继续其工作。

内核空间代码具有在“内核模式”下运行的属性,这(在典型的桌面 -x86- 计算机中)是您所需要的调用在ring 0下执行的代码通常在x86架构中,有4个保护环。 Ring 0(内核模式)、Ring 1(可能由虚拟机管理程序或驱动程序使用)、Ring 2(可能由驱动程序使用,但我对此不太确定)。 Ring 3 是典型应用程序运行的环境。它是权限最小的环,在其上运行的应用程序可以访问处理器指令的子集。环 0(内核空间)是特权最高的环,可以访问所有机器指令。举个例子,“普通”应用程序(如浏览器)不能使用 x86 汇编指令lgdt来加载全局描述符表,也hlt不能停止处理器。

如果是第一个,那么是否意味着普通用户程序不能拥有超过3GB的内存(如果划分为3GB + 1GB)?另外,在这种情况下,内核如何使用高内存,因为高内存中的页面将映射到哪个虚拟内存地址,因为 1GB 的内核空间将被逻辑映射?

对于这个问题的答案,请参考wag 的出色回答Linux 上的高内存和低内存是什么?

答案2

CPU环是最明显的区别

在 x86 保护模式下,CPU 始终处于 4 个环之一。 Linux内核只使用0和3:

  • 0 表示内核
  • 3 对于用户

这是内核与用户态的最严格和最快速的定义。

为什么 Linux 不使用环 1 和环 2:https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used

当前环是如何确定的?

当前环是通过以下组合选择的:

  • 全局描述符表:GDT条目的内存表,每个条目都有一个对Privl环进行编码的字段。

    LGDT指令将地址设置为当前描述符表。

    也可以看看:http://wiki.osdev.org/Global_Descriptor_Table

  • 段寄存器CS、DS等,它们指向GDT中某个条目的索引。

    例如,CS = 0表示 GDT 的第一个条目当前对于执行代码处于活动状态。

每个戒指能做什么?

CPU 芯片的物理构建使得:

  • 环0可以做任何事情

  • 环 3 无法运行多个指令并写入多个寄存器,最值得注意的是:

    • 无法改变自己的戒指!否则,它可以将自己设置为ring 0,而rings将毫无用处。

      换句话说,无法修改当前的段描述符,它决定了当前的环。

    • 无法修改页表:https://stackoverflow.com/questions/18431261/how-does-x86-paging-work

      换句话说,不能修改CR3寄存器,并且分页本身阻止了页表的修改。

      出于安全性/易于编程的原因,这可以防止一个进程看到其他进程的内存。

    • 无法注册中断处理程序。这些是通过写入内存位置来配置的,这也可以通过分页来防止。

      处理程序在环 0 中运行,并且会破坏安全模型。

      换句话说,不能使用LGDT和LIDT指令。

    • in不能执行像和 之类的 IO 指令out,因此可以进行任意硬件访问。

      否则,例如,如果任何程序可以直接从磁盘读取,则文件权限将毫无用处。

      更准确地说,感谢迈克尔·佩奇:操作系统实际上可以允许环 3 上的 IO 指令,这实际上是由任务状态段

      如果环 3 一开始没有获得许可,那么环 3 就不可能授予自己这样做的权限。

      Linux 总是不允许它。也可以看看:https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss

程序和操作系统如何在环之间转换?

  • 当CPU打开时,它开始运行环0中的初始程序(很好,但这是一个很好的近似值)。你可以认为这个初始程序是内核(但通常它是然后调用仍在环 0 中的内核的引导加载程序)。

  • 当用户态进程希望内核为其执行某些操作(例如写入文件)时,它会使用生成中断的指令,例如int 0x80或者syscall向内核发出信号。 x86-64 Linux 系统调用 hello world 示例:

    .data
    hello_world:
        .ascii "hello world\n"
        hello_world_len = . - hello_world
    .text
    .global _start
    _start:
        /* write */
        mov $1, %rax
        mov $1, %rdi
        mov $hello_world, %rsi
        mov $hello_world_len, %rdx
        syscall
    
        /* exit */
        mov $60, %rax
        mov $0, %rdi
        syscall
    

    编译并运行:

    as -o hello_world.o hello_world.S
    ld -o hello_world.out hello_world.o
    ./hello_world.out
    

    GitHub 上游

    发生这种情况时,CPU 会调用内核在启动时注册的中断回调处理程序。这里有一个注册处理程序并使用它的具体裸机示例

    该处理程序在环 0 中运行,它决定内核是否允许此操作、执行该操作并在环 3 中重新启动用户态程序。x86_64

  • 当。。。的时候exec使用系统调用时(或者当内核将开始/init), 内核准备寄存器和内存新的用户态进程,然后跳转到入口点并将CPU切换到环3

  • 如果程序尝试做一些顽皮的事情,例如写入禁止的寄存器或内存地址(由于分页),CPU 还会调用环 0 中的某些内核回调处理程序。

    但由于用户态很顽皮,内核这次可能会杀死该进程,或者用信号给它一个警告。

  • 当内核启动时,它会设置一个具有固定频率的硬件时钟,该时钟会定期生成中断。

    该硬件时钟生成运行环 0 的中断,并允许它调度要唤醒的用户态进程。

    这样,即使进程没有进行任何系统调用,也可以进行调度。

拥有多个戒指有什么意义?

分离内核和用户态有两个主要优点:

  • 制作程序会更容易,因为您更确定一个程序不会干扰另一个程序。例如,一个用户态进程不必担心由于分页而覆盖另一程序的内存,也不必担心将硬件置于另一进程的无效状态。
  • 它更安全。例如,文件权限和内存分离可以防止黑客应用程序读取您的银行数据。当然,这假设您信任内核。

如何玩转它?

我创建了一个裸机设置,这应该是直接操作环的好方法:https://github.com/cirosantilli/x86-bare-metal-examples

不幸的是,我没有耐心制作用户态示例,但我确实进行了分页设置,因此用户态应该是可行的。我很想看到拉取请求。

或者,Linux 内核模块在环 0 中运行,因此您可以使用它们来尝试特权操作,例如读取控制寄存器:https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers-cr0-cr2-cr3-from-a-program-getting-segmenta/7419306#7419306

这里有一个方便的 QEMU + Buildroot 设置尝试一下而不杀死你的主机。

内核模块的缺点是其他 kthread 正在运行,可能会干扰您的实验。但理论上你可以用你的内核模块接管所有中断处理程序并拥有系统,这实际上是一个有趣的项目。

负环

虽然英特尔手册中实际上并未引用负环,但实际上有些 CPU 模式比环 0 本身具有更多功能,因此非常适合“负环”名称。

一个例子是虚拟化中使用的管理程序模式。

欲了解更多详情,请参阅:

手臂

在 ARM 中,环被称为异常级别,但主要思想保持不变。

ARMv8中存在4个异常级别,常用为:

  • EL0:用户区

  • EL1:内核(ARM 术语中的“管理程序”)。

    使用指令(SuperVisor Call)输入svc,以前称为swi 统一组装前,这是用于进行 Linux 系统调用的指令。你好世界 ARMv8 示例:

    你好.S

    .text
    .global _start
    _start:
        /* write */
        mov x0, 1
        ldr x1, =msg
        ldr x2, =len
        mov x8, 64
        svc 0
    
        /* exit */
        mov x0, 0
        mov x8, 93
        svc 0
    msg:
        .ascii "hello syscall v8\n"
    len = . - msg
    

    GitHub 上游

    在 Ubuntu 16.04 上使用 QEMU 进行测试:

    sudo apt-get install qemu-user gcc-arm-linux-gnueabihf
    arm-linux-gnueabihf-as -o hello.o hello.S
    arm-linux-gnueabihf-ld -o hello hello.o
    qemu-arm hello
    

    这是一个具体的裸机示例注册 SVC 处理程序并执行 SVC 调用

  • EL2:管理程序, 例如Xen

    使用指令(HyperVisor 调用)输入hvc

    虚拟机管理程序对于操作系统来说,就像操作系统对于用户空间一样。

    例如,Xen 允许您同时在同一系统上运行多个操作系统,例如 Linux 或 Windows,并且它将操作系统彼此隔离以确保安全性和易于调试,就像 Linux 对用户态程序所做的那样。

    虚拟机管理程序是当今云基础设施的关键部分:它们允许多个服务器在单个硬件上运行,使硬件使用率始终接近 100%,并节省大量资金。

    例如,AWS 在 2017 年之前一直使用 Xen迁移到 KVM 成为新闻

  • EL3:另一个级别。待办事项示例。

    使用指令输入smc(安全模式呼叫)

ARMv8 架构参考模型 DDI 0487C.a- 第 D1 章 - AArch64 系统级程序员模型 - 图 D1-1 精美地说明了这一点:

在此输入图像描述

随着 ARM 的出现,情况发生了一些变化ARMv8.1 虚拟化主机扩展 (VHE)。这个扩展允许内核在 EL2 中高效运行:

在此输入图像描述

VHE 的创建是因为 Linux 内核虚拟化解决方案(例如 KVM)已经超过了 Xen(参见上面提到的 AWS 转向 KVM),因为大多数客户端只需要 Linux VM,并且正如您可以想象的那样,所有这些都在一个单一的虚拟机中项目中,KVM 比 Xen 更简单并且可能更高效。因此,现在主机 Linux 内核在这些情况下充当虚拟机管理程序。

请注意,也许是出于事后诸葛亮的考虑,ARM 对权限级别的命名约定比 x86 更好,而不需要负级别:0 表示较低,3 表示最高。较高级别往往比较低级别更容易创建。

可以通过指令查询当前的EL MRShttps://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc

ARM 不要求所有异常级别都存在,以允许不需要该功能来节省芯片面积的实现。 ARMv8“异常级别”说:

实现可能不包括所有异常级别。所有实现都必须包括 EL0 和 EL1。 EL2和EL3是可选的。

例如,QEMU 默认为 EL1,但可以使用命令行选项启用 EL2 和 EL3:https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulate-a53-power-up

代码片段在 Ubuntu 18.10 上测试。

答案3

如果是第一个,那么是否意味着普通用户程序不能拥有超过3GB的内存(如果划分为3GB + 1GB)?

是的,在普通的 Linux 系统上就是这种情况。有一组“4G/4G”补丁在某个点上浮动,使用户和内核地址空间完全独立(以性能为代价,因为它使内核更难访问用户内存),但我不认为它们曾被上游合并,随着 x86-64 的兴起,人们的兴趣逐渐减弱

另外,在这种情况下,内核如何使用高内存,因为高内存中的页面将映射到哪个虚拟内存地址,因为 1GB 的内核空间将被逻辑映射?

Linux过去的工作方式(并且在内存与地址空间相比较小的系统上仍然如此)是整个物理内存被永久映射到地址空间的内核部分。这允许内核无需重新映射即可访问所有物理内存,但显然它无法扩展到具有大量物理内存的 32 位计算机。

于是低内存和高内存的概念就诞生了。 “低”内存被永久映射到内核地址空间。 “高”记忆则不然。

当处理器运行系统调用时,它在内核模式下运行,但仍在当前进程的上下文中。因此它可以直接访问当前进程的内核地址空间和用户地址空间(假设您没有使用前面提到的4G/4G补丁)。这意味着将“高”内存分配给用户态进程是没有问题的。

将“高”内存用于内核目的更是一个问题。要访问未映射到当前进程的高端内存,必须将其临时映射到内核的地址空间。这意味着额外的代码和性能损失。

相关内容