강좌

HOME > 강좌 >
강좌| 리눅스 및 오픈소스에 관련된 강좌를 보실 수 있습니다.
 
리눅스 커널과 디바이스 드라이버 프로그래밍
조회 : 23,354  


리눅스 커널과 디바이스 드라이버 프로그래밍

 

강사소개 :  백창우

동국대학교에서 컴퓨터공학을 전공했고 RTOS 개발, 리눅스 커널, 디바이스 드라이버, 임베디드 시스템등과 관련된 이론 및 실무 경험을 보유하고 있다. 유닉스, 리눅스 프로그래밍 필수 유틸리티, TCP/IP 소켓프로그래밍 등의 단행본을 저술했다.

 

 

첫번째 강좌 : 리눅스와 리눅스 커널, 어떻게 다르지?

 

 

필자가 리눅스와 처음 인연을 맺게 된 때는 1997년이다. 당시 리눅스를 소개하는 대부분의 책들은 항상 리눅스는 핀란드 헬싱키 대학의 리눅스 토발즈가 만든 운영체제로서…” 와 같은 말로 시작하곤 했다. 요즘 나오는 책들에게도 가끔씩 이런한 문구를 볼 수 있는데 예전에 비해서 현저하게 줄어든 것을 확인할 수 있다. 왜냐하면 누구나 그 사실을 알고 있기 때문이다. 더 나아가 IT분야에서 성장하기 위해서라면 리눅스를 몰라서는 안되는 시대가 됐다. 리눅스에 대한 이해를 위해 리눅스 커널부터 살펴보자.

 

국내에 리눅스가 처음 소개됐던 10년 전에는 리눅스를 아는 사람보다 모르는 사람이 더 많았다고 리눅스를 OS로 사용한다는 사실만으로도 별종 취급 받기가 일쑤였다. 하지만 세상이 바뀌어서 리눅스는 이제 우리 IT 산업 전 분야에서 널리 활용되고 있다. 특히 서버, 임베디드분야에서 맹활약을 펼치고 있는데 2005년도 IDC 발표자료와 KIPA 발표 자료에 따르면 서버 시장에서 19.1%를 전체 임베디드 시장의 31%를 차지하고 있는 명실상부한 메이저 운영체제로 자리잡았다. 이러한 사실만 놓고 볼 때 IT분야에서 성장하기 위해서라면 이제는 더 이상 리눅스를 몰라서는 안된다는 사실을 확인할 수 있다. 필자는 이 코너를 통해 리눅스 커널(kernel)에 대해 소개하고 실질적으로 리눅스 커널은 어떠한 과정을 거쳐 부팅되는가를 소스 레벨에서 설명하며, 리눅스에서 디바이스 드라이버를 어떻게 만드는가에 대해서 설명하고자 한다.

 

* 리눅스란 무엇인가?

 

흔히들 레드햇(RedHat) 리눅스, 데비앙(Debian) 리눅스 , 젠투(Gentoo) 리눅스라고 말하지만 엄밀한 의미에서 리눅스는 리눅스 커널만을 의미한다. 우리가 소위 말하는 레드햇이니 데비앙, 젠투 리눅스는 배포판을 의미하지, 리눅스 자체를 의미하지 않는다. 리눅스 커널은 리누스 토발즈를 위시한 세계 각지의 개발자에 의해 오늘도 활발하게 개발되고 있다. 이렇게 개발되는 이 커널을 리눅스라 말한다.

이러한 커널에는 말 그대로 커널만 있기 때문에 이것만 가지고는 아무것도 할 수 없다. 이 커널을 가지고 각 개발 업체들이 컴파일러(gcc, g++), 편집기(vi, emacs ), 웹브라우저(모질라 등)과 같은 응용 어플리케이션을 추가해서 만든 것이 리눅스 배포판이다. 배포판은 배포판을 만드는 업체에서 붙이는 이름에 따라 레드햇, 데비앙, 젠투 등으로 나뉘어진다. 배포판이 다르다고 해서 리눅스 커널이 달라지는 것은 아니다. 배포판이 다름으로 인해 포함되는 응용 어플리케이션의 종류는 다르지만 리눅스 커널은 버전이 다른다는 것과 벤더에 따라 약간의 수정이 가해졌다는 것 빼고는 동일하게 사용되고 있다.

이 리눅스 커널은 현재 2.6.16 버전까지 개발됐는데 해가 바뀔 때마다 개발 속도를 더해가고 있다. 이는 리눅스 커널개발에 많은 사람들이 참여하는 것이 가장 큰 이유겠지만 리누스 토발즈가 이전에 근무하던 트랜스메타를 떠나 오픈소스 개발연구소(OSDL)에서 리눅스 kernel 개발에 전념하는 것도 이유가 될 수 있을 것이다.

 

* 리눅스 커널의 이해

 

리눅스 커널 버전은 linux-2.4.18 혹은 linux-2.6.16과 같은 형식을 띄고 있다. 이는 전통적인 유닉스의 버전 표기법이 약간 변형된 형태이다. 제일 앞에 있는 숫자는 메이저 버전이라고 해서 커널의 구조나 기능에 급격한 변화가 있을 때마다 바뀌는 버전이다. 두번째 오는 숫자는 마이너 버전이라고 해 메이저급처럼 큰 변화는 아니지만 내부 구조면에서 많은 변화가 있을 때 바뀌는 버전이다.

이 마이너 버전이 2.6.x 와 같이 짝수인 경우에는 안정된 커널 소스임을 의미하며 2.5.x 와 같이 홀수인 경우에는 개발 버전을 의미한다. 마지막으로 오는 숫자는 패치 버전이라고 해 기능, 구조적 변화는 없지만 버그등으로 인해 소스가 약간 수정됐을 때마다 바뀌는 버전이다. 그리고 간혹 2.6.16.9 와 같이 패치버전 다음에 숫자 또는 문자가 올 수 있는데 이는 개발자 개인이 붙이는 비공식 버전을 의미한다.

현재 리눅스 커널 버넌은 2.6.16 인데 이 버전정보만으로 우리는 리눅스 커널이 크게 두번째 바뀌었고 두번 바뀐 것중에서 3번째 안정 버전이고(홀수는 개발 버전) 패치는 16번 가해졌다는 것을 알 수 있다.

리눅스 커널의 구조를 그림으로 나타내면 아래와 같다.

 

 

이 그림은 리눅스 커널의 구조를 잘 보여 준다. 사실 리눅스커널의 구조가 그림과 같이 명확하게 기능별로 나뉘어져 있지는 않다. 다른 OS들 특히 RTOS 에 비하면 리눅스 커널의 구조는 다소 경계가 모호한 편이다. 하지만 기능적으로는 이렇게 구분돼 있다고 보면 된다.

 

System Call Interface

 

시스템 콜 인터페이스는 user mode 프로세스인 응용 애플리케이션이 커널의 기능을 사용 가능하게 해준다. 유닉스 시스템의 대표적인 특징 중에 하나는 user mode kernel mode가 나뉘어져 있다는 것이다. 리눅스 역시 처음 시작은 유닉스를 모델로 만들어졌기 때문에 user mode kernel mode 로 나뉘어져 있다.

Kernel mode는 특권 mode로서 모든 주소 공간에 접근할 수 있고 모든 명령(instruction)을 수행할 수 있다. 그러나 user mode 에서는 user mod에 할당된 주소 공간만 접근할 수 있고 kernel mode의 주소 공간에 대해서는 접근 불가능하다. 또한 시스템에 치명적인 영향을 끼칠 수 있는 특권 명령(lgdt, lidt, cli, sti )들은 사용할 수가 없다.

이렇게 user mode kernel mode를 나누어 놓은 이유는 응용 애플리케이션의 비정상적인 혹은 악의적인 수행에 대해서 시스템을 보호하기 위한 것이다.

