ドライバーでは、これらの3種類のinit関数が使用されているのをよく見ます。
module_init()
core_initcall()
early_initcall()
組み込みモジュールの初期化順序を決定します。ドライバーは、ほとんどの場合_device_initcall
_(または_module_init
_;以下を参照)を使用します。早期初期化(_early_initcall
_)は、通常、実際のドライバーが初期化される前にハードウェアサブシステム(電源管理、DMAなど)を初期化するために、アーキテクチャ固有のコードによって使用されます。
_init/main.c
_ を見てください。 _Arch/<Arch>/boot
_および_Arch/<Arch>/kernel
_のコードによってアーキテクチャ固有の初期化がいくつか行われた後、移植可能な_start_kernel
_関数が呼び出されます。最終的に、同じファイル内で_do_basic_setup
_が呼び出されます:
_/*
* Ok, the machine is now initialized. None of the devices
* have been touched yet, but the CPU subsystem is up and
* running, and memory and process management works.
*
* Now we can finally start doing some real work..
*/
static void __init do_basic_setup(void)
{
cpuset_init_smp();
usermodehelper_init();
shmem_init();
driver_init();
init_irq_proc();
do_ctors();
usermodehelper_enable();
do_initcalls();
}
_
_do_initcalls
_への呼び出しで終了します:
_static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
"early",
"core",
"postcore",
"Arch",
"subsys",
"fs",
"device",
"late",
};
static void __init do_initcall_level(int level)
{
extern const struct kernel_param __start___param[], __stop___param[];
initcall_t *fn;
strcpy(static_command_line, saved_command_line);
parse_args(initcall_level_names[level],
static_command_line, __start___param,
__stop___param - __start___param,
level, level,
&repair_env_string);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
_
上記の名前と関連するインデックスを見ることができます:early
は0、core
は1などです。これらの各___initcall*_start
_エントリは、呼び出される関数ポインターの配列を指します。次々と。これらの関数ポインターは、実際のモジュールと組み込みの初期化関数であり、_module_init
_、_early_initcall
_などで指定します。
どの関数ポインターがどの___initcall*_start
_配列に入るかを決定するものは何ですか?リンカは、_module_init
_および_*_initcall
_マクロからのヒントを使用してこれを行います。組み込みモジュール用のこれらのマクロは、特定のELFセクションに関数ポインターを割り当てます。
module_init
_を使用した例組み込みモジュール(_.config
_のy
で設定)を考慮すると、_module_init
_は次のように単純に展開されます( _include/linux/init.h
_ ):
_#define module_init(x) __initcall(x);
_
そして、我々はこれに従います:
_#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
_
したがって、現在、module_init(my_func)
は__define_initcall(my_func, 6)
を意味します。これは__define_initcall
_です:
_#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
_
つまり、これまでのところ、次のとおりです。
_static initcall_t __initcall_my_func6 __used
__attribute__((__section__(".initcall6.init"))) = my_func;
_
うわー、たくさんのGCCのものが、それは___initcall_my_func6
_という名前のELFセクションに置かれる新しいシンボル_.initcall6.init
_が作成されることを意味するだけであり、ご覧のように、指定された関数(_my_func
_)。このセクションにすべての関数を追加すると、最終的に関数ポインターの完全な配列が作成され、すべて_.initcall6.init
_ ELFセクションに格納されます。
このチャンクをもう一度見てください:
_for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
_
_module_init
_で初期化されたすべての組み込みモジュールを表すレベル6を見てみましょう。 ___initcall6_start
_から始まり、その値は_.initcall6.init
_セクション内に登録された最初の関数ポインターのアドレスであり、___initcall7_start
_(除外)で終わり、毎回_*fn
_(これは_initcall_t
_です。これは_void*
_であり、アーキテクチャに応じて32ビットまたは64ビットです)。
_do_one_initcall
_は、現在のエントリが指す関数を呼び出すだけです。
特定の初期化セクション内で、リンカーが___initcall_*
_シンボルをそれぞれのELF initで次々に連結するため、初期化関数が別の前に呼び出される理由を決定するのは、Makefile内のファイルの順序です。セクション。
この事実は、実際にはカーネルで使用されています。デバイスドライバー( _drivers/Makefile
_ ):
_# GPIO must come after pinctrl as gpios may need to mux pins etc
obj-y += pinctrl/
obj-y += gpio/
_
tl; dr:Linuxカーネルの初期化メカニズムは、GCCに依存しているとはいえ、本当に美しいです。
_module_init
_ は、Linuxデバイスドライバーのエントリーポイントとして使用される関数をマークするために使用されます。
という
do_initcalls()
(組み込みドライバーの場合)*.ko
_モジュールの場合)ドライバーモジュールごとにONLY 1 module_init()
があります。
*_initcall()
関数は通常、さまざまなサブシステムを初期化するための関数ポインターを設定するために使用されます。
do_initcalls()
Linuxカーネルソースコード内 には、さまざまなinitcallのリストの呼び出しと、Linuxカーネルのブートアップ中に呼び出される相対的な順序が含まれています。
early_initcall()
core_initcall()
postcore_initcall()
Arch_initcall()
subsys_initcall()
fs_initcall()
device_initcall()
late_initcall()
*.ko
_モジュールのmodprobe
またはinsmod
。デバイスドライバーでmodule_init()
を使用することは device_initcall()
。
コンパイル中、Linuxカーネル内のさまざまなドライバーオブジェクトファイル(_*.o
_)をリンクする順序は重要です。実行時に呼び出される順序を決定します。
_
*_initcall
_同じレベルの関数
は、ブート時に呼び出されます。リンクされた順に。
たとえば、_drivers/scsi/Makefile
_のSCSIドライバーのリンク順序を変更すると、SCSIコントローラーが検出される順序、したがってディスクの番号付けが変更されます。
リンカーコードがカーネルコードの初期化に使用される関数ポインターを提供するようにどのように構成されているかに焦点を当てている人はいないようです。そのため、Linuxカーネルがいかに美しくinit呼び出し用のリンカースクリプトを作成するか見てみましょう。
上記の素晴らしい回答は、関数をinitcallとして定義する方法、定義された関数にアクセスするグローバル変数、および初期化時に定義されたinitcallを実際に呼び出す関数など、Linux Cコードがすべてのinitcallを作成および管理できる方法を示したためですフェーズ、私は再びそれらを再訪したくない。
したがって、ここでは、initcall_levels []と呼ばれるグローバル配列変数の各要素がどのように定義されているのか、それはどういう意味なのか、 initcall_levels配列の各要素などが指すメモリに含まれています。
まず、変数がLinuxカーネルリポジトリのどこで定義されているかを理解してみましょう。 init/main.cファイルを見ると、initcall_levels配列のすべての要素がmain.cファイルで定義されておらず、どこかからインポートされていないことがわかります。
extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];
しかし、これらの変数はLinuxリポジトリのCソースコードで宣言されていないことがわかりますが、変数はどこから来たのでしょうか? リンカスクリプトから!
Linuxは、プログラマーがアーキテクチャ固有のリンカースクリプトファイルを生成するのに役立つ多くのヘルパー関数を提供します。これらの関数は、initcallsのヘルパーも提供するlinux/include/asm-generic/vmlinux.lds.hファイルで定義されます。
#define __VMLINUX_SYMBOL(x) _##x
#define __VMLINUX_SYMBOL_STR(x) "_" #x
#else
#define __VMLINUX_SYMBOL(x) x
#define __VMLINUX_SYMBOL_STR(x) #x
#endif
/* Indirect, so macros are expanded before pasting. */
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)
#define INIT_CALLS_LEVEL(level) \
VMLINUX_SYMBOL(__initcall##level##_start) = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
KEEP(*(.initcallearly.init)) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
VMLINUX_SYMBOL(__initcall_end) = .;
Initcallsにはいくつかのマクロが定義されていることが簡単にわかります。最も重要なマクロは、INIT_CALLSで、プレーンCコードおよび入力セクションでアクセスできるリンカースクリプトシンボルを定義するリンカースクリプト構文を出力します。
詳細には、INIT_CALLS_LEVEL(x)マクロを呼び出すたびに、__ initcall ## level _ ## startという新しいシンボルが定義されます( CPPの##連結操作を参照 );このシンボルはVMLINUX_SYMBOL(__ initcall ## level ## _ start)=。;によって生成されます。たとえば、INIT_CALLS_LEVEL(1)マクロは、__ initcall1_startという名前のリンカースクリプトシンボルを定義します。
その結果、シンボル__initcall0_start〜__initcall7_startはリンカースクリプトで定義され、externキーワードで宣言することでCコードで参照できます。
また、INIT_CALLS_LEVELマクロは。initcallN.initと呼ばれる新しいセクションを定義します。ここで、Nは0〜7です。生成されたセクションには、セクション属性で指定された__define_initcallなどのマクロ。
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
作成されたシンボルとセクションは、リンカースクリプトによって正しく構成され、1つのセクション(.init.dataセクション)に配置されます。これを有効にするには、INIT_DATA_SECTIONマクロが使用されます。そして、私たちが見たINIT_CALLSマクロを呼び出すことがわかります。
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
INIT_DATA \
INIT_SETUP(initsetup_align) \
INIT_CALLS \
CON_INITCALL \
SECURITY_INITCALL \
INIT_RAM_FS \
}
そのため、INIT_CALLSマクロを呼び出すことにより、Linuxリンカーは__ initcall0_startto__ initcall7_startを見つけますシンボルと。initcall0.initから。initcall7.initセクション.init.dataセクション。背中合わせに配置されています。ここで、各シンボルにはデータは含まれていませんが、生成されたセクションの開始位置と終了位置を見つけるために使用されることに注意してください。
次に、コンパイルされたLinuxカーネルに、生成されたシンボル、セクション、および関数が正しく含まれているかどうかを見てみましょう。 Linuxカーネルをコンパイルした後、nmツールを使用して、vmlinuxと呼ばれるコンパイル済みのLinuxイメージで定義されているすべてのシンボルを取得できます。
//ordering nm result numerical order
$nm -n vmlinux > symbol
$vi symbol
ffffffff828ab1c8 T __initcall0_start
ffffffff828ab1c8 t __initcall_ipc_ns_init0
ffffffff828ab1d0 t __initcall_init_mmap_min_addr0
ffffffff828ab1d8 t __initcall_evm_display_config0
ffffffff828ab1e0 t __initcall_init_cpufreq_transition_notifier_list0
ffffffff828ab1e8 t __initcall_jit_init0
ffffffff828ab1f0 t __initcall_net_ns_init0
ffffffff828ab1f8 T __initcall1_start
ffffffff828ab1f8 t __initcall_xen_pvh_gnttab_setup1
ffffffff828ab200 t __initcall_e820__register_nvs_regions1
ffffffff828ab208 t __initcall_cpufreq_register_tsc_scaling1
......
ffffffff828ab3a8 t __initcall___gnttab_init1s
ffffffff828ab3b0 T __initcall2_start
ffffffff828ab3b0 t __initcall_irq_sysfs_init2
ffffffff828ab3b8 t __initcall_audit_init2
ffffffff828ab3c0 t __initcall_bdi_class_init2
上記に示すように、__ initcall0_startと__initcall2_startシンボルの間に、pure_initcallマクロで定義されたすべての関数が配置されています。たとえば、ipc/shim.cファイルで定義されているipc_ns_init関数を見てみましょう。
static int __init ipc_ns_init(void)
{
const int err = shm_init_ns(&init_ipc_ns);
WARN(err, "ipc: sysv shm_init_ns failed: %d\n", err);
return err;
}
pure_initcall(ipc_ns_init);
上記に示すように、pure_initcallマクロを使用して、__ initcall0_startシンボルによって配置される.initcall0.initセクションにipc_ns_init関数を配置します。したがって、以下のコードに示すように、.initcallN.initセクションのすべての関数は1つずつ順番に呼び出されます。
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);