- 컨테이너에 대해 공부하면서 리눅스 커널에 대한 이해가 간략하게나마 필요함을 느끼게 되었고, 이에 본 시리즈를 작성합니다.
- [Linux Kernel] 시리즈는 olc.kr의 강의를 기반으로 작성되었으며, 이해가 되지 않는 부분은 여러 블로그들을 참고하였습니다. 참고한 블로그는 각 글의 하단 Reference탭에 명시하였습니다.
운영체제란 ?
- 운영체제: 하드웨어 자원들을 관리하고 프로그램들을 지원해주는 시스템
- 운영체제의 목표
- 사용자와 하드웨어 사이에서 중개자 역할을 함
- 사용자가 각 하드웨어의 동작을 이해할 필요가 없이 OS가 알아서 이를 처리
- 하드웨어를 효율적으로 사용할 수 있도록 함
프로그램이란 ?
- 프로그래밍 언어로 코딩을 할 때, 다양한 함수들과
main()
을 만들고, 이들을 컴파일 하면 바이너리가 나옴. 우리는 이것을 프로그램이라고 부름.
- 그렇다면 왜 대부분의 프로그램들은 분리되어 있는가?
→ 프로그램들이 분리되어 있지 않고, 하나의 커다란 프로그램으로 운영될 경우 비효율성이 발생
- 거대한 프로그램은 사용하지 않을 프로그램까지 모두 실행하기 때문에 부팅 시간, 메모리 사용 등에 있어서 심각한 비효율성을 초래함
- 위와 같은 이유로 리눅스도 OS를 하나의 커다란 프로그램으로 만든 것이 아니라 Kernel, Shell, Utility 등의 프로그램으로 분할해놓은 것임
프로그램 vs 프로세스
- 프로그램은 보조 기억 장치에서 실행되기만을 기다리는 정적인 데이터의 집합임
- 프로그램이 메모리에 적재되면, 프로세스가 됨. 즉, 프로세스란 실행 중인 프로그램을 뜻함
Kernel, Utility, Shell, File
- Kernel
- Memory resident한 독립된 C 프로그램
- ** Memory resident: 부팅되고나서부터 꺼질 때까지 항상 메모리에 상주하고 있는 것
- 커널이 아닌 다른 프로그램들은 Memory resident한 특성을 가지지 않음 → 대신 수요에 의해 load되는 Disk resident한 특성을 가짐
- Utility
- Disk resident한 C 프로그램
- Disk resident하기 때문에 항상 현 주소가 disk이며,
- 사용자가 요청하면 메모리에 올라옴.
- 따라서 Utility는 command(job)라고도 부림
- Shell
- 많은 프로그램들(Utility)들이 disk로부터 언제 올라오고, 언제 내려가는지 등을 컨트롤하는 프로그램(A special utility)
- 한 마디로 표현하면 Job(command) Control을 담당함
- File
- 유닉스에서 파일은 Sequence of bytes를 의미함. 즉, 바이트들의 배열을 의미.
- 모든 함수와 명령어들은 결국 기계어로 해석하면 0과 1의 나열에 불과함
- 유닉스에서는 모든 것이 file임
커널-쉘-유틸리티의 관계
- 가장 처음 컴퓨터를 키면, 부팅이 되며 Kernel(
a.out
)이 메모리에 올라오게 됨
- 부팅이 완료되고 user들이 터미널을 키면, 그 터미널 위에서 shell이란 프로그램이 (메인 메모리에) 올라옴
- 이후 유저가 키보드로 커맨드를 입력하기를 기다림
- 커맨드가 입력되면, shell은 커맨드에 대응하는 Utility를 디스크로부터 가져와서 실행시킴
- 위 그림처럼, 만약 User B가 ppt를 실행시키라고 명령을 내리면 쉘이 child process를 생성해서 ppt를 실행시켜주게 됨
Linux vs Window
Resource Usage 관련
- 윈도우는 Single-user 시스템이고, 리눅스는 Multi-user 시스템임.
- 따라서 리눅스에서는 여러 사람이 동시에 리소스를 사용하기 때문에 최대한 효율있게 사용하여야 함
Protection 관련
- Protection : 리눅스에는 여러 사람의 정보가 같이 있기 떄문에 Protection을 더 잘 신경써야함
- 이러한 보안 문제를 해결하기 위해 리눅스에서는 사전방지(prevent)에 많은 노력을 기울이게 됨
- 리눅스의 사전방지(prevent) 시스템은 다음과 같다.
- 사용자가 I/O 작업을 할 땐 반드시 커널에 요청하는 방식으로 진행할 수 있음
- 이때 커널이 요청을 받게 되면, 커널이 가지고 있는 function으로 I/O를 해주는데, 무조건적으로 해주는 것이 아니라, 그 I/O가 정상적인 것인지 검사를 하고 난 뒤에 해주게 됨
- 즉, 커널에 요청이 들어오면, 커널 내 function을 이용해서 I/O를 진행하는데, 이러한 요청을 바로 System call이라고 부름
Binary bit mode (for System call)
- 리눅스는 system call을 위해 하드웨어(CPU)에 Binary bit mode라는 개념을 도입합.
- cpu에는 바이너리 비트인 모드 비트가 있으며, 이것은 0 또는 1의 값을 가짐.
- 만약 모드 비트가 kernel mode로 세팅되어 있으면, cpu는 어떠한 메모리 영역도 접근할 수 있으며, 어떠한 명령도 다 수행할 수 있음
- 하지만 user mode로 되어 있으면 cpu는 내 주소 영역에만 접근 가능하며 제한적인 명령어 수행(로컬한 영역)이 가능함.
리눅스 CPU 명령어 처리 과정
- CPU 명령어 사이클이 돌면서 PC 레지스터에서 인출된 명령어의 주소. 즉, 수행해야 할 명령의 주소를 먼저 메모리에게 보냄
- 메모리는 해당 주소를 받게되고, 이것이 IR 레지스터에 들어가게 됨. 즉 IR(Instruction Register) 레지스터에 이번에 수행해야 할 명령어가 오게 됨
- MAR(Memory Address Register)는 MAR이 읽어들인 주소에 해당하는 데이터를 MBR(Memory Buffer Register)에 담아 IR에 보냄
- 명령어는 그림 우측 하단처럼 opcode + operand로 구성됨
- op-code에는 수행해야 할 명령어가 있으며
- operands에는 메모리 주소가 담겨져 있음
- IR 레지스터에 있는 명령어의 op-code를 들여다보고, operands 부분에 들어있는 주소에 해당하는 메모리 주소에 담겨 있는 값을 메모리에서 또 가져온 뒤, 실제 연산이 수행됨
리눅스 CPU 명령어 처리 과정에서의 모드 비트 검사
CPU 명령어 처리 과정을 다시 보면, 레지스터에서 메모리로 수행할 명령어의 주소를 보낼 때
- 당시 mode bit가User 모드라면, CPU와 메모리 사이(요즘에는 주로 CPU)에 존재하는 MMU(Memory Management Unit) 하드웨어가 CPU에서 메모리에 연결되어 있는 버스를 살펴보면서, 전송되는 address를 체킹함
- 만약 요청한 명령어의 address가 자신의 로컬 범위 밖에 위치한다면, 요청한 명령어 주소를 얻지 못하고 끝나버림
- 만약 정상적으로 명령어를 가져왔다면(Instruction Fetch), Decode 단계에서 op-code를 보고 어떠한 역할인지를 파악하는데, 만약 Privileged op-code(중요한 역할을 하는 실행)를 시도하려 한다면 그 순간 CPU를 뻇겨버림
- 물론 Kernel 모드라면 위와 같은 검증 절차를 전혀 밟지 않는다.
그렇다면, User가 파일을 읽고 쓰는 로직을 구현하려 한다고 해보자. 내 프로세스는 User Mode이고, 커널이 아닌데 어떻게 I/O를 할 수 있을까?
User가 I/O를 하는 방법
User가 파일을 읽는 로직을 코딩 해 놓고 실제 컴파일을 하고나서 바이너리르 보면, 그 안에는 I/O statement가 없음.
- 즉 소스코드에서만 개발자가 입출력을 관리하는 것처럼 보일 뿐 실제로는 System Call을 통해 커널이 가지고 있는 function을 호출하는 것임
개발자가 I/O를 담은 소스코드를 컴파일한다면, 컴파일러가 해당 부분들에 chmodk(change CPU protection mode to Kernel)이라는 function이 수행될 수 있게끔 만들어줌. 이를 통해 CPU의 모드비트를 바꾸고, I/O 명령을 수행하게 됨.
- 이때, 커널의 fuction을 호출한다는 건 커널에게 부탁을 하고, 커널이 해당 함수를 수행해주는 개념임. 즉, System call을 의미
chmodk가 수행되면, 하드웨어는 다음과 같은 동작을 수행함
- chmodk는 privileged 명령임.
- 따라서 user 프로그램은 이를 수행하지 못하고, CPU (권한)을 뻇겨버림 → 이를 trap에 걸린다고 표현함
- trap이 될 때(즉, system call을 통해 커널에게 요청을 할 때) 요청 명령이 어떤 처리(read, write, open, close)인지를 알려줄 파라미터가 함께 trap handler로 들어가게 됨
- trap에 걸리고 나면 mode bit가 커널 모드로 바뀌게 되며, trap handler 루틴(트랩을 처리하는 루틴) 이 수행되게 됨
이후 소프트웨어에서는 다음과 같은 동작이 수행됨
- 유저가 해당 디스크나 메모리 영역에 권한(read, write, execute)이 있는지를 확인
- 정상적인 요청이면, 커널에서 I/O를 수행하고, 다시 trap을 통해 사용자 프로그램에게 정보를 넘겨줌
모든 프로세스는 두 개의 stack을 가지게 됨
- user-mode에서 수행될때 유저의 프로그램안에 있는 함수의 로컬 변수를 계속해서 호출해야하니까 stack이 필요
- kernel-mode에서도 커널 안에 있는 함수의 로컬 변수를 계속 호출해야 하기 떄문에 stack이 필요
- 결국 모든 프로세스는 user stack, kernel stack 2개를 포함
위 과정을 정리하면 다음과 같다.
- 프로그램이 수행되면, 위 그림과 같이 user-mode와 kernel-mode가 번갈아가며 수행되는 것을 알 수 있다.
- 유저모드가 자신의 코드를 수행하다가 커널에게 부탁할 일이 생기면 System call을 함.
- 커널모드로 집입하여
Kernel a.out
이 진행되며 커널이 일을 처리한 후 다시 유저모드로 돌아와서 작업을 진행
- 위 과정을 프로그램이 종료할 때까지 반복
Reference