摘自【linux内核修炼之道 第五章】
为 Linux 添加新的系统调用是件相对容易的事情,主要包括有 4 个步骤:编写系统调用服务例程;添
加系统调用号;修改系统调用表;重新编译内核并测试新添加的系统调用。下面以一个并无实际用处的 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
10
goto error;
}
11
12 printk(“Hello, %s!\n”, name);
13 return 0;
14 error:
15
return ret;
}
第二个 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 <unistd.h>
01 #include <sys/syscall.h>
02 #include <sys/types.h>
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!
由执行结果可见,系统调用添加成功。
什么时候需要添加新的系统调用
虽说添加一个新的系统调用非常简单,但这并不意味着用户有必要这么做。添加系统调用需要修改内
核源代码、重新编译内核,如果更进一步希望自己添加的系统调用能够得到广泛的应用,就需要得到官方
的认可并分配一个固定的系统调用号,还需要将该系统调用在每个需要支持的体系结构上实现。因此我们
最好使用其他替代方法和内核交换信息,如下所示。
使用设备驱动程序。创建一个设备节点,通过 read 和 write 函数进行读写访问,使用 ioctl 函数进行设
置操作和获取特定信息。
这种方法最大的好处在于可以模块式加载卸载,
避免了编译内核等过程,
而且调用接口固定,容易操作。
使用 proc 虚拟文件系统。利用 proc 接口获取系统运行信息和修订系统状态是一种很常见的手段,比
如读取/proc/cpuinfo 可以获得当前系统的 CPU 信息,
通过设备驱动提供的 proc 接口还可以设置硬
件寄存器。
sysfs 文件系统。sysfs 文件系统在 2.6 内核被引入,是一个类似于 proc 文件系统的特殊文件系统,用于对
系统的设备进行管理,它把实际连接到系统上的设备和总线组织成层次结构,并向用户提供详细的内核数
据结构信息,用户可以利用这些信息以实现和内核的交互