Skip to content

Latest commit

 

History

History
548 lines (427 loc) · 31 KB

linux-initialization-6.md

File metadata and controls

548 lines (427 loc) · 31 KB

커널 초기화. Part 6.

아키텍쳐 별 초기화

이전의 파트에서 우리는 arch/x86/kernel/setup.c에서 아키텍쳐 별(우리의 경우 x86_64) 초기화를 보았고 NX bit의 지원에 의존하는 _PAGE_NX플래그로 설정된 x86_configure_nx함수를 끝냈습니다. 이전에 작성한 setup_arch함수와 start_kernel이 매우 크므로 이번과 다음 파트에서 우리는 아키텍쳐 별 초기화 프로세스를 계속할 것입니다. x86_configure_nx의 다음 함수는 parse_early_param입니다. 이 함수는 init/main.c에서 정의됐으며 이름에서 알 수 있듯이, 이 함수는 커널 커맨드 라인을 분석하고 주어진 매개변수에 따라 다른 서비스를 설정합니다.(모든 커널 커맨드 라인 매개변수는 Documentation/kernel-parameters.txt에서 찾을 수 있습니다). 당신은 아마 초기 파트에서 우리가 어떻게 earlyprintk을 설정했는지 기억할 것입니다. 초반부에 우리는 커널 매개변수를 봤습니다. 그것들의 값은 arch/x86/boot/cmdline.ccmdline_find_option함수와 __cmdline_find_option,__cmdline_find_option_bool헬퍼입니다. 우리는 아키텍쳐에 의존하지 않는 일반적인 커널 파트를 보며 다른 접근법을 사용합니다. 리눅스 소스 코드를 읽는 중이라면 다음과 같은 호출에 주목하십시오:

early_param("gbpages", parse_direct_gbpages_on);

early_param매크로는 두 매개변수를 가집니다:

  • 커맨드 라인 매개변수의 이름;
  • 주어진 매개변수가 전달되면 호출되는 함수

다음과 같이 정의됩니다:

#define early_param(str, fn) \
        __setup_param(str, fn, fn, 1)

include/linux/init.h에 포함됩니다. early_param매크로가 __setup_param매크로를 호출하는 것을 볼 수 있습니다:

