Go는 Google에서 만든 프로그래밍 언어로 C 언어의 차기 버전 언어라는 느낌이 물씬 풍깁니다. C언어에 익숙한 개발자라면 C의 장점을 그대로 가지면서 C언어에서 불편했던 부분을 개선했다는 것을 느낄 수 있습니다. 최근 프로그래밍의 주요 이슈인 병렬화 기능도 언어 차원에서 매우 직관적이고 심플하게 제공합니다. 데니스 리치와 더불어 유닉스를 개발했던 켄 톰슨이 개발자로 참여하고 있습니다.
< 유닉스 창시자 켐 톰슨(좌)과 데니스 리치(우) >
Go의 태생은 C언어가 많이 쓰이는 분야인 시스템 프로그램 분야에 사용 할 목적으로 개발 되었다고 합니다. 하지만 지금은 발전을 거듭하여 범용 프로그래밍 언어로 바뀌었습니다. 제 개인적인 생각으로는 C 처럼 시스템(저수준) 분야에서 자유롭게 쓰기에는 제약 사항이 많아서 조금 힘들것 같습니다. 가비지 컬렉터가 실행파일에 포함되는 방식이라 바이너리 조작이 복잡합니다. 뭐 이것도 Go 컴파일러를 어떻게 구현하느냐에 따르는 점이라서 Go 프로젝트에서 필요성을 느낀다면 추후에 추가되지 않을까 기대 해 봅니다.
프로그래밍 언어의 범용성과 시스템 친화성(?)은 서로 상반된 특성이라 언어를 설계할때 언어의 철학에 따라 적절한 합의점(threshold)을 정해야 합니다. Go는 태생이 시스템 언어에서 범용으로 발전한 언어라서 그런지 C++과 Java 사이 에 위치하고 있습니다. 즉, 시스템 친화성을 기준으로 보면, C > C++ > Go > Java, C# > Python, Perl 정도가 될 것 같습니다. 서로 상반된 특징 이지만 양쪽 모두를 수용할 수 있는 기능이 추가되면 좋겠습니다.
이제부터 Xen은 가상 주소(Virtual Address)를 사용하기 위해 페이지 테이블을 만든다. 지금 시점에서 생성하는 페이지 테이블은 부팅 단계에서만 임시로 사용되며 나중에 실제 사용 할 페이지 테이블로 교체된다. [그림 1]은 x86_64 아키텍처 롱 모드에서 64 비트 가상 주소를 물리 주소로 변환하는 4단계 페이징 과정을 보여준다.
/* Hook identity-map, xen-map, and boot-map L3 tables into PML4. */ --- (3)
mov $(sym_phys(l3_bootmap)+7),%eax
mov %eax,sym_phys(idle_pg_table) + 0*8
mov $(sym_phys(l3_identmap)+7),%eax
mov %eax,sym_phys(idle_pg_table)+
l4_table_offset(DIRECTMAP_VIRT_START)*8
mov $(sym_phys(l3_xenmap)+7),%eax
mov %eax,sym_phys(idle_pg_table) +
l4_table_offset(XEN_VIRT_START)*8
Xen은 2 메가바이트 페이지를 사용하는 페이지 테이블을 생성한다. 롱 모드에서 2 메가바이트 페이지를 사용 할 때는 3단계 페이징을 한다. x86_64 아키텍처에서는 페이지 크기를 4 킬로바이트, 2 메가바이트, 4 메가바이트, 1기가바이트(롱 모드 전용)로 설정 할 수 있으며, 아키텍처 동작모드와 페이지 크기에 따라 페이징 단계가 모두 다르다. 일반적으로 운영체제에서는 4 킬로바이트 페이지를 가장 많이 사용한다.
loop 명령어는 ecx 레지스터 값만큼 루프를 돈다. l2_identmap/l2_xenmap/l2_bootmap 세 개의 페이지 디렉토리 테이블의 8개의 엔트리를 초기화 한다(1). 각 페이지 테이블 엔트리를 모두 동일한 값(0x1e3)으로 설정한다. 페이지 디렉토리 테이블의 엔트리는 [그림 2]와 같다.
[그림 2] 2 메가바이트페이지디렉토리엔트리[1]
페이지 디렉토리 엔트리의 7번째 비트가 1이면 2 메가바이트 페이지를 사용하고, 0이면 4 킬로바이트 페이지를 사용한다. 4 킬로바이트 페이지를 사용할 때는 'G', 'D' 필드는 사용되지 않고, '물리 페이지 기준 주소(Physical Page Base Address)'는 '페이지 테이블 주소(Page Table Base Address)'를 가리킨다. 즉, 페이징 단계가 하나 더 늘어난다(4단계 페이징).
여기서 페이지 디렉토리 엔트리를 'PRESENT+RW+A+D+2MB+GLOBAL'로 설정하였다. 페이지 디렉토리 엔트리에서 각 필드가 의미하는 바는 [표 1]과 같다.
표 1 페이지 테이블 엔트리 필드
마지막에 eax 레지스터에 '(1 <<L2_PAGETABLE_SHIFT)' 값을 더해준 이유는 해당 페이지의 물리 메모리 페이지 기준 주소(Physical Page Base Address)를 설정해 주기 위함이다. L2_PAGETABLE_SHIFT는 21로 설정되어 있으므로 쉬프트 연산을 하면 2 메가바이트가 된다.
L2 페이지 테이블 설정이 끝나면, 한 단계 상위 테이블인 L3 페이지 테이블(페이지 디렉토리 오프셋 테이블)을 설정한다. L2 페이지 테이블과 마찬가지로 l3_identmap/l3_xenmap/l3_bootmap 세 종류의 페이지 테이블을 각각 설정한다. l3_identmap 테이블은 네 개의 엔트리를 각각 네 개의 l2_identmap 테이블로 설정하며, 하나의 페이지 크기가 2 메가바이트이므로 모두 4 기가바이트 영역을 매핑한다. 나머지 l3_xenmap과 l3_bootmap은 각각 하나의 엔트리에 하나의 l2 페이지 테이블을 매핑한다(2).
L3 페이지 테이블의 엔트리를 설정 할 때 L2 페이지 테이블 주소에 7을 더한 값을 설정하는 이유는 P/RW/US 필드를 설정하기 위함이다. L3 페이지 테이블 엔트리는 [그림 3]와 같다.
[그림 3] 2 메가바이트페이지디렉토리포인터테이블엔트리[1]
각 필드가 의미하는 바는 L2 페이지 테이블 엔트리와 마찬가지로 [표 1]과 같다.
페이지 테이블 초기화 과정의 마지막 단계로 최상위 페이지 테이블인 L4 페이지 테이블(페이지 맵 레벨 4 테이블)인 idle_pg_table 설정한다(3). l3_identmap/l3_xenmap/l3_bootmap 세 개의 L3 페이지 테이블의 주소와 P/RW/US 필드를 설정하여 각 엔트리에 설정하였다. 현재까지 Xen 초기화 과정에서 현재 생성된 페이지 테이블은 [그림 4]와 같다. [그림 4]에서 최하위 2 메가바이트가 4 킬로바이트 페이지로 사용하도록 되어있다. 이 코드는 [예제 1]에는 나와 있지 않지만 이후 비디오 메모리 영역을 설정하기 위해서 하나의 2 메가바이트 페이지를 4 킬로바이트 페이지로 매핑한다. 즉, 현재 Xen에서 사용하는 페이지는 4 킬로바이트 페이지와 2 메가바이트 페이지를 혼합해서 사용한다. 앞에서 언급 하였듯이 페이지 디렉토리 엔트리의 7번째 비트에 따라 페이지 크기를 다르게 설정 할 수 있다.
[그림 4] Xen 부트페이지테이블
현재는 초기화 과정에서 사용되는 페이지 테이블 설정은 완료하였으나 아직 CR3 레지스터에 페이지 테이블 주소를 설정하지 않았고, 페이징을 활성화 하지 않은 상태이다. 즉, 페이지 테이블은 설정 하였지만 아직까지 가상 주소를 사용하지 않는다.
참고자료
[1] AMD64 Architecture Programmer's Manual Volume 2: System Programming
Xen은 반 가상화(Para Virtualization)을 사용하는 오픈 소스 가상화 프로그램이다.
반 가상화는 하드웨어를 완전히 가상화하지 않고 하이퍼바이저(Hypervisor, VMM(Virtual Machine Monitor)라고도 한다.)에 하드웨어 제어를 요청하는 방식을 말한다. 반 가상화는 하드웨어를 전부 가상화하는 전 가상화(Full Virtualization)에 비하여 성능이 좋다는 장점이 있지만, 커널을 일부 수정해야 한다는 단점이 있다.
여기서는 Xen 하이퍼바이저 초기화 과정에 대해 알아 볼 것이다. 즉, Xen 하이퍼바이저가 부트로더에 의해 메모리에 로드 되어 도메인 0 커널이 실행되기까지의 과정을 서술한다. 버전은 최신 배포 버전인 Xen 4.1.x 버전을 기준으로 한다.
사용자가 컴퓨터에 전원을 인가하였을 때 처음 시스템이 초기화되는 과정을 '부트스트랩(Bootstrap)' 과정이라고 한다. 컴퓨터는 처음 전원이 인가되면 가장 먼저 ROM(Read Only Memory)에 있는 시스템 BIOS(Basic Input/Output System)를 실행한다.BIOS는 컴퓨터 하드웨어에 대한 일련의 테스트 과정을 거쳐서 장치를 인식하고, 해당 장치가 제대로 동작하는지 확인한다. 이 과정을 'POST(Power-On Self-Test)'이라고 한다. POST 과정이 끝나면 하드웨어 장치를 초기화하고, 부팅 할 운영체제를 찾는다. 플로피 디스크와 하드 디스크, CD-ROM, USB 저장장치 등의 저장장치에서 BIOS에 설정된 순으로 검색한다. 저장장치의 첫 번째 섹터에 접근하여 유효한 운영체제가 있으면 메모리 물리 주소 0x7c00으로 로드하고 해당 주소로 점프한다.
'부트로더(Boot Loader)'는 운영체제 커널이 로드 되기 위해 BIOS에서 로드 하는 프로그램이다. 하드디스크로 부팅 할 때 하드 디스크의 첫 번째 섹터, 즉, 'MBR(Master Boot Record)'에 부트로더 코드가 존재한다. 아주 오래 전에는 부트로더의 크기가 작아서 한 섹터 크기인 512바이트로 충분하였지만, 현대에 사용하는 부트로더는 많은 기능을 내장하고 있기 때문에, 512바이트로는 턱없이 부족하다. 그래서, 첫 번째 섹터 512바이트에 들어 있는 코드에서 가장 기본적인 시스템 초기화를 거친 후에 부트로더의 나머지 부분을 로드 한다.
[그림 1] Xen 초기화과정비교
부트로더는 시스템 초기화를 거친 후에 운영체제 커널을 로드 한다. 하지만, Xen을 설치한 시스템은 부트로더가 커널을 로드 하는 것이 아니라 Xen 하이퍼바이저를 로드 한다. Xen 하이퍼바이저는 초기화 과정을 거친 뒤에 시스템에서 사용하는 운영체제 커널을 로드 한다.
부트로더에서 Xen으로 제어권을 넘겨줄 때 최초로 실행되는 Xen의 시작점은 ENTRY(start) 이다.
Xen 시작점(Entry Point)
예제 A. 1 Xen 시작점(Entry Point) (xen\arch\x86\boot\head.S)
부트로더에서 리얼 모드에서 보호 모드로 전환하는 작업이 끝난 뒤에 Xen으로 제어 권이 넘어 온다. Xen의 시작 코드가 .code32 로 되어 있는 이유는 현재 x86 아키텍처가 보호 모드이므로 32비트 단위로 코드를 동작하겠다는 의미다. 참고로 리얼 모드는 16 비트 단위로 동작하며, 보호 모드는 32비트 단위, 롱 모드는 64 비트 단위로 동작한다.
Xen에서 사용하는 심볼(Symbol)들의 주소는 가상 주소 __XEN_VIRT_START 부터 존재한다. __XEN_VIRT_START는 x86_64 아키텍처에서 0xffff82c480000000로 정의되어 있다. 현재 Xen은 보호 모드이지만 페이징(Paging)이 활성화되어 있지 않기 때문에 가상 주소를 사용할 수 없다. sym_phys(sym) 함수는 심볼의 물리 주소를 리턴 하므로, 페이징이 활성화되기 이전까지 심볼에 접근하기 위해 사용한다.
x86-64 동작 모드(Operation Mode)
x86-64 아키텍처는 롱 모드(Long Mode)와 레거시 모드(Legacy Mode) 크게 두 가지 동작 모드를 가지고 있다. 레거시 모드는 x86 아키텍처가 64 비트를 지원하기 이전까지의 리얼 모드(Real Mode), 보호 모드(Protected Mode), 가상 8086 모드(Virtual 8086 Mode)가 있다. 롱 모드는 운영체제와 어플리케이션 모두 64 비트를 사용하는 64 비트 모드와 운영체제는 64 비트 모드, 어플리케이션은 32 비트 모드로 동작하는 호환 모드(Compatibility Mode)가 있다. x86-64 아키텍처에서 이렇게 많은 동작 모드가 존재하는 이유는 아키텍처가 발전하면서 이전 동작 모드의 어플리케이션의 호환성을 위해서 이다. Intel에서는 롱 모드를 그냥 64 비트 모드라고 부른다. 최신 x86-64 아키텍처 기술에 대한 용어는 Intel과 AMD간에 같은 의미를 지닌 다른 용어가 있으므로 혼동하지 않도록 주의해야 한다.
표 1 x86-64 동작 모드
페이징이 활성화 되기 이전에 사용되는 논리 주소(Logical Address)는 세그먼트 식별자(Segment Identifier)와 오프셋(Offset)으로 구성된다. 세그먼트 식별자는 16 비트 '세그먼트 셀렉터(Sement Selector)'이며, 오프셋은 32 비트이다. 세그먼트, 오프셋을 이용하여 논리 주소를 사용하는 것을 '세그멘테이션(Segmentation)'이라고 한다. x86 아키텍처는 세그먼트 셀렉터를 담는 목적으로 사용하는 '세그먼트 레지스터(Segment Register)'를 가지고 있다. x86 아키텍처는 다음과 같은 세그먼트 레지스터를 가지고 있다.
CS 코드 세그먼트 레지스터(Code Segment Register). 프로그램 명령어를 담고 있는 세그먼트를 가리킨다. CS 레지스터가 로드 되면, 프로세서의 '현재 특권 레벨(Current-Privilege Level, CPL)'을 CS 세그먼트 디스크립터에 설정된 '디스크립터 특권 레벨(Descriptor-Privilege Level, DPL)'로 설정한다.
SS 스택 세크먼트 레지스터(Stack Segment Register). 현재 프로그램의 스택을 담고 있는 세그먼트를 가리킨다.
DS 데이터 세그먼트 레지스터(Data Segment Register). 정적인 데이터와 전역 데이터데 담고 있는 세그먼트를 가리킨다.
ES, FS, GS 선택적인 데이터 세그먼트 레지스터. 메모리 복사나 문자열 연산 등에 선택적으로 사용하는 데이터 세그먼트를 가리킨다.
롱 모드에서는 FS와 GS는 유효 주소(Effective Address)를 구하기 위한 용도로 사용되고, 나머지 세그먼트 레지스터는 특권 레벨 필드 등 몇 가지 필드를 제외하고는 사용하지 않는다.
세그먼트 셀렉터는 '세그먼트 디스크립터(Segment Descriptor)'를 가리킨다. 세그먼트 디스크립터를 담고 있는 테이블을 '세그먼트 디스크립터 테이블(Segment Descriptor Table)'이라고 한다. x86 아키텍처는 '전역 디스크립터 테이블(Global Descriptor Table, GDT)', '지역 디스크립터 테이블(Local Descriptor Table)', '인터럽트 디스크립터 테이블(Interrupt Descriptor Table, IDT)' 세 개의 세그먼트 디스크립터 테이블이 있다.
세그먼트 레지스터의 TI 필드 값에 따라 GDTR, LDTR, IDTR 중에서 어느 세그먼트 디스크립터 테이블을 사용 할 것인지를 판단한다. 세그먼트 디스크립터 테이블 레지스터(GDTR/LDTR/IDTR)는 해당 디스크립터 테이블이 메모리 상에 어디에 존재하는지를 나타내는 주소를 담고 있다. 세그먼트 레지스터의 Index 필드는 디스크립터 테이블의 몇 번째 인덱스의 디스크립터를 사용 할 것인지 나타낸다.
세그먼터 디스크립터에는 세그먼트의 시작점인 Base Address와 세그먼트 끝을 나타내는 Limit로 세그먼트를 설정 한다. 논리 주소의 오프셋과 세그먼트 디스크립터의 기준 주소를 더해서 실제 물리주소를 알아낸다. [그림 2]는 이러한 세그멘테이션 과정을 보여준다. 참고로 32 비트 보호 모드에서는 세그멘테이션 결과로 나온 주소가 물리 주소가 아니라 '선형 주소(Linear Address)'이다. 선형 주소에서 페이징을 거쳐야 실제 물리 주소를 알 수 있다. 롱 모드에서는 세그멘테이션을 사용하지 않는다.
[그림 2] 세그멘테이션
예제 A. 2 GDT/Multiboot/BSS 초기화 (xen\arch\x86\boot\head.S)
__start:
cld
cli
/* Initialise GDT and basic data segments. */ --- (1)
lgdt %cs:sym_phys(gdt_boot_descr)
mov $BOOT_DS,%ecx
mov %ecx,%ds
mov %ecx,%es
mov %ecx,%ss
/* Check for Multiboot bootloader */ --- (2)
cmp $0x2BADB002,%eax
jne not_multiboot
/* Save the Multiboot info struct (after relocation) for later use. */ --- (3)
mov $sym_phys(cpu0_stack)+1024,%esp
push %ebx
call reloc
mov %eax,sym_phys(multiboot_ptr)
/* Initialize BSS (no nasty surprises!) */ --- (4)
mov $sym_phys(__bss_start),%edi
mov $sym_phys(_end),%ecx
sub %edi,%ecx
xor %eax,%eax
rep stosb
Xen에서 가장 먼저 하는 일이 세그멘테이션을 위하여 부팅 단계에서 사용하는 GDT를 설정하는 것과 각 세그먼트 레지스터를 초기화 하는 것이다. lgdt 명령어로 부트 과정에서 임시로 사용 할 GDT 디스크립터를 GDTR 레지스터에 설정 하고, DS/ES/SS 데이터 세그먼트를 BOOT_DS로 초기화 한다(1). 세그먼트 디스크립터 하나의 크기는 64 바이트 이며 BOOT_DS는 0x18 이므로 GDT에서 네 번째 세그먼트 디스크립터를 가리킨다. GDT의 첫 번째 세그먼트 디스크립터는 모두 0으로 초기화하여 사용하지 않고, 두 번째 세그먼트 디스크립터는 코드 세그먼트로 사용한다.
세그먼트 레지스터를 초기화 하고 난 다음에 EAX 레지스터에 저장된 매직넘버 0x2BADB002를확인하여부트로더에서제대로제어가넘어온것인지확인한다(2).
gcc 컴파일러 확장을 사용해서 cpu0_stack 변수를 .bss.stack_aligned 섹션에 할당 하였다.
x86 아키텍처에서는스택이아래로자란다. 즉, 푸쉬연산을하면스택포인터의주소가감소한다. 반대로팝연산을하면스택포인터주소가증가한다. 스택포인터레지스터(esp)를 cpu0_stack 변수에서 1024 바이트만큼더한곳으로설정하여 1 킬로바이트크기를가진스택을생성하였다. 스택이만들어졌으므로이제부터함수호출이가능하다. 'x86 함수 호출 규약(x86 function call convention)'에 의하면 함수를 호출 시 인자를 스택으로 전달하도록 되어 있다. 현재 ebx 레지스터는 부트로더에서 넘겨준 멀티부트 정보의 주소로 설정되어 있다. 이것을 스택에 넣어서 reloc 함수의 인자로 전달한다. reloc 함수를호출하여부트로더에서넘어온멀티부트정보를 Xen에서사용하기위해서 multiboot_info_t 타입변수에채워넣는다(3).
BSS은 '초기값이 없는 전역 변수'와 '동적 메모리 할당' 두 가지 용도로 사용되는 메모리 영역이다. 초기값이 없는 전역 변수는 항상 0으로 초기화 되어 있다고 가정하고 사용하므로 BSS 영역을 모두 0으로 초기화 해 주어야 한다(4).
예제 A. 3 CPU 정보 확인 (xen\arch\x86\boot\head.S)
/* Interrogate CPU extended features via CPUID. */
CPUID 명령어는 eax 레지스터 값에 따라 CPU 정보를 알려준다. eax 레지스터에 0x80000000 값을 쓰고 cpuid를 실행하면 확장 기능(Extended Function) cpuid의 최대 입력 값이 나온다. 이것이 0x80000000 보다 크면 확장 기능 cpuid를 지원 하는 것이고, 아니면 CPU는 확장 기능 cpuid를 지원하지 않는다. eax 레지스터에 0x80000001를 써 넣고 cpuid를 호출하여 CPU의 확장 기능 정보를 구하고 현재 CPU가 64 비트 모드(혹은 롱 모드)를 지원하는지 검사한다. cupid 명령어를 사용하여 CPU의 다양한 정보를 구할 수 있지만 현재 cpuid로 얻어 오는 CPU 정보는 <표 2>와 같다.
포팅이란 무엇인가? '포팅(porting)'은 이미 만들어져 있는 소프트웨어를 이기종에서 동작 할 수 있도록 하는 일련의 행위을 말한다. 간단한 포팅의 예로, C 표준을 제대로 준수한 C로 작성된 프로그램을 ARM 아키텍처에서 동작 할 수 있도록 ARM용 컴파일러로 재컴파일만 하면 포팅이 된다. 이때 개발자가 사용하는 작업 머신(호스트(Host)라고 한다)과 타겟(Target) 머신이 서로 다르다. 이렇게, 호스트와 타겟이 이기종일때 호스트에서 타겟에서 실행되는 실행파일을 생성하는 컴파일러를 '크로스 컴파일러(Cross Compiler)'라고 한다. 크로스 컴파일러를 포함하여, 포팅에 필요한 소포트웨어 툴(tool)들의 집합(즉, 체인)을 '툴체인(Toolchain)'이라고 한다. 가끔, 툴체인과 크로스 컴파일러라는 용어를 같은 의미로 혼용해서 쓰기도 한다.
본론으로 들어가서, 리눅스 커널 3.0은 사실 2.6대 버전과 크게 달라진 점이 없다. 소프트웨어 버전 넘버링을 할 때에 버전을 급격히 올리는 경우는 (1) 프로그램 구조나 설계가 바뀌었거나, (2) 많은 기능이 추가 되었을 경우이다. 리눅스 커널 3.0 버전 넘버링은 단순히 리눅스 커널 탄생 20주년을 기념하기 위해 리누스 토발즈(Linus Tovalds)가 재미로 붙인 것이다. 그래서 리눅스 커널 2.6대 최신 버전을 포팅해 본 개발자라면 누구든지 3.0을 포팅 할 수 있다.
먼저 리눅스 커널 3.0을 포팅 할 타겟을 선정해야 한다. 필자가 사용한 타겟은 WIZnet(http://www.wiznet.co.kr/)사의 W5300E01-ARM 이다. 이유는? W5300E01-ARM 보드는 필자가 WIZnet에 재직하던 시절에 만든 보드이기 때문에 세상에서 가장 잘아는 ARM 보드이다. (블로그 사진도 WIZnet 재직 시절 모습이다.) W5300E01-ARM 보드 사양은 다음과 같다.
여기서 다루는 내용은 리눅스 커널 포팅에 대한 내용으로 한정한다. 부트로더와 루트 파일 시스템은 기존 타겟에 포팅되어 있는 것을 그대로 사용한다.
환경 설정 리눅스 커널을 타겟 머신(W5300E01-ARM)에 포팅하려면 툴체인을 설치 해야 한다.gcc 코드를 다운로드 받아서 ARM용 gcc로 컴파일하여 툴체인을 제작할 수도 있다.이런 작업은 많은 시간이 걸리므로 컴파일러 전문회사에서 만들어 둔 것을 쓰도록 하자. CodeSourcery 사의 툴체인을 다운로드 받는다.(링크)다운 받은 파일을 실행하면 설치가 시작 된다.
$ sh arm-2011.03-42-arm-none-eabi.bin
CodeSourcery 툴체인이 디폴트로 설치되는 경로는 '$HOME/CodeSourcery' 이다. 툴체인을 어디서든지 실행 할 수 있도록 PATH를 설정 해 주자. 필자는 '$HOME/.bashrc' 파일에 PATH를 설정하는 것을 선호한다.'$HOME/.bashrc' 파일에 아래 내용을 추가한다.
쉘에서 'arm-none-'까지 타이핑하고 Tab 키를 눌러보라. 아래와 같이 나오면 툴체인 설치가 완료된 것이다.
< 툴체인 설치 완료 >
리눅스 커널 빌드 환경 설정
먼저, 리눅스 커널 3.0 코드를 kerenl.org 에서 다운로드 받는다. (링크) 이 글을 포스팅 하는 시점에서 리눅스 커널은 3.0.4 버전이 stable 최신 버전이고, 3.1 버전이 개발 중 이다. 3.0 버전이 나온지 얼마되지 않았는데, 벌써 3.1 버전이 개발 중인 것을 보면 갑자기 리눅스 커널 버전 넘버링이 빨라진 느낌이다.
다운 받은 리눅스 커널 코드 압축을 해제한다. 작업 경로는 본인이 선호하는 경로 아무대나 풀어라. 아래 예는 단지 하나의 예일 뿐이다.
$ cp ~/Download/linux-3.0.tar.gz ~/work/ $ cd ~/work $ tar zxvf linux-3.0.tar.gz
리눅스 커널 Makefile을 열어보면, ARCH와 CROSS_COMPILE 이라는 환경 변수가 있다. 이것이 리눅스 커널에서 타겟을 설정하는 환경 변수이다. 해당 환경 변수를 현재 타겟에 맞추어 주자. 이것을 리눅스 커널 Makefile에 직접 써넣는 행위는 하지 말아라. 아름답지 못한 행위다.
리눅스 커널은 컴파일 옵션으로 여러 타겟에 적합하도록 설정 할 수 있다. 리눅스 커널을 빌드 해 본 독자라면 'make menuconfig'로 커널이 동작하는 타겟에 맞도록 컴파일 설정을 해 본 적이 있을 것이다. 현재 타겟은 ARM 아키텍처를 사용하는 S3C2410을 MCU를 사용한다. 우선 빌드가 잘 되는지 테스트 하기 위해서 리눅스 커널에 포함되어 있는 S3C2410 기본 설정을 적용해서 빌드 해 보자.
$ make s3c2410_defconfig $ make zImage
'fs/binfmt_aout.c:256:30: error: 'SEGMENT_SIZE' undeclared (first use in this function)' 라는에러 메세지가 뜨면 'linux'가 define 되지 않아서 생기는 문제이니 리눅스 커널 최상위 Makefile에 다음 내용을 추가한 뒤 빌드를 재시도 해보자. (-Dlinux 추가)
리눅스 커널은 다양한 아키텍처에 이식성(portabillity)이 좋도록 설계되어 있다. 리눅스 커널 코드의 'arch' 디렉토리는 각 아키텍처에 종속된 루틴이 구현 되어 있다. 현재 타겟에서 사용하는 아키턱체는 ARM 아키텍처 이므로, 'arch/arm' 디렉토리가 타겟 아키텍처 관련 코드이다. S3C2410은 ARM 아키텍처를 기반으로 SoC 형태로 만들어진 MCU이다. 리눅스 커널 코드의 'arch/arm/mach-s3c2410'에 S3C2410 종속적인 코드가 있다. S3C2410 MCU를 사용하는 타겟 보드가 한 두개는 아닐 것이다. MCU가 같더라도 메모리 크기나 사용하는 디바이스들이 모두 다르다. 타겟 보드에 종속적인 루틴은 리눅스 커널 코드의 'arch/arm/mach-s3c2410/mach-xxx' 이다. 예를 들어, 타겟 보드가 SMDK이면 SMDK 보드에 종속적인 루틴은 'arch/arm/mach-s3c2410/mach-smdk2410.c' 파일에 구현되어 있다.
W5300E01-ARM 보드도 타겟 보드에 종속적인 타겟 보드 파일을 작성해야 한다. 리눅스 커널 코드에서 해당 디렉토리로 이동하여 파일을 생성하자.
$ cd arch/arm/mach-s3c2410 $ touch mach-w5300e01.c
이제부터 'mach-w5300e01.c' 파일을 하나씩 작성해 나갈 것이다. 소스 파일 최상위에는 저작자와 라이센스(GPLv2), 소스 파일에 대한 간략한 내용을 주석으로 남겨야 한다. 기존 GPL 코드를 복사해서 고쳐서 작성한 것이라면 GPL 라이센스에 따라 원저작자 표시를 해야한다.
/* linux/arch/arm/mach-s3c2410/mach-w5300e01.c * * Copyright (C) 2011 WIZnet * All rights reserved. * * @Author: Taehun Kim * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA * * @History: * derived from linux/arch/arm/mach-s3c2410/mach-smdk2410.c, written by * Jonas Dietsch. * ***********************************************************************/
필요한 헤더 파일들을 include 하자. 필자는 리눅스 커널 버전이 올라가면서 헤더 파일 경로가 변경되어서 약간 짜증났었다. 리눅스 커널은 프로젝트가 활발하게(active) 진행되기 때문에 자료구조(data structure)나 헤더 파일 경로등의 변경이 잦다는 점이 장점이자 단점이다.
S3C2410 CS(Chip Selector) 2번(nGCS2)과 3번(nGCS3)이 각각 W5300, 문자 LCD(Charater LCD) 디바이스에 접근 할 수 있도록 연결되어 있다. 물리 주소(Physical Address) 0x10000000과 0x18000000에 I/O를 하게 되면 이는 각각 W5300과 문자 LCD 레지스터에 I/O 한다. 회로도의 접두어 'n'은 'Negative'의 'n' 이며, 액티브 로우(Active-Low)로 동작한다는 의미이다. 즉, 평상시에 핀(pin)의 신호(signal)가 HIGH(5V or 3.3V)로 되어 있다가, LOW(0V)로 떨어졌을때 디바이스가 동작한다. 디바이스는 CS 시그널을 받지 않으면 다른 핀으로 들어오는 시그널들은 모두 무시된다.
위의 S3C2410 메모리 맵과 W5300E01-ARM의 회로도(파란색으로 줄친 부분)을 참고하라.
그런데 리눅스 커널에서는 물리 주소를 사용 할 수 없다. 가상 주소를 사용 한다. 페이징(Paging)이라는 기법을 써서 가상 주소를 물리 주소로 변환한다. 디바이스 I/O 하기 위해서 물리 주소를 가상 주소로 매핑해주어야 한다. 타겟에서 사용하는 디바이스의 I/O 물리 주소를 가상 정보로 매핑하는 정보를 작성한다.
(가상주소), (물리 메모리 페이지 프레임 번호), (크기), (타입) 순이다. '__phys_to_pfn()' 함수는 물리 주소를 페이지 프레임 번호로 변환해준다.
UART 설정
임베디드 장비는 UART 포트 하나를 표준 입/출력 터미널로 사용한다. 리눅스 커널 포팅 작업을 하면서 문제가 생기면 에러 메세지를 확인해야 하는데, UART 설정이 잘못되어 있으면 아무런 메세지를 볼 수 없으므로 정말 난감하다. Trace32와 같은 고가의 디버깅 장비가 항상 사용 할 수 있는 여건도 아닐테니 말이다. (있으면 편하긴 하다.)
S3C2410은 세 개의 UART 포트를 제공한다. 위 의 설정 값은 S3C2410 내에 UART 관련 레지스터에 설정 할 값이다. 결론적으로 Baudrate = 115200, Data = 8 bit, Parity = None, Stop = 1 bit, Hardware Flow Control = No, Software Flow Control = No(115200 8N1/No/No)로 설정하겠다는 것이다. 자세한 내용은 'S3C2410A User's Manual 11. UART'를 참고하라. 포팅 경험이 많지 않은 독자라면 S3C2410A 데이타 시트를 펼쳐 놓고 레지스터에 설정하는 값이 무엇을 의미하는지 하나씩 확인 해 보는 것을 추천한다.
NAND 플래시 메모리 설정 임베디드 보드에 사용하는 NAND 플래시 메모리(Flash Memory)는 용도에 따라 파티션(Partition)을 나누어서 사용한다. 이러한 파티션 정보를 MTD(Memory Technology Device) 파티션이라고 한다. 현재 타겟의 MTD 파티션은 다음과 같이 구성되어 있다.
< W5300E01-ARM의 NAND 플래시 메모리 MTD 파티션 >
'Bootloader' 파티션은 U-boot 부트로더 이미지가 저장되어 있다. S3C2410은 NAND 플래시 메모리 부팅을 지원하기 위해서 내부에 '스태핑스톤(Steppingstone)' 이라고 불리는 내장 SRAM 메모리를 가지고 있다. 전원이 인가되면 S3C2410은 NAND 플래시 메모리 첫 번째 4 KByte를 스태핑스톤에 로드하여 부팅한다. 'Boot Params' 파티션은 리눅스 커널에 전달되는 '부트 파라메터(Boot Parameter)'가 저장되는 영역이다. 'Linux Kernel' 파티션은 리눅스 커널 이미지가 저장되어 있다. 부트로더에서 이 파티션에 있는 리눅스 커널 이미지를 램에 로드하여 커널로 점프한다. 'Ramdisk' 파티션은 루트 파일 시스템(Root file system)으로 사용하는 램 디스크 이미지가 저장되어 있다. 'JFFS2' 파티은 JFFS2 NAND 플래시 파일 시스템으로 포맷되어진 영역으로 별도의 데이타가 들어 있지는 않지만 램 디스크에서 마운트하여, 보존해야 되는 데이타를 저장하는 용도로 사용한다.
's3c2410_nand_set' 구조체는 타겟에서 사용하는 NAND 플래시 메모리 집합에 대한 설정 정보이다. 현재 타겟은 NAND 플래시 메모리가 하나 밖에 없으므로, NAND 메모리 갯수와 앞서 추가한 MTD 파티션 정보를 설정 해준다.
's3c2410_platform_nand' 구조체는 NAND 플래시 메모리 설정을 위한 최상위 자료구조 이다. NAND 플래시에 접근 할 때 사용하는 핀의 타이밍 설정에 대한 설정 값과 앞서 설정한 NAND 플래시 메모리 집합에 대한 정보를 설정한다. 'tacls'는 CLE(Command Latch Enable)/ALE(Address Latch Enable) 핀에 시그널이 인가 되는 시점에서 WE(Write Enable)/OE(Output Enable) 핀에 시그널이 인가되는데 까지 걸리는 시간을 걸리는 타이밍을 설정하는 값이다. (나노초 단위) 'twrph0'는 WE/OE 핀에 시그널이 활성화 되어있는 시간을 설정하는 값이다. 'twrph1'는 WE/OE 핀이 시그널이 비활성화 되는 시점부터 CLE/ALE 핀의 시그널이 비활성화 되는데까지 걸리는 타이밍을 설정하는 값이다.
아래 NAND 플래시 메모리에 명령어(command)를 전달하는 시점의 타이밍도를 참고하면 위에 설명한 내용을 이해하기 수월할 것이다. 자세한 내용은 현재 타겟에서 사용하는 NAND 플래시 메모리, K9F1208U0, 의 데이타시트를 참고하라. 구글링 해보면 금방 찾을 수 있을 것이다.
< K9F1208U0의 명령어 래치 싸이클(출처> 'K9F1208U0 데이타시트') >
그 외 타겟에서 사용하는 디바이스들의 정보를 추가하고, 지금까지 작성한 디바이스 설정 정보를 등록하는 함수를 추가한다.
초기화 함수(w5300e01_init())에서 타겟에서 사용하는 GPIO 핀을 설정 하였다. GPIO(General Purpose Input/Output)는 명칭 그대로 범용으로 사용 할 수 있게 만든 핀이므로, 입/출력 및 인터펍트등 여러가지 용도로 사용 할 수 있다. 위의 초기화 함수에서 가장 중요한 부분은 GPF 0 번 GPIO 핀을 외부 인터럽트 용도로 설정 한 부분이다. 이 핀은 타겟의 네트워크 칩인 W5300의 인터럽트 핀과 연결되어 있다. (회로도 참고.) GPIO 핀을 인터럽트용으로 설정하지 않으면 디바이스의 인터럽트를 수신 할 수 없으므로 주의해야 한다.
머신 타입(Machine Type) 추가
ARM 리눅스 커널은 초기화 과정에서 머신 타입(Machine Type)을 체크하도록 되어있다. ARM 리눅스 커널의 대부이신 러셀 킹(Russell King)님이 도입한 방식인데, 유일하게 ARM 아키텍처에만 존재한다. 이러한 방식을 개인적으로는 좋아하지 않지만 관례이므로 맞춰주도록 하자.
지금은 정식 등록 절차를 거쳐야 할 이유가 없으니 리눅스 커널 코드에 머신 타입을 임의로 추가 하도록 하자.
머신 타입은 부트로더도 연관이 있다. 부트로더에서 현재 타겟의 머신 타입을 설정해서 리눅스 커널에 전달 해 준다. 부트로더는 기존에 포팅된 U-boot를 그대로 사용하기로 하였으니, 이전에 사용했던 W5300E01-ARM의 머신 타입을 그대로 사용 해야 한다. 그렇지 않으면 머신 타입이 유효하지 않으므로 부팅이 되지 않는다.
이렇게 타겟 보드, W5300E01-ARM, 에 종속적인 루틴을 작성하는 작업이 모두 끝났다. 이제는 리눅스 커널 컴파일 옵션을 수정하는 작업을 하도록 하자.
리눅스 커널 컴파일 설정
앞서 작성한 타겟 머신에 종속적인 루틴(mach-w5300e01.c)이 리눅스 커널 빌드시 커널 이미지에 포함이 되도록 해주어야 한다. 먼저 S3C2410 종속적인 코드가 들어 있는 'arch/arm/mach-s3c2410' 디렉토리에서 Kconfig 파일을 수정해주자. Kconfig 파일은 'make menuconfig' 등으로 리눅스 커널 컴파일 옵션을 설정 할 때 보여지는 메뉴를 구성하는 파일들이다.
arch/arm/mach-s3c2410/Kconfig (하이라이트 된 부분을 추가한다.)
config MACH_QT2410 bool "QT2410" select CPU_S3C2410 select S3C_DEV_USB_HOST select S3C_DEV_NAND help Say Y here if you are using the Armzone QT2410
config MACH_W5300E01 bool "W5300E01-ARM Board" select CPU_S3C2410 help Say Y here if you are using the W5300E01-ARM board
endmenu
Makefile에 'mach-w5300e01.c' 소스 파일이 빌드되어 리눅스 커널 이미지에 포함 되도록 추가 해주자.
arch/arm/mach-s3c2410/Makefile (하이라이트 된 부분을 추가 한다.)
이제 리눅스 커널 수정은 모두 끝났다. 커널 이미지를 빌드 하기 전에 컴파일 옵션을 설정하자.
리눅스 커널 컴파일 설정
타겟에 적합하도록 리눅스 커널 컴파일 설정을 한다. 현재 사용 중인 쉘에서 환경변수 ARCH와 CROSS_COMPILE이 제대로 설정되어 있는지 다시 확인하자. 리눅스 커널은 ARCH 환경변수에 따라 사용되는 Kconfig 파일이 다르므로 컴파일 설정 메뉴가 다르게 나타난다.
$ make menuconfig
< 리눅스 커널 컴파일 설정 >
현재 타겟에 필요한 리눅스 커널 컴파일 설정은 아래와 같다.
General setup ---> Kernel compress mode (Gzip) (W5300E01) Default hostname [*] System V IPC RCU Subsystem ---> RCU Implementation (UP-only small-memory-footprint RCU) ---> (16) Kernel log buffer size (16 => 64KB, 17 => 128KB) [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support [*] Support initial ramdisks compressed using gzip [*] Optimize for size [*] Embedded system Choose SLAB allocator (SLOB (Simple Allocator)) --->
[*] Enable loadable module support ---> [*] Module unloading
System Type ---> [*] MMU-based Paged Memory Management Supprt ARM system type (Samsung S3C2410, S3C2412, S3C2413, S3C2416, S3C2440, S3C2442) [*] S3C Reboot on decompression error [*] Force UART FIFO on during boot process (0) S3C UART to use for low-level message (0) Number of additional GPIO pins (0) Space between gpio banks [*] ADC common driver support [*] PWM device support S3C2410 Machines ---> [*] W5300E01-ARM Board Kernel Features ---> Memory split (3G/1G user/kernel split) ---> Preemption Model (No Forced Preemption (Server)) ---> (4096) Low address space to protect from user allocation
Boot options ---> (0x0) Compressed ROM boot loader base address (0x0) Compressed ROM boot loader BSS address (mem=64M console=ttySAC0,115200 root=/dev/ram0 rw initrd=0x31000000,12M) Kernel command line type (Use bootloader kernel arguments if available)
Floating point emulation ---> [*] NWFPE math emulation [*] Support extended precision
Userspace binary formats ---> [*] Kernel support for ELF binaries [*] Write ELF core dumps with partial segments
Power management options ---> [*] Suspend to RAM and stanby [*] Run-time PM core functionality
Device Drivers ---> Generic Driver Options ---> <*> Userspace firmware loading support <*> Memory Technology Device (MTD) support ---> <*> NAND Device Support ---> <*> NAND Flash support for Samsung S3C SoCs [*] Block devices ---> <*> Loopback device support <*> RAM block device support (1) Default number of RAM disks (12288) Default RAM disk size (kbytes) [*] Watchdog Timer Support ---> <*> S3C2410 Watchdog [*] USB support ---> <*> Support for Host-side USB [*] Real Time Clock ---> [*] Set system time from RTC on startup and resume (rtc0) RTC used to set the system time [*] /dev/rtcN (charater devices) <*> Samsung S3C series SoC RTC
File systems ---> <*> Second extended fs support <*> Ext3 journalling file system support Pseudo filesystems ---> [*] /proc file system support [*] Miscellaneous filesystems ---> <*> Journalling Flash File System v2 (JFFS2) support (0) JFFS2 debugging verbosity (0 = quiet, 2 = noisy) [*] JFFS2 write-buffering support [*] Network File Systems --->
Kernel hacking ---> (0) S3C UART to use for low-level debug
이제 리눅스 커널 3.0 포팅 작업이 모두 끝났다. 빌드해서 타겟에 올려보자. 타겟의 부트로더가 U-boot이므로 U-boot에서 사용하는 형태의 uImage 커널을 빌드해야 한다. 생성된 커널 이미지를 부트로더에서 다운로드 할 수 있도록 tftp 서버 디렉토리에 복사 한다. tftp 설정 방법은 첨부된 'W5300E01-ARM Manual 5장. 개발 환경 구축'을 참고하기 바란다.
$ make uImage $ sudo cp arch/arm/boot/uImage /var/lib/tftpboot/
< 리눅스 커널 컴파일 >
타겟의 부트로더에서 tftp를 이용하여 커널 이미지와 램 디스크 이미지를 다운로드 받는다. 부트로더에서 tftp로 이미지를 다운로드 받으려면 'setenv' 커맨드를 이용해서 타겟의 IP 주소와 tftp 서버가 설치된 호스트의 IP 주소를 설정해야 한다. 예를 들어, 타겟의 IP가 '192.168.1.53'이고 호스트의 IP가 '192.168.1.2'로 설정되어 있으면 U-boot에서 다음과 같이 커맨드를 입력 한다.
아래 램 디스크 이미지를 다운 받아서 tftp 서버 디렉토리에 복사하고, 타겟의 부트로더에서 tftp를 이용하여 램 디스크 이미지와 리눅스 커널 이미지를 다운로드 한다. 주의해야 할 점은 'W5300E01-ARM Manual'에는 램 디스크 주소가 0x30400000로 되어 있는데, 이 영역은 커널 이미지를 압축을 풀면서 덮어 쓰여져 버린다. 그래서, 램 디스크가 로드되는 주소를 0x31000000로 변경하였다. 필자는 커널 포팅을 1시간만에 끝내 놓고 이 문제 때문에 이틀 동안이나 삽질하였다. 원인을 찾느라고 리눅스 커널 초기화 과정에서 램 디스크 관련 루틴을 모두 분석하였다.
Domain U의 Grant Table의 1번 인덱스에 Domain 0과 machine memory 6번 page와 매핑하도록 기록합니다.
I/O request ring buffer에 DISK의 sector, 자신의 domain, request 타입, grant reference(GR) 번호를 기록합니다.
Domain 0의 Backend Driver는 I/O request ring buffer에서 해당 정보를 읽어서 XEN에서 자신의 pseudo-physical 2번 page가 machine memory 6번 page와 매핑하라는 정보와, 이것이 grant reference 1번이라고 XEN에 알려줍니다.
XEN에서 Domain 0의 2번 page와 machine memory의 6번 page의 매핑을 끊고, XEN의 Active Grant Table에 1번 인덱스 내용을 지웁니다. 그리고, 작업이 완료되었다고 'Response'를 I/O ring buffer에 기록합니다.
Domain U는 I/O ring buffer에서 Response를 읽어서 작업이 완료되었다는 것을 알고, Grant Table에서 1번 인덱스 내용을 삭제합니다.
드디어 리눅스 커널 소스 분석 스터디가 끝났다. 처음 스터디를 시작하면서 목표로 했던 리눅스 부팅 과정이 완료되는 부분까지 분석을 완료하였다. 작년 2월에 시작해서 올해 2월에 끝났으니 1년이라는 오랜 기간 동안 처음에 시작했던 멤버 대부분이 같이 끝을 볼수 있어서 기뻤다. 기나긴 여정동안 포기하지 않고 마무리를 제대로 할 수 있어서 뿌듯했다. 혼자서 했으면 시작조차 할 엄두가 나지 않았을텐데, 같이 스터디 하시는 분들이 계셨기에 무사히 끝낼수 있었던것 같다. 항상 느끼는 부분이지만 같이 스터디 하시는 분들은 참으로 대단하신 분들이다.
저번주에 회사에서 태안반도 봉사활동 가느라 한주 빠졌었는데 저번주에 스터디 참여 인원이 2분이었다고 한다. 저번주에 끝내는 목적으로 스터디를 하여서인지, 2분이서 엄청난 분량을 하셨다. 저번주 스터디에 참여 하신 형주님께서 이번에 스터디 시작에 앞서 저번주에 했던 부분을 잠깐 리뷰 해 주셨다.
저번주 했던 부분은 start_kernel()의 radix_tree_init() ~ rest_init() 일부까지 진행을 하였다. 대부분이 관련된 자료구조 초기화 부분이고, 현재 타겟으로 하고 있는 아키텍처인 x86_64에서 안쓰이는 함수가 몇몇 있어서 진도를 빨리 나갈수 있었다고 하였다.
이번주에는 start_kernel() -> rest_init()에서 커널 스레드로 생성하는 init() 함수와 cpu_idle() 함수를 분석하였다. rest_init() 함수는 init 커널 스레드를 생성하고 스케쥴링 한다. 이후에 다시 rest_init()로 스케쥴링 되었을때는 cpu_idle() 함수를 실행하여, idle 프로세스로 바뀌게 된다.
커널 스레드인 init() 함수는 크게 보면 다음과 같은 작업을 한다.
멀티 프로세서의 초기화(이 시점에서 부팅 CPU를 제외한 다른 CPU가 활성화 된다.)
기타 부가 기능들의 초기화 함수 실행(커널에 포함된 디바이스 드라이버의 초기화 함수등)
표준 입출력 파일 open(표준 입력(0)/표준 출력(1)/표준 에러(2))
init 프로그램의 실행
init() 함수는 마지막에 init 프로그램을 실행하는데, 이때 실행할 init 프로그램을 전역변수 execute_command를 설정하면 사용자가 지정 가능하다. 정확하게 분석 하지는 않았지만, 부트로더에서 부팅시에 커널 파라메타로 넘겨주는듯 하다. 그리고 run_init_process()로 init 프로그램을 실행 하는데, run_init_process()는 내부적으로 kernel_execve()를 실행하여, 프로세스를 바꾸어주는 역활을 한다. 즉, 커널 스레드 init()를 "init" 프로그램으로 변경한다.
cpu_idle() 함수는 현재 시스템에서 별다른 프로세스가 실행되지 않을때 실행되는 우선순위가 낮은 프로세스 이다. while(1)로 묶여있어서, 무한루프를 돌면서 idle 프로세스를 실행한다. idle 프로세스 실행이 끝나면, 스케쥴러를 호출하여, 스케쥴링 되어야 할 다른 프로세스가 있으면 그 프로세스를 실행한다. idle 프로세스 수행중에 인터럽트가 발생하면, idle 프로세스가 종료된다.(do_irq() 함수에서 exit_idle() 함수를 호출하여 idle 프로세스의 수행을 끝낸다.)
리눅스는 프로세스 생성을 복사/변경 매커니즘으로 한다. fork() 시스템 콜로 부모와 같은 복사본을 만들고, exec() 시스템콜로 자신에 맞게 변경을 해서 프로세스를 생성하게 된다. 이때, 모든 프로세스의 조상이 되는 PID(프로세스 ID) 1번인 프로세스가 init 프로세스 이다. init 프로세스 생성은 앞서 보았던 부팅 과정 마지막에 init() 함수에서 init 프로세스로 변경되는 부분에서 생성이 되었다. init 프로세스는 모든 프로세스의 조상의 역활뿐만 아니라, /etc/inittab을 참고하여 리눅스 부팅때 수행되는 프로그램들을 기술한 스크립트를 실행하는 역활도 한다.
이제 리눅스 커널 스터디 관련 포스팅도 끝이다. 앞으로 남은 일정은 2월 한달동안 각자 맡은 부분 리뷰 자료를 만들기로 하였고, 3월동안 지금까지 했던 스터디의 리뷰를 통해, 리눅스 부팅과정 및 리눅스 커널의 구조에 대한 이해도를 높이기로 하였다.
그리고, 리뷰 자료를 체계적으로 정리하여, 지금까지 스터디 했던 내용을 좀더 많은 사람들과 공유 할 수 있도록 리눅스 부팅 프로세스 관련 기사를 월간 마이크로소프트웨어에 기재 하기로 하였다. 확실한 것은 아니고, 일단 자료의 퀄리티를 높여서 마소 측에 컨택을 해보기로 하였다.
아직 살아온 날보다 살아갈 날이 더 많이 남았고, 그렇게 오랜 시간을 살아온건 아니지만, 지금까지 살아오면서 느낀 점이 하나 있다. 무슨 일을 하던간에 그것을 제대로 하려고 하면, '시작'과 '끝'이 가장 중요 하다는 것이다. 일단 '시작'을 해야 되든 안되는 결과가 나올것이고, 성공적인 결과를 도출하려면 '끝' 마무리를 신경을 써야한다.
각각에 필요한 조건들 중에 개인적으로 중요시 여기는 것들이 몇가지 있다. '시작' 하는데 필요한것은 '열정'과 '배짱', '끝'을 제대로 마무리하는데 필요한것은 '프로의식'과 '근성'이다. 주위를 보면 무조건 어렵다고 시도(시작)조차 하지 않는 사람들은 세월이 흐르더라도 발전하지 못하고 항상 제자리 걸음을 하면서 현실에 안주하며 살아간다. 그리고 사소한 것이라고 그것을 건성으로 하는 사람들은 결과는 나오겠지만, 그것이 제대로 된 것이 하나도 없을것이다. 이런 조건들을 모두 갖춘 사람들은 무슨 일을 하더라도, 성공을 했거나, 할 것이다.
스터디 후기에 갑자기 왠 인생타령이냐 하겠지만, 요즘들어 커널 스터디가 끝나는 시점이 되다보니 조금씩 너프해지는게 아쉬워서 그런다. 시작할때의 열정이 지금은 많이 사라져서, 많이 안타깝다. 물론 이제 한걸음만 더 나아가면 스터디가 끝나기 때문에, 흐지부지 된다던지 하는 일은 없을 것이다. 마무리를 잘해서 제대로 했던 스터디로 기억에 남도록 얼마남지않은 기간동안 다시 열정을 불태워야 겠다.
이날은 개인 사정상 참여인원이 적고, 늦게 시작하고 일찍 마쳐서 진도를 얼마 나가지는 못했다. 다행히도 장소를 제공해주시는 상민님께서 나오셨고, 스터디 진행하는 효율적인 방법을 찾아서 만족스러운 스터디를 하였다. 이날 진행했던 부분은 start_kernel() -> vfs_caches_init() -> files_init()와 mnt_init()를 진행하였다.
file_init()는 VFS(Virtual File System)에서 열린 파일을 관리하는 file 객체를 초기화 해주는 함수이다. 처음에 현재 여유공간에서 할당 할 수 있는 inode 또는 dcache의 수의 10%를 현재 시스템에서 열수 있는 파일 갯수로 초기화 한다.(max_files) 그리고 files_defer_init() 함수를 호출한다. files_defer_init() 함수는 현재 시스템의 CPU만큼 루프를 돌면서 fdtable_defer_list_init()를 호출한다. fdtable_defer_list_init() 함수는 현재 CPU의 fdtable_defer_list 변수 값을 가져와서 워크 큐를 free_fdtable_work()로 초기화 한다. 이렇게 하면 free_fdtable_work()가 워크큐를 실행하는 시점에 수행하게 된다. struct fdtable이 있는데 struct fdtable_defer를 따로 둔 이유는 fdtable은 프로세스 당 한개씩 할당되는 자료구조이다. 그런데, fdtable_defer는 CPU당 한개씩 할당된다. fdtable_defer에 워크큐를 둠으로써, fdtable에 워크큐를 넣는것을 피할 수 있다. 마지막으로 file_init()는 cpu 카운터의 nr_files를 0으로 초기화 하고 수행을 끝낸다.
mnt_init()는 파일 시스템을 초기화 하는 함수 이다. 자료구조 관련 동작은 vfsmount 슬랩을 생성하고, mount_hashtable을 위해서 1 page를 할당한다. 그리고 sysfs_init()와 init_rootfs(), init_mount_tree()를 호출하여 다음과 같은 동작을 한다.
sysfs 초기화(/sys 에 있는 파일 시스템)
루트 파일 시스템 초기화
파일 시스템의 작업 경로 설정
파일 시스템의 root 경로 설정
이번주는 회사에서 태안반도 봉사활동 참여하느라 스터디에 참여하지 못한다. 1월 중으로 분석을 끝내기로 하여서, 진도를 상당히 많이 나갈텐데 참석하지 못하니 아쉽다.