Linux内核设计与实现 -- 第17章 设备与模块

17.1 设备类型

  • 块设备 blkdev
    可寻址,寻址以块为单位(不同设备块大小不同),支持重定位(seeking)即数据随机访问。
    例:硬盘、蓝光光碟, Flash存储设备。块设备是通过称为"块设备节点"的特殊文件来访问的,
    并且通常被挂载为文件系统。
  • 字符设备 cdev
    不可寻址,仅提供数据流式访问(按字符,或者字节)。字符设备的例子有键盘、鼠标、打印机,还有大部分伪设备。字符设备通过"字符设备节点"的特殊文件来访问的。与块设备不同,应用程序通过直接访问设备节点与字符设备交互。
  • 网络设备 也称为ethermet devices
    提供了对网络(例如Internet)的访问,这是通过一个物理适配器和一种特定的协议(如IP协议)进行的。网络设备是通过套接字API这样的特殊接口来访问。
  • 其它设备
    针对单个任务,不通用。例如杂项设备(miscellaneous device),简写miscdev,实际为简化的字符设备。
    伪设备(pseudo device),设备驱动是虚拟的,仅提供内核功能。如内核随机数发生器,空设备,零设备,满设备,内存设备。大部分设备驱动表社物理设备。

17.2 模块

17.2.1 Hello, World

17.2.2 构建模块

