添加新的系统调用

本文档描述了向 Linux 内核添加新的系统调用所涉及的步骤,其内容在 Documentation/process/submitting-patches.rst 中常规提交建议的基础上进行了补充。

系统调用替代方案

在添加新的系统调用时,首先要考虑的是是否有其他替代方案更适合。尽管系统调用是用户空间与内核之间最传统、最明显的交互点,但也有其他可能性——请选择最适合您接口的方式。

  • 如果所涉及的操作可以看起来像文件系统对象,那么创建新的文件系统或设备可能更有意义。这也使得将新功能封装在内核模块中变得更容易,而无需将其构建到主内核中。

    • 如果新功能涉及内核通知用户空间某事已发生的操作,那么为相关对象返回一个新的文件描述符将允许用户空间使用 poll/select/epoll 来接收该通知。

    • 然而,不映射到类似 read(2)/write(2) 操作的功能必须作为 ioctl(2) 请求来实现,这可能导致 API 不够透明。

  • 如果您只是暴露运行时系统信息,那么在 sysfs(参见 Documentation/filesystems/sysfs.rst)或 /proc 文件系统中添加新节点可能更合适。然而,访问这些机制要求挂载相关文件系统,这并非总是可行(例如,在命名空间/沙盒/chroot 环境中)。避免向 debugfs 添加任何 API,因为这不被视为用户空间的“生产”接口。

  • 如果操作特定于某个文件或文件描述符,那么添加一个 fcntl(2) 命令选项可能更合适。然而,fcntl(2) 是一个多路复用系统调用,它隐藏了大量的复杂性,因此此选项最适用于新功能与现有 fcntl(2) 功能非常相似,或新功能非常简单的情况(例如,获取/设置与文件描述符相关的简单标志)。

  • 如果操作特定于某个任务或进程,那么添加一个 prctl(2) 命令选项可能更合适。与 fcntl(2) 类似,此系统调用是一个复杂的多路复用器,因此最好保留给现有 prctl() 命令的近似模拟,或获取/设置与进程相关的简单标志。

设计 API:规划扩展

新的系统调用构成了内核 API 的一部分,并且必须无限期地得到支持。因此,在内核邮件列表中明确讨论接口是一个非常好的主意,并且规划接口的未来扩展至关重要。

(系统调用表充斥着许多未遵循此原则的历史案例,以及相应的后续系统调用——eventfd/eventfd2dup2/dup3inotify_init/inotify_init1pipe/pipe2renameat/renameat2——因此请从内核的历史中吸取教训,从一开始就规划好扩展性。)

对于只接受少量参数的简单系统调用,实现未来可扩展性的首选方法是在系统调用中包含一个 `flags` 参数。为确保用户空间程序能在不同内核版本之间安全地使用 `flags`,请检查 `flags` 值是否包含任何未知标志,如果包含,则拒绝该系统调用(返回 EINVAL)。

if (flags & ~(THING_FLAG1 | THING_FLAG2 | THING_FLAG3))
    return -EINVAL;

(如果尚未使用任何 `flags` 值,请检查 `flags` 参数是否为零。)

对于涉及大量参数的更复杂的系统调用,首选的方法是将大部分参数封装到一个通过指针传递的结构体中。这样的结构体可以通过在其中包含一个 `size` 参数来应对未来的扩展。

struct xyzzy_params {
    u32 size; /* userspace sets p->size = sizeof(struct xyzzy_params) */
    u32 param_1;
    u64 param_2;
    u64 param_3;
};

只要后续添加的任何字段(例如 param_4)被设计为零值可提供先前的行为,那么这就能处理双向的版本不匹配情况。

  • 为了应对较新的用户空间程序调用较旧内核的情况,内核代码应检查结构体预期大小之外的任何内存是否为零(实际上是检查 param_4 == 0)。

  • 为了应对较旧的用户空间程序调用较新内核的情况,内核代码可以将结构体的较小实例零扩展(实际上是设置 param_4 = 0)。

