1、背景
压缩卡驱动提供给文件系统KAPI,供文件系统对文件数据进行压缩和解压。在测试中,最初采用的方法是通过文件系统提供的系统调用,利用文件系统在处理系统调用时,会调用到驱动的KAPI,来完成对压缩卡KAPI及其更下层(包含硬件)正确性的测试。考虑到这种方法,可能会由于文件系统对KAPI的具体使用方式而屏蔽一些问题的发现,因此展开了对KAPI的直接测试。由于KAPI是内核态的接口,无法在用户态直接调用,因此要最终完成对KAPI的更直接测试,需要借助编写内核模块(Kernel
Module),来实现用户进程对KAPI的访问;此外,还要解决用户态和内核态两者的交互。下图中表示了引入基于内核模块的测试方法,可以使我们的测试程序在调用关系上更加接近被测模块,从而在测试效率和覆盖性上得到改进。
在本文中,将主要介绍实现基于内核模块的测试的相关知识与代码编写方式。
2、用户态和内核态
操作系统中,虚拟内存被分为内核空间和用户空间。内核空间被用于内核,内核扩展功能和一些设备驱动的运行;用户空间是用户进程在用户态下运行时所使用。在
linux系统中,内核拥有地址为3G到4G的内存空间,并且它是被共享的,而用户进程拥有0到3G的用户空间,每个用户进程拥有独立的空间。
CPU根据PSW寄存器中的模式bit,可以工作在用户态(Ring 3)或内核态(Ring
0),用户态能执行有限的非特权的CPU指令。在内核态可以执行所有指令。工作在用户态的进程无法直接访问硬件和内核的内存空间,这是出于运行安全的考虑。当然,操作系统也会也提供系统调用或中断等方法,让用户进程可以切换到内核态,以访问内核空间或硬件。系统调用或中断,可以看作是有限度的开放给用户进程对内核和硬件的访问,从而保证了系统安全。
在驱动项目中,驱动程序工作在内核空间中,它直接提供KAPI给内核,这样我们就无法通过用户进程完成对该KAPI的测试。当然,通过执行内核提供的系统调用,当发生磁盘读写时可以间接的调用到该KAPI,但是内核对其有限或具体的调用方式往往屏蔽了底层驱动的部分缺陷,这种测试方法也带来发现问题后追查定位的困难。要改变这些不足,最直接的思路就是跨越对内核特殊逻辑间接调用的依赖,通过直接对KAPI进行调用的方法完成测试。
为了实现对工作在内核态的KAPI的访问,必须向内核置入内核态的代码以发起调用,这将通过kernel
module来完成,会在第三部分具体介绍。而为了实现对测试数据输入,执行发起,结果输出采集等的灵活控制,还必须实现用户态和内核态的交互,具体方法将在第四部分介绍。
3. Kernel Module
要达成对KAPI的直接调用和测试,需要添加工作在内核态的代码实现对该函数的访问,这里我们借助kernel
module来完成。kernel module提供了不需要重新编译kernel image,就向内核引入新代码和逻辑的支持。Kernel
Module通常是以ko为扩展名的文件,通过lsmod命令可以查看已经加载的Kernel Module,通过insmod/rmmod命令完成指定的的模块的加载和卸载。Kernel
Module最基本的编程模式是编程者通过宏定义module_init和module_exit,指定在模块加载和卸载时进行的初始化或清理等工作。一个基本的hello
world程序如下。向init中就可以加入对KAPI进行调用的函数,当模块被insmod时就会在内核态被执行。但是这与灵活的完成各种测试任务还有差距,为了能在用户态完成测试数据的准备和输入,测试结果的获取和对执行的控制,还需要引入用户态和内核态的交互。关于Kernel
Module更详细的编译方式和编程模式介绍,可以参考 http://www.tldp.org/LDP/lkmpg/2.6/html/
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
#include <linux/init.h> /* Needed for the macros */
static int __init hello_init(void){
printk(KERN_INFO "Hello, world 2\n");
return 0;
}
static void __exit hello_exit(void){
printk(KERN_INFO "Goodbye, world 2\n");}
module_init(hello_init);
module_exit(hello_exit); |
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;
} |
4.5 af_netlink
netlink是一种特殊的socket,用于用户态与内核态的双向通讯。在实现用户和内核交互的各种方式中,netlink的主要特点得意于它继承了
socket的一些基本特性,包括异步通讯,多播,双向性,不需要额外的文件。在用户态中,netlink的使用与标准的socket
API相同,在内核态,则需要使用专门的API。下面介绍具体的使用方法:
在用户态中,首先通过要创建socket,其中指定domain必须为AF_NETLINK,协议为通常SOCK_RAW,协议类型为NETLINK_GENERIC或其它自定义类型
sd = socket(AF_NETLINK, SOCK_RAW,NETLINK_GENERIC);
然后通过bind绑定源端的地址,地址结构定义如下,其中nl_family为AF_NETLINK,nl_pad
目前无用填充0,nl_pid为进程id,若为0代表内核;nl_groups用于组播时的组号。
struct sockaddr_nl {
sa_family_t nl_family;
unsigned short nl_pad;
__u32 nl_pid;
__u32 nl_groups;
} saddr;
bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)); |
通过sendmsg可以发送消息msg到指定的地址。
ret = sendmsg(sd, &msg, 0);
在msg的所有元素中,msg_name需要指向一个sockaddr_nl
结构的首地址,用来表示发送的目的端的地址,如果是发送到内核,其中的nl_pid字段置为0;msg_iov是要发送消息集合的向量,向量中的每一项代表一条消息。每一项指向数据的首部为一个nlmsghdr结构,其字段定义了该条消息长度,消息类型,序号,发送者进程id等;随后跟随的是消息的主体数据部分。
当要接收消息时,通过recvmsg可以获得类似的消息向量,从而获得数据及发送者等有关信息。
在内核态,通过netlink_kernel_create可以在内核中新建socket结构并注册接收到消息的回调函数input,其原型为:
struct sock * netlink_kernel_create(int
unit, void (*input)(struct sock *sk, int len));
当接收到消息时,回调函数input中的sk指针就指向了刚刚创建的socket在内核中的结构,通过对该结构的访问,可以获得要接收的数据。一种基本的input实现如下:
void input (struct sock *sk, int len) {
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *data = NULL;
while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) {
nlh = (struct nlmsghdr *)skb->data;
data = NLMSG_DATA(nlh);
}
} |
此外,sock_release是在内核中释放socket的方法;而通过netlink_unicast和netlink_broadcast可以在内核中令netlink
socket发送数据,具体的方法可以参考 http://blog.chinaunix.net/u/19940/showart_144827.html
4.6 其它方法
以上所介绍的方法具有一个共同特点,它们不需要较高版本的内核支持,添加新的功能不需要重新编译内核或替代内核中的原有功能。当然,还有一些其它方法,可能会需要内核版本或重新编译内核等条件的支持,但同样能达到用户态和内核态交互这一目标,比如修改或添加新的系统调用,或利用sysfs,relayfs
等特殊的虚拟文件系统。这里不再一一介绍。
5 总结
本文主要解决了如何对内核态的函数接口进行测试,其中包括置入内核态代码以调用到内核API接口的方法,使测试程序从用户态进入内核态的方法,以及如何实现用户空间和内核空间的交互等。目前,已经基于上述方法实现了对压缩卡KAPI的直接测试,测试代码已经应用到压缩卡基本功能(压缩和解压)的测试,异常测试和压力测试。未来将会视需求实现或完善出更通用的,使用方式也更为灵活的内核态接口的测试工具。 |