系统调用是用户空间和内核空间之间的接口,是用户程序与操作系统之间的桥梁。

Linux 内核提供了一系列的系统调用,用户程序可以通过这些系统调用来请求内核执行某些操作。

比如,open 系统调用用于打开一个文件,read 系统调用用于读取文件内容,write 系统调用用于写入文件内容等。

本文将介绍如何为 Linux Kernel 添加一个新的系统调用。

添加系统调用

为 Linux Kernel 添加系统调用的步骤如下:

  1. 修改 arch/x86/entry/syscalls/syscall_64.tbl 文件,添加新的系统调用号。

系统调用号是一个整数,用于标识系统调用。

具体来说,我们需要在 syscall_64.tbl 文件中添加一行,格式如下:

<number> <abi> <name> <entry point> [<compat entry point> [noreturn]]

number 是系统调用号, 它是一个整数, read 的系统调用号是 0, write 的系统调用号是 1, 以此类推
abi 的全称是 Application Binary Interface,它通常可以指定为 common64x32
name 是系统调用的名称
entry point 是系统调用的入口地址

我们在这个文件中添加一个新的系统调用,用来获取当前机器的 CPU 数量。

463 common  get_cpu_num         sys_get_cpu_num 

  1. include/linux/syscalls.h 文件中声明新的系统调用。

syscalls.h 文件中添加一个新的系统调用声明,格式如下:

asmlinkage long sys_get_cpu_num(void);

为什么要使用 asmlinkage 呢?asmlinkage 确保系统调用函数的参数通过堆栈传递,而不是通过寄存器。这在某些架构或特定调用约定下是必要的。尽管在 x86_64 架构上,系统调用的参数通常通过寄存器传递,但使用 asmlinkage 可以确保在不同架构上参数传递的一致性。

示例
假设我们有一个系统调用 sys_my_syscall,它接受两个参数 int arg1 和 int arg2。

在 x86_64 架构上
在 x86_64 架构上,系统调用的参数通常通过寄存器传递:

asmlinkage long sys_my_syscall(int arg1, int arg2) {
   // 参数 arg1 和 arg2 通过寄存器传递
   return arg1 + arg2;
}

在这个例子中,arg1 和 arg2 会通过 rdi 和 rsi 寄存器传递。

在 x86 (32-bit) 架构上
在 x86 (32-bit) 架构上,系统调用的参数通过堆栈传递:

asmlinkage long sys_my_syscall(int arg1, int arg2) {
    // 参数 arg1 和 arg2 通过堆栈传递
    return arg1 + arg2;
}

在这个例子中,arg1 和 arg2 会通过堆栈传递。

  1. kernel/sys.c 文件中实现新的系统调用。

sys.c 文件中实现新的系统调用,格式如下:

SYSCALL_DEFINE0(get_cpu_num)
{
	return num_present_cpus();
}

SYSCALL_DEFINE0 是一个宏,用于定义一个参数个数为 0 的系统调用。

num_present_cpus 是一个内核函数,用于获取当前机器的 CPU 数量。

相应地,也存在 SYSCALL_DEFINE1SYSCALL_DEFINE2SYSCALL_DEFINE3 等宏,用于定义参数个数为 1、2、3 的系统调用。

例如:

SYSCALL_DEFINE1(umask, int, mask)
{
	mask = xchg(&current->fs->umask, mask & S_IRWXUGO);
	return mask;
}

SYSCALL_DEFINE1 宏用于定义一个参数个数为 1 的系统调用,umask 是系统调用的名称,int 是参数的类型,mask 是参数的名称。

测试系统调用

添加系统调用后需要重新编译内核,如果你不知道怎么编译内核,请先阅读 Linux Kernel 从编译到运行

  1. make mrproper 清理内核源码目录
  2. make menuconfig 配置内核

  1. make -j8 bzImage 编译内核

将编译好的内核镜像文件拷贝到前一篇文章workspace 目录下。

  1. 编写测试程序

workspace/initramfs 目录下创建一个测试程序 test_sys_call.c,内容如下:

#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>

int main() {
    printf("Pid = %ld\n", syscall(39));
    long present_cpus = syscall(463);
    printf("Number of present CPUs: %ld\n", present_cpus);
    return 0;
}
  1. 编译测试程序

因为我们在 QEMU 中运行测试程序,所以在编译时需要采用静态链接的方式。

静态链接就是将程序所需要的库文件全部打包到可执行文件中,这样在运行时就不需要再去加载库文件。

gcc -static test_sys_call.c -o test_sys_call

此时我的workspace目录结构如下:

$ tree -L 2
.
├── busybox
├── bzImage
├── initramfs
│   ├── bin
│   ├── etc
│   ├── init
│   ├── proc
│   ├── sbin
│   ├── sys
│   ├── test_sys_call
│   └── test_sys_call.c
├── initramfs.img
└── Makefile

7 directories, 7 files

  1. 运行测试程序

我们要在 QEMU 中运行测试程序,首先修改一下 Makefile 文件,为 QEMU 启动时设置 CPU 数量。

-smp n 参数用于设置 CPU 数量。

.PHONY: initramfs run clean

initramfs:
	cd initramfs && find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.img

run:
	qemu-system-x86_64 \
	-kernel bzImage \
	-initrd initramfs.img \
	-nographic \
	-append "console=ttyS0" \
	-m 1024 \
	-smp 1 

clean:
	rm -f initramfs.cpio initramfs.img

然后我们依次执行以下命令:

make clean
make initramfs
make run

启动 QEMU 后,我们可以在 QEMU 中运行测试程序:

修改 Makefile 文件中的 -smp 1-smp 4,然后重新执行 make run 命令,再次执行测试程序:

可以看到此时CPU数量变成了 4, 同时出现了 Kernel Panic,暂时先不管它。

修改 -smp 15,然后重新执行 make run 命令,再次执行测试程序:

可以看到此时CPU数量变成了 15。

总结

本文介绍了如何为 Linux Kernel 添加一个新的系统调用,以及如何测试系统调用。

  1. 系统调用是一个和CPU架构相关的概念,不同的CPU架构有不同的系统调用号。
  2. x86 架构下添加一个新的系统调用的步骤是:修改 arch/x86/entry/syscalls/syscall_64.tbl 文件,添加新的系统调用号;在 include/linux/syscalls.h 文件中声明新的系统调用;在 kernel/sys.c 文件中实现新的系统调用。