《LINUX设备驱动程序》学习笔记 ——03

发布时间 2023-09-16 11:21:01作者: 成信吴彦祖(略胜亿筹)

1. 学习模块前的一些基础知识

  头文件:内核是一个特定的环境,对需要和它接口的代码有其自己的一些要求,所以大部分的模块代码中都会包含相当数量的头文件,其中有几个头文件是专门用于模块的,因此会出现在每个可装载的模块中:

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

  对于部分需要在装载模块时进行传参,我们还需要加上头文件 moduleparam.h 。

  许可证:linux中内核模块分为专用模块、通用模块,如果不加上 MODULE_LICENSE 声明许可证,则会被视为私有模块,开发者不太愿意帮助内核装载私有模块而导致出现问题的用户。

2. 初始化和关闭

  初始化函数负责注册模块所提供的任何设施。定义通常如下:

static int __init initialization_founction(void)
{

}

  定义初始化函数时,static 声明该函数只在该文件中有意义,但这并非强制规定,因为即使不带有 static ,如果在内核其他文件中调用该函数也必须被显式导出(一种规范)。__init 在内核中表示该函数仅在初始化期间被使用,在模块装载完成后内核就会将该函数丢掉,这样该函数占用的空间就会被释放。__init 和 __initdata是可选的。(不允许在初始化完成后还要使用的函数中加这两个标记)。

  module_init 的使用是强制性的,没有这个定义,初始化函数永远不可能被调用。该函数会在模块的目标代码中增加一个特殊的段,用于说明初始化代码的位置。

  模块可以注册不同类型的设施,包括不同类型的设备、文件系统、密码变换等。对于每种设施对应有具体的内核函数来完成注册。在这期间,指针会被广泛的使用。

  grep register_ 可以找到大部分注册函数,因为 register_ 是注册函数的前缀。

3. 清除函数

  每个重要的模块都需要一个清除函数,函数定义如下

static void __exit cleanup_function(void)
{

}

  该函数没有返回值,所以被声明为void。__exit 修饰次标记该代码仅用于模块卸载,如果【1】模块被内嵌到内核中 或者【2】内核的配置不允许卸载模块,则被标记为 __exit 的函数会被简单的丢弃。(故被 __exit 标记的函数只允许在模块被卸载,或者系统关闭时才能被调用)

  如果一个模块没有定义清除函数,则内核不允许卸载该模块。

4. 初始化过程中的错误处理(要么继续前进,要么全身而退[goto函数有奇效])

  当我们在内核中注册设施时要时刻铭记注册可能会失败。即使是最简单的动作都需要内存分配,而所需要的内存可能无法获得。因此模块代码必须始终检查返回值,并确保所请求的操作已真正成功。

  继续前进:在注册设施时遇到任何错误,首先要判断模块是否可以继续初始化。通常,在某个注册失败后可以通过降低功能来继续运转。因此,只要可能,模块应该继续向前,并尽可能提供其功能。

  全身而退:LINUX中没有记录模块都注册了哪些设施,因此必须由模块自行撤销已注册的设施。若出于某种原因无法自行撤销,则内核会处于一种不稳定状态,这是因为内核中包含了一些指向并不明确的内部的代码指针。此时唯一有效的解决办法是重新引导系统。

5. goto和清除函数在撤销已注册设施时的选择

  goto 函数虽然经常被诟病,但是在处理错误时非常有用(有时甚至是唯一的情况)。因为通过goto处理错误可以避免大量复杂的、高度缩进的 ”结构化“ 逻辑。

int __init 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;
	
    fail_those: unregister_this(ptr2, "skull");
	fail_that: unregister_this(prt1, "skull");
	fail_this: return err;
}

  /******************************************* 以上代码中的处理顺序需要注意 ************************************/

  以上是一个简单的通过goto进行错误处理的示例,在出错时候使用goto语句,它将只撤销出错时刻以前所成功注册的那些设施。

  如果不用goto,有一种方法是通过记录任何成功注册的设施,然后在出错的时候调用模块的清除函数。清除函数将仅仅回滚已成功完成的步骤。然而这种方案需要更多的代码和CPU时间,在效率上不及 goto ,goto语句仍然是最好的错误恢复机制。

  但是当涉及大量设施的时候,goto 则会变得难以管理,因为所有用于清除设施的代码在初始化函数中重复,同时一些符号交织在一起。而此时合理使用清除函数能够减少代码的重复并且使得代码更清晰更有条理。以下是一个简单示例:

struct something *item1;
struct something *item2;
int stuff_ok;
/* 清除函数(注意,此处清除函数并没有被__exit标记!) */
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 = -ENOMEN;
    
    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;

fail:
    my_cleanup();
    return err;
}