We're hiring!
*

An introduction to Linux kernel initcalls

Mylène Josserand avatar

Mylène Josserand
July 14, 2020

Share this post:

Reading time:

Initcalls, which serve to call functions during boot, were implemented early on in the development of the Linux Kernel. While they have not required significant changes since version 2.4 released around 2001, tracing support was added by Steven Rostedt in 2018, which can be useful to measure time spent on each initcall to allow for further debugging.

Read on as we take a closer look at initcalls, including their purpose in the Linux kernel, their usage (how to create your function and call one of the definitions), ways to debug them (using initcall_debug or FTrace), as well as a short overview of their implementation.

Purpose and usage

As mentioned, the purpose of initcalls are to call functions at different stages during the boot process and are used in many architectures and drivers:

$ git grep _initcall drivers/ arch/ | wc -l
2453

Initcalls are defined as regular functions with the __init attribute and one of the helpers which will define the type of initcall you want to use. Here is a list of initcalls and their distributions in the code:

The implementation will be explained in the next part of this blog post but keep in mind the main ideas behind initcalls:

  • Using initcalls will create ELF sections in the object file.
  • The programmer creates the needed function that will go into these specific sections.
  • Those sections will be iterated later by the kernel which will execute functions located there.
  • Each section corresponds to one initcall level.

If you are not familiar with the ELF (Extensible Linking Format) file format, see this a very nice overview by Corkami:

Examples

Here is an example of an initcall which will be executed at the postcore stage.

static int __init mypostcore_init(void)
{
	return 0;
}
postcore_initcall(mypostcore_init);

You will find many real-world examples in the kernel. For instance, the proc filesystem entry creation for all kernel debug symbols:

atic int __init kallsyms_init(void)
{
        proc_create("kallsyms", 0444, NULL, &kallsyms_proc_ops);
        return 0;
}
device_initcall(kallsyms_init);

Using initcalls implies some benefits: your code is more modular and maintainable because we don't need to explicitly pass, store and call function pointers - instead we only mark function as appropriate level initcall and it will be automagically invoked at the appropriate time. However, knowing which one should be used is difficult to distinguish. The level name reflects the order of initcall, in which part initcall will be called, but you have to understand the dependencies and where your function should be executed.

Sometimes, using an initcall is not necessary and using module_init() can be enough. Notices that these initcalls function can be used only for builtin modules. For modules that can also be loadable, module_init() is the key.

module_init()

Initcalls are executed at early stages of the initialization of a module but only for the ones that are statically linked to the kernel. But how about modules than can be loadable or builtin? Many modules (builtin or loadable) don't need to be executed so early because they are not necessary for a device to become usable, for example. In that case, you should use one of the last initcalls levels or module_init(). Thanks to that, it will save time consumed at bootup and let most important probing functions to be executed earlier.

If a module is compiled builtin (y in your configuration), module_init() function will be called during do_initcalls() because this function will be a simple link to device_initcall function (i.e one of the last initcalls during boot process). Here is the code in include/linux/module.h:

#ifndef MODULE
#define module_init(x)	__initcall(x);
#define module_exit(x)	__exitcall(x);

And __initcall() is calling device_initcall():

$ git grep __initcall include/linux/
[...]
include/linux/init.h:#define __initcall(fn) device_initcall(fn)
include/linux/module.h:#define module_init(x)   __initcall(x);

In case of a loadable module, it will be during the insertion time by using syscalls. All initcalls function will be replaced by a module_init() one:

#else /* MODULE */
#define early_initcall(fn)		module_init(fn)
#define core_initcall(fn)		module_init(fn)
#define postcore_initcall(fn)		module_init(fn)
#define arch_initcall(fn)		module_init(fn)
#define subsys_initcall(fn)		module_init(fn)
#define fs_initcall(fn)			module_init(fn)
#define rootfs_initcall(fn)		module_init(fn)
#define device_initcall(fn)		module_init(fn)
#define late_initcall(fn)		module_init(fn)

#define console_initcall(fn)		module_init(fn)
[...]

We will see this use case in the second article that will go deeper in the module_init() implementation.

