이 글은 건국대학교 2024년 1학기 운영체제 수업과 『Operating Systems: Three Easy Pieces』 를 참고하여 작성되었습니다.
『Operating Systems: Three Easy Pieces』
6장. Limited Direct Execution (제한적 직접 실행 원리)
이전 게시물에서 다룬 시분할 기법을 구현하기 위해서는 context switch의 오버헤드를 최소화하여 성능을 개선해야 하고, CPU를 제어하면서 프로세스를 효율적으로 실행시킬 수 있어야 합니다. 이번 글에서는 OS가 CPU를 어떻게 제어하는지 살펴보겠습니다.
Direct Execution은 process에게 CPU자원을 할당한 후, OS의 중간 개입 없이 프로세스가 자유롭게 실행되는 방식입니다.

OS
1. process list (PCB) 생성
2. process가 사용할 메모리를 할당
3. 메모리에 프로그램을 load
4. 프로그램을 실행하기 위한 user stack을 만들고, argc, argv를 user stack에 저장한다.
5. 실행 전에 모든 register 값들을 초기화한다.
6. main() 함수를 호출한다.
program
7. main() 함수가 실행되고 return 된다. (끊김 없이 실행된다)
OS
8. process가 사용한 메모리를 free
9. 해당 process를 위해 할당했던 PCB도 process list에서 제거한다.
💥 해당 방식은 프로세스가 CPU를 계속 가지고 있기 때문에 OS가 중간에 관여할 수 없습니다. 이로 인해 아래 두 가지 문제 상황을 야기합니다. 첫째, 프로세스가 제한된 작업(restricted operation)을 수행하는지 감시 또는 제어를 할 수 없습니다. 둘째, 중간에 OS가 관여할 수 없으므로 시분할 기법을 사용할 수 없습니다.
🌟 이러한 문제를 해결하기 위해, 프로세스가 실행되는 도중 어떠한 제한을 둬서 중간중간 OS가 간섭할 수 있는 장치를 마련하였습니다. 이러한 방식을 Limited Direct Execution 라고 합니다.
restricted operation에는 disk와 같은 디바이스에게 요청을 보내고 수행해야 하는 I/O 요청, CPU나 메모리에 대한 자원을 더 많이 필요로 하는 작업 등이 있습니다.
응용 프로그램들은 restricted operation 을 수행할 수 있어야 하지만, OS의 통제 하에 수행되어야 합니다.
하드웨어(CPU)는 다양한 프로세서 모드를 제공하여 각 모드마다 실행 권한의 수준을 다르게 부여합니다. 이러한 모드 구분을 통해 안정적인 시스템을 운영할 수 있습니다.
intel CPU의 경우 4가지 모드를 제공하지만, 주로 level0와 level3, 이렇게 2가지 모드만 사용합니다.
user mode (level 3) 는 일반 프로세스의 소스코드가 실행되는 모드입니다. 해당 모드에서는 제한된 작업을 수행할 수 없기 때문에, 제한된 작업을 실행하려 하면 exception이 발생하면서 프로그램이 종료됩니다.
kernel mode (level 0) 는 OS가 동작하는 모드로, 제한된 작업을 수행할 수 있습니다.
Q. 그럼 유저 모드에서 제한된 작업을 실행하고 싶으면 어떻게 해야할까?
A. 유저 모드에서 system call을 호출하여 커널 모드로 전환한 후, 제한된 작업을 실행시키면 됩니다!
system call 내부에서는 프로세서 모드를 커널 모드로 전환하기 위해 trap 명령어와 return-from-trap 명령어를 사용합니다.
trap 명령어는 프로세스의 모드를 유저 모드에서 커널 모드로 전환시켜주고, return-from-trap 명령어는 제한된 작업을 모두 마친 프로세스를 커널 모드에서 유저 모드로 전환시켜줍니다.
Q. 근데 이렇게 프로세서 모드를 나누고 모드를 전환하기 위해 system call을 사용해도, 커널 모드가 된 후에 제한된 작업을 제어할 수 없으면 다 소용없는 거 아닐까? 커널 모드가 된 후에 아무 작업이나 마구 수행하면 어떡해?
A. OS는 프로세스가 수행하고자 하는 제한된 작업이 무엇인지 확인하고 감시하기 위해 trap table을 사용합니다. 프로세스는 제한된 작업의 주소를 직접 명시할 수 없고 trap table의 인덱스만 명시할 수 있습니다.
trap table은 트랩이 발생했을 때 실행되어야 하는 트랩 핸들러 (= 제한된 작업을 수행하는 녀석) 의 위치를 저장하는 자료구조 입니다. trap table은 인덱스 번호와 trap handler의 주소로 이루어져 있고, 프로세스들은 trap table의 인덱스 번호를 명시함으로써 trap handler를 호출할 수 있습니다.
trap table은 운영 체제 내에 단 하나만 존재하며, OS가 부팅될 때 초기화됩니다. (프로세스마다 존재하는게 아님) 응용 프로그램이 커널 모드 (OS) 에 접근할 수 있는 유일한 통로는 trap table 입니다. 이는 사용자 모드에서 커널 모드로 전환할 때 필요한 감시가 엄격한 게이트로 비유할 수 있습니다.