응용 애플리케이션은 user mode에서 동작하기 때문에 커널 영역에 있는 커널 함수 또는 커널 자료 구조에 대해 접근할 수 없다. 응용 애플리케이션이 커널 영역에 있는 커널 함수 또는 자료 구조를 사용하기 위해서는 반드시 시스템 콜을 사용해야 한다.

시스템 콜은 소프트웨어 인터럽트를 사용해 구현된다. 응용 애플리케이션은 소프트웨어 인터럽트 명령어(ex: int 0x80, swi)를 명시적으로 호출해서 시스템 콜을 호출한다.

 

Memory Management

 

메모리 자원은 시스템에 있어서 매우 귀중한 자원이다. 메모리를 얼마나 효율적으로 관리하느냐에 따라 시스템 성능이 결정된다. 메모리를 비효율적으로 관리한다면 메모리를 할당/해제하는데 많은 시간이 걸리고 단편화로 인해 남아 있는 공간을 제대로 활용하지 못하는 문제가 발생한다.

메모리 매니지먼트는 시스템에 있는 메모리 자원을 관리하는 부분으로서 리눅스는 메모리 관리 알고리듬으로 buddy system slab할당자를 사용한다.

 

Task Management

 

태스크 매니지먼트는 태스크를 관리하는 부분이다. 태스트의 생성,소멸,중단 등을 담당한다. 태스크를 스케줄링하고 우선순위를 집행하고 등의 일이 모두 태스크 매니지먼트에서 이루어지는 일이다.

 

IPC(Interprocess Communication)

 

IPC는 프로세스간  통신을 담당하는 부분이다. 프로세스 모델에서는 페이징(paging)기법을 사용해 가상 주소를 사용하는데 각 프로세스에는 각기 다른 4GB(아키텍처마다 다름. i386, arm ) 크기의 가상 주소 공간을 가지게 된다. 이렇게 되면 프로세스 A 0x100번지와 프로세스 B 0x100번지는 가상 주소는 같지만 물리 주소는 완전히 다른 곳을 가르키게 된다.

페이징기법을 사용해 프로세스의 주소 공간을 분리함으로 생기는 장점으로는 다른 프로세스의 비정상적 혹은 악의적 수행으로부터 보호받을 수 있고 요구 페이징/swapping 기법을 사용해 적은 메모리를 가지고도 4GB 메모리가 있는 것처럼 사용할 수 있으며 애플리케이션을 컴파일할 때 수행 위치에 대해서 고민하지 않아도 된다는 등 많은 장점이 있다.

반면 단점으로는 가상 주소를 물리 주소로 변화하는데 오버헤드가 발생하고 프로세스간 통신에 있어서 쉽지 않다는 문제가 발생하게 된다.

IPC는 프로세스간 통신 기능을 제공해 주는 부분으로서 리눅스에서 IPC signal, message queue, shared memory, semaphore 등을 제공하고 있다.

 

VFS(Virtual File System)

 

가상 파일 시스템은 각기 다른 파일 시스템과 디바이스 드라이브 등에 대해서 동일한 인터페이스를 제공하는 부분이다. 예를 들면 이렇다. ext2 파일 시스템에 있는 텍스트 파일에 데이터를 쓰던 vfat 파일 시스템에 있는 텍스트 파일에 파일을 쓰던 혹은 사운드 카드에 소리가 나게 데이터를 보내든 모두 write 시스템 콜을 사용한다.

이렇게 각기 다른 파일 시스템, 디바이스에 대해서 open, read, write, close 등과 같은 동일한 인터페이스 사용할 수 있게 해주는 부분이 바로 vfs이다.

리눅스가 이렇게 성장할 수 있게 된 이유중의 하나가 바로 이 VFS때문이다. VFS가 있음으로 인해 리눅스는 여러 파일 시스템을 사용할 수 있게 되었고 그럼으로 여러 운영체제와 함께 공존해서 사용하는데 장점이 있었다.

 

BSD Socket Interface

 

bind, connect, accept, send, recv 등으로 대변되는 BSD socket interface 를 제공하는 부분이다.

 

File System

 

ext2, ext3, vfat, jfs 등의 파일 시스템을 제공하는 부분이다.

 

Network Protocol Stack

 

ipv4, ipv6, atm, x25 와 같은 프로토콜 스택을 가지고 있는 부분이다.

 

Device Driver

 

하드디스크, 키보드, 마우스, CD롬등 디바이스 드라이버를 포함하고 있는 부분이다. 예전에 리눅스는 디바이스 드라이브 지원이 미미해 사용하기 힘든 운영체제였다. 특히나 X-windows 시스템을 띄우기 위해서는 그래픽 카드 디바이스 드라이브가 있어야 하는데 대부분 사람들의 그래픽 카드를 리눅스에서 지원해 주지 못했었다. 때문에 X-windows 화면을 본 사람들을 축복받은 사람이라고 부르던 시절이 있었다. 현재는 물론 지난날 추억일 뿐이다.

 

출처 : 공개 SW 리포트 1 60 ~ 63 페이지 발췌(2006 6) - 한국소프트웨어 진흥원 공개SW사업팀 발간

 


리눅스 커널과 디바이스 드라이버 프로그래밍

 

 

두번째강좌 : 리눅스 커널, 어떻게 움직이나?

 

 

리눅스를 보다 기술적으로 이해하기 위해 리눅스 커널에 대해 살펴 봤다. 리눅스커널이 어떤 구조로 되어 있는 지 어떤 기능을 제공하는 지는 리눅스 관련 서적에서 대부분 다루고 있으나 리눅스커널의 소스를 하나씩 분석, 짚어봄으로써 리눅스 커널을 보다 잘 이해할 수 있다. 커널은 매우 많은 파일과 방대한 자료 구조로 이뤄져 있으므로 방대한 소스를 분석하기 위한 도구의 종류와 사용법을 익혀야 할 필요가 있다. 그러면 리눅스 커널의 컴파일 과정을 알아 보고 커널분석을 위해 도움을 줄 수 있는 프로그램에는 어떤 것들이 있는 지 알아 보도록 하자.

 

지난호에는 리눅스 커널의 구조에 대해서 알아 봤다. 이러한 리눅스 커널의 구조는 기능적으로 리눅스 커널의 각 블록을 구분한 것으로서 실제로는 그림1과 같은 디렉토리 구조로 이뤄져 있다.

[그림 1]

그림은 실제 리눅스 커널 2.6.13의 소스에 있는 디렉토리 구조를 나타낸 것으로 각 디렉토리에는 같은 종류의 소스파일들이 모여 있다.

이러한 소스 파일들은 사용자가 설정한 커널 옵션에 따라 선택적으로 컴파일, 링크돼 하나의 커널 이미지가 만들어 진다. 커널 옵션이란 make menuconfig 또는 make xconfig 명령을 통해 사용자가 설정하는 것으로서 커널의 기능을 추가/제거하고 커널의 특성을 결정지을 때 사용하는 것이다. 예를 들면 사용자가 make menuconfig 명령을 통해 vfat 파일 시스템을 커널에 추가했다면 CONFIG_VFAT_FS 커널 옵션이 활성화된다.

리눅스 커널은 다음과 같은 과정을 통해 컴파일된다.

 

먼저 커널소스의 top 디렉토리에서 make menuconfig 명령을 입력하면 (그림1)과 같은 과정이 일어 난다.

 

(그림1) 2.4x 버전 대 리눅스 커널의 컴파일 과정을 나타낸 그림이다. 2.6.x 버전 커널 역시 위 과정과 크게 다르지 않기 때문에 2.4.x 버전 커널 컴파일 과정을 가지고 설명하고자 한다.

 

리눅스 커널 컴파일 과정

 