To summarize, if you don't have any reasons to execute a function at early stage in booting process, you should use module_init() if your module can be loaded (which is the case in most of non-core drivers).

Debugging

A debug command-line parameter exists to print 2 messages while executing all initcalls functions. For that, you should use initcall_debug introduced in 2.5.67 in the command-line.

[...]
[    0.040325] calling  init_mmap_min_addr+0x0/0x20 @ 1
[    0.040345] initcall init_mmap_min_addr+0x0/0x20 returned 0 after 0 usecs
[    0.040357] calling  net_ns_init+0x0/0x140 @ 1
[    0.040635] initcall net_ns_init+0x0/0x140 returned 0 after 0 usecs
[    0.040740] calling  vfp_init+0x0/0x1d0 @ 1
[    0.040758] VFP support v0.3: implementor 41 architecture 2 part 30 variant 7 rev 5
[    0.040777] initcall vfp_init+0x0/0x1d0 returned 0 after 0 usecs
[    0.040786] calling  ptrace_break_init+0x0/0x2c @ 1
[    0.040796] initcall ptrace_break_init+0x0/0x2c returned 0 after 0 usecs
[    0.040808] calling  register_cpufreq_notifier+0x0/0x10 @ 1
[    0.040819] initcall register_cpufreq_notifier+0x0/0x10 returned 0 after 0 usecs
[    0.040829] calling  v6_userpage_init+0x0/0x8 @ 1
[    0.040839] initcall v6_userpage_init+0x0/0x8 returned 0 after 0 usecs
[    0.040849] calling  cpu_hotplug_pm_sync_init+0x0/0x18 @ 1
[    0.040860] initcall cpu_hotplug_pm_sync_init+0x0/0x18 returned 0 after 0 usecs
[    0.040869] calling  alloc_frozen_cpus+0x0/0x8 @ 1
[    0.040878] initcall alloc_frozen_cpus+0x0/0x8 returned 0 after 0 usecs
[    0.040887] calling  wq_sysfs_init+0x0/0x30 @ 1
[    0.040973] initcall wq_sysfs_init+0x0/0x30 returned 0 after 0 usecs
[    0.040983] calling  ksysfs_init+0x0/0xa4 @ 1
[    0.041026] initcall ksysfs_init+0x0/0xa4 returned 0 after 0 usecs
[    0.041037] calling  pm_init+0x0/0x7c @ 1
[    0.041107] initcall pm_init+0x0/0x7c returned 0 after 0 usecs
[    0.041119] calling  rcu_set_runtime_mode+0x0/0x18 @ 1
[    0.041130] initcall rcu_set_runtime_mode+0x0/0x18 returned 0 after 0 usecs
[...]

This is a great debug parameters to detect which initcalls are taking too much time, particularly in case of boot-time improvements. However, as these debug logs are directly printed on the console and it is printing messages for all initcalls, it can be difficult to retrieve a particular information in this verbose log without having our great binary: grep.

This is why ftrace has been introducted on initcalls. If you are not familiar with FTrace, here is a really nice video from Steven Rostedt.

