一、模块传参
module_param函数的作用是在加载内核模块时,将参数传递给模块,或者为驱动模块中的某一个变量赋值。如果在加载模块时没有明确为这些变量赋值,那么它们将使用默认值。
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/变量名 指定操作权限
#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的示例:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("Dual BSD/GPL");
static char *who = "world";
static int times = 1;
module_param(times, int, S_IRUSR);
module_param(who, charp, S_IRUSR);
static int hello_init(void)
{
int i;
for(i = 0; i < times; i++)
printk(KERN_ALERT "(%d) hello, %s!\n", i, who);
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, %s!\n", who);
}
module_init(hello_init);
module_exit(hello_exit);
在这个例子中,我们定义了两个全局变量
who
和times
,并使用module_param
宏将它们暴露为模块参数。这样,当使用
insmod
命令加载模块时,我们可以传递参数给这两个变量,例如:insmod my_module.ko times=3 who="kernel"
。在模块加载后,
times
和who
的值就会被设置为传递的参数值,然后在模块的初始化函数hello_init
中使用这些值。需要注意的是,module_param宏是Linux 2.6内核中新增的,并且被定义在
include/linux/moduleparam.h
文件中。此外,还需要确保模块的其他部分(如初始化函数和退出函数)已经正确实现,以便在加载和卸载模块时能够正确地处理这些参数。
module_param_array函数在Linux内核模块编程中用于传递数组类型的参数给模块。当需要为内核模块传递多个值,并将这些值存储在一个数组中时,这个函数就非常有用。
module_param_array(name,type,&num,perm);
/*
name、type、perm同module_param,type指数组中元素的类型
&num:存放数组大小变量的地址,可以填NULL(确保传参个数不越界)
传参方式 insmod xxx.ko 数组名=元素值0,元素值1,...元素值num-1
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("Dual BSD/GPL");
static int numbers[10];
static int count;
module_param_array(numbers, int, &count, S_IRUSR);
static int __init my_module_init(void)
{
int i;
for (i = 0; i < count; i++) {
printk(KERN_INFO "Number[%d] = %d\n", i, numbers[i]);
}
return 0;
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Module exited.\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
在这个例子中,我们定义了一个整数数组
numbers
和一个整数count
。使用module_param_array
宏,我们将numbers
数组暴露为模块参数,并将count
的地址传递给宏,以便在加载模块时存储数组中元素的数量。当使用
insmod
命令加载此模块时,可以通过逗号分隔的列表为numbers
数组传递多个值,例如:insmod my_module.ko numbers=1,2,3,4,5
。加载模块后,numbers
数组将包含传递的值,count
变量将设置为数组中元素的数量。在模块的初始化函数my_module_init
中,我们可以遍历数组并打印每个元素的值。请注意,这个示例假定最多会有10个元素被传递给数组。在实际应用中,应该根据实际需要设置数组的大小,并确保在加载模块时不会超过这个大小,以避免潜在的缓冲区溢出问题。此外,还需要确保模块的其他部分(如初始化函数和退出函数)已经正确实现,以便在加载和卸载模块时能够正确地处理这些参数。
二、模块依赖
既然内核模块的代码与其它内核代码共用统一的运行环境,也就是说模块只是存在形式上独立,运行上其实和内核其它源码是一个整体,它们隶属于同一个程序,因此一个模块或内核其它部分源码应该可以使用另一个模块的一些全局特性。
一个模块中这些可以被其它地方使用的名称被称为导出符号,所有导出符号被填在同一个表中这个表被称为符号表。
最常用的可导出全局特性为全局变量和函数
查看符号表的命令:nm
nm查看elf格式的可执行文件或目标文件中包含的符号表,用法:
`nm 文件名` (可以通过man nm查看一些字母含义)
两个用于导出模块中符号名称的宏:
EXPORT_SYMBOL(函数名或全局变量名)
EXPORT_SYMBOL_GPL(函数名或全局变量名) 需要GPL许可证协议验证
使用导出符号的地方,需要对这些符号进行extern声明后才能使用这些符号
B模块使用了A模块导出的符号,此时称B模块依赖于A模块,则:
1. 编译次序:
先编译模块A,再编译模块B,当两个模块源码在不同目录时,需要:
i. 先编译导出符号的模块A
ii. 拷贝A模块目录中的Module.symvers到B模块目录
iii. 编译使用符号的模块B。否则编译B模块时有符号未定义错误
2. 加载次序:先插入A模块,再插入B模块,否则B模块插入失败
3. 卸载次序:先卸载B模块,在卸载A模块,否则A模块卸载失败
补充说明:
内核符号表(直接当文本文件查看)
/proc/kallsyms运行时 /boot/System.map编译后
三、内核空间和用户空间
为了彻底解决一个应用程序出错不影响系统和其它app的运行,操作系统给每个app一个独立的假想的地址空间,这个假想的地址空间被称为虚拟地址空间(也叫逻辑地址),操作系统也占用其中固定的一部分,32位Linux的虚拟地址空间大小为4G,并将其划分两部分:
1. 0~3G 用户空间 :每个应用程序只能使用自己的这份虚拟地址空间
2. 3G~4G 内核空间:内核使用的虚拟地址空间,应用程序不能直接使用这份地址空间,但可以通过一些系统调用函数与其中的某些空间进行数据通信
实际内存操作时,需要将虚拟地址映射到实际内存的物理地址,然后才进行实际的内存读写
四、执行流
执行流:有开始有结束总体顺序执行的一段独立代码,又被称为代码上下文
计算机系统中的执行流的分类:
执行流:
1. 任务流
任务上下文(都参与CPU时间片轮转,都有任务五状态:就绪态 运行态 睡眠态 僵死态 暂停态)
1. 进程
2. 线程
2.1. 内核线程:内核创建的线程
2.2. 应用线程:应用进程创建的线程
2. 异常流--异常上下文
1. 中断
2. 其它异常
应用编程可能涉及到的执行流:
1. 进程
2. 线程
内核编程可能涉及到的执行流:
1. 应用程序自身代码运行在用户空间,处于用户态 ----------------- 用户态app
2. 应用程序正在调用系统调用函数,运行在内核空间,处于内核态,即代码是内核代码但处于应用执行流(即属于一个应用进程或应用线程) ---- 内核态app
3. 一直运行于内核空间,处于内核态,属于内核内的任务上下文 --------- 内核线程
4. 一直运行于内核空间,处于内核态,专门用来处理各种异常 --------- 异常上下文
五、模块编程与应用编程的比较
不同点 | 内核模块 | 应用程序 |
API来源 | 不能使用任何库函数 | 各种库函数均可以使用 |
运行空间 | 内核空间 | 用户空间 |
运行权限 | 特权模式运行 | 非特权模式运行 |
编译方式 | 静态编译进内核镜像或编译特殊的ko文件 | elf格式的应用程序可执行文件 |
入口函数 | init_module | main |
运行方式 | 模块中的函数在需要时被动调用 | 从main开始顺序执行 |
退出方式 | cleanup_module | main函数返回或调用exit |
浮点支持 | 一般不涉及浮点运算,因此printk不支持浮点数据 | 支持浮点运算,printf可以打印浮点数据 |
并发考虑 | 需要考虑多种执行流并发的竞态情况 | 只需考虑多任务并行的竞态 |
程序出错 | 可能会导致整个系统崩溃 | 只会让自己崩溃 |
六、内核接口头文件查询
大部分API函数包含的头文件在include/linux目录下,因此:
1. 首先在include/linux 查询指定函数:grep 名称 ./ -r -n
2. 找不到则更大范围的include目录下查询,命令同上