11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Raspberry Pi2(Linux Kernel)のブートシーケンスを読む(その6) start_kernel内のsetup_arch()

Posted at

##前回
cgroupの初期化からページアドレスの初期化までを説明しました。
今回は、setup_arch()だけを読み解きます。

##今回の対象範囲

init/main.c
asmlinkage __visible void __init start_kernel(void)
{
        (省略)
	setup_arch(&command_line);            //対象範囲 ここだけ
	mm_init_cpumask(&init_mm);
	setup_command_line(command_line);
	(省略)
}

##setup_arch内のsetup_processorまで
setup_archは、100行近い関数であるため、分割して説明します。

arch/arm/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
	const struct machine_desc *mdesc;

	setup_processor();
        (省略)

ローカル変数のmachine_desc構造体はarch/arm/include/asm/mach/arch.hで定義され、
ボード固有の情報を管理します。
続く処理であるsetup_processor()の定義は、以下の通りです。

arch/arm/kernel/setup.c
static void __init setup_processor(void)
{
	struct proc_info_list *list;

	list = lookup_processor_type(read_cpuid_id());
	if (!list) {
		pr_err("CPU configuration botched (ID %08x), unable to continue.\n",
		       read_cpuid_id());
		while (1);
	}

	cpu_name = list->cpu_name;
	__cpu_architecture = __get_cpu_architecture();

#ifdef MULTI_CPU
	processor = *list->proc;
#endif
#ifdef MULTI_TLB
	cpu_tlb = *list->tlb;
#endif
#ifdef MULTI_USER
	cpu_user = *list->user;
#endif
#ifdef MULTI_CACHE
	cpu_cache = *list->cache;
#endif

	pr_info("CPU: %s [%08x] revision %d (ARMv%s), cr=%08lx\n",
		cpu_name, read_cpuid_id(), read_cpuid_id() & 15,
		proc_arch[cpu_architecture()], get_cr());

	snprintf(init_utsname()->machine, __NEW_UTS_LEN + 1, "%s%c",
		 list->arch_name, ENDIANNESS);
	snprintf(elf_platform, ELF_PLATFORM_SIZE, "%s%c",
		 list->elf_name, ENDIANNESS);
	elf_hwcap = list->elf_hwcap;

	cpuid_init_hwcaps();

#ifndef CONFIG_ARM_THUMB
	elf_hwcap &= ~(HWCAP_THUMB | HWCAP_IDIVT);
#endif
#ifdef CONFIG_MMU
	init_default_cache_policy(list->__cpu_mm_mmu_flags);
#endif
	erratum_a15_798181_init();

	elf_hwcap_fixup();

	cacheid_init();
	cpu_init();
}

lookup_processor_typeは、CPUの識別子やCPU固有の関数群、CPU固有のマジックナンバーを
保存する構造体(proc_info_list)へのポインタを保持しています(間違えている可能性 大)。
proc_info_list構造体は、arch/arm/mm/proc-v7.Sで定義されています。
if (!list)のブロックでは、正しいCPU情報が取得できなかった旨を表示します。

proc_info_list構造体からCPU名を取得し、
__get_cpu_architecture()からアーキテクチャの種類(CPU_ARCH_ARMv7)を取得します。
その後に取得する情報は、以下に示す関数を管理する構造体へのポインタです。
 ・CPUがアボートした際やidle状態に移行する際の関数など(processor構造体)
 ・ユーザ・カーネル空間のTLBをフラッシュする関数(cpu_tlb_fns構造体)
 ・high pageのコピー・クリアを行う関数(cpu_user_fns構造体)
 ・I-cacheやD-cacheをクリアする関数など(cpu_cache_fns構造体)

そして、CPU情報をpr_notice()、snprintfで出力した後に、
proc_info_list構造体からelf_hwcap(unsigned int型)の値を取得します。
次に、cpuid_init_hwcaps()内で、elf_hwcap、elf_hwcap2の値を更新します。
ここでのelf_hwcapは"Hardware Capabilities"の略で、以下の役割を持ちます(宣言部コメント抜粋)。

