嵌入式系统驱动初级【2】——内核模块下_参数和依赖

发布于:2023-02-06 ⋅ 阅读:(578) ⋅ 点赞:(0)

一、模块传参

函数介绍:

module_param_array(name,type,&num,perm)

module_param_array(name,type,&num,perm)

```c
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 即:-rxw-rxw-r---

```

```c
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可查看这些参数的描述信息

在/home/linux/fs4412/mydriverscode下创建一个para_test.c:

#include <linux/module.h>
#include <linux/kernel.h>

int arges = 10;
char *argv = "imysy_22";
int arry[5] = {1,2,3,4,5};

module_param(arges,int,0664);
module_param(argv,charp,0664);
module_param_array(arry,int,NULL,0664);

int __init para_test_init(void)
{
	printk("arges = %d\n",arges);
	printk("argv = %s\n",argv);
	
	char i;
	printk("arry[5] = ");
	for(i = 0;i < 5;i++)
	{
		printk("%d ",arry[i]);	
	}
	printk("\n");

	return 0;
}

void __exit para_test_exit(void)
{
	printk("para_test will exit\n");
}


MODULE_LICENSE("GPL");

module_init(para_test_init);
module_exit(para_test_exit);

编辑Makefile,添加:obj-m += para_test.o

ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/linux/fs4412/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 += myhello.o
obj-m += para_test.o    //加上这一行


endif

make 编译

make

执行下列命令即生效:

 插入模块时传参:

 输出:

二、模块依赖

​       既然内核模块的代码与其它内核代码共用统一的运行环境,也就是说模块只是存在形式上独立,运行上其实和内核其它源码是一个整体,它们隶属于同一个程序,因此一个模块或内核其它部分源码应该可以使用另一个模块的一些全局特性。

一个模块中这些可以被其它地方使用的名称被称为导出符号,所有导出符号被填在同一个表中这个表被称为符号表。

最常用的可导出全局特性为全局变量和函数  

1、实现两个模块依赖

建立一个新的目录:~/fs4412/mydriverscode/TwoModulea

建立两个.c文件:modulea.c

#include <linux/module.h>
#include <linux/kernel.h>

int gx = 666;

EXPORT_SYMBOL(gx);

int __init modulea_init(void)
{
	printk("modulea's 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);

moduleb.c

#include <linux/module.h>
#include <linux/kernel.h>

extern int gx;


int __init moduleb_init(void)
{
	printk("moduleb's 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);

在这个文档中建立Makefile

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/linux/fs4412/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

输入以下命令

make

sudo dmesg -C    //删掉之前的打印信息

sudo insmod modulea.ko    //插入模块a

sudo insmod moduleb.ko    //插入模块b

dmesg

可以看到模块b成功用上了模块a的全局变量gx。

2、拓展

查看符号表的命令: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.  线程

       1.  内核线程:内核创建的线程

       2.  应用线程:应用进程创建的线程

2. 异常流--异常上下文

   1. 中断

   2. 其它异常

应用编程可能涉及到的执行流:

1. 进程

2. 线程    

内核编程可能涉及到的执行流:  

1. 应用程序自身代码运行在用户空间,处于用户态   -----------------  用户态app

2. 应用程序正在调用系统调用函数,运行在内核空间,处于内核态,即代码是内核代码但处于应用执行流(即属于一个应用进程或应用线程) ----  内核态app

3. 一直运行于内核空间,处于内核态,属于内核内的任务上下文 --------- 内核线程

4. 一直运行于内核空间,处于内核态,专门用来处理各种异常 --------- 异常上下文

五、模块编程与应用编程的比较

| 不同点   | 内核模块                                                             | 应用程序                             

——————————————————————————————————————

| API来源  | 不能使用任何库函数                                          | 各种库函数均可以使用                 

| 运行空间 | 内核空间                                                            | 用户空间                                     

| 运行权限 | 特权模式运行                                                     | 非特权模式运行                                

| 编译方式 | 静态编译进内核镜像或编译特殊的ko文件          | elf格式的应用程序可执行文件          

| 运行方式 | 模块中的函数在需要时被动调用                        | 从main开始顺序执行                   

| 入口函数 | init_module                                                         | main                                 

| 退出方式 | cleanup_module                                                 | main函数返回或调用exit               

| 浮点支持 | 一般不涉及浮点运算,printk不支持浮点数据     | 支持浮点运算 

| 并发考虑 | 需要考虑多种执行流并发的竞态情况                 | 只需考虑多任务并行的竞态             

| 程序出错 | 可能会导致整个系统崩溃                                   | 只会让自己崩溃                       

六、内核接口头文件查询

大部分API函数包含的头文件在include/linux目录下,因此:

1. 首先在include/linux 查询指定函数:grep  名称  ./   -r   -n

2. 找不到则更大范围的include目录下查询,命令同上

本文含有隐藏内容,请 开通VIP 后查看