Here is a little example of how to use it in case of initcalls-tracing:

  • The available events can be retrieved using the debugfs entry:
    # mount -t debugfs nodev /sys/kernel/debug
    # cat /sys/kernel/debug/tracing/available_events | grep initcall
    initcall:initcall_finish
    initcall:initcall_start
    initcall:initcall_level
    
  • If you are familiar with FTracing, using it for initcalls is a little different because we need to configure it before the boot process. That's why we have to add parameters to the command-line to configure which events we want to retrieve. Example of my command-line to retrieve all initcall_level, initcall_start and initcall_finish events:
    # cat /proc/cmdline
    console=ttyS0,115200 earlyprintk root=/dev/mmcblk0p2 rootwait trace_event=initcall:initcall_level,initcall:initcall_start,initcall:initcall_finish
    
  • And you can retrieve the current event through trace entry on debugfs:
    # mount -t debugfs nodev /sys/kernel/debug
    # cat /sys/kernel/debug/tracing/trace
    # tracer: nop
    #
    # entries-in-buffer/entries-written: 1090/1090   #P:4
    #
    #                              _-----=> irqs-off
    #                             / _----=> need-resched
    #                            | / _---=> hardirq/softirq
    #                            || / _--=> preempt-depth
    #                            ||| /     delay
    #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
    #              | |       |   ||||       |         |
              -0     [000] ....     0.000125: initcall_level: level=console
              -0     [000] ....     0.000136: initcall_start: func=con_init+0x0/0x220
              -0     [000] ....     0.000232: initcall_finish: func=con_init+0x0/0x220 ret=0
              -0     [000] ....     0.000235: initcall_start: func=univ8250_console_init+0x0/0x3c
              -0     [000] ....     0.000246: initcall_finish: func=univ8250_console_init+0x0/0x3c ret=0
           swapper/0-1     [000] ....     0.002016: initcall_level: level=early
           swapper/0-1     [000] ....     0.002026: initcall_start: func=trace_init_flags_sys_exit+0x0/0x24
           swapper/0-1     [000] ....     0.002029: initcall_finish: func=trace_init_flags_sys_exit+0x0/0x24 ret=0
           swapper/0-1     [000] ....     0.002031: initcall_start: func=trace_init_flags_sys_enter+0x0/0x24
           swapper/0-1     [000] ....     0.002032: initcall_finish: func=trace_init_flags_sys_enter+0x0/0x24 ret=0
           swapper/0-1     [000] ....     0.002034: initcall_start: func=cpu_suspend_alloc_sp+0x0/0xac
           swapper/0-1     [000] ....     0.002037: initcall_finish: func=cpu_suspend_alloc_sp+0x0/0xac ret=0
           swapper/0-1     [000] ....     0.002038: initcall_start: func=init_static_idmap+0x0/0x100
           swapper/0-1     [000] ....     0.002084: initcall_finish: func=init_static_idmap+0x0/0x100 ret=0
           swapper/0-1     [000] ....     0.002088: initcall_start: func=sunxi_mc_smp_init+0x0/0x3a8
           swapper/0-1     [000] ....     0.002102: initcall_finish: func=sunxi_mc_smp_init+0x0/0x3a8 ret=-19
           swapper/0-1     [000] ....     0.002104: initcall_start: func=spawn_ksoftirqd+0x0/0x58
           swapper/0-1     [000] ....     0.002213: initcall_finish: func=spawn_ksoftirqd+0x0/0x58 ret=0
           swapper/0-1     [000] ....     0.002217: initcall_start: func=migration_init+0x0/0x54
           swapper/0-1     [000] ....     0.002219: initcall_finish: func=migration_init+0x0/0x54 ret=0
           swapper/0-1     [000] ....     0.002220: initcall_start: func=check_cpu_stall_init+0x0/0x28
           swapper/0-1     [000] ....     0.002223: initcall_finish: func=check_cpu_stall_init+0x0/0x28 ret=0
           swapper/0-1     [000] ....     0.002224: initcall_start: func=srcu_bootup_announce+0x0/0x44
           swapper/0-1     [000] ....     0.002235: initcall_finish: func=srcu_bootup_announce+0x0/0x44 ret=0
           swapper/0-1     [000] ....     0.002236: initcall_start: func=rcu_spawn_gp_kthread+0x0/0x150
           swapper/0-1     [000] ....     0.002299: initcall_finish: func=rcu_spawn_gp_kthread+0x0/0x150 ret=0
           swapper/0-1     [000] ....     0.002302: initcall_start: func=cpu_stop_init+0x0/0xbc
           swapper/0-1     [000] .n..     0.002384: initcall_finish: func=cpu_stop_init+0x0/0xbc ret=0
           swapper/0-1     [000] .n..     0.002387: initcall_start: func=init_events+0x0/0x70
           swapper/0-1     [000] .n..     0.002403: initcall_finish: func=init_events+0x0/0x70 ret=0
           swapper/0-1     [000] .n..     0.002404: initcall_start: func=init_trace_printk+0x0/0xc
    [...]
    

Once again, FTrace logging allows to have great information to detect portions of code whose execution take a lot of time.