有关此方法的示例,请参阅 perf_event_open(2)perf_copy_attr() 函数(位于 kernel/events/core.c 中)。

设计 API:其他考虑事项

如果您的新系统调用允许用户空间引用内核对象,它应使用文件描述符作为该对象的句柄——当内核已经有使用文件描述符的机制和良好定义的语义时,请勿发明新的用户空间对象句柄类型。

如果您的新 xyzzy(2) 系统调用确实返回一个新的文件描述符,那么 `flags` 参数应该包含一个等同于在新 FD 上设置 O_CLOEXEC 的值。这使得用户空间能够关闭 xyzzy() 和调用 fcntl(fd, F_SETFD, FD_CLOEXEC) 之间的时间窗口,在该窗口中,另一个线程中意外的 fork()execve() 可能导致描述符泄露给被执行的程序。(然而,请抵制重用 O_CLOEXEC 常量实际值的诱惑,因为它具有架构特定性,并且是 O_* 标志编号空间的一部分,该空间已相当满。)

如果您的系统调用返回一个新的文件描述符,您还应该考虑在该文件描述符上使用 poll(2) 系列系统调用的含义。使文件描述符准备好读写是内核向用户空间指示相应内核对象上已发生事件的常用方式。

如果您的新 xyzzy(2) 系统调用涉及文件名参数

int sys_xyzzy(const char __user *path, ..., unsigned int flags);

您还应该考虑 xyzzyat(2) 版本是否更合适

int sys_xyzzyat(int dfd, const char __user *path, ..., unsigned int flags);

这为用户空间指定相关文件提供了更大的灵活性;特别是它允许用户空间使用 AT_EMPTY_PATH 标志为已打开的文件描述符请求功能,从而有效地免费提供了一个 fxyzzy(3) 操作。

- xyzzyat(AT_FDCWD, path, ..., 0) is equivalent to xyzzy(path,...)
- xyzzyat(fd, "", ..., AT_EMPTY_PATH) is equivalent to fxyzzy(fd, ...)

(有关 `*at()` 调用原理的更多详细信息,请参阅 openat(2) 手册页;有关 `AT_EMPTY_PATH` 的示例,请参阅 fstatat(2) 手册页。)

如果您的新 xyzzy(2) 系统调用涉及描述文件内偏移量的参数,请将其类型设置为 loff_t,以便即使在 32 位架构上也能支持 64 位偏移。

如果您的新 xyzzy(2) 系统调用涉及特权功能,则需要由适当的 Linux `capability` 位(通过调用 capable() 进行检查)进行管理,如 capabilities(7) 手册页所述。选择一个管理相关功能的现有 `capability` 位,但尽量避免将许多仅模糊相关的功能合并到同一个位下,因为这违背了 `capability` 旨在分割 root 权限的目的。特别是,避免添加对已经过于通用的 CAP_SYS_ADMIN `capability` 的新用途。

如果您的新 xyzzy(2) 系统调用操作的是调用进程之外的进程,则应进行限制(通过调用 ptrace_may_access()),以便只有具有与目标进程相同权限或具有必要 `capability` 的调用进程才能操作目标进程。

最后,请注意,某些非 x86 架构如果明确为 64 位且作为奇数参数(即参数 1、3、5)的系统调用参数,则更容易处理,以允许使用连续的 32 位寄存器对。(如果参数是指针传递结构体的一部分,则此问题不适用。)

API 提案

为了便于审查新的系统调用,最好将补丁集划分为独立的块。这些块应至少包含以下作为独立提交的项目(每个项目将在下面进一步描述):

  • 系统调用的核心实现,以及原型、通用编号、Kconfig 更改和回退存根实现。

  • 为特定架构(通常是 x86,包括所有 x86_64、x86_32 和 x32)连接新的系统调用。

  • 通过 tools/testing/selftests/ 中的自测试,演示新系统调用在用户空间中的使用。

  • 新系统调用的手册页草稿,可以是封面信中的纯文本,也可以是(独立的)man-pages 仓库的补丁。