#define __setup_param(str, unique_id, fn, early)                \
        static const char __setup_str_##unique_id[] __initconst \
                __aligned(1) = str; \
        static struct obs_kernel_param __setup_##unique_id      \
                __used __section(.init.setup)                   \
                __attribute__((aligned((sizeof(long)))))        \
                = { __setup_str_##unique_id, fn, early }

이 매크로는 __setup_str_*_id변수(*주어진 함수 이름에 따라 다름)를 정의하고 주어진 커맨드 라인 매개변수의 이름을 정의합니다. 다음 줄에서 우리는 obs_kernel_param타입의 __setup_*변수의 정의와 초기화를 볼 수 있습니다. obs_kernel_param구조체의 정의:

struct obs_kernel_param {
        const char *str;
        int (*setup_func)(char *);
        int early;
};

and contains three fields: 그리고 3개의 필드를 포함합니다:

  • 커널 매개변수의 이름;
  • 매개변수에 따라 다른 무언가를 설정하는 함수;
  • 필드가 초기 값(1)인지 아닌지(0)를 결정.

참고 __set_param매크로를 정의하는 __section(.init.setup)속성. 이것은 모든 __setup_str_*.init.setup섹션에 위치될 수 있음을 의미합니다. 더욱이 include/asm-generic/vmlinux.lds.h에서 볼 수 있는 것처럼 그것들은 __setup_start__setup_end사이에 배치될 것입니다:

#define INIT_SETUP(initsetup_align)                \
                . = ALIGN(initsetup_align);        \
                VMLINUX_SYMBOL(__setup_start) = .; \
                *(.init.setup)                     \
                VMLINUX_SYMBOL(__setup_end) = .;

이제 우리는 매개변수가 어떻게 정의되는지 알았습니다. parse_early_param의 구현으로 돌아갑시다:

void __init parse_early_param(void)
{
        static int done __initdata;
        static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;

        if (done)
                return;

        /* All fall through to do_early_param. */
        strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
        parse_early_options(tmp_cmdline);
        done = 1;
}

parse_early_param함수는 두개의 정적 변수를 정의합니다. 먼저 doneparse_early_param가 이미 호출됬는지 확인하고 두번째는 커널 커맨드 라인을 위한 임시 저장소 입니다. 그 다음 우리는 방금 정의한 임시 커맨드 라인에 boot_command_line를 복사하고 동일한 소스 코드 main.c파일에서 parse_early_options함수를 호출합니다. parse_early_optionskernel/params.c에서 parse_args함수를 호출합니다. parse_args는 주어진 커맨드 라인을 분석하고 do_early_param함수를 호출합니다. 이 함수 __setup_start에서 __setup_end으로 이동하고, 매개변수가 early인 경우 obs_kernel_param함수를 호출합니다. 그 다음 early 커맨드 라인 매개변수에 의존하는 모든 서비스가 설정된 다음 parse_early_paramx86_report_nx을 호출합니다. 이 파트의 도입부에서 썻듯이 우리는 이미 NX-bitx86_configure_nx로 설정했습니다. 다음으로 arch/x86/mm/setup_nx.cx86_report_nx함수는 NX의 정보를 출력합니다. 참고로 x86_report_nxx86_configure_nx의 바로 다음이 아니라 parse_early_param를 호출한 다음 호출합니다. 대답은 간단합니다: 커널이 noexec 매개변수를 지원하므로 우리는 그것을 parse_early_param다음에 호출합니다:

noexec		[X86]
			On X86-32 available only on PAE configured kernels.
			noexec=on: enable non-executable mappings (default)
			noexec=off: disable non-executable mappings

우리는 그것의 booting 시간을 볼 수 있습니다:

NX

다음으로 함수의 호출을 볼 수 있습니다:

	memblock_x86_reserve_range_setup_data();

이 함수는 같은 arch/x86/kernel/setup.c 소스 코드 파일에서 정의됐습니다. setup_data메모리를 다시 매핑하고 setup_data(setup_data에 대한 정보는 이전의 파트에서 읽을 수 있고 ioremapmemblock에 관해서는 리눅스 커널 메모리 관리에서 읽을 수 있습니다)메모리 블록을 전달합니다.

다음 단계에서는 다음과 같은 조건문을 볼 수 있습니다:

	if (acpi_mps_check()) {
#ifdef CONFIG_X86_LOCAL_APIC
		disable_apic = 1;
#endif
		setup_clear_cpu_cap(X86_FEATURE_APIC);
	}

먼저 arch/x86/kernel/acpi/boot.cacpi_mps_check함수는 CONFIG_X86_LOCAL_APICCONFIG_x86_MPPARSE작동 옵션에 의존합니다:

int __init acpi_mps_check(void)
{
#if defined(CONFIG_X86_LOCAL_APIC) && !defined(CONFIG_X86_MPPARSE)
        /* mptable code is not built-in*/
        if (acpi_disabled || acpi_noirq) {
                printk(KERN_WARNING "MPS support code is not built-in.\n"
                       "Using acpi=off or acpi=noirq or pci=noacpi "
                       "may have problem\n");
                 return 1;
        }
#endif
        return 0;
}

내장 MPS또는 다중프로세서 사양테이블을 확인합니다. CONFIG_X86_LOCAL_APIC가 설정되고 CONFIG_x86_MPPAARSE이 설정되지 않은 경우, 다음 옵션들 중 하나가 커널에 전달되면 acpi_mps_check는 경고 메시지를 출력합니다: acpi=off, acpi=noirq 또는 pci=noacpi. acpi_mps_check1을 반환하면 그것은 로컬 APIC가 비활성화되고 setup_clear_cpu_cap매크로에서 현재 CPUdml X86_FEATURE_APIC비트가 지워진 것을 의미합니다.(CPU마스크에 대한 것은 CPU마스크에서 읽을 수 있습니다).

초기 PCI 덤프

다음 단계에서는 다음 코드를 사용하여 PCI장치의 덤프를 만듭니다:

#ifdef CONFIG_PCI
	if (pci_early_dump_regs)
		early_dump_pci_devices();
#endif

pci_early_dump_regs변수는 arch/x86/pci/common.c에 정의되었고 이 값은 커널 커맨드 라인 매개변수:pci=earlydump에 의존합니다. 우리는 drivers/pci/pci.c안에서 이 매개변수의 정의를 찾을 수 있습니다:

early_param("pci", pci_setup);

pci_setup함수는 pci=문자열을 가져온 다음 분석합니다. 이 함수는 drivers/pci/pci.c안의 __weak에서 정의된 pcibios_setup을 호출하고 모든 아키텍쳐는 __weak분석을 중단하는 동일한 함수를 정의합니다. 예를 들어 x86_64아키텍쳐 종속 버전은 arch/x86/pci/common.c에 있습니다:

char *__init pcibios_setup(char *str) {
        ...
		...
		...
		} else if (!strcmp(str, "earlydump")) {
                pci_early_dump_regs = 1;
                return NULL;
        }
		...
		...
		...
}

