Linux设备驱动程序 第三版 读书笔记(一)
Bob Zhang
2017.08.25
编写基本的Hello World模块
#include <linux/init.h>
#include <linux/module.h> // 声明模块的许可证书
MODULE_LICENSE("Dual BSD/GPL"); static __init hello_init(void)
{
// KERN_ALERT表示的是日志级别
printk(KERN_ALERT "Hello, world\n");
return ;
}
static __exit void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
} // 注册模块初始化函数,在模块安装到内核时会被调用
module_init(hello_init);
// 注册模块的退出函数,在模块从内核移除时会被调用
module_exit(hello_exit);
需要注意的地方是,内核代码不支持浮点数。
模块的编译
obj-m += hello.o
但是如果需要编译的模块由多个文件组成,则可以使用下面的代码:
obj-m := module.o
module-objs := file1.o file2.o
模块装载
一般使用insmod对模块进行装载:
insmod hello.ko
但是insmod不会对要装载的模块的依赖做检查,如果模块引用了内核中没有的符号,则会报“unresolved symbol”的错误。如果要想检查模块的依赖再装载,可以使用modprobe命令。
modprobe, 如同 insmod, 加载一个模块到内核. 它的不同在于它会查看要加载的模块, 看是否它引用了当前内核没有定义的符号. 如果发现有, modprobe 在定义相关符号的当前模块搜索路径中寻找其他模块. 当 modprobe 找到这些模块( 要加载模块需要的 ), 它也把它们加载到内核.如果你在这种情况下代替以使用 insmod , 命令会失败, 在系统日志文件中留下一条 " unresolved symbols "消息.
模块查看
当模块装载好之后,可以使用lsmod检查模块是否真的装载到内核中了:
#查看所有加载到内核的模块
lsmod #查看指定的模块,如hello.ko
lsmod | grep hello
lsmod 程序生成一个内核中当前加载的模块的列表. 一些其他信息, 例如使用了一个特定模块的其他模块, 也提供了. lsmod 通过读取 /proc/modules 虚拟文件工作. 当前加载的模块的信息也可在位于 /sys/module 的 sysfs 虚拟文件系统找到.
模块卸载
#卸载hello.ko
rmmod hello.ko
我们可以使用rmmod工具从内核中移除模块。注意,如果内核认为模块仍然在使用状态(例如,某个程序正打开由该模块导出的设备文件),或者内核被配置为禁止移除模块,则无法移除该模块。配置内核并使得内核在模块忙的时候仍能“强制”移除模块也是可能的。但是,如果读者在某种情况下希望利用这种特性,则重新引导系统可能是更加合适的做法。
内核符号表
Linux内核头文件提供了一个方便的方法来管理符号对模块外部可见性,从而减少了可能造成的名字空间污染(名字空间中的名称可能会和内核其他地方定义的名称发生冲突),并且适当隐藏信息。如果一个模块需要向其他模块导出符号,则应该使用下面的宏。
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
这两个宏均用于将给定的符号导出到模块外部。_GPL版本使得要导出的模块只能被GPL许可证下的模块使用。符号符号必须在模块文件的全局部分导出,不能在函数中导出,这是因为上面这两个宏江被扩展成为一个特殊的变量声明,而该变量必须是全局的。该变量将在模块许可执行文件的特殊部分(即一个“ELF段”)中保存,在装载时,内核通过这个段来寻找模块导出的变量。
其他宏定义
大部分内核代码中都要包含相当数量的头文件,以便获得函数、数据类型和变量的定义。我们将在用到这些文件时向读者介绍,但是有几个头文件是专门用于模块的,因此必须出现在每个可装载的模块中。故而,所有的模块代码中都包含下面两行代码:
#include <linux/module.h>
#include <linux/init.h>
module.h包含有可装载模块需要的大量符号和函数的定义。包含init.h的目的是指定初始化和清理函数,就像我们在hello模块中看到的那样。大部分的模块还包括了“moduleparam.h”头文件,这样我们就可以在装载模块的时候向模块传递参数。接下来介绍一些常用的宏。
尽管不是严格要求,但模块应该制定代码所使用的许可证。为此我们只需要包含MODULE_LICENSE行:
MODULE_LICENSE("GPL");
内核能够识别的许可证有“GPL”(任一版本的GNU(GNU's Not Unix)通用公共许可证)、“GPL v2”(GPL版本2)、“GPL and additional rights(GPL及附加权利)”、“Dual BSD/GPL(BSD/GPL双重许可证)”、“Dual MPL/GPL(MPL/GPL双重许可证)”以及“Proprietary(专有)”。如果一个模块没有显示地标记为上述内核可识别的许可证,则会被假定是专有的,而内核装载这种模块就会被“污染”。
可在模块中包含的其他描述性定义包括:
// 描述模块作者
MODULE_AUTHOR(BobZhang<zhangbob@email.com>);
// 简短说明模块用途
MODULE_DESCRIPTION("This is a hello world demo module.");
// 代码修订号;有关版本字串的创建惯例,请参考<linux/module.h>中的注释
MODULE_VERSION(version);
// 模块的别名
MODULE_ALIAS("hello_world");
// 告诉用户空间模块所支持的设备
MODULE_DEVICE_TABLE(device_table);
上述MODULE_声明可出现在源文件中源代码函数以外的任何地方。但新近的内核编码习惯是将这些声明放在文件的最后。
模块参数
内核提供一种方式传递参数给模块,那就是在运行insmod或modprobe命令时,将要传递给模块的参数给出,而modeprobe还可以从他的配置文件(/etc/modprobe.conf)中读取参数值。这两个命令可在命令行接受几种参数类型的赋值。为了演示这种功能,我们假定对前面的hello模块做一些必要的增强。我们添加了两个参数:一个是整数值,其名称为howmany;另一个是字符串,名称为whom。在装载这个增强的模块时,将相whom问候howmany次。这样我们可用下面的命令来装载该模块:
insmod hello.ko howmany= whom="Bob"
上面这条命令的效果会让hello模块打印10次“hello, Bob”。
更改后的hello模块如下:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h> static char *whom = "world";
static int howmany = ;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO); // 声明模块的许可证书
MODULE_LICENSE("Dual BSD/GPL"); static __int hello_init(void)
{
int count = ;
// KERN_ALERT表示的是日志级别
for(; count < howmany; ++count)
printk(KERN_ALERT "hello, %s\n", whom);
return ;
}
static __exit void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
} // 注册模块初始化函数,在模块安装到内核时会被调用
module_init(hello_init);
// 注册模块的退出函数,在模块从内核移除时会被调用
module_exit(hello_exit);
内核支持的模块参数类型如下:
类型 |
描述 |
bool invbool |
布尔值(取true或false),关联的变量应该是int型。invbool类型反转其值,也就是说,true值变成false,而false变成true。 |
charp |
字符指针值。内核会为用户提供的字符串分配内存,并相应设置指针。 |
int long short uint ulong ushort |
具有不同长度的基本整数类型。以u开头的类型用于无符号值。 |
模块装载器也支持数组参数,贼提供数组值时用逗号划分各数组成员。要声明数组参数,需要使用下面的宏:
module_param_array(name, type, num, perm);
其中,name是数组的名称(也就是参数的名称),type是数组原书的类型,num是一个证书变量,而perm是常见的访问许可值。如果装载时设置数组参数,则num会被设置为用户提供的值的个数。模块装载器会拒绝接受超过数组大小的值。
module_param中的最后一个成员是访问许可值,我们应使用<linux/stat.h>存在的定义。这个值用来控制水能够访问sysfs中对模块参数的表述。如果perm被设置为0,就不会有对应的sysfs入口项;否则,模块参数会在/sys/module中出现,并设置为给定的访问许可。如果参数使用S_IRUGO,则任何人均可读取该参数,但不能修改;S_IRUGO|S_IWUSR允许root用户修改该参数。注意,如果一个参数通过sysfs而被修改,则如同模块修改了这个参数的值一样,但是内核不会以任何方式通知模块。大多数情况下,我们不应该让模块参数是可写的,除非我们打算检测这种修改并作为相应的动作。
在用户空间编写驱动
可以在用户空间编写驱动,欲知详情请google或者bing搜索。
当前进程
虽然内核模块不像应用程序那样顺序执行,然而内核执行的大多数操作还是和某个特定的进程相关。内核代码可以通过访问全局项current来获得当前进程。current在<asm.current.h>中定义,是一个指向struct task_struct的指针,而task_struct结构在<linux/sched.h>文件中定义。current指针指向当前正在运行的进程。在open、read等操作系统调用的执行过程中,当前进程指的是调用这些系统调用的进程。如果需要,内核代码可以通过current获得与当前进程相关的信息。
printk(KERNE_INFO "The process is \"%s\" [pid %i]\n",
current->comm, current->pid);
存储在current->comm成员中的命令是当前进程所执行的程序文件的基本名称(base name),如果必要,会裁剪到15个字符以内。
未完待续~
一切伟大的思想和行动都有一个微不足道的开始。
Any great thoughts and actions has a small beginning.