2.7. 初始化和关闭
模块的初始化函数负责注册模块所提供的任何设施。这里的设施指的是一个可以被应用程序访问的新功能,它可能是一个完整的驱动程序或者仅仅是一个新的软件抽象。初始化函数定义通常如下所示:
static int __init initialization_function(void)
{
/* Initialization code here */
}
module_init(initialization_function);
分析:
初始化函数应该被声明为static ,因为这种函数在特定文件之外没有其它意义。因为一个模块函数如果要对内核其它部分可见,则必须被显式导出,这并不是什么强制性规则。__init标记对内核来讲是一种暗示,表明该函数仅在初始化期间使用。在模块被加载之后,模块加载器就会将初始化函数扔掉,可将函数占用的内存释放出来。__init和__initdata的使用是可选的,虽然有点繁琐,但是很值得使用。注意:不要在结束初始化之后仍要使用的函数或者数据结构上使用这两个标记。在内核源代码中可能还会遇到__devinit 和 __devinitdata,只有在内核未配置支持热插拔的情况下,这两个标记才会被翻译为__init和__initdata。
module_init的使用是强制性的。这个宏会在模块的目标代码中增加一个特殊的段,用于说明内核初始化函数所在的位置。没有这个定义,初始化函数不会被调用。
模块可以注册许多不同类型的设施,包括不同类型的设备、文件系统、密码变换等。对每种设施,对应有具体的内核函数用来完成注册。传递到内核注册函数中的参数通常是指向用来描述新设施以及设施名称的数据结构指针,而数据结构通常包含指向模块函数的指针,这样,模块体中的函数就会在恰当的时间被内核调用。
能够注册的设施类型包括串口、杂项设备、sysfs入口、/proc文件、可执行域以及线路规程等。很多可注册的设施所支持的功能属于软件抽象范畴,而不与任何硬件直接相关。这种类型的设施能够被注册,是因为它们能够以某种方式集成到驱动程序功能当中。
还有其他一些设施可以注册为特定驱动程序的附加功能,但它们的用途有限,它们使用内核符号表一节中提到的层叠技术。可以在内核源码中grep EXPORT_SYMBOL ,并找出由不同驱动程序提供的入口点。大部分注册函数以 register_做前缀,因此找到它们的另一种方法是在内核源码中grep register_。
2.7.1. 清除函数
每个重要的模块都需要一个清除函数,该函数在模块被移除前注销接口并向系统中返回所有资源。该函数的定义如下:
static void __exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);
清除函数没有返回值,因此被声明为void。 __exit修饰词标记该代码仅用于模块卸载(编译器将把该函数放在特殊的ELF段)。如果模块被直接内嵌到内核中,或者内核的配置不允许卸载模块,则被标记为__exit的函数将被丢弃。出于以上原因,被标记为__exit的函数只能在模块被卸载或者系统被关闭时调用,其它的任何用法都是错误的。module_exit声明用于帮助内核找到模块的清除函数是必需的。
如果一个模块为定义清除函数,则内核不允许卸载该模块。
2.7.2. 初始化过程中的错误处理
当在内核中注册设施时,要时刻铭记注册可能会失败。即使是最简单的动作,都需要内存分配,而所需的内存可能无法获得。因此模块代码必须始终检查返回值,并确保所请求的操作已真正成功。
如果在注册设施时遇到任何错误,首先要判断模块是否可以继续初始化。通常,在某个注册失败后可以通过降低功能来继续运转。因此,只要可能,模块应该继续向前并尽可能提供其功能。
如果在发生了某个特定类型的错误之后无法继续装载模块,则要将出错之前的任何注册工作撤销掉。Linux中没有记录每个模块都注册了哪些设施,因此,当模块的初始化出现错误之后,模块必须自己撤销已注册的设施。如果由于某种原因未能撤销已注册的设施,则内核会处于一种不稳定状态,这是因为内核中包含了一些指向并不存在的代码的内部指针。在这种情况下,唯一有效的解决办法是重新引导系统。因此,必须在初始化过程出现错误时认真完成正确的工作。错误恢复的处理有时使用goto语句比较有效。通常情况下很少使用goto,但在处理错误时(可能是唯一的情况)goto却非常有用。错误情况下的goto的仔细使用可避免大量复杂的、高度缩进的结构化逻辑。因此,内核经常使用goto来处理错误。不管初始化过程在什么时候失败,下面的例子(使用了虚构的注册和撤销注册函数)都能正确工作:
int __init my_init_function(void)
{
int err;
/* 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; /* 返回错误 */
}
这段代码准备注册三个(虚构的)设施。在出错的时候使用goto语句,来撤销出错时刻以前所成功注册的设施。
另一种观点不支持goto的使用,而是记录任何成功注册的设施,然后在出错的时候调用模块的清除函数。清除函数将仅仅回滚已成功完成的步骤。这种替代方法需要更多的代码和CPU时间,在追求效率的代码中使用goto语句是最好的错误恢复机制。
my_init_function 的返回值err是一个错误编码。在Linux内核中,错误编码是定义在<linux/errno.h>中。如果不想使用其他函数返回的错误编码,而想使用自己的错误编码,则应该包含<linux/errno.h>,以使用诸如-ENODEV、 -ENOMEM之类的符号值。每次返回合适的错误编码是一个好习惯,因为用户程序可以通过 perror函数或类似的途径将它们转换为有意义的字符串。
模块的清除函数需要撤销初始化函数所注册的所有设施,并且习惯上(但不是必须的)以相反于注册的顺序撤销设施:
void __exit my_cleanup_function(void)
{
unregister_those(ptr3, "skull");
unregister_that(ptr2, "skull");
unregister_this(ptr1, "skull");
}
如果初始化和清除工作涉及很多设施,则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; /* success */
fail:
my_cleanup();return err;
}
如这段代码表示,根据调用的注册、分配函数的语义,可以使用或不使用外部标志来标记每个初始化步骤的成功。不管是否需要使用标志,这种方式的初始化能够很好地扩展到对大量设施的支持,因此比前面介绍的技术更具优越性。需要注意的是,因为清除函数被非退出代码调用,不能将清除函数标记为__exit。
2.7.3. 模块装载竞争
首先,在注册完成之后,内核的某些部分可能会立即使用刚刚注册的任何设施。在初始化函数还在运行时,内核完全可能会调用自己的模块。因此,在首次注册完成之后,代码就应该准备好被内核的其它部分调用;在用来支持某个设施的所有内部初始化完成之前,不要注册任何设施。
还必须考虑,当初始化失败而内核的某些部分已经使用了模块所注册的某个设施时应该如何处理。如果这种情况可能发生在自己的模块上,则根本不应该出现初始化失败的情况,毕竟模块已经成功导出了可用的功能及符号。如果初始化一定要失败,则应该仔细处理内核其它部分正在进行的操作,并且要等待这些操作的完成。