4. 用户空间和内核空间的交互
在解决了在内核空间置入可运行代码后,需要解决的是用户空间和内核空间的交互。具体来说,需要达到以下三个功能:用户空间的程序向内核空间下的程序控制,用户空间到内核空间的数据传递,内核空间到用户空间的数据传递。以下小节,都旨在利用系统提供给我们的各种接口,实现以上三个目标中的一个或几个。
4.1 printk
printk是内核用来记录系统运行日志的方法。对于用户,可以通过dmesg命令查看近期的系统日志信息,或者直接访问/var/log/kernel 查看内核输出的所有历史log。在kernel module中调用printk是最简单的传递信息到用户空间的方法。printk函数的使用方法和用户态下的printf类似,区别是可以通过 KERN_INFO等宏输出从0-7的指定级别的log信息。常见的使用方式如下:
char myname[] = "chinacodec/n";
printk(KERN_INFO "Hello, world %s!/n", myname);
4.2 伪字符设备
在linux中,用户对设备的操作往往被抽象为对文件的操作。利用这一特性,可以通过注册和实现伪字符设备到内核,来实现用户进程和内核空间的交互。当在用户空间执行对该伪设备的open/read/write/ioctl/mmap/release等操作时,这些被复用的系统调用就会使进程从用户态进入到内核态,从而在内核中完成事先注册的操作,当然可以包括对KAPI的调用等。
具体方法是,首先,在kernel module中通过register_chrdev注册一种伪字符设备到内核,参数包括:设备的major号,需要和系统已有设备不冲突;设备的名称name;文件操作函数集fops。register_chrdev的定义如下:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
其中,结构file_operations中包含一系列的函数指针,对应每种系统调用(包含read/write/open等),使用中,可以用如下的方式赋值(而不必为每一个元素都赋值),那些未显式赋值的元素被赋值为null。
static struct file_operations fops = {
.read = device_read,
.write = device_write,
.ioctl = device_ioctl,
.open = device_open,
.release = device_release
};
register_chrdev一般在kernel module中的init函数中执行,当insmod这个module时,设备就被注册到内核中。然后,用户就可以通过mknod命令可以创建对应的字符设备文件。然后通过open/write/read/ioctl/mmap/release等系统调用以访问设备文件的方式,访问该设备。每种调用都会执行到在注册设备时注册的对应的文件操作函数。此外,对应于register_chrdev,从内核卸载该伪设备驱动的函数为 unregister_chrdev。
以ioctl为例,当以上面的file_operations注册了伪字符设备后,当用户对伪设备文件执行ioctl后,调用会进入内核态,执行 device_ioctl函数。如果我们在自定义的device_ioctl函数中去调用KAPI,就实现了用户进程对KAPI的访问。
4.3 普通文件读写
内核态中,可以完成对用户文件系统任意文件的访问。因此,可以在内核态将要输出的信息写入文件,写入后用户态程序直接读取文件就可以完成从内核空间向用户空间的数据传递。但是在内核态下,对文件进行访问的调用函数和用户态下的系统调用有所区别。通常的使用方法是通过filp_open打开文件,然后利用获得的文件指针得到文件操作函数,以读取和写入文件,基本代码如下:
tmp _filp = filp_open(dst_file_name, O_RDWR | O_CREAT, 00);
tmp _copied = dst_filp->f_op->write(tmp_filep, buffer, size, offset);
tmp _len = dst_filp->f_op->read(tmp _filep, buffer, size, offset);
因为是在内核中对文件操作,所以为了通过系统调用中对缓冲区内存地址参数的检查,需要修改检查允许范围。方法是在read或write操作前通过set_fs扩大允许空间,操作后再通过setfs恢复到此前的允许范围,具体方法是:
orig_fs = get_fs();
set_fs(KERNEL_DS);
//write or read
set_fs(orig_fs);
4.4 Proc文件系统
proc文件系统,是当前内核或内核模块,和用户交互的主要方式,它通过将虚拟的文件系统挂载在/proc下,利用虚拟文件读写在用户和内核态间传递信息。通过内核模块,可以向/proc下注册新的文件,指定用户读写该文件时的回调函数;这样,当用户读写该文件时,工作在内核态的回调函数就可以执行信息交互的有关工作。
向内核中注册/proc下文件的调用是create_proc_entry,创建中需要指定文件名,访问权限和父节点名,返回为指向 proc_dir_entry结构的指针。通过该返回指针,可以进一步修改文件的用户id,组id,绑定的内核数据等;但最为关键的是可以指定用户读或写该文件时,在内核中被执行的回调函数。下面是一个向proc文件系统中注册新文件的示例:
static int __init proc_module_init(void){
entry = create_proc_entry("astring", 0644, myprocroot);
if (entry) {
entry->data = &string_var;
entry->read_proc = &string_read_proc;
entry->write_proc = &string_write_proc;
}
return 0
}
static void __exit procfs_exam_exit(void){
remove_proc_entry("astring", myprocroot);
remove_proc_entry("myproctest", NULL);
}
//read proc
int string_read_proc(char *page, char **start, off_t off,
int count, int *eof, void *data){
count = sprintf(page, "%s", (char *)data);
return count;
}
//write proc
int string_write_proc(struct file *file, const char __user *buffer,
unsigned long count, void *data){
if (count > STR_MAX_SIZE) {
count = 255;
}
copy_from_user(data, buffer, count);
return count;