2. Linux Device Drivers -- 读书笔记(一)

编译并构建模块

创建模块源文件
// hello.c 源文件
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
    printk(KERN_INFO "Hello kernel module enter.\n");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO "Hello kernel module exit.\n");
}

module_init(hello_init);
module_exit(hello_exit);
编译
KVERS = $(shell uname -r)
obj-m := hello.o

all:
	make -C /lib/modules/$(KVERS)/build -M=$(pwd) modules
clean:
	make -C /lib/modules/$(KVERS)/build -M=$(pwd) clean
	rm *.ko

编译结束后在当前目录下生成hello.ko文件。可以通过file hello.ko查看其为elf格式文件。

加载卸载模块
insmod hello.ko # 或者
modprobe hello.ko

rmmod hello.ko # 卸载模块

modinfo hello.ko # 模块信息

lsmod | grep -i hello # 查看是否加载成功

insmod命令是通过系统调用支持模块加载的,该系统调用定义在<linux/module.c>中,函数名为sys_init_module()。

内核通过vmalloc为模块分配了内存。

版本依赖

在生成模块文件时,在自建模块的编译过程中,构建进程会将vermagic.o文件与模块相关联,以此记录其目标内核版本,编译器版本和其他信息。如果想将与当前内核版本不同的模块加载入内核,将会导致命令执行失败。

一些与版本有关的宏
#include <linux/version.h>

UTS_RELEASE // for example "2.6.10"
LINUX_VERSION_CODE // for example 2.6.10 --> 132618
KERNEL_VERSION(major, minor, release) // 比较版本时经常使用此宏定义。

平台依赖

常见的平台有IA32(x86)、x86-64等,与有明显限制的旧的80386处理器相比。现代处理器在指令集上有更丰富的功能,更快的速度去进入内核,处理器内部锁和复制数据。可以处理36bits以上的物理内存,允许这些系统有大于4GB的内存。而内核深度依赖于这些硬件。

所以,模块在特定内核下工作时,还需要配置相同目标处理器。当一个模块被加载的时候,内核会检查当前模块指定的配置并确保它们相匹配、

如果你计划编写一个通用的驱动,你将要考虑这些各种各样的因素

内核符号表

加载模块进入内核之后,模块中的符号将会加入到内核的符号表中。

新建模块的层级

  1. fat
    1. msdos
  2. usb
    1. input
    2. usbcore
  3. new device driver
    1. low-level device driver1
    2. low-level device driver2

将模块内符号提供给其他模块使用时需要用到的宏

  1. EXPORT_SYMBOL(name)
  2. EXPORT_SYMBOL_GPL(name)

_GPL仅允许GPL协议的模块使用。

关于模块的一些宏定义

#include <linux/module.h>
MODULE_AUTHOR();
MODULE_VERSION();
MODULE_DESCRIPTION();
MODULE_ALIAS();
MODULE_DEVICE_TABLE(); // 告诉用户态程序该模块支持的设备

初始化函数

static int __init initialization_function(void)
{
    /* initialization code here */
}
module_init(initialization_function);
// 声明为static,文件外不可见。
// __init 前缀表示只在初始化时运行该函数。
// __initdata 初始化数据,在其他时候不能用
// 热插拔支持 __devinit __devinitdata,在内核没有配置热插拔时,将被视为__init和__initdata
// module_init() 是必不可少的。
// 注册函数的前缀 register_

清除函数

static void __exit cleanup_function(void)
{
    /* Cleanup code here */
}
module_exit(cleanup_function);
// __exit 修饰符表示该函数仅在退出模块时使用。
// 若未使用module_exit()指定卸载函数,内核将不运行卸载行为。

初始化时错误处理

常使用goto语句进行错误处理,一般情况下

int __init my_init_function(void)
{
    int err;
    /* registration takes a pointer and a name */
    err = register_this(ptr1, "skull");
    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;
}
#include <linux/error.h>
void __exit my_exit(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_thing(item2);
    if (stuff_ok)
        unregister_stuff();
    return;
}