make menuconfig 명령을 입력하면 먼저 기존에 make menuconfig 시 생성한 심볼릭 링크 디렉토리인 include/asm 디렉토리를 지우고 컴파일하고자 하는 아키텍처의 헤더 파일이 들어 있는 디렉토리를 include/asm 디렉토리에 심볼릭 링크한다.

가령 컴파일하고자 하는 아키텍처가 i386이라면 include/asm-i386 디렉토리를 include/asm 디렉토리로 심볼릭 링크한다. 이는 커널 소스 내에서 아키텍처 종속적인 헤드 파일에 대해서 #include <asm/timer.h> 와 같은 형태로 참조하도록 돼 있기 때문이다.

커널 2.6에서는 조금 방식이 바뀌었는데 make zImage 또는 make bzImange 하는 순간에 include/asm-i386 디렉토리가 include/asm 디렉토리로 심볼릭 링크된다.

include/asm 심볼릭 링크를 만들었으면 scipts/lxdialog 디렉토리에 lxdialog 프로그램을 컴파일한다. make menuconfig 명령을 입력하면 푸른 화면의 메뉴가 나타남을 확인할 수 있을 것이다. 이는 ncurses 라이브러리를 사용하는 lxdialog 프로그램이 띄워 주는 것이다.

lxdialog make menuconfig 명령외에도 리눅스 응용애플리케이션의 화면 인터페이스를 위해 많이 사용되는 프로그램이다. 실제로 배포판에 따라 /usr/bin./lxdialog 또는 /usr/bin/dialog 프로그램이 존재함을 확인할 수 있다.

다음으로 scripts/Menuconfig 스크립트가 arch/i386/config.in 을 파싱해서 lxdialog의 인수로 넘겨주면 menuconfig 화면이 출력된다.

커널2.6에서는 이 방식이 약간 바뀌어 lxdialog 컴파일 이전에 scripts/kconfig/mconf 를 컴파일하게 되는데 scripts/kconfig/mconf arch/i386/Kconfig 를 파싱해서 lxdialog 의 인자로 넘겨 주도록 되어 있다.

2.4버전의 arch/i386/config.in 파일과 2.6버전의 arch/i386/Kconfig 파일은 커널 옵션의 종속 관계를 정리해 놓은 파일로서 여기에는 각 옵션에 대한 정의와 해당 옵션이 종속돼 있는 dhqtusemfd에 대해서 정리해 놓고 있다.

2.4 버전의 config.in 파일과 2.6버전의 Kconfig파일은 그 기능에서 동일하지만 문법적인 면에서 약간 차이가 있다. 2.4 버전의 config.in 파일은 문법이 다소 난잡하고 종속관계 기술이 힘들었던 반면 2.6 버전의 Kconfig 파일은 문법이 간결하며 종속관계 기술이 매우 쉽다.

makde menuconfig 명령을 내렸을 때 화면에 메뉴가 표시되는 것은 모두 config.in 또는 Kconfig 에서 메뉴를 정의하고 있기 때문이다. config.in 또는 Kconfi 파일을 바꾸게 되면 make menuconfig 화면을 바꿀 수 있게 된다.

사용자가 menuconfig 화면에서 커널 옵션을 설정하고 menuconfig 화면을 종료하면 include/linux/autoconf.h 파일과 .config 파일이 생성된다. include/linux/autoconf.h

파일은 C 헤더 파일로서 사용자가 선택한 커널 옵션이 C 매크로로 define 되어 있다. 나중에 autoconf.h 파일은 거의 대부분의 커널 소스에 include 되는데 #ifdef 문을 통해 커널 소스에서 선택적인 컴파일 또는 배열의 크기를 결정하는 등의 일에 사용된다.

.config 파일은 Makefile include 되는데 커널 옵션이 make 매크로로 정의되어 있다. .config 에 정의된 매크로를 참조해 make 는 컴파일해야 될 디렉토리 및 파일을 결정하고 링크 시 포함해야 될 object 에 대해서 결정하게 된다.

커널 옵션 설정이 끝났고 include/linux/autoconf.h 파일과 .config 파일이 생성됐으면 make dep 명령을 내리게 된다.

make dep 명령은 make 유틸리티와 관계된 내용으로서 각 소스 파일이 어떤 파일과 종속관계에 있는 지 파악해  .depend 라는 파일 종속 관계 정의 파일을 생성한다.  .depend 파일 역시 makefile include 돼 종속관계에 있는 파일이 변경시에 해당 소스 파일을 재 컴파일할 수 있게 한다. make dep 명령은 커널 2.4 까지 사용하였지만 커널 2.6 에서는 더 이상 사용되지 않고 있다.

다음으로 make zImage 또는 make bzImage 명령을 내려 커널을 컨파일하게 된다. 이 때 컴파일 되는 소스 파일은 사용자가 make menuconfig 를 통해 module dl 아니라 커널에 포함시킨 기능들에 대해서만 컴파일해서 링크하게 된다. 물론 커널 이미지에 포함할 지 모듈로 포함시킬 지는 make menuconfig 시에 생성된 .config 파일에 다 정의 되어 있다.

make zImage make bzImage 명령은 i386 아키텍처에서 커널 사이즈에 따라 로드되는 위치를 달리 할 때 사용되는 명령이다. 나중에 설명하겠지만 i386 아키텍처는 부팅 시 부트로드에 의해 0x90000 이후 번지에 사용되는데 0x10000 ~ 0x90000 번지까지 커널이 들어 간다면 make zImage 명령을 사용할 수 있다.

그러나 커널 사이즈가 커서 0x10000 ~ 0x90000 번지에 들어갈 수 없다면 make bzImage 명령을 사용해 컴파일해야 한다. make zImage 명령으로 컴파일하면 부팅시 커널을 0x10000 번지로 로드하고 make bzImage 명령으로 컴파일하게 되면 0x100000 위치에 로드하게 된다. i386 에서 bzImage zImage 의 차이는 부팅시 zImage 가 약간 빠르다는 것외에는 별반 차이가 없다.

 

리눅스 커널 소스를 분석해 보자

 

커널 이미지를 생성했으면 make modules 명령을 통해 사용자가 menuconfig module로 지정한 기능들에 대해서 컴파일하게 된다. 커널 module 은 파샬 링크된 elf object 로서 insmod 명령을 통해 커널 심볼 테이블을 참조해 최종 링크가 될 object 이다. 이 부분에 대해서는 나중에 자세히 다루도록 하겠다.

module 이 다 만들어졌으면 make modules_install 명령을 통해 module로 컴파일 된 object /lib/[커널버전] 디렉토리에 복사면 커널 컴파일 과정은 종료하게 된다.

지금까지 리눅스 커널의 대략적인 내용에 대해서 알아 봤다.

이제 리눅스 커널 부팅 과정에 대해서 알아 보자

리눅스 커널을 제대로 알기 위해서는 책만 보아서는 절대로 알 수 없다. 책을 보면 리눅스 커널이 대충 어떠한 기능을 제공하고 어떠한 구조로 돼 있다는 것을 알 수 있다. 하지만 그건 어디까지나 이론이다.

system call 을 예로 들자면 여러 많은 책들이 system call 에 대해서 설명하고 있는데 system call 의 이론은 책이 잘 설명하고 있지만 system call 의 구현에 대허서는 리눅스 커널 소스만큼 잘 설명해 놓은 것이 없다. 책을 보면 system call 을 이해할 수 있지만 system call 을 직접 구현할 수는 없다. 이론만 알고 구현을 못한다면 제대로 된 개발자 혹은 엔지니어라고 말할 수 있을까?