arch/arm/include/asm/hwcap.h
/*
 * This yields a mask that user programs can use to figure out what
 * instruction set this cpu supports.
 */

/*
 * この変数(elf_hwcaps)は、どの命令セットがこのCPUでサポートされているかを判断するために、
 * ユーザプログラムがマスクとして用います(意訳)
 */

上記のコメントを補足しますと、プログラムはハード固有の命令を持つケースがあります。
固有の命令を持っていない場合(ハード環境が異なる場合)、その命令を実行すると、
プログラムが好ましくない挙動をする可能性があります。
そのため、elf_hwcapの値(ビット)を確認することによって、命令が存在するかどうかを確認します。
(ORACLEの説明を読む限り、Sun Studio compilersではリンク段階で確認しています)

続く処理では、キャッシュポリシーが存在するかどうかを確認します。
init_default_cache_policy()とキャッシュ情報を管理するcachepolicy構造体は、
以下のように定義されています。

arch/arm/mm/mmu.c
struct cachepolicy {
	const char	policy[16];
	unsigned int	cr_mask;
	pmdval_t	pmd;
	pteval_t	pte;
	pteval_t	pte_s2;
};
static struct cachepolicy cache_policies[] __initdata = {
	{
		.policy		= "uncached",
		.cr_mask	= CR_W|CR_C,
		.pmd		= PMD_SECT_UNCACHED,
		.pte		= L_PTE_MT_UNCACHED,
		.pte_s2		= s2_policy(L_PTE_S2_MT_UNCACHED),
	}
        (省略。この配列では、合計5個分のポリシーが記載されている)
};
void __init init_default_cache_policy(unsigned long pmd)
{
	int i;

	initial_pmd_value = pmd;

	pmd &= PMD_SECT_TEX(1) | PMD_SECT_BUFFERABLE | PMD_SECT_CACHEABLE;

	for (i = 0; i < ARRAY_SIZE(cache_policies); i++)
		if (cache_policies[i].pmd == pmd) {
			cachepolicy = i;
			break;
		}

	if (i == ARRAY_SIZE(cache_policies))
		pr_err("ERROR: could not find cache policy\n");
}

init_default_cache_policyでは、__cpu_mm_mmu_flags(引数pmd)と一致するポリシーを探します。
ここでのpmdとはPage Middle Directoryの略で、ポリシーは以下の5つです。
(疑問として、「pmdとキャッシュの関連性」がありますが、後日調べます)
 ・uncached:キャッシュを使用しない
 ・buffered:キャッシュをバッファとして使用する
 ・writethrough:メモリにデータを書き込む際、キャッシュにも同じ内容を書き込む
 ・writeback:データをキャッシュに書き込み、空き時間が発生した際にキャッシュ内容をメモリに書き込む
 ・writealloc:キャッシュにミスヒットした際(キャッシュに目的のデータが存在しなかった場合)、
        該当するメモリ上のデータをキャッシュに書き込んでから、そのデータを更新・参照する

erratum_a15_798181_init()は、Cortex-a15のエラッタに関する処理ですので、割愛します。
余談ですが、入社後に初めてハードウェアマニュアルを読んだ時に、エラッタの文字を見つけて、
「遊◯戯◯王カードじゃん」とニヤつきました。「ハードも不具合あるのか!」と当たり前の事を考えたりも。

elf_hwcap_fixup()では、CPUがLDREX/STREXおよびLDREXB/STREXBをサポートしている場合、
スワップの使用を避けるように、elf_hwcapの値を更新します。
ここでのLDREX/STREXとは、ロックなしでアトミックなロード・ストアが可能な命令です。
しかし、マルチコア環境ではアトミックな操作が保証出来ないケースが存在します(ABA問題参照)。

cacheid_init()では、アーキテクチャ(正しくは命令セット)に合わせてcacheidを初期化します。

