반응형
출처 : http://kelp.or.kr/korweblog/stories.php?story=02/02/09/1824065&topic=29 이규명님의 강좌

Linux 시스템의 부팅과정의 이해

시스템의 부팅이라는 과정은 시스템에 전원이 인가된 상태 또는 reset 버튼을 누르고 난 상태에서 시스템이 완전히 동작 가능한 상태까지 이르는 과정을 이야기합니다.(Warm booting의 경우를 제외하도록 하죠.) PC 기반의 Linux 시스템의 경우 login 메시지를 볼 때까지를 의미하고 있습니다.
 Embedded Linux 시스템도 당연히 부팅이라는 과정을 거쳐야 합니다. 전체적인 부팅과정은 bootloader를 수행하고 linux kernel을 수행하고 kernel위에서 user application들이 동작하는 순이 됩니다. 그러면 bootloader와 linux kernel, user application이 어떠한 방법으로 연결되고 각각이 무슨 일을 하고있는지를 알아보도록 하겠습니다.

이야기에 앞서 이 글에서는 특별한 architecture에 기반하지 않은 일반적인 이야기임을 알아주셨으면 합니다. 예를 들
어 PC의 BIOS 코드가 어느 address에 위치하고 어떤 address에서 external ROM을 찾고 하는 구체적인 내용은 
전혀 없습니다. 

--- BootLoader
CPU가 reset 신호를 받고 나면 Bootrom이라고 불리우는 ROM에서 instruction 읽어와서 수행을 시작합니다. 이 
bootrom에 보통 bootloader가 존재합니다. Bootloader가 하는 일은 매우 간단합니다. 우선 시스템이 동작하기 위해
서 꼭 필요한 하드웨어들을 초기화하고 bootloader 자신이 써야 할 하드웨어를 초기화 하게됩니다. 대부분의 시스템에 존재하는 SDRAM의 크기를 알아보고 SDRAM Controller를 프로그램하여 CPU가 SDRAM에 접근가능 하도록 만듭니다.(보통 bootrom이 자기자신을 SDRAM에 복사하여 수행되기도 합니다.) 그리고 나머지 UART나 Ethernet controller등 bootloader에서 사용하는 하드웨어에 대한 초기화를 수행합니다.
 그 다음은 linux kernel을 가져와 메모리의 지정된 위치에 복사합니다. 당연히 linux kernel을 가져오는 방법
은 여러 가지가 있을 수 있습니다. Serial cable로 download 받기도 하고, tftp등을 이용하여 network으로 
download받을 수도 있으며, disk로부터 읽어올 수 있습니다.
 그 다음은 linux kernel로 jump(Program Counter의 값을 바꾸는 instruction을 의미하겠죠?)하면 bootloader가 하는 일은 끝납니다.
 보통은 jump 하기 전에 bootloader가 쓰고 있던 하드웨어를 다시 reset 상태로 만들어주어 interrupt가 발생하지 않도록 합니다.
 Linux kernel의 경우 argument를 받을 수 있으므로 argument를 유저에게 입력 받아 kernel에 넘겨주는 일도 수행합니다. PC의 경우 BIOS라고 불리우는 bootrom이 존재합니다. 이 녀석도 위와 같이 mainboard에 있는 하드웨어들을 초기화 시키고 POST라고 하여 하드웨어가 제대로 동작하고 있는지 Test를 수행한 다음 disk에 있는 MBR이라는 특정 부분을 읽어서 메모리에 복사해 놓습니다. 그 다음 그 메모리 영역으로 jump하여 제어권을 넘기죠. 일반적인 PC 기반의 Linux 시스템에서는 MBR이라는 곳에 있는 내용은 LILO라고 하는 OS Loader입니다. LILO가 linux kernel을 disk로부터 읽어서 메모리에 복사해 놓고 jump하게 됩니다. 따라서 embedded 시스템에서 이야기하는 bootloader라는 것이 PC에서는 BIOS와 LILO의 협동으로 구현되고 있다고 볼 수 있습니다.
 재미있는 것은 Linux kernel은 시스템에 SDRAM이 얼마나 있는지 알지 못합니다. 특정 embedded 시스템의 경우는 예외겠지만 PC의 경우는 사용자가 원하는 만큼의 SDRAM을 증설할 수 있습니다. Bootloader의 경우 board에 존재하는 hardware를 모두 알고 있으므로 SDRAM의 사이즈를 알 수 있고 그것을 어떠한 방법을 통해 kernel에 알려줄 필요가 있습니다. 예를 들어 메모리의 특정 address에 size를 적어놓는 방법도 있을 수 있고, software trap을 이용하는 방법