따라서 CONFIG_PCI옵션이 설정되고 pci=earlydump옵션이 커널 커맨드 라인에 전달되면, arch/x86/pci/early.cearly_dump_pci_devices함수가 호출됩니다. 이 함수는 다음과 같이 noearly pci 매개변수를 확인합니다:

if (!early_pci_allowed())
        return;

그리고 그것이 전달되면 반환합니다. 각 PCI 도메인은 최대 256개의 버스를 호스트 할 수 있으며 각 버스는 최대 32개의 장치를 호스트 할 수 있습니다. 루프로 가봅시다:

for (bus = 0; bus < 256; bus++) {
                for (slot = 0; slot < 32; slot++) {
                        for (func = 0; func < 8; func++) {
						...
						...
						...
                        }
                }
}

read_pci_config함수로 pci설정을 읽읍시다.

이것이 전부입니다. 우리는 pci의 세부사항은 깊게 들어가지 않지만 특수 Drivers/PCI파트에서 자세한 것을 볼 것입니다.

메모리 분석 완료

다음은 커널 설정의 첫 단계파트에서 얻은 early_dump_pci_devices의 사용 가능한 메모리 및 e820과 관련된 몇 가지 함수입니다.

	/* update the e820_saved too */
	e820_reserve_setup_data();
	finish_e820_parsing();
	...
	...
	...
	e820_add_kernel_range();
	trim_bios_range(void);
	max_pfn = e820_end_of_ram_pfn();
	early_reserve_e820_mpc_new();

이것을 봅시다. 보시다시피 첫 번째 함수는 e820_reserve_setup_data입니다. 이 함수는 우리가 위에서 본 memblock_x86_reserve_range_setup_data와 거의 같지만 우리의 경우 주어진 E820_RESERVED_KERN 타입의 e820map에 새 지역을 더하는 e820_update_range을 호출합니다. 다음 함수는 sanitize_e820_map함수의 e820map을 제거하는 finish_e820_parsing함수입니다. 이 두 함수외에도 우리는 e820과 연관된 몇 가지 함수를 위의 목록에서 볼 수 있습니다. e820_add_kernel_range함수는 커널의 시작과 끝의 물리적 주소를 취합니다:

u64 start = __pa_symbol(_text);
u64 size = __pa_symbol(_end) - start;

.text .data.bsse820mapE820RAM로 표시되는지 확인하고 아니라면 경고 메시지를 출력합니다. 다음으로 trm_bios_range함수는 e820Map의 4096바이트를 E820_RESERVED으로 업데이트하고 sanitize_e820_map를 호출해서 그것을 다시 제거합니다. 다음으로 e820_end_of_ram_pfn함수를 호출해서 마지막 페이지의 프레임 넘버를 얻습니다. 모든 메모리 페이지는 고유의 넘버 Page frame number를 가집니다. e820_end_of_ram_pfn함수는 e820_end_pfn 호출의 최대값을 반환합니다:

unsigned long __init e820_end_of_ram_pfn(void)
{
	return e820_end_pfn(MAX_ARCH_PFN);
}