arch/arm/kernel/setup.c
static void __init cacheid_init(void)
{
	unsigned int arch = cpu_architecture();
        (省略)
	
	unsigned int cachetype = read_cpuid_cachetype();
		if ((cachetype & (7 << 29)) == 4 << 29) {
			/* ARMv7 register format */
			arch = CPU_ARCH_ARMv7;
			cacheid = CACHEID_VIPT_NONALIASING;
			switch (cachetype & (3 << 14)) {
			case (1 << 14):
				cacheid |= CACHEID_ASID_TAGGED;
				break;
			case (3 << 14):
				cacheid |= CACHEID_PIPT;
				break;
			}
        (省略)
}

ここで登場する"VIPT"、"ASID"、"PIPT"は、アドレスからキャッシュを参照する際の方法を意味します。
仮想アドレスを物理アドレスに変換してから、キャッシュアクセスすると実行速度が遅くなるため、
以下のような「仮想アドレスを用いて、キャッシュアクセスする方法」が考え出されたそうです。

 ・VIPTは、仮想アドレスからキャッシュを参照する方法(Virtually Indexed)、
  かつキャッシュのタグ配列に格納するアドレスが物理アドレスの方法(Physically Tagged)
 ・ASIDは、仮想アドレスからキャッシュを参照する方法、
  かつキャッシュのタグ配列に格納するアドレスが仮想アドレスとaddress space ID(ASID)の方法
 ・PIPTは、仮想アドレスを物理アドレスに変換後、キャッシュを参照する方法(Physically Indexed)、
  かつキャッシュのタグ配列に格納するアドレスが物理アドレスの方法

次に、以下に示すcpu_init()では、スタックの設定を行います。

arch/arm/kernel/setup.c
struct stack {
	u32 irq[3];
	u32 abt[3];
	u32 und[3];
	u32 fiq[3];
} ____cacheline_aligned;

void notrace cpu_init(void)
{
#ifndef CONFIG_CPU_V7M
	unsigned int cpu = smp_processor_id();
	struct stack *stk = &stacks[cpu];

        (エラーチェックおよびコメントを省略)
	set_my_cpu_offset(per_cpu_offset(cpu));
	cpu_proc_init();
        (コメントおよびTHUMB命令に関する設定を省略)

	__asm__ (
	"msr	cpsr_c, %1\n\t"
	"add	r14, %0, %2\n\t"
	"mov	sp, r14\n\t"
	"msr	cpsr_c, %3\n\t"
	"add	r14, %0, %4\n\t"
	"mov	sp, r14\n\t"
	"msr	cpsr_c, %5\n\t"
	"add	r14, %0, %6\n\t"
	"mov	sp, r14\n\t"
	"msr	cpsr_c, %7\n\t"
	"add	r14, %0, %8\n\t"
	"mov	sp, r14\n\t"
	"msr	cpsr_c, %9"
	    :
	    : "r" (stk),
	      PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),
	      "I" (offsetof(struct stack, irq[0])),
	      PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),
	      "I" (offsetof(struct stack, abt[0])),
	      PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE),
	      "I" (offsetof(struct stack, und[0])),
	      PLC (PSR_F_BIT | PSR_I_BIT | FIQ_MODE),
	      "I" (offsetof(struct stack, fiq[0])),
	      PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE)
	    : "r14");
#endif
}

set_my_cpu_offset()では、特権専用ソフトウェアスレッドレジスタ(TPIDRPRW)にオフセットを保存します。
ここでのTPIDRPRWとはシステム制御レジスタの一種で、ソフトウェアスレッドID・プロセスIDを保存します。
また、オフセットはCPU毎の固有の情報を取得する際に使用します。
この「CPU周りの操作」が理解できていないので、後日追記します。

インラインアセンブラの部分では、cpsr(Current Program Status Register)の値を変更し、
IRQ(割り込み)、ABORT(処理の中断)、UND(未定義命令例外)、FIQ(高速割り込み)のそれぞれが
発生した際に使用するスタック(stack構造体)を設定します。
cpsr_cを書き換える際には、PLCによるビット操作を行います(入力オペランドを"%番号"で指定しています)。
最後の"msr cpsr_c, %9"によって、SVC(SuperVisor Call)が使用できるモードに戻ります。

##setup_arch内の最後まで

