你的理解很接近,关键在于大多数编译器不会直接写入系统调用,因为程序调用的函数(例如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根源的遗留物。这些函数对所有架构都有工作实现,但名称仍然过时。
所有额外的段处理是因为内核和用户空间可能共享某些可用地址空间。在32位平台上(数字易于理解),内核通常具有1GB虚拟地址空间,用户进程通常具有3GB虚拟地址空间。
当进程调用内核时,内核将“修正”页面表权限,以允许其访问整个范围,并获得预填充的TLB条目,以访问用户提供的内存。非常成功。但是,当内核必须切换回用户空间时,它必须刷新TLB以删除对内核地址空间页面的缓存特权。
但是,1GB虚拟地址空间不足以容纳巨型机器上的所有内核数据结构。维护缓存文件系统和块设备驱动程序、网络堆栈以及系统中所有进程的内存映射的元数据可能需要大量数据。
因此,不同的“分割”可用:两个gigs用于用户,两个gigs用于内核,一个gig用于用户,三个gigs用于内核等。随着内核空间的增加,用户进程空间将减少。因此,有一个4:4
内存分割,为用户进程提供四GB,为内核提供四GB,并且内核必须操作段描述符才能访问用户内存。在进入和退出系统调用时刷新TLB,这是一个相当大的速度惩罚。但它可以让内核维护更大的数据结构。
64位平台的更大页面表和地址范围可能会使所有前述内容看起来都有点古老了。我真心希望如此。