新的系统调用提案,与对内核 API 的任何更改一样,应始终抄送给 linux-api@vger.kernel.org

通用系统调用实现

您的新 xyzzy(2) 系统调用的主要入口点将命名为 sys_xyzzy(),但您应使用适当的 SYSCALL_DEFINEn() 宏而不是显式地添加此入口点。'n' 表示系统调用的参数数量,宏将系统调用名称以及参数的(类型,名称)对作为参数。使用此宏允许有关新系统调用的元数据可供其他工具使用。

新的入口点还需要在 include/linux/syscalls.h 中有一个相应的函数原型,标记为 `asmlinkage` 以匹配系统调用的调用方式。

asmlinkage long sys_xyzzy(...);

一些架构(例如 x86)有自己特定的系统调用表,但其他一些架构共享一个通用系统调用表。通过在 include/uapi/asm-generic/unistd.h 的列表中添加一个条目,将您的新系统调用添加到通用列表。

#define __NR_xyzzy 292
__SYSCALL(__NR_xyzzy, sys_xyzzy)

同时更新 `__NR_syscalls` 计数以反映新增的系统调用,并请注意,如果在同一个合并窗口中添加了多个新的系统调用,您的新系统调用号可能会被调整以解决冲突。

文件 kernel/sys_ni.c 提供了每个系统调用的回退存根实现,返回 -ENOSYS。也请在此处添加您的新系统调用。

COND_SYSCALL(xyzzy);

您的新内核功能以及控制它的系统调用通常应是可选的,因此为其添加一个 CONFIG 选项(通常在 init/Kconfig 中)。对于新的 CONFIG 选项,通常需要:

  • 包含对新功能以及该选项控制的系统调用的描述。

  • 如果该选项应向普通用户隐藏,请使其依赖于 `EXPERT`。

  • 在 Makefile 中,使任何实现该功能的新源文件依赖于 `CONFIG` 选项(例如 obj-$(CONFIG_XYZZY_SYSCALL) += xyzzy.o)。

  • 仔细检查在禁用新 `CONFIG` 选项的情况下,内核是否仍能成功构建。

总结一下,您需要一个包含以下内容的提交:

  • 新功能的 CONFIG 选项,通常在 init/Kconfig

  • 入口点的 SYSCALL_DEFINEn(xyzzy, ...)

  • include/linux/syscalls.h 中的相应原型

  • include/uapi/asm-generic/unistd.h 中的通用表条目

  • kernel/sys_ni.c 中的回退存根

自 6.11 版本起

从内核版本 6.11 开始,以下架构的通用系统调用实现不再需要修改 include/uapi/asm-generic/unistd.h

  • arc

  • arm64

  • csky

  • hexagon

  • loongarch

  • nios2

  • openrisc

  • riscv

相反,您需要更新 scripts/syscall.tbl,并且(如果适用)调整 arch/*/kernel/Makefile.syscalls

由于 scripts/syscall.tbl 作为跨多个架构的通用系统调用表,因此此表中需要一个新的条目。

468   common   xyzzy     sys_xyzzy

请注意,在 scripts/syscall.tbl 中添加一个带有“common” ABI 的条目也会影响所有共享此表的架构。对于更受限或特定于架构的更改,请考虑使用架构特定的 ABI 或定义一个新的 ABI。

如果引入了一个新的 ABI,例如 xyz,也应相应地更新 arch/*/kernel/Makefile.syscalls

syscall_abis_{32,64} += xyz (...)

