7.4. Boot-Time Initialization Routines

 

7.4. Boot-Time Initialization Routines

Most initialization routines have two interesting properties:

  • They need to be executed at boot time, when all the kernel components get initialized.

  • They are not needed once they are executed.

The next section, "xxx_initcall Macros," describes the mechanism used to run initialization routines at boot time, taking into consideration these properties as well as priorities among modules. The later section "Memory Optimizations" shows how routines and data structures that are no longer needed can be freed at link time or runtime by using smart tagging.

7.4.1. xxx_initcall Macros

The early phase of the kernel boot consists of two main blocks of initializations:

  • The initialization of various critical and mandatory subsystems that need to be done in a specific order. For instance, the kernel cannot initialize a PCI device driver before the PCI layer has been initialized. See the later section "Example of dependency between initialization routines" for another example.

  • The initialization of other kernel components that do not need a strict order: routines in the same priority level can be run in any order.

The first part is taken care of by the code that comes before do_initcalls in Figure 5-1 in Chapter 5. The second part is taken care of by the invocation of do_initcalls shown close to the end of do_basic_setup in the same figure. The initialization routines of this second part are classified based on their role and priority. The kernel executes those initialization routines one by one, starting from the ones placed in the highest-priority class (core_initcall). The addresses of those routines, which are needed to invoke them, are placed in the .initcallN.init memory sections of Figure 7-3 by tagging them with one of the xxx_initcall macros in Table 7-1.

The area used to store the addresses of the routines tagged with the xxx_initcall macros is delimited by a starting address (_ _initcall_start) and an ending address (_ _initcall_end). In the excerpt of the do_initcalls function that follows, you can see that it simply takes the function addresses one by one from that area and executes the functions they point to:

static void _ _init do_initcalls(void)
{
        initcall_t *call;
        int count = preempt_count( );

        for (call = _ _initcall_start; call < _ _initcall_end; call++) {
            ... ... ...
            (*call)( );
            ... ... ...
        }
        flush_scheduled_work( );
}

The routines invoked by do_initcalls are not supposed to change the preemption status or disable IRQs. Because of that, after each routine execution, do_initcalls checks whether the routine has made any changes, and adjusts the preemption and IRQ status if necessary (not shown in the previous snapshot).

It is possible for the xxx_initcall routines to schedule some work that takes place later. This means that the tasks handled by those routines may terminate asynchronously, at unknown times. The call to flush_scheduled_work is used to make do_initcalls wait for those asynchronous tasks to complete before returning.

Note that do_initcalls itself is marked with _ _init: because it is used only once within do_basic_setup during the booting phase, the kernel can discard it once the latter is done.

_ _exitcall is the counterpart of _ _initcall. It is not used much directly, but rather via other macros defined as aliases to it, such as module_exit, which we introduced in the section "Module Initialization Code."

7.4.1.1. Example of _ _initcall and _ _exitcall routines: modules

I said in the section "Module Initialization Code" that the module_init and module_exit macros, respectively, are used to tag routines to be executed when the module is initialized (either at boot time if built into the kernel or at runtime if loaded separately) and unloaded.

This makes a module the perfect candidate for our _ _initcall and _ _exitcall macros: in light of what I just said, the following definition from include/linux/init.h of the macros module_init and module_exit should not come as a surprise:

#ifndef MODULE
... ... ...
#define module_init(x)    _ _initcall(x);
#define module_exit(x)    _ _exitcall(x);

#else
... ... ...
#endif

module_init is defined as an alias to _ _initcall for code statically linked to the kernel: its input function is classified as a boot-time initialization routine.

module_exit follows the same scheme: when the code is built into the kernel, module_exit becomes a shutdown routine. At the moment, shutdown routines are not called when the system goes down, but the code is in place to allow it.[*]

[*] User-Mode Linux is the only architecture that actually makes use of shutdown routines. It does not use _ _exitcall macros, but defines its own macro, _ _uml_exitcall. The home page of the User-Mode Linux project is http://user-mode-linux.sourceforge.net.

7.4.1.2. Example of dependency between initialization routines