1. 유저 모드에서 실행 중인 프로세스가 제한된 작업을 수행하고자 시스템 콜을 호출합니다.
2. 시스템 콜은 trap 명령어를 사용하여 프로세서 모드를 유저 모드에서 커널 모드로 전환합니다.
3. OS는 trap table에서 해당 트랩의 인덱스를 찾은 후, 이에 매핑된 trap handler를 실행합니다.
4. trap handler는 내부의 system call table을 참조하여 요청된 시스템 콜 함수를 실행합니다.
(system call table은 시스템 콜 인덱스와 시스템 콜 함수의 주소로 이루어져 있습니다.)
5. trap handler가 처리를 완료하면, OS는 return-from-trap 명령어를 통해 프로세서 모드를 커널 모드에서 유저 모드로 다시 전환합니다.

1. OS 가 아래의 일을 수행
return-from-trap 실행 2. CPU 가 아래의 일을 수행

3. 유저 모드에서 program 의 main() 함수가 실행된다.
trap 발생4. CPU 가 아래의 일을 수행
5. 커널 모드에서 trap handler가 작업을 마치면, OS 가 return-from-trap 실행
6. CPU 가 아래의 일을 수행
7. main() 함수가 일을 모두 수행되고 return 됨 → exit() 을 통해 trap 이 호출됨
8. OS 가 프로세스가 사용한 메모리를 free 시키고,
해당 프로세스의 PCB도 process list에서 제거한다.
요약

OS는 CPU 없이 독자적으로 실행될 수 있는 프로세스가 아니므로, 응용 프로그램이 계속 돌면서 CPU를 독점하고 있으면 OS가 실행될 수 없습니다.
1번에서 OS가 제한된 작업을 감시하는 방법을 살펴보았고, 그 방법은 시스템 콜이 호출될 대에만 OS가 개입하는 것으로 충분했습니다. 하지만 시분할 기법을 효과적으로 구현하기 위해서는 시스템 콜의 호출 여부와 관계없이 OS가 지속적으로 관여할 수 있어야 합니다. 이를 위해선 어떤 방법을 사용해야 할까요?
Q. 어떻게 하면 OS가 동작하지 않을 때에도 process가 CPU 자원을 반납하도록 만들 수 있을까?
협조 방식에서는 모든 프로세스들이 주기적으로 CPU를 반납할 것이라 가정합니다.
system call 호출 시 : 시스템 콜 내부에 스케줄링 코드를 포함시켜, 시스템 콜이 호출될 때마다 time sharing이 실행되도록 하는 방식입니다. 예를 들어, yield 시스템 콜을 사용하여 프로세스가 CPU 자원을 자발적으로 양보하도록 합니다.
error 발생 시 : error가 발생하면 프로세스는 OS에게 CPU 제어권을 넘긴 후 종료됩니다. 어떤 수를 0으로 나누는 연산을 수행하거나, 허가되지 않은 메모리에 접근을 시도할 경우 에러가 발생합니다.
💥 이 방식에서 OS가 수동적인 역할을 하게 되며, 시스템 콜이나 에러 발생을 기다리는 것 외에는 할 수 있는 일이 제한됩니다. 만약 두 가지 모두 발생하지 않고 프로세스가 비협조적인 경우, OS가 자동적으로 제어권을 되찾을 수 있는 방법은 없습니다.
🌟 timer interrupt : 하드웨어 도움 받기
이 방식은 프로세스가 시스템 콜이나 에러가 발생시키지 않아도 OS가 개입할 수 있도록 도와줍니다. CPU 내부의 타이머가 정해진 시간마다 interrupt를 발생시키고, 이 때 interrupt handler가 작동하여 OS가 CPU 제어권을 되찾게 됩니다.
privileged 작업을 제한하고 time sharing 기법을 적용한 'Limited Direct Execution' 동작 방식에 대해 알아보겠습니다. 다음 예시는 timer interrupt 발생 시 프로세스 A에서 B로 context switch가 일어나는 보여줍니다.

1. 프로세스 A가 실행되는 중에 timer interrupt 가 발생하면, trap 명령어가 실행됩니다.
2. CPU가 kernel stack에 regs/PC (Uregs(A))를 저장하고 프로세서 모드를 커널 모드로 전환한 후, OS의 trap handler를 호출합니다.
3. OS의 interrupt handler가 실행되면서 interrupt를 처리하고, switch 함수를 호출하여 context switch를 수행합니다.
(interrupt handler는 현재 실행 중인 프로세스 A의 kernel stack을 빌려 실행됩니다.)
return-from-trap 명령어를 통해 프로세스 B로 이동합니다.4. CPU는 B의 kernel stack에 저장되어 있던 Uregs(B)를 복구하고, 프로세서 모드를 유저 모드로 전환한 후, B의 PC로 jump 합니다. -> 프로세스 B가 실행됩니다.
요약

kernel stack에 저장되는 Uregs(A) 는 응용 프로그램이 유저 모드에서 실행될 때의 필요한 context 정보(ex.regs/PC)를 포함합니다. 반면, Kregs(A) 는 process A가 커널 모드에서 실행될 때의 context 정보를 의미하며, interrupt handler가 실행 중인 프로세스의 address space을 사용하여 실행되기 때문에, Kregs(A)는 interrupt handler의 context 정보도 포함합니다.