【问题标题】:How do system calls work?系统调用如何工作?
【发布时间】:2011-09-08 15:45:06
【问题描述】:

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

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

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

我的理解正确吗?

附上我的理解的粗略图:

【问题讨论】:

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


    【解决方案1】:

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

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

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

    【讨论】:

    • 啊哈!那是我花了半个小时寻找的缺失环节。 :D
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      是的,你的理解完全正确,一个C程序可以直接调用系统调用,当该系统调用发生时,它可以是一系列调用,直到汇编Trap。我认为您的理解可以极大地帮助新手。检查我在其中调用“系统”系统调用的代码。

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

      【讨论】:

        【解决方案4】:

        您的理解非常接近;诀窍是大多数编译器永远不会编写系统调用,因为程序调用的函数(例如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 位平台更大的页表和地址范围可能使前面的所有内容看起来很古怪。无论如何,我当然希望如此。

        【讨论】:

        • 在带有 ASID 的 TLB 上,不需要刷新。但除此之外,这是一个非常全面的答案。
        • @ninjalj,哦,那会很有帮助。让我猜猜,它们主要在 PAE 系统或 64 位平台上可用? :)
        • 它们至少在 MIPS 上可用。
        【解决方案5】:

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

        【讨论】:

        • 您能否详细说明什么是“C runtime 库”。另外,允许用户程序直接调用系统调用而不通过任何库,对吗?
        • @p2pnode:C 运行时库是 C 程序通常链接的库,在 Unix 中通常称为 libc。是的,程序可以直接调用系统调用。
        • 那么如果程序可以直接调用系统调用,这些调用将如何编译呢?在这里,C 运行时库似乎没有角色可以发挥..?
        • @p2pnode:你会写内联汇编来调用系统调用。
        【解决方案6】:

        是的,你说的很对。一个细节,当编译器编译一个系统调用时,它会使用系统调用的number而不是name。比如这里是list of Linux syscalls(老版本,但概念还是一样的)。

        【讨论】:

          猜你喜欢
          • 2023-04-09
          • 2014-11-17
          • 2015-02-23
          • 2014-07-25
          • 1970-01-01
          • 1970-01-01
          • 2011-12-28
          • 2011-06-17
          相关资源
          最近更新 更多