일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 글루민
- 자 이제 시작이야
- 핀토스 프로젝트 3
- 핀토스 프로젝트 1
- 핀토스 프로젝트 4
- 글리치
- 핀토스 프로젝트
- 파란 장미
- multi-oom
- botw
- 끝
- 아직도 실험 중
- 황금 장미
- 셋업
- Project 1
- 제발흰박
- 내일부터
- 핀토스 프로젝트 2
- 바빠지나?
- 가테
- PINTOS
- 일지 시작한 지 얼마나 됐다고
- alarm clock
- 마일섬
- 노가다
- Today
- Total
거북이의 쉼터
(2022.04.08) Kernel Abstraction (1) 본문
1. Abstraction과 OS의 관계
본격적인 이야기를 시작하기 전에 "Abstraction", 즉 추상화란 무엇인지 살펴보자. 추상화는 주어진 물체, 또는 형상의 주요하지 않아 보이는 일부 세부사항을 무시함으로서 그를 이해하기 쉽도록 하는 과정, 또는 결과물이다. 중요한 것만 남기는 가지치기라고 이해하면 될 것이다.
그러면 이러한 추상화와, OS의 역할은 무슨 관계가 있을까. OS는 프로그램에게 HW을 사용할 수 있는 유용한 인터페이스를 제공함과 동시에 HW의 지나치게 세부적인 사항을 숨기는 것에 있다. 즉 이러한 역할 자체에 추상화의 개념이 내제되어 있는 것이다. Abstraction은 OS의 3가지 역할 중 Illusionist을 이루는 데 기여한다. HW의 세부적인 사항 및 제한 사항을 숨기는 것을 추상화로 이루어낼 수 있다.
2. OS와 어플리케이션의 분리 (Protection)
OS가 일반 어플리케이션과 분리될 필요는 간단히 설명하면 user / kernel 사이의 신뢰도가 박살이 났기에 protection이 요구되기 때문이다. 더 길게 설명하자면, user 어플리케이션이 buggy, 또는 악의적인 행동을 초래할 경우 운영체제는 이로 발생하는 여파가 다른 어플리케이션이나, OS 본인에게 전파되는 것을 방지할 필요가 있기 때문이다. 이 때문에 OS는 기본적으로 어플리케이션을 신뢰하지 않도록 프로그램되어야 한다. Protection은 OS의 3가지 역할 중 Referee를 이루는데 기여한다.
3. 프로세스와 Dual Mode, 특권 명령
프로세스는 제한된 권한으로 실행되고 있는 프로그램을 지칭한다. 크게 프로세서 내에서 프로세스에 의해 연이어 진행되는 작업들을 지칭하는 쓰레드와, 해당 프로세스가 접근할 수 있는 메모리인 주소 공간으로 구성된다. 이 때문에, 프로세스는 프로세서와 메모리를 종합해 추상화한 것, 즉 abstraction of machine으로 취급할 수 있다.
이러한 프로세스는 유저 어플리케이션이 실행되고 있는 것이며, OS 커널이 유저 어플리케이션을 관리하기 위해 계속 실행되고 있어야 함을 생각하면 프로세스와 OS 커널이 동시에 메모리에 들어가 있게 된다. 만약 Protection을 고려하지 않는다면, 한 유저 프로세스에서 다른 유저 프로세스 또는 OS 메모리 공간에 접근하는 것이 가능할 것이며, 이는 보안상의 문제를 야기할 수 있다. 따라서, 일반적인 프로세스들과 OS를 분리하기 위해서는 다음의 3가지 사항을 고려할 필요가 있다.
- 어플리케이션이 중요한 명령을 실행하는 것을 방지해야 한다.
- 한 어플리케이션이 다른 어플리케이션의 메모리에 읽고 쓸 수 없도록 해야 한다.
- OS는 어플리케이션으로부터 실행 권한을 가져올 수 있어야 한다.
첫 번째, 두 번째는 나름 자명해 보인다. 마지막 문제의 경우에는 유저 프로세스가 의도치 않게, 또는 악의적으로 무한 루프를 수행하는 경우, 이를 제어하기 위해 실행 권한을 가져올 필요가 있기 때문이다.
첫 번째 문제부터 고쳐보자. 어떻게 유저 프로세스가 제한된 명령어만 실행하도록 할 수 있을까? 가장 간단한 방법으로는 지속적으로 프로세스를 감시하는 방법이 있다. 시뮬레이터 내에서 프로세스를 돌리면서, 해당 프로세스가 실행하는 명령이 허가된 명령이라면 실행하고, 아니라면 프로세스를 죽여버리는 것이다. Javascript의 기본적인 모델은 해당 방법대로 실행되는 프로세스의 명령을 검사한다. 그러나 이 방식의 단점은 무지하게 느리다는 것이다. 모든 명령어에 SW 단의 검사가 기본적으로 붙기 때문이다.
소프트웨어 단에서는 이를 해결할 방법이 마땅치 않다. OS 디자이너들이 문제에 부딪힐 때마다 괴롭히는 것이 HW 디자이너들이기 때문에 HW 단에서 이를 해결할 방법을 고안했다. 이 검사를 HW 단에서 직접할 수 있다면 더 빨라질 것이다. 실행 단계를 나누어서 특정 명령이 특정 단계에서만 실행될 수 있도록 미리 지정해둔다면, 어떤 명령이 들어왔을 때 이를 실행할 지 여부를 알 수 있다. 이러한 해결 방법을 Dual Mode Operation, 또는 단계를 고리 형태로 만든 것에서 착안하여, Ring Mode Operation이라고 부른다. Ring 0는 kernel로 가장 높은 권한을 가진 단계이며, Ring 3는 일반적인 어플리케이션이 실행되는 단계이다. hlt 같이 중요한 명령어는 Ring 0에서만 실행될 수 있도록 사전에 지정한다.
이를 앞으로는 간단히 커널 모드, 유저 모드로 칭할 것이다. 커널 모드일 때는 모든 권한을 가지고 어떤 메모리에도 접근할 수 있으며, 유저 모드에서는 제한된 권한을 가지고 모든 명령이 실행되기 전에 CPU가 이를 검사하며, 특정 메모리에만 접근할 수 있다. x86에서는 해당 모드 값을 관리하는 EFLAGS 레지스터가 있으며, 이 레지스터 값에 접근하는 것 또한 제한되어 있다. 만약 특정 프로세스가 강제로 이러한 명령을 실행하려고 하면, HW 특히 CPU가 OS에게 특정 프로세스가 이상한 명령을 실행하려고 한 위반 사실을 알려준다. 이러한 통보가 바로 exception이며, 이를 raise exception to kernel이라고 한다. 이쯤에서 제한된 명령, 즉 특권 명령 (privileged instructions)의 예시를 살펴보면,
- hlt
- 파일 시스템에 읽고 쓰기
- EFLAGS 레지스터 설정하기 (모드 변경)
- 특정 주소로 jmp
이와 같은 것들이 있다.
이 중 다른 것들은 모르겠지만, 파일 시스템에 읽고 쓰기같이 어플리케이션에서도 반드시 써야 하는 것들이 특권 명령으로 포함이 된 것을 볼 수 있다. 이러한 기능은 어플리케이션이 실행되는 유저 모드에서는 사용할 수 없기 때문에 불편함을 초래할 것이다. 그러면 어떻게 해야 할까. 어플리케이션에게는 특수한 명령인 System Call을 허용하는 것이다. System Call은 실행하게 되면 유저 모드에서 커널 모드로 모드 스위치가 일어나게 한다. 이를 통해 read/write 등 반드시 필요한 명령을 수행할 경우에 한해, 커널로 권한을 높여주는 것이다. 우리가 자주 쓰는 C 라이브러리의 printf 같은 API들도 내부적으로 보면 해당 System Call을 부르도록 구현이 되어있다.
핀토스를 보면 알겠지만 Syscall 핸들링은 커널에서 일어난다. Syscall은 유저 프로세스에서 커널에게 특정 동작을 대신 수행해주도록 요청을 보내는 것이라고 볼 수 있다. 유저 프로세스에서 Syscall을 사용하면 해당 코드가 HW 트랩을 일으키면서 커널 내부에 정의된 핸들러가 실행이 된다. 모든 요청이 완료되면 다시 유저 프로세스로 실행이 돌아간다.
System Call은 OS 3역할 중 Glue를 성취하는데 기여한다. 표준화된 방식으로 제한된 명령어를 실행하도록 하기 때문이다. 이 쯤에서 궁금한 사람이 있을 수 있다. Protection이 OS 3역할 중 Referee를 담당하는데, System Call은 Glue를 담당한다니 뭔가 이상하다고 말이다. 나도 처음에는 헷갈렸던 부분이지만 System Call 자체는 Protection이 아니다. System Call은 단지 Protection을 위해 Dual Mode와 Privileged Instruction을 만들었으나, 그로부터 파생되는 제한 사항을 극복하기 위해 부차적으로 생성된 것이기 때문이다. 따라서 이러한 구분은 valid 한 것이다.
4. Memory Protection
Protection을 위해 또 생각해야 할 것은 프로세스가 메모리에 접근하는 순간이다. 만약 허용되어서는 안되는 메모리 공간에 접근한다면? 이러한 경우를 생각하면 메모리 또한 일반 프로세스로부터 보호할 필요가 있다. 이 때문에 OS는 어플리케이션이 스크린의 버퍼 메모리에 직접적으로 접근하는 것을 허용하지 않으며, syscall 등의 간접적인 방법을 통해서만 접근할 수 있도록 하는 것이다.
비단 스크린의 버퍼 메모리 뿐만 아니라 유저 프로세스 각각의 메모리 또한 다른 프로세스로부터 보호될 필요가 있다. 이를 위해 가상화를 사용해 가상 메모리 공간을 구현한다. OS는 메모리 가상화를 통해 Protection 뿐만 아니라 각 프로세스가 각각 고유한 주소 공간을 가지고 있다는 환상을 줄 수 있다. 예를 들어 P1, P2가 둘 다 0x30000의 가상 메모리 공간을 필요로 한다면, 둘 모두에게 0x00000부터 0x30000까지의 가상 주소 공간을 사용할 수 있도록 하는 것이다. 두 프로세스 모두 동일한 주소에 접근하더라도 실제로 메모리가 담기는 물리 메모리 주소를 다르게 한다면 문제가 없다.
이러한 가상화에는 필연적으로 물리 주소로의 변환이 필요하다. 이를 위해 HW인 Memory Management Unit에서 가상 메모리에서 물리 메모리로의 변환을 제공한다. 각 프로세스는 가상 주소를 사용해 메모리에 접근하려는 시도를 하고, MMU에서 물리 주소로 변환할 때, 정당하다면 변환 후 결과를 반환하고, 그렇지 못한 주소라면 exception의 한 종류인 fault가 일어나, 커널의 fault handler로 통보된다. 만약 paging 메커니즘이 segment라면 segmentation fault가, page라면 page fault가 일어날 것이다. 이는 후에 paging을 할 때 더 자세히 볼 것이다.
당연한 말이지만, paging을 하는 명령어 또한 privileged instruction이어야 할 것이다. 만약 아니라면, 임의의 물리 주소로 가상 주소가 매핑될 수 있도록 유저 프로세스가 조작해 임의의 물리 주소에 접근하는 것이 가능할 것이기 때문이다.
5. (Timer) Interrupt
이제 Protection을 위한 마지막 필요 요소인 Interrupt를 살펴보자. 만약 OS가 제어권을 되찾기 위한 과정이 마련되어 있지 않다면 어떠한 프로세스가 무한 루프를 실행하게 되면 그 장치는 더 이상 사용할 수 없을것이다. 이러한 끔찍한 일을 방지하기 위해 OS는 제어권을 복구할 방법을 마련할 필요가 있다. 이를 위해 또다시 HW 제작자에게 가서 징징된 결과가 인터럽트이다.
CPU에서 일정 HW 시그널을 주기적으로 커널에게 보내서 이를 처리하게 한다면 실행되고 있는 유저 프로세스는 일시적으로 중단되고, 미리 구현해놓은 인터럽트 핸들러가 실행된다. 커널은 "잘" 구현되어서 무한히 실행되지 않고, 보류하는 것이 가능하기 때문에 주기적으로 커널이 실행되도록 하는 것이다. 인터럽트를 직접적으로 유저 프로세스한테 전달되도록 만들면 악의적으로 씹을 가능성이 있기 때문에 반드시 커널로 배달하도록 해야 한다.
6. 정리
주로 Protection을 살펴본 것 같다. 정리하자면, 다음과 같다.
Protection(OS 3역할 중 Referee 담당)을 위한 3가지 장치는 다음과 같다.
- Privileged Instruction
- Memory Protection
- Timer Interrupt
Dual Mode에 의한 Protection에서 User -> Kernel Mode Switch가 일어나는 순간은:
- Interrupt
- Exception
- System Call
이며, 반대로 Kernel -> User Mode Switch가 일어나는 순간은:
- 새로운 프로세스, 쓰레드의 시작
- Interrupt, Exception, Syscall로부터 반환
- Context Switch가 끝나고 프로세스, 쓰레드 실행
- User-level upcall
이다.
이 중 인터럽트는 다음 포스팅에서 더 자세히 다뤄보도록 하자.
'코딩 삽질 > OS 요약 정리' 카테고리의 다른 글
(2022.04.10) Scheduling (0) | 2022.04.10 |
---|---|
(2022.04.09) Concurrency and thread (0) | 2022.04.09 |
(2022.04.09) Programming Interface (0) | 2022.04.09 |
(2022.04.09) Kernel Abstraction (2) (0) | 2022.04.09 |
(2022.04.08) Introduction (0) | 2022.04.08 |