用户态的程序如果想访问内核态的程序,并不能直接访问,而是通过中断处理机制来完成的。CPU提供一个陷入指令(Traps),亦称访管指令。用户态程序通过syscall附加系统调用号以及参数来完成对内核接口的调用。
syscall是glibc封装的接口,其底层实现主要是发起一个陷入指令中断int $0x80,然后内核处理处理此中断函数。
系统调用号,是一系列从0开始递增的数字,其实质是一组数组的下标,而这个数组存放一内核接口函数的地址。这样在内核响应陷入指令后,就可以通过系统调用号找到相应的函数地址,然后来调用相应的内核函数。
不同架构的实现略有不同,此处主要针对x86_64架构。在相关模块中添加源文件fio_test.c(此处添加到fs目录下),并添加相应的代码:
#include <linux/init.h>
#include <linux/syscalls.h>
#include <linux/time_namespace.h>
#define KTP_COUNT 128
struct timespec64 kernel_tp[KTP_COUNT] = {0};
void clock_time_ex(size_t idx)
{
if (idx >= KTP_COUNT)
return;
ktime_get_ts64(&kernel_tp[idx]);
timens_add_monotonic(&kernel_tp[idx]);
}
SYSCALL_DEFINE2(get_fio_time, struct __kernel_timespec __user *, tp, size_t, idx)
{
int error = 0;
if (idx >= KTP_COUNT)
return -EINVAL;
printk("hello World!\n");
clock_time_ex(idx);
if (put_timespec64(&kernel_tp[idx], tp))
error = -EFAULT;
return error;
}
SYSCALL_DEFINE2宏展开大概是:
asmlinkage long sys_get_fio_time(struct __kernel_timespec __user * tp, size_t idx);
将源文件添加到fs目录下makefile文件。
makefile此处是利用隐式编译,即只需要知道目录文件名,就会自动去找同名的源文件并进行编译。
函数声明一般放在/include/linux/syscalls.h文件中,也可以放在arch/x86目录下的syscalls.h文件中。
其中的asmlinkage是一个必需的限定词,用于通知gcc编译器从堆栈中提取函数的参数,而不是从寄存器传递。这样无论是标准C还是汇编代码,都可以调用此函数。
asmlinkage long sys_get_fio_time(const __kernel_timespec __user * tp, size_t idx);
不同的架构,会有不同的系统调用表文件,此处针对x86_64,则位于/arch/x86/entry/syscalls目录,目录中有两个文件,分别为syscall_32.tbl和syscall_64.tbl,前者针对i386架构的cpu,后者针对的是x86_64架构的cpu。此处在syscall_64.tbl中添加。函数声明和syscall.tbl一起组成一个索引对应函数地址的数组。因为Linux内核文件为elf文件,其中不包括函数符号,只能直接调用函数地址。为了更方便地调用函数地址,提供一个系统调用号来与函数地址配对。
syscall.tbl中的abi类型,有i386,64,x32,分别对应gcc的-m32,-m64,mx32。abi类型还有一个common,其兼容-m64和-mx32编译的版本。默认一般填写common即可。
下面是直接编译内核,并将相关文件拷贝到系统目录,并更新grub默认项为当前编译版本。
sudo make -j6
sudo mkinitramfs /lib/modules/5.17.12 -o /boot/initrd.img-5.17.12
sudo cp arch/x86/boot/bzImage /boot/vmlinuz-5.17.12
sudo cp System.map /boot/System.map-5.17.12
sudo update-grub
335是函数get_fio_time对应的系统调用号,glibc封装了一个标准函数syscall可以调用系统调用号。
可以直接通过系统调用号来调用:
struct timespec ts = {0};
syscall(335, &tS, 0);
可以参考glibc将其封装为一个接口,方便调用:
long get_fio_time(struct timespec *tp, int idx)
{
return syscall(335, tp, idx);
}
完整的测试代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <time.h>
long get_fio_time(struct timespec *tp, int idx)
{
return syscall(335, tp, idx);
}
int main(void)
{
char buf[256] = {0};
struct timespec ts = {0};
get_fio_time(&ts, 0);
printf("time1:%ld-%lu\n",ts.tv_sec, ts.tv_nsec);
clock_gettime(1, &ts);
printf("time2:%ld-%lu\n",ts.tv_sec, ts.tv_nsec);
return 0;
}
get_fio_time的作用与clock_gettime一样的。
我们还可以在在dmesg内核日志中看到内核接口中的hello World的打印信息。