arch/arm/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
	const struct machine_desc *mdesc;

	setup_processor();
	mdesc = setup_machine_fdt(__atags_pointer);
	if (!mdesc)
		mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
	machine_desc = mdesc;
	machine_name = mdesc->name;
	dump_stack_set_arch_desc("%s", mdesc->name);

	if (mdesc->reboot_mode != REBOOT_HARD)
		reboot_mode = mdesc->reboot_mode;

	init_mm.start_code = (unsigned long) _text;
	init_mm.end_code   = (unsigned long) _etext;
	init_mm.end_data   = (unsigned long) _edata;
	init_mm.brk	   = (unsigned long) _end;

	/* populate cmd_line too for later use, preserving boot_command_line */
	strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
	*cmdline_p = cmd_line;

	parse_early_param();

	early_paging_init(mdesc, lookup_processor_type(read_cpuid_id()));
	setup_dma_zone(mdesc);
	sanity_check_meminfo();
	arm_memblock_init(mdesc);

	paging_init(mdesc);
	request_standard_resources(mdesc);

	if (mdesc->restart)
		arm_pm_restart = mdesc->restart;

	unflatten_device_tree();

	arm_dt_init_cpu_maps();
	psci_init();

	if (!is_smp())
		hyp_mode_check();

	reserve_crashkernel();
    (一部のコンフィグを省略)
}

ボード固有の情報を管理するmachine_desc構造体に対する設定を行います。
まず、setup_machine_fdt()によって、Device Treeからボード固有の情報を取得します。
そして取得した情報をグローバル変数machine_desc、machine_name、reboot_modeに
コピーします(無駄な操作のように感じますが、何か理由があるのでしょうか)。

init_mm.start_codeから始まる処理ですが、mm_struct構造体であるinit_mmはmm/init-mm.cで宣言されています
この構造体の定義はinclude/linux/mm_types.hに記載され、この内容をメモリディスクリプタと呼びます。
ここでのメモリディスクリプタとは、プロセスアドレス空間に関する全ての情報を管理する構造体です。

今回の処理では、このメモリディスクリプタにinitプロセス用のメモリセクション情報を書き込みます。
詳細なセクション情報は、リンカスクリプト(arch/arm/kernel/vmlinux.lds.S)に記載されています。
このタイミングで書き込まれるセクション情報は以下の通りです。
 ・start_code:関数が格納されているセクションの先頭アドレス
 ・end_code:関数が格納されているセクションの終端アドレス
 ・end_data:初期値ありデータの終端アドレス
 ・brk:現時点でのヒープ領域の終端アドレス

strlcpy()では、boot_command_lineの内容をcmd_lineにコピー(保存)します。
引数はarch/arm/kernel/setup.cで、static char __initdata cmd_line[COMMAND_LINE_SIZE];
init/main.cで、char __initdata boot_command_line[COMMAND_LINE_SIZE];
arch/arm/include/uapi/asm/setup.cで、#define COMMAND_LINE_SIZE 1024とそれぞれ定義されています。

そして、setup_arch()の引数であるcmdline_pの値を書き換えます(ポインタのポインタ……)。
ここで書き換えた値を用いて、start_kernel()内のsetup_command_line()で初期設定を行うようです。
(暫く後の処理なので、別の場で読み解きます)

parse_early_param()では、earlyフラグがついたコマンドラインオプションのチェックを行います。
early_paging_init()では、LPAE(Large Physical Address Extension)で4GB以上のアドレス空間を使用できるように、
ページテーブルを作り直すようです(読みきれていないため、詳しい記述は後日に記載します)。

request_standard_resources()では、各メモリ資源の登録作業を行います(以下、定義)。

arch/arm/kernel/setup.c
/*
 * Standard memory resources
 */
static struct resource mem_res[] = {
	{
		.name = "Video RAM",
		.start = 0,
		.end = 0,
		.flags = IORESOURCE_MEM
	},
	{
		.name = "Kernel code",
		.start = 0,
		.end = 0,
		.flags = IORESOURCE_MEM
	},
	{
		.name = "Kernel data",
		.start = 0,
		.end = 0,
		.flags = IORESOURCE_MEM
	}
};

#define video_ram   mem_res[0]
#define kernel_code mem_res[1]
#define kernel_data mem_res[2]

