linux设备驱动程序(第三版)读书笔记2

来源:互联网 发布:叶根友特楷简体 mac版 编辑:程序博客网 时间:2024/06/05 02:03

2.2. Hello World 模块
许多编程书籍从一个 "hello world" 例子开始, 作为一个展示可能的最简单的程序的方法.
本书涉及的是内核模块而不是程序; 因此, 对无耐心的读者, 下面的代码是一个完整的
"hello world"模块

#include<linux/init.h>#include<linux/module.h>MODULE_LICENSE("DualBSD/GPL");static inthello_init(void){printk(KERN_ALERT"Hello, world\n");return 0;}static voidhello_exit(void){printk(KERN_ALERT"Goodbye, cruel world\n");}module_init(hello_init);module_exit(hello_exit);


 

2.3. 内核模块相比于应用程序

在我们深入之前, 有必要强调一下内核模块和应用程序之间的各种不同.
不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模块只注册
自己以便来服务将来的请求, 并且它的初始化函数立刻终止. 换句话说, 模块初始化函数
的任务是为以后调用模块的函数做准备; 好像是模块说, " 我在这里, 这是我能做的."模
块的退出函数( 例子里是 hello_exit )就在模块被卸载时调用. 它好像告诉内核, "我不
再在那里了, 不要要求我做任何事了."
这种编程的方法类似于事件驱动的编程, 但是虽然
不是所有的应用程序都是事件驱动的, 每个内核模块都是. 另外一个主要的不同, 在事件
驱动的应用程序和内核代码之间, 是退出函数: 一个终止的应用程序可以在释放资源方面
懒惰, 或者完全不做清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立
的东西, 否则会保留一些东西直到系统重启


一个模块在内核空间运行, 而应用程序在用户空间运行. 这个概念是操作系统理论的基础.

Unix 从用户空间转换执行到内核空间, 无论何时一个应用程序发出一个系统调用或者被硬
件中断挂起时. 执行系统调用的内核代码在进程的上下文中工作 -- 它代表调用进程并且
可以存取该进程的地址空间. 换句话说, 处理中断的代码对进程来说是异步的, 不和任何
特别的进程有关.
模块的角色是扩展内核的功能; 模块化的代码在内核空间运行. 经常地一个驱动进行之前
提到的两种任务: 模块中一些的函数作为系统调用的一部分执行, 一些负责中断处理.


内核编程中有几个并发的来源. 自然的, Linux 系统运行多个进程, 在同一时间, 不止一
个进程能够试图使用你的驱动. 大部分设备能够中断处理器; 中断处理异步运行, 并且可
能在你的驱动试图做其他事情的同一时间被调用.

结果, Linux 内核代码, 包括驱动代码, 必须是可重入的 -- 它必须能够同时在多个上下
文中运行.
数据结构必须小心设计以保持多个执行线程分开, 并且代码必须小心存取共享
数据, 避免数据的破坏. 编写处理并发和避免竞争情况( 一个不幸的执行顺序导致不希望
的行为的情形 )的代码需要仔细考虑并可能是微妙的. 正确的并发管理在编写正确的内核
代码时是必须的;


应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 堆栈, 当然, 是用来保存函数调用
历史以及所有的由当前活跃的函数创建的自动变量. 内核, 相反, 有一个非常小的堆栈;
它可能小到一个, 4096 字节的页.
你的函数必须与这个内核空间调用链共享这个堆栈. 因
此, 声明一个巨大的自动变量从来就不是一个好主意; 如果你需要大的结构, 你应当在调
用时间内动态分配.


常常, 当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名
通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 如果你调
用这个函数, 确信你知道你在做什么."


内核代码不能做浮点算术. 


2.4.2. 加载和卸载模块

模块建立之后, 下一步是加载到内核. 如我们已指出的, insmod 为你完成这个工作. 这个
程序加载模块的代码段和数据段到内核, 接着, 执行一个类似 ld 的函数, 它连接模块中
任何未解决的符号连接到内核的符号表上. 但是不象连接器, 内核不修改模块的磁盘文件, 
而是内存内的拷贝.


