《Linux内核修炼之道》第5章讲解系统调用,它是应用程序和内核间的桥梁,学习并理解它是我们走向内核的一个很好的过渡。本节为大家介绍如何实现一个新的系统调用。
一个系统调用的实现并不需要去关心如何从用户空间转换到内核空间,以及系统调用处理程序如何去执行,你需要做的只是遵循几个固定的步骤。
为Linux添加新的系统调用是件相对容易的事情,
主要包括有4个步骤:
编写系统调用服务例程(内核函数);添加系统调用号;修改系统调用表;重新编译内核并测试新添加的系统调用。
https://www.jianshu.com/p/5e96fd50f3b5 可以看到内核函数、系统调用号、系统调用表之间的关系。
- 下面以一个并无实际用处的hello系统调用为例,来演示上述几个步骤。
(1)编写系统调用服务例程。
遵循前面所述的几个原则,hello系统调用的服务例程实现为:
01 asmlinkage long sys_hello(void)
02 {
03 printk("Hello!\n");
04 return 0;
05 }
通常,应该为新的系统调用服务例程创建一个新的文件进行存放,但也可以将其定义在其他文件之中并加上注释做必要说明。同时,还要在include/linux/syscalls.h文件中添加原型声明:
asmlinkage long sys_hello(void);
sys_hello函数非常简单,仅仅打印一条语句,并没有使用任何参数。如果我们希望hello系统调用不仅能打印"hello!"欢迎信息,还能够打印出我们传递过去的名称,或者其他的一些描述信息,则sys_hello函数可以实现为:
01 asmlinkage long sys_hello(const char __user *_name)
02 {
03 char *name;
04 long ret;
05
06 name = strndup_user(_name, PAGE_SIZE);
07 if (IS_ERR(name)) {
08 ret = PTR_ERR(name);
09 goto error;
10 }
11
12 printk("Hello, %s!\n", name);
13 return 0;
14 error:
15 return ret;
16 }
第二个sys_hello函数使用了一个参数,在这种有参数传递发生的情况下,编写系统调用服务例程时必须仔细检查所有的参数是否合法有效。因为系统调用在内核空间执行,如果不加限制任由用户应用传递输入进入内核,则系统的安全与稳定将受到影响。
参数检查中最重要的一项就是检查用户应用提供的用户空间指针是否有效。比如上述sys_hello函数参数为char类型指针,并且使用了__user标记进行修饰。__user标记表示所修饰的指针为用户空间指针,不能在内核空间直接引用,原因主要如下。
用户空间指针在内核空间可能是无效的。
用户空间的内存是分页的,可能引起页错误。
如果直接引用能够成功,就相当于用户空间可以直接访问内核空间,产生安全问题。
因此,为了能够完成必须的检查,以及在用户空间和内核空间之间安全地传送数据,就需要使用内核提供的函数。比如在sys_hello函数的第6行,就使用了内核提供的strndup_user函数(在mm/util.c文件中定义)从用户空间复制字符串name的内容。
(2)添加系统调用号。
每个系统调用都会拥有一个独一无二的系统调用号,所以接下来需要更新include/asm-i386/unistd.h文件,为hello系统调用添加一个系统调用号。
328 #define __NR_utimensat 320
329 #define __NR_signalfd 321
330 #define __NR_timerfd 322
331 #define __NR_eventfd 323
332 #define __NR_fallocate 324
333 #define __NR_hello 325 /*分配hello系统调用号为325*/
334
335 #ifdef __KERNEL__
336
337 #define NR_syscalls 326 /*将系统调用数目加1修改为326*/
(3)修改系统调用表。
为了让系统调用处理程序system_call函数能够找到hello系统调用,我们还需要修改系统调用表sys_call_table,放入服务例程sys_hello函数的地址。
322 .long sys_utimensat /* 320 */
323 .long sys_signalfd
324 .long sys_timerfd
325 .long sys_eventfd
326 .long sys_fallocate
327 .long sys_hello /*hello系统调用服务例程*/
新的系统调用hello的服务例程被添加到了sys_call_table的末尾。我们可以注意到,sys_call_table每隔5个表项就会有一个注释,表明该项的系统调用号,这个好习惯可以在查找系统调用对应的系统调用号时提供方便。
(4)重新编译内核并测试。
为了能够使用新添加的系统调用,需要重新编译内核,并使用新内核重新引导系统。然后,我们还需要编写测试程序对新的系统调用进行测试。针对hello系统调用的测试程序如下:
00 #include
01 #include
02 #include
03
04 #define __NR_hello 325
05
06 int main(int argc, char *argv[])
07 {
08 syscall(__NR_hello);
09 return 0;
10 }
然后使用gcc编译并执行:
$gcc -o hello hello.c
$./hello
Hello!
由执行结果可见,系统调用添加成功。