买公司的网站建设,怎么免费建设网站,小程序定制 seo营销,wordpress 登录后查看文章目录 前言一、设置测试系统二、Hello World 模块1、代码详解2、执行效果 三、内核模块相比于应用程序1、用户空间和内核空间2、内核的并发3、当前进程4、几个别的细节 四、编译和加载1、编译模块2、加载和卸载模块3、版本依赖 五、内核符号表六、预备知识七、初始化和关停1… 文章目录 前言一、设置测试系统二、Hello World 模块1、代码详解2、执行效果 三、内核模块相比于应用程序1、用户空间和内核空间2、内核的并发3、当前进程4、几个别的细节 四、编译和加载1、编译模块2、加载和卸载模块3、版本依赖 五、内核符号表六、预备知识七、初始化和关停1、清理函数2、初始化中的错误处理3、模块加载竞争 八、模块参数1、模块支持的模块参数2、访问许可值3、例程 九、在用户空间做十、快速参考 前言
本章介绍所有的关于模块和内核编程的关键概念通过一个 hello world 模块来认识驱动加载的流程及相关细节。 一、设置测试系统
我是在虚拟机上进行的开发查看当前 Linux 系统的内核版本
uname -r二、Hello World 模块
1、代码详解
hello.c
#include linux/init.h
#include linux/module.h
MODULE_LICENSE(Dual BSD/GPL);static int hello_init(void)
{printk(KERN_ALERT Hello, world\n);return 0;
}static void hello_exit(void)
{printk(KERN_ALERT Goodbye, cruel world\n);
}module_init(hello_init);
module_exit(hello_exit);这个模块定义了两个函数一个在模块加载到内核时被调用hello_init以及一个在模块被去除时被调用hello_exit。moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色。另一个特别的宏MODULE_LICENSE是用来告知内核该模块带有一个自由的许可证没有这样的说明在模块加载时内核会抱怨。printk 函数在 Linux 内核中定义并且对模块可用它与标准 C 库函数 printf 的行为相似。内核需要它自己的打印函数因为它靠自己运行没有 C 库的帮助模块能够调用 printk 是因为在 insmod 加载了它之后模块被连接到内核并且可存取内核的公用符号。 字串 KERN_ALERT 是消息的优先级。可以用 insmod 和 rmmod 工具来测试这个模块注意只有超级用户可以加载和卸载模块。
Makefile
ifneq ($(KERNELRELEASE),)
obj-m : hello.o
else
KERNELDIR ? /lib/modules/$(shell uname -r)/build
PWD : $(shell pwd)
default:$(MAKE) -C $(KERNELDIR) M$(PWD) modules
endifobj-m : hello.o 表明有一个模块要从目标文件 hello.o 建立在从目标文件建立后结果模块命名为 hello.ko 如果你有一个模块名为 module.ko是来自 2 个源文件( 姑且称之为file1.c 和 file2.c )正确的书写应当是: obj-m : module.o module-objs : file1.o file2.o KERNELDIR ? /lib/modules/$(shell uname -r)/build 如果这个 KERNELDIR 为空说明你没有指定内核库文件的路径那么它就会给 KERNELDIR 赋值因为顶层 Makefile 通过这个环境变量知道内核库文件在哪里。 PWD : $(shell pwd) 获取当前所执行命令的目录 $(MAKE) -C $(KERNELDIR) M$(PWD) modules 这个命令开始是改变它的目录到用 -C 选项提供的目录下( 就是说你的内核源码目录 )。它在那里会发现内核的顶层 makefile这个 M 选项使 makefile 在试图建立模块目标前回到你的模块源码目录这个目标依次地是指在 obj-m 变量中发现的模块列表在我们的例子里设成了 hello.o。
这个 makefile 在一次典型的建立中要被读 2 次当从命令行中调用这个 makefile它注意到 KERNELRELEASE 变量没有设置它利用这样一个事实来定位内核源码目录即已安装模块目录中的符号连接指回内核建立树如果你实际上没有运行你在为其而建立的内核你可以在命令行提供一个 KERNELDIR 选项设置 KERNELDIR 环境变量或者重写 makefile 中设置 KERNELDIR 的那一行。一旦发现内核源码树makefile 调用 default: 目标来运行第 2 个 make 命令( 在 makefile 里参数化成 $(MAKE)) 象前面描述过的一样来调用内核建立系统在第 2 次读makefile 设置 obj-m并且内核的 makefile 文件完成实际的建立模块工作。
2、执行效果
①、准备好 hello.c 和 Makefile ②、make 编译
make查看当前目录下编译产物其中 hello.ko 是我们需要用到的驱动模块 ③、加载 hello.ko 模块
sudo insmod hello.ko④、lsmod 显示已经加载到内核中的模块的状态信息
lsmod⑤、查看加载时的打印信息
sudo dmesg -c⑥、卸载 hello.ko 模块 ⑦、查看卸载时的打印信息
sudo dmesg -c三、内核模块相比于应用程序
不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务每个内核模块只注册自己以便来服务将来的请求并且它的初始化函数立刻终止。模块初始化函数的任务是为以后调用模块的函数做准备模块的退出函数就在模块被卸载时调用。这种编程的方法类似于事件驱动的编程但是虽然不是所有的应用程序都是事件驱动的每个内核模块都是。另外一个主要的不同在事件驱动的应用程序和内核代码之间是退出函数一个终止的应用程序可以在释放资源方面懒惰或者完全不做清理工作但是模块的退出函数必须小心恢复每个由初始化函数建立的东西否则会保留一些东西直到系统重启。一个应用程序可以调用它没有定义的函数连接阶段使用合适的函数库解决了外部引用。 printf 是一个这种可调用的函数并且在 libc 里面定义。一个模块在另一方面只连接到内核它能够调用的唯一的函数是内核输出的那些; 没有库来连接。内核编程和应用程序编程之间的重要不同是每一个环境是如何处理错误在应用程序开发中段错误是无害的一个调试器常常用来追踪错误到源码中的问题而一个内核错误至少会杀掉当前进程如果不终止整个系统。
1、用户空间和内核空间
一个模块在内核空间运行而应用程序在用户空间运行这个概念是操作系统理论的基础。cpu 在被设计时有保护系统软件不被应用程序破坏的功能。且这种保护功能分为不同级别当 cpu 中存在多个级别时unix 通常使用最高级和最低级即超级用户级和用户级也即内核空间和用户空间。在 Unix 下内核在最高级运行也称之为超级模式 这里任何事情都允许而应用程序在最低级运行所谓的用户模式这里处理器控制了对硬件的直接存取以及对内存的非法存取。模块的角色是扩展内核的功能模块化的代码在内核空间运行经常地一个驱动进行之前提到的两种任务模块中一些的函数作为系统调用的一部分执行一些负责中断处理。
2、内核的并发
常见引起并发原因
linux 系统中通常正在运行多个并发进程并且可能有多个进程同时使用我们的驱动程序。大多数设备能够中断处理器而中断处理程序异步运行而且可能在驱动程序正试图处理其他任务时被调用。linux 可以运行在多处理器上因此可能同时有多个处理器在使用该进程。
3、当前进程
Current 在asm.current.h中定义是一个指向 struct task_struct 的指针而 task_struct 结构在 linux/sched.h 中定义。Current 指针指向当前正在运行的进程在 openread 等系统调用的执行过程中当前进程指的是调用这些系统调用的进程。
struct task_struct *current;
current-id 当前进程的id
current-comm. :当前进程的命令名4、几个别的细节
如果我们需要大的结构应该调用动态分配该结构而不是声明大的自动变量。常见函数前加有 __ 两个下划线这种函数通常是接口的底层组件实际上双下划线是告诉程序员谨慎使用后果自负。内核代码不支持浮点数运算。
四、编译和加载
1、编译模块
上面已讲解这里不再讲述。
2、加载和卸载模块
模块建立之后下一步是加载到内核insmod 完成这个工作。这个程序加载模块的代码段和数据段到内核接着执行一个类似 ld 的函数它连接模块中任何未解决的符号连接到内核的符号表上。modprobe 工具值得快速提及一下。modprobe 和 insmod 类似加载一个模块到内核。它的不同在于它会查看要加载的模块看是否它引用了当前内核没有定义的符号。如果发现有modprobe 在定义相关符号的当前模块搜索路径中寻找其他模块。当 modprobe 找到这些模块要加载模块需要的它也把它们加载到内核。如果你在这种情况下代替以使用 insmod命令会失败在系统日志文件中留下一条 “unresolved symbols” 消息。模块可以用 rmmod 工具从内核去除。注意如果内核认为模块还在用就是说一个程序仍然有一个打开文件对应模块输出的设备或者内核被配置成不允许模块去除模块去除会失败可以配置内核允许“强行”去除模块 甚至在它们看来是忙的。如果你到了需要这选项的地步但是事情可能已经错的太严重以至于最好的动作就是重启了。只有系统调用函数的名字前边带有 sys_ 前缀。lsmod 列出当前装载到内核中的所有模块。lsmod 通过读取 /proc/modules 虚拟文件工作。当前加载的模块的信息也可在位于 /sys/module 的 sysfs 虚拟文件系统找到。
3、版本依赖
如果你编写一个模块想用来在多个内核版本上工作特别地是如果它必须跨大的发行版本你可能只能使用宏定义和 #ifdef 来使你的代码正确建立利用 linux/version.h 中发现的定义。这个头文件自动包含在 linux/module.h定义了下面的宏定义:
UTS_RELEASE 这个宏定义扩展成字符串描述了这个内核树的版本例如, “2.6.10”。 LINUX_VERSION_CODE 这个宏定义扩展成内核版本的二进制形式版本号发行号的每个部分用一个字节表示。例如 2.6.10 的编码是 132618 ( 就是0x02060a )。有了这个信息, 你可以几乎是容易地决定你在处理的内核版本。 KERNEL_VERSION(major,minor,release) 这个宏定义用来建立一个整型版本编码从组成一个版本号的单个数字。例如 KERNEL_VERSION(2.6.10) 扩展成 132618这个宏定义非常有用当你需要比较当前版本和一个已知的检查点。
五、内核符号表
通常情况下一个模块完成它自己的功能不需要输出如何符号。但是你需要输出符号在任何别的模块能得益于使用它们的时候。linux 内核头文件提供了方便来管理你的符号的可见性因此减少了命名空间的污染将与在内核别处已定义的符号冲突的名子填入命名空间并促使了正确的信息隐藏。如果你的模块需要输出符号给其他模块使用应当使用下面的宏定义: EXPORT_SYMBOL(name);EXPORT_SYMBOL_GPL(name);上面宏定义的任一个使得给定的符号在模块外可用。_GPL 版本的宏定义只能使符号对 GPL 许可的模块可用。符号必须在模块文件的全局部分输出在任何函数之外因为宏定义扩展成一个特殊用途的并被期望是全局存取的变量的声明这个变量存储于模块的一个特殊的可执行部分一个 “ELF 段” 内核用这个部分在加载时找到模块输出的变量。
六、预备知识
有几个文件对模块是特殊的必须出现在每一个可加载模块中。因此几乎所有模块代码都有下面内容 #include linux/module.h#include linux/init.h moudle.h 包含了大量加载模块需要的函数和符号的定义你需要 init.h 来指定你的初始化和清理函数。不是严格要求的但是你的模块确实应当指定它的代码使用哪个许可。做到这一点只需包含一行 MODULE_LICENSE: MODULE_LICENSE(“GPL”); 内核认识的特定许可有GPL适用 GNU 通用公共许可的任何版本“GPL v2”只适用 GPL 版本 2“GPL and additional rights”“Dual BSD/GPL”Dual MPL/GPL和 “Proprietary”除非你的模块明确标识是在内核认识的一个自由许可下否则就假定它是私有的内核在模块加载时被弄污浊了。可以在模块中包含的其他描述性定义有 MODULE_AUTHOR声明谁编写了模块。MODULE_DESCRIPION一个人可读的关于模块做什么的声明 MODULE_VERSION一个代码修订版本号; 看 linux/module.h 的注释以便知道创建版本字串使用的惯例MODULE_ALIAS 模块为人所知的另一个名子以及 MODULE_DEVICE_TABLE ( 来告知用户空间模块支持那些设备 )。
七、初始化和关停
模块初始化函数注册模块提供的任何功能实际的初始化函数定义常常如:
static int __init initialization_function(void)
{
/* Initialization code here */
}
module_init(initialization_function);初始化函数应当声明成静态的因为它们不会在特定文件之外可见声明中的 __init 标志可能看起来有点怪它是一个给内核的暗示给定的函数只是在初始化使用模块加载者在模块加载后会丢掉这个初始化函数使它的内存可做其他用途。一个类似的标签__initdata给只在初始化时用的数据。使用 __init 和 __initdata 是可选的但是它带来的麻烦是值得的使用 moudle_init 是强制的这个宏定义增加了特别的段到模块目标代码中表明在哪里找到模块的初始化函数。 没有这个定义你的初始化函数不会被调用大部分注册函数以 register_ 做前缀因此找到它们的另外一个方法是在内核源码里查找 register_
1、清理函数
每个非试验性的模块也要求有一个清理函数它注销接口在模块被去除之前返回所有资源给系统。这个函数定义为
static void __exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);清理函数没有返回值, 因此它被声明为 void__exit 修饰符标识这个代码是只用于模块卸载通过使编译器把它放在特殊的 ELF 段如果你的模块直接建立在内核里或者如果你的内核配置成不允许模块卸载标识为 __exit 的函数被简单地丢弃。因为这个原因一个标识 __exit 的函数只在模块卸载或者系统停止时调用任何别的使用是错的。再一次moudle_exit 声明对于使得内核能够找到你的清理函数是必要的。
2、初始化中的错误处理
你必须记住一件事在注册内核设施时注册可能失败。即便最简单的动作常常需要内存分配分配的内存可能不可用。因此模块代码必须一直检查返回值并且确认要求的操作实际上已经成功。
int __init my_init_function(void)
{int err;err register_this(ptr1, skull); /* registration takes a pointer and a name */if (err)goto fail_this;err register_that(ptr2, skull);if (err)goto fail_that;err register_those(ptr3, skull);if (err)goto fail_those;return 0; /* success */fail_those:unregister_that(ptr2, skull);fail_that:unregister_this(ptr1, skull);fail_this:return err; /* propagate the error */
}模块清理函数必须撤销任何由初始化函数进行的注册并且惯例但常常不是要求的是按照注册时相反的顺序注销设施。
void __exit my_cleanup_function(void)
{unregister_those(ptr3, skull);unregister_that(ptr2, skull);unregister_this(ptr1, skull);return;
}如果你的初始化和清理比处理几项复杂goto 方法可能变得难于管理因为所有的清理代码必须在初始化函数里重复有时包括几个混合的标号因此一种不同的代码排布证明更成功。 使代码重复最小和所有东西流线化你应当做的是无论何时发生错误都从初始化里调用清理函数。清理函数接着必须在撤销它的注册前检查每一项的状态以最简单的形式代码看起来象这样
struct something *item1;
struct somethingelse *item2;
int stuff_ok;
void my_cleanup(void)
{if (item1)release_thing(item1);if (item2)release_thing2(item2);if (stuff_ok)unregister_stuff();return;
}
int __init my_init(void)
{int err -ENOMEM;item1 allocate_thing(arguments);item2 allocate_thing2(arguments2);if (!item2 || !item2)goto fail;err register_stuff(item1, item2);if (!err)stuff_ok 1;elsegoto fail;return 0; /* success */fail:my_cleanup();return err;
}清理函数当由非退出代码调用时不能标志为 __exit。
3、模块加载竞争
内核的某些别的部分会在注册完成之后马上使用任何你注册的设施这是完全可能的换句话说内核将调用进你的模块在你的初始化函数仍然在运行时所以你的代码必须准备好被调用一旦它完成了它的第一个注册。不要注册任何设施直到所有的需要支持那个设施的你的内部初始化已经完成。
八、模块参数
模块参数可以在运行 insmod 或 modprobe 命令装载模块时赋值modprobe 可以从配置文件/etc/modprobe.conf中读取参数值。
在 insmod 改变模块参数之前模块必须让参数对 insmod 命令可见。参数使用 module_param变量名类型访问许可值宏来声明它定义在 moduleparam.h。
所有的模块参数都应该在源文件中给定一个默认值。
1、模块支持的模块参数
bool、invbool取反true 变为 falsefalse 变为 truecharp字符指针int、long、short、unit、ulong、ushort数组参数module_param_array(数组名类型值的个数访问许可值)
模块中的钩子可让我们自定义类型
2、访问许可值
使用 linux/stat.h 中定义的值
设置为0不会有对应的 sysfs 入口项否则模块参数会在 /sys/module/如下所示S_IRUGO 任何人都可以读取但不能修改S_IRUGO | S_IWUSR 允许 root 用户修改
大多数情况下不应该让模块参数是可写的
3、例程
hello.c
#include linux/init.h
#include linux/module.h
#include linux/moduleparam.h
MODULE_LICENSE(Dual BSD/GPL);static char *hello_str hello;
static int hello_cnt 2;
module_param(hello_str, charp, S_IRUGO);
module_param(hello_cnt, int, S_IRUGO);static int hello_init(void)
{printk(KERN_ALERT Hello, world\n);printk(%s, %d\n, hello_str, hello_cnt);return 0;
}static void hello_exit(void)
{printk(KERN_ALERT Goodbye, cruel world\n);
}module_init(hello_init);
module_exit(hello_exit);加载 hello 模块驱动并查看打印信息
sudo insmod hello.ko
sudo dmesg模块加载后可以在 /sys/module/模块名/parameters 目录下查看参数
cd /sys/module/hello/parameters/
ls
cat hello_cnt
cat hello_str九、在用户空间做
用户空间驱动的好处在于
完整的 C 库可以连接驱动可以进行许多奇怪的任务不用依靠外面的程序实现使用策略的工具程序常常随着驱动自身发布程序员可以在驱动代码上运行常用的调试器而不必走调试一个运行中的内核的弯路。如果一个用户空间驱动挂起了你可简单地杀掉它驱动的问题不可能挂起整个系统除非被控制的硬件真的疯掉了。用户内存是可交换的不象内核内存一个不常使用的却有很大一个驱动的设备不会占据别的程序可以用到的 RAM除了在它实际在用时。一个精心设计的驱动程序仍然可以如同内核空间驱动允许对设备的并行存取。如果你必须编写一个封闭源码的驱动用户空间的选项使你容易避免不明朗的许可的情况和改变的内核接口带来的问题。
用户空间的设备驱动的方法有几个缺点最重要的是
中断在用户空间无法用在某些平台上有对这个限制的解决方法例如在 IA32 体系上的 vm86 系统调用。只可能通过内存映射 /dev/mem 来使用 DMA而且只有特权用户可以这样做。存取 I/O 端口只能在调用 ioperm 或者 iopl 之后此外不是所有的平台支持这些系统调用而存取/dev/port 可能太慢而无效率这些系统调用和设备文件都要求特权用户。响应时间慢因为需要上下文切换在客户和硬件之间传递信息或动作。更不好的是如果驱动已被交换到硬盘响应时间会长到不可接受使用 mlock 系统调用可能会有帮助但是常常的你将需要锁住许多内存页因为一个用户空间程序依赖大量的库代码mlock 也限制在授权用户上。最重要的设备不能在用户空间处理包括但不限于网络接口和块设备。
十、快速参考 insmod modprobe rmmod 用户空间工具加载模块到运行中的内核以及去除它们。 #include linux/init.h module_init(init_function); module_exit(cleanup_function); 指定模块的初始化和清理函数的宏定义。 __init __initdata __exit __exitdata 函数__init 和 __exit和数据__initdata 和 __exitdata的标记只用在模块初始化或者清理时间。 #include linux/sched.h 最重要的头文件中的一个这个文件包含很多驱动使用的内核 API 的定义包括睡眠函数和许多变量声明。 struct task_struct *current; 当前进程。 current-pid current-comm 进程 ID 和 当前进程的命令名。 obj-m 一个 makefile 符号内核建立系统用来决定当前目录下的哪个模块应当被建立。 /sys/module /proc/modules /sys/module 是一个 sysfs 目录层次包含当前加载模块的信息。/proc/moudles 是旧式的那种信息的单个文件版本。其中的条目包含了模块名每个模块占用的内存数量以及使用计数另外的字串追加到每行的末尾来指定标志对这个模块当前是活动的。 vermagic.o 来自内核源码目录的目标文件描述一个模块为之建立的环境。 #include linux/module.h 必需的头文件它必须在一个模块源码中包含。 #include linux/version.h 头文件包含在建立的内核版本信息。 LINUX_VERSION_CODE 整型宏定义对 #ifdef 版本依赖有用。 EXPORT_SYMBOL (symbol); EXPORT_SYMBOL_GPL (symbol); 宏定义用来输出一个符号给内核。第 2 种形式输出没有版本信息第 3 种限制输出给 GPL 许可的模块。 MODULE_AUTHOR(author); MODULE_DESCRIPTION(description); MODULE_VERSION(version_string); MODULE_DEVICE_TABLE(table_info); MODULE_ALIAS(alternate_name); 放置文档在目标文件的模块中。 module_init(init_function); module_exit(exit_function); 宏定义声明一个模块的初始化和清理函数。 #include linux/moduleparam.h module_param(variable, type, perm); 宏定义创建模块参数可以被用户在模块加载时调整或者在启动时间对于内嵌代码。类型可以是 boolcharpintinvboolshortushortuintulong 或者 intarray。 #include linux/kernel.h int printk(const char * fmt, …); 内核代码的 printf 类似物。 我的qq2442391036欢迎交流