系统调用如何工作?

时间:2011-06-05 07:51:10

标签: compiler-construction process operating-system interrupt system-calls

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

但是,现在让我更进一步,更深入地分析引擎盖下发生的事情。编译器如何编译系统调用?它可能会将进程提供的系统调用名称和参数压入堆栈,然后将汇编指令说成“TRAP”或其他东西 - 基本上是汇编指令来调用软件中断。

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

我的理解是否正确?

附上我理解的粗略图表: enter image description here

6 个答案:

答案 0 :(得分:14)

你的理解非常接近;诀窍是大多数编译器永远不会编写系统调用,因为程序调用的函数(例如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位平台上(数字易于理解),内核通常具有1千兆字节的虚拟地址空间,而用户进程通常具有3千兆字节的虚拟地址空间。

当进程调用内核时,内核将“修复”页表权限以允许它访问整个范围,并为用户提供的内存获得预先填充TLB entries的好处。巨大的成功。但是当内核必须上下文切换回用户空间时,它必须刷新TLB以删除内核地址空间页面上的缓存权限。

但问题是,对于大型机器上的所有内核数据结构,1 GB的虚拟地址空间就足够了。维护缓存文件系统和块设备驱动程序,网络堆栈以及系统上所有进程的内存映射的元数据可能会占用大量数据。

因此可以使用不同的'splits':两个用户演出,两个演出用于内核,一个演出用于用户,三个演出用于内核等。随着内核空间的增加,用户进程的空间也会下降。因此有一个4:4内存分割,它为用户进程提供4千兆字节,内核为4千兆字节,内核必须摆弄分段描述符才能访问用户内存。 TLB刷新进入和退出系统调用,这是一个非常显着的速度惩罚。但它让内核保持了更大的数据结构。

更大的页表和64位平台的地址范围可能使前面所有的外观变得古怪。无论如何,我当然希望如此。

答案 1 :(得分:8)

是的,你已经非常正确了。但有一个细节,当编译器编译系统调用时,它将使用系统调用的编号而不是名称。例如,这里是list of Linux syscalls(对于旧版本,但概念仍然相同)。

答案 2 :(得分:4)

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

答案 3 :(得分:3)

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

在GNU系统上(我猜其他人也一样),'mkdir()'函数使用syscall()函数。系统调用函数/宏通常用C实现。例如,查看INTERNAL_SYSCALL中的sysdeps/unix/sysv/linux/i386/sysdep.hsyscall中的sysdeps/unix/sysv/linux/i386/sysdep.S(glibc)。

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

答案 4 :(得分:3)

如果您想直接从程序中执行系统调用,则可以轻松执行此操作。 它取决于平台,但是假设您想从文件中读取。每个系统调用都有一个数字。在这种情况下,您将read_from_file系统调用的编号放在寄存器EAX中。系统调用的参数放在不同的寄存器或堆栈中(取决于系统调用)。在寄存器填充了正确的数据并准备好执行系统调用之后,执行指令INT 0x80(取决于体系结构)。 该指令是一个中断,导致控制进入操作系统。然后操作系统识别寄存器EAX中的系统调用号,相应地起作用并将控制权交还给进行系统调用的进程。

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

答案 5 :(得分:0)

是的,你的理解绝对正确,一个C程序可以调用直接系统调用,当系统调用发生时,它可以是一系列调用直到汇编陷阱。我认为你的理解可以帮助一个新手。检查这个代码,我称之为“系统”系统调用。

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