이 포스팅은 Operating Systems: Three Easy Pieces, Remzi H. Arpaci-Dusseau & Andrea C. Arpaci-Dusseau을 읽고 개인 학습용으로 정리한 글입니다.
성능 저하 문제: 시스템에 과중한 오버헤드를 주지 않으면서 가상화를 구현할 수 있을까?
제어 문제: CPU에 대한 통제를 유지하면서 프로세스를 효율적으로 실행시킬 수 있는 방법은 무엇인가?
-> 제어권을 상실하면 한 프로세스가 영원히 실행을 계속할 수 있음
-> 제어권을 상실하면 한 프로세스가 접근해서는 안되는 정보에 접근하게 됨
프로그램을 CPU 상에서 그냥 직접 실행 시킴
운영체제가 프로그램을 실행하기 시작할 때
-> 프로세스 목록에 해당 프로세스 항목을 생성
-> 메모리 할당
-> 프로그램 코드를 디스크에 탑재
-> 진입점(ex. main() 루틴)을 찾아 그 지점으로 분기
-> 사용자 코드 실행 시작
프로그램이 운영체제가 원치않는 일을 하지 않는 다는 것을 보장할 수 없다
프로세스 실행 시 운영체제가 프로그램의 실행을 중단하고 다른 프로세스로 전환시킬 수 없다
-> CPU를 가상화하기 위한 시분할 기법 구현 X
만일 프로세스가 특수한 종류의 연산을 수행하길 원한다면 어떨게 될 것인가?
사용자 모드(user mode): 사용자 모드에서 실행되는 코드는 할 수 있는 일이 제한된다
-> 프로세서가 예외를 발생시키고, 운영체제는 해당 프로세스를 제거한다
커널 모드(kernel mode): 커널 모드에서 실행되는 코드는 모든 특수한 명령어를 포함하여 원하는 모든 작업을 수행할 수 있다
-> 운영체제의 중요한 코드들이 커널 모드에서 실행됨
-> ex. 파일 시스템 접근, 프로세스 생성 및 제거, 다른 프로세스와의 통신, 메모리 할당, ...
사용자 프로세스가 특권 명령어를 실행해야 할 때는 어떻게 해야 하는가?
시스템 콜: 커널은 시스템 콜을 통하여 자신의 주요 기능을 사용자 프로그램에게 제공
시스템 콜을 실행하기 위해 프로그램은 trap 특수 명령어 실행
-> 커널 안으로 분기 & 특권 수준을 커널 모드로 상향 조정
완료되면 운영체제는 return-from-trap 특수 명령어 호출
-> 특권 수준을 사용자 모드로 다시 하향 조정 & 호출한 사용자 프로그램으로 리턴
하드웨어는 trap 명령어 수행 시, 호출한 프로세스의 필요한 레지스터들을 저장해야 함
-> 운영체제가 return-from-trap 명령어 실행 시 사용자 프로세스로 제대로 리턴할 수 있도록
x86에서는 프로그램 카운터, 플래그와 다른 몇 개의 레지스터들을 각 프로세스의 커널 스택(kernel stack)에 저장
-> return-from-trap 명령어가 이 값들을 스택에서 pop하여 사용
호출한 프로세스는 커널 내부의 분기할 주소를 명시할 수 없다
-> trap이 운영체제 코드의 어디를 실행할지 어떻게 알 것인가?
커널은 부팅 시에 트랩 테이블(trap table) 생성
(컴퓨터가 부트될 때는 커널모드에서 작동하기 때문에 하드웨어를 원하는 대로 제어 가능)
운영체제는 특권 명령어를 사용하여 하드웨어에게 트랩 해들러(trap handler)의 위치를 알려줌
프로세스는 커널 스택을 각자 가지고 있다
-> 커널 모드로 진입/진출할 때 하드웨어에 의해 프로그램 카운터와 범용 레지스터 등의 레지스터 저장/복원되는 용도로 사용
⚡프로세스가 자신의 할 일을 다 하고 main()에서 리턴할 때 일반적으로 스텁(stub) 코드로 리턴
-> 스텁 코드가 프로그램을 종료시킬 때 exit() 시스템 콜 호출
-> 운영체제로 트랩되어 운영체제가 프로세스 정리
협조(cooperative) 방식: 운영체제가 프로세스들이 합리적으로 행동할 것이라고 신뢰
-> 너무 오랫동안 실행할 가능성이 있는 프로세스는 주기적으로 CPU를 포기할 것이라고 가정
우호적인 프로세스는 자주 시스템 콜을 호출하여 CPU의 제어권을 운영체제에게 넘겨줌
이런 유형의 운영체제는 yeild 시스템 콜 제공
-> 이 시스템 콜은 운영체제에게 제어를 넘겨 운영체제가 다른 프로세스를 실행할 수 있게 함
응용 프로그램이 비정상적인 행위를 하게 되면 운영체제에게 제어가 넘어간다
(ex. 어떤 수를 0으로 나누는 연산 실행, 접근할 수 없는 메모리 접근 시도, ...)
-> 운영체제로의 trap 발생
-> 운영체제가 해당 행위를 하는 프로세스 종료
협조 방식에서 프로세스가 무한 루프에 빠졌을 경우 컴퓨터를 다시 부팅하는 수밖에 없다
-> 프로세스가 비협조적인 상황에서도 CPU의 제어를 획득하는 방법은 무엇인가?
타이머 인터럽트(timer interrupt): 수 밀리 초마다 인터럽트를 발생시키도록 함
-> 인터럽트가 발생하면 현재 수행 중인 프로세스 중단 & 미리 구성된 운영체제의 인터럽트 핸들러 실행
부팅 과정 진행 중에 운영체제는 타이머 시작 & 타이머 인터럽트가 발생했을 때 실행해야 할 코드를 하드웨어에게 알려줌
⚡타이머는 특정 명령어를 통해 끌 수도 있다
운영체제가 제어권을 다시 획득하면 현재 실행중인 프로스세를 계쏙 실행할 것인지 다른 프로세스로 전환할 것인지 결정해야 함
-> 운영체제의 스케쥴러(scheduler)에 의해 결정
다른 프로세스로 전환하기로 결정되면 운영체제는 문맥 교환(context switch)이라고 알려진 코드를 실행
운영체제는 프로세스 전환을 위해 ⚡저수준의 어셈블리 코드 사용
-> 현재 실행 중인 프로세스의 범용 레지스터, PC, 커널 스택 포인터 저장
-> 곧 실행 된 프로세스의 범용 레지스터, PC 복원, 커널 스택을 이 프로세스의 커널 스택으로 전환
-> 운영체제가 마지막으로 return-from-trap 명령어를 실행하면, 곧 실행된 프로세스가 현재 실행 중인 프로세스가 됨
위 과정이 실행되는 동안 두 번의 레지스터 저장/복원이 일어남
(1) 타이머 인터럽트가 발생했을 때
-> 프로세스의 ⚡사용자 레지스터가 하드웨어에 의해 해당 프로세스의 커널 스택에 암묵적으로 저장됨
(2) 운영체제가 프로세스 A -> 프로세스 B로 전환하기로 결정했을 때
-> 프로세스의 ⚡커널 레지스터가 운영체제에 의해 해당 프로세스의 프로세스 구조체에 저장됨
시스템 콜을 처리하는 도중에 타이머 인터럽트가 발생하면 어떤 일이 생기는가?
하나으 인터럽트를 처리하고 있을 때 다른 인터럽트가 발생하면 어떤 일이 생기는가?
간단한 해법: 인터럽트를 처리하는 동안 인터럽트 불능화
-> 하나의 인터럽트가 처리되고 있는 동안 다른 어떤 인터럽트로 cpu에게 전달되지 않는다
락(lock) 기법 개발
-> 내부 자료 구조에 동시에 접근하는 것 방지
-> 커널 안에서 동시에 다수의 활동이 진행될 수 있도록 허용
제한적 직접 실행: CPU 가상화를 구현하기 위한 핵심적인 저수준 기법
CPU에서 실행하고 싶은 프로그램을 실행시키지만, 운영체제가 CPU를 사용하지 못하더라도 프로세스의 작업을 제한할 수 있도록 하드웨어 셋업
부팅할 때 트랩 핸들러 함수 셋업 & 인터럽트 타이머 시작 & 제한된 모드에서만 프로세스가 실행되도록 함
운영체제는 특별한 연산을 수행할 때, 프로세스가 CPU를 독점할 때, 다른 프로세스로 전환해야 할 때만 개입