개발 공부
[운영체제][OSTEP] 제한적 직접 실행 원리
제어를 유지하면서 효과적으로 CPU를 가상화하는 방법에 대해 공부해봅니다.

![[운영체제][OSTEP] 제한적 직접 실행 원리](https://www.datocms-assets.com/66479/1686988115-ostep.jpg?auto=format&w=860)
1️⃣ 기본 원리: 제한적 직접 실행
"직접 실행": 프로그램을 CPU상에서 그냥 직접 실행시키는 것
(아무 제한이 없는) 직접 실행 프로토콜의 순서
프로세스 목록의 항목을 생성
프로그램 메모리 할당
메모리에 프로그램 탑재
argc/argv를 위한 스택 셋업
레지스터 내용 삭제
call main( ) 실행
main( ) 실행
main에서 return 명령어 실행
프로세스 메모리 반환
프로세스 목록에서 항목 제거
그러나!
이 접근법은 CPU를 가상화함에 있어 몇 가지 문제를 일으킴
프로그램을 직접 실행시킬 때, 운영체제가 원치 않는 일을 하지 않는다는 것을 어떻게 보장하는 가?
CPU를 가상화하는 데 필요한 시분할(time sharing) 기법을 어떻게 구현할 수 있느냐?
이러한 질문에 답하면서 CPU 가상화에 대한 기법을 발전시키는 과정 중에 "제한적"이라는 이름이 어디서 비롯되었는지 살펴보자
프로그램 실행에 제한을 두지 않으면 운영체제는 어떠한 것도 제어할 수 없음 ➡️ 단순한 라이브러리
2️⃣ 문제점 1: 제한된 연산
직접 실행의 장점은 빠르게 실행된다는 것
기본적으로 프로그램이 하드웨어 CPU에서 실행되기 때문
그러나 CPU에서 직접 실행시키면 새로운 문제가 발생
프로세스가 디스크 입출력 요청, 시스템 자원 추가 할당 요청과 같은 특수한 종류의 연산 수행을 원하는 경우
프로세스가 원하는 대로 할 수 있게 방치하는 방안 ➡️ 바람직한 시스템을 구축하는 데에는 방해 요인
⭐ 프로세스가 제한된 연산(입출력 연산 등)을 수행하기 위해 운영체제와 하드웨어가 해야 할 일은 무엇인가?
사용자 모드(user mode)
사용자 모드에서 실행되는 코드는 할 수 있는 일이 제한됨
만약 프로세스가 실행 중에 입출력 요청을 할 수 없도록 설정하면 입출력 요청을 했을 때 프로세스가 예외를 발생시키고 운영체제는 해당 프로세스를 제거
커널 모드(kernel mode)
사용자 모드와 대비되는 모드로서 운영체제의 중요한 코드들이 실행
커널 모드에서 실행되는 코드는 모든 특수한 명령어를 포함하여 원하는 모든 작업을 수행
시스템 콜의 사용
사용자 프로세스가 디스크 읽기와 같은 특권 명령어를 실행해야 하는 경우 ➡️ 시스템 콜 사용
파일 시스템 접근, 프로세스 생성 및 제거, 다른 프로세스와의 통신 및 메모리 할당 등
대부분의 운영체제는 수백 개의 시스템 콜을 제공
초기 Unix 시스템은 약 20개 정도의 시스템 콜을 제공
trap과 return-from-trap 특수 명령어
시스템 콜을 실행하기 위해 프로그램은 trap 특수 명령어를 실행해야 함
이 명령어는 커널 안으로 분기하는 동시에 특권 수준을 커널 모드로 상향 조정
커널 모드로 진입 ➡️ 운영체제는 모든 명령어 실행 가능 ➡️ 프로세스가 요청한 작업 처리 가능
완료되면 운영체제는 return-from-trap 특수 명령어를 호출
이 명령어는 특권 수준을 사용자 모드로 다시 하향 조정하면서 호출한 사용자 프로그램으로 리턴
하드웨어는 trap 명령어를 수행할 때 주의가 필요
호출한 프로세스의 필요한 레지스터들을 저장 ➡️ 운영체제가 return-from-trap 명령어 실행 시 사용자 프로세스로 제대로 리턴할 수 있기 위함
x86에서는 프로그램 카운터, 플래그와 다른 몇 개의 레지스터를 각 프로세스의 커널 스택(kernel stack)에 저장
return-from-trap 명령어가 이 값들을 스택에서 pop하여 사용자 모드 프로그램의 실행을 다시 시작
trap이 운영체제 코드의 어디를 실행할지 어떻게 아는가?
호출한 프로세서는 분기할 주소를 명시할 수 없음
주소를 명시한다는 것은 커널 내부의 원하는 지점을 접근할 수 있다는 것 ➡️ 위험 ⚠️
커널이 임의의 코드를 실행하기 위해서는 접근 권한 검사가 끝난 후 분기
따라서 커널은 trap 발생 시 어떤 코드를 실행할지 신중하게 통제
트랩 테이블(trap table)
커널이 부팅 시에 만들고 이를 이용하여 시스템을 통제
컴퓨터가 부트될 때는 커널 모드에서 동작 ➡️ 하드웨어를 원하는 대로 제어 가능
운영체제의 초기 작업 중 하나: 하드웨어에게 예외 사건이 일어났을 때 어떤 코드를 실행해야 하는지 알려줌
운영체제는 특정 명령어를 사용하여 하드웨어에게 트랩 핸들러(trap handler)의 위치를 알려줌
시스템 콜 등 예외적인 사건이 발생했을 때 하드웨어는 어느 코드로 분기하여 실행할지 알 수 있음
정확한 시스템 콜을 특정하기 위해 각각의 시스템 콜에 시스템 콜 번호를 할당함
사용자 코드는 원하는 시스템 콜 번호를 레지스터나 스택의 명시된 위치에 배치하는 일을 담당
운영체제는 트랩 핸들러 내에서 시스템 콜을 처리할 때 이 번호들을 검사하여 유효한 지 확인하고 유효하다면 해당 코드를 실행
사용자 코드는 이동할 정확한 주소를 지정할 수 없고 번호를 통해 특정 서비스를 요청 ➡️ 일종의 보호 역할 🛡️
하드웨어에게 트랩 테이블의 위치를 알려주는 것은 매우 강력한 기능
따라서 이는 특권 명령어이며 사용자 모드에서 사용 불가능
제한적 직접 실행 프로토콜의 순서
[운영체제: 부트 (커널 모드)] 트랩 테이블 초기화
[하드웨어] syscall 핸들러의 주소 기억
[운영체제: 부트 (실행 모드)] 프로세스 목록에 항목 추가
[운영체제: 부트 (실행 모드)] 프로그램을 위한 메모리 할당
[운영체제: 부트 (실행 모드)] 프로그램을 메모리에 탑재
[운영체제: 부트 (실행 모드)] argv를 사용자 스택에 저장
[운영체제: 부트 (실행 모드)] 레지스터와 PC를 커널 스택에 저장
[운영체제: 부트 (실행 모드)] return-from-trap
[하드웨어] 커널 스택으로부터 레지스터 복원
[하드웨어] 사용자 모드로 이동
[하드웨어] main으로 분기
[프로그램 (사용자 모드)] main( )을 실행
[프로그램 (사용자 모드)] 시스템 콜 호출
[프로그램 (사용자 모드)] 운영체제로 trap
[하드웨어] 레지스터를 커널 스택에 저장
[하드웨어] 커널 모드로 이동
[하드웨어] 트랩 핸들러로 분기
[운영체제: 실행 (커널 모드)] 트랩 처리
[운영체제: 실행 (커널 모드)] syscall의 임무 수행
[운영체제: 실행 (커널 모드)] return-from-trap
[하드웨어] 커널 스택으로부터 레지스터 복원
[하드웨어] 사용자 모드로 이동
[하드웨어] 트랩 이후의 PC로 분기
[프로그램 (사용자 모드)] main에서 리턴
[프로그램 (사용자 모드)] trap(exit( )를 통하여)
[운영체제: 실행 (커널 모드)] 프로세스의 메모리를 반환
[운영체제: 실행 (커널 모드)] 프로세스 목록에서 제거
프로세스는 커널 스택을 각자 가지고 있음
커널 모드로 진입/진출 시 하드웨어에 의해 프로그램 카운터와 범용 레지스터 등의 레지스터가 저장되고 복원되는 용도로 사용
LDE(제한적 직접 실행) 프로토콜은 두 단계로 진행
전반부(부팅 시)
커널은 트랩 테이블을 초기화
CPU는 나중에 사용하기 위해 테이블의 위치 기억
커널은 이러한 작업을 커널 모드에서만 사용할 수 있는 명령어를 이용하여 수행(위의 순서에 밑줄로 표시)
후반부(프로세스 실행 시)
return-from-trap을 이용하여 사용자 프로세스를 시작할 때 몇 가지 작업을 수행
새로운 프로세스를 위한 노드를 할당하여 프로세스 리스트에 삽입, 메모리 할당 작업 등 포함
return-from-trap 명령어는 CPU를 사용자 모드로 전환하고 프로세스 실행을 시작
프로세스가 시스템 콜을 호출하면 운영체제로 다시 trap
운영체제는 시스템 콜 처리 후 return-from-trap 명령어를 사용하여 다시 제어를 프로세스에게 넘김
프로세스는 작업을 마친 후 main( )에서 리턴 ➡️ 일반적으로 스텁 코드로 리턴하고 스텁 코드가 프로그램을 종료시킴
종료시킬 때 exit( ) 시스템 콜을 호출하고 다시 운영체제로 trap
운영체제는 정리 작업을 진행하고 모든 일이 완료됨
3️⃣ 문제점 2: 프로세스 간 전환
운영체제는 실행 중인 프로세스를 계속 실행할 것인지, 멈추고 다른 프로세스를 실행할 것인지를 결정
CPU에서 프로세스가 실행 중이라는 것은 운영체제는 실행 중이지 않다는 것을 의미
⭐ 운영체제는 어떻게 CPU를 다시 획득하여 프로세스를 전환할 수 있는가?
협조 방식: 시스템 콜 기다리기
과거의 몇몇 시스템에서 채택되었던 방식
Mac11, Alt79 등
운영체제가 프로세스들이 합리적으로 행동할 것이라고 신뢰
너무 오래 실행할 가능성이 있는 프로세스 ➡️ 운영체제의 다른 작업 실행 결정을 위해 주기적으로 CPU를 포기할 것이라고 가정
대부분의 프로세스는 자주 시스템 콜을 호출 ➡️ CPU의 제어권을 운영체제에게 넘겨줌
이런 유형의 운영체제는 yield 시스템 콜을 제공 ➡️ 운영체제에게 제어를 넘겨 운영체제가 다른 프로세스를 실행할 수 있게함
응용 프로그램이 비정상적인 행위를 하는 경우 ➡️ 운영체제에게 제어가 넘어감
0으로 나누기, 접근할 수 없는 메모리에 접근 등
운영체제로의 trap이 일어남 ➡️ 운영체제는 다시 CPU를 획득하여 해당 프로세스를 종료
즉, 협조 방식의 스케줄링 시스템에서 운영체제가 CPU의 제어권을 다시 획득하는 방법
시스템 콜이 호출되기를 기다림
불법적인 연산이 일어나기를 기다림
단, 프로세스가 무한 루프에 빠져 시스템 콜을 호출할 수 없다면? ➡️ 컴퓨터 재부팅(협조 방식의 한계)
비협조 방식: 운영체제가 전권을 행사
타이머 인터럽트(timer interrupt)를 이용
수 밀리 초마다 인터럽트를 발생시키도록 프로그램 가능
인터럽트 발생 ➡️ 현재 수행 중인 프로세스는 중단되고 미리 구성된 운영 체제의 인터럽트 핸들러(interrupt handler)가 실행 ➡️ 운영체제는 CPU 제어권을 다시 얻음
운영체제는 하드웨어에게 타이머 인터럽트가 발생했을 때 실행해야 할 코드를 알려주어야 함
부팅 시 운영체제가 이러한 작업을 진행
부팅 과정 진행 중 운영체제가 타이머를 시작
타이머는 특정 명령어를 수행하여 끌 수도 있음
인터럽트 발생 시 하드웨어의 작업
실행 중이던 프로그램의 상태를 저장(다양한 레지스터가 커널 스택에 저장) ➡️ return-from-trap 명령어가 프로그램을 다시 시작할 수 있도록 해야 함
시스템 콜이 호출되었을 때 하드웨어가 하는 동작과 매우 유사
문맥의 저장과 복원
스케줄러(scheduler): 현재 실행 중인 프로세스의 계속 실행 vs. 다른 프로세스로 전환을 결정
후자의 경우 운영체제는 문맥 교환(context switch) 코드를 실행
프로세스 전환을 위해 운영체제는 저수준 어셈블리 코드를 사용하여 다음을 수행
현재 실행 중인 프로세스의 범용 레지스터, PC, 현재 커널 스택 포인터를 저장
곧 실행될 프로세스의 범용 레지스터, PC를 복원하고 커널 스택을 이 프로세스의 커널 스택으로 전환
운영체제가 return-from-trap 명령어를 실행하면 프로세스가 전환
switch( ) 루틴: 하드웨어의 타이머 인터럽트 발생 후 운영체제(커널 모드)에서 수행
A 프로세스 ➡️ B 프로세스로 전환되는 경우
A의 레지스터의 현재 값을 A의 프로세스 구조체에 저장
B의 프로세스 구조체에서 B의 레지스터를 복원
B의 커널 스택을 사용하도록 스택 포인터를 바꿈
두 번의 레지스터의 저장/복원이 일어남
[1] 타이머 인터럽트 발생 시: 실행 중이던 프로세스의 커널 스택에 실행 중인 프로세스의 사용자 레지스터가 하드웨어에 의해 암묵적으로 저장
[2] 운영체제가 프로세스를 전환하기로 결정 시: 커널 레지스터는 운영체제에 의하여 해당 프로세스의 프로세스 구조체에 저장
4️⃣ 병행성에 대한 우려
운영체제는 인터럽트 또는 trap을 처리하는 도중에 다른 인터럽트가 발생할 떄 어떤 일이 생기는 가에 대해 신중하게 고려할 필요가 있음
운영체제가 할 수 있는 간단한 해법은 인터럽트를 처리하는 동안 인터럽트를 불능화 시키는 것
인터럽트를 너무 오랫동안 불능화시키면 인터럽트를 놓치게 되고 기술적으로도 좋지 않음
운영체제는 내부 자료 구조에 동시에 접근하는 것을 방지하기 위해 많은 정교한 락(lock) 기법을 개발해 옴
커널 안에서 동시에 다수의 활동이 진행될 수 있게 허용
📚 참고 문헌
Operating Systems: Three Easy Pieces ― 6: Mechanism: Limited Direct Execution