新增新的系統呼叫

本文件描述了向 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://kernel.linux.club.tw/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/` 中的其他程式碼中出現。

參考文獻和資料來源