운영체제 아주 쉬운 세가지 이야기 책에 대한 스터디를 진행한다.
이 글에서는 가상화에 대해 다룬 3장부터 6장까지의 내용을 정리한다.
3장. 가상화에 관한 대화
대표적으로 자원인 CPU 로 예를 든다.
가상화는 한 개 또는 소수의 CPU 를 각 응용 프로그램에게 여러 개의 CPU인 것처럼 보이도록 한다.
4장. 프로세스 개념
- 프로세스
- 실행 중인 프로그램
- 프로그램
- 디스크 상에 존재하며, 실행을 위한 명령어와 정적 데이터의 묶음
- 시분할(time sharing) 기법
- 한 프로세스를 중단하고 다른 프로세스를 실행하는 작업을 반복하며 CPU를 가상화
- 사용자는 CPU가 있는지 신경쓰지 않아도 프로그램을 동시에 실행 가능
- CPU 를 공유하기 때문에 프로세스의 성능은 낮아짐
- 어떤 프로그램을 실행할지는 운영체제의 스케줄링 정책(scheduling policy) 에 의해 결정
4.1 프로세스의 개념
프로세스는 실행 중인 프로그램이다.
프로세스의 하드웨어 상태 중 중요한 구성 요소는 메모리와 레지스터다.
- 메모리(주소 공간, address space)
- 명령어 저장
- 읽고 쓰는 데이터 저장
- 레지스터
- 많은 명령어들이 레지스터를 읽거나 갱신
- 프로그램 카운터(program counter, PC) = 명령어 포인터(instruction pointer, IP)
- 어느 명령어가 실행 중인지 알려줌
- 스택 포인터(stack pointer) 와 프레임 포인터(frame pointer)
- 함수의 변수와 리턴 주소를 저장하는 스택을 관리
4.2 프로세스 API
- 생성 (Create)
- 새로운 프로세스를 생성하는 인터페이스
- 제거 (Destroy)
- 프로제스를 강제로 제거하는 인터페이스
- 대기 (Wait)
- 어떤 프로세스의 실행 중지를 기다리는 인터페이스
- 각종 제어(Miscellaneous Control)
- 여러 가지 제어 기능 제공 (ex. 프로세스 일지정지, 재개, 다시 시작)
- 상태(Status)
- 상태 정보를 얻어내는 인터페이스
4.3 프로세스 생성
- 운영체제는 프로그램 코드와 정적 데이터(static data) 를 메모리, 프로세스의 주소공간에 탑재(load)
- 디스크의 코드와 정적 데이터 바이트를 읽어서 메모리에 저장
- 현대 운영체제는 필요한 부분만 메모리에 탑재, 페이징(paging) 과 스와핑(swapping) 동작 이해 필요
- 특정 크기의 메모리 공간이 프로그램에 스택(run-time stack) 용도로 할당
- 지역 변수, 함수 인자, 리턴 주소 등을 저장하기 위한 용도
- 힙(heap) 을 위한 메모리 영역 할당
- 동적으로 할당된 데이터를 저장하기 위한 용도
- 연결 리스트, 해시 테이블, 트리 등 크기가 가변적인 자료 구조를 위해 사용
- 입출력과 관계된 초기화 작업 수행
- 프로그램 실행을 위한 준비(프로그램의 시작 지점, entry point)
- CPU 를 새로 생성된 프로세스에게 넘기고 실행
4.4 프로세스 상태
- 실행(Running)
- 프로세서에서 실행 중 (명령어를 실행 중)
- 준비(Ready)
- 실행할 준비가 되어있지만 운영체제가 다른 프로세스를 실행하는 등의 이유로 대기
- 대기(Blocked)
- 다른 사건을 기다리는 동안 프로세스의 수행을 중단 (ex. 디스크에 대한 입출력)
이외에도 다른 상태들도 존재
- 초기 상태 (initial)
- 프로세스가 완전히 생성되기 전까지의 상태
- 최종 (final) 상태 = 좀비(zombie) 상태
- 프로세스는 종료되었지만 사용하던 자원들이 반납되지 않은 상태
- 부모(parent) 프로세스가 성공적으로 실행을 마쳤는지 파악하는 데 유용
- 부모 프로세스는 자식 프로세스의 종료를 대기하는 시스템 콜 호출 (ex.
wait()
)
- Process0 에서 입출력을 요청하고 작업이 완료되기를 기다리면서 대기 상태로 전이
- 운영체제는 Process0 이 CPU 를 사용하지 않는 것을 감지하고 Process1 실행
- Process1 이 실행되는 동안 Process0 은 준비 상태
- Process1 종료 후, Process0 실행되어 종료
4.5 자료구조
운영체제도 일종의 프로그램으로 다양한 정보를 유지하기 위해 자료 구조를 소유
- 프로세스 리스트
- 프로세스 상태를 파악하기 위한 리스트
- 레지스터 문맥(register context) 자료구조
- 프로세스가 중단되었을 때 프로세스의 레지스터 값 저장
- 레지스터 값을 복원하여 프로세스 실행 재개 (문맥 교환, context switch)
5장. 프로세스 API
Unix 시스템의 프로세스 생성에 관해 소개한다.
5.1 fork() 시스템 콜
fork()
프로세스 생성에 사용되는 시스템 콜
- 일반적으로 생성한 프로세스는 부모 프로세스, 새로 생성된 프로세스는 자식 프로세스
- 자식 프로세스는 자신의 주소공간, 레지스터, PC 값을 가짐 (부모 프로세스와 다름)
fork()
반환 값- 부모 프로세스: 자식 프로세스 PID(process identifier)
- 자식 프로세스: 0
5.2 wait() 시스템 콜
부모 프로세스가 자식 프로세스의 종료를 대기하는 시스템 콜
5.3 exec() 시스템 콜
자기 자신이 아닌 다른 프로그램을 실행
새로운 프로세스를 생성하는 것이 아닌 현재 실행 중인 프로그램을 다른 프로그램으로 대체
fork()
: 자신의 복사본을 생성하여 실행
exec()
: 복사본이 아닌 다른 프로그램을 실행
exec() 실행 과정
- 실행 파일의 이름과 인자가 주어지면 코드와 정적 데이터를 읽어 현재 실행중인 프로세스의 코드 세그멘트와 정적 데이터 부분을 덮어 쓰기
- 힙과 스택 및 프로그램 다른 주소 공간들로 초기화
- 프로세스의 인자를 전달하여 프로그램 실행
5.4 왜 이런 API를?
Unix 의 쉘(사용자 프로그램)을 구현하기 위해서는 fork()
와 exec()
분리가 필요
그래야 쉘이 fork()
를 호출하고 exec()
호출하기 전에 코드를 실행할 수 있다.
그래서 쉘은 많은 유용한 일을 쉽게 할 수 있다.
5.5 프로세스 제어와 사용자
Unix 시스템에는 fork()
, exec()
, 및 wait()
외에도 많은 프로세스 인터페이스가 있다.
kill()
시스템 콜- 프로세스에게 멈추거나 끝내기와 같은 시그널(signal)을 보내는 데 사용
- 대부분 단축키 설정 되어 있음
- control-c : INGINT(인터럽트) 시그널로 종료시키는 단축키
- control-z : SIGSTP(멈춤) 시그널로 실행 도중에 프로세스 멈춤
signal()
시스템 콜- 외부 사건을 프로세스에게 전달
- 개별 프로세스 또는 그룹 단위로 시그널을 받거나 처리
5.6 유용한 도구들
ps
- 어떤 프로세스가 실행 중인지 알아보기 위해 사용
top
- 시스템에 존재하는 프로세스와 그 프로세스가 CPU 및 자원들을 얼마나 사용하는지 알아보기 위해 사용
kill
- 프로세스에 임의의 시그널을 보낼 때 사용
- MenuMeter 를 Macintosh 의 toolbar 에 실행시키면 CPU 이용률 점검 가능
6장. 제한적 직접 실행 원리(Limited Direct Execution)
운영체제는 CPU 시간을 나누어 씀으로써 가상화를 구현한다.
하지만 가상화 기법을 구현하기 위해서는 다음과 같은 문제점을 해결해야 한다.
- 성능 저하
- 시스템에 과도한 오버헤드를 주지 않아야 함
- 제어 문제
- 운영체제가 자원 관리의 책임자로 제어권을 유지해야 함
6.1 기본 원리: 제한적 직접 실행
제한적 직접 실행(Limited Direct Execution)
-
운영체제 개발자들은 프로그램을 빠르게 시작하기 위해 개발
- CPU 상에서 직접 프로그램을 실행
- 운영체제가 프로세스 목록에 해당 프로세스 항목을 만들고 메모리에 할당, 코드를 디스크에서 탑재하고 진입점을 찾아 코드 실행
- CPU 를 가상화하면서 몇 가지 문제가 발생
- 프로그램이 운영체제가 원치않는 일을 할 수 있음
- 어떻게 프로그램의 실행을 중단하고 다른 프로세스로 전환할 것인가 (어떻게 시분할 기법을 구현할 것인가)
6.2 문제점 1: 제한된 연산
기본적으로 프로그램이 하드웨어 CPU 에서 실행되기 때문에 빠르게 실행된다.
하지만 프로세스에서 디스크 입출력이나 시스템 자원 추가 할당 같은 특수한 연산을 수행하길 원한다면 문제가 발생한다.
이러한 문제 때문에 실행되는 코드를 제한하는 사용자 모드(user mode) 가 도입되었다.
이와 대비되는 모드로 커널 모드(kernel mode) 에서는 운영체제의 중요한 코드들이 실행된다.
사용자 프로세스에서 특권 명령어 실행 과정
- 하드웨어는 특권 명령어를 실행하기 위해서 사용자 프로세스에게 시스템 콜을 제공
- 시스템 콜을 실행하기 위해 프로그램은 trap 명령어를 실행
- trap 명령어로 특권 수준을 커널 모드로 상향 조정하고 요청한 작업을 처리
- 프로세스의 필요한 레지스터들(ex. 프로그램 카운터, 플래그) 커널 스택에 저장
- 처리가 완료되면 return-from-trap 명령어를 호출하여 특권 수준을 하향 조정 및 사용자 프로그램으로 반환
- 커널 스택에서 팝(pop) 하여 사용자 모드 다시 시행
커널은 부팅 시에 트랩 테이블(trap table) 만들어 시스템을 통제한다.
컴퓨터가 부트될 때 커널 모드에서 동작하여 특정 명령어로 하드웨어에게 트랩 핸들러(trap handler) 의 위치를 알려준다.
하드웨어는 이 위치를 저장하고 있다가 시스템 콜의 고유 번호를 통해 처리가 가능하다.
6.3 문제점 2: 프로세스 간 전환
직접 실행의 두 번째 문제는 프로제스의 전환이다.
협조 방식: 시스템 콜 호출시 까지 대기
협조(cooperative) 방식은 각 사용자 프로세스가 비정상적인 행동은 하지 않을 것으로 가정
CPU를 장기간 사용하는 프로세스들이 다른 프로세스가 사용할 수 있도록 주기적으로 CPU 를 반납할 것이라 믿음
- 협조 방식을 사용하는 운영체제는
yield
시스템 콜로 다른 프로세스에세 CPU 를 할당할 수 있는 기회 제공 - 응용 프로그램이 비정상적인 행동을 하면 트랩이 일어나 CPU 획득하여 해당 프로세스 종료
- 협조 방식의 스케줄링 시스템은 근본적으로 수동
- 시스템 콜 호출을 기다리거나 불법적인 연산을 대기
- 프로세스가 무한 루프에 빠져서 시스템 콜을 호출할 수 없으면 문제 발생
비협조 방식: 운영체제가 제어권 확보
비협조 방식은 타이머 인터럽트(timer interrupt) 로 프로세스를 중단하고 인터럽트 핸들러(interrupt handler) 를 실행
인터럽트 핸들러는 운영체제의 일부로
인터럽트를 처리하는 과정에서 실행중인 프로세스를 중단하고 운영체제에게 CPU 제어권을 넘겨 필요한 작업을 수행
문맥의 저장과 복원
운영체제의 스케줄러(scheduler) 는 실행중인 프로세스를 계속 실행할 지, 전환할 지를 결정한다.
프로세스 전환을 결정하면 운영체제는 문맥 교환(context switch) 코드를 실행한다.
문맥 교환은 실행 중인 프로세스의 레지스터 값들을 커널 스택 같은 곳에 저장하고 실행될 프로세스의 레지스터 값을 복원하는 것이다.
6.4 병행실행으로 인한 문제
인터럽트나 트랩을 처리하는 도중에 다른 인터럽트가 발생할 때 주의가 필요하다.
간단한 해결책으로 인터럽트를 처리하는 동안에는 인터럽트를 불능화 하는 것이다.
하지만 이러한 기법은 인터럽트를 장기화 불능화하는 경우 손실되는 인터럽트가 발생될 수 있어서 신중하게 사용해야 한다.
운영체제는 커널 내부의 자료 구조들이 락(lock) 으로 보호되기 때문에 내부에서 다수의 작업들이 동시에 진행되는 것이 가능하다.
하지만 구성과 작동이 매우 복잡해지고 이 때문에 문제점과 버그들이 발생한다.