Implementation

Now we know what initcalls are for, let's look at how they are implemented in Linux Kernel's code. One part of the implementation of initcalls is in include/linux/init.h.

#define pure_initcall(fn)			__define_initcall(fn, 0)
#define core_initcall(fn)			__define_initcall(fn, 1)
#define postcore_initcall(fn)			__define_initcall(fn, 2)
#define arch_initcall(fn)			__define_initcall(fn, 3)
#define subsys_initcall(fn)			__define_initcall(fn, 4)
#define fs_initcall(fn)				__define_initcall(fn, 5)
#define rootfs_initcall(fn)			__define_initcall(fn, rootfs)
#define device_initcall(fn)			__define_initcall(fn, 6)
#define late_initcall(fn)			__define_initcall(fn, 7)

Initcalls are defined thanks to a generic __define_initcall() with two arguments:

    • The function name
    • An id which is a way to order the initcalls. The ordering is not according to a number because if you look closer at this argument, its value is equals to rootfs for rootfs_initcall(). All initcalls defined using postcore_initcall will have a second argument of 2. This is the main difference between all initcalls: the ID (that can be both number and string, see rootfs). It will be different depending on the type of initcall used.

Note: Let’s focus on postcore_initcall in the rest of this article.

#define postcore_initcall(fn) __define_initcall(fn, 2)

Let’s look at this definition in the kernel source:

You have our dummy example: a simple function that is defined as a postcore initcall. The postcore_initcall definition is calling the __define_initcall() with its ID: 2. The __define_initcall(fn, id) is calling another define_initcall() with an additional arguments: __sec. For all initcalls, it will be equals to .initcall + the id of the initcall. For our postcore initcall, the last parameter will be .initcall2.

The last __define_initcall() will look like this (code and expanded versions):

Too many colors, right?

Let's take a break for now and keep the explanation of that part for the next article!

Conclusion

In this first blog post, we have seen an overview of initcalls:

  • their purpose: being able to call a function at different stages of the kernel booting process;
  • their usage: simply create your function and call one of the definitions of initcalls depending on the level of the booting process you need (core_initcall, postcore_initcall, fs_initcall, device_initcall, ...);
  • a short explanation of module_init() function and the difference (or not...) with initcalls;
  • 2 ways to debug initcalls: initcall_debug and FTrace. They are very useful in case the boot-time must be as fast as possible;
  • and a short overview of their implementation in the kernel: each initcall has an id corresponding to its level and their implementation is based on __define_initcall().

The next part will go deeper into the implementation of initcalls, particularly about this colorful __device_initcall() macro.


Resources

Comments (0)


Add a Comment






Allowed tags: <b><i><br>Add a new comment:


Search the newsroom

Latest Blog Posts

Automatic regression handling and reporting for the Linux Kernel

14/03/2024

In continuation with our series about Kernel Integration we'll go into more detail about how regression detection, processing, and tracking…

Almost a fully open-source boot chain for Rockchip's RK3588!

21/02/2024

Now included in our Debian images & available via our GitLab, you can build a complete, working BL31 (Boot Loader stage 3.1), and replace…

What's the latest with WirePlumber?

19/02/2024

Back in 2022, after a series of issues were found in its design, I made the call to rework some of WirePlumber's fundamentals in order to…

DRM-CI: A GitLab-CI pipeline for Linux kernel testing

08/02/2024

Continuing our Kernel Integration series, we're excited to introduce DRM-CI, a groundbreaking solution that enables developers to test their…

Persian Rug, Part 4 - The limitations of proxies

23/01/2024

This is the fourth and final part in a series on persian-rug, a Rust crate for interconnected objects. We've touched on the two big limitations:…

How to share code between Vulkan and Gallium

16/01/2024

One of the key high-level challenges of building Mesa drivers these days is figuring out how to best share code between a Vulkan driver…

Open Since 2005 logo

We use cookies on this website to ensure that you get the best experience. By continuing to use this website you are consenting to the use of these cookies. To find out more please follow this link.

Collabora Ltd © 2005-2024. All rights reserved. Privacy Notice. Sitemap.