int __init my_init(void)
{
    int err = -ENOMEM;
    item1 = allocate_thing(arguments);
    item2 = allocate_thing2(arguments2);
    if (!item1 || !item2)
        goto fail;
    err = register_stuff(item1, item2);
    if (!err)
        stuff_ok = 1;
    else
        goto fail;
    return 0; /* success */

fail:
    my_cleanup();
    return err;
}

这种初始化错误处理方式比之前的更优雅。值得一提的是,这里的cleanup函数并没有使用__exit修饰。所以其并不是退出模块时所做的动作。

模块加载竞争

我们必须考虑模块加载时的竞争条件。如若不然,将会对整个系统的稳定性造成负面的影响。

在内核的一部分区域,内核可以使用你注册的功能(在完成注册之后)。也就是说当你的初始化还未完成时,内核就有可能会调用你的模块。所以模块代码在完成第一个注册后必须准备好响应来自内核的调用

又或者,当模块初始化已经要失败时,内核中仍可能在使用模块中注册的资源。这种情况下,应该不将其作为初始化失败。毕竟内核中已经可以使用一部分模块的功能了。如果初始化一定失败,一定要小心地等待这些仍未完成的动作直到其完成。

模块参数

在不同系统中,驱动需要知道多个参数。

这些可以从要使用的设备编号到驱动程序应该如何操作的许多方面。例如,SCSI适配器的驱动经常有控制标记命令队列,IDE驱动允许用户控制DMA操作。如果是更古老的设备,驱动甚至需要清楚地告知硬件的IO端口或者IO内存地址。

这些参数值在使用 insmod 命令或者 modprobe 命令时可以被指定。后者可以将参数值读取出来。(/etc/modprobe.conf文件)。

insmod hellop howmany=10 whom="Mom"

要支持命令行输出参数的功能,则在模块内需要先有这些参数对应的定义。我们可以通过 module_param() 宏来定义参数。

static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

数据类型:

bool、invbool、charp、int、long、short、uint、ulong、ushort。

数组参数:使用逗号分隔

module_param_array(name, type, num, perm);
// name --> 数据名
// type --> 数据类型
// num --> 数量
// perm --> 权限值

所有的参数都需要在模块内定义一个初值,使用命令行加载内核时只是改变了这些变量的值。

最后,关于module_param参数中权限的定义在<linux/stat.h>文件中。权限值将决定谁可以在sysfs中访问这些变量。如果perm被设置为0,那么该变量将不会存储在sysfs中。除此之外,可在/sys/module/目录下找到对应的模块。

S_IRUGO --> 该权限可被读取但不能被改变。

S_IRUGO | S_IWUSR --> 该权限允许root用户改变参数值。

如果参数被sysfs接口所改变,参数值就像是被模块所修改的,但是模块本身却不知道这改动。模块参数应该不可写,除非模块本身可以检测到改动或者模块需根据此做反应。

用户态驱动

优势:

  1. 完整的C库支持
  2. 便于调试
  3. 在异常时可以通过kill清除进程
  4. 内存在不用时是可交换的
  5. 也能支持并行
  6. 支持闭源软件

USB驱动的用户态驱动libusb,网站:http://libusb.sourceforge.net

还有个例子是X server,它可以知道硬件的功能,并给所有的客户端提供用户界面。

如果参数被sysfs接口所改变,参数值就像是被模块所修改的,但是模块本身却不知道这改动。模块参数应该不可写,除非模块本身可以检测到改动或者模块需根据此做反应。

用户态驱动

优势:

  1. 完整的C库支持
  2. 便于调试
  3. 在异常时可以通过kill清除进程
  4. 内存在不用时是可交换的
  5. 也能支持并行
  6. 支持闭源软件

USB驱动的用户态驱动libusb,网站:http://libusb.sourceforge.net

还有个例子是X server,它可以知道硬件的功能,并给所有的客户端提供用户界面。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值