您的位置:首页 > 运维架构 > Linux

LDD:Ch 2 构造和运行模块

2015-07-30 19:57 369 查看
Sample Code可以在https://github.com/ykdsea/linux-ldd-samples.git 下载。

本章只讨论模块,不涉及特定类型的设备。模块只是实现kernel功能的特定方式,不是唯一方式。

Hello world模块

module的相关的宏定义都在include/linux/module.h中。

module_init,module_exit也是宏,在init.h中定义。

init和exit函数指针的声明如下:

typedef int (*initcall_t)(void);  //注意,返回0才是success,负数指明错误号。
typedef void (*exitcall_t)(void);


用户空间和内核空间

模块运行在内核空间,而应用程序运行在用户空间。

这两个概念是操作系统运行的理论基础之一。操作系统需要保护资源不被非法访问,在CPU能够保护系统不被应用软件破坏的时候才能完成。一般通过cpu的级别来控制访问的权限,目前的cpu一般都具有2个以上的保护级别,Unix只使用其中的两种,最高和最低的两个来实现用户空间和内核空间。

内核空间和用户空间,除了有不同的优先权等级,也都有自己的内存映射。

当执行系统调用或者发生硬件中断的时候,unix将运行模式从用户空间切换到内核空间。

系统调用的时候,内核代码运行在进程的上下文中,能够访问到进程的地址空间。

而中断是和用户进程异步的,和进程是没有关系的。

模块是在内核空间中运行来提供功能。

内核中的并发

内核编程要记住,任意时候都可能有很多事情在同时发生。

有几个原因要求我们注意并发的问题:

系统中会同时有多个进程运行,并且可能有多个进程同时要去使用我们的驱动程序。

驱动程序在处理任务的时候,很可能被中断打断。

还有一些软件的机制也会异步运行,比如定时器。

SMP系统中,极大可能有多个cpu要去使用我们的驱动程序。

因此,驱动程序必须是可重入的。

当前进程的访问

内核中通过current变量来访问当前进程,它是指向struct task_struct的指针,通过current->comm可以访问到当前进程的程序名,current->pid可以找到当前进程的进程号。

current在早期是一个全局的变量,在

其他一些小细节

kernel的stack很小,可能只有4096个字节,所以不能使用过大局部变量,如果必须需要,那么要通过动态分配的方式来申请。

带有”_ _”标示的api,一般是接口的底层组件,它表明意义是“谨慎使用,后果自负”

内核不支持浮点运算。如果打开了浮点的支持,会需要额外保存恢复浮点寄存器的状态,这样没有意义,也浪费时间。

编译和装载

编译模块

首先要确保编译工具的版本的正确性,在documention/Changes中可以查到。

其次是编写module的makefile。

obj-m : [moduel_name].o
[module_name]-objs := file1.o file2.o

default:
make -C [kernel_folder] M=$(shell pwd) modules


obj-m是指定module的object名字;

[module_name]-objs是声明了module object要如何生成;

-C可以在make file中看到,是kernel的root目录;

PS:需要将对应内核先配置再编译一次,才能编译模块。如果内核的输出被重新指定,那么模块的make也要加上-O参数来指定编译目录。

装载和卸载模块

insmod和ld有些类似,将模块的代码和数据load到内存中去,然后用内核的符号表解析模块中未解析的符号。和ld不同的是,insmod只是修改了内存中的代码,不会修改磁盘上的符号。

insmod可以支持接受命令行的参数,并将这些参数传递给具体的模块。这样,模块在不同的情况下由用户来进行配置,有更好的灵活性。

modprobe也是用来装载模块,和insmod不用地方在于,它在装载时候会搜索其他依赖的模块,并将其也装载进来。

对于insmod来说,如果依赖的module没有装载,会报”resolvedsymblos”的错误。

lsmod列出已经insmod好的模块,lsmod是通过读取/proc/modules来获取模块信息的。

一个小规则, “sys_”开头的函数,只有系统调用函数才是这样的。

版本依赖

modversins,是由CONFIG_MODVERSIONS定义的,他会对内置的一些函数的symbol加上特定的版本信息,这样的情况下,如果Module也同样使用modversions,则需要严格的保证两者在同样的版本下。

内核不会假定一个给定的模块是针对正确的内核版本构造的,所以我们需要方法来帮助内核来检测模块的兼容性:

在模块的构造过程中,将模块和内核树中的vermagic.o链接,vermagic.o中包含了大量的有关内核的信息,比如目标内核的版本,编译器版本以及一些重要的配置变量的设置,这些信息可以用来检查模块和内核的额兼容性。如果发生兼容问题的时候,会报”Invalid module format”的log。(在3.14kernel中默认已经链接了vermagic的值,不需要开发者额外做什么)

modversions,kernel和module编译的时候,都使用modversions,这样会进行很严格的匹配。

如果需要module实现能够支持多个版本的linux兼容,因为内核开发接口的变化,则模块需要使用宏来区分不同的linux版本。

linux在linux/version.h中定义相关的宏,包含以下常用的宏:

UTS_RELEASE:给出内核版本的字符串,如”2.6.10”。

LINUX_VERSION_CODE:版本号的二进制表示。如版本2.6.10,整型数为132618,二进制位0x02060a,每8个bit为对应一个版本信息,这也意味着每个版本号的区间只有0~255.

KERNEL_VERSION(major,minor, release):从版本号创建出整型数的版本号平也就是和LINUX_VERSION_CODE一样的形式。