리눅스 커널 소스 분석 과정을 통해 리눅스 커널에 대한 막연한 두려움을 없애고 리눅스 커널의 부팅 과정을 이해하는데 도움을 얻을 수 있다. 필자의 견해로는 가장 중요한 것은 리눅스커널에 대한 막연한 두려움을 없애는 것이다. 리눅스 커널도 사람이 작성한 프로그램인 만큼 이해 못할 만큼 어려운 것이 아니다. 양이 많아서 그렇지 하나 하나 따져 보면 별것이 아니라는 사실을 확인할 수 있을 것이다.

불을 뿜어대는 무서운 용이 지키고 있는 공주를 구하기 위해 지혜로운 기사는 갑옷과 칼로 완전 무장을 하고 용과 맞서 싸운다. 커널 분석하는 것 역시 마찬가지이다.

커널은 매우 많은 파일과 방대한 자료 구조로 이뤄져 있어서 무턱대고 분석을 시도한다면 커널의 세계에서 길을 잃고 헤매고 만다. 초행길 여행을 떠날 때 적어도 길 안내를 해 줄 수 있는 지도와 나침반을 챙기는 것처럼 커널을 분석할 때도 방대한 양의 소스를 분석하기 위한 도구의 종류와 사용법에 대해서 간단하게 익혀야 할 필요가 있다. 그럼 간단하게 커널 분석을 위해 도움을 줄 수 있는 프로그램에는 어떤 것들이 있는 지 알아보도록 하겠다.

 

●  screen + vim + ctags + cscope

 

전통적인 unix/linux 환경에서 사용하는 소스 분석 프로그램이다. vim ctags cscop 를 연동해 소스 분석을 하는데 많은 편의를 제공한다. 예를 들면 ctags 를 사용하면 어떤 함수나 변수가 사용 되었을 때 그 함수나 변수가 정의된 곳으로 한번에 찾는 것이 가능하다. 그리고 cscope 는 어떤 변수나 함수가 사용된 모든 곳을 찾는데 편리하다.

각각의 유틸리티에 대해서 자세한 사용법을 알려 주면 좋겠지만 지면이 허락하지 않기 때문에 소개정도만 할까 한다. 해당 유틸리티는 unix/linux 개발 환경에서 필수적인 유틸리티이기 때문에 되도록 사용법을 익혀 두기 바란다.

 

●  lxr

 

커널 소스를 웹브라우저로 분석할 수 있게 해주는 도구이다. 독자들의 리눅스 서버에 직접 설치해서 사용해도 되고 설치하기가 귀찮으면 이미 설치돼 있는 lxr 을 사용하면 된다.

http://lxr.linux.no 에 가보면 이미 설치돼 있는 lxr 을 이용해 커널소스를 분석할 수 있다. vim + ctags + cscope 환경에 익숙하지 않은 독자들은 lxr 을 이용하기 바란다.

 

●  기타

 

기타 커널 소스를 분석하기 위한 여러 프로그램들이 많이 있다. 대표적인 프로그램들로써 source insite, source navigate 등이 있고 각자의 취향에 맞게 사용하면 된다. 필자가 생각하기에 전통적인 unix/linux 환경에서 소스 분석 프로그램으로는 vim + ctags + cscope 만한 것도 없다고 생각하는데 다소 배우기 까다로운 단점이 있다. 그렇지만 위에서 설명한 여러 도구들이 지원하는 모든 기능을 vim + ctags + cscope 에서 모두 제공하고 있고 또한 분석과 동시에 주석을 다는 것도 가능하기 때문에 vim + ctags + cscope 분석 환경을 추천한다.

다음호에는 리눅스 커널이 어떻게 부팅되는지 과정을 상세히 알아 보겠다.

 

 

출처 : 공개 SW 리포트 2 66 ~ 69 페이지 발췌(2006 8) - 한국소프트웨어 진흥원 공개SW사업팀 발간


리눅스 커널과 디바이스 드라이버 프로그래밍

 

 

세번째강좌 : 리눅스 커널의 부팅 과정 해부하기

 

 

리눅스 이해를 돕기위해 리눅스 커널이 어떤 구조로 돼 있는지 어떤 기능을 제공하는 지 살펴봤다. 지난 호에서는 리눅스 커널의 컴파일 과정을 알아 보고 커널 분석을 위해 도움을 줄 수 있는 프로그램에는 어떤 것들이 있는 지 알아 봤으며 이번 호에는 리눅스 커널의 부팅 과정에 대해 알아 보고자 한다. 리눅스 커널의 부팅 과정은 하드웨어 아키텍처에 따라 또 CPU 에 따라 각각 다른데, 커널을 직접 고쳐서 사용해 볼 수 있는 임베디드 기기용 CPU를 통해 부팅과정을 알아 보기로 한다.

 

리눅스뿐만 아니라 거의 모든 OS의 부팅 과정에서 하는 일을 단순화시키면 두가지 일밖에 없다. 하드웨어 초기화,  커널 자료 구조 초기화가 바로 부팅과정에서 일어나는 일이다. 이 중 하드웨어 초기화는 프로세서에 따라 달라지는 부분이라서 아키텍처 종속저깅ㄹ 수 밖에 없다. 이제 리눅스 커널의 부팅 과정을 하나씩 짚어 보자. 앞서 언급한 대로 어떤 하드웨어 아키텍처인가에 따라 하드웨어 초기화부분은 달라질 수 있다. 예를 들면 x86 과 같은 아키텍처는 부팅할 때 segmentation 관련 주소 레지스터를 초기화하는데 이러한 segmentation x86 아키텍처에만 있으며 ARM 과 같은 아키텍처에서는 존재하기 않기 때문에 관련 설정도 필요 없다. 이렇듯 하드웨어 초기화 부분이 아키텍처마다 다르기 때문에 우리는 부팅 코드 분석에 앞서 어떠한 아키텍처를 중심으로 분석을 할 것인지 결정을 내려야 한다.

x86 아키텍처를 분석하면 가장 많이 사용하는 아키텍처이기 때문에 마음에 와 닿는 바가 클 것이다. 그렇지만 x86 아키텍처는 서버와 데스트톱 시장에서 주류를 이루고 있어 커널을 직접적으로 고쳐서 사용하는 일이 드물다. 때문에 커널을 분석하는 것에만 의의가 있고 분석한 것을 이용해 보기는 힘든 아키텍처이다. 따라서 임베디드 시장에서 광범위하게 사용되고 있는 ARM 아키텍처의 리눅스 커널-2.6.13 버전으로 분석해 보기로 한다.

그리고 같은 ARM 아키텍처라고 해도 CPU 종류에 따라서 core 또는 하드웨어 IP(Intellectual Property, : timer, interrupt controller, memory controller )가 다르기 때문에 하드웨어를 초기화하는 코드도 달라진다. 때문에 하드웨어 아키텍처와 함께 CPU도 정해서 분석할 것인데 여기서는 임베디스 시스템에서 광범위하게 사용되고 있는 삼성 S3C2440 CPU 의 커널을 분석하고자 한다.

 

리눅스 커널 부팅 과정의 전체 구조

 

그림 1 ARM 아키텍처에서 리눅스 커널의 부팅 과정을 도식화한 것이다. 리눅스 커널의 부팅 과정을 간단하게 보면 그림 1과 같다고 말할 수 있다. 물론 각 부분은 보다 복잡하게 돼 있다.

 

이 과정을 간단하게 설명하면 다음과 같다. 먼저 최초 전원이 인가되면 ARM 아키텍처에서는 bootloader 코드가 수행된다. bootloader 코드는 보통 0x0 번지에 위치한다.

bootloader는 최초 커널을 RAM에 로딩하기 위해 clock 을 초기화하고 RAM 컨트롤러를 초기화한다. 그리고 커널을 메인 메모리에 로딩하고 제어권을 커널에게 넘긴다.

그러면 커널의 제일앞에 있는 코드를 수행하게 되는데 커널의 제일 앞부분에는 arch/arm/boot/compressed/head.S  커널의 압축을 푸는 코드가 들어 있다. 커널은 보통 사이즈를 줄이기위해 압축돼 있는데 head.S 는 커널이 동작하는 위치에 압축을 풀게 된다.

