系统调用如何工作?

我明白,用户可以拥有一个进程,每个进程有一个地址空间(其中包含有效的内存位置,这个过程可以参考)。 我知道一个进程可以调用系统调用并将parameter passing给它,就像任何其他库函数一样。 这似乎表明,所有的系统调用都是通过共享内存等方式进入进程地址空间的。但也许,这只是一个错觉,因为在高级编程语言中,系统调用看起来像任何其他函数,当一个进程调用它。

但是,现在让我进一步深入分析一下底下发生了什么。 编译器如何编译系统调用? 它可能会将进程提供的系统调用名称和参数压入堆栈,然后将汇编指令称为“TRAP”或其他东西 – 基本上就是调用软件中断的汇编指令。

该TRAP汇编指令由硬件执行,首先将模式位从用户切换到内核,然后将代码指针设置为开始中断服务例程。 从这一点开始,ISR以内核模式执行,从堆栈中获取参数(这是可能的,因为内核可以访问任何内存位置,甚至是用户进程拥有的内存位置),并执行系统调用结束放弃CPU,再次切换模式位,用户进程从停止的地方开始。

我的理解是正确的吗?

附上的是我的理解粗略的图表: 在这里输入图像说明

你的理解非常接近; 诀窍是大多数编译器不会写系统调用,因为程序调用的函数(例如getpid(2)chdir(2)等)实际上是由标准C库提供的。 标准C库包含系统调用的代码,无论是通过INT 0x80还是SYSENTER调用。 这将是一个奇怪的程序,使系统调用没有图书馆的工作。 (即使perl提供了一个syscall()函数,可以直接进行系统调用!疯了吧?)

接下来是内存。 操作系统内核有时对用户进程内存有简单的地址空间访问。 当然,保护模式是不同的,用户提供的数据必须复制到内核​​的受保护地址空间,以防止在系统调用过程中修改用户提供的数据:

 static int do_getname(const char __user *filename, char *page) { int retval; unsigned long len = PATH_MAX; if (!segment_eq(get_fs(), KERNEL_DS)) { if ((unsigned long) filename >= TASK_SIZE) return -EFAULT; if (TASK_SIZE - (unsigned long) filename < PATH_MAX) len = TASK_SIZE - (unsigned long) filename; } retval = strncpy_from_user(page, filename, len); if (retval > 0) { if (retval < len) return 0; return -ENAMETOOLONG; } else if (!retval) retval = -ENOENT; return retval; } 

这不是系统调用本身,而是系统调用函数调用的帮助函数,它将文件名复制到内核的地址空间中。 它检查以确保整个文件名驻留在用户的数据范围内,调用一个从用户空间复制string的函数,并在返回之前执行一些完整性检查。

get_fs()和类似的函数是Linux x86根目录的残余。 这些函数在所有体系结构中都有工作实现,但名称仍旧陈旧。

所有额外的细分工作是因为内核和用户空间可能共享可用地址空间的一部分。 在一个32位平台上(数字很容易理解),内核通常会有一个千兆字节的虚拟地址空间,用户进程通常会有三千兆字节的虚拟地址空间。

当一个进程调用内核时,内核将修正页表权限以允许其访问整个范围,并获得用户提供的内存的预填充TLB条目的好处。 巨大的成功。 但是,当内核必须切换回用户空间时,必须刷新TLB以删除内核地址空间页面上的caching权限。

但诀窍是,一个千兆字节的虚拟地址空间对于大型机器上的所有内核数据结构是不够的。 维护系统中所有进程的caching文件系统和块设备驱动程序的元数据,networking堆栈以及内存映射可能占用大量的数据。

所以有不同的“分割”可用:用户两个演出,内核演出两个,用户演出一个,内核演出三个。随着内核空间的增加,用户进程空间减less。 所以有一个4:4内存拆分,给用户进程4千兆字节,4千兆字节的内核,内核必须摆弄段描述符才能访问用户内存。 TLB刷新进入和退出系统调用,这是一个相当显着的速度惩罚。 但是它可以让内核维护更大的数据结构。

64位平台的大得多的页表和地址范围可能使所有的前面看起来很古怪。 无论如何,我确实希望如此。

是的,你说得对。 一个细节是,当编译器编译系统调用时,它将使用系统调用的号码而不是名称 。 例如,这里是一个Linux系统调用列表 (对于旧版本,但概念仍然相同)。

您实际上调用了C运行时库。 插入TRAP的不是编译器,而是将TRAP封装到库调用中的C库。 其余的理解是正确的。

如果您想直接从您的程序执行系统调用,则可以轻松完成。 这是平台依赖,但假设你想从文件读取。 每个系统调用都有一个号码。 在这种情况下,您将read_from_file系统调用的编号放在寄存器EAX中。 系统调用的参数被放置在不同的寄存器或堆栈中(取决于系统调用)。 在寄存器填充正确的数据之后,您就可以执行系统调用了,您将执行指令INT 0x80 (取决于体系结构)。 该指令是导致控制权转移到操作系统的中断。 然后操作系统识别寄存器EAX中的系统调用号码,并作出相应的处理,并将控制权交给进行系统调用的进程。

系统调用的使用方式很容易改变,并取决于给定的平台。 通过使用为这些系统调用提供简单接口的库,可以使您的程序更加独立于平台,并且代码的可读性和写入速度会更快。 考虑直接用高级语言实现系统调用。 你会需要像内联汇编,以确保数据被放在正确的寄存器。

正常的程序通常不会“编译系统调用”。 对于每个系统调用,你通常使用相应的用户空间库函数(通常在类Unix系统上的libc中实现)。 例如, mkdir()函数将其参数转发给mkdir系统调用。

在GNU系统上(我想对其他人来说是一样的),在'mkdir()'函数中使用了一个syscall()函数。 syscall函数/macros通常以C语言实现。例如,查看sysdeps/unix/sysv/linux/i386/sysdep.hsysdeps/unix/sysv/linux/i386/sysdep.h syscall (glibc )。

现在,如果你看看sysdeps/unix/sysv/linux/i386/sysdep.h ,你可以看到对内核的调用是由ENTER_KERNEL完成的,历史上这个调用是在i386 CPU中调用中断0x80 。 现在它调用一个函数(我猜这是在linux-gate.so中实现的,这是一个由内核映射的虚拟SO文件,它包含了为你的CPUtypes创build一个系统调用的最有效的方法)。

是的,你的理解是绝对正确的,一个C程序可以直接调用系统调用,当系统调用发生的时候它可以是一连串的调用,直到汇编陷阱。 我认为你的理解可以帮助新手。非常感谢你的帮助。请检查我正在调用“system”系统调用的这段代码。

 #include < stdio.h > #include < stdlib.h > int main() { printf("Running ps with "system" system call "); system("ps ax"); printf("Done.\n"); exit(0); }