添加新的系统调用

本文档描述了向 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——所以要从内核的历史中吸取教训,并从一开始就为扩展做计划。)

对于只接受几个参数的简单系统调用,允许未来扩展的首选方法是在系统调用中包含一个标志参数。为了确保用户空间程序可以在内核版本之间安全地使用标志,请检查标志值是否包含任何未知标志,如果包含,则拒绝系统调用(并返回 EINVAL

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

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

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

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) 系统调用确实返回一个新的文件描述符,则标志参数应包含一个等效于在新 FD 上设置 O_CLOEXEC 的值。这使得用户空间有可能关闭 xyzzy() 和调用 fcntl(fd, F_SETFD, FD_CLOEXEC) 之间的时间窗口,在另一个线程中意外的 fork()execve() 可能会将描述符泄漏到 exec 后的程序。(但是,要抵制重用 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 功能位(通过调用 capable() 进行检查)进行管理,如 capabilities(7) 手册页中所述。选择一个管理相关功能的现有功能位,但尽量避免将大量仅有模糊关系的功能组合在同一位下,因为这违背了功能拆分 root 权限的目的。特别是,避免添加已经过度通用的 CAP_SYS_ADMIN 功能的新用途。

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

最后,请注意,如果显式为 64 位的系统调用参数位于奇数编号的参数上(即参数 1、3、5),那么某些非 x86 架构会更容易,以允许使用连续的 32 位寄存器对。(如果参数是按指针传入的结构的一部分,则此问题不适用。)

提出 API

为了使新的系统调用易于审查,最好将补丁集分成单独的块。这些应至少包括以下项目作为不同的提交(每个提交将在下面进一步描述)

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

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

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

  • 新系统调用的手册页草案,可以以纯文本形式放在封面信中,也可以作为对(单独的)手册页存储库的补丁。

新的系统调用提案,与对内核 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 中的回退存根

x86 系统调用实现

要为 x86 平台连接你的新系统调用,你需要更新主系统调用表。 假设你的新系统调用在某些方面不是特殊的(请参见下文),这涉及到在 arch/x86/entry/syscalls/syscall_64.tbl 中添加一个“通用”条目(用于 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;
    /* ... */
};

在结构 xyzzy_args 中,则结构 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

兼容性系统调用 (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 版本不通用,那么它的系统调用表也需要调用一个 stub 来调用 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)操作。如果你的新系统调用与这些操作中的一个类似,则应更新审计系统。

更一般地说,如果存在一个与你的新系统调用类似的现有系统调用,那么值得在整个内核中搜索该现有系统调用,以检查是否还有其他特殊情况。

测试

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

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

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

对于新功能的更广泛和更彻底的测试,你还应考虑将测试添加到 Linux 测试项目或 xfstests 项目,以进行与文件系统相关的更改。

手册页

所有新的系统调用都应该附带完整的手册页,最好使用 groff 标记,但纯文本也可以。如果使用 groff,则在补丁集的封面邮件中包含手册页的预渲染 ASCII 版本会很有帮助,以便于审查人员查阅。

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

不要在内核中调用系统调用

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

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

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

此规则的例外情况仅允许在特定于架构的覆盖、特定于架构的兼容性包装器或 arch/ 中的其他代码中。

参考资料和来源