그리고 제어권을 실제 커널의 head arch/arm/kernel/head.S 로 옮기게 된다. 실제 커널의 head arch/arm/kernel/head.S는 주로 하드웨어 초기화 및 BSS 초기화, XIP 적용 등을 담당하고 커널의 main 함수격인 start_kernel 로 제어를 넘기게 된다.

start_kernel 함수에서는 실로 여러가지 일들을 하게 돼 있다. 이 그림에서는 간단하게 세가지 일로 추려 놓았지만 실제로 커널이 부팅하면서 하는 거의 대부분의 일은 startr_kernel 함수에서 해 주게 돼 있다.

startr_kernel 함수의 막바지에는 init이라는 kernel thread를 생성하는데 init kernel thread 는 각종 디바이스 드라이브의 초기화 루틴을 호출해서 디바이스들을 초기화해주고 / 디렉토리를 마운트 해 주게 된다. 마지막으로 execv 시스템 콜을 사용해 /sbin/init 프로세스를 생성하게 된다.

/sbin/init 프로세는 /etc/rc.d 에 있는 각종 초기화 스크립트를 수행해 기본적으로 수행해야 하는 데몬 프로세스를 수행시켜주고 최종적으로 mingetty 라는 가상 터미널을 띄워주게 된다. mingetty 는 수행과 동시에 login 프로세스를 수행하게 되는데 그러면 우리에게 친숙한 loggin: 프롬프트를 보게 된다.

매우 복잡한 절차같지만 이는 부팅 과정을 최대한 단순하게 설명한 것이다. 그러니 실제로 소스를 분석할 때는 얼마나 복잡할까? 실제 소스를 분석하는 데는 책으로 써도 모자랄 만큼이다. 때문에 앞으로 소스를 가지고 설명할 때는 중요한 부분만을 중점적으로 설명하도록 하겠다. 그럼 이제부터 본격적으로 소스 분석을 해 보도록 하겠다.

 

bootloader code 수행

 

이전에 커널 부팅 과정 전체 구조에서 설명했듯이 ARM 아키텍처는 전원이 인가되는 순간 CPU에서 0x0 번지에 있는 code를 수행하기 시작한다. 보통 0x0 번지에는 NORflash 메모리가 매핑돼 있고 그 속에는 u-boot, blob 등과 같은 bootloader 코드가 들어가 있다.

제일 처음 수행되는 bootloader 의 역할은 최종적으로 flash 또는 ROM에 있는 커널의 이미지를 RAM에 로드하는 일이다. RAM 은 주로 가격이 낮은 DRAM 이 많이 사용되는데 DRAM 을 사용하기 위해서 DRAM controller 를 초기화해줘야 한다. 그러기 위해서는 먼저 시스템에서 사용되는 clock 을 초기화 하는 일이 선행돼야 한다.

결국 bootloader 에서 가장 중요하게 하는 일은 커널을 RAM load 하기 위해 시스템 clock을 초기화하고 DRAM controller 를 초기화해서 DRAM 을 사용 가능하게 하고 최종적으로 커널 이미지를 DRAM에 로드하는 일이다.

이것이 bootloader 가 수행하는 일반적인 일이다. 임베디드 시스템의 종류에 따라서 bootloader 에서 해줄 일이 천차만별이기 때문에 일반적인 경우만 짚고 넘어가겠다.

 

arch/arm/boot/compresse/head.S 수행

 

bootloader 에서 커널을 미리 약속된 DRAM 주소에 로드하고 나면 커널 이미지의 제일 처음에 있는 arch/arm/boot/compressed/head.S 의 코드가 수행된다.

그림 2 linux-2.6 ARM 아키텍처 커널의 구조를 나타낸 그림이다.

아키텍처마다 또는 커널의 버전마다 생성되는 커널의 이미지 구조가 약간씩 다르다. arch/arm/boot/compressed/Makefile 을 분석해 보면 어떤 모양으로 커널 이미지가 만들어 지는 지 알 수 있다.

그림 2에서 head.o head.S 가 컴파일된 object로서 커널의 이미지가 압축돼서 만들어진 piggy.o 의 압축을 풀고 커널이 컴파일될 당시 Base 주소로 잡힌 위치로 커널을 옮겨 놓는 역할을 담당한다. piggy.o gzip 알고리듬으로 압축된 커널에 elf 헤더를 붙인 커널 이미지로서 커널의 본쳉 해당한다. 마지막으로 misc.o 는 압축을 풀기 위한 gzip 알고리듬을 가지고 있는 object이다.

그럼 이제 본격적으로 head.S 에 대해서 알아 보자 head.S PIC(PositionINdependent Code)로 작성돼 있다. 때문에 head.S 는 메모리상의 어떤 위치에 두더라도 정상적으로 수행 가능하게 돼 있다.

맨 앞에 보면 쓸데없는 define 문으로 어지럽게 돼 있을 것이다. 부팅시 그 부분은 건너뛰고 아래와 같이 start 라벨에서 시작하게 된다.

위 소스에서 (.)으로 시작하는 것들은 모두 assembly에서 특수한 지시를 하기 위한 assembly directive 이다.  .rept ~  .endr   .rept .endr 사이에 있는 명령을 .rept 다음에 오는 횟수만큼 반복해서 쓰라는 의미이다.

즉 실제 이 코드는 mov r0, r0 8번 코딩해 놓은 것과 동일한 것이다. r0 r0에 넣는 것은 아무 의미 없는 일이고 nop 8번 수행한 것과 동일하다.

다음으로 bootloader 에서 받아온 아키텍처 ID r7 에 저장하는 것과 Angel bootloader 로 부팅했을 시 처리하는 일이 있는데 그냥 넘어가도 좋다.

다음으로  r0 LC0 의 주소를 저장하고 r0의 주소를 기준으로 r1,r2,r3,r4,r5,r6,ip,sp 레지스터의 값을 각각 로드하는데 각각에는 LC0의 값에서 r1의 값을 빼는데 r0에는 LC0의 주소가 들어 있고 r0에도 LC0의 주소가 들어 있게 된다.

정상적으로 커널이 로드됐다면 r0에서 r1을 빼면 0이 되기 때문에 beq not_relocated 명령에 의해 not_reloacated 라벨로 branch 하게 된다.

위에서 보는 것이 LC0 라벨 이하로 잡혀 있는 데이터 값들이다. 각각의 데이터는 4byte로 돼 있고 이전에 본 code에 의해 각 레지스터의 값으로 채워지게 된다.

not_relocated 에서는 제일 먼저 BSS를 초기화하게 된다. 여기서 BBS는 커널의 BSS가 아니라 head.o, piggy.o, misc.o BSS 를 의미한다.

나중에 커널 압축이 풀리고 커널의 진짜 head  arch/arm/kernel/head.S 가 수행되면서 커널의  BSS가 초기화되도록 한다.

BSS를 초기화 했으면 cache enable 한다. cache_on 라벨에서는 call_cache_fn 라벨로 점프한다.

그리고 r1 r2에 각각 프로세서의 ID값과 mask 값을 받아와서 아래에 있는 proc_types structure 배열에서 프로세서의 ID값과 mask 값이 일치하는 것을 찾아 cache_on 함수를 수행시켜 주는데 S3C2440 CPU ARMv4ㅆ에 해당되기 때문에 아래 부분을 수행하게 된다.

결국 _armv4_cache_on을 점프해서 _armv4_cache_on 라벨이하로 수행되는데 _setup_mmu 를 호출해 MMU를 셋업하고 _common_cache_on 호출해 cache enable 하고 마지막으로 I-cache, D-cache, TLB flash 해 주는 역할을 수행하도록 돼 있다.

 

