06.CPU-mechanisms : 제한적 직접 실행 원리
# 시작하며
여러 작업들이 동시에 실행되는 것처럼 보이게 하기 위해서 CPU를 가상화 해야한다. 이렇게 가상화된 CPU들은 동시에 실행되는 것처럼 보이지만 실제로는 물리적인 CPU를 공유하고 있는 것이다. 즉 돌아가면서 쓰고 있다는 것이다. 잠깐씩 실행시키면서 번걸아 쓰는 이러한 방식을 시분할방식(Time sharing)이라고 했었다. 이렇게 시분할방식을 사용하면 두가지 문제가 생긴다.
첫번째는 성능에 관한 이슈이다. 나눠쓴다는 것은 하나를 계속 쓸때보다는 성능 저하가 일어날 수 밖에 없다. 어떻게하면 과중한 오버헤드를 주지 않고 가상화를 구현 할 수 있을까?
두번째는 제어문제이다. CPU를 나눠쓰끼 때문에 제어를 명확히 하지 않으면, A프로세스가 B프로세스에 영향을 끼칠 수 있다. 또는 A 프로세스가 영원히 장악하여 B프로세스를 영영 실행시키지 않 을 수 있다.
이번 장에서는 어떻게 하면 이러한 것들을 극복 할 수 있는지 메커니즘에 대해서 알아보도록 한다.
# 기본 원리 : 제한적 직접 실행(Limited Direct Execution)
프로그램을 빠르게 실행하기 위해 제한적 직접실행이라는 기법을 개발했다. 그렇다면 제한 없는 직접실행이 뭔지 알고 넘어가고 거기서는 무슨 문제가 발생했기 때문에 제한적 직접실행을 하게 된 것일까?
## 제한없는(?) 직접실행
간단히 프로그램을 CPU상에서 "직접 실행" 시키는 것이다.
위 그림과 같다. 모든 준비를 운영체제에서 마치고 main()으로 분기시키고 프로세스가 실행된다. 그리고 실행이 끝나면 return 하게 되고 운영체제가 해당 프로세스를 메모리에서 제거시키면서 프로그램을 끝내게 된다. 그러나 이 방법은 CPU를 가상화 하는데 있어서 몇가지 문제점을 나타낸다.
### 문제점
첫번째는 프로그램을 직접 실행하게 된다면, 프로그램(프로세스)가 운영체제가 원하지 않는 일 또는 해서는 안되는 일을 하는 것을 어떻게 감시하고 못하게 할 것인가에 대한 문제이다.
두번째는 시분할(time sharing) 기법을 어떻게 구현할 수 있느냐는 것이다. 그러기 위해서는 프로그램의 실행을 중단하고 다른 프로세스로 전환해야하기 때문이다.
# 1. 제한된 연산 -> 프로세스에게 제한적인 권한을 주기
직접 실행은 말처럼 직접실행하게 된다. CPU에서 직접실행한다는 뜻인데 당연히 빠를 수 밖에 없다. 그렇다면 여기서 문제가 발생한다. CPU에서 직접연산을 하기 때문에 디스크 입출력 요청이나 CPU 또는 메모리와 같은 시스템 자원에대한 추가할당과 같은 요청이 들어오는 경우이다.
프로세스가 원하는 대로 할 수 있게 방치하는 방안을 떠올리겠지만, 모두가 그렇듯 적절하지 않은 것이다. 디스크 입출력에 대해서 제한하지 않으면 프로세스는 내 맘대로 전체 디스크를 읽고 쓸 수 있기 때문이다.
그렇기 때문에 사용자(user mode)와 커널모드(kernel mode)라는 개념을 도입한다. 말 처럼 사용자 모드(user mode)에서 실행되는 코드들은 할 수 있는 동작을 제한한다. 입출력 요청을 못하도록 제한하고, 만약 그러한 요청이 들어올 경우 예외를 발생시키는 것이다. 커널모드(kernel mode)는 특수한 명령어 I/O 요청든 모든 작업들을 수행 하는 권한을 가지고 있다.
사용자 모드에서는 특정 요청이나 접근을 제한했지만, 모든 프로그램들에서는 그러한 요청이 존재한다. 그렇다면 이 방법을 해결하기 위해 시스템콜을 이용한다. 앞선 포스팅에서 fork()나 exec()와 같은 명령어들이 시스템 콜이라고 했었다. 시스템 콜을 실행하기 위해 프로그램은 trap 명령어를 실행해야 한다. 이 명령어는 유저 -> 커널안으로 분기하면서 모드 또한 커널모드로 조정시킨다. 이렇게 되면 운영체제는 모든 명령어를 처리 할 수 있게 되는 것이다. 그리고 요청했던 행동이 끝나면 다시 유저 모드로 돌아가기 위해 return-from-trap 명령어를 호출하여 호출했던 사용자 프로그램으로 돌아간다.
## trap이 될 때
### 레지스터와 플래그는 어떻게?
이렇게 trap이 된다는 것은 사용하고 있던 프로세스가 바뀐다는 의미가 같다. 즉, cpu는 현재 실행중이던 프로세스를 잠시 중지하고 운영체제로 이동한다. 여기서 고민해볼 소지가 등장하는데, cpu는 이제 운영체제영역에서의 무엇인가를 실행할 것이다. 그렇게 되면 레지스터와 같은 것들을 자연스럽게 사용하게 될 것이다. 그렇다면 기존 프로세스가 사용하던 레지스터들이 변형이 일어 나게 될 것이고 다시 운영체제에서의 행동이 끝나고 돌아가도 실행중이던 것들을 정상적으로 이용 할 수 없다. 그렇기 때문에 커널 스택(kernel stack)에 플래그와 레지스터와 같은것들을 저장해놓는다. return-from-trap 명령어들은 이렇게 저장했던 레지스터와 플래그들을 다시 원상 복귀 시켜 놓는 역할을 하는 것이다.
### 어떻게 trap을 전달하는 것인가?
또 한가지 의문점이 든다. 유저모드의 프로세스가 trap을 발생시키면 운영체제의 어떤 부분을 실행해야 하는 것일까? 유저모드가 운영체제의 그 실행 부분(주소)을 알려 줘야하는 것일까? 이건 말이 안된다. 왜냐하면 주소를 안다는 것 자체가 커널 내부를 안다는 뜻이고 커널 내부의 원하는 지점에 접근 할 수 있다는 것이다. ==> 이 이슈는 교수님이 퀴즈에 출제하셨었다. 틀렸던 기억이 있다.
그렇기 때문에 부팅 시 트랩 테이블(trap table)을 만들어, 어떠한 예외가 들어 왔을때 어디를 실행하라는 것을 명시한다. 부팅 시에는 당연히 커널 모드 일 것이다. 이제 이 트랩테이블의 위치를 알려줘야한다. 바로 위 문단에서 이야기 했듯이 유저모드의 프로세스는 이걸 알면 안된다고 했다. 대신 하드웨어에게 트랩 핸들러의 위치를 알려준다. 이제 하드웨어에서 어떠한( 키보드, 하드디스크, 시스템 콜 .. ) 인터럽트가 발생하거나 정보를 전달받게 되면, 해당 위치를 기억하고 있다가 그 하드웨어가 가지는 특정 숫자를 넘겨주고 trap table에서 그 주소를 찾아 이동하게 되는 것이다.
당연히 Trap Table의 위치를 알려주는 것 또한 강력한 기능이기 때문에 커널모드에서 이루어 져야 한다.
## 표로 정리하기
제한적 직접 실행(Limited Direct Execution) 지금까지 이야기했던거와 가팅 크게 두 파트로 나뉜다. 부팅시 테이블을 초기화하고 하드웨어에게 이 정보를 알려준다.
유저-커널 모드를 시스템 콜과 return-from-trap을 사용하며 왔다갔다 하면서 제어권을 주고 받는다. 프로세스가 main()에서 리턴하면서 exit()를 호출하고 운영체제로 트랩하고, 메모리를 반환 및 프로세스를 제거한다.
# 2. 프로세스간 전환 해결하기
직접 실행은 CPU에 대한 권한을 누군가 명확히 구분해주지 않기 때문에 프로세스간 전환이 어렵다. 왜 그냥 위처럼 왔다갔다 하면 되는거 아닌가? 라는 생각이 들 수 있지만, 프로세스가 실행중이라는 것은 운영체제가 실행되고 있지 않다는 것이다. 운영체제가 실행되지 않는데 어떻게 전환이 가능한 것일까? 교재에서는 " 할 수 없다"라고 했다. 그렇기 때문에 운영체제는 CPU를 다시 획득하여 프로세스를 전환해야 한다.
## 협조(cooperative) 방식
어찌생각하면 너무나도 이상적인 방법이다. 프로세스가 운영체제에게 시스템콜을 통하여 호출해주거나, 파일 입출력과 같은 I/O를 요청함으로써 불러내느 것이다. 즉, 너무 오래 실행될거 같은 프로세스라면 CPU를 넘겨준다는 아주 우호적인 프로세스들이 있다는 가정이다. 세상에 착한사람만 있는 것이 아니다. 프로세스도 마찬가지이다. 그렇기 때문에 좋은 방법은 아니라는 것이다. 또한 해당 프로세스가 무한루프에 빠져버리게 되어도 운영체제는 별다른 조치를 해줄 수가 없다. 컴퓨터를 재부팅 시키는 물리적인 방법을 제외하곤 말이다. 해결책은 협조 없이 운영체제가 다시 CPU권한을 가져야 하는 것이다.
## 비협조 적인 프로세스을 위해 : 타이머 인터럽트
간단히 타이머 인터럽트(timer Interrupt)를 이용하여 해결 한다. 특정 시간마다 인터럽트를 발생시켜 운영체제에게 CPU권한을 준다. 이제 운영체제는 CPU권한을 얻었기 때문에 프로세스를 스케줄링해주든지 등등 어떠한 동작을 수행 할 수 있게 된다.
제한적 직접 실행(Limited Direct Execution)에서 하드웨어 I/O나 시스템콜을 위해서 운영체제의 trap table을 하드웨어에게 알려준거와 똑같이 운영체제는 이러한 준비를 한다. 어디를 실행할지 미리 알려준다.
또한 프로세스가 중간에 중지 됐다가 다른프로세서가 실행되고 다시 돌아오는 과정은 프로세서가 보기에는 연속된 동작이여야 한다. 타이머 인터럽트가 발생했을 때에도 레지스터와 플래그들을 프로세스별로 저장시킨 후, return-from-trap할때 복원시킨다.
## 문맥의 저장과 복원 : context switch
인터럽트가 발생하여 다음 프로세스를 실행시키기 위한 것은 스케줄러가 결정한다. 이 방법은 다음 포스팅에서 자세히 다룬다. 간단히만 이야기하자면, 100개의 프로세스가 존재한다면 그 다음은 어떤 프로세스를 실행시켜야 하는 것에 대한 방법이다.
여튼 스케줄러에 의해서 다른 프로세스로 바뀌는것이 결정되면 문맥 교환(context switch)이라고 알려진 코드를 실행한다. 하나의 프로세스가 운영체제와 trap을 일으키며 정보를 저장-복원했던거와 다르게( 이건 저장 -> 커널 모드 -> 복원 -> 유저모드 의 반복이다) return-from-trap의 명령어는 트랩을 일으킨 프로세스의 정보를 복원하는 것이 아닌 바뀔 프로세스의 정보를 복원시켜야 한다. 즉 A의 정보가 아닌 B의 정보를 복원시키면 B가 실행이 되는 것이다.
아래 연대기를 참고하여 이해해보자.
### 인터럽트를 처리중에 인터럽트가 발생하면?
내가 시스템소프트웨어에서 열심히 어셈블리코딩을 할때는 커널모드에서는 인터럽트요청(불능화)을 무시해버렸다. 여기서 뭔가 아토믹(Atomic)한 것에 대한 이슈가 있었다. 또 그사이에 많은 인터럽이 온다면 많은 정보가 무시 될 수도 있을 것 같다는 생각을 해본다.
Lock 기법을 사용할 수 있다고 한다. 이건 아직 안배웠다. 그래서 잘 모르곘지만 병행성 부분을 공부하면 알게 될 것이라고 한다!
# 끝내며
CPU는 어쩌면 우리가 생각했던거와 같이 아주 평범한 것 같기도하다. 그냥 엄청 빨리 왔다갔다 거리는거 아닌가..?
이게 사진이 없으니까 너무 칙칙해 보인다..
'•Compter Science > Operating System' 카테고리의 다른 글
[OS/OSTEP] 08.CPU-sched-mlfq : 멀티 레벨 피드백 큐(MLFQ) #5 (0) | 2022.04.18 |
---|---|
[OS/OSTEP] 07.CPU-sched : CPU 스케줄링 정책(scheduling policy) #4 (0) | 2022.04.18 |
[OS/OSTEP] 05.CPU-API 프로세스 API #2 (0) | 2022.04.17 |
[OS/OSTEP] 04.CPU-intro 프로세스의 개념 #1 (0) | 2022.04.17 |
[OS/OSTEP] 운영체제 공부를 시작하며 #0 (0) | 2022.04.17 |