동시에 작업을 실행시키기 위해 CPU를 시간을 나누어 씀 방식으로 자원을 공유해 가상화를 구현한다.
운영체제는 가상화를 통해 효율적이게 운영을 하고있다 하지만 가상화를 하면서도 프로세스에 제어권을 잃지 않아야한다.
종종 프로세스는 하드웨어에 대한 제어가 필요하다 이에 대한 제어권은 운영체제가 잘 통제해야한다. 제어권을 잃어버린다면 영영 프로세스가 종료되지 않거나, 접근권한을 넘어버릴 수 있다.
그래서 운영체제는 성능저하 없이 제어권을 유지해야한다.
기본 원리 : 직접 실행
직접 실행은 간단하다. 프로그램을 그냥 CPU에서 실행시킨다. 그러면 아래의 표와 같이 동작한다.
| 운영체제 | 프로그램 |
|---|---|
| 프로세스 목록의 항목을 생성 | |
| 프로그램 메모리 할당 | |
| 메모리에 프로그램 탑재 | |
| argc/argv를 위한 스택 셋업 | |
| 레지스터 내용 삭제 | |
| call main() 실행 | |
| main() 실행 | |
| main에서 return 명령어 실행 | |
| 프로세스 메모리 반환 | |
| 프로세스 목록에서 항목 제거 |
간단하고 사용하기 쉽게 된다. 그런데 과연 가상화로 CPU 시분할은 어떻게 구현되는 가? 시분할을 하려면 프로그램을 중간에 중단 시켜야하는 데 그럴 방법이 지금 보이지 않는다.
문제점: 이 방식은 프로그램이 원하는 모든 작업을 수행할 수 있어 위험
문제: 제한된 직접연산
사용자 모드와 커널 모드
운영체제는 두 가지 실행 모드 제공
- 사용자 모드(User Mode): 제한된 권한으로 실행
- 커널 모드(Kernel Mode): 모든 하드웨어 자원에 접근 가능
시스템 콜 메커니즘
프로세스가 특정 작업(I/O, 메모리 할당 등)을 수행해야 할 때:
- trap 명령어로 커널 모드 진입
- 운영체제가 요청 처리
- return-from-trap으로 사용자 모드 복귀
제한적 직접 실행 프로토콜
| 운영체제 @부트 (커널 모드) | 하드웨어 |
|---|---|
| 트랩 테이블을 초기화한다 | |
| syscall 핸들러의 주소를 기억한다 |
| 운영체제 @실행 (커널 모드) | 하드웨어 | 프로그램 (사용자 모드) |
|---|---|---|
| 프로세스 목록에 항목을 추가한다 | ||
| 프로그램을 위한 메모리를 할당한다 | ||
| 프로그램을 메모리에 탑재한다 | ||
| argv를 사용자 스택에 저장한다 | ||
| 레지스터와 PC를 커널 스택에 저장한다 | ||
| return-from-trap | 커널 스택으로부터 레지스터를 복원한다 사용자 모드로 이동한다 main으로 분기한다 | |
| main() 실행 … 시스템 콜을 호출한다 | ||
| 트랩 핸들러로 분기한다 | 레지스터를 커널 스택에 저장한다 커널 모드로 이동한다 | |
| syscall의 작업을 수행한다 | ||
| return-from-trap | 커널 스택으로부터 레지스터를 복원한다 사용자 모드로 이동한다 트랩 이후의 PC로 분기한다 | |
| main에서 리턴한다 trap(exit()를 통하여) | ||
| 프로세스의 메모리를 반환한다 | ||
| 프로세스 목록에서 제거한다 |
return-from-trap 을 할 때 각 프로세스의 사용하던 레지스터들의 값 같은 것들을 저장해 사용자 프로세스로 제대로 리턴시켜준다.
x86 시스템의 경우 **커널 스택(kernel stack)**에 저장한다. 그리고 return-from-trap실행 시 커널 스택에서 pop을 해서 반환한다.
트랩 테이블(trap table)
컴퓨터 부팅 시 생성되며, 운영 체제가 하드웨어에 트랩 핸들러(trap handler) 위치를 알려준다.
트랩 핸들러를 통해 하드웨어에서 어떠한 인터럽트 발생 시 하드웨어는 무엇을 할 수 있는지 알수 있다.
트랩 테이블을 활용해 트랩 핸들러 위치를 알려주는 것 또한 특권 명령어로 사용자모드로 실행 시 실행되지 않아야한다.
문제: 프로세스 간 전환
프로세스는 CPU에서 동작한다. 그 기간동안 운영체제는 실행되지 않는다. 그렇다면 어떻게 운영체제가 다시 CPU를 획득할 수 있는가?
협조 방식: 시스템 콜 기다리기
낙관적으로 프로세스가 CPU의 점유하는 것을 기다리기
운영체제가 CPU 점유를 얻을 수 있을만한 시나리오
- 프로세스에서 시스템 콜을 통한 운영체제 CPU 획득
- 프로세스에서 비정상적인 행위(0으로 나누기)로 인한 운영체재로 Trap 발생 -> CPU 획득
비협조 방식: 운영체제가 전권을 행사
프로세스가 시스템 콜 혹은 실수를 하지 않는다면 운영체제는 할 수 있는 것이 없다.
타이머 인터럽트(Timer Interrupt)
타이머 장치가 일정 시간마다 인터럽트를 발생시켜 운영체제는 **인터럽트 핸들러(interrupt handler)**로 CPU를 다시 점유한다.
물론 부팅 시 하드웨어에게 인터럽트 발생 시 실행해야할 코드를 알려주어야한다.
시스템 콜과 유사하게 동작된다.
인터럽트 발생 -> 상태 저장 -> 운영체제 -> return-from-trap -> 프로그램 다시 시작
문맥 저장/복원/교한
인터럽트 된 후 2가지 선택이 있다
- 기존 프로세스를 실행
- 새로운 프로세스 실행
운영체제의 스케줄링에 따른다. 새로운 프로세스가 시작되면 기존 레지스터 값들은 커널 스택에 저장되고 다음프로세스가 시작된다.
// 부팅 시
1. 타이머 인터럽트 핸들러 등록
2. 타이머 시작 (예: 10ms마다 인터럽트)
// 실행 중
1. 타이머 인터럽트 발생
2. 현재 프로세스 상태 저장
3. 스케줄러가 다음 프로세스 결정
4. 문맥 교환(Context Switch) 수행
xv6의 문맥 교환 코드 예시:
void switch(struct context **old, struct context *new) {
// 1. 현재 프로세스의 레지스터를 old에 저장
// 2. new로부터 새 프로세스의 레지스터를 복원
// 3. 스택 포인터를 새 프로세스의 커널 스택으로 전환
}
| 운영체제 @부트 (커널 모드) | 하드웨어 |
|---|---|
| 트랩 테이블을 초기화한다 | |
| syscall 핸들러, 타이머 핸들러의 주소를 기억한다 | |
| 인터럽트 타이머를 시작시킨다 | |
| 타이머를 시작시킨다 X msec 지난 후 CPU를 인터럽트한다 |
| 운영체제 @실행 (커널 모드) | 하드웨어 | 프로그램 (사용자 모드) |
|---|---|---|
| 프로세스 A … | ||
| 타이머 인터럽트 | ||
| A의 레지스터를 A의 커널 스택에 저장 | ||
| 커널 모드로 이동 | ||
| 트랩 핸들러로 분기 | ||
| 트랩을 처리한다 | ||
| switch() 루틴 호출 | ||
| A의 레지스터를 A의 proc 구조에 저장 | ||
| B의 proc 구조로부터 B의 레지스터를 복원 | ||
| B의 커널 스택으로 전환 | ||
| return-from-trap (B 프로세스로) | ||
| B의 커널 스택을 B의 레지스터로 저장 | ||
| 사용자 모드로 이동 | ||
| B의 PC로 분기 | ||
| 프로세스 B … |
조금 더?
- 인터럽트 중 인터럽트?
- 레지스트 저장 중 인터럽트?
생각할게 많다… Lock 과 같은 동시성 얘기를 할 수 있을 것 같다.