MMU를 셋업하는 부분은 page table 을 초기화하는 부분이 다소 복잡하기 때문에 생략하기로 하고 cache enable 하는 부분인 _common_cahce_on 의 코드는 아래와 같다.

먼저 TTB 레지스터에 초기화한 page table 의 시작주소를 넣는다. 그리고 도메인 접근 제러를 로드하고, I-cache enable 한다. 이 과정이 끝나면 다시 이전의 _armv4_cache_on 라벨에서 bl_common_cache_on 명령어 다음으로 돌아 가서 I-cache, D-cache, TLB flash 시켜 주고 mov pc, r12 명령어에 의해 이전에 not_relocated 라벨의 bl cache_on 다음 명령으로 돌아가게 된다.

그러면 sp 레지스터값을 설정해서 커널의 압축을 풀 때 사용할 stack 을 설정한다.

그리고 커널의 크기나 위치를 봐서 바로 압축을 풀 수 있다면 wont_overwrite 라벨로 점프한다.

마지막으로 bl decompress_kernel 명령으로 커널 압축을 풀고 b call_kernel 명령으로 실제 커널의 head arch/arm/kernel/head.S 로 점프하면 arch/arm/boot/compressed/head.S 의 일은 끝나게 된다.

 

출처 : 공개 SW 리포트 358 ~ 62 페이지 발췌(2006 10) - 한국소프트웨어 진흥원 공개SW사업팀 발간


리눅스 커널과 디바이스 드라이버 프로그래밍

 

 

네번째강좌 : 리눅스 커널, 압축 그 이후의 과정

 

 

지난 호에는 임베디드 시장에서 광범위하게 사용되고 있는 ARM 아키텍처의 리눅스커널-2.6.13 버전으로 리눅스 커널이 어떻게 구동되는지 단계별로 알아 봤다. 전원이 인가된 ARM 아카텍처는 부트로더 코드를 수행하고 이 부트로더는 최초커널을 메인 메모리에 로디해 제어권을 커널에게 넘긴다. 그러면 커널 제일 앞에 있는 코드가 수행하는데 이것이 커널의 압축을 푸는 코드다. 이번 호에는 커널 압축이 해제된 후 리눅스 커널이 어떻게 구동하는 지 알아 보자.

 

arch/arm/boot/compressed/head.S 에 의해 압축이 풀린 커널은 이제 비로소 진짜 커널의 head arch/arm/kernel/head.S 를 수행하게 된다. arch/arm/kernel/head.S XIP data section RAM으로 옮기는 일을 수행하고 MMU enable 하며 kernel RSS section 을 초기화하고 최종적으로 커널의 메인 함수격인 start_kernel() 로 분기하는 역할을 수행한다.

여기서 말하는 XIP란 커널 코드를 ROM이나 NOR 플래시 메모리 상에서 바로 수행하는 것을 의미한다. 일반적으로는 코드와 데이터를 RAM에 복사해 수행하나 XIP를 사용하면 데이터만 RAM에 카피하므로 수행해서 RAM 사용량을 코드 사이즈만큼 절약할 수 있다.

 

arch/arm/kernel/head.S

 

arch/arm/kernel/head.S 를 설명하기전에 압축이 풀린 커널은 메모리상에 아래와 같은 구조를 갖게 된다.

이러한 구조는 arch/arm/kernel/vmlinx.lds.S 파일을 보면

먼저 CPSR 레지스터 값을 설정해 SVC mode로 전환하고 IRQ, FIQ disables 한다. SVC mode ARM 에서 사용되는 일종의 특권모드로서 system call 을 위해 사용된다. 부팅을 위해선 이렇게 interrupt disable 하고 특권모드로 전환해야 한다. 아직 interrupt 처리를 위한 자료구조나 HW가 초기화하기 않았기 때문에 부팅중에 interrupt 가 발생하면 시스템이 예기치 못한 상황에 빠질 수 있다.

__lockup_processor_type 함수는 해당 프로세서를 초기화하기 위한 proc_info 구조체를 받아 오는 함수이다. proc_info 구조체는 커렁에서 지원하는 ARM 계열 프로세서에 대해서 초기화를 위한 루틴과 프로세서를 확인하기 위한 mask 값 등을 가지고 있는 구조체이다.

각각의 프로세서 타입에 맞는 proc_info 구조체는 arch/arm/kernel/vmlinux.lds.S 파일에 설정된 바와 같이 init section __proc_info_begin __proc_info_end 심볼 사이에 배열 형태로 존재한다. 이렇게 init section 에 배열 형태로 등록되어 있는 proc_info 를 찾는 데는 아래와 같은 절차를 따른다.

우리가 분석하고 있는 s3c2440 프로세서의 proc_info arch/arm/mm/porc-amm920.S 파일에 있는 __arm920_proc_info 구조체인데 아래와 같은 구조를 가지고 있다. 그리고 arch/arm/mm  디렉토리에 가보면 각각의 프로세서 타입에 따른 proc_info 구조체를 가지고 있음을 확인할 수 있을 것이다.

해당 프로세서의 proc_info 구조체를 받아 왔으면 __lockup_machine_type 함수를 호출해 arch_info 구조체를 r5에 받아 온다. arch_info 는 각각의 보드를 초기화 하기 위한 루틴이 등록되어 있는 구조체로서 proc_info 와 마찬가지로 arch/arm/kernel/vmlinux.lds.S 파일에 __arch_info_begin __arch_info_end 심볼 사이에 각각의 보드에 맞는 arch_info 구조체가 배열 형태로 잡혀 있다.

proc_info 와 마찬가지로 arch_info 에 있는 machine type 값과 bootloader 에서 넘어온 아키텍처 ID 값과 비교해 같으면 r5로 해당 arch_info 를 리턴하고 다르면 다음 arch_info machine type 값을 계속해서 비교하게 된다.

proc_info arch_info 는 프로세서 혹은 보드 종속적인 초기화를 위한 루틴들이 들어 있고 나중에 이러한 초기화 루틴들을 호출해 HW를 초기화 하게 된다.

위 내용은 arch/arm/mach-s3c2410/mach-smdk2440.c 파일에 있는 smdk2440 보드의 arch_info 구조체를 보여준다. MCHINE_START(S2440, SMDK2440) 에 있는 S3C2440 define 되어 있는 smdk2440 보드의 machine type 값이다.

각각의 보드에 대한 arch_info 구조체는 arch/arm/mach-* 디렉토리에서 확인할 수 있을 것이다.

다음으로는 __create_page_tables 함수를 호출해 page table 을 생성한다. 먼저 커널의 시작 부분에서 0x4000 앞에 있는 0x30004000 위치에 swapper level 1 page table 16KB 만큼 생성한다. 그리고 커널이 있는 범위를 포함한 0x30000000 부터 4MB 공간만큼은 page table 에 매핑한다.

swapper 는 커널 부팅할 때 제일 처음 등록되어 있는 프로세스의 PCB(Process Control Block) 로서 메모리상에 해당 PCB의 공간이 처음부터 잡혀 있다. 부팅하는 과정을 통해 swapper PCB에 있는 각각의 필드들은 초기값이 설정되고 부팅이 끝나는 시점에 init kernel thread 는 이 swapper 로부터 fork 하게 된다.

ldr rl3, __switch_data 루틴은 stack point __switch_data 심볼의 주소를 넣는 부분이다.

__switch_data 에는 각종 커널 심볼의 주소를 가지고 있고 이러한 커널 심볼의 주소들은 XIP 를 사용할 경우 data section RAM에 이동할 때나 커널의 BSS section 을 초기화할 때 사용된다.