static void __init request_standard_resources(const struct machine_desc *mdesc)
{
	struct memblock_region *region;
	struct resource *res;

	kernel_code.start   = virt_to_phys(_text);
	kernel_code.end     = virt_to_phys(_etext - 1);
	kernel_data.start   = virt_to_phys(_sdata);
	kernel_data.end     = virt_to_phys(_end - 1);

	for_each_memblock(memory, region) {
		res = memblock_virt_alloc(sizeof(*res), 0);
		res->name  = "System RAM";
		res->start = __pfn_to_phys(memblock_region_memory_base_pfn(region));
		res->end = __pfn_to_phys(memblock_region_memory_end_pfn(region)) - 1;
		res->flags = IORESOURCE_MEM | IORESOURCE_BUSY;

		request_resource(&iomem_resource, res);

		if (kernel_code.start >= res->start &&
		    kernel_code.end <= res->end)
			request_resource(res, &kernel_code);
		if (kernel_data.start >= res->start &&
		    kernel_data.end <= res->end)
			request_resource(res, &kernel_data);
	}

	if (mdesc->video_start) {
		video_ram.start = mdesc->video_start;
		video_ram.end   = mdesc->video_end;
		request_resource(&iomem_resource, &video_ram);
	}

	/*
	 * Some machines don't have the possibility of ever
	 * possessing lp0, lp1 or lp2
	 */
	if (mdesc->reserve_lp0)
		request_resource(&ioport_resource, &lp0);
	if (mdesc->reserve_lp1)
		request_resource(&ioport_resource, &lp1);
	if (mdesc->reserve_lp2)
		request_resource(&ioport_resource, &lp2);
}

最初のkernel_codeを書き換える処理は、resource構造体の初期化処理に相当します。
resource構造体はデバイスに割り当てたI/O資源(ポート)やメモリ情報を管理する仕組みで、定義は以下の通りです。

include/linux/ioport.h
struct resource {
	resource_size_t start; //資源範囲の先頭
	resource_size_t end;   //資源範囲の末尾
	const char *name;      //資源所有者の説明
	unsigned long flags;   //各種フラグ
	struct resource *parent, *sibling, *child;  //資源ツリー中の親、子、次の資源へのポインタ
};

kernel_codeの初期化では、virt_to_physマクロの仮想-物理アドレス変換を用いて、
.textと.dataセクションの先頭と末尾を物理アドレスで格納しています。
その後の処理全てはrequest_resource()によって、各メモリ資源の登録作業を行います。

unflatten_device_tree()では、Device Treeを用いて、
device_node構造体(ボード固有の情報)の初期化を行います。

include/linux/of.h
struct device_node {
	const char *name;
	const char *type;
	phandle phandle;
	const char *full_name;
	struct fwnode_handle fwnode;

	struct	property *properties;
	struct	property *deadprops;	/* removed properties */
	struct	device_node *parent;
	struct	device_node *child;
	struct	device_node *sibling;
	struct	kobject kobj;
	unsigned long _flags;
	void	*data;
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
};

「ボード固有の情報なのに、ノード?」と不思議に思いましたが、各種周辺機器と一緒に、
ボード自体もファイルとして扱われるようです。考えてみれば、ボードだけ特別扱いする必要も無いですね。

同様に、arm_dt_init_cpu_maps()、psci_init()でも、
CPU情報とPSCI情報(Power State Coordination Interface)をDevice_nodeに組み込みます。
ここでのPSCIとは、パワーマネージメント機能の一つで、CPUコアをidle状態にさせたり、
CPUの電源をON/OFF状態の切り替え、サスペンド機能を提供するインターフェイスです。

reserve_crashkernel()では、その名の通り、kdump用のメモリを確保します。
ここでのメモリは、kdumpがLinux Kernelのクラッシュ原因を示す情報(vmcore)を作成するために使用し、
そのサイズはカーネルビルド時のオプションでcrashkernel=<size>(デフォルト=128MB)で指定します。
これで、setup_arch()の処理は終了です。

#次回
未定
Raspberry Pi Computer Architecture Essentialsを購入したので、
暫くはカーネルコード読む時間が取れません。

##参考
CPU cache

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?