尽管linux是单块内核的操作系统(整个系统内核运行于一个单独保护域),但linux内核是模块化组成的,允许运行时动态插入删除代码,些代码(包括相关的子例程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,或简称为模块。
2.6内核中,采用了新的"kbuild"构建系统。

  1. 放在内核源代码树种
    设备驱动程序放在内核源码树根目录下/drivers的子目录下。字符设备存在于drivers/char/目录下,而块设备存放在drivers/block/目录下,USB设备则存放在drivers/usb/目录下。
    假设想建立自己代码的子目录,你的驱动程序是一个钓鱼竿和计算机的接口,名为Fish Master XL3000,那么你需要在drivers/char/目录下建立一个名为 fishing 的子目录。接下来需要向 drivers/char/下的 Makefle 文件中添加一行。编辑 deriverschar/Makefle/并加入∶
    obj-m += fishing/
    
    这行编译指令告诉模块构建系统,在编译模块时需要进入fishing/子目录中。更可能发生的情况是,你的驱动程序的编译取决于一个特殊配置选项;比如,可能的CONFIG_FISHING_POLE(请看17.2.6 节,它会告诉你如何加入一个新的编译选项)。如果这样,你需要用下面的指令代替刚才那条指令 ∶
    obj-$(CONFIG_FISHING_POLE)+= fishing/
    
    最后,在 drivers/char/ishing/下,需要添加一个新Makefle文件,其中需要有下面这行指令∶
    obj-m t= fishing.o
    
    一切就绪了,此刻构建系统运行将会进入 fishing/目录下,并且将 fshing.c编译为fishing.ko模块。虽然你写的扩展名是.0,但是模块被编译后的扩展名却是 .ko。
    再一个可能,要是钓鱼竿驱动程序编译时有编译选项,可能需要这么来做∶
    obj-$(CONFIG_FISHING_POLE)+= fishing.o
    
    假如钓鱼竿驱动程序需要更加智能化——它可以自动检测钓鱼线。这时驱动程序源文件可能就不再只有一个了。只要把你的Makefile 做如下修改就可搞定∶
    obj-$(CONFIG_PISHING_POLE)+= fishing.o
    fishing-objs := fishing-main.o fishing-1line.o
    
    每当设置了CONFIG_FISHING_POLE,fishing-main.c和 fishing-line.c就一起被编译和连接
    到 fishing.ko 模块内。
    最后一个注意事项是,在构建文件时可能需要额外的编译标记,如果这样;只需在Makefile 中添加如下指令∶
    EXTRA_CFLAGS += -DTITANIUM_POLE
    
    如果吧的源文件置于drivers/char/目录下,并且不建立新目录的话,那么将前面提到的行(也就是原来处于drivers/char/fishing/下你自己的Makefle中的)都加入到
    drivers/char/Makefile 中即可。
  2. 放在内核代码外
    在自己的源代码树目录建立一个Makefile文件
    obj-m :=fishing.o
    
    多个源文件
     obj -m :=fishing.o
    fishing-objs := fishing-main.o fishing-line.o
    
    块在内核内和在内核外构建的最大区别在于构建过程。当模块在内核源代码树外围时,必须告诉 make如何找到内核源代码文件和基础 Makefile 文件。不过要完成这个工作同样不难∶
    make -C /kernel/source/location SUBDIRS=$pwD modules
    
    在这个例子中,/kernel/source/location是配置的内核源代码树。不要把要处理的内核源代码树放在 /usr/src/linux 下,而要移到home 目录下某个方便访问的地方。

17.2.3 安装模块

编译后的模块将被装入到目录 /lib/modules/version/kernel/下,在 kermel/目录下的每一个目录都对应着内核源码树中的模块位置。如果使用的是2.6.34 内核,而且将你的模块源代码直接放在 drivers/char/下,那么编译后的钓鱼竿驱动程序的存放路径将∶/lib/modules/2.6.34/kerneldrivers/char/fishing.ko。
下面的构建命令用来安装编译的模块到合适的目录下 ∶

make modules_instal1

通常需要以root权限运行。

17.2.4 产生模块依赖性

Linux模块之间存在依赖性,若想产生内核依赖关系的信息,root用户可运行命令

depmod

了执行更快的更新操作,那么可以只为新模块生成依赖信息

depmod -A

模块依赖关系信息存放在/lib/modules/version/modules.dep 文件中。

17.2.5 载入模块

通过 insmod 命令。这是个功能很有限的命令,它能做的就是请求内核载入指定的模块。insmod 程序不执行任何依赖性分析或进一步的错误检查,以root 身份运行命令∶

insmod module.ko 
// module.ko是要载入的模块名称。比如
insmod fishing.ko

类似,卸载模块使用rmmod命令,以root身份运行:

rmmod module
rmmod fishing

上面两个命令不智能,先进工具modprobe提供了模块依赖性分析、错误智能检查、错误报告以及许多其他功能和选项。modprobe 命令不但会加载指定的模块,而且会自动加载任何它所依赖的有关模块。所以说
它是加载模块的最佳机制。为了在内核 via modprobe 中插入模块,需要以root身份运行∶

modprobe module [ module parameters ]

其中,参数 module 指定了需要载入的模块名称,后面的参数将在模块加载时传入内核(见17.2.7 )。
modprobe 命令也可用来从内核中卸载模块,当然这也需要以root身份运行∶

modprobe -r modules

参数 modules 指定一个或多个需要卸载的模块。与rmmod 命令不同,modprobe也会卸载给定模块所依赖的相关模块,但其前提是这些相关模块没有被使用。Linux用户手册第8部分提供了上述命令的使用参考,里面包括了命令选项和用法。

17.2.6 管理配置选项

上面提到只要设置了CONFIG_FISHING_POLE 配置选项,钓鱼竿模块就将被自动编译。下面以钓鱼竿驱动程序为例,看看一个新的配置选项如何加入。
由于2.6内核中新引入了"kbuild"系统,所需做的全部就是向kconfig文件中添加一项,用以对应内核源码树。对驱动程序而言,kconfg 通常和源代码处于同一目录。如果钓鱼竿驱动程序在目录drivers/char/下,那么drivers/char/kconfig 也同时存在。
如果建立了一个新子目录,而且也希望kconfig文件存在于该目录中的话,那么必须在一个已存在的 kconfig 文件中将它引入:

source "drivers/char/fishing/Kconfig"

这里所谓存在的 Kconfig 文件可能是 drivers/char/Kconfg。
Kconfg 文件很方便加入一个配置选型,请看钓鱼竿模块的选项,如下所示∶

config FISHING_POLE
	tristate "Fish Master 3000 support"
	default n
	help
		If you say Y here,support for the Fish Master 3000 with computer interface will be compiled into the kernel and accessible via a device node.You can also say M here and the driver will be built as a module named fishing.ko.
		If unsure, say N.

配置选项第一行定义了该选项所代表的配置目标。注意 CONFIG_前缀并不需要写上。
第二行声明选项类型为 tristate,也就是说可以编译进内核(Y),也可作为模块编译(M),或者干脆不编译它(N)。如果编译选项代表的是一个系统功能,而不是一个模块,那么编译选项将用 bool指令代替 tristate,这说明它不允许被编译成模块。处于指令之后的引号内文字为该
选项指定了名称。
第三行指定了该选项的默认选择,这里默认操作是不编译它(N)。也可以把默认选择指定为编译进内核(Y),或者编译成一个模块(M)。对驱动程序而言,默认选择通常为不编译进内核(N)。
Help 指令的目的是为该选项提供帮助文档。各种配置工具都可以按要求显示这些帮助。
除了上述选项以外,还存在其他选项。比如depends指令规定了在该选项被设置前,首先要设置的选项。假如依赖性不满足,那么该选项就被禁止。比如,如果加入指令∶

depends on FISH_TANK

配置选项中,那么就意味着在 CONFIG_FISH_TANK 被选择前,我们的钓鱼竿模块是不能使用的(Y或者 M)。Select指令和depends类似,它们只有一点不同之处——只要是select指定了谁,它就会强行将被指定的选项打开。所以这个指令可不能向 depends那样滥用一通,因为它会自动的激活其他配置选项。它的用法和 depends 一样。比如:

select BAIT

意味着当 CONFIG_FISHING_POLE被激活时,配置选项 CONFIG_BAIT必然一起被激活。
如果select和depends同时指定多个选项,那就需要通过&&指令来进行多选。使用depends 时,你还可以利用叹号前缀来指明禁止某个选项。比如∶

depends on EXAMPLB_DRIVERS && !NO_FISHING_ALLOWED

这行指令就指定驱动程序安装要求打开CONFIG_EXAMPLE_DRIVERS选项,同时要禁止CONFIG_NO_FISHING_ALLOWED 选项。
tristate 和bool选项往往会结合if指令一起使用,这表示某个选项取决于另一个配置选项。
如果条件不满足,配置选项不但会被禁止,甚至不会显示在配置工具中,比如,要求配置系统只
有在 CONFIG_x86 配置选项设置时才显示某选项。请看下面指令:

bool "Deep Sea Mode" if OCEAN

If指令也可与 default 指令结合使用,强制只有在条件满足时 default选项才有效。
配置系统导出了一些元选项(meta-option)以简化生成配置文件。比如:

  • CONFIG_EMBEDDED是用于关闭那些用户想要禁止的关键功能(比如要在嵌入系统中节省珍贵的内存);
  • CONFIG_BROKEN_ON_SMP用来表示驱动程序并非多处理器安全的。通常该项不应设置,标记它的目的是确保用户能知道该驱动程序的弱点。当然,新的驱动程序不应该使用该标志。
  • CONFIG_EXPERIMENTAL选项,它是一个用于说明某项功能尚在试验或处于beta版阶段的标志选项。该选项默认情况下关闭,同样,标记它的目的是为了让用户在使用驱动程序前明白潜在风险。

17.2.7 模块参数

Linux提供为驱动程序声明参数的框架,用户在系统启动或者模块装载时可以再指定参数值,这些参数对于驱动程序属于全局变量。值得一提是模块参数同时出现在sysfs文件系统中(后面会介绍)。
定义一个参数可以通过宏module_param()完成:

module_param(name, type, perm);

name:用户可见参数名,模块中存放模块参数的变量名。
type:存放参数类型,可以是byte、short、ushort、int、uint、long、ulong、charp、bool或invbol。其中byte类型存放在char 类型变量中,boolean类型存放在 int 变量中,其余的类型都一致对应C语言的变量类型。
perm:指定了模块在 sysfs 文件系统下对应文件的权限,该值可以是八进制的格式,比如0644(所有者可以读写,组内可以读,其他人可以读);或是S_Ifoo的定义形式,比如S IRUGOS_IWUSR(任何人可读,user 可写);如果该值是零,则表示禁止所有的 sysfs项。
上面的宏其实并没有定义变量,必须在使用该宏前进行变量定义。通常使用类似下面的语句完成定义∶

/* 在模块参数控制下,允许在钓鱼竿上用活鱼饵 */
static int allow_live_bait = 1; /* 默认功能允许*/
module_param(allow_live_bait,bool,0644); /* 一个Boolean类型 */

这个值处于模块代码文件外部,allow_live_bait 是个全局变量。
有可能模块的外部参数名称不同于它对应的内部变量名称,这时就该使用宏 module_param_named()定义了∶

module_param_named(name,variable,type,perm);

参数 name是外部可见的参数名称,参数 variable是参数对应的内部全局变量名称。比如∶

static unsigned int max_test = DEFAULT_MAX_LINE_TEST;
module_param_named(maximum_line_test,max_test,int,0);

通常,需要用一个charp 类型来定义模块参数(一个字符串),内核将用户提供的这个字符串拷贝到内存,而且将变量指向该字符串。比如∶

static char *name;
module_param(name,charp,0);

如果需要,也可使内核直接拷贝字符串到指定的字符数组。宏module_param_string()可完成上述任务∶

module _param_string(name,string,len,perm);

这里参数 name为外部参数名称,参数 string 是对应的内部变量名称,参数 len是 string命名缓冲区的长度(或更小的长度,但是没什么太大的意义),参数 perm是sysfs文件系统的访问权限(如果为零,则表示完全禁止 sysfs 项),比如∶

static char species [BUF_LEN];
module_param_string(specifies,species,BUF_LEN,0);

可接受逗号分隔的参数序列,这些参数序列可通过宏 module_param_array()存储在C数组中∶

module_param_array(name, type,nump,pexm);

参数 name 仍然是外部参数以及对应内部变量名,参数 type是数据类型,参数 perm是 sysfs文件系统访问权限,这里新参数是nump,它是一个整型指针,该整型存放数组项数。注意由参数 name指定的数组必须是静态分配的,内核需要在编译时确定数组大小,从而保证不会造成溢出。该函数用法相当简单,比如∶

static int fish [MAX_FISH];
static int nr_fish;
module_param_array(fish,int,&nr_fish,0444);

可以将内部参数数组命名区别于外部参数,这时需使用宏∶

module_param_array_named(name,array,type,nump,perm);

其中参数和其他宏一致。
可使用 MODULE_PARM_DESC()描述参数∶

static unsigned short size = 1;
module_param(size,ushort,0644);
MODULE_PARM_DESC(size,"The size in inches of the fishing pole.");

上述所有宏需要包含 <linux/module.h> 头文件。

17.2.8 导出符号表

模块被载入后,就会被动态地连接到内核。注意,它与用户空间中的动态链接库类似,只有当被显式导出后的外部函数,才可以被动态库调用,被模块调用。在内核中,导出内核函数需要使用特殊的指令∶ EXPORT_SYMBOL()和 EXPORT_SYMBOL_GPL()。
导出的内核符号表被看做导出的内核接口,甚至称为内核 API。导出符号相当简单,在声明函数后,紧跟上 EXPORT_SYMBOL()指令就搞定了,比如∶

/*
* get_pirate_beard_color - 返回当前 priate 胡须的颜色,
* @pirate是一个指向pirate结构体的指针;颜色定义在文件<linux/beard_colors.h>中
* /
int get_pirate_beard_color(struct pirate *p)
{
	return p->beard.color;
}
EXPORT_SYMBOL(get_pirate_beard_color);

假定 get_pirate_beard_color()同时也定义在一个可访问的头文件中,那么现在任何模块都可以访问它。有一些开发者希望自己的接口仅仅对GPL-兼容的模块可见,内核连接器使用MODULE_LICENSE()宏可满足这个要求。如果你希望先前的函数仅仅对标记为GPL协议的模块可见,那么需要用:

EXPORT_SYMBOL_GPL(get_pirate_beard_color);

如果代码被配置为模块,那么必须确保当它被编译为模块时,它所用的全部接口都已被导出,否则就会产生连接错误(而且模块不能成功编译)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值