一、模块传参
module_param(name,type,perm);//将指定的全局变量设置成模块参数 /* name:全局变量名 type: 使用符号 实际类型 传参方式 bool bool insmod xxx.ko 变量名=0 或 1 invbool bool insmod xxx.ko 变量名=0 或 1 charp char * insmod xxx.ko 变量名="字符串内容" short short insmod xxx.ko 变量名=数值 int int insmod xxx.ko 变量名=数值 long long insmod xxx.ko 变量名=数值 ushort unsigned short insmod xxx.ko 变量名=数值 uint unsigned int insmod xxx.ko 变量名=数值 ulong unsigned long insmod xxx.ko 变量名=数值 perm:给对应文件 /sys/module/name/parameters/变量名 指定操作权限 一般用0664 #define S_IRWXU 00700 #define S_IRUSR 00400 #define S_IWUSR 00200 #define S_IXUSR 00100 #define S_IRWXG 00070 #define S_IRGRP 00040 #define S_IWGRP 00020 #define S_IXGRP 00010 #define S_IRWXO 00007 #define S_IROTH 00004 #define S_IWOTH 00002 //不要用 编译出错 #define S_IXOTH 00001 */
module_param_array(name,type,&num,perm); /* name、type、perm同module_param,type指数组中元素的类型 &num:存放数组大小变量的地址,可以填NULL(确保传参个数不越界) 传参方式 insmod xxx.ko 数组名=元素值0,元素值1,...元素值num-1 */
可用MODULE_PARAM_DESC宏对每个参数进行作用描述,用法:
MODULE_PARM_DESC(变量名,字符串常量);
字符串常量的内容用来描述对应参数的作用
modinfo可查看这些参数的描述信息
#include <linux/module.h> #include <linux/kernel.h> int gx = 10; int *gstr = "hello"; int garr[5] = {1,2,3,4,5}; module_param(gx,int,0664); module_param(gstr,charp,0664); module_param_array(garr,int,NULL,0664); int __init testparam_init(void) { int i = 0; printk("gx=%d\n",gx); printk("gstr=%s\n",gstr); for(i = 0; i < 5; i++) { printk("%d",garr[i]); } printk("\n"); return 0; } void __exit testparam_exit(void) { printk("testparam will exit\n"); } MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ming qiang Tian"); MODULE_DESCRIPTION("It is only a simple test"); MODULE_ALIAS("HI"); module_init(testparam_init); module_exit(testparam_exit);
二、模块依赖
既然内核模块的代码与其它内核代码共用统一的运行环境,也就是说模块只是存在形式上独立,运行上其实和内核其它源码是一个整体,它们隶属于同一个程序,因此一个模块或内核其它部分源码应该可以使用另一个模块的一些全局特性。
一个模块中这些可以被其它地方使用的名称被称为导出符号,所有导出符号被填在同一个表中这个表被称为符号表。
最常用的可导出全局特性为全局变量和函数
查看符号表的命令:nm nm查看elf格式的可执行文件或目标文件中包含的符号表,用法:
nm 文件名
(可以通过man nm查看一些字母含义)
nm xx.ko
两个用于导出模块中符号名称的宏:
EXPORT_SYMBOL(函数名或全局变量名) EXPORT_SYMBOL_GPL(函数名或全局变量名) 需要GPL许可证协议验证
使用导出符号的地方,需要对这些符号进行extern声明后才能使用这些符号
B模块使用了A模块导出的符号,此时称B模块依赖于A模块,则:
-
编译次序:先编译模块A,再编译模块B,
-
当两个模块源码在不同目录时,需要:
i. 先编译导出符号的模块A
ii. 拷贝A模块目录中的Module.symvers到B模块目录
iii. 编译使用符号的模块B。否则编译B模块时有符号未定义错误
-
加载次序:先插入A模块,再插入B模块,否则B模块插入失败
-
卸载次序:先卸载B模块,在卸载A模块,否则A模块卸载失败
补充说明: 内核符号表(直接当文本文件查看) /proc/kallsyms运行时 /boot/System.map编译后
nm vmlinux
#include <linux/module.h> #include <linux/kernel.h> int gx; EXPORT_SYMBOL(gx); int __init modulea_init(void) { printk("In module_a init gx=%d\n",gx); return 0; } void __exit modulea_exit(void) { printk("modulea will exit\n"); } MODULE_LICENSE("GPL"); module_init(modulea_init); module_exit(modulea_exit);
#include <linux/module.h> #include <linux/kernel.h> int gy; //EXPORT_SYMBOL(gy); extern int gx; int __init moduleb_init(void) { printk("In module_b init gx=%d\n",gx); return 0; } void __exit moduleb_exit(void) { printk("moduleb will exit\n"); } MODULE_LICENSE("GPL"); module_init(moduleb_init); module_exit(moduleb_exit);
ifeq ($(KERNELRELEASE),) ifeq ($(ARCH),arm) KERNELDIR ?= /home/linux/Linux_4412/linux-3.14 ROOTFS ?= /opt/4412/rootfs else KERNELDIR ?= /lib/modules/$(shell uname -r)/build endif PWD := $(shell pwd) modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules modules_install: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install clean: rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions else obj-m += modulea.o obj-m += moduleb.o endif
三、内核空间和用户空间
为了彻底解决一个应用程序出错不影响系统和其它app的运行,操作系统给每个app一个独立的假想的地址空间,这个假想的地址空间被称为虚拟地址空间(也叫逻辑地址),操作系统也占用其中固定的一部分,32位Linux的虚拟地址空间大小为4G,并将其划分两部分:
-
0~3G 用户空间 :每个应用程序只能使用自己的这份虚拟地址空间
-
3G~4G 内核空间:内核使用的虚拟地址空间,应用程序不能直接使用这份地址空间,但可以通过一些系统调用函数与其中的某些空间进行数据通信
实际内存操作时,需要将虚拟地址映射到实际内存的物理地址,然后才进行实际的内存读写
mmu之前是物理地址,之后是虚拟空间
四、执行流
执行流:有开始有结束总体顺序执行的一段独立代码,又被称为代码上下文
计算机系统中的执行流的分类:
执行流:
-
任务流--任务上下文(都参与CPU时间片轮转,都有任务五状态:就绪态 运行态 睡眠态 僵死态 暂停态)
-
进程
-
线程
-
内核线程:内核创建的线程
-
应用线程:应用进程创建的线程
-
-
-
异常流--异常上下文
-
中断
-
其它异常
-
应用编程可能涉及到的执行流:
-
进程
-
线程
内核编程可能涉及到的执行流:
-
应用程序自身代码运行在用户空间,处于用户态 ----------------- 用户态app
-
应用程序正在调用系统调用函数,运行在内核空间,处于内核态,即代码是内核代码但处于应用执行流(即属于一个应用进程或应用线程) ---- 内核态app
-
一直运行于内核空间,处于内核态,属于内核内的任务上下文 --------- 内核线程
-
一直运行于内核空间,处于内核态,专门用来处理各种异常 --------- 异常上下文
五、模块编程与应用编程的比较
不同点 | 内核模块 | 应用程序 |
---|---|---|
API来源 | 不能使用任何库函数 | 各种库函数均可以使用 |
运行空间 | 内核空间 | 用户空间 |
运行权限 | 特权模式运行 | 非特权模式运行 |
编译方式 | 静态编译进内核镜像或编译特殊的ko文件 | elf格式的应用程序可执行文件 |
运行方式 | 模块中的函数在需要时被动调用 | 从main开始顺序执行 |
入口函数 | init_module | main |
退出方式 | cleanup_module | main函数返回或调用exit |
浮点支持 | 一般不涉及浮点运算,因此printk不支持浮点数据 | 支持浮点运算,printf可以打印浮点数据 |
并发考虑 | 需要考虑多种执行流并发的竞态情况 | 只需考虑多任务并行的竞态 |
程序出错 | 可能会导致整个系统崩溃 | 只会让自己崩溃 |
六、内核接口头文件查询
大部分API函数包含的头文件在include/linux目录下,因此:
-
首先在include/linux 查询指定函数:grep 名称 ./ -r -n
-
找不到则更大范围的include目录下查询,命令同上
-
grep MODULE_AUTHOR ./ -r -n
day3作业
编写3个内核模块A、B、C,A依赖于B,B依赖于C,完成对它们的编译、运行、卸载
a
#include <linux/module.h> #include <linux/kernel.h> int gx = 20; EXPORT_SYMBOL(gx); int __init modulea_init(void) { printk("In module_a init gx=%d\n",gx); return 0; } void __exit modulea_exit(void) { printk("modulea will exit\n"); } MODULE_LICENSE("GPL"); module_init(modulea_init); module_exit(modulea_exit);
b
#include <linux/module.h> #include <linux/kernel.h> int gy = 30; EXPORT_SYMBOL(gy); extern int gx; int __init moduleb_init(void) { printk("In module_b init gx=%d\n",gx); printk("In module_b init gy=%d\n",gy); return 0; } void __exit moduleb_exit(void) { printk("moduleb will exit\n"); } MODULE_LICENSE("GPL"); module_init(moduleb_init); module_exit(moduleb_exit);
c
#include <linux/module.h> #include <linux/kernel.h> extern int gy; int __init moduleb_init(void) { printk("In module_c init gy=%d\n",gy); return 0; } void __exit moduleb_exit(void) { printk("moduleb will exit\n"); } MODULE_LICENSE("GPL"); module_init(moduleb_init); module_exit(moduleb_exit);
ifeq ($(KERNELRELEASE),) ifeq ($(ARCH),arm) KERNELDIR ?= /home/linux/Linux_4412/linux-3.14 ROOTFS ?= /opt/4412/rootfs else KERNELDIR ?= /lib/modules/$(shell uname -r)/build endif PWD := $(shell pwd) modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules modules_install: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install clean: rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions else obj-m += modulea.o obj-m += moduleb.o obj-m += modulec.o endif
day3.1
什么是内核空间什么是用户空间 他们之间怎么传递的?
在Linux中,操作系统和驱动程序运行在内核空间,应用程序运行在用户空间。 两者不能简单的使用指针传递数据,因为Linux使用的虚拟内核机制,当内核空间使用用户空间指针时,对应的数据可能不在内存中(数据已被换出)。用户空间的内存采用段页式,内核空间也有自己的规则。
32位的Linux内核虚拟地址空间,一般低3G的地址(0-3G)划分为用户空间,高1G的地址(3~4G)划为内核空间,也就是说内核空间可以使用的线性地址只有1G。
用户空间的应用程序,在业务实现时, 1、如果很简单,比如你只实现个printf("hello world");,并没有和内核进行交互,那么就不会存在两个空间的数据拷贝。 2、但是实际上,很多的业务实现都要与内核进行交互,各种上网,操作一些字符、块设备,调用open()、write()、read()、ioctl()等等函数,就要用到系统调用。这些都会触发一个中断,并通过中断的方式陷入到内核去执行,但用到的还是用户态进程,然后将进程需要的数据返回到用户空间。 更加细节的可自行再去学习。