模块的初始化和关闭
更多内容请参考Linux设备驱动程序学习----目录
1. 初始化函数
模块的初始化函数负责注册模块所提供的任何设施,即可以被应用程序访问的新功能,可能是一个完整的驱动程序或者仅仅是一个新的软件抽象。初始化函数的定义通常如下所示:
static int __init initialization_function(void)
{
// 初始化代码
return 0;
}
module_init(initialization_function);
初始化函数被声明为static,因为初始化函数在特定文件之外没有其他意义。__init标记表明该函数仅在初始化期间使用。在模块被装载之后,模块装载器就会将初始化函数扔掉,可将该函数占用的内存释放出来。
注意:不要在结束初始化之后仍要使用的函数或数据结构上使用__init和__initdata标记。对于__devinit和__devinitdata,只有在内核未被配置为支持热插拔设备的情况下,才会被翻译为__init和__initdata。
module_init()宏的使用是强制性的,会在模块的目标代码中增加一个特殊的段,用于说明内核初始化函数所在的位置。如果没有这个定义,初始化函数永远不会被调用。
2. 清除函数
每个模块都需要一个清除函数,在模块被移除前注销接口并向系统中返回所有资源。该函数定义如下:
static void __exit cleanup_function(void)
{
// 清除代码
}
module_exit(cleanup_function);
清除函数没有返回值,__exit修饰词标记该代码仅用于模块卸载,编译器会把该函数放在特殊的ELF段中。如果模块被直接编译到内核中,或者内核配置不允许卸载模块,则被标记为__exit的函数将被直接丢弃。所以被标记为__exit的函数只能在模块被卸载或者系统关闭时被调用,其他任何用法都是错的。module_exit()声明对于内核找到模块的清除函数是必需的。如果一个模块未定义清除函数,则内核不允许卸载该模块。
3. 初始化过程中的错误处理
在内核中注册设施时,注册可能会失败。即使最简单的动作,都需要内存分配,而所需的内存可能无法获得。因此模块代码必须始终检查返回值,并确保所请求的操作已真正执行成功。如果在注册设施时遇到错误,首先要判断模块是否可以继续初始化,只要可能,模块应该继续向前并尽可能提供其功能。
如果在发生了某个特定类型的错误之后无法继续装载模块,则要将出错之前的所有注册工作都撤销掉。即当模块的初始化出现错误之后,模块必须自行撤销已注册的设施。如果未能撤销已注册的设施,则内核会处于一种不稳定状态,这时,唯一有效的解决办法就是重新引导系统。所以必须在初始化过程出现错误时认真完成正确的工作。
错误恢复的处理有时使用goto语句非常有效。正常情况下,很少使用goto,但是唯一在错误处理时却非常有效。内核经常使用goto来处理错误。如下例子所示:
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; // 成功
fail_those:
unregister_that(ptr2, "skull");
fail_that:
unregister_that(ptr1, "skull");
fail_this:
return err; // 返回错误
}
在出错的时候使用goto语句,将只撤销出错时刻以前所成功注册的那些设施。
另一种方法是,记录任何成功注册的设施,在出错的时候调用模块的清除函数。清除函数将仅仅回滚已成功完成的步骤。这种方法需要更多的代码和CPU时间,因此在追求效率的代码中使用goto语句是最好的错误恢复机制。
在Linux内核中错误编码是定义在
模块的清除函数需要撤销初始化函数所注册的所有设施,并且习惯上以相反于注册的顺序撤销设施,如下所示:
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;
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 (!item1 || !item2)
goto fail;
err = register_stuff(item1, item2);
if (!err)
stuff_ok = 1;
else
goto fail;
return 0; // 返回成功
fail:
my_cleanup();
return err; // 返回错误
}
如上代码所示,根据调用的注册/分配函数的语义,可以使用或不使用外部标记来标记每个初始化步骤的成功。这种方式的初始化能很好地扩展到对大量设施的支持。注意:因为清除函数被非退出代码调用,因此不能将清除函数标记为__exit;
4. 模块装载竞争
模块装载中也存在竞态。在模块注册完成之前,内核的某些部分可能会立即使用我们刚刚注册的任何设施,即在初始化函数还在运行的时候,内核就完全可能会调用我们的模块。因此,在首次注册完成之后,代码就应该准备好被内核的其他部分调用;在支持某个设施的所有内部初始化完成之前,不要注册任何设施。
当模块初始化失败而内核的某些部分已经使用了模块所注册的某个设施时,此时根本不应该出现模块初始化失败,因为模块已经成功导出了可用的功能及符号。如果初始化一定要失败,则应该仔细处理内核其他部分正在进行的操作,并且要等待这些操作的完成。
更多内容请参考Linux设备驱动程序学习----目录