Linux内核驱动程序之最简单的Hello World模块

这里不为真实的硬件设备编写内核驱动程序,为了方便描述编写内核驱动程序的过程,我们使用一个虚拟的硬件设备。先大概看下内核驱动程序的编写流程,例子没有实际意义,只是模板。

1.在kernel/drivers目录下新建hello文件夹,在该文件夹中创建hello.c文件:

#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void) {
    printk(KERN_ALERT "Hello, world\n");
    return 0;
}

static void __exit hello_exit(void) {
    printk(KERN_ALERT "Goodbye, cruel world\n");
}

MODULE_LICENSE("Dual BSD/GPL");

module_init(hello_init);
module_exit(hello_exit);

该模块定义了两个函数,在模块加载到内核时被调用(hello_init)以及在模块被移除时被调用(hello_exit)。module_init和module_exit使用了特别的内核宏来指出这两个函数的角色。另一个特别的宏(MODULE_LICENSE)是用来告知内核该模块带有一个自由的许可证,没有该宏的话在模块加载时内核会警告。

__init和__exit标志是给内核的暗示,给定的函数只能在加载和卸载模块时调用。

printk函数在Linux内核中定义并且对模块可用,因为在insmod加载了它之后,模块被连接到内核并且可存取内核的公用符号(函数和变量)。KERN_ALERT是消息的优先级。

2.在hello目录中新增Makefile文件,Makefile是执行编译命令make时用到的:

# If KERNELRELEASE is defined, we've been invoked from the kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
	obj-m := hello.o
# Otherwise we were called directly from the command line; invoke the kernel build system.
else
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
	PWD := $(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

其中KERNELDIR是内核源码路径。PWD是当前路径。

3.在hello目录下键入make执行Makefile文件进行编译,执行结果如下图:


4.加载和卸载模块(只有超级用户才可以加载和卸载模块)


modprobe 类似于insmod,加载一个模块到内核。它的不同在于它会查看要加载的模块是否引用了当前内核没有定义的符号。如果有,modprobe在定义相关符号的模块路径中寻找相关模块,当找到这些模块时,它也把它们加载到内核。

lsmod 查看内核中当前加载的模块列表。

模块代码中应当指定它使用的许可,如:

MODULE_LICENSE("Dual BSD/GPL");

内核识别的许可有:"GPL"(适用GNU通用公共许可的任何版本),"GPLv2"(只适用GPL版本2),"GPL and additional rights","Dual BSD/GPL","Dual MPL/GPL"和"Proprietary"。

也可以在模块中包含其他描述性的定义,如:MODULE_AUTHOR(声明模块的作者),MODULE_DESCRIPTION(声明关于模块的描述),MODULE_VERSION(版本号),MODULE_ALIAS(模块的别名)以及MODULE_DEVICE_TABLE(模块支持的设备)。

编写习惯是把以MODULE_开头的声明性代码放在文件的末尾。

初始化中的错误处理

在注册内核模块时可能会失败,因为即便最简单的操作也需要分配内存,分配的内存可能不可用,因此模块代码必须一直检查返回值,并确认操作执行成功。如果在注册模块时发生错误导致模块不能正常运行,则必须注销失败前所执行的操作。错误恢复常用goto语句。如:

static int __init my_init_function(void) {
	int err;
	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;
}

上面例子代码试图注册3个模块,在注册失败的情况下使用了goto语句以便执行注销操作。err是一个错误码,在Linux内核里错误码是负数,定义在<linux/errno.h>中,如-ENODEV,-ENOMEM等等。

卸载时按照注册时相反的顺序执行注销操作,卸载模块代码:

static void __exit my_cleanup_function(void) {
	unregister_those(ptr3, "skull");
	unregister_that(ptr2, "skull");
	unregister_this(ptr1, "skull");
	return;
}

模块参数

在使用insmod或者modprobe加载模块时可以指定可变参数的值,后者也可以从它的配置文件(/etc/modprobe.conf)读取参数的值。参数用module_param宏定义来声明,它定义在moduleparam.h文件中。module_param使用了3个参数:变量名、变量类型以及权限掩码。如对hello world模块进行改进(称为hellop),增加两个参数:一个整型值howmany,一个字符串whom:

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

则可以这样加载:

insmod hellop howmany=10 whom="Haha"

一旦以这样的方式加载,则会打印10次“hello, Haha”。

模块参数支持许多类型:

bool、invbool:invbool类型与bool相反,真值为false。

charp:字符指针值。

int,long,short,uint,ulong,ushort:u开头的为无符号值。

数组参数module_param_array(name, type, num, perm);

name是数组的名字,type是数组元素的类型,num是一个整型变量,perm是通常的权限值。

权限值定义在<linux/stat.h>文件中,权限为S_IRUGO时所有人可读不可写,为S_IRUGO|S_IWUSR则允许root用户改变参数的值。

当前进程

尽管内核模块不像应用程序一样顺序执行,但是内核做的大部分操作是代表一个特定进程的。内核代码可以通过存取全局项current引用当前进程,它在<asm/current.h>中定义,它产生一个指向task_struct结构的指针,在<linux/sched.h>中定义。current指针指向当前正在运行的进程。在一个系统调用执行期间,利润open或者read,当前进程是发出调用的进程,内核代码可以通过使用current来使用进程特定的信息。

如下面的语句是通过存取task_struct结构中的某些字段打印了当前进程的进程id和命令名称:

printk(KERN_INFO "The process is \"%s\" (pid %i)\n", current->comm, current->pid);
下一篇在此模板基础上编写一个字符驱动。

  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值