平台依赖

主要是考虑到不同的处理器情况下如何正确的工作,这里我们一样使用vermagic.o来让内核帮助我们检测我们的模块针对处理器的编译是否和内核一致。

内核符号表

模块可以向内核导出一些符号,来方便别的模块使用,这样我们可以基于这个模块够构建出更上层的模块,比如说usb输入设备就是构造在usbcore和input模块之上的。

Linux内核提供了一组宏来管理符号对模块外部的可见性来减少名空间的污染,并且可以适当的隐藏一些信息。

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);


带”_GPL”的只能被GPL模块所使用,这两个宏要在全局部分声明,在函数内部声明是无效的。

export出去的symbol会被保存在module可执行文件一个特定的section中,在内核加载模块的时候,通过这个段来寻找模块导出的变量。

初始化和关闭(init函数)

module的初始化函数一般如下:

static int __init initialization_function(void)
{
/*implement here*/
}
module_init(initialization_function);


static是必须的,因为Init只对本module有效。

__init对于最终object没有实际的意义,它是告诉内核,这个函数仅仅在初始化的时候使用,在用完之后,内核装载器可以把这个函数占用的内存释放出去了。

__init_data和__init类似,修饰的是变量而已。

当然在内核没有被配置成支持热插拔的情况下才会如此这两个声明。

module_init实际是在ko中生成了一个特殊的段(或者说是一个symbol?),让内核能够找到初始化函数。

清除函数(exit函数)

和init类似,每个模块都要注册一个exit函数来清除模块所使用的资源,一般如下:

static void __exit cleanup_function(void)
{
/* implement here*/
}
module_exit(cleanup_function);


__exit用来修饰模块的exit函数,如果该模块直接内嵌于内核或者内核不允许卸载模块,则这个__exit修饰的函数会直接被丢弃。

exit函数对于模块不是必须的,可以不定义,不定义则意味着模块不能被卸载。

初始化过程中的错误处理

模块代码在初始化过程中必须始终检查所有操作返回值确保所有的操作都已经真正的成功了。

如果初始化的时候出现了错误,首先判断模块是否还能继续初始化,否则只能进行出错处理来退出。

通常建议模块在出错的时候,可以降低需求和提供的功能来让模块可以工作起来。

出错处理中,需要将所有已经申请的资源都释放掉,否则系统会处于一个不稳定状态。

出错处理,比较常见的是使用goto语句,来统一进行资源的释放。

另外一种方式是记录下申请的资源,然后调用exit函数来清除已经申请的资源(注意不能声明__exit)。

出错返回的错误编码在linuxt/errno.h中定义的负整数。

模块装载竞争

竞态(race conditon),对于我们编写模块的时候要考虑下面的两个点:

模块注册后,内核可能会立刻使用注册好的任何功能。

当初始化失败的时候,内核某些模块已经开始使用模块祖册好的功能的时候要如何处理。

模块参数

模块的参数使用module_param宏来声明,宏在moduleparam.h中定义。

module_param定义需要3个参数,变量名字,变量类型和用于sysfs入口的访问掩码,声明必须要放在全局部分,一般放在source的头部。一般如下:

staticchar *whom = "world";
module_param(whom, charp, S_IRUGO)


内核支持模块参数的类型有:bool, invbool, charp, int ,long ,short, uint, ulong, ushort。如果我们需要的类型不在其中,也可以自定义类型。

模块也支持数组参数,一般如下:

int numlist[10]={0};
module_param_array(numlist, int, 100, IRUGO);


所有的模块参数都需要有一个默认值,来确保可以不带参数装载模块也能正常工作。

模块参数一般会在/sys/modules/[module_name]/parameters/中生成对应的sysfs文件,如果声明的perm为0,则不会有这个sysfs入口。

如果模块参数通过sysfs被修改,内核不会对模块进行通知,需要模块自己检测到,并作出相应的变化。

模块参数的权限,不能将写权限放给Other用户,编译系统会报“negative width in bit-field ‘’”的错误。

在用户空间编写驱动程序

用户控件编写驱动程序相对容易,在某些情况下是替代内核空间驱动程序的一个好方法。

用户空间驱动程序有如下优点:

和C库链接,实现功能会更加的方便,可以不在需要提供策略的用户程序。

可以用一般的调试器进行调试,相对内核调试更加方便。

如果用户空间驱动程序挂了,只要杀掉重启即可,不会对系统造成严重影响。

在用户空间,内存可以被交换,如果驱动不经常使用,这样可以节省一些空间。而内核驱动程序一定是一直驻在内存中。

仍然可以支持对设备的并发访问。

如果需要对驱动程序闭源,那么用户空间的驱动程序可以更加容易避免内核的许可问题。

一般,用户空间的驱动程序实现为一个服务进程,它会代替内核作为硬件控制的唯一代理,客户应用程序连接到服务进程来和实际设备交互。

用户驱动程序一样有其缺点:

不可以使用中断。

只有通过mmap方式来直接访问内存。

只有调用ioperm或者iopl才能访问IO端口,并且访问很慢。

响应相比会慢一些。

如果驱动程序被交换出去,那么响应会速度会非常慢。

不能处理一些很重要的设备,比如网络和块设备。

android的HAL和对应的service实际就是构成一个用户空间的驱动程序,这样就规避内核的GPL许可问题,厂商可以直接给出HAL的so来驱动硬件。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  kernel linux