一、引言:机制与策略分离的现状
机制——mechanism,需要提供什么样的功能;
策略——policy,怎样实现这些功能。
首先,我们从操作系统的角度出发,来了解一下什么是“机制与策略分离”的思想。
简单来说,操作系统是处于硬件与应用程序之间的一个中间层。这样来看,操作系统同时承担着两个角色,一个角色是对硬件资源的管理者,另一个角色则是对应用程序的服务者。
对于操作系统与硬件之间的关系来说,硬件提供了许多功能(也就是提供了机制),而操作系统它屏蔽掉了硬件的细节,给予了具体的实现(也就是提供了策略);
对于操作系统与应用程序之间的关系来说,操作系统提供了许多功能(也就是提供了机制),而应用程序它屏蔽了操作系统的细节,给予了具体的实现(也就是提供了策略)。
我们来设想一下,如果不采用这种“机制与策略分离”的思想,而是将机制与策略揉成一团,会有什么样的后果?
很明显,策略会变得很死板,很难去适应用户需求的改变;并且任何策略上的改变都非常有可能会动摇机制。
以操作系统的角度出发,很容易将“机制与策略分离”的思想理解成为了不同层面与它的底层之间的关系(一个层面将它的底层封装起来,实现底层对上层的透明)。其实这样也有一定的道理,但我却认为这种思想不应该仅仅局限于分层的概念之中,即使是同一个层次的编程问题,也是可以使用“机制与策略分离”的思想的。
现在使用“机制与策略分离”思想的方法有:
将应用按照一个库来编写,例如这个库包括许多由内嵌脚本语言驱动的C服务程序,而至于整个应用的控制流程则使用脚本来撰写的,而不是C语言;
将应用程序分为可以协作的前端和后端进程,前端实现策略,后端实现机制。比起仅用单个进程的整体实现方式来说,这种设计方式大大降低整体复杂度。
二、内核模块的设计思想分析
(1). 内核
内核,是一个操作系统的核心,按照体系结构可分为两类:微内核(Micro Kernel)和单内核(Monolithic Kernel)。
在微内核架构中,内核只提供操作系统的核心功能,如实现进程管理、存储器管理、进程间通信、I/O设备管理等,而其他的应用层IPC、文件系统功能、设备驱动模块则不被包含到内核功能中,属于微内核之外的模块,所以针对这些模块的修改不会影响到微内核的核心功能。
单内核架构则是将上述包括微内核以及微内核之外的应用层IPC、文件系统功能、设备驱动模块都编译成一个整体。其优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。Linux操作系统正是采用了单内核结构,为了解决这一缺点,Linux中引入了内核模块这一机制。
(2). 内核模块
内核模块,全称Loadable KernelModule(LKM),是一种在内核运行时加载一组目标代码来实现某个特定功能的机制。
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行,在运行时它被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不一样的。模块由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序和其他内核上层功能。
内核模块具备如下特点:
1. 模块本身不被编译进内核映像,这控制了内核的大小;
2. 模块一旦被加载,它就和内核中的其它部分完全一样。
模块机制使内核预编译时不必包含很多无关功能,把内核做到最精简,后期可以根据需要进行添加;
针对驱动程序,因为涉及到具体的硬件,很难使用通用的,且其中可能包含了各个厂商的私密接口,厂商几乎不会允许开发者把源代码公开,这就和Linux的许可相悖,模块机制很好地解决了这个冲突,允许驱动程序后期进行添加,而不合并到内核中。
(3). 内核模块的机制及数据结构
类似于普通的可执行文件,内核模块经过编译后得到的.ko文件,也是一个可重定位目标文件。
既然是可重定位文件,在把模块加载到内核的时候就需要进行重定位。(如果一个程序的可执行文件总能加载到自己的理想位置,一般不需要重定位。对于动态库文件,库文件格式是一致的,但是可能需要加载多个库文件,那么有些库文件必然无法加载到自己的理想位置,就需要进行重定位。而内核模块由于和内核共享同一个内核地址空间,更不能保证自己的理想地址不被占用,所以一般情况内核模块需要进行重定位。)
还有一个重要的工作即是解决模块之间的依赖,模块A中引用了其他模块的函数,那么在加载到内核之前,模块A并不知道所引用的函数地址,因此只能做一个标记,在加载到内核的时候再根据符号表解决引用问题。
这些都是在加载内核模块的核心——系统调用sys_init_module中完成的。
数据结构module
每一个内核模块在内核中都对应一个数据结构struct module,所有的模块通过一个链表维护。
以下为struct module的部分注释:
Structmodule{ Enum module_state state; /* 所有的模块构成双链表,表头为全局变量modules */ Struct list_head list; /* 模块名字,唯一,一般存储去掉.ko的部分 */ Char name[MODULE_NAME_LEN]; /* 导出符号信息,指向一个kernel_symbol的数组,有num_syms个表项 */ Const struct kernel_symbol *syms; /* 同样有num_syms个表项,不过存储的是符号的校验和 */ Const unsigned long *crcs; Unsigned int num_syms; /* 具体意义同上面的符号,不过这里只适用于GPL兼容的模块 */ Unsigned int num_gpl_syms; Const struct kernel_symbol *gpl_syms; Const unsigned long *gpl_crcs; /* 模块初始化函数的指针*/ Int (*init) (void); /* 如果该函数不为空,则init结束后就可以调用进行适当释放 */ Void *module_init; /* 核心数据和代码部分,在卸载的时候会调用 */ Void *module_core; /* 对应于上面的init和core函数,决定各自占用的大小 */ Unsigned int init_size, core_size;#ifdefCONFIG_MODULE_UNLOAD /* 模块间的依赖关系记录*/ Struct list_head source_list; Struct list_head target_list; /* 等待队列,记录哪些进程等待模块被卸载 */ Struct task_struct *waiter; /* 卸载退出函数,模块中定义的exit函数 */ Void (*exit) (void); ...};
数据结构modul_use
模块间的依赖关系通过两个结点source_list和target_list记录,前者记录哪些模块依赖于本模块,后者记录本模块依赖于哪些模块。
结点通过module_use记录,module_use如下:
Structmodule_use { Struct list_head source_list; Struct list_head target_list; Struct module *source, *target;};
每个module_use记录一个映射关系,注意这里把source和target放在一个一个结构里,因为一个关系需要在源模块和目标模块都做记录。
如果模块A依赖于模块B,则生成一个module_use结构,其中source_list字段链入模块B的module结构的source_list链表,而source指针指向模块A的module结构;而target_list链入模块A的target_list链表,target指针指向模块B的模块结构。
参考下面的代码:
Staticint add_module_usage(struct module *a, struct module *b){ Struct module_use *use; Pr_debug(“Allocating new usage for %s.\n”,a->name); Use = kmalloc(sizeof(*use), GFP_ATOMIC); If (!use) { Printk(KREN_WARNING “%s: out of memoryloading\n”, a->name); Return –ENOMEM; } Use->source = a; List_add(&use->source_list,&b->source_list); List_add(&use->target_list,&a->target_list); Return 0;}
数据结构kernel_symbol
内核模块几乎不会作为完全独立的存在,均需要引用其他模块的函数,而这一机制就是由符号机制保证的。
参考module数据结构,有:
Conststruct kernel_symbol *syms;
Syms指针指向一个符号数组,也可以称之为符号表,不过是局部的符号表。
下面是kernel_symbol结构:
Structkernel_symbol{ Unsigned long value; Const char *name;};
结构很简单,value记录符号地址,而name自然就是符号名字。
借助find_symbol函数可以解决尚未引用的符号:
Conststruct kernel_symbol *find_symbol(const char *name,Struct module **owner,Const unsigned long **crc,Bool gplok,Bool warn){ Structfind_symbol_arg fsa; Fss.name= name; Fss.gplok= gplok; Fss.warn= warn; If(each_symbol_section(find_symbol_in_section, &fss)) { If(owner) *owner= fss.owner; If(crc) *crc= fss.crc; Returnfss.sym; } Pr_debug(“Failedto find symbol %s\n”, name); ReturnNULL; }
首先把参数信息封装成一个find_symbol_arg结构,然后调用了each_symbol_section,并传入在section中查找symbol的函数find_symbol_in_section。
三、内核模块与哈希表的应用
实验目的:
使用内核模块,获取CPU的时间信息并将其存储在一个hash表中,然后对该hash表进行一次输出。
卸载该模块的时候,删去该hash表的结点。
实验代码:
第一部分:hashlist.c
#include#include#include#include#include#include#include#includeMODULE_LICENSE("GPL");structcpulist{ struct hlist_head head;};structcpunode{ int cpu; u64 user; u64 nice; u64 system; u64 idle; u64 iowait; u64 irq; u64 softirq; u64 steal; u64 guest; u64 guest_nice; struct hlist_node node;};structcpulist cpuhead;staticint __init hlist_init(void){ struct hlist_node *pos; struct cpunode *listnode; struct cpunode *p; int i; INIT_HLIST_HEAD(&(cpuhead.head)); for_each_online_cpu(i) { struct kernel_cpustat *kcs =&kcpustat_cpu(i); listnode = (struct cpunode *)kmalloc(sizeof(structcpunode), GFP_KERNEL); listnode->cpu = i; listnode->user =kcs->cpustat[CPUTIME_USER]; listnode->nice =kcs->cpustat[CPUTIME_NICE]; listnode->system =kcs->cpustat[CPUTIME_SYSTEM]; listnode->idle =kcs->cpustat[CPUTIME_IDLE]; listnode->iowait =kcs->cpustat[CPUTIME_IOWAIT]; listnode->irq =kcs->cpustat[CPUTIME_IRQ]; listnode->softirq =kcs->cpustat[CPUTIME_SOFTIRQ]; listnode->steal =kcs->cpustat[CPUTIME_STEAL]; listnode->guest =kcs->cpustat[CPUTIME_GUEST]; listnode->guest_nice =kcs->cpustat[CPUTIME_GUEST_NICE]; hlist_add_head(&(listnode->node),&(cpuhead.head)); } hlist_for_each(pos, &(cpuhead.head)) { p = hlist_entry(pos, struct cpunode,node); printk("cpu: %d\nuser: %lld\nnice:%lld\nsystem: %lld\nidle: %lld\niowait: %lld\nirq: %lld\nsoftirq: %lld\nsteal:%lld\nguest: %lld\nguest_nice: %lld\n\n", p->cpu, p->user,p->nice, p->system, p->idle, p->iowait, p->irq, p->softirq,p->steal, p->guest, p->guest_nice); } return 0;}staticvoid __exit hlist_exit(void){ struct hlist_node *pos, *n; int i; struct cpunode *p; hlist_for_each_safe(pos, n,&(cpuhead.head)) { hlist_del(pos); p = hlist_entry(pos, struct cpunode,node); i = p->cpu; kfree(p); printk("cpu: %d removed...\n",i); }}module_init(hlist_init);module_exit(hlist_exit);
第二部分:Makefile
obj-m:=hashlist.oKDIR:=/lib/modules/$(shell uname -r)/buildPWD:=$(shellpwd)default: $(MAKE) -C $(KDIR) M=$(PWD) modulesclean: $(MAKE) -C $(KDIR) M=$(PWD) clean
四、实验结果分析:
加载结果:
卸载结果:
结果分析:
这个模块的主要功能其实是复现了/proc/stat/的部分功能,结果成功。
通过这个模块的编写,感受最深刻的应该就是内核模块也可以使用内核中的一些函数,这样对我们的书写和功能的实现都有很大的帮助,比如我在该模块的书写中,使用了很多内核中定义好的遍历cpu的宏函数。
还有就是对hash表这种数据结构的理解也更上一层楼,这种数据结构的使用很方便,设计很有艺术性,通常都是内嵌在一个结构体中,主结构体和成员都可以依靠偏移量来寻找对方,想法很棒。
五、结论
内核模块的确很方便,可以在内核运行时动态加载和卸载,但无疑也付出了一定的代价,内核模块之间的许多关系都要依靠内核创建新的数据结构来描述和管理,这无疑增加系统的负担。
但是,从现实的情况来看,内核模块的小瑕疵和优点比起来简直不值一提,它让驱动开发这些工作更加方便,避免了耗时严重的整体编译,大大提升了效率和创造力。
这只是一方面,内核模块的引入,其实也是一种“机制与策略分离”的思想。开发者实现的一组内核模块,几乎不会受到策略的限制,它们可以在内核中创造更高级的机制,让用户程序实现更高级的策略。