总结一下,您需要一个包含以下内容的提交:

  • 新功能的 CONFIG 选项,通常在 init/Kconfig

  • 入口点的 SYSCALL_DEFINEn(xyzzy, ...)

  • include/linux/syscalls.h 中的相应原型

  • scripts/syscall.tbl 中添加新条目

  • (如果需要)在 arch/*/kernel/Makefile.syscalls 中更新 Makefile

  • kernel/sys_ni.c 中的回退存根

x86 系统调用实现

为了在 x86 平台上连接您的新系统调用,您需要更新主系统调用表。假设您的新系统调用没有特殊之处(参见下文),这需要在 arch/x86/entry/syscalls/syscall_64.tbl 中添加一个“common”条目(适用于 x86_64 和 x32)

333   common   xyzzy     sys_xyzzy

并在 arch/x86/entry/syscalls/syscall_32.tbl 中添加一个“i386”条目。

380   i386     xyzzy     sys_xyzzy

同样,如果在相关的合并窗口中存在冲突,这些数字可能会被更改。

兼容系统调用(通用)

对于大多数系统调用,即使用户空间程序本身是 32 位的,也可以调用相同的 64 位实现;即使系统调用的参数包含显式指针,这也会被透明地处理。

然而,在某些情况下,需要兼容层来处理 32 位和 64 位之间的尺寸差异。

第一种情况是,如果 64 位内核也支持 32 位用户空间程序,因此需要解析可能包含 32 位或 64 位值的 (__user) 内存区域。特别地,当系统调用参数是以下情况时需要这样做:

  • 指向指针的指针

  • 指向包含指针的结构体(例如 struct iovec __user *)的指针

  • 指向可变大小整数类型(time_toff_tlong 等)的指针

  • 指向包含可变大小整数类型的结构体的指针。

第二种需要兼容层的情况是,如果系统调用的某个参数即使在 32 位架构上也是显式 64 位类型,例如 loff_t__u64。在这种情况下,从 32 位应用程序传递到 64 位内核的值将被拆分为两个 32 位值,然后需要在兼容层中重新组装。

(请注意,指向显式 64 位类型的系统调用参数不需要兼容层;例如,splice(2) 的类型为 loff_t __user * 的参数不会触发对 compat_ 系统调用的需求。)

系统调用的兼容版本称为 compat_sys_xyzzy(),并通过 COMPAT_SYSCALL_DEFINEn() 宏添加,类似于 `SYSCALL_DEFINEn`。此实现版本作为 64 位内核的一部分运行,但期望接收 32 位参数值,并进行必要的处理。(通常,compat_sys_ 版本将值转换为 64 位版本,然后调用 sys_ 版本,或者两者都调用一个公共的内部实现函数。)

兼容入口点还需要在 include/linux/compat.h 中有一个相应的函数原型,标记为 `asmlinkage` 以匹配系统调用的调用方式。

asmlinkage long compat_sys_xyzzy(...);

如果系统调用涉及在 32 位和 64 位系统上布局不同的结构体,例如 struct xyzzy_args,那么 `include/linux/compat.h` 头文件也应包含该结构体的兼容版本(struct compat_xyzzy_args),其中每个可变大小字段都具有与 struct xyzzy_args 中的类型相对应的适当 compat_ 类型。compat_sys_xyzzy() 例程随后可以使用此 compat_ 结构体来解析来自 32 位调用的参数。

例如,如果有字段

struct xyzzy_args {
    const char __user *ptr;
    __kernel_long_t varying_val;
    u64 fixed_val;
    /* ... */
};

在 `struct xyzzy_args` 中,那么 `struct compat_xyzzy_args` 将有

struct compat_xyzzy_args {
    compat_uptr_t ptr;
    compat_long_t varying_val;
    u64 fixed_val;
    /* ... */
};

通用系统调用列表也需要调整以允许兼容版本;include/uapi/asm-generic/unistd.h 中的条目应使用 __SC_COMP 而不是 __SYSCALL

#define __NR_xyzzy 292
__SC_COMP(__NR_xyzzy, sys_xyzzy, compat_sys_xyzzy)

总结一下,您需要:

  • 兼容入口点的 COMPAT_SYSCALL_DEFINEn(xyzzy, ...)

  • include/linux/compat.h 中的相应原型

  • (如果需要)在 include/linux/compat.h 中的 32 位映射结构体

  • include/uapi/asm-generic/unistd.h 中使用 __SC_COMP 而非 __SYSCALL 实例