e820_end_pfn에서 특정 아키텍쳐(MAX_ARCH_PFNx86_64를 위해 0x400000000)의 최대 페이지 프레임 수를 가져옵니다. e820_end_pfn에서 우리는 모든 e820슬롯을 통과하고 e820E820_RAM 또는 E820_PRAM타입에 진입했는지 확인합니다. 왜냐하면 우리는 이 타입들만을 위해 페이지 프레임 넘버를 계산했기 때문입니다. 기본 주소와 현재 e820 진입을 위한 페이지 프레임 넘버의 마지막 주소를 얻고 이 주소에 대해 몇 가지 확인을 합니다:

for (i = 0; i < e820.nr_map; i++) {
		struct e820entry *ei = &e820.map[i];
		unsigned long start_pfn;
		unsigned long end_pfn;

		if (ei->type != E820_RAM && ei->type != E820_PRAM)
			continue;

		start_pfn = ei->addr >> PAGE_SHIFT;
		end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;

        if (start_pfn >= limit_pfn)
			continue;
		if (end_pfn > limit_pfn) {
			last_pfn = limit_pfn;
			break;
		}
		if (end_pfn > last_pfn)
			last_pfn = end_pfn;
}
	if (last_pfn > max_arch_pfn)
		last_pfn = max_arch_pfn;

	printk(KERN_INFO "e820: last_pfn = %#lx max_arch_pfn = %#lx\n",
			 last_pfn, max_arch_pfn);
	return last_pfn;

다음으로 우리는 last_pfn에서 루프에 있는 것이 특정 아키텍쳐(우리의 경우 x86_64)의 최대 페이지 프레임 수보다 크지 않은지 확인하고 마지막 페이지 프레임 넘버의 정보를 출력하고 그것을 반환합니다. 우리는 dmesg출력에서 last_pfn을 볼 수 있습니다:

...
[    0.000000] e820: last_pfn = 0x41f000 max_arch_pfn = 0x400000000
...

그 후, 가장 큰 페이지 프레임 넘버를 계산할 때, max_low_pfnlow memory 또는 첫 4기가바이트 이하의 가장 큰 페이지 프레임 넘버인지를 계산합니다. 4기가바이트 이상의 RAM을 설치한 경우 max_low_pfne820_end_of_ram_pfn과 동일한 e820_end_of_low_ram_pfn함수의 결과가 될 것이지만 4기가바이트의 제한이 있습니다. 다른 방식으로는 max_low_pfn이 다음처럼 max_pfn과 같아질 것입니다:

if (max_pfn > (1UL<<(32 - PAGE_SHIFT)))
	max_low_pfn = e820_end_of_low_ram_pfn();
else
	max_low_pfn = max_pfn;
		
high_memory = (void *)__va(max_pfn * PAGE_SIZE - 1) + 1;

다음으로 우리는 주어진 물리적 메모리에 의해 가상 주소를 반환하는 __va매크로의 high_memory(직접 맵 메모리에서 상한을 정의)를 계산합니다.

DMI 스캐닝

다음 단계는 다른 메모리 지역과 e820 슬롯을 조작한 후 컴퓨터에 대한 정보를 모으는 것입니다. 데스크탑 관리 인터페이스 및 다음 함수를 통해 모든 정보를 얻을 수 있습니다:

dmi_scan_machine();
dmi_memdev_walk();

먼저 dmi_scan_machinedrivers/firmware/dmi_scan.c에 정의되어 있습니다. 이 함수는 시스템 관리 BIOS구조체를 통해서 정보를 추출합니다. SMBIOS테이블에 접근하기 위해 지정된 두 가지 방법이 있습니다: EFI구성 테이블에서 SMBIOS테이블의 포인터를 얻고 0xF00000x10000주소 사이의 물리적 메모리를 스캔합니다. 두 번째 방법을 보겠습니다. dmi_scan_machine함수는 early_ioremap를 확장하는 dmi_early_remap에서 0xf00000x10000 사이의 메모미를 다시 매핑합니다:

void __init dmi_scan_machine(void)
{
	char __iomem *p, *q;
	char buf[32];
	...
	...
	...
	p = dmi_early_remap(0xF0000, 0x10000);
	if (p == NULL)
			goto error;

모든 DMI헤더 주소를 반복하고 _SM_문자열을 검색해서 찾습니다:

memset(buf, 0, 16);
for (q = p; q < p + 0x10000; q += 16) {
		memcpy_fromio(buf + 16, q, 16);
		if (!dmi_smbios3_present(buf) || !dmi_present(buf)) {
			dmi_available = 1;
			dmi_early_unmap(p, 0x10000);
			goto out;
		}
		memcpy(buf, buf + 16, 16);
}

_SM_문자열은 000F0000h0x000FFFFF 사이에 있어야 합니다. 여기에서 우리는 16바이트를 동일한 memcpy에서 memcpy_fromiobuf에 복사하고 버퍼에서 dmi_smbios3_presentdmi_present를 실행합니다. 이 함수는 첫 4바이트가 _SM_ 문자열인지 확인하고 SMBIOS버전을 얻고 DMI구조체 테이블 길이, 테이블 주소 등등 _DMI_의 속성을 얻습니다. 다음으로 이 함수 중 하나가 완료되면 dmesg출력에서 결과를 볼 수 있습니다:

[    0.000000] SMBIOS 2.7 present.
[    0.000000] DMI: Gigabyte Technology Co., Ltd. Z97X-UD5H-BK/Z97X-UD5H-BK, BIOS F6 06/17/2014

dmi_scan_machine의 끝으로 우리는 이전에 재매핑된 메모리를 언매핑합니다:

dmi_early_unmap(p, 0x10000);

두 번째 함수는 dmi_memdev_walk입니다. 이해할 수 있도록 메모리 장치를 살펴봅시다:

void __init dmi_memdev_walk(void)
{
	if (!dmi_available)
		return;

	if (dmi_walk_early(count_mem_devices) == 0 && dmi_memdev_nr) {
		dmi_memdev = dmi_alloc(sizeof(*dmi_memdev) * dmi_memdev_nr);
		if (dmi_memdev)
			dmi_walk_early(save_mem_devices);
	}
}

이것은 DMI가 사용가능한지(이전 함수 dmi_scan_machine에서 얻었습니다) 확인하고 dmi_walk_early에서 메모리 장치의 정보와 다음과 같이 정의된 dmi_alloc을 수집합니다.

#ifdef CONFIG_DMI
RESERVE_BRK(dmi_alloc, 65536);
#endif

RESERVE_BRKarch/x86/include/asm/setup.h에 정의되었고 brk섹션에서 주어진 크기의 공간을 예약합니다.


init_hypervisor_platform();
x86_init.resources.probe_roms();
insert_resource(&iomem_resource, &code_resource);
insert_resource(&iomem_resource, &data_resource);
insert_resource(&iomem_resource, &bss_resource);
early_gart_iommu_check();

SMP 설정

다음 단계는 SMP구성의 분석입니다. 함수 내부를 호출하는 find_smp_config함수의 호출을 실행합니다:

static inline void find_smp_config(void)
{
        x86_init.mpparse.find_smp_config();
}

x86_init.mpparse.find_smp_configarch/x86/kernel/mpparse.cdefault_find_smp_config함수에 있습니다. default_find_smp_config함수 안에서 우리는 SMP설정을 위해 몇 개의 메모리 지역을 스캔해서 그것들을 찾은 경우 반환합니다.

if (smp_scan_config(0x0, 0x400) ||
            smp_scan_config(639 * 0x400, 0x400) ||
            smp_scan_config(0xF0000, 0x10000))
            return;

우선 모든 smp_scan_config함수는 몇 가지 변수를 정의합니다:

unsigned int *bp = phys_to_virt(base);
struct mpf_intel *mpf;

첫 번째는 SMP구성을 스캔 할 메모리 지역의 가상 주소이고, 두 번째는 mpf_intel구조체에 대한 포인터입니다. mpf_intel가 무엇인지 이해해봅시다. 모든 정보는 멀티 프로세서 구성 데이터 구조체에 저장됩니다. mpf_intel은 이 구조체에 있습니다:

struct mpf_intel {
        char signature[4];
        unsigned int physptr;
        unsigned char length;
        unsigned char specification;
        unsigned char checksum;
        unsigned char feature1;
        unsigned char feature2;
        unsigned char feature3;
        unsigned char feature4;
        unsigned char feature5;
};

설명서에서 읽었듯이 시스템 BIOS의 주요 기능 중 하나는 MP 부동 포인터 구조체와 MP 구성 테이블을 구성하는 것입니다. 그리고 운영 체제는 멀티 프로세서 구성에 대한 정보에 접근해야하며 mpf_intel은 멀티프로세서 구성 테이블의 물리적 주소(두 번째 매개변수를 참조)를 저장해야합니다. 따라서 smp_scan_config는 주어진 메모리 범위를 통해서 루프를 탐색하고 그곳에서 MP floating pointer structure를 찾으려 시도합니다. 현재 바이트 포인터가 SMP서명을 가리키는 지 확인하고 체크썸을 확인합니다. 그리고 루프에서 mpf->specification가 1 또는 4(1 또는 4로 설명되어야 함)인지 확인합니다:

while (length > 0) {
if ((*bp == SMP_MAGIC_IDENT) &&
    (mpf->length == 1) &&
    !mpf_checksum((unsigned char *)bp, 16) &&
    ((mpf->specification == 1)
    || (mpf->specification == 4))) {

        mem = virt_to_phys(mpf);
        memblock_reserve(mem, sizeof(*mpf));
        if (mpf->physptr)
            smp_reserve_memory(mpf);
	}
}

memblock_reserve의 검색에 성공하면 주어진 메모리 블록을 예약하고 멀티프로세서 구성 테이블의 물리적 주소를 예약합니다. 이것에 관한 문서 - 멀티프로세서 설명서에서 찾아볼 수 있습니다. SMP 특별 파트에서도 더 상세한 내용을 읽을 수 있습니다.

추가적인 초기 메모리 초기화 루틴

setup_arch의 다음 단계에서 우리는 초기 단계를 위해 페이지 테이블 버퍼를 할당하는 early_alloc_pgt_buf함수의 호출을 볼 수 있습니다. 페이지 테이블 버퍼는 brk영역에 배치됩니다. 구현을 살펴봅시다:

void  __init early_alloc_pgt_buf(void)
{
        unsigned long tables = INIT_PGT_BUF_SIZE;
        phys_addr_t base;

        base = __pa(extend_brk(tables, PAGE_SIZE));

        pgt_buf_start = base >> PAGE_SHIFT;
        pgt_buf_end = pgt_buf_start;
        pgt_buf_top = pgt_buf_start + (tables >> PAGE_SHIFT);
}

첫째로 페이지 테이블 버퍼의 크기를 얻으면 현재 리눅스 커널 4.0에서 (6 * PAGE_SIZE)INIT_PGT_BUF_SIZE가 될 것입니다. 따라서 페이지 테이블 버퍼의 크기를 얻으면 크기와 정렬이라는 두 매개변수를 사용해 extend_brk함수를 호출합니다. 이름에서 알 수 있듯이 이 함수는 brk영역을 확장합니다. 리눅스 커널 스크립트 brkBSS의 바로 다음 메모리에서 볼 수 있습니다:

	. = ALIGN(PAGE_SIZE);
	.brk : AT(ADDR(.brk) - LOAD_OFFSET) {
		__brk_base = .;
		. += 64 * 1024;		/* 64k alignment slop space */
		*(.brk_reservation)	/* areas brk users have reserved */
		__brk_limit = .;
	}

또는 readelf유틸을 사용해서 찾을 수 있습니다:

brk area

__pa매크로를 통해 새로운 brk의 물리적 주소를 얻었으면, 기본 주소와 페이지 테이블 버퍼의 끝을 계산합니다. 다음 단계는 페이지 테이블 주소를 얻었으니 reserve_brk함수를 사용해서 brk영역을 위한 메모리 블록을 예약합니다.

static void __init reserve_brk(void)
{
	if (_brk_end > _brk_start)
		memblock_reserve(__pa_symbol(_brk_start),
				 _brk_end - _brk_start);

	_brk_start = 0;
}

참고로 reserve_brk의 끝에서 아무것도 할당하지 않기 때문에 brk_start을 0으로 설정했습니다. brk를 위한 메모리 블록을 예약한 다음 cleanup_highmap함수를 사용해 커널 매핑 범위를 벗어난 메모리 영역을 언매핑해야합니다. 커널 매핑이 __START_KERNEL_map_end - _text 또는 _text커널을 매핑하는 level2_kernel_pgt, databss인 것을 기억하십시오. clean_high_map의 시작에서 다음과 같은 매개변수들을 정의합니다.

unsigned long vaddr = __START_KERNEL_map;
unsigned long end = roundup((unsigned long)_end, PMD_SIZE) - 1;
pmd_t *pmd = level2_kernel_pgt;
pmd_t *last_pmd = pmd + PTRS_PER_PMD;

커널 매핑의 시작과 끝이 정의된 지금, 우리는 _textend사이에 있지 않은 모든 커널 페이지 미들 디렉터리 엔트리와 클린 엔트리를 통해 루프로 이동합니다:

for (; pmd < last_pmd; pmd++, vaddr += PMD_SIZE) {
        if (pmd_none(*pmd))
            continue;
        if (vaddr < (unsigned long) _text || vaddr > end)
            set_pmd(pmd, __pmd(0));
}

그 후에 memblock_set_current_limit함수(memblock에 대한 것은리눅스 커널 메모리 관리 파트 2에서 읽을 수 있습니다)를 사용해 memblock할당을 위한 제한을 설정합니다. 그것은 ISA_END_ADDRESS 또는 0x100000가 되고 memblock_x86_fill함수의 호출 e820에 따라서 memblock의 정보를 채웁니다. 커널 초기화 시간에서 이 함수의 결과를 볼 수 있습니다.

MEMBLOCK configuration:
 memory size = 0x1fff7ec00 reserved size = 0x1e30000
 memory.cnt  = 0x3
 memory[0x0]	[0x00000000001000-0x0000000009efff], 0x9e000 bytes flags: 0x0
 memory[0x1]	[0x00000000100000-0x000000bffdffff], 0xbfee0000 bytes flags: 0x0
 memory[0x2]	[0x00000100000000-0x0000023fffffff], 0x140000000 bytes flags: 0x0
 reserved.cnt  = 0x3
 reserved[0x0]	[0x0000000009f000-0x000000000fffff], 0x61000 bytes flags: 0x0
 reserved[0x1]	[0x00000001000000-0x00000001a57fff], 0xa58000 bytes flags: 0x0
 reserved[0x2]	[0x0000007ec89000-0x0000007fffffff], 0x1377000 bytes flags: 0x0

The rest functions after the memblock_x86_fill are: early_reserve_e820_mpc_new allocates additional slots in the e820map for MultiProcessor Specification table, reserve_real_mode - reserves low memory from 0x0 to 1 megabyte for the trampoline to the real mode (for rebooting, etc.), trim_platform_memory_ranges - trims certain memory regions started from 0x20050000, 0x20110000, etc. these regions must be excluded because Sandy Bridge has problems with these regions, trim_low_memory_range reserves the first 4 kilobyte page in memblock, init_mem_mapping function reconstructs direct memory mapping and setups the direct mapping of the physical memory at PAGE_OFFSET, early_trap_pf_init setups #PF handler (we will look on it in the chapter about interrupts) and setup_real_mode function setups trampoline to the real mode code.

That's all. You can note that this part will not cover all functions which are in the setup_arch (like early_gart_iommu_check, mtrr initialization, etc.). As I already wrote many times, setup_arch is big, and linux kernel is big. That's why I can't cover every line in the linux kernel. I don't think that we missed something important, but you can say something like: each line of code is important. Yes, it's true, but I missed them anyway, because I think that it is not realistic to cover full linux kernel. Anyway we will often return to the idea that we have already seen, and if something is unfamiliar, we will cover this theme.

결론

It is the end of the sixth part about linux kernel initialization process. In this part we continued to dive in the setup_arch function again and it was long part, but we are not finished with it. Yes, setup_arch is big, hope that next part will be the last part about this function.

If you have any questions or suggestions write me a comment or ping me at twitter.

Please note that English is not my first language, And I am really sorry for any inconvenience. If you find any mistakes please send me PR to linux-insides.

링크