Initialization and Shutdown
初始化和关闭
模块初始化函数注册由模块提供的任何功能。这些新的功能(整个驱动、新的软件抽象)可以被应用程序访问。实际的初始化定义是这样的:
初始化函数应该是static的,因为他们在外部文件中不可见。__init标记表示这个函数仅仅在初始化时使用。模块加载器在模块加载之后会丢弃初始化函数,标记他的内存区域可用。在初始化时,有一个类似的标记__initdata,表示数据只是在初始化时使用,不过要确认不能用在初始化完成之后继续使用的函数或数据结构上。在内核源代码中,可能还会遇到__devinit和__devinitdata,只有当内核没有配置支持热插拔(hotplug)设备时候,转换成__init和_initdata.module_init的使用是强制的,这个宏添加特别的段到模块目标代码中,表明在哪里可用找到初始化函数。如果没有这个定义,初始化函数就不会被调用。
模块可用注册很多不同类型的设备,文件系统,加密转换等等。对于每个设施,都有一个特定的内核函数来完成注册。传递给内核注册函数的参数通常是一些描述要注册的新设施和他们的名字的数据结构的指针。数据结构包含模块函数指针,模块中的函数也就是这样被调用的。
1. The Cleanup Function
清除函数
每个模块都需要一个清除函数,注销接口,在模块退出的时候返回资源给系统。
清除函数没有返回值,所以要声明为void。__exit修饰符标记这段代码作为模块卸载时使用(通过编译器把它放置到一个特殊的ELF段)。如果模块直接建立到内核或者是内核被配置成禁止模块卸载,那么标记为__exit的函数会被丢弃。所以标记__exit的函数只能存在于模块卸载或系统关闭中,任何其他场合都是错误的。module_exit宏使内核可以找到模块清除函数。如果模块没有定义清除函数,那么内核不会允许模块被卸载。
2. Error Handling During Initialization
初始化时的错误处理
必须要考虑到,在设施注册到内核时,可能会出错。即使最简单的请求内存,都有可能失败。所以模块必须要检查返回值,确保请求操作成功。
如果在注册设施时,出现错误,第一件事情是决定模块是否可以继续初始化。通常,模块在初始化错误后,如果必要的话,可以继续降低功能执行。只要有可能,模块应该一直向前,在发生错误后提供一些功能。如果在发生了某种错误之后,模块不能加载,必须要撤销在事变之前的任何注册动作。如果在某点上失败,模块必须要回收所有初始化。如果不能注销这些东西,那么内核会处于一种不稳定的状态。它包含了不存在的内部指针。通常情况下,只有重新
启动系统。初始化时,务必要小心行事。
有时候,goto语句是最好的错误恢复处理方法。一般地,我们不喜欢使用goto语句,但是在此时它是非常有用的。在错误情况下,小心的使用goto会排出大量复杂的、“结构化”的逻辑。因此,在内核中,goto经常被用来恢复错误。
下面的代买在发生错误时会正确的执行:
这段代码试图注册三个虚拟设施。这里使用了goto语句,他们只是在初始化出错时候,对那些已经注册成功的设施进行注销。
另一种方法,不需要过多的goto语句,跟踪成功注册的部分,在出现错误时调用模块清除函数。清除函数仅仅回卷成功注册的步骤。不过,这种方法需要更多的代码和CPU周期,所以很多时候还是采用goto来做错误恢复。
my_init_function的返回值,err,是一个错误代码。在Linux内核中,错误代码是负数,在linux/errno.h中定义。如果你想创建自己的错误代码取代你从其他函数的返回值,你应该包含<linux/errno.h>,使用诸如-ENODEV, -ENOMEM的符号。返回有意义的错误代码比较好,因为用户程序可以把他们转换成有意思的字符,比如使用perror或者类似的方法。
很显然,模块清理函数必须撤销任何由初始化函数进行的注册,虽然不做要求,但是一般都是按照注册时相反的顺序注销设施:
如果初始化和清除函数比较复杂,那么使用goto方式处理错误恢复比较难以管理,因为所有的清除代码必须要在初始化函数中重复使用。因而一种不同的代码层次证明是成功的。
为了降低代码重复率,使所有的事情都流线化,应该无论错误何时发生,都从初始化函数中调用清除函数。清除函数必须在撤销注册之前检查每一项的状态。代码最简单的形式如下:
正如这段代码所示,可能需要或者不需要外部标识来表示初始化步骤的成功,这依赖于调用的注册/分配函数的语义。如论是否需要此标志,这种技术比以前的要好。然而要注意,清除函数由非退出代码调用时不能被标记__exit.
3. Module-Loading Races
模块加载竞争
如果在编写初始化函数时不小心,会出现威胁到整个系统稳定性的情况。