ドライバーでは、これら 3 種類の init 関数が使用されているのをよく見かけます。
module_init()
core_initcall()
early_initcall()
- どのような状況で使用する必要がありますか?
- また、初期化の他の方法はありますか?
ドライバーでは、これら 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
is 0、core
is 1 など) で確認できます。これらの各__initcall*_start
エントリは、次々に呼び出される関数ポインターの配列を指しています。module_init
これらの関数ポインタは、実際のモジュールと組み込みの初期化関数であり、early_initcall
、 などで指定します。
どの関数ポインタがどの__initcall*_start
配列に入るかを決定するものは何ですか? module_init
リンカーは、および*_initcall
マクロからのヒントを使用してこれを行います。組み込みモジュールのこれらのマクロは、関数ポインタを特定の ELF セクションに割り当てます。
module_init
組み込みモジュール ( in で構成) を考慮するとy
、.config
単純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
だけ.initcall6.init
ですmy_func
. すべての関数をこのセクションに追加すると、最終的に関数ポインターの完全な配列が作成され、すべてが.initcall6.init
ELF セクション内に格納されます。
このチャンクをもう一度見てください。
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
で初期化されたすべての組み込みモジュールを表すレベル 6 を見てみましょうmodule_init
。から始まり__initcall6_start
、その値は.initcall6.init
セクション内に登録された最初の関数ポインターのアドレスであり、 __initcall7_start
(除外された) で終わり、サイズごとに*fn
(これは でありinitcall_t
、これは でありvoid*
、32 ビットまたは 64 ビットであるビットはアーキテクチャに依存します)。
do_one_initcall
現在のエントリが指す関数を呼び出すだけです。
特定の初期化セクション内で、初期化関数が別の前に呼び出される理由を決定するのは、単純に Makefile 内のファイルの順序です。これは、リンカが__initcall_*
それぞれの ELF init でシンボルを次々に連結するためです。セクション。
この事実は、カーネルで実際に使用されます。たとえば、デバイス ドライバー ( 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
モジュールの場合)ドライバー モジュールごとに1 つmodule_init()
だけ存在できます。
関数は通常、さまざまなサブシステムを初期化するための*_initcall()
関数ポインターを設定するために使用されます。
do_initcalls()
Linux カーネル ソース コード内には、さまざまな initcalls のリストの呼び出しと、Linux カーネルの起動時に呼び出される相対的な順序が含まれています。
early_initcall()
core_initcall()
postcore_initcall()
arch_initcall()
subsys_initcall()
fs_initcall()
device_initcall()
late_initcall()
modprobe
またはモジュールinsmod
の。*.ko
module_init()
デバイス ドライバーで使用することは、を登録することと同じdevice_initcall()
です。
*.o
コンパイル時に、Linux カーネル内でさまざまなドライバー オブジェクト ファイル ( ) をリンクする順序が重要であることに注意してください。実行時に呼び出される順序を決定します。
*_initcall
同じレベルの関数は、リンクされている順序で
ブート中に呼び出されます。
たとえば、SCSI ドライバのリンク順序をdrivers/scsi/Makefile
変更すると、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 に対していくつかのマクロが定義されていることが簡単にわかります。最も重要なマクロは、プレーン C コードと入力セクションでアクセスできるリンカー スクリプト シンボルを定義するリンカー スクリプト構文を生成するINIT_CALLSです。
詳細には、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_startから__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 マクロを使用して、ipc_ns_init 関数を __initcall0_start シンボルのそばにある .initcall0.init セクションに配置します。したがって、以下のコードに示すように、.initcallN.init セクション内のすべての関数が 1 つずつ順番に呼び出されます。
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);