control해서 CPU를 얼마나 효율적으로 가상화할 수 있을까?
OS는 time sharing을 통해 physical CPU를 공유할 필요가 있다.
- 문제
Performance
: 우리는 어떻게 추가적인 오버헤드 없이 가상화를 구현할 수 있을까?
Control
: 어떻게 우리는 CPU 컨트롤을 유지하면서 효율적으로 프로세스를 실행시킬 수 있을까?
Direct Execution
- 단순히 CPU에서 직접적으로 프로그램을 실행한다고 가정해보자.
- OS에서 프로세스 리스트에서 entry를 생성하고
- 프로그램의 메모리를 할당하고
- 메모리에서 프로그램을 적재하고
- argc, argv로 stack을 설정한다음
- 레지스터를 초기화하고
- main 함수를 불러서 실행하면
- Program에서 main함수를 실행하고
- return을 불러서 실행하면
- OS에서 다시 프로세스의 메모리를 해제하고
- 프로세스 리스트에서 제거한다.
💡 실행중인 프로그램에서 limits(제한) 없이 os는 어느것도 제어할 수 없다. 그저 Library가 될 뿐이다.
Restricted Operation
문제 1. 제한된 명령어
만약 프로세스가 디스크에서 I/O 요청하는 이슈라던가 CPU 또는 메모리같은 시스템 자원에 더 많은 접근을 얻는 제한된 명령어 종류를 수행하기 원한다면?
솔루션 : 보호되는 control transfer를 사용한다.
- User mode: 하드웨어 자원에 대한 완전한 접근을 가지지 않는 어플리케이션
- Kernel mode: OS는 기계의 모든 자원에 대한 접근 권한을 가짐.
System call
- kernel이 조심스럽게 특정 주요 기능을 유저 프로그램에게 보여주는것을 허락한다. 예를들면,
- 파일 시스템 접근
- 프로세스 생성 및 제거
- 다른 프로세스와의 의사소통
- 더 많은 메모리 할당
Trap Instruction
- kernel로 jump한다.
- privilege level(권한 레벨)을 kermel mode로 올린다.
Return-from trap instruction
trap instruction으로부터 얻는 return 값은?
- user program이라고 불리는 곳으로부터 리턴된다.
- privilege level을 user mode로 다시 낮춘다.
trap은 어느 코드가 OS안에서 실행되어야하는지 어떻게 아는걸까?
- trab table과 trab handler가 있기 때문
trab table
- interrupt descriptor table 또는 interrupt vector table 라고도 불림
trap handler
- 코드는 프로그램이 trap 명령어를 실행할 때 실행되어야 한다.
System-call number
- 각각의 시스템 콜은 시스템 콜 number가 배정되어 있다.
- 따라서 user code는 레지스터에 원하는 시스템 콜 번호를 배치할 책임이 있다.
System call Handler
→ 원래 이렇게 유저 프로그램에서 printf 함수 실행하면 해당 라이브러리를 찾고 그 안에 시스템콜 원형인 write를 찾아서 kernel모드에서 write 시스템 콜을 수행하고 이것이 terminal output에 보인다.
→ glibc에서 찾은 것은 결국 system call stub에서 찾을 수 있고, 잘 보면 write, printf는 write 시스템콜을 strcpy는 그 자체 open, fdopen은 open 시스템콜을 kernel level에서 찾아서 수행한다.
위 사진은 fork() 시스템 콜을 수행했을 때의 그림이다.
먼저 User program에서 fork() 함수를 수행하면 libc.a라는 라이브러리 파일에서 fork()를 찾는다.
movl 2, %eax
int $0x80
이 코드를 확인할 수 있는데, 여기서 eax는 레지스터이고 int 뒤에 쓰인 것은 Interrupt vector table에 저장된 system call 주소이다.
- 참고로 아까 interrupt vector table의 다른 말 2가지 더 있었다. = interrupt descriptor table = trap handler
하여튼 Interrupt vector table의 0x80번지를 살펴보자. 이 함수를 통해 kernel에서 system call entry에 접근할 수 있고, 여기서 정의된 sys_call table에서 아까 %eax로 가져온 2라는 값이 system call table의 2번에 있다. 2번에 저장된 sys_fork() 에서 fork의 구현이 가능하다.
Limited Direction Execution Protocol
-
os에서 trap table을 초기화한다.
-
process list를 위해 entry 를 생성한다.
프로그램의 메모리를 할당한다.
메모리에 프로그램을 적재한다.
argv로 user stack을 설정한다.
reg, PC로 kernel stack을 채운다.
-
kernel stack에 의해 레지스터가 다시 저장된다.
user mode로 이동하고, main으로 jump한다.
-
user mode에서 main함수를 실행하고 system call → os로 trap
-
위 과정을 계속 반복하다가
program에서 exit 호출을 trap하면서 return되면
-
OS에서 프로세스의 메모리를 해제하고
프로세스 리스트에서 제거한다.
문제 2. 프로세스 간 스위칭
- 어떻게 OS는 프로세스 사이에서 스위치 하기 위해 cpu의 제어권을 다시 얻을 수 있을까?
- 협력적인 접근법 : Wait for system calls
- 비협력적인 접근법 : The OS takes control
→ system call을 기다리면서 제어권을 주기를 기다리고, 프로세스가 정말로 잘 주면 좋겠지만, 비협력적일 수도 있다. 이럴땐 OS가 강제로 빼앗아야 한다.
Wait for system calls
협력적인 접근법 : 시스템 콜을 기다린다.
프로세스는 주기적으로 CPU를 포기한다. yield
와 같은 시스템콜을 만듦으로써.
- OS는 몇몇 다른 일을 수행하기로 결정한다.
- 어플리케이션이 불법적인 일을 할 때 OS로 제어권을 이전하기도 한다.
- 0으로 나누거나
- 엑세스 할 수 없는 메모리에 접근하거나
- ex) Mancintosh OS의 초기 버전, 구 제록스 알토 시스템
💡 process는 무한 루프에 갇힌다. → **Reboot the machine**
OS Takes Control
비협력적인 접근법 : OS가 접근권을 가진다.
- Timer Interrupt → 커널을 실행한다.
- 부팅 시퀀스 중에 OS가 타이머를 시작한다.
- 타이머는 몇 밀리초마다 인터럽트를 발생시킨다.
- 인터럽트가 발생한 경우
- 현재 실행중인 프로세스가 중지된다.(halt)
- 프로그램의 상태를 충분히 저장한다.
- OS에서 미리 구성된 인터럽트 핸들러가 실행된다.
💡 Timer Interrupt는 OS가 cpu에서 다시 실행할 능력을 준다.
Saving and Restoring Context
Context Switch
- 어셈블리 코드의 low-level
- current process의 몇몇 레지스터 값을 그 커널의 stack에 저장한다.
- 일반적인 purpose register와
- PC 값과
- kernel stack pointer 들을
- 커널 스택에서 곧 실행될 프로세스를 위해 몇가지를 다시 저장한다.
- 곧 실행될 프로세스를 위해 커널 스택으로 스위치한다.
→ trap table을 초기화하고 interrupt timer를 시작한다.
→ 프로세스 A를 실행하다가
→ Hardware에서 Timer interrupt가 발생하면
→trap을 처리하고 switch 함수를 실행한다.
→ 현재 실행중인 프로세스의 레지스터 값들을 kernel의 stack에 저장한다.
동시성에 관한 걱정?
interrupt나 trap handling 하는 중에 무언가 발생한다면 다른 인터럽트가 발생할까?
OS 는 이 상황들을 다룬다.
- interrupt processing 하는 도중 interrupt는 불가능하다.
- 내부 데이터 구조에 대한 동시 접근을 보호하기 위해서 여러가지 정교한 잠금 방식을 사용한다.