感兴趣的读者可能想看看内核如何支持 insmod: 它依赖一个在 kernel/module.c 中定义
的系统调用. 函数 sys_init_module 分配内核内存来存放模块 ( 这个内存用 vmalloc 分
配; 看第 8 章的 "vmalloc 和其友" ); 它接着拷贝模块的代码段到这块内存区, 借助内
核符号表解决模块中的内核引用, 并且调用模块的初始化函数来启动所有东西.  
如果你真正看了内核代码, 你会发现系统调用的名子以 sys_ 为前缀. 这对所有系统调用
都是成立的, 并且没有别的函数. 记住这个有助于在源码中查找系统调用.  



2.5. 内核符号表

什么是内核符号表(Kernel-Symbol-Table)


OK,我们现在已经理解了最基本的系统调用和模块的概念,但是还需要继续理解另一个十分关键的概念--内核符号表。

让我们看一眼/proc/ksyms.在里面的每一个表项代表着一个公共的内核符号.这些符号是可以被我们的LKM引用的.再认真的看一眼这个文件,你会发现很多有趣的事情.

这个文件真的很有趣,他可以帮助我们看到我们的LKM到底可以调用那些函数.但是这也同时带来一个问题,在我们的LKM中所存取的每一个符号(像函数名)也会被列在这个文件里面,也会被其他人看到.因此,一个经验丰富的系统管理员就会发现我们小小的LKM并且杀掉他.

有很多方法可以阻止管理员发现我们的LKM,这可以看第二章节.在第二章节中提到的方法可以被称之为'Hacks',但是当我们在看第二章节的内容时你会发现里面并没有提到"如何使你的LKM符号不列在/proc/ksyms中"这样的方法.在第二章中我们没有提到这个问题是基于以下理由的:

你并不需要什么特殊的技巧来使你的模块的符号不在/proc/ksyms中出现.LKM开发者们用如下的常用代码来声明他们模块的符号.

static struct symbol_table module_syms= {

/*定义自己的符号表!!*/

#include <linux/symtab_begin.h>    

/*我们想引用的符号表*/

...                  

};

register_symtab(&module_syms);    

/*实际的注册工作*/

正如我所说的,我们并不需要对外公开我们的符号,所以我们只要用如下的语句就可以了

register_symtab(NULL);

这条语句必须放在init_module()函数中,要记住!


我们已经看到 insmod 如何对应共用的内核符号来解决未定义的符号. 表中包含了全局内
核项的地址 -- 函数和变量 -- 需要来完成模块化的驱动. 当加载一个模块, 如何由模块
输出的符号成为内核符号表的一部分. 通常情况下, 一个模块完成它自己的功能不需要输
出如何符号. 你需要输出符号, 但是, 在任何别的模块能得益于使用它们的时候.  
新的模块可以用你的模块输出的符号, 你可以堆叠新的模块在其他模块之上. 模块堆叠在
主流内核源码中也实现了: msdos 文件系统依赖 fat 模块输出的符号, 某一个输入 USB 
设备模块堆叠在 usbcore 和输入模块之上.  


linux 内核头文件提供了方便来管理你的符号的可见性, 因此减少了命名空间的污染( 将
与在内核别处已定义的符号冲突的名子填入命名空间), 并促使了正确的信息隐藏. 如果你
的模块需要输出符号给其他模块使用, 应当使用下面的宏定义:  
EXPORT_SYMBOL(name); 
EXPORT_SYMBOL_GPL(name); 


大部分内核代码包含了许多数量的头文件来获得函数, 数据结构和变量的定义. 我们将在
碰到它们时检查这些文件, 但是有几个文件对模块是特殊的, 必须出现在每一个可加载模
块中. 因此, 几乎所有模块代码都有下面内容:  
#include <linux/module.h> 
#include <linux/init.h> 
moudle.h 包含了大量加载模块需要的函数和符号的定义. 你需要 init.h 来指定你的初始
化和清理函数, 如我们在上面的 "hello world" 例子里见到的, 这个我们在下一节中再讲. 
大部分模块还包含 moudleparam.h, 使得可以在模块加载时传递参数给模块. 我们将很快
遇到.