컴퓨터는 왜 계층적인 시스템 구조를 가지게 되었는지, 운영체제가 어떻게 장치와 상호작용하는지,
어떻게 운영체제는 CPU를 통제하면서 프로세스를 효율적으로 실행하고, 어떻게 시분할 기법을 적용하는지 알아본다.
시스템 구조
- CPU와 메인 메모리가 메모리 버스로 연결되어 있다.
- 그래픽 카드, 네트워크 카드, 사운드 카드 등 장치들은 범용 I/O 버스에 연결되어 있다. 현대 시스템에서는 PCI(Peripheral Component Interconnect)를 사용하고 있다. 이는 plug and play를 지원하여 고속 I/O 장치들을 메인보드에 쉽게 추가하거나 교체할 수 있게 한다.
- 마우스, 키보드, 디스크 같은 상대적으로 느린 장치들은 주변장치용 버스에 연결되어 있다.
왜 계층적인 시스템 구조가 생기는가?
- 버스가 고속화되려면 더 짧아져야 하고, 고속 메모리 버스는 주변 장치들을 수용할 공간이 없다. 또한 상대적으로 고성능을 내는 버스는 비싸기 때문이다.
- 즉, 계층적 구조를 택하여 고성능 장치들은 CPU에 가깝게 배치하였고, 느린 성능의 장치들은 그보다 멀리 배치한다. 디스크처럼 느린 장치들을 주변 I/O 버스에 연결하여 많은 장치들을 연결할 수 있게 한다.
운영체제 장치 제어 방법
- 운영체제가 장치를 제어한다는 사실을 알고 있다. 어떻게 제어하는지 어떻게 기술이 발전해 갔는지 알아보자.
1. 폴링(Polling)
- 단순화된 장치의 인터페이스는 세 개의 레지스터로 구성될 수 있다. 상태(Status) 레지스터는 장치의 현재 상태를 읽을 수 있고, 명렁어(Command) 레지스터는 장치가 특정 동작을 수행하도록 요청할 때, 그리고 데이터(Data) 레지스터는 장치에 데이터를 보내거나 받거나 할 때 사용한다.
- 운영체제와 장치 동작 코드
while (STATUS == BUSY)
;
데이터를 DATA 레지스터에 쓰기
명령어를 COMMAND 레지스터에 쓰기 (그러면 장치가 명령어를 실행한다)
while (STATUS == BUSY)
;
- 먼저 반복문으로 장치의 상태 레지스터를 읽어서 명령의 수신 가능 여부를 확인한다. (Polling)
- 운영체제가 데이터 레지스터에 어떤 데이터를 전달한다. 데이터 전송에 메인 CPU가 관여하는 경우를 Programmed I/O라고 한다.
- 운영체제가 명령 레지스터에 명령어를 기록한다. 이 레지스터에 명령어가 기록되면 데이터는 이미 준비되었다고 판단하고 명령어를 처리한다.
- 마지막으로 운영체제는 디바이스가 처리를 완료했는지 확인하는 풀링 반복문을 돌면서 기다린다. (성공과 실패 에러 코드를 받게 된다)
- 기본 방식은 간단하며 제대로 동작하지만, 매우 비효율적이다. 다른 프로세스에 CPU를 양도하지 않고, 장치가 동작으로 완료하는 동안 계속 루프를 돌면서 장치 상태를 검사하고 있다.
어떻게 하면 CPU 오버헤드를 줄일 수 있을까?
2. 인터럽트(Interrupt)
- 장치를 풀링하는 대신 운영체제는 입출력 작업을 요청한 프로세스의 CPU를 빼앗아 다른 프로세스에 양도한다. 장치가 작업을 끝마치면 하드웨어 인터럽트를 발생시키고 CPU는 운영체제가 미리 정의해놓은 인터럽트 서비스 루틴(interrupt service routine, ISR) 또는 간단하게 인터럽트 핸들러를 실행한다. 즉, 인터럽트를 활용하여 A프로세스가 I/O를 하는 시간에 B프로세스에게 CPU를 주는 것이다.
인터럽트는 항상 최적의 해법을 제공하는가? 아쉽게도 아니다. 대부분의 작업이 한 번의 폴링만으로 끝날 정도로 매우 빠른 장치인 경우에는 인터럽트를 사용하게 되면 오히려 느려진다. 즉, 인터럽트는 느린 장치에 대해서만 효과적이다. 그 외의 경우 인터럽트 처리와 문맥 교환 비용이 인터럽트의 이점을 넘어서게 된다.
- 네트워크 대용량 사용자 요청이 아주 빈번하게 인터럽트로 처리하게 된다면, 인터럽트만 처리하다가 사용자의 요청을 처리할 수 없는 현상을 겪을 것이다.
3. DMA(Direct Memory Access)
- 빠른 입출력 장치들은 대량의 데이터들을 전송해야 한다. DMA는 CPU의 중재 없이 device controller가 buffer 내용을 메모리에 block 단위로 직접 전송한다.
- DMA controller는 바이트 단위가 아닌 블럭 단위로 인터럽트를 발생시켜 운영체제에게 데이터 전송이 완료되었음을 알린다.
제한적 직접 실행
- 운영체제 개발자들은 프로그램을 빠르게 실행하기 위하여 제한적 직접 실행(Limited Direct Execution)이라는 기법을 개발하였다. 이는 프로그램을 CPU 상에서 직접 실행시키는 것이다.
운영체제 | 프로그램
- 프로세스 목록 항목 생성
- 프로그램 메모리 할당
- 메모리에 프로그램 탑재
- argc/argv를 위한 스택 셋업
= 레지스터 내용 삭제
- call main() 실행
- main() 실행
- main에서 return 실행
- 프로세스 메모리 반환
- 프로세스 목록에서 항목 제거
- 효율적으로 동작하지만 2가지 의문점이 발생한다. 첫째로, 해당 프로그램을 신뢰할 수 있는가에 대한 문제이다. 프로세스가 원하는 모든 기능을 제공하면 보안에 취약해진다. 둘째로, 시분할 기법을 적용하기 위해 어떻게 프로세스간 전환을 할 수 있는지에 대한 문제이다. 이에 대한 해결책을 하나씩 알아본다.
1. 사용자 모드와 커널 모드로 구분 (mode bit)
- 어떻게 사용자 프로그램에 제한된 연산을 제공할 수 있을까? 운영체제 뿐만 아니라 하드웨어 지원이 필수적이다.
- mode bit을 통해 사용자 모드와 커널 모드를 구분한다. 사용자 모드(mode bit = 1)에서는 제한적인 기능만 가능하다. 예를 들어 자신의 프로세스 메모리 공간에만 접근이 가능하다던지, 입출력 작업을 할 수 없게 만드는 것이다. 만약 다른 프로세스 메모리 공간에 접근하거나 입출력 작업을 한다면 프로세서가 예외를 발생시키고, 운영체제는 해당 프로세스를 제거한다.
사용자가 입출력 작업 같은 특권 명령어를 실행해야 할 때는 어떻게 하는가?
- 사용자 프로세스에 시스템 콜을 제공하여 파일 시스템 접근, 프로세스 생성 및 제거, 다른 프로세스와의 통신을 가능하게 한다.
- 시스템 콜을 실행하기 위해서는 trap 특수 명령어를 실행해야 한다. 이 명령어는 커널 안으로 접근하는 동시에 특권 수준을 커널 모드로 상향한다. 이를 통해 특권 명령을 실행한 후 retrun-from-trap 특수 명령어로 호출하여 특권 수준을 다시 사용자 모드로 하향시킨다. 이를 위해 program counter, 해당 프로그램의 정보들을 커널 스택(kernel stack)에 저장한다.
- 이를 위해 커널은 부팅 시에 트랩 테이블(trap table)을 만들고 예외 사건이 일어났을 때 어떤 코드를 실행하는지 알 수 있게 한다. 운영체제는 특정 명령어를 사용하여 하드웨어에게 트랩 핸들러의 위치를 알려준다. 이 또한 특권 명령이다.
1. 사용자 프로그램은 운영체제에 I/O를요청
2. trap을 사용하여 인터럽트 벡터의 특정 위치로 이동
3. 제어권이 인터럽트 벡터가 가리키는 인터럽트 서비스 루틴으로 이동
4. 올바른 I/O 요청인지 확인 후 I/O를 수행
5. I/O 완료 시 제어권을 시스템콜 다음 명령으로 옮김
2. 프로세스 간 전환 지원 (timer)
- 운영체제는 실행 중인 프로세스를 계속 실행할 것인지, 멈추고 다른 프로세스를 실행할 것인지 결정해야 한다. 하지만 특정 프로세스를 실행하고 있다는 것은 운영체제가 실행 중이지 않다는 것을 의미한다.
운영체제는 어떻게 다시 CPU를 획득할 수 있는가?
- 프로세스가 시스템 콜을 호출해서 CPU 제어권을 운영체제에 넘겨주면 다행이지만, 그렇지 않을수도 있다. 이 경우 타이머 인터럽트(Timer Interrupt)를 이용하여 일정 시간이 지나면 현재 수행 중인 프로세스를 중단시키고 미리 구성된 운영체제의 인터럽트 핸들러(interrupt handler)가 실행시킨다.
- 다른 프로세스로 전환하기로 결정했다면 문맥 교환(context switch) 코드가 실행된다. 이는 현재 실행 중인 프로세스의 레지스터 값을 커널 스택 같은 곳에 저장하고, 곧 실행될 프로세스의 커널 스택으로부터 레지스터 값을 복원하는 것이다.
참고자료
- 2014 이화여대 반효경 운영체제 강의
- 운영체제, 아주 쉬운 세 가지 이야기