운영체제가 어떤 프로그램을 실행시키는 가장 간단한 방법은, 이를 직접 실행시키는 것이다.

프로세스에 메모리를 할당하고 프로그램 코드를 디스크에 탑재하여 CPU 상에서 그대로 실행시킴을 의미하는데, 이렇게 될 경우 여러 문제점이 발생한다.

1. 프로그램이 OS가 원하지 않는 (악의적인) 코드를 수행할 수도 있다. 이 때, OS는 CPU의 제어권을 해당 프로세스에게 넘겨줬기 때문에 이를 막을 방법이 없다.

2. OS가 여러 프로세스를 동시에 실행시킬 수 없다. 즉, time sharing이 불가능하다.

 

그래서 등장한 개념이 Limited Direct Execution인데 무엇을 제한하는 것일까?

이러한 문제점을 해결하기 위해 실제 OS가 프로세스를 실행시키는 원리를 알아보자.

 

1. 프로세스에 의한 원치 않는 코드의 실행 막기 (Restricted Operations)

user mode와 kernel mode의 개념을 도입했다.

OS의 중요한 코드를 실행시키기 위해서는 kernel mode로의 전이가 필요하게끔 만들었다.

즉, user mode에서는 디스크 읽고 쓰기와 같은 privileged operation을 실행시킬 수 없다.

그럼 프로세스가 하드웨어와 관련한 특정 작업을 필요로 할 때는 어떻게 할까?

하드웨어는 사용자 프로세스에게 system call을 제공한다. 그리고 커널은 이러한 system call을 통하여 privileged operation 기능을 제공한다.

이러한 기능에는 파일 시스템 접근, 프로세스 생성과 제거, 다른 프로세스와의 통신 및 메모리 할당 등이 포함된다.

시스템 콜을 실행하기 위해 프로그램은 trap 특수 명령어를 실행해서 kernel mode를 획득한다.

kernel mode에서는 OS를 통해 원하는 명령어를 모두 실행할 수 있다.

프로세스가 요청한 작업이 끝나면 return-from-trap 특수 명령어를 호출하여 user mode로 복귀한다.

 

다만, trap을 실행할 때도 제약조건이 있다.

만약 프로그램이 커널의 특정부분으로 실행흐름을 점프할 수 있다면 임의의 코드를 실행할 수 있어 위험하다.

커널은 부팅 시에 trap table을 만들고 하드웨어가 필요로 하는 trap handler의 위치를 알려준다.

하드웨어는 이 정보를 이용해서 특정 작업을 수행할 수 있다.

 

아래의 표는 부팅에서부터 프로그램 실행과 종료까지의 과정을 잘 설명해준다.

 

 

2. 프로세스 간의 전환방법 (Switching Between Processes)

1) 시스템 콜을 기다리기 (Cooperative Approach)

이는 과거의 시스템에서 주로 채택되었던 방식으로, 프로세스가 system call을 호출하여 CPU 제어권이 OS에게 돌아올 때까지 기다리는 방식이다.

이러한 유형의 OS에서는 yield 라는 system call을 제공한다. 이 system call은 단순히 제어권을 OS에게 돌려줘서 다른 프로세스를 실행할 수 있게 해주는 역할을 한다.

또한 프로그램이 접근권한이 없는 곳에 접근을 시도한다거나 어떤 값을 0으로 나누려 시도하는 등의 오류를 발생시키면 trap이 실행되어 OS가 제어권을 가지게 된다.

 

2) OS가 제어권 행사하기 (Non-Cooperative Approach)

위에서 설명한 cooperative approach와 같은 경우에서 무한루프와 같은 사유로 제어권이 OS에게 넘어오지 않으면 어떡할건가?

지금 설명할 방식에서는 항상 OS에게 제어권이 넘어오도록 설계가 되어있다.

주로 사용하는 방법은 timer interrupt를 이용하는 것이다.

이 장치는 정해진 밀리초마다 진행 중인 프로세스를 중단시키고 미리 구성된 OS의 interrupt handler가 실행된다. 이러한 방식으로 OS는 다시 제어권을 얻게되고 원하는 일을 수행한다. 현재의 프로세스를 계속 실행시키거나 다른 프로세스를 실행시킬 수도 있다.

추가로, 하드웨어는 interrupt가 발생했을 때 실행되어야 될 코드를 미리 알고 있어야되고 후에 return-from-trap 명령어에 의해 프로그램이 정상적으로 다시 작동할 수 있도록 interrupt가 발생했을 때 프로그램의 레지스터값 등의 상태를 저장해놓아야 한다.

 

3. context의 저장과 복원

OS가 어느 프로세스를 얼마나 실행시킬지는 scheduler라는 부분에 의해 결정된다.

OS는 프로세스의 전환이 일어날때 다음과 같은 context switch 코드를 실행한다.

먼저, 이미 실행중이던 프로세스의 레지스터를 커널 스택과 같은 곳에 저장하고

앞으로 실행할 프로세스의 레지스터 값을 이미 저장해놓은 값을 이용하여 복원한다.

이 과정을 잘 설명해놓은 표가 아래에 있다.

 

 

위 표에서 OS는 timer interrupt에 의해 CPU 제어권을 획득한다.

그 뒤 process A를 중지시키고 process B를 실행시킬 것을 결정한다.

핵심을 설명하자면,

timer interrupt가 실행될때 process A의 context는 kernel stack에 저장된다.

OS가 process switching을 결정한 후에는 process A의 레지스터를 A의 프로세스 구조체에 저장하고

B의 프로세스 구조체에서 process B의 레지스터를 복원하면서 B의 커널스택으로 전환한다.

하드웨어는 process B의 실행을 위해 커널스택에 저장된 B의 레지스터 값을 복원해주고 실행을 재개한다.

timer interrupt에 의해 제어권을 OS에게 넘겨주기 위해 하드웨어가 프로세스의 레지스터 값을 커널스택에 저장하는 것과 process switching을 위해 OS가 프로세스의 레지스터 값을 프로세스 구조체에 저장하는 것의 차이를 기억하자!

process A의 레지스터를 구조체에 저장한 뒤 B 구조체로부터 레지스터를 복원해오면 마치 OS가 A가 아닌 B로부터 커널로 trap 된 것처럼 만든다.

다음은 OS가 실행하는 switch의 코드이다.

old와 new는 각각의 프로세스 구조체 안에 위치해있는 context 구조체이다.

 

 

4. 기타 (concurrency 관련)

다른 interrupt를 처리하고 있는 중간에 다른 interrupt가 발생한다면?

system call을 처리하고 있을 때 timer interrupt가 발생한다면?

과 같은 문제를 제기할 수 있다.

 

이러한 내용을 다루는 것이 바로 concurrency인데 후에 자세하게 설명된다.

간단한 방법으로는 한개의 interrupt를 처리하고 있을 때는 다른 interrupt를 disable 시키는 것이 있을 수 있다.

또한 OS는 다양한 lock 기술을 활용해 커널 안에서 다수의 활동이 진행될 수 있도록 해준다.

 

마지막으로, 원서에는 이 단원을 잘 정리해놓은 몇개의 문장이 수록되어 있어서 가져와봤다.

 

'Operating Systems: Three Easy Pieces' 카테고리의 다른 글

07 CPU Scheduling (2)  (0) 2018.10.16
07 CPU Scheduling (1)  (1) 2018.10.16
[Operating Systems: Three Easy Pieces] 책소개  (0) 2018.10.15
05 Process API  (1) 2018.10.03
04 Processes  (0) 2018.10.03

+ Recent posts