adr lr, __enable_mmu 명령은 아래에 있는 add pc, r10, #PROCINFO_INITFUNC 명령을 수행하고 나서 리턴된 주소를 __enable_mmu 로 설정하는 부분이다. 이전에  __lookup_processor_type 함수를 호출해 r5에 받아온 proc_info 구조체 주소는 r10에 옮겨졌다. add pc, r10, $PROCINFO_INITFUNC 명령은 proc_info 에 있는 프로세서 초기화 함수를 호출하는 부분이다. s3c2440 프로세서의 경우에는 __arm920_setup 함수를 호출하게 된다.

__arm920_setup 함수는 I-cache D-cache, write back buffer, TLB 를 각각 초기화 한다. __arm920_setup  함수의 수행이 끝나면 adr lr, __enalbe_mmu 명령에 의해 리턴할 주소가 __enable_mmu 로 설정돼 있기 때문에 __enalbe_mmu 가 수행되게 된다.

__enable_mmu 에서는 cpl5 c3, c0 레지스터를 설정해 도메인을 설정하고 c3, c0 레지스터에 이전에 만들어준 page table 의 주소 값을 설정해 준다. 그리고 최종적으로 MMU enable paging 시작하게 된다. 이제부터 주소는 모두 가상 주소를 사용하게 된다.

__enable_mmu 마지막에는 __mmap_switched 가 수행되는데 XIP를 사용한다면 data section RAM으로 옮기고 커널의 BSS section 을 초기화 한다.

그리고 최종적으로 C함수의 시작인 start_kernel 로 분기해 이제 본격적인 커널 자료구조 최기화를 시작하게 된다. 이로써 커널의 진짜 head head.S 의 초기화 과정이 끝나게 된다. 다음 시간에는 start_kernel 함수에서 어떤 과정을 거쳐 부팅하게 되는 지 알아 보도록 하겠다.

 

출처 : 공개 SW 리포트 4호 페이지 60 ~ 63 발췌(2006 12) - 한국소프트웨어 진흥원 공개SW사업팀 발간

 


리눅스 커널과 디바이스 드라이버 프로그래밍

 

 

다섯번째강좌 : 리눅스 커널의 소스 분석

 

 

이번 과정에서는 리눅스 커널의 메인 함수라고 할 수 있는 start_kernel() 함수에 대해 분석해 보도록 하겠다. arch/arm/kernel/head.S 에서 프로세서 관련 초기화를 끝내게되면 init/main.c start_kernel() 함수로 넘어 오게 된다. start_kernel() 은 매우 방대한 함수로서 각종 정보를 수집해 커널에서 사용하는 각종 자료 구조를 초기화하고 프로세스 각종 하드웨어에 대한 초기화를 담당한다. start_kernel() 함수의 최종 목표는 각종 초기화를 통해 프로세스가 수행할 수 있는 환경을 만들어 주는 데 있다.

 

init/main.c:start_kernel()

 

start_kernel() 함수에 들어가게 되면 가장 먼저 하는 일은 start_kernel() 함수에 동시에 여러 프로세서가 수행하지 못하게 커널을 막는 일(lock)이다.

 

asmlinkage void __init start_kernel(void)