도 존재할 수 있습니다. 아무튼 kernel에서 SDRAM의 크기를 알 수 있는 interface를 bootloader는 제공해야 합니다. 
실제로는 SDRAM의 크기 뿐아니라 Interrupt routing에 관련된 부분이나 PCI configuration space read/write를 
할 수 있는 interface 등 여러 가지 내용을 kernel에 알려줘야 합니다.
 Embedded 시스템의 특성상 linux kernel을 올리는 사람이 board를 알 수 있으므로 linux kernel의 arch/*/ 및에 있는 파일들을 수정하여 직접 구현할 수도 있습니다. 구체적인 내용은 architecture dependent이므로 더 이상 언급하지 않겠습니다.

--- Linux kernel
 Linux kernel이 무엇을 수행하는 녀석인지는 설명 드리지 않겠습니다. OS kernel의 수행이 무엇을 의미하는지를 다루
는 것은 너무나도 방대하기 때문에 여기서 말씀드릴 내용이아니라고 생각됩니다.
 Linux kernel이 어디서부터 수행되고 결국에는 어떻게 끝나는지(실제로는 시스템의 작동이 멈출 때까지는 끝나지는 않죠)만 간략히 말씀 드립니다. (소스 코드에 대한 자세한 분석은 다른 자료들이 있을 겁니다.)
 압축되지 않은 Linux kernel의 경우 보통 arch/*/kernel/head.S에 entry point가 있습니다.(압축되어 있는 linux kernel의 경우 보통 arch/*/boot/../head.S에 entry point가 있습니다.
 그 entry point부터 수행되는 코드는 압축된 image를 메모리에 풀고 다시 압축되지 않는 kernel의 entry point로 jump하는 작업을 수행하게 됩니다.) 그 곳부터 수행이 시작되어 architecture와 관련 있는 hardware를 초기화하고 C 코드가 수행될 수 있도록 하는 작업을 합니다.(보통은 Stack영역을 확보하고 BSS 섹션을 0로 초기화합니다.)
 그 다음은 init/main.c에 있는 start_kernel() 함수를 호출합니다.
 start_kernel() 함수는 다시 arch_init, trap_init 등의 architecture dependent한 초기화 함수를 부르고 linux 
kernel의 각 구성부분에 대한 초기화 함수를 호출합니다. start_kernel() 함수의 맨 마지막 부분에서 드디어 kernel_thread(init, …)함수로 PID가 1인 kernel thread를 하나 만들고 start_kernel() 함수 자체는 idle task가 됩니다.
 init kernel thread는 root를 mount하고 다음과 같은 코드로 끝이 납니다.(다른 kernel thread를 만들고 initrd와 
관련된 재미있는 부분이 있습니다만…)

if (open("/dev/console", O_RDWR, 0) < 0)
printk("Warning: unable to open an initial console.\n");

(void) dup(0);
(void) dup(0);

if (execute_command)
execve(execute_command,argv_init,envp_init);
execve("/sbin/init",argv_init,envp_init);
execve("/etc/init",argv_init,envp_init);
execve("/bin/init",argv_init,envp_init);
execve("/bin/sh",argv_init,envp_init);
panic("No init found. Try passing init= option to kernel.");

 코드 설명을 간단하게 해드리면 우선 /dev/console을 open합니다.(file descriptor 0에 열렸을 겁니다.)
 그 다음 dup system call을 이용하여
 file descriptor 1번과 file descriptor 2번을 똑 같은 device(/dev/console)로 만들어 줍니다.
 당연히 file descriptor 0,1,2는 각각 standard input, output, error을 위해 사용되게 됩니다.
 그 다음 execute_command라고 하는 kernel argument(init=…로 주는 argument입니다.)가 있으면 그
것을 exec하고 없으면 /sbin/init, /etc/init, /bin/init, /bin/sh 순으로 exec을 시도합니다.
 하나라도 성공하면 PID 1번으로 그 프로세스가 동작하겠죠.
 모두 실패하면 kernel panic이 납니다. (open, dup, exec등의 system call을 kernel 레벨에서 사용한다니 재미있습니다.
 kernel 레벨에서 사용 가능한 system call은 include/asm/unistd.h에서 살펴보실 수 있습니다.

보통의 PC기반 linux 시스템의 경우 /sbin/init이 exec될 겁니다. 그 프로그램이 바로 system V init 라는 프로그램
인데 user 레벨 초기화(IP address setting등)를 수행하고 데몬들을 부르는 여러 가지 shell script를 fork하고 
getty를 fork하여 login메시지를 뿌리도록 하는 녀석입니다. 기회가 되면 다음에 살펴보도록 하겠습니다.




Linux 시스템의 부팅과정의 이해(2)

 지난 글에서 bootloader와 kernel까지의 부팅과정을 알아봤습니다. 
 이제는 kernel의 맨 마지막에서 exec하는 /sbin/init이란 녀석을 알아보겠습니다.
 지난 글에서 이야기했듯이 이 프로그램의 이름은 system V init(이하 init)이랍니다. 이 녀석이 어떤 일을 하는지에 대
해서 대략적으로 알아보도록 하겠습니다. 자세한 내용은 직접 이 프로그램의 소스코드를 살펴보시기 바랍니다.

-- System V init이 하는 일
 init이 하는 일은 의외로 간단합니다. 한마디로 말씀 드리면 DOS시절의 자동 실행 배치 파일(autoexec.bat)과 같은 일을 수행한다고 생각하시면 편합니다. (물론 autoexec.bat 보다는 훨씬 복잡하고 다양한 일을 하지만…)
 init은 우선 utmp, wtmp 파일을 만듭니다. 이 파일의 내용은 사용자 login 내용이 log되는 파일이라는 합니다.(자세한 내용은 man utmp를 통해 보시기 바랍니다.) 파일들을 만들고 나서는 /etc/inittab(이하 inittab) 파일의 내용에 따라 순서대로 수행을 하게 됩니다. inittab 파일의 내용에 따라서 수행되는 내용이 다르므로 이 파일에 대해서는 자세히 알아볼 필요가 있습니다.
 알아 보기에 앞서 run level이라는 것을 알아야 합니다. Run level은 init이 수행할 여러 가지 서비스를 그룹으로 묶기 위한 숫자라고 이해하시면 됩니다.
 예를 들어 1번 run level에서는 A1, B1과 같은 서비스를 수행하고 2번 run level에서는 A2, B2와 같은 서비스를 수행하는 식으로 숫자로 서비스들을 나타내기 위한 약속입니다.(이해가 잘 안되더라도 꾹 참고 보시면…)
 보통 run level은 0에서부터 6까지의 숫자로 나타내고 Redhat linux에서는 다음과 같은 관행을 따릅니다.(적어도 6.1까지는…)
RunLevel 0 : halt(시스템 종료와 관계된 서비스)
RunLevel 1 : Single User mode(단일 사용자 모드)
RunLevel 2 : Multiuser without NFS(NFS 서비를 제외한 다중 사용자 모드)
RunLevel 3 : Multiuser(다중 사용자 모드)
RunLevel 4 : 사용되지 않음
RunLevel 5 : X11(X11을 이용한 login 화면을 내보내는 다중 사용자 모드)
RunLevel 6 : Reboot(리부팅과 관련된 서비스)

inittab 파일의 내용은 name:level:action:command 라는 형식의 줄의 연속입니다. (물론 #으로 시작되는 comment를 제외합니다.) 각각의 항을 설명드리겠습니다.

name : 그냥 이름입니다. 아무 의미가 없이 /etc/inittab 파일의 다른 줄과 구별을 위해 쓰이는 이름입니다. 보통은 매우 짧은(<=4) character string입니다.
command : 수행할 명령입니다.
level : command를 수행할 run level을 나타냅니다.
action : command를 수행할 때를 나타내는 지시어입니다.
그럼 제가 가지고 있는 Redhat 기반의 linux machine의 inittab 파일의 내용을 살펴보겠습니다.

id:3:initdefault:
=> 시스템의 부팅시에 default runlevel을 3으로 정한다는 뜻입니다. 현재 대부분의 linux distribution의 default runlevel값은 5일 겁니다.

l0:0:wait:/etc/rc.d/rc 0
l1:1:wait:/etc/rc.d/rc 1
l2:2:wait:/etc/rc.d/rc 2
l3:3:wait:/etc/rc.d/rc 3
l4:4:wait:/etc/rc.d/rc 4
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6
=> wait지시어는 각 runlevel에 진입했을 때 한번 수행하라는 뜻을 가집니다.
 /etc/rc.d/rc runlevel을 수행하도록 되어 있습니다. /etc/rc.d/rc는 shell script인데 내용을 살펴보시면 network를 start/stop시키고 internet daemon등의 daemon등을 start/stop 시키는 다른 shell script를 호출하는 것으
로 되어 있습니다. 이 스크립트가 수행되면서 Starting XXXX services ……. [OK]란 메시지가 콘솔에 나오게 됩니
다. default run level이 3이므로 부팅할 때는 /etc/rc.d/rc 3으로 수행됩니다.

ca::ctrlaltdel:/sbin/shutdown -t3 -r now
=> 모든 runlevel에서 crtl+alt+del이 눌렸을 경우 /sbin/shutdown -t3 -r now를 수행하도록 합니다.

1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6
=> 2,3,4,5 run level에서 /sbin/mingetty를 수행하도록 합니다. 
getty는 화면(정확하게는 tty)에 /etc/issue파일의 내용을 찍고 "login:"메시지를 출력하고 입력을 기다리는 프로그램
입니다. mingetty는 getty의 한 종류일 것으로 추측됩니다.

Respawn이란 action 지시어는 command의 프로세스가 종료되면 다시 command를 수행하라는 뜻입니다. getty로 이야기를 해보면 login -> bash로 계속 exec되다가 bash이 exit로 종료되면 getty가 다시 수행되어 화면에 "login:" 메시지가 찍히게 됩니다.

x:5:respawn:/etc/X11/prefdm ?nodaemon
=> 5번 run level에서 /etc/X11/prefdm을 respawn의 방법으로 수행합니다. 이 녀석이 바로 graphic을 이용한 login화면을 내보내는 프로그램일 겁니다. 당연히 default run level이 3이므로 부팅할 때는 수행되지 않습니다.

기타 등등 여러 줄이 있으나 생략하고 각자의 linux 시스템에 있는 /etc/inittab을 직접 살펴보시길 권합니다. 그리고 시스템이 부팅될 때 수행되는 daemon들이 어떤 script에서 수행되고 있는지 귀찮지만 계속 따라가 보시면 재미 있을 겁니다. (symbolic link를 이용해서 여러 가지 shell script가 reference될 겁니다.)

그리고 알아두면 좋은 내용으로 현재의 시스템 run level을 utmp파일을 참조해서 알아내주는 'runlevel'이라는 명령이 있습니다.(보통은 /sbin 디렉토리에 있으므로 PATH가 안잡혀 있으면 /sbin/runlevel로 수행하면 됩니다.) 그리고 shutdown, halt, reboot 과 같은 명령도 보통의 경우는 init에게 signal을 보내 현재의 run level을 바꾸라고 알려주는 프로그램일 뿐입니다.(물론 각각이 하는 일이 따로 있습니다.) 또한 init이 PID가 1이 아닌 상황에서 수행되
면 run level을 바꾸는 용도로 사용할 수 있다고 합니다.

그리고 PID가 1인 init은 Unix 환경에서 zombie process를 처리할 때도 이용했습니다.(linux에서도 그런지는 살펴봐야 겠습니다.) 더 자세한 내용을 이야기하면 "초보자 코너"에 알맞은 내용도 아니고
……



Linux 시스템의 부팅과정의 이해(3)

수박 겉핥기식이지만 Linux 시스템의 부팅과정을 모두 살펴 보았습니다. 살펴본 내용으로 우리가 무엇을 할 수 있고 어떤 에러에 대처할 수 있는 지 알아볼 차례입니다.

-- 어떤 에러가 날 수 있는가?
 당연히 bootrom, kernel, user program(init) 모두에서 에러가 발생할 수 있습니다. bootrom과 kernel에서의 에러는 system dependent하기 때문에 뭐라 꼭 집어 말씀드릴 수 없습니다. 
 system dependent인 부분은 CPU architecture에 따라서 다르고 CPU가 같다고 하더라도 address map은 board 설계에 따라서 매우 달라질 수 있기 때문에 말씀 드리기 곤란하고 여기서는 질문/답변 코너에 자주 올라오는 에러 메시지 2개에 대해서만 말씀 드리도록 하겠습니다.

(a) "No init found. Try passing init= option to kernel."
 linux kernel에서 나는 에러 메시지로 부팅과정(1)에서 알아본 바와 같이 여러 번의 exec시도를 실패했을 때 나오는 메시지입니다. 
부팅과정(1)에서 살펴 본 바와 같이 kernel은 마지막(?) 부분에서 "init=" kernel argument로 넘어온 실행 program, /sbin/init, /etc/init, /bin/init, /bin/sh 순으로 exec을 시도합니다. exec을 모두 실패하면 위와 같은 메시지를 내보내는 panic()함수를 부르게 됩니다.
 exec이 실패하는 경우는 우선 program이 존재하지 않을 때, program과 같이 loading되는 shared object가 존재하지 않을 때, program과 shared object의 permission이 올바르지 않을 때 등입니다. 그것을 check하고 제대로 된 root file system을 만들어주면 해결됩니다.

(b) "Id "xxx" respawning too fast: disabled for x minutes"
 이 메시지는 init(system V init)에서 내보내는 에러 메시지입니다. 
 /etc/inittab에서 action이 respawn인 줄에서 실행하는 command가 respawn이 매우 빠르게 되고 있다는 말입니다. respawn action은 command가 수행되어 process가 생기면 그 process가 죽었을 때 다시 command를 수행하라는 말을 나타내는 것이므로 respawn이 매우 빠르게 된다는 말은 command를 수행하여 생기는 process가 너무 빨리 죽는다는 말이 되겠습니다. 그럼 "왜 빨리 죽을까?"만 알아보면 해결은 쉽겠습니다. 그렇다면 shell상에서 직접 그 command를 수행해 보고 그것이 제대로 수행되고 있는지 살펴 보시길 바랍니다. 제대로 수행되고 종료되는 command라면 그것을 respawn action으로 정하지 마십시오. 그런 경우는 respawn action을 하지 말고 다른 방법을 강구하는 것이 좋을 듯 싶습니다. command가 수행되지 못하고 에러 메시지를 뿌리고 죽으면 그것에 해당하는 에러를 고쳐야 합니다. 또한 shell상에서 command의 수행 자체가 이루어지지 않는다면 "No init .."문제처럼 exec을 제대로 못하는 것이므로 program의 존재 유무, shared object의 존재 유무, program과 shared object의 permission등을 체크해 보고 정확하게 root file system을 다시 구성하셔야 합니다.

-- 무엇을 할 수 있나?
 개발 과정 중에 있는 embedded linux 시스템은 상당히 flexible하게 구성되었을 겁니다. bootrom은 kernel과 ramdisk의 download를 serial 또는 ethernet 등을 통해서 하게 되고(ethernet이 있는데도 개발 과정에서 ramdisk를 serial로 download하시는 분들이 있으면 특별한 이유가 없는 한 NFS root file system을 이용할 것을 권해드립니다.) linux 시스템이 부팅을 끝내면 getty를 수행하여 user의 login을 허용하여 여러 명이 동시에 개발을 했을 수도 있습니다. 개발이 끝나면 flash나 ROM등의 시스템 자원을 낭비하면 안되므로 필요 없는 부분은 전부 없애야 합니다.
 bootrom의 경우 serial 또는 ethernet 등을 이용하는 kernel download하는 부분 등은 모두 없어져야 하겠습니다.(debug목적을 위해 꼭 필요한 부분이 아니면…) 시스템의 hardware를 초기화하는 부분과 flash나 기타 저장장치에서 kernel과 ramdisk를 loading하는 루틴만 남아있으면 족합니다. linux kernel은 당연히 device driver들을 포함하여 최소한으로 configuration하여 크기를 줄이면 됩니다. kernel image를 ELF형식 보다는 bin형식으로 바꾸거나 하는 일도 할 수 있습니다만 여기서는 관련된 내용이 아니므로 말씀드리지 않겠습니다.
 Ramdisk는 당연히 꼭 필요한 파일들만으로 구성하면 됩니다.

Embedded 시스템 대부분에서 특정 user application은 부팅과 동시에 수행이 되어야 합니다. 그런 application은 /etc/inittab에 등록하거나 이미 등록된 shell script에서 실행하도록 바꾸어서 자동으로 수행하도록 만들 수 있습니다. 굳이 system V init을 이용하기 싫다면 init을 없애고 자동으로 수행되는 user application이 init이 해야 하고 시스템에 필요한 일(utmp, wtmp생성 및 몇 가지 signal handler 등록 등, 필요한 경우에만..)을 대신 처리하고 그 user application이 kernel이 끝나자 마자 수행되도록 하면 됩니다. kernel이 끝나자 마자 수행되려면 당연히 "init=" kernel argument를 이용하던지 아니면 kernel 소스의 exec부분을 고쳐서 user application이 바로 exec되도록 하면 됩니다. 단 그렇게 한 경우 그 user application은 시스템의 동작이 멈출 때까지 종료되면 안됩니다. PID가 1인 process가 죽으면 kernel에서 뭐라고 투덜될 겁니다.
반응형

'Linux > Linux 일반' 카테고리의 다른 글

gcc 이야기  (0) 2009.02.07
SDL 컴파일 방법, directFB  (0) 2009.02.07
Ubuntu 7.10 에서 프레임버퍼 콘솔 사용하기  (0) 2009.02.07
Posted by Real_G