自 6.11 版本起

这适用于“通用系统调用实现”下 自 6.11 版本起 列出的所有架构,arm64 除外。有关更多信息,请参阅 兼容系统调用 (arm64)

您需要在 scripts/syscall.tbl 中的条目中扩展一个额外的列,以指示在 64 位内核上运行的 32 位用户空间程序应命中兼容入口点。

468   common     xyzzy     sys_xyzzy    compat_sys_xyzzy

总结一下,您需要:

  • 兼容入口点的 COMPAT_SYSCALL_DEFINEn(xyzzy, ...)

  • include/linux/compat.h 中的相应原型

  • 修改 scripts/syscall.tbl 中的条目,以包含一个额外的“compat”列

  • (如果需要)在 include/linux/compat.h 中的 32 位映射结构体

兼容系统调用 (arm64)

在 arm64 上,有一个专门用于面向 32 位 (AArch32) 用户空间的兼容系统调用表:arch/arm64/tools/syscall_32.tbl。您需要在此表中添加一行以指定兼容入口点。

468   common     xyzzy     sys_xyzzy    compat_sys_xyzzy

兼容系统调用 (x86)

为了为具有兼容版本的系统调用连接 x86 架构,需要调整系统调用表中的条目。

首先,arch/x86/entry/syscalls/syscall_32.tbl 中的条目会增加一个额外的列,以指示在 64 位内核上运行的 32 位用户空间程序应命中兼容入口点。

380   i386     xyzzy     sys_xyzzy    __ia32_compat_sys_xyzzy

其次,您需要弄清楚新系统调用的 x32 ABI 版本应该如何处理。这里有一个选择:参数的布局应与 64 位版本匹配,或者与 32 位版本匹配。

如果涉及指向指针的指针,则决定很简单:x32 是 ILP32,因此布局应与 32 位版本匹配,并且 arch/x86/entry/syscalls/syscall_64.tbl 中的条目被拆分,以便 x32 程序命中兼容包装器。

333   64       xyzzy     sys_xyzzy
...
555   x32      xyzzy     __x32_compat_sys_xyzzy

如果不涉及指针,则最好为 x32 ABI 重用 64 位系统调用(因此 arch/x86/entry/syscalls/syscall_64.tbl 中的条目保持不变)。

无论哪种情况,您都应检查参数布局中涉及的类型是否确实从 x32 (-mx32) 精确映射到 32 位 (-m32) 或 64 位 (-m64) 等效类型。

返回其他位置的系统调用

对于大多数系统调用,一旦系统调用完成,用户程序会精确地从中断的地方继续执行——在下一条指令处,堆栈与系统调用之前相同,大多数寄存器也相同,并且使用相同的虚拟内存空间。

然而,少数系统调用的行为有所不同。它们可能返回到不同的位置(rt_sigreturn),或者更改程序的内存空间(fork/vfork/clone),甚至更改架构(execve/execveat)。

为了允许这种情况,系统调用的内核实现可能需要将额外的寄存器保存并恢复到内核堆栈,从而完全控制系统调用后执行的继续位置和方式。

这是架构特定的,但通常涉及定义汇编入口点,用于保存/恢复额外的寄存器并调用真实的系统调用入口点。

对于 x86_64,这在 arch/x86/entry/entry_64.S 中实现为一个 stub_xyzzy 入口点,并且系统调用表(arch/x86/entry/syscalls/syscall_64.tbl)中的条目会进行相应调整。

333   common   xyzzy     stub_xyzzy

在 64 位内核上运行的 32 位程序的等效实现通常称为 stub32_xyzzy,并在 arch/x86/entry/entry_64_compat.S 中实现,系统调用表 arch/x86/entry/syscalls/syscall_32.tbl 中有相应的调整。

380   i386     xyzzy     sys_xyzzy    stub32_xyzzy

