Linux内核如何"listen"到C库?
How does the Linux kernel "listen" to the C library?
我正在尝试构建一个 "big picture" 来了解 Linux 内核和用户 space 的工作原理,但我很困惑。
我知道 userspace 使用系统调用 "talk" 到内核,但我不知道怎么做。我试图阅读 C 库和内核源代码,但它们很复杂且不易理解。我还阅读了几本有关操作系统概念性事实的书籍,例如管理进程、内存、设备,但它们并没有使 "transition" (userspace->kernel) 清晰。
那么,用户 space 和内核 space 之间的转换究竟发生在哪里? C 库如何 运行 Linux 内核中的代码 运行 宁在机器中?
打个比方:想象有一所房子。房子是锁着的。打开房子的钥匙就在房子里面。屋子里只有一个人,内核。用户space 是试图进入房屋的人。我的问题是:内核如何知道房子外面有人想要钥匙,以及哪种机制允许使用该钥匙打开房子?
许多处理器都有调用特定"trap"或"interrupt"的指令,Linux内核专门设置了这样的"trap"或"interrupt"系统调用。
库以某种方式设置处理器寄存器,然后执行特殊的陷阱或中断指令,使处理器进入特权模式并调用内核的 trap/interrupt 处理函数,该函数解码值在寄存器中并调用适当的函数来处理系统调用。
这是最常见的方式,基本上所有需要在内核和用户之间进行隔离的系统都是这样做的-space。
这很简单 - 这个人可以使用门铃让内核知道它在外面等着。在我们的例子中,这个门铃通常是一个特殊的 CPU 异常、软件中断或专用指令,允许用户 space 应用程序使用并且内核可以处理。
所以程序是这样的:
首先你需要知道系统调用号。每个系统调用都有其唯一的编号,内核内部有一个 table 将这些编号映射到特定的函数。对于相同的数字,每个体系结构可以有不同的 table 条目。在两种不同的体系结构上,相同的数字可能映射到不同的系统调用。
然后你设置你的参数。这也是特定于体系结构的,但与在通常的函数调用之间传递参数没有太大区别。通常,您会将参数放在特定的 CPU 寄存器中。这在本架构的 ABI 中有描述。
然后你进入系统调用。根据体系结构,这可能意味着导致某些异常或执行专用的 CPU 指令。
内核有一个特殊的处理函数,它在调用系统调用时在内核模式下运行。它将暂停进程执行,存储所有特定于此进程的信息(这称为 context switch
),读取系统调用编号和参数并调用适当的系统调用例程。它还将确保将 return 值放在适当的位置,供 user-space 读取并在系统调用例程完成时安排进程(恢复其上下文)。
例如,要让内核知道您要在 x86_64 上调用系统调用,您可以使用 sysenter
指令和 %rax
寄存器中的系统调用编号。使用寄存器传递参数(如果我没记错的话)%rdi
、%rsi
、%rdx
、%rcx
、%r8
和 %r9
.
您还可以使用在 32 位 x86 CPUs 上使用的旧方法 - 软件中断号 0x80(int 0x80
指令)。同样,系统调用号在 %rax
寄存器中指定,参数转到(同样,如果我没记错的话)%ebx
、%ecx
、%edx
、%esi
, %edi
, %ebp
.
ARM 非常相似 - 您将使用 "supervisor call" 指令 (SVC #0
)。您的系统调用编号将进入 r7
寄存器,所有参数将进入寄存器 r0-r6
并且系统调用的 return 值将存储在 r0
.
中
其他架构和操作系统使用类似的技术。细节可能有所不同 - 软件中断号可能不同,参数可能使用不同的寄存器甚至使用堆栈传递,但核心思想是相同的。
我正在尝试构建一个 "big picture" 来了解 Linux 内核和用户 space 的工作原理,但我很困惑。 我知道 userspace 使用系统调用 "talk" 到内核,但我不知道怎么做。我试图阅读 C 库和内核源代码,但它们很复杂且不易理解。我还阅读了几本有关操作系统概念性事实的书籍,例如管理进程、内存、设备,但它们并没有使 "transition" (userspace->kernel) 清晰。 那么,用户 space 和内核 space 之间的转换究竟发生在哪里? C 库如何 运行 Linux 内核中的代码 运行 宁在机器中?
打个比方:想象有一所房子。房子是锁着的。打开房子的钥匙就在房子里面。屋子里只有一个人,内核。用户space 是试图进入房屋的人。我的问题是:内核如何知道房子外面有人想要钥匙,以及哪种机制允许使用该钥匙打开房子?
许多处理器都有调用特定"trap"或"interrupt"的指令,Linux内核专门设置了这样的"trap"或"interrupt"系统调用。
库以某种方式设置处理器寄存器,然后执行特殊的陷阱或中断指令,使处理器进入特权模式并调用内核的 trap/interrupt 处理函数,该函数解码值在寄存器中并调用适当的函数来处理系统调用。
这是最常见的方式,基本上所有需要在内核和用户之间进行隔离的系统都是这样做的-space。
这很简单 - 这个人可以使用门铃让内核知道它在外面等着。在我们的例子中,这个门铃通常是一个特殊的 CPU 异常、软件中断或专用指令,允许用户 space 应用程序使用并且内核可以处理。
所以程序是这样的:
首先你需要知道系统调用号。每个系统调用都有其唯一的编号,内核内部有一个 table 将这些编号映射到特定的函数。对于相同的数字,每个体系结构可以有不同的 table 条目。在两种不同的体系结构上,相同的数字可能映射到不同的系统调用。
然后你设置你的参数。这也是特定于体系结构的,但与在通常的函数调用之间传递参数没有太大区别。通常,您会将参数放在特定的 CPU 寄存器中。这在本架构的 ABI 中有描述。
然后你进入系统调用。根据体系结构,这可能意味着导致某些异常或执行专用的 CPU 指令。
内核有一个特殊的处理函数,它在调用系统调用时在内核模式下运行。它将暂停进程执行,存储所有特定于此进程的信息(这称为
context switch
),读取系统调用编号和参数并调用适当的系统调用例程。它还将确保将 return 值放在适当的位置,供 user-space 读取并在系统调用例程完成时安排进程(恢复其上下文)。
例如,要让内核知道您要在 x86_64 上调用系统调用,您可以使用 sysenter
指令和 %rax
寄存器中的系统调用编号。使用寄存器传递参数(如果我没记错的话)%rdi
、%rsi
、%rdx
、%rcx
、%r8
和 %r9
.
您还可以使用在 32 位 x86 CPUs 上使用的旧方法 - 软件中断号 0x80(int 0x80
指令)。同样,系统调用号在 %rax
寄存器中指定,参数转到(同样,如果我没记错的话)%ebx
、%ecx
、%edx
、%esi
, %edi
, %ebp
.
ARM 非常相似 - 您将使用 "supervisor call" 指令 (SVC #0
)。您的系统调用编号将进入 r7
寄存器,所有参数将进入寄存器 r0-r6
并且系统调用的 return 值将存储在 r0
.
其他架构和操作系统使用类似的技术。细节可能有所不同 - 软件中断号可能不同,参数可能使用不同的寄存器甚至使用堆栈传递,但核心思想是相同的。