Linux 系统调用
最广泛使用的 CPU 架构是 IA32,又名 x86,它是 386、486、奔腾 I、Pro、II 和 III、AMD 竞争对手 K6 和 Athlon 系列,以及 VIA/Cyrix 和 Integrated Device Technologies 等其他厂商的 CPU 的架构。由于它是最广泛使用的架构,因此将在此处作为说明性示例。首先,我将介绍 IA32 类型 CPU 为处理系统调用提供的机制,然后展示 Linux 如何使用这些机制。回顾一些广义术语
内核是运行在保护模式下并有权访问硬件特权寄存器的操作系统软件。内核不是系统上运行的单独进程。它是操作系统的核心,控制进程的调度以实现多任务处理,并提供一组常驻内存的例程,每个用户空间进程都可以访问这些例程。
一些操作系统采用微内核架构,其中设备驱动程序和其他代码按需加载和执行,不一定始终驻留在内存中。
单内核架构在 UNIX 实现中更为常见;它是经典设计(如 BSD)采用的设计。
Linux 内核主要是一个单内核:即,所有设备驱动程序都是内核本身的一部分。与 BSD 不同,Linux 内核的设备驱动程序可以是“可加载的”,即,它们可以通过用户命令从内存中加载和卸载。
基本上,多任务处理是通过这种方式完成的:内核快速地在进程之间切换控制权,使用时钟中断(和其他方式)来触发从一个进程到另一个进程的切换。当硬件设备发出中断时,中断处理程序在内核中找到。当进程执行需要等待结果的操作时,内核介入并将进程置于适当的睡眠或等待状态,并在其位置调度另一个进程。
除了多任务处理,内核还包含实现用户程序和硬件设备、虚拟内存、文件管理以及系统许多其他方面之间接口的例程。
用户空间代码可以通过多种方式调用内核例程以实现上述所有功能。利用内核的一种直接方法是让进程执行系统调用。共有 116 个系统调用;这些文档可以在手册页中找到。
系统调用是正在运行的任务向内核发出的请求,要求内核代表它提供某种服务。一般来说,系统调用调用的内核服务构成了硬件和用户空间程序之间的抽象层,允许程序员实现一个操作环境,而无需使其程序过于具体地适应于单一品牌或系统硬件组件的精确特定组合。系统调用还在编程语言之间发挥这种通用化功能;例如,read 系统调用将从文件描述符读取数据。对于程序员来说,这看起来像另一个 C 函数,但实际上,read 的代码包含在内核中。
IA32 CPU 识别出两类需要处理器特别关注的事件:中断和异常。两者都会导致强制上下文切换到新的过程或任务。
中断可能在程序执行期间意外发生,用于响应信号;它们是来自硬件的信号,表明需要处理器关注。当硬件设备发出中断时,中断处理程序在内核中找到。下个月,我们将更详细地讨论中断。
IA32 识别出两种中断源:可屏蔽中断(其向量由硬件确定)和不可屏蔽中断(NMI 中断或 NMI)。
异常要么是处理器检测到的,要么是从软件发出(抛出)的。当过程或方法遇到无法处理的异常情况(异常条件)时,它可以抛出异常。任何类型的异常都由位于线程的过程或方法调用堆栈上的处理程序例程(_异常处理程序_)捕获。这可能是调用过程或方法,或者如果该过程或方法不包含处理异常条件的代码,则可能是其调用过程或方法,依此类推。如果程序的线程之一抛出任何过程(或方法)都未捕获的异常,则该线程将过期。
异常告诉调用过程发生了异常(但不一定是罕见的)情况,例如,使用无效参数调用了方法。当您抛出异常时,您正在执行一种结构化的“go to”,从程序中检测到异常条件的位置到可以处理该异常条件的位置。异常处理程序应根据每个异常处理程序能够处理的错误范围的通用程度,放置在程序模块级别,以便尽可能少的异常处理程序能够涵盖在程序现场应用中将遇到的尽可能多的异常。
在 Java 中,异常是对象。除了抛出类在 java.lang 中声明的对象外,您还可以抛出您自己设计的对象。要创建您自己的可抛出对象类,您需要将其声明为 Throwable 系列的某个成员的子类。但是,通常,您定义的可抛出类应扩展 Exception 类——它们应该是“异常”。通常,异常对象的类指示遇到的异常条件的类型。例如,如果抛出的异常对象的类为 illegalArgumentException,则表示有人向方法传递了非法参数。
当您抛出异常时,您实例化并抛出一个对象,该对象的类在 java.lang 中声明,派生自 Throwable,它有两个直接子类:Exception 和 Error。错误(Error 系列的成员)通常针对更严重的问题抛出,例如 OutOfMemoryError,这些问题可能不容易处理。错误通常由 Java API 或 Java 虚拟机的方法抛出。一般来说,您编写的代码应该只抛出异常,而不是错误。
Java 虚拟机使用异常对象的类来决定是否允许任何 catch 子句处理异常。catch 子句还可以通过直接查询异常对象以获取您在实例化期间(在抛出之前)嵌入其中的信息来获取有关异常条件的信息。Exception 类允许您指定详细消息作为字符串,可以通过在异常对象上调用 getMessage 来检索该字符串。
每个 IA32 中断或异常都有一个数字,在 IA32 文献中称为其向量。NMI 中断和处理器检测到的异常已分配了 0 到 31(包括 31)范围内的向量。可屏蔽中断的向量由硬件确定。外部中断控制器在中断确认周期期间将向量放在总线上。32 到 255(包括 255)范围内的任何向量都可以用于可屏蔽中断或编程异常。
在 /usr/src/linux/boot/head.S 中找到的 startup_32 代码通过调用 setup_idt 在启动时启动一切。此例程设置一个 IDT(中断描述符表),其中包含 256 个条目,每个条目 4 个字节长,总共 1024 字节,偏移量为 0-255。应该注意的是,IDT 包含中断处理程序和异常处理程序的向量,因此“IDT”在某种程度上用词不当,但事实就是如此。
startup_32 实际上没有加载任何中断入口点,因为只有在启用分页并将内核重定位到 0xC000000 后才会执行此操作。有时,主要是在启动期间,内核必须加载到某些地址,因为底层 BIOS 架构要求这样做。在控制权完全传递给内核后,Linux 内核可以将其自身放置在它想要的任何位置。通常,这在内存中非常高,但在 2GB 限制以下。
当调用 start_kernel(在 /usr/src/linux/init/main.c 中找到)时,它会调用 trap_init(在 /usr/src/linux/kernel/traps.c 中找到)。trap_init 通过宏 set_trap_gate(在 /usr/include/asm/system.h 中找到)设置 IDT,并按照“偏移量描述”表所示初始化中断描述符表。
此时,系统调用的中断向量尚未设置。它由 sched_init(在 /usr/src/linux/kernel/sched.c 中找到)初始化。要将中断 0x80 设置为 _system_call 入口点的向量,请调用
set_system_gate (0x80, &system_call)
同时看到的中断和异常的优先级在侧边栏“中断的运行时优先级”中显示。
Linux 系统调用接口通过 libc(通常是 glibc)中的存根进行向量化,并且专门使用“寄存器参数化”,即,堆栈不用于参数传递。libc 库中的每个调用通常都是 syscallX 宏,其中 X 是实际例程使用的参数数量。在 Linux 下,系统调用的执行是通过可屏蔽中断或异常类传输(例如,“抛出”异常对象)调用的,由 0x80 中的指令引起。向量 0x80 用于将控制权转移到内核。此中断向量在系统启动期间与其他重要向量(如系统时钟向量)一起初始化。在汇编级别(在用户空间中),它看起来像列表 1。如今,此代码包含在 glibc2.1 库中。0x80 硬编码到 Linux 和 glibc 中,作为将控制权转移到内核的系统调用号。在启动时,内核已将 IDT 向量 0x80 设置为“调用门”(请参阅 arch/i386/kernel/traps.c:trap_init)
set_system_gate(SYSCALL_VECTOR,&system_call)
向量布局在 include/asm-i386/hw_irq.h 中定义。
直到执行 int $0x80 后,调用才会转移到内核入口点 _system_call。此入口点对于所有系统调用都是相同的。它负责保存所有寄存器,检查以确保调用了有效的系统调用,然后最终通过 _sys_call_table 中的偏移量将控制权转移到实际的系统调用代码。它还负责在系统调用完成后但在返回用户空间之前调用 _ret_from_sys_call。
系统调用入口点的实际代码可以在 /usr/src/linux/kernel/sys_call.S 中找到,许多系统调用的代码可以在 /usr/src/linux/kernel/sys.c 中找到。其余代码分布在整个源文件中。一些系统调用,如 fork,有自己的源文件(例如,kernel/fork.c)。
CPU 在 int $0x80 之后执行的下一条指令是 entry.S:system_call 中的 pushl %eax。在那里,我们首先保存所有用户空间寄存器,然后我们范围检查 %eax 并调用 sys_call_table[%eax],这是实际的系统调用。
由于系统调用接口专门使用寄存器参数化,因此单个系统调用最多可以使用六个参数。%eax 是系统调用号;%ebx、%ecx、%edx、%esi、%edi 和 %ebp 是用作 param0-5 的六个通用寄存器;并且 %esp 不能使用,因为它在内核进入 ring 0(即内核模式)时被内核覆盖。
如果需要更多参数,可以将一些结构放置在地址空间中您想要的任何位置,并从寄存器(不是指令指针,也不是堆栈指针;内核空间函数使用堆栈进行参数和局部变量)指向该结构。这种情况非常罕见;大多数系统调用要么没有参数,要么只有一个参数。
系统调用返回后,我们检查进程结构中的一个或多个状态标志;确切的数量将取决于系统调用。creat 可能会留下十几个标志(existing、created、locked 等),而 sync 可能只返回一个。
如果没有待处理的工作,我们将恢复用户空间寄存器并通过 iret 返回到用户空间。iret 之后的下一条指令是列表 1 中显示的用户空间 popl %ebx 指令。
由于可变长度参数列表,一些系统调用比其他系统调用更复杂。复杂系统调用的示例包括 open 和 ioctl。但是,即使是复杂的系统调用也必须使用相同的入口点;它们只是在参数设置方面有更多的开销。每个 syscall 宏都扩展为一个汇编例程,该例程设置调用堆栈帧并通过中断(通过指令 int $0x80)调用 _system_call。例如,setuid 系统调用编码为
_syscall1(int,setuid,uid_t,uid)
它扩展为列表 2 中显示的汇编代码。
用户空间调用代码库可以在 /usr/src/libc/syscall 中找到。参数布局和实际系统调用号的硬编码不是问题,因为系统调用永远不会真正更改;它们只是“引入”和“废弃”。废弃的系统调用在 entry.S 的系统调用表中标记有 old_ 前缀,并且对其的引用从下一个 glibc 中删除。一旦没有应用程序再使用该系统调用,其插槽将被标记为“未使用”,并且可能可重用于新引入的系统调用。
如果用户希望跟踪程序,那么了解系统调用期间发生的事情同样重要。因此,程序的跟踪通常也包括通过系统调用的跟踪。这是通过父进程(跟踪进程)和子进程(被跟踪进程)之间来回传递 SIGSTOP 和 SIGCHLD 来完成的。当执行被跟踪进程时,每个系统调用之前都有一个 sys_ptrace 调用。这使得被跟踪进程在每次进行系统调用时向跟踪进程发送 SIGCHILD。被跟踪进程立即进入 TASK_STOPPED 状态(在 task_struct 结构中设置一个标志)。然后,跟踪进程可以使用多用途系统调用 _ptrace 检查被跟踪进程的整个地址空间。跟踪进程发送 SIGSTOP 以允许再次执行。
添加您自己的系统调用实际上非常容易。请按照以下步骤进行操作。请记住,如果您不让所有要运行程序的机器上都提供这些系统调用,则结果将是不可移植的代码。
在 /usr/src/linux/ 目录下创建一个目录来保存您的代码。
将任何包含文件放在 /usr/include/sys/ 和 /usr/include/linux/ 中。
将通过链接您的新内核代码生成的可重定位模块添加到顶层 Makefile 的 ARCHIVES 和子目录添加到 SUBDIRS 行。有关示例,请参阅 fs/Makefile,目标 fs.o。
向 unistd.h 添加 #define __NR_xx 以分配系统调用的调用号,其中 xx(索引)是与您的系统调用相关的描述性内容。它将用于通过 sys_call_table 设置向量以调用您的代码。
在 sys.h 中的 sys_call_table 中为您的系统调用添加一个入口点。它应该与您在上一步中分配的索引 (xx) 匹配。
NR_syscalls 变量将自动重新计算。
修改 kernel/fs/mm/ 等中的任何内核代码,以考虑支持您的新代码所需的环境。
从顶层源代码目录级别运行 make 以生成包含您的新代码的新内核。
此时,您必须要么向您的库添加系统调用,要么在您的用户程序中使用正确的 _syscalln 宏,以便您的程序可以访问新的系统调用。386DX 微处理器程序员参考手册 和 James Turley 的 高级 80386 编程技术 是有用的参考资料。
Linux/IA32 内核系统调用的列表可以在存档文件 ftp.linuxjournal.com/pub/lj/listings/issue75/4048.tgz 中找到,其中包含列表。注意:这些不是 libc“用户空间系统调用”,而是 Linux 内核提供的真正的内核系统调用。信息来源是 GNU libc 项目,https://gnu.ac.cn/。
电子邮件:moshe@moelabs.com
Moshe Bar (moshe@moelabs.com) 是一位以色列系统管理员和操作系统研究员,他于 1981 年开始在配备 AT&T UNIX Release 6 的 PDP-11 上学习 UNIX。他拥有计算机科学硕士学位。他的新书《Linux 内核内幕》将于今年出版。您可以访问 Moshe 的网站 http://www.moelabs.com/。