{

lock_kernel();

 

 

 

lock_kernel() SMP 를 사용하지 않을 때는 do {} while ({}) 코드로 바뀌어서 아무 일도 수행하지 않고 SMP를 사용하게 되면 lib/kernel lock.c 에 정의 되어 있는 lock_kernel() 함수가 호출되어 spinlock lock하는 코드로 바뀌게 된다.

sopin lock multi-core에서 사용되는 lock 메커니즘의 일종으로 spin lock 을 획득한 하나의 프로세서만이 다음 코드를 수행할 수 있고 spin lock을 획득하지 못한 나머지 프로세서들은 spin lock에서 계속 무한 루프(loop)를 돌면서 lock이 풀릴 때까지 대기한다.

SMP 시스템의 특징은 interrupt disable 혹은 preemption disable 을 할 지라도 다른 프로세서에서 critical section 을 침범할 수 있기 때문에 spin lock을 사용해야 다른 프로세서의 침범을 막을 수 있다.

다음으로 오는 것이 page_address_init() 함수와 printk()함수이다. page_address_init() 함수는 high 메모리를 사용할 시에만 실제 함수가 호출되고 arm high 메모리를 사용하지 않는 아키텍처에서는 do {} while({}) 코드로 바뀌게 된다.

 

page_address_init(); // high mem을 사용하지 않은 arm에서는 사용되지 않음

printk(KERN_NOTICE); // print level kernel notice 로 설정

printk(linux_banner) ; // 리눅스 kernel버전과 build 환경을 출력

 

아래 두 printk() 함수는 커널 부팅할 때 제일 먼저 띄워 주는 아래와 같은 메시지를 출력하는 부분이다.

 

Linux     version      2.6.11-1.1369_FC4

(bhcompile@decompose.build.redhat.com)(gcc version 4.0.0

20050525(Red Hat 4.0.0-9))#1Thu Jun 2 22:55:56 ET 2005

 

다음으로 호출되는 함수가 setup_arch() 함수이다.

setup_arch() 함수는 아키텍처 관련 초기화를 담당하는 함수로서 프로세서와 보드 관련 정보를 초기화하고, 부트 로더에서 넘겨준 명령을 파싱해서 설정하고 메모리의 영역을 초기화하는 등과 같은 일을 수행한다.

 

setup_arch(&command_line);

 

다음은 setup_arch() 함수에서 호출하는 중요 함수들을 나타낸 것이다.

 

//arch/arm/mm/proc-아키텍처.S에서 아키텍처_proc_info 정보를 받아 와서

//processor 정보를 설정

setup_processor();

//arch/arm/mach-s3c2410/mach-smdk2440.c에 정의되어 있는 arch_info 정보를

//가지게 mdesc설정

mdesc = setup_machine(machine_arch_type);

//swapper init_mm kernel code 관련 정보 설정

init_mm.stat_code = (unsigned long)&_test;

init_mm.end_code = (unsigned long)&_etext;

//부트로더에서 넘어온 kernel 명령어를 해석해 설정

// ex) kernel /boot/vmlinuz-2.4.20-19.9 ro root=LABEL=/vga=788 hdc=ide-scsi

parse_cmdline(cmdline_p, from);

//bootmap을 초기화, page table을 초기화, zone을 설정

paging_init(&meminfo, mdesc);

//각종 cpu 정보를 출력

cpu_init();

 

사실 위에서 설명한 내용은 setup_arch() 함수가 해주는 내용을 매주 단순화 시킨 내용이다. setup_arch()가 해주는 일은 매우 방대하고 아키텍처 종속적인 부분이 많아 다 설명하려면 이번장을 다 채우고도 모자랄 정도이다. 이번 장의 목적은 커널 소스의 전체적인 흐름을 보여 주는 것이 목적이기 때문에 setup_arch()가 대략적으로 어떤 일을 하는지만 알고 넘어가기로 한다.

setup_arch() 함수 다음으로는 setup_per_cpu_areas()함수가 있다. setup_per_cpu_areas() 함수는 SMP에서만 하는 일이 있고 single 프로세서에서는 아무 일도 하지 않는다.

setup_per_cpu_areas() 함수는 SMP일 경우에 CPU 개수만큼 일정한 메모리 공간을 할당하고 __per_cpu_stat 부터 __per_cpu_end까지 복사한다.

 

stup_per_cpu_areas();

 

SMP 머신에서는 동시에 여러 프로세서가 동작하기 때문에 자원에 대한 동기화가 매우 중요해진다.

이전에 SMP 머신에서 동기화로서 spin lock을 사용한다고 이야기하였다. 하지만 이렇게 spin lock 을 무분별하게 사용하면 spin lock을 획득한 프로세서는 작업을 계속 진행할 수 있는 반면 획득하지 못한 프로세서는 루프를 돌며 대기하게 된다. 때문에 대기하는 프로세서가 많아 지면 시스템 성능에 좋지 않은 영향을 미칠 수 있다.

이러한 현상을 예방하기 위한 방법에는 여러 가지가 있는데 그 중에 가장 간단한 방법은 각각의 프로세서가 자신이 사용하는 자원을 별도로 가지도록 하는 방법이 있다. 이렇게 각 프로세서가 자신만이 사용하는 자원을 저장해 놓은 장소가 바로 per-cpu storage 라는 곳이다.

다음으로 smp_prepare_boot_cpu() 함수가 수행된다.

smp_prepare_boot_cpu() 함수는 현재 수행하고 있는 cpu online으로 표시하는 함수이다.

 

smp_prepare_boot_cpu() ;

 

sched_init() 함수는 각 cpu run-queue를 초기화해주는 함수이다. 모든 실행중에 프로세스들은 run-queue 에 들어가게 되는데 run-queue의 구조와 스케줄링 알고리즘에 따라 스케줄링 성능이 결정되게 된다. 리눅스 2.4 까지는 run-queue list 형태로 되어 있었다. 2.6대에 와서는 priority array가 변경되어 보다 실시간성 및 성능이 향상되었다.

 

sched_init();

 

이러한 run-queue에는 SMP 머신과 NUMA 시스템을 많이 고려하고 있는데 매우 복잡한 자료 구조와 알고리즘으로 이루어져 있다. sched_init() 함수는 이러한 run-queue 의 기본 자료 구조의 초기값을 설정하는 함수이다.

preempt_disable() 함수는 preempt count 를 증가시켜 다른 taskpreemption 되는 것을 막는다. task간의 동기화를 위한 방법에는 intrerrupt disable, preempt disable 기타 동기화 객체로 크게 나뉠 수 있는데 이 중 interrupt disable 이 가장 강력한 방법이다. 하지만 interrupt disables 의 문제는 interrupt 를 막아 버림으로써 시스템의 반응속도를 떨어 뜨릴 수 있다.

 

preempt_disable();

 

때문에 간단한 동기화 같은 경우 preempt disable 을 사용하곤 하는데 preempt disable 은 간단히 말해 lock 변수를 하나 두고 lock 변수의 값이 0 이면 해당 부분 scheduling 이 발생할 수 있고 0 이 아니면 scheduling 이 발행하지 않게 하는 매커니즘이다.

rcu-init() 함수는 rcu(Raad Copy Update)라는 동기화 객체와 관련이 있는 초기화 함수이다. rcu SMP상에서 사용되는 동기화 객체로서 데이터를 변경하고자 할 때는 원본 데이터를 복사한 후 복사한 데이터를 변경하고 링크를 원본 데이터에서 끊고 변경한 데이터가 연결되게 바꾼다. 이후 원본 데이터를 아무도 참조하지 않을 때 원본 데이터를 제거하는 매커니즘이다.

 

run_init();

 

rcu_init() 함수는 현재 수행중인 cpu 에 해당하는 ruc_data rcu_bh_data를 초기화하고 관련 tasklet 을 설정하는 함수이다.

init_IRQ interrupt를 초기화하고 각 interrupt handler를 등록하는 역할을 수행한다. init_IRQ는 결국 내부적으로 arch/arm/mach-s3c2410/irq.c 에 있는 s3c24xx_init_irq()함수를 호출하게 되는데 s3c24xx_init_irq() 함수에서 s3c24xx 프로세서에 따른 interrupt 초기화를 해주게 된다.

 

init_IRQ();

 

init_timers() kernel timer를 초기화하는 함수이다. c프로그래밍을 할 때 sleep()함수등을 사용하게 되면 일정기간 프로세스가 sleep() 되었다가 깨어나는 것을 알 수 있을 것이다. 이렇게 일정기간 후에 어떻나 특정한 이벤트를 처리해주는 일이 바로 kernel timer 에서 해주는 일이다. 리눅스의 kernel timer timervector 자료구조로 되어 있고 init_timers() 함수는 timer vector 자료 구조를 초기화 해 준다. softirq_init() 함수는 soft irq 관련 초기화를 해 주는 함수로서 softirq_vectasklet_action tasklet_hi_action 을 등록해 주는 일을 한다.

 

//pid를 가지고 hash table에서 process description 을 빨리 검색하기 위해서

//hash table을 초기화

pidhash_init();

//kernel timer 를 초기화

init_timers();

//softirq_vec를 초기화

softirq_init();

//h/w timer를 초기화

time_init();

//console에 대해서 printk를 사용할 수 있도록 설정

console_init();

 

time_init() 함수는 s3c24xx ㅍ로세서의 h/w를 설정해서 clock enable 하고 time tic interval 을 설정한다. time_init() 함수는 최종적으로 arch/arm/mach-s3c2410/time.c 파일에 있는 s3c2410_timer_init() 를 호출해서 s3c24xx 프로세서의 timer를 초기화해 준다.

local_irq_enable() 함수는 arm state registar에서 i-bit 1로 설정해서 local irq 가 발생할 수 있게 한다.

mem_init() 함수는 매우 중요한 함수로써 free page를 모아 buddy system을 만드는 역할을 수행한다. 리눅스에서 사용하는 메모리 allocator 는 크게 두가지 있는데 하나는 buddy system 이고 나머지 하나는 slab allocator 이다. 이중 buddy system page 단위의 메모리를 관리할 때 사용하는 알고리즘이다.

buddy 알고리즘의 특징으로는 최대한 선형된 메모리 공간을 많이 확보한다는 것이다. 이렇게 해서 메모리의 외부 단편화를 막아준다.

 

//profile 기능이 enable 되어 있으면 profile 정보를 쌓아 둘 수 있는 buffer 를 할당

profile_init();

//local irq enable

local_irq_enable();

//directory, inode hash table 생성

vfs_caches_init_early();

//bank 사이의 공간을 free, pgdate 에 속하는 page 영역을 free시키면서 buddy로 만듦

//memory 관련 정보를 출력

mem_init();

//generalic_cache-slab 과 모든 cpu에 대해 cache-slab을 할당

kmem_cache_init();

 

kmem_cache_init()함수는 리눅스에서 사용하는 또 다른 메모리 관리 매커니즘인 slab 을 생성하는 역할을 수행한다. slab는 앞으로 사용하게 될 특정한 사이즈의 메모리 조각들을 미리 만들어 놓는 방법이다. task_struct 와 같은 자료 구조는 kernel에서 매우 많이 사용, 할당, 해체된다.

이렇게 자주 사용되고 할당과 해제가 반복되는 자료 구조들은 미리 kernel에서 만들어 놓는데 이렇게 미리 만들어진 자료구조의 묶음이 바로 slab 이라는 단위이다. 이전에 이야기한 buddy system은 외부 단편화는 막아 줄 수 있지만 내부 단편화에 대해서는 막아 줄 수가 없다. slab는 내부 단편화를 막는 역할도 수행한다.

지금까지 리눅스 kernel start_kernel 함수에 대해서 알아보았다. 최대한 축약해서 썼지만 아직 많이 남아 있다. 다음에는 마지막으로 start_kernel 의 나머지 부분에 대해서 설명하도록 하겠다.

 

 

출처 : 공개 SW 리포트 5호 페이지 56 ~ 59 발췌(2007 2) - 한국소프트웨어 진흥원 공개SW사업팀 발간

 

 

(참고) 리눅스 커널과 디바이스 드라이버 프로그래밍 연재는 다섯번째강좌 리눅스 커널의 소스 분석 까지만 연재되었음.

 


[원글링크] : https://www.linux.co.kr/home2/board/subbs/board.php?bo_table=lecture&wr_id=1651


이 글을 트위터로 보내기 이 글을 페이스북으로 보내기 이 글을 미투데이로 보내기

 
한국소프트웨어진흥원 공개SW사업팀