如果系统调用需要兼容层(如前一节所述),那么 stub32_ 版本需要调用系统调用的 compat_sys_ 版本,而不是原生的 64 位版本。此外,如果 x32 ABI 实现与 x86_64 版本不通用,那么其系统调用表也需要调用一个存根,该存根再调用 compat_sys_ 版本。

为了完整起见,最好也设置一个映射,以便用户模式 Linux 仍然可以工作——它的系统调用表将引用 `stub_xyzzy`,但 UML 构建不包含 arch/x86/entry/entry_64.S 实现(因为 UML 模拟寄存器等)。解决这个问题就像在 arch/x86/um/sys_call_table_64.c 中添加一个 `#define` 一样简单。

#define stub_xyzzy sys_xyzzy

其他细节

大多数内核以通用方式处理系统调用,但偶尔也会有例外情况,可能需要根据您的特定系统调用进行更新。

审计子系统就是这样一个特例;它包含(架构特定的)函数,用于对某些特殊类型的系统调用进行分类——特别是文件打开(open/openat)、程序执行(execve/exeveat)或套接字多路复用器(socketcall)操作。如果您的新系统调用与其中一个类似,那么审计系统应该更新。

更一般地,如果存在一个与您的新系统调用类似的现有系统调用,那么值得在整个内核范围内对现有系统调用进行 `grep`,以检查是否存在其他特殊情况。

测试

新的系统调用显然应该进行测试;同时,向审查者提供用户空间程序如何使用该系统调用的演示也很有用。结合这些目标的一个好方法是在 tools/testing/selftests/ 下的新目录中包含一个简单的自测试程序。

对于新的系统调用,显然不会有 libc 包装函数,因此测试需要使用 syscall() 来调用它;此外,如果系统调用涉及一个新的用户空间可见结构体,则需要安装相应的头文件才能编译测试。

确保自测试在所有支持的架构上成功运行。例如,检查它在编译为 x86_64 (-m64)、x86_32 (-m32) 和 x32 (-mx32) ABI 程序时是否正常工作。

为了对新功能进行更广泛和彻底的测试,您还应该考虑将测试添加到 Linux Test Project,或者对于文件系统相关的更改,添加到 xfstests 项目。

手册页

所有新的系统调用都应附带完整的手册页,理想情况下使用 groff 标记,但纯文本也可以。如果使用 groff,在补丁集的封面邮件中包含预渲染的 ASCII 版本手册页会很有帮助,以方便审阅者。

手册页应抄送给 linux-man@vger.kernel.org。有关更多详细信息,请参阅 https://linuxkernel.org.cn/doc/man-pages/patches.html

请勿在内核中调用系统调用

如上所述,系统调用是用户空间与内核之间的交互点。因此,系统调用函数,例如 sys_xyzzy()compat_sys_xyzzy(),应仅通过系统调用表从用户空间调用,而不能从内核中的其他地方调用。如果系统调用功能在内核内部有用,需要在新旧系统调用之间共享,或者需要在系统调用及其兼容变体之间共享,则应通过“辅助”函数(例如 ksys_xyzzy())来实现。然后,此内核函数可以在系统调用存根(sys_xyzzy())、兼容系统调用存根(compat_sys_xyzzy())和/或其他内核代码中调用。

至少在 64 位 x86 上,从 v4.17 版本开始,内核中将严格要求不直接调用系统调用函数。它对系统调用使用不同的调用约定,其中 struct pt_regs 在系统调用包装器中即时解码,然后将处理移交给实际的系统调用函数。这意味着在系统调用入口处,只传递特定系统调用实际需要的参数,而不是始终用随机用户空间内容填充六个 CPU 寄存器(这可能在调用链中导致严重问题)。

此外,内核数据和用户数据之间的数据访问规则可能不同。这也是为什么通常不建议调用 sys_xyzzy() 的另一个原因。

此规则的例外仅允许在架构特定的重载、架构特定的兼容性包装器或 `arch/` 中的其他代码中出现。

参考文献和资料来源