net_dev_init was introduced in Chapter 5. Device drivers register with the kernel with their module_init routine, which, as described in the section "The Big Picture" in Chapter 6, registers its devices with the networking code. Both net_dev_init and the various module_init functions for built-in drivers are invoked at boot time by do_initcalls. Because of that, the kernel needs to make sure no device registrations take place before net_dev_init has been executed. This is enforced transparently thanks to the marking of device driver initialization routines with the macro device_initcall (or its alias, _ _initcall), while net_dev_init is marked with subsys_initcall. In Figure 7-3, you can see that subsys_initcall routines are executed earlier than device_initcall routines (the memory sections are sorted in priority order).

7.4.1.3. Legacy code

Before the introduction of the set of xxx_initcall macros, there was only one macro to mark initialization functions: _ _initcall. The use of only a single macro created a heavy limitation: no execution order could be enforced by simply marking routines with the macro. In many cases, this limitation is not acceptable due to intermodule dependencies, and other considerations. Therefore, the use of _ _initcall could not be extended to all the initialization functions.

_ _initcall used to be employed mostly by device drivers. For backward compatibility with pieces of code not yet updated to the new model, it still exists and is simply defined as an alias to device_initcall.

Another limitation, which is still present in the current model, is that no parameters can be provided to the initialization routines. However, this does not seem to be an important limitation.

 

7.5. Memory Optimizations

Unlike user-space code and data, kernel code and data reside permanently in main memory, so it is important to reduce memory waste in every way possible. Initialization code is a good candidate for memory optimization . Given their nature, most initialization routines are executed either just once or not at all, depending on the kernel configuration. For example:

  • The module_init routines are executed only once when the associated module is loaded. When the module is statically included in the kernel, the kernel can free the module_init routine right at boot time, after it runs.

  • The module_exit routines are never executed when the associated modules are included statically in the kernel. In this case, therefore, there is no need to include module_exit routines into the kernel image (i.e., the routines can be discarded at link time).

The first case is a runtime optimization, and the second one is a link-time optimization.

Code and data that are used only during the boot and are not needed thereafter are placed in one of the memory sections shown in Figure 7-3. Once the kernel has completed the initialization phase, it can discard that entire memory area. This is accomplished by the call to free_init_mem,[*] as shown in Figure 5-1 in Chapter 5. Different macros are used to place code into the different memory sections of Figure 7-3.

[*] This is the memory that boot-time messages of the following sort refer to: "Freeing unused kernel memory: 120k freed".

If you look at the example in the earlier section "New Model: Macro-Based Tagging," you can see that the two input routines to module_init and module_exit are (usually) tagged with _ _init and _ _exit, respectively: this is done precisely to take advantage of the two properties mentioned at the start of this section.

7.5.1. _ _init and _ _exit Macros

The initialization routines executed in the early phase of the kernel are tagged with the macro _ _init.

As mentioned in the previous section, most module_init input routines are tagged with this macro. For example, most of the functions in Figure 5-1 in Chapter 5 (before the call to free_initmem) are marked with _ _init.

As shown by its definition here, the _ _init macro places the input routine into the .text.init memory section:

#define _ _init    _ _attribute_ _ ((_ _section_ _ (".text.init")))

This section is one of the memory areas freed at runtime by free_initmem.

_ _exit is the counterpart of _ _init. Routines used to shut down a module are placed into the .text.exit section. This section can be discarded at link time directly for modules build into the kernel. However, a few architectures discard it a runtime to deal with cross-references. Note that the same section, for modules loaded separately, can be removed at load time when the kernel does not support module unloading. (There is a kernel option that keeps the user from unloading modules.)

7.5.2. xxx_initcall and _ _exitcall Sections

The memory sections where the kernel places the addresses to the routines tagged with the xxx_initcall and _ _exitcall macros are also discarded:

  • The xxx_initcall sections shown in Figure 7-3 are discarded at runtime by free_initmem.

  • The .text.exit section used for _ _exitcall functions is discarded at link time because right now the kernel does not call the _ _exitcall routines on system shutdown (i.e., it does not use a mechanism similar to do_initcalls).

7.5.3. Other Optimizations

Other examples of optimizations include the macros in Table 7-3:


_ _devinit

When the kernel is not compiled with support for Hotplug, routines tagged with _ _devinit are not needed anymore at the end of the boot phase (after all the devices have been initialized). Because of this, when there is no support for Hotplug, _ _devinit becomes an alias to _ _init.


_ _devexit

When a PCI driver is built into a kernel without support for Hotplug, the routine to which pci_driver->remove is initialized, and which is tagged with _ _devexit, can be discarded because it is not needed. The routine can be discarded also when the module is loaded separately into a kernel that does not have support for module unloading.


