您的理解非常接近;诀窍是大多数编译器永远不会编写系统调用,因为程序调用的函数(例如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;
}
虽然它本身不是系统调用,但它是一个由系统调用函数调用的辅助函数,它将文件名复制到内核的地址空间中。它检查以确保整个文件名位于用户的数据范围内,调用从用户空间复制字符串的函数,并在返回之前执行一些完整性检查。
get_fs() 和类似的函数是 Linux x86-root 的残余。这些函数具有适用于所有架构的工作实现,但名称仍然过时。
所有与段有关的额外工作是因为内核和用户空间可能共享部分可用地址空间。在 32 位平台上(数字很容易理解),内核通常会有 1 GB 的虚拟地址空间,而用户进程通常会有 3 GB 的虚拟地址空间。
当一个进程调用内核时,内核将“修复”页表权限以允许它访问整个范围,并获得用户提供内存的预填充TLB entries 的好处。巨大的成功。但是当内核必须上下文切换回用户空间时,它必须刷新 TLB 以删除内核地址空间页面上的缓存权限。
但诀窍在于,1 GB 的虚拟地址空间对于大型机器上的所有内核数据结构不够。维护缓存文件系统和块设备驱动程序、网络堆栈以及系统上所有进程的内存映射的元数据可能需要大量数据。
因此可以使用不同的“拆分”:用户使用两个 gig,内核使用两个 gig,用户使用一个 gig,内核使用三个 gig,等等。随着内核空间的增加,用户进程的空间下降。所以有一个4:4 内存分割,给用户进程 4 GB,给内核 4 GB,内核必须摆弄段描述符才能访问用户内存。 TLB 被刷新进入和退出系统调用,这是一个非常显着的速度损失。但它让内核可以维护更大的数据结构。
64 位平台更大的页表和地址范围可能使前面的所有内容看起来很古怪。无论如何,我当然希望如此。