第二章 构造和运行模块

一、Hello World模块

hello.c

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

MODULE_LICENSE("Dual BSD/GPL");

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

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

module_init(hello_init);
module_exit(hello_exit);
hello.c

Makefile

# if KERNELRELEASE is defined, we've benn 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
Makefile

moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核 

printk函数和print函数类似,字串KERN_ALERT是消息优先级

 

与应用程序的不同

应用程序:

  • 应用程序从头执行到尾。
  • 可以不管理资源的释放或者其他的清理工作。
  • 可以调用它并未定义的函数,通过解析外部引用从而使用适当的函数库。
  • 开发过程中,段错误是无害的。并且可以使用调试器跟踪到源代码中的问题所在。

内核:

  • 模块初始化的函数任务就是为以后调用模块函数预先做准备。
  • 模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.。
  • 模块能调用的函数仅仅是由内核导出的哪些函数,
  • 一个内核的段错误即使不影响整个系统,也至少会杀死当前的进程。

 

用户空间和内核空间

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

系统至少有两种级别,内核(超级模式),最低级模式(用户模式),以此控制对硬件的直接存取以及对内存的非法访问。

内核空间和用户空间,都有自己对应的内存映射(自己的地址空间)

 

内核的并发

即使最简单的内核模块,都需要在编写时铭记:同一时刻,可能会有许多事情正在发生。

当你查看内核 API 时, 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名通常是一个低层的接口组件, 应当小心使用.

如果不注意并发问题,可能导致出现很难调试的灾难性错误。

 

内核的版本检查

UTS_RELEASE
这个宏定义扩展成字符串, 描述了这个内核树的版本. 例如, "2.6.10".

LINUX_VERSION_CODE

这个宏定义扩展成内核版本的二进制形式, 版本号发行号的每个部分用一个字节表示. 例如, 2.6.10 的编码是 132618 ( 就是, 0x02060a ). [4]4有了这个信息, 你可以(几乎是)容易地决定你在处理的内核版本.
KERNEL_VERSION(major,minor,release)
这个宏定义用来建立一个整型版本编码, 从组成一个版本号的单个数字. 例如,KERNEL_VERSION(2.6.10) 扩展成 132618. 这个宏定义非常有用, 当你需要比较当前版本和一个已知的检查点.

 

二、模块编译和装载

首先,读者应该确保具备了正确的版本编译器、模块工具和其他必要的工具。

只有系统调用的名字前带有sys_前缀,而其他函数都没有这个前缀。这种命名在源码中grep系统调用时非常方便。

  • insmod:可以接收一些命令行选项(参见手册),并且可以在装载时给模块变量。
  • rmmod:如果内核认为模块依然使用,或者内核被配置为禁止移除,则无法移除模块。
  • lsmod:通过读取/proc/modules虚拟文件来获得这些信息。有关已装载的模块信息在/sys/module下找到
  • 查看系统日志文件(/var/log/messages或者系统配置使用的文件),将看到导致模块装载失败的具体原因

 

模块的堆叠

可以使用modeprobe,类似于insmod。

如果你的模块需要输出符号给其他模块使用,应当使用下面的宏定义:

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);

 

几乎所有模块都有

#include <linux/module.h>

#include <linux/init.h>

  • MODULE_LICENSE("GPL")
  • MODULE_AUTHOR(声明谁编写了模块)
  • MODULE_DESCRIPION(一个人说明的关于模块做什么的声明)
  • MODULE_VERSION(一个diamante修订版本号)
  • MODULE_ALIAS(模块为人所知的另一个名字)
  • MODULE_DEVICE_TABLE(来告知用户空间,模块支持哪些设备)

 

模块的初始化和关停

模块的初始化通常是:

static int __init initialization_function(void)

{

  /* Initialization code here */

}

module_init(initializatioon_function);
module_init

__init标志是给内核的一个暗示,给定的函数只是在初始化使用,加载后会丢掉这个初始化函数。

__initdata给只在初始化时用的数据,后面可能还会有__devinit和__devinitdata

 

清理函数

static void __exit cleanup_function(void)

{

  /* Cleanup code here */

}

module_exit(cleanup_function);
module_exit

清理函数没有返回值,__exit修饰符用于模块卸载

 

初始化汇总的错误处理

在之策内核设施时,注册可能失败,几遍最简单的动作常常需要内存分配,分配的内存可能不可用。

因此模块代码必须一致检查返回值,并且确认要求的操作实际上已经成功。

如果注册时发生任何错误,首先第一的事情是决定模块是否能够五路你如何继续初始化它自己。

任何时候,你的模块应当尽力向前,并提供事情失败后具备的能力。

 

如何模块证实失败,不能完全加载,必须取消失败前注册动作。因此初始化某个点失败,模块必须自己退回所有东西。否则内核就处于不稳定状态。

  • 使用goto是处理错误回复的最好情况
  • 使用错误编码是一个好习惯,诸如-ENODEV、-ENOMEM
int _init my_init_function(void)
{
  int err;

  err = register_this(ptr1, "skull");  /* registration takes a pointer and a name */
    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;        /* propagate the error */
}
错误处理

 err是错误码,在<linux/errno.h>中。显然在模块清理函数中,是按照注册时相反的顺序注销设施。

void __exit my_cleanup_function(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_thing2(item2);
    if(stuff_ok)
        unregister_stuff();
    return;
}

int __init my_init(void)
{
    int err = -ENOMEM;

    item1 = allocate_thing(arguments);
    item2 = allocate_thing2(arguments2);
    if(!item2 || !item2)
        goto fail;

    err = register_stuff(item1, item2);
    if(!err)
        stuff_ok = 1;
    else
        goto fail;
    return 0;

fail:
    my_cleanup();
    return err;
}
goto清理

 

模块加载竞争

内核的某些别的部分会在注册完成之后马上使用任何你注册的设施。换句话说,内核将调用进你的模块,在你初始化函数任然在运行时,所以你的代码必须准备好被调用,一旦完成注册。

 

模块参数

insmod hellop howmany=10 whom="Mom"
模块参数

在模块中参数用module_param宏定义来声明,它定义在moduleparam.h module_param使用了3个参数:变量名,类型,以及一个权掩码用来做一个辅助的sysfs入口。

static char *whom = "world";
static int howmany = 1;
mdoule_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
模块参数用例

模块参数支持许多类型:

bool、invbool  一个布尔型,invbool颠倒了值

charp      字符指针

int、long、short、uint、ulong、ushort   整型,u开头是无符号值

数组参数,用逗号间隔的列表提供的值,模块加载者也支持。

module_param_array(name, type, num, perm);

name:数组名

type:数组元素类型

num:整型变量

perm:通常的权限值

 

posted @ 2018-06-06 23:35  习惯就好233  阅读(306)  评论(0编辑  收藏  举报