系统调用是用户空间和内核空间之间的接口,是用户程序与操作系统之间的桥梁。
Linux 内核提供了一系列的系统调用,用户程序可以通过这些系统调用来请求内核执行某些操作。
比如,open
系统调用用于打开一个文件,read
系统调用用于读取文件内容,write
系统调用用于写入文件内容等。
本文将介绍如何为 Linux Kernel 添加一个新的系统调用。
添加系统调用
为 Linux Kernel 添加系统调用的步骤如下:
- 修改
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,它通常可以指定为 common
,64
或 x32
name
是系统调用的名称
entry point
是系统调用的入口地址
我们在这个文件中添加一个新的系统调用,用来获取当前机器的 CPU 数量。
463 common get_cpu_num sys_get_cpu_num
- 在
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 会通过堆栈传递。
- 在
kernel/sys.c
文件中实现新的系统调用。
在 sys.c
文件中实现新的系统调用,格式如下:
SYSCALL_DEFINE0(get_cpu_num)
{
return num_present_cpus();
}
SYSCALL_DEFINE0
是一个宏,用于定义一个参数个数为 0 的系统调用。
num_present_cpus
是一个内核函数,用于获取当前机器的 CPU 数量。
相应地,也存在 SYSCALL_DEFINE1
、SYSCALL_DEFINE2
、SYSCALL_DEFINE3
等宏,用于定义参数个数为 1、2、3 的系统调用。
例如:
SYSCALL_DEFINE1(umask, int, mask)
{
mask = xchg(¤t->fs->umask, mask & S_IRWXUGO);
return mask;
}
SYSCALL_DEFINE1
宏用于定义一个参数个数为 1 的系统调用,umask
是系统调用的名称,int
是参数的类型,mask
是参数的名称。
测试系统调用
添加系统调用后需要重新编译内核,如果你不知道怎么编译内核,请先阅读 Linux Kernel 从编译到运行
make mrproper
清理内核源码目录make menuconfig
配置内核
make -j8 bzImage
编译内核
将编译好的内核镜像文件拷贝到前一篇文章中workspace
目录下。
- 编写测试程序
在 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;
}
- 编译测试程序
因为我们在 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
- 运行测试程序
我们要在 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 添加一个新的系统调用,以及如何测试系统调用。
- 系统调用是一个和CPU架构相关的概念,不同的CPU架构有不同的系统调用号。
- x86 架构下添加一个新的系统调用的步骤是:修改
arch/x86/entry/syscalls/syscall_64.tbl
文件,添加新的系统调用号;在include/linux/syscalls.h
文件中声明新的系统调用;在kernel/sys.c
文件中实现新的系统调用。