_ _devinitdata

When there is no support for Hotplug, this data too is needed only at boot time. Normally, device drivers use this macro to tag the banner strings that the pci_driver-> probe functions print when initializing a device. PCI drivers, for instance, tag the pci_device_id tables with _ _devinitdata: once the system has finished booting and there is no support for Hotplug, the kernel does not need the tables anymore.

This section has given you only a few examples of removing code. You can learn more by browsing the source code, starting, for instance, from the architecture-dependent definitions of the /DISCARD/ section.

7.5.4. Dynamic Macros' Definition

In the previous sections, I introduced a few macros, such as _ _init and the various versions of xxx_initcall. We have also seen that the routines passed to the module_init macro are tagged with macros such as _ _initcall. Because most kernel components can be either compiled as modules or statically linked to the kernel, the choice made changes the definitions of these macros to apply the memory optimizations introduced in the previous section.

In particular, the definition of the macros in Table 7-1, as you can see in include/linux/init.h, change depending on whether the following symbols are defined within the scope of the file that includes include/linux/init.h:


CONFIG_MODULE

Defined when there is support for a loadable module in the kernel (the "Loadable module support" configuration option)


MODULE

Defined when the kernel component that the file belongs to is compiled as a module


CONFIG_HOTPLUG

Defined when the kernel is compiled with "Support for hot-pluggable devices" (an option in the "General setup" menu)

While MODULE can have different values for different files, the other two symbols are kernel-wide properties and therefore are either set or not set consistently throughout a kernel.

Among the macros in Tables 7-1 and 7-2, we are mostly interested in the following ones from the perspective of NIC driver initialization: _ _init, _ _exit, _ _initcall, and _ _exitcall. Summarizing what was discussed so far, Figure 7-4 shows the effectiveness of the macros in the previous list in saving memory, based on whether the symbols MODULE and CONFIG_HOTPLUG are defined (let's suppose the kernel had support for loadable modulesi.e., that CONFIG_MODULE is defined). As you can see from the figure, there is a lot going on when the kernel does not have support for both loadable modules and Hotplug, compared to when both of those options are supported: the more restrictions you have, the more optimizations you get.

Figure 7-4. Effect of macros in Table 7-1, following numbered lists in text


Let's see one by one the meaning of the points 1 through 6 in Figure 7-4, keeping in mind the generic structure of a device driver as shown earlier in the section "New Model: Macro-Based Tagging" and the definitions of _ _initcall and _ _exitcall that we saw earlier in the section "Memory Optimizations."

Here are the optimizations that can be applied when compiling a module as part of the kernel:

  1. module_exit routines will never be used; so by tagging them with _ _exit, the programmer makes sure they will not be included in the image at link time.

  2. module_init routines will be executed only once at boot time, so by tagging them with _ _init, the programmer lets them be discarded once they are executed.

  3. module_init(fn) becomes an alias to _ _initcall(fn), which makes sure fn will be executed by do_initcalls, as we saw in the section "xxx_initcall Macros."

  4. module_exit(fn) becomes an alias to _ _exitcall(fn). This places the address to the input function in the .exitcall.exit memory section, which makes it easier for the kernel to run it at shutdown time, but the section is actually discarded at link time.

    Let's use PCI devices as a reference, and see what other optimizations the lack of support for Hotplug introduces. These concern the pci_driver->remove function, which is called when a module is unloaded, once for each device registered by that module (see the section "The Big Picture" in Chapter 6).

  5. Regardless of whether MODULE is defined, when there is no support for Hotplug in the kernel, devices cannot be removed from a running system. Therefore, the remove function will never be invoked by the PCI layer and can be initialized to a NULL pointer. This is indicated by the _ _devexit_p macro.

  6. When there is no support for Hotplug or for modules in the kernel, the driver's routine that would be used to initialize pci_driver->remove is not needed by the module. This is indicated by the _ _devexit macro. Note that this is not true when there is support for modules. Because a user is allowed to load and unload a module, the kernel needs the remove routine.

Note that point 5 is a consequence of point 6: if you do not include a routine in the kernel, you cannot refer to it (i.e., you cannot initialize a function pointer to that routine).[*]

[*] See the snapshot in the section "Example of PCI NIC Driver Registration" in Chapter 6.

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值