Search
Duplicate

운영체제/ 시스템 구조

운영체제 서비스

운영체제는 프로그램 실행 환경을 제공한다. 운영체제는 프로그램과 그 프로그램의 사용자에게 특정 서비스를 제공한다. 운영체제 서비스는 프로그래머가 프로그래밍을 쉽게 할 수 있는 편리함을 제공한다. 그림 2.1은 운영체제 서비스의 다양함과 그들이 어떻게 연관되어 있는지를 보여주는 관점을 보여준다.
사용자 인터페이스(User Interface)
거의 모든 운영체제는 사용자 인터페이스를 제공한다. 이 인터페이스는 여러 형태로 제공될 수 있는데, 그 중 하나는 명령어 라인 인터페이스(command-line interface, CLI)로서 문자열 명령과 이를 입력할 수 있는 방법을 사용한다.
다른 형태는 명령어와 명령어를 제어하는 디렉티브(directive)가 파일 형태로 입력되고 그 파일이 실행되는 배치 인터페이스(batch interface)이다.
그래피컬 사용자 인터페이스(graphical user interface, GUI)가 가장 보편적이다. 이 인터페이스는 입출력을 지시하고, 메뉴를 선택하고 선택하여 키보드로 문자열을 입력할 수 있는 위치 결정 장치를 가지는 윈도 시스템을 말한다. 몇몇 시스템은 둘 또는 세 가지 모두의 형태를 제공한다.
프로그램 수행(Program execution)
시스템은 프로그램을 메모리에 적재해 실행할 수 있어야 한다. 프로그램은 정상적이든 혹은 비정상적이든 (오류를 표시하면서) 실행을 끝낼 수 있어야 한다.
입출력 연산(I/O operation)
수행 중인 프로그램은 입출력을 요구할 수 있다. 이러한 입출력에는 파일 혹은 입출력 장치가 연관될 수 있다. 특정 장치에 대해서는 특수한 기능이 요구될 수 있다. 효율과 보호를 위해 사용자들은 통상 입출력 장치를 직접 제어할 수 없다. 따라서 운영체제가 입출력 수행의 수단을 제공해야 한다.
파일 시스템 조작(File system manipulation)
파일 시스템은 특히 중요한 분야이다. 명백히 프로그램은 파일을 읽고 쓸 필요가 있다. 프로그램은 또한 이름에 의해 파일을 생성하고 삭제할 수 있고 지정된 파일을 찾을 수 있어야 하고 파일의 정보를 열거할 수 있어야 한다.
마지막으로 몇몇 프로그램은 파일 소유권에 기반을 둔 권한 관리를 이용하여 파일이나 디렉터리의 접근을 허가하거나 거부할 수 있게 한다. 많은 운영체제들은 때로는 개인의 선택에 의해 그리고 떄로는 특정 특성과 성능 특성을 제공하기 위해 다양한 파일 시스템을 제공한다.
통신(Communication)
한 프로세스가 다른 프로세스와 정보를 교환해야 할 필요가 있는 여러 상황이 있다. 이러한 통신을 수행하는 두 가지 중요한 방법이 있다. 첫 번째는 동일한 컴퓨터에서 수행되고 있는 프로세스들 사이에서 일어나고, 두 번째는 컴퓨터 네트워크에 의해 함께 묶여 있는 서로 달느 컴퓨터 시스템 상에서 수행되는 프로세스들 사이에서 일어난다.
통신은 공유 메모리를 통해 구현될 수도 있고, 메시지 전달(message passing) 기법에 의해 구현될 수도 있는데, 후자의 경우 정보의 패킷들이 운영체제에 의해 프로세스들 사이를 이동한다.
오류 탐지(Error detection)
운영체제는 모든 가능한 오류를 항상 의식하고 있어야 한다. 오류는 CPU, 메모리, 하드웨어, 입출력 장치, 또는 사용자 프로그램에서 일어날 수 있다.
운영체제는 올바르고 일관성 있는 계산을 보장하기 위해 각 유형의 오류에 대해 적당한 조치를 취해야 한다. 물론 운영체제가 오류에 어떻게 반응하며 수정하는가에 대한 다양한 변종이 존재한다. 디버깅 설비는 시스템을 효율적으로 사용할 수 있는 사용자와 프로그래머의 능력을 향상시킨다.
사용자에게 도움을 주는 것이 목적이 아니라 시스템 자체의 효율적인 동작을 보장하기 위한 운영체제 기능들도 존재한다. 다수의 사용자가 사용하는 시스템에서는 사용자들 간 컴퓨터 자원을 공유하게 함으로써 효율성을 얻을 수 있다.
자원 할당(Resource allocation)
다수의 사용자나 다수의 작업들이 동시에 실행될 때 그들 각각에 자원을 할당해 주어야 한다. 운영체제는 여러 가지 다른 종류의 자원을 관리한다. 어떤 것(예컨대 CPU 사이클, 주 메모리, 파일 저장 장치 등)들은 특수한 할당 코드를 가질 수 있는 반면 다른 것(예컨대 입출력 장치 등)들은 훨씬 일반적인 요청과 방출 코드를 가질 수 있다.
예컨대 CPU를 최대한 효율적으로 이용하기 위해 운영체제는 CPU 스케쥴링 루틴이 CPU의 속도, 반드시 실행해야 할 작업들, 사용 가능한 레지스터의 수와 다른 요인들을 고려하도록 해야 한다. 또한 프린터, USB 저장장치 드라이브 및 다른 주변 장치를 할당하는 루틴이 있을 수 있다.
회계(Accounting)
우리는 사용자가 어떤 종류의 컴퓨터 자원을 얼마나 많이 사용하는지를 추적할 수 있기를 원한다.
이와 같은 기록 관리는 회계 또는 단순히 사용 통계를 내기 위해 사용된다. 사용 통계는 컴퓨팅 서비스를 개선하기 위해 시스템을 재구성하고자 하는 연구자에게 귀중한 자료가 될 수 있다.
보호(Protection)와 보안(Security)
다중 사용자 컴퓨터 시스템 또는 네트워크로 연결된 컴퓨터 시스템에 저장된 정보의 소유자는 그 정보의 사용을 통제하길 원한다. 서로 다른 여러 프로세스가 병행하게 수행될 떄, 한 프로세스가 다른 프로세스나 운영체제 자체를 방해해서는 안 된다.
보호는 시스템 자원에 대한 모든 접근이 통제되도록 보장하는 것을 필요로 한다. 외부로부터의 시스템 보안 또한 중요하다. 이러한 보안은 각 사용자가 자원에 대한 접근을 원할 때 통상 패스워드를 사용해서 시스템에게 자기 자신을 인증하는 것으로부터 시작된다.
보안은 네트워크 어댑터 등과 같은 외부 입출력 장치들을 부적합한 접근 시도로부터 지키고, 침입의 탐지를 위해 모든 접속을 기록하는 것으로 범위를 넓힌다.
만약 시스템이 보호되고 보안이 유지되려면 시스템 전체에 걸쳐 예방책(precaution)이 제정되어야 한다. 하나의 사슬은 가장 약한 연결 고리만큼 강한 법이다.

사용자 운영체제 인터페이스

명령 해석기(Command-Interpreter)

어떤 운영체제는 커널에 명령 해석기를 포함하고 있다. Windows나 UNIX 같은 다른 운영체제는 명령 해석기를 작업이 시작되거나 사용자가 처음 로그온 할 떄 수행되는 특수한 프로그램으로 췰급한다.
선택할 수 있는 여러 명령어 해석기를 제공하는 시스템에서 이 해석기는 셸(shell)이라 불린다.
명령어 해석기의 중요한 기능은 사용자가 지정한 명령을 가져와서 그것을 수행하는 것이다. 이 수준에서 제공된 많은 명령들은 파일을 조작한다. MS-DOS와 UNIX 셸은 이런 방식으로 실행된다.
이 명령어들은 두 가지 일반적인 방식으로 구현될 수 있다. 한 가지 방법은 명령 해석기 자체가 명령을 실행할 코드를 갖고 있는 경우이다.
예컨대 한 파일을 삭제하기 위한 명령은 명령 해석기가 자신의 코드의 한 부분으로 분기하고 그 코드 부분이 매개 변수를 설정하고 적절한 시스템 호출을 한다.
이 경우 제공될 수 있는 명령의 수가 명령 해석기의 크기를 결정하는데, 그 이유는 각 명령이 자신의 구현 코드를 요구하기 때문이다.
여러 운영체제 중 UNIX에 의해 사용되는 다른 대안의 접근 방법은 시스템 프로그램에 의해 대부분의 명령을 구현하는 것이다.
이러한 경우 명령 해석기는 전혀 그 명령을 알지 못한다. 단지 메모리에 적재되어 실행될 파일을 식별하기 위해 명령을 사용한다. 따라서 파일을 삭제하는 아래의 UNIX 명령은 rm이라 불리는 파일을 찾아서 그 파일을 메모리에 적재하고, 그것을 매개변수 file.txt로 수행한다. rm 명령과 관련된 기능은 rm이라는 파일 내의 코드로 완전하게 정의된다.
이러한 방법으로 프로그래머는 적합한 이름을 가진 새로운 파일을 생성함으로써 시스템에 새로운 명령을 쉽게 추가할 수 있다. 명령 해석기 프로그램은 아주 작아질 수 있으며, 새로운 명령을 추가하기 위해 변경될 필요가 없다.
rm file.txt
Shell

그래피컬 사용자 인터페이스(Graphical User Interface)

그래피컬 사용자 인터페이스는 1970년대 초 Xerox PARC 연구 센터에서 수행된 연구의 일부로부터 기인하고, 최초의 GUI는 1973년 출시된 Xerox Alto 컴퓨터에 처음으로 등장하였다. 그러나 그래피컬 인터페이스는 1980년대에 Apple Macintosh 컴퓨터에 의해 널리 사용되게 되었다.
(생략)

인터페이스 선택

(생략)

시스템 호출

시스템 호출은 운영체제에 의해 사용 가능하게 된 서비스에 대한 인터페이스를 제공한다. 특정 저수준 작업(예컨대 하드웨어를 직접 접근해야 하는 작업)은 어셈블리 명령을 사용하여 작성되어야 하더라도 이러한 호출은 일반적으로 C와 C++ 언어로 작성된 루틴 형태로 제공된다.
운영체제가 어떻게 시스템 호출을 사용 가능하게 만드는지에 대해 논의하기 전에 시스템 호출이 어떻게 사용되는지를 설명하는 예를 보자.
한 파일로부터 데이터를 읽어서 다른 파일로 복사하는 간단한 프로그램을 작성한다고 가정해 보자. 프로그램이 필요로 하는 첫 번째 입력은 두 개의 파일, 즉 입력 파일과 출력 파일의 이름일 것이다. 이 이름들은 운영체제 설계에 따라 여러 가지 방법으로 지정할 수 있다.
(생략)
일단 두 개의 파일 이름이 얻어지면 프로그램은 반드시 입력 파일을 오픈하고 출력 파일을 생성한다. 각각의 이러한 연산은 또 다른 시스템 호출을 필요로 하며, 각 연산에서 오류 조건이 발생할 수 있고 이는 추갖거인 시스템 호출을 필요로 한다.
프로그램이 입력 파일을 오픈하려고 할 때 그 파일에 대한 접근이 금지되어 있거나 그 이름을 갖는 파일이 존재하지 않을 경우가 있다. 이런 경우 프로그램은 콘솔에 메시지를 출력하고(또 다른 일련의 시스템 호출이다), 그리고 비정상적으로 종료(또 다른 시스템 호출이다)한다.
만약 입력 파일이 존재하면 새로운 출력 파일을 생성해야 한다. 이때 동일한 이름을 가진 출력 파일이 이미 존재하는 경우가 있다. 이러한 상황은 프로그램을 중단(abort)(하나의 시스템 호출임)하게 하거나 또는 우리는 기존 파일을 삭제(다른 시스템 호출임)한 후, 새로운 파일을 생성(다른 시스템 호출임)할 수도 있다.
대화형 시스템에서 또 다른 방법은 기존의 파일을 대체할 것인지 혹은 프로그램을 중단할 것인지를 사용자에게 물어보는(프롬프트 메시지의 출력과 터미널로부터 응답을 읽기 위한 일련의 시스템 호출) 것이다.
이제 두 개의 파일이 준비되면, 입력 파일로부터 읽어서(하나의 시스템 호출), 출력 파일에 기록(또 다른 시스템 호출)하는 루프에 들어가게 된다.
각 읽기와 쓰기는 가능한 여러 가지 오류 상황의 정보를 반환해야 한다. 입력에서 프로그램이 파일의 끝에 도달하거나 읽기 중에 하드웨어 오류(이를테면 패리티 오류)가 발생할 수도 있다. 쓰기 연산 시 출력 장치에 따라 여러 가지 오류들(예컨대 디스크 공간 부족)이 발생할 수도 있다.
마지막으로 전체 파일이 복사된 후, 프로그램은 두 개의 파일을 닫고(또 다른 시스템 호출), 콘솔 또는 윈도우에 메시지를 기록하고(추가의 시스템 호출들), 결국 정상적으로 종료(마지막 시스템 호출)하게 된다. 이러한 시스템 호출의 순서가 그림 2.5에 예시되어 있다.
이렇듯 간단한 프로그램이라도 운영체제의 기능을 아주 많이 사용하게 된다. 종종 초당 수천 개의 시스템 호출을 수행하게 된다. 대부분의 사용자들은 이러한 정도의 상세를 결코 알지 못한다.
대부분의 응용 개발자들은 응용 프로그래밍 인터페이스(Application Programming Interface, API)에 따라 프로그램을 설계한다.
API는 각 함수에게 전달되어야 할 매개변수들과 프로그래머가 기대할 수 있는 반환 값을 포함하여 응용 프로그래머가 사용 가능한 함수의 집합을 명시한다.
응용 프로그래머가 사용 가능한 가장 흔한 세 가지 API는 Windows 시스템을 위한 Windows API, POSIX 기반 시스템을 위한 POSIX API(거의 모든 버전의 UNIX, Linux 및 Mac OS X를 포함한다), Java 가상 기계에서 실행될 수 있는 프로그램을 위한 Java API이다.
프로그래머는 운영체제가 제공하는 코드의 라이브러리를 통하여 API를 활용한다. UNIX와 Linux 시스템에서 C 언어로 작성된 프로그램을 위해서 제공되는 라이브러리는 libc로 불린다. 특별히 언급하지 않는 한 이 책에서 사용되는 시스템 호출의 이름은 일반적인 예임을 명심하라. 모든 운영체제는 고유의 시스템 호출 이름을 가진다.
막후에서 API를 구성하는 함수들은 통상 응용 프로그래머를 대신하여 실제 시스템 호출을 호출한다. 예컨대 Windows 함수 CreateProcess()(당연하게 새로운 프로세스를 생성하는데 사용된다)는 실제로 Windows 커널의 NTCreateProcess() 시스템 호출을 부른다.
왜 응용 프로그래머는 실제 시스템 호출을 부르는 것보다 API에 따라 프로그래밍하는 것을 선호하는가? 그렇게 하는데는 몇 가지 이유가 있다.
한 가지 이점은 프로그램의 호환성과 관련 있다. API에 따라 프로그램을 설계하는 응용 프로그래머는 자신의 프로그램이 같은 API를 지원하는 어느 시스템에서건 컴팡리 되고 실행된다는 것을 기대할 수 있다. (현실적으로는 컴퓨터 구조의 차이 때문에 보이는 것보다 쉽지는 않다)
게다가 실제 시스템 호출은 종종 좀 더 자세한 명세가 필요하고 프로그램 상에서 작업하기가 응용 프로그래머에게 가용한 API보다 더 어렵다. 그럼에도 불구하고 API 함수를 호출하는 것과 커널의 관련된 시스템 호출을 호출하는 것에는 강한 상관관계가 존재한다. 사실 대부분의 POSIX와 Windows API는 UNIX, Linux 및 Windows 운영체제가 제공하는 고유의 시스템 호출과 유사하다.
프로그래밍 언어들을 위한 실행시간 지원 시스템은(컴파일러와 함께 제공되는 라이브러리에 내장된 함수의 집합) 운영체제가 제공하는 시스템 호출에 대한 연결로서 동작하는 시스템 호출 인터페이스를 제공한다.
이 시스템 호출 인터페이스는 API 함수의 호출을 가로채어 필요한 운영체제 시스템 호출을 부른다. 통상 각 시스템 호출에는 번호가 할당되고 시스템 호출 인터페이스는 이 번호에 따라 색인되는 테이블을 유지한다. 시스템 호출 인터페이스는 의도하는 시스템 호출을 부르고 시스템 호출의 상태와 반환 값을 돌려준다.
호출자는 시스템 호출이 어떻게 구현되고 실행 중 무슨 작업을 하는지 아무 것도 알 필요가 없다. 호출자는 단지 API를 준수하고 시스템 호출의 결과로서 운영체제가 무엇을 할 것인지만 이해하면 된다.
따라서 운영체제 인터페이스에 대한 대부분의 자세한 내용은 API에 의해 프로그래머로부터 숨겨지고 실행시간 지원 라이브러리에 의해 관리된다.
API, 시스템 호출, 운영체제의 고나계가 그림 2.6에 도시되어 있다. 그림 2.6은 사용자 응용이 open() 시스템 호출을 불렀을 때 운영체제가 어떻게 처리하는지를 설명하고 있다.
시스템 호출은 사용되는 컴퓨터에 따라 다른 방법으로 발생한다. 종종 단순히 원하는 시스템 호출이 무엇인지보다 더 많은 정보가 요구될 경우도 있다. 필요한 정보의 유형과 양은 특정 운영체제와 호출에 따라 다양하다.
예컨대 입력을 받아들이기 위해 입력원(source)으로 사용될 파일이나 장치와 함께 읽어들인 데이터를 저장할 메모리 버퍼의 주소와 길이를 명시할 필요가 있다. 물론 장치나 파일 그리고 길이는 시스템 호출에 암묵적(implicit)일 수 있다.
운영체제에 매개변수를 전달하기 위해 세 가지 일반적인 방법을 사용한다.
가장 간단한 방법은 매개변수를 레지스터 내에 전달하는 것이다. 그러나 어떤 경우는 레지스터보다 더 많은 매개변수가 있을 수 있다. 이러한 경우에 매개변수는 메모리 내의 블록이나 테이블에 저장되고 블록의 주소가 레지스터 내에 매개변수로 전달된다 (그림 2.7)
매개변수는 프로그램에 의해 스택(stack)에 넣어질(push) 수 있고, 운영체제에 의해 꺼내진다(pop off).
몇몇 운영체제는 블록이나 스택 방법을 선호하는데, 이들 접근법은 매개변수들의 개수나 길이를 제한하지 않기 때문이다.

시스템 호출의 유형

시스템 호출은 다섯 가지의 중요한 범주 즉, 프로세스 제어, 파일 조작, 장치 조작, 정보 유지 보수와 통신, 보호 등으로 묶을 수 있다.
프로세스 제어
끝내기(end), 중지(abort)
적재(load), 수행(execute)
프로세스 생성, 프로세스 종료
프로세스 속성(attributes) 획득, 프로세스 속성(attributes) 설정
시간을 기다림
사건을 기다림(wait event), 사건을 알림(signa event)
메모리 할당 및 자유화
파일 조작(File Manipulation)
파일 생성(create file), 파일 삭제(delete file)
열기(open), 닫기(close)
읽기, 쓰기, 위치 변경(reposition)
파일 속성 획득 및 설정
장치 관리(Device Management)
장치를 요구, 장치를 방출
읽기, 쓰기, 위치 변경(reposition)
장치 속성 획득, 장치 속성 설정
장치의 논리적 부착(attach) 또는 분리(detach)
정보 유지(Information Maintenance)
시간과 날짜의 설정과 획득
시스템 데이터의 설정과 획득
프로세스, 파일, 장치 속성의 획득
프로세스, 파일, 장치 속성의 설정
통신(Communication)
통신 연결의 생성, 제거
메시지의 송신, 수신
상태 정보 전달
원격 장치의 부착(attach) 및 분리(detach)

프로세스 제어(Process Control)

실행 중인 프로그램은 수행을 정상적으로(end()) 또는 비정상적으로(abort()) 멈출 수 있어야 한다. 만약 현재 실행 중인 프로그램을 비정상적으로 중지하기 위해 시스템 호출이 행해지거나 또는 프로그램에 문제가 발생해 오류 트랩(trap)을 유발할 경우, 때때로 메모리 덤프가 행해지고 오류 메시지가 생성된다.
이 덤프는 디스크에 기록되고 문제의 원인을 결정하기 위해 디버거에 의해 검사될 수 있다.
정상이거나 비정상인 상황에서 운영체제는 명령 해석기로 제어를 전달해야 한다. 명령 해석기는 이어 다음 명령을 읽는다. 대화식 시스템에서 명령 해석기는 단순히 다음 명령을 계속 수행하며, 사용자가 오류에 응답하는 적절한 명령을 내릴 것을 가정한다.
GUI 시스템에서는 팝업 윈도우가 사용자에게 오류를 알리고 지시를 기다린다. 일괄 처리 시스템에서는 명령 해석기가 통상 잡(job) 전체를 종료하고 다음 잡을 계속한다. 몇몇 시스템에서는 오류가 발생할 경우 특별한 복구 행위를 지시하는 제어 카드를 허용한다.
만약 프로그램이 입력에서 오류를 발견하고 비정상으로 종료하기를 원한다면 프로그램은 오류 수준을 정의하기를 원할 수도 있다.
보다 높은 등급의 오류 매개변수를 전달함으로써 더 치명적인 오류를 나타낼 수 있다. 이렇게 함으로써 정상 종료를 등급 0의 오류로 정의하여 정상 종료와 비정상 종료를 결합시킬 수도 있다.
명령 해석기 혹은 그 다음 프로그램은 이 오류 등급을 사용하여 다음 행동을 자동으로 결정할 수 있다.
한 프로그램을 실행하고 있는 프로세스나 잡(job)이 다른 프로그램을 적재(load())하고 실행(execute())하기를 원할 수 있다. 이 기능은 명령 해석기가 사용자 명령, 마우스의 클릭(click) 혹은 일괄처리 명령 등을 통하여 지시된 프로그램을 실행하는 것을 허용한다.
여기서 흥미로운 질문은 적재된 프로그램이 종료되었을 때 어디로 제어를 되돌려 주느냐 하는 것이다. 이 질문은 기존 프로그램이 유실될지, 보관될지, 새로운 프로그램과 병행하게 실행을 계속하도록 허용할 것인지 하는 문제와 관련 있다.
만약 새로운 프로그램이 종료되었을 떄 제어가 기존 프로그램으로 되돌아간다면, 우리는 반드시 기존 프로그램의 메모리 이미지를 보관해야 한다. 따라서 우리는 실질적으로 한 프로그램이 다른 프로그램을 호출하는 기법을 만든 셈이 된다.
만약 두 프로그램이 병행하게 수행된다면, 우리는 다중 프로그래밍 될 새로운 잡이나 프로세스를 생성한 것이다. 종종 이런 특정 목적을 위한 시스템 호출이 있다(create_process() 또는 submit_job())
만약 우리가 새로운 잡이나 프로세스 혹은 잡들이나 프로세스들의 집합을 생성한다면, 우리는 그들의 실행을 제어할 수 있어야 한다.
이러한 제어는 잡의 우선순위, 최대 허용 실행 시간 등으 포함하여 잡 혹은 프로세스의 속성들을 결정하고 재설정(reset) 할 수 있는 능력을 필요로 한다(get_process_attributes() 및 set_process_attributes()). 우리는 또한 새로 생성한 잡이나 프로세스가 잘못되었거나 더 이상 필요 없다면 종료하기를 원할 수 있다. (terminate_process())
새로운 잡이나 프로세스를 생성한 후에는 우리는 이들 실행이 끝나기를 기다려야 할 필요가 있을 수 있다. 우리는 일정 시간만큼 기다리기를 원할 수 있다 (wait_time()). 그리고 보다 가능성이 큰 경우는 우리가 특정 사건이 일어날 때까지 기다리는 것이다 (wait_event()). 그 경우 잡이나 프로세스들은 그 사건이 일어나면 신호를 보내야 한다 (signal_event()).
둘 이상의 프로세스들은 데이터를 빈번하게 공유한다. 공유되는 데이터의 일관성을 보장하기 위해 운영체제는 종종 프로세스가 공유 데이터를 잠글 수 있는 시스템 호출을 제공한다. 그러면 잠금이 해제될 떄까지 어느 프로세스도 데이터에 접근할 수 없게 된다.
통상 그런 시스템은 acquire_lock()과 release_lock() 시스템 호출을 제공한다. 병행 프로세스들의 조정(coordination)을 처리하는 이런 유형의 시스템 호출은 6장에서 다룬다.
프로세스와 잡 제어는 너무 많은 측면과 다양성이 있기 때문에 우리는 이러한 개념들을 명확히 하기 위해 단일 태스킹 시스템과 다중 태스킹 시스템의 두 예를 사용할 것이다.
MS-DOS 운영체제는 단일 태스킹 시스템의 예로 컴퓨터가 시동될 때 호출되는 하나의 명령 해석기를 가진다(그림 2.9(a)).
MS-DOS는 단일 태스킹이기 때문에 하나의 프로그램을 수행하기 위해 간단한 방법을 사용하며 새로운 프로세스를 생성하지 않는다.
MS-DOS는 프로그램을 메모리에 적재하며, 이때 가능한 한 많은 메모리를 프로그램에 제공하기 위해 자신의 대부분을 덮어 쓴다(그림 2.9(b)). 이어 MS-DOS는 명령 포인터를 프로그램의 첫 번째 명령으로 설정한다.
그 다음 프로그램이 수행되며, 오류가 있을 경우는 트랩이 발생되거나 아니면 종료를 위해 시스템 호출을 수행한다. 어느 경우든 다음에 이용하기 위해 오류 코드가 시스템 메모리에 저장된다. 이 동작에 이어 명령 해석기의 덮어 쓰이지 않은 적은 부분이 실행을 재개한다.
이것의 첫 번째 일은 명령 해석기의 나머지를 디스크로부터 다시 적재하는 것이다. 이 일이 수행되면 명령 해석기는 앞의 오류 코드를 사용자나 다음 프로그램이 이용할 수 있게 해준다.
FreeBSD(Berkeley UNIX로부터 비롯됨)는 다중 태스킹 시스템의 예이다. 사용자가 시스템에 로그인할 때 사용자가 선택한 쉘이 수행된다. 이 쉘은 명령을 받아서 사용자가 요청한 프로그램을 수행한다는 점에서 MS-DOS 명령 해석기와 유사하다. 그렇지만 FreeBSD는 다중 태스킹 시스템이기 때문에 명령 해석기는 다른 프로그램이 실해오디는 동안 수행을 계속할 수 있다(그림 2.10)
새로운 프로세스를 시작하기 위해 쉘은 fork() 시스템 호출을 실행한다. 그런 다음 선택된 프로그램이 exec() 시스템 호출을 통해 메모리에 적재되고, 이어 프로그램이 수행된다. 명령이 내려진 방법에 따라 쉘은 프로세스가 종료하기를 기다리거나 또는 ‘백그라운드’에서 프로세스를 수행한다.
후자의 경우 쉘은 바로 또 다른 명령을 요청한다. 프로세스가 백그라운드에서 수행될 때, 그 프로세스는 키보드로부터 직접 입력을 받을 수 없는데, 이는 쉘이 그 자원을 사용하고 있기 때문이다. 따라서 입출력은 파일, 또는 GUI 인터페이스를 통해 행해진다.
반면 사용자는 쉘에게 다른 프로그램을 수행하도록 요청하거나 수행 중인 프로세스의 진행 사항을 감시하게 하거나, 그 프로그램의 우선순위를 변경하는 등의 요청을 자유스럽게 할 수 있다.
프로세스가 끝나면 종료하기 위해 exit() 시스템 호출을 수행하며, 호출한 프로세스에게 상태 코드 0을 돌려주거나 0이 아닌 오류 코드를 돌려준다. 이런 상태 또는 오류 코드는 쉘 또는 다른 프로그램들이 이용할 수 있게 된다.

파일 관리(File Management)

우리는 우선 파일을 생성(create()) 하고 삭제(delete()) 할 수 있어야 한다. 이들 시스템 호출은 파일 이름이 파일 속성의 일부를 요구한다.
파일이 생성되면 그것을 열고(open()) 사용해야 한다. 또한 읽고(read()), 쓰고(write()), 그리고 위치 변경(reposition(), 예컨대 되감기(rewind())나 파일의 끝으로 건너뛰기) 할 수 있다. 마지막으로 파일 닫기(close())가 필요하다.
파일 시스템이 파일을 조작하기 위해 디렉터리 구조를 가진다면, 우리는 디렉터리에 대해서도 이와 같은 연산 집합이 필요할 것이다. 추가로 파일이나 디렉터리에 대해 여러 속성의 값을 결정할 수 있어야 하고, 필요하다면 그것을 재설정(reset) 할 수 있어야 한다.
파일 속성은 파일 이름, 파일 타입, 보호 코드, 회계 정보 등을 포함한다. 이러한 기능을 위해서는 최소한 파일 속성 획득(get_file_attribute())과 파일 속성 설정(set_file_attribute())의 두 시스템 호출이 필요하다.
몇몇 운영체제는 파일 이동(move())와 복사(copy()) 등의 훨씬 더 많은 시스템 호출들을 제공한다. 일부 시스템들은 코드와 다른 시스템 호출을 이용하여 동일한 작업을 수행하는 API를 제공할 수도 있고 일부 시스템은 단순히 동일한 작업을 수행하는 시스템 프로그램을 제공하기도 한다.
만일 이 시스템 프로그램이 다른 프로그램에 의해 호출 가능하다면 다른 프로그램 입장에서는 이 시스템 프로그램이 API가 된다.

장치 관리(Device Management)

프로세스는 작업을 계속 수행하기 위해 추가 자원을 필요로 할 수 있다. 이러한 추가 자원은 주 기억장치, 디스크 드라이브, 파일에의 접근 등이 될 수 있다. 만약 자원들을 사용할 수 있다면 이들 자원이 주어지고 제어가 사용자 프로그램으로 복귀될 수 있다. 그렇지 않으면 프로그램은 충분한 자원이 사용 가능하게 될 때까지 기다려야 한다.
운영체제에 의해 제어된느 다양한 자원들은 장치로 간주될 수 있다. 이 장치들의 일부는 물리 장치(예컨대 디스크 드라이브)이고 다른 장치들은 추상적 혹은 가상적 장치(예컨대 파일)로 생각할 수 있다.
다수의 사용자가 동시에 사용하는 시스템은 독점적인 장치 사용을 보장받기 위해 우선 그 장치를 요청(request()) 하는 것을 요구한다. 그 장치의 사용이 끝나면 우리는 그것을 반드시 방출(release()) 해야 한다. 이러한 기능은 파일의 열기, 닫기 시스템 호출과 비슷하다.
다른 운영체제들은 장치에 대해 통제되지 않은 접근을 허용한다. 예상되는 위험은 7장에서 설명될 장치에 대한 잠재적 경쟁과 교착상태이다.
일단 장치를 요청하고 파일에서와 같이 그 장치를 읽고(read()), 쓰고(write()), 그리고 위치 변경할(reposition()) 수 있다.
사실 입출력 장치와 파일들간에는 유사성이 많기 때문에 UNIX를 포함한 많은 운영체제가 이들 둘을 통합된 파일-장치 구조(file-device structure)로 결합하였다. 이 경우 같은 시스템 호출들이 파일과 장치에 대해 사용된다. 때로 입출력 장치들은 특별한 파일 이름, 디렉터리 배치 또는 파일 속성으로 식별된다.
아래의 숨은 시스템 호출은 다르더라도 UI를 이용하여 파일과 장치를 비슷한 것처럼 만들 수 있다. 이것은 운영체제와 사용자 인터페이스를 구축하는데 필요한 설계 결정 사항 중의 한 예이다.

정보의 유지(Information Maintenance)

많은 시스템 호출은 단순히 사용자 프로그램과 운영체제간의 정보 전달을 위해 존재한다. 예컨대 대부분의 시스템은 현재 시간과 날짜를 되돌려주는 시스템 호출을 가지고 있다.
시스템 호출들의 또 다른 집합은 프로그램을 디버깅하는데 유용하다. 많은 시스템들은 메모리를 덤프(dump) 하기 위한 시스템 호출을 제공하며, 이것은 디버깅 하는데 유용하다.
프로그램 추적(trace)은 각 명령어가 실행될 때 이들을 하나씩 나열하며 이러한 시스템 호출은 보다 소수의 시스템에서만 제공된다.
심지어 마이크로프로세서들도 단일 단계(single step)라고 알려진 CPU 모드를 제공하는데, 여기서는 매번 명령어 수행 후 CPU에 의해 트랩이 수행된다. 트랩은 통상 디버거에 의해 포착된다.
많은 운영체제는 프로그램의 시간 프로파일(time profile)을 제공한다. 시간 프로파일은 그 프로그램이 특정 위치 혹은 위치의 집합에서 수행한 시간의 양을 나타낸다.
시간 프로파일은 추적 설비(tracing facility)나 정규 타이머 인터럽트를 필요로 한다. 타이머 인터럽트가 발생할 때마다 프로그램 카운터의 값이 기록된다. 따라서 타이머 인터럽트가 충분히 빈번하게 일어나면, 프로그램이 여러 부분에서 소비한 시간의 통계적 그림을 얻을 수 있다.
더욱이 운영체제는 현재 운영되고 있는 모든 프로세스들에 관한 정보를 가지고 있으며, 이러한 정보에 접근하기 위한 시스템 호출들이 있다.
일반적으로 그 프로세스 정보를 재설정 하기 위한 시스템 호출도 있다(get_process_attributes() 및 set_process_attributes())

통신(Communication)

통신 모델에는 메시지 전달과 공유 메모리의 두 가지 일반적인 모델이 있다. 메시지 전달 모델에서는 통신하는 두 프로세스가 정보를 교환하기 위해 서로 메시지를 주고 받는다. 메시지는 두 프로세스 사이에 직접 교환되거나 우편함을 통해 간접적으로 교환될 수 있다.
통신이 이루어지기 전에 연결이 반드시 열려야 한다. 상대 통신자(communicator)가 동일한 CPU에 있는 프로세스이든지 또는 통신 네트워크에 의해 연결된 다른 컴퓨터에 있는 프로세스이든지 간에 그 이름을 반드시 알고 있어야 한다.
네트워크의 각 컴퓨터는 호스트 이름을 가지며, 각 컴퓨터는 이들 이름으로 일반적으로 알려져 있다. 마찬가지로 각 프로세스는 프로세스 이름을 가지고 있으며, 이 이름은 운영체제에 의해 동등한 식별자로 변환되고 이 식별자는 운영체제가 그 프로세스를 가리키는데 사용할 수 있다. get_hostid()와 get_processid() 시스템 호출은 이러한 변환을 수행한다.
이들 식별자는 그 후 시스템의 통신 모델에 따라 파일 시스템에 의해 제공되는 범용의 open과 close 호출에 전달되거나 특정 open_connection()과 close_connection() 시스템 호출에 전달된다.
수신 프로세스는 통상 통신이 일어날 수 있도록 accept_connection() 호출에 자신의 허가(permission)를 제공한다. 연결을 받아들일 프로세스들의 대부분은 특수 목적의 디먼(daemon)으로서 이들은 그러한 목적을 위해 제공된 시스템 프로그램들이다. 그들은 연결을 위해 대기(wait_for_connection()) 호출을 수행하고 연결이 이루어질 때 깨어난다.
클라이언트(client)로 알려진 토인의 출발지와 서버(server)로 알려진 수신 디먼은 이어 read_message()와 write_message() 시스템 호출에 의해 메시지들을 교환한다. close_connection() 호출은 통신을 종료한다.
공유 메모리 모델에서 프로세스는 다른 프로세스가 소유한 메모리 영역에 대한 접근을 위해 shared_memory_create()와 shared_memory_attach() 시스템 호출을 사용한다. 정상적으로 운영체제는 한 프로세스가 다른 프로세스의 메모리 접근 하는 것을 막으려고 한다는 것을 기억하라.
공유 메모리는 두 개 이상의 프로세스가 이러한 제한을 제거하는데 동의할 것을 필요로 한다. 그런 후 이들 프로세스들은 이러한 공유 영역에서 데이터를 읽고 씀으로써 정보를 교환할 수 있다.
데이터의 형식은 운영체제의 제어 하에 있는 것이 아니라 이들 프로세스들에 의해 결정된다. 프로세스들은 또한 동일한 위치에 동시에 쓰지 않도록 보장할 책임을 가진다.
이러한 두 가지 방법은 운영체제에서 보편적이며, 대부분의 시스템들은 둘 다 구현한다. 메시지 전달은 소량의 데이터를 교환할 때 유용한데, 이는 피해야 할 충돌이 없기 때문이다. 메시지 전달은 또한 컴퓨터 간의 통신을 위해 메모리 공유보다 구현하기 쉽다.
공유 메모리는 한 컴퓨터 안에서는 메모리 전송 속도로 수행할 수 있기 때문에 최대 속도와 편리한 통신을 허용한다. 그렇지만 보호와 동기화 부분에서 여러 문제점을 가지고 있다.

보호(Protection)

보호는 컴퓨터 시스템이 제공하는 자원에 대한 접근을 제어하기 위한 기법을 제공한다. 역사적으로 보호는 다수의 사용자를 가지는 다중 프로그램 시스템에서만 고려되는 문제였다. 그러나 네트워킹과 인터넷의 출현으로 서버에서 휴대용 컴퓨터까지 모든 컴퓨터 시스템에서 보호를 고려하여야 한다.
통상 보호를 지원하는 시스템 호출은 set_permission()과 get_permission()을 포함하는데, 파일과 디스크와 같은 자원의 허가 권한을 설정하는데 이용된다. allow_user()와 deny_user() 시스템 호출은 특정 사용자가 지정된 자원에 대해 접근이 허가 혹은 불허 되었는지의 여부를 명시한다.

시스템 프로그램

현대 시스템의 또 다른 면은 시스템 프로그램의 집합체이다. 논리적인 컴퓨터 계층 구조를 나타내는 그림 1.1을 기억해 보면, 최하위 수준은 하드웨어이고, 다음은 운영체제, 그 다음은 시스템 프로그램, 마지막이 응용 프로그램이다.
시스템 프로그램은 시스템 유틸리티(system utility)로도 알려진 프로그램 개발과 실행을 위해 보다 편리한 환경을 제공한다. 그들 중 몇몇은 단순히 시스템 호출에 대한 사용자 인터페이스이며, 반면 나머지는 훨씬 더 복잡하다. 이들은 다음 몇 가지 범주로 분류할 수 있다.
파일 관리
이들 프로그램은 파일과 디렉터리를 생성, 삭제, 복사, 개명, 인쇄, 덤프, 리스트하고 일반적으로 조작한다.
상태 정보
어떤 프로그램들은 단순히 시스템에게 날짜, 시간, 사용 가능한 메모리와 디스크 공간의 양, 사용자 수 혹은 이와 비슷한 상태 정보를 묻는다. 다른 프로그램들은 더 복잡하여 상세한 성능, 로깅 및 디버깅 정보를 제공한다.
통상 이 프로그램들은 정보를 단말기나 다른 출력 장치 혹은 파일로 포맷하여 인쇄하거나 또는 GUI의 윈도우에 표시한다. 몇몇 시스템은 환경 정보를 저장하고 검색할 수 있는 등록(registry) 기능을 지원하기도 한다.
파일 변경
디스크나 달느 저장 장치에 저장된 파일의 내용을 생성하고 변경하기 위해 다수의 문장 편집기(text editor)가 사용 가능하다. 파일의 내용을 검색하거나 변환하기 위한 특수 명령어가 제공 되기도 한다.
프로그래밍 언어 진원
일반적인 프로그래밍 언어들에 대한 컴파일러, 어셈블러, 디버거 및 해석기가 종종 운영체제와 함께 사용자에게 제공되거나 별도로 다운로드 받을 수 있다.
프로그램 적재와 수행
일단 프로그램이 어셈블되거나 컴파일 된 후, 그것이 수행되려면 반드시 메모리에 적재되어야 한다. 시스템은 절대 로더(absolute loader), 재배치 가능 로더(relocatable loader), 링키지 에디터(linkage editor)와 중첩 로더(overlay loader) 등을 제공할 수 있다. 또한 고급어나 기계어를 위한 디버깅 시스템도 필요하다.
통신
이들 프로그램은 프로세스, 사용자, 그리고 다른 컴퓨터 시스템들 사이에 가상 접속을 이루기 위한 기법을 제공한다. 이들 프로그램은 사용자가 다른 사용자 화면으로 메시지를 전송하거나, 웹 페이지 이곳저곳을 읽거나 전자 우편 메시지를 보내거나 원거리에서 로그인하거나, 한 기계에서 다른 기계로 파일을 전송할 수 있게 한다.
백그라운드 서비스
모든 범용 시스템은 부팅할 때 특정 시스템 프로그램을 시작시킬 수 있는 방법을 가지고 있다. 이러한 프로세스 중 일부는 자신들의 할 일을 완수하면 종료하는 반면 일부는 시스템이 정지될 때까지 계속해서 실행되는 프로세스도 존재한다. 항상 실행되는 시스템 프로그램 프로세스는 서비스, 서브시스템, 또는 디먼으로 알려져 있다.
대부분의 운영체제는 시스템 프로그램과 함께 일반적인 문제점을 해결하거나 일반적인 연산을 수행하는데 유용한 프로그램들도 제공한다.
이러한 응용 프로그램에는 웹브라우저, 워드프로세서와 텍스트 포맷터(text formatter), 스프레드시트, 데이터베이스 시스템, 컴파일러, 도면 제작 그리고 통계분석 패키지, 게임 등이 포함된다.
대부분의 사용자가 보는 운영체제의 관점은 실제의 시스템 호출에 의해서보다는 시스템 프로그램과 응용에 의해 정의된다.

운영체제 설계 및 구현

설계 목표(Design Goals)

시스템을 설계하는데 첫째 문제점은 시스템의 목표와 명세를 정의하는 일이다. 시스템 설계는 최상위 수준에서는 하드웨어와 시스템 타입(일괄처리, 시분할, 단일 사용자, 다중 사용자, 분산, 실시간 혹은 범용)의 선택에 의해 영향을 받을 것이다.
이 최상위 설계 수준을 넘어서면 요구 조건들을 일일이 명시하는 것이 훨씬 더 어려워진다. 그러나 이러한 요구 조건은 근본적으로 사용자 목적과 시스템 목적의 두 가지 기본 그룹으로 나눌 수 있다.
(생략)
운영체제의 명세와 설계는 매우 창조적인 일이다. 어떤 교재도 이런 문제점을 해결하는 방법을 알려 줄 수는 없지만, 소프트웨어 공학 분야에 의해 개발된, 특히 운영체제에 적용 가능한 일반적인 원칙들이 존재한다.

기법과 정책(Mechanisms and Policies)

한 가지 중요한 원칙은 기법(mechanism)으로부터 정책을 분리하는 것이다. 기법은 어떤 일을 어떻게 할 것인가를 결정하는 것이고, 정책은 무엇을 할 것인가를 결정하는 것이다.
예컨대 타이머 구조는 CPU 보호를 보장하기 위한 기법이지만, 특정 사용자를 위해 타이머를 얼마나 오랫동안 설정할지를 결정하는 것은 정책적 결정이다.
정책과 기법의 분리는 융통성을 위해 아주 중요하다. 정책은 장소가 바뀌거나 시간이 흐름에 따라 변경될 수 있다. 최악의 경우 정책의 각 변경이 저변에 깔려 있는 기법의 변경을 요구하게 된다. 정책의 변경에 민감하지 않은 일반적인 기법이 보다 바람직하다. 그렇게 되면 정책의 변경은 시스템의 일부 매개변수만을 재정의하도록 요구한다.
예컨대 한 유형의 프로그램이 다른 유형의 프로그램보다 높은 우선순위를 가지도록 하는 기법을 생각해 보자. 만일 기법이 정책으로부터 적절하게 분리되면 입출력 중심 프로그램이 CPU 중심 프로그램보다 높은 우선순위를 가지도록 하는 정책을 지원하거나 또는 그 반대 정책을 지원할 수 있다.
마이크로 커널 기반 운영체제는 원시(primitive) 빌딩 블록의 기본 집합을 구현함으로써 기법과 정책의 분리를 극단적으로 추구한다. 이 블록들은 정책으로부터 거의 자유로우며, 보다 고급의 기법과 정책들이 사용자 생성 커널 모듈이나 사용자 프로그램 자체를 통해 첨가될 수 있도록 한다.
예컨대 UNIX의 역사를 생각해 보자. 처음에는 시분할 스케쥴러를 가지고 있었다. Solaris의 나중 버전에는 스케줄링이 적재 가능한 테이블에 의해 제어된다. 현재 적재된 테이블에 따라 시스템은 시분할, 일괄처리, 실시간, fair share 또는 둘 이상의 조합 방식으로 스케줄 된다.
스케줄링 기법을 범용으로 구현하려면 load-new-table 명령 하나로 다양한 정책 변경을 수용할 수 있다.
또 다른 방향으로 극단적인 시스템으로는 Windows와 같은 시스템이 있는데, 여기서는 시스템에 대한 전체적 외관과 느낌을 강제하기 위해 기법과 정책이 시스템 내에 코드화 되어 있다. 인터페이스 자체가 커널과 시스템 라이브러리에 내장되어 있기 때문에 모든 응용들은 유사한 인터페이스를 가진다. Max OS X도 유사한 기능을 가지고 있다.

구현(Implementation)

초창기 운영체제는 어셈블리어로 작성되었다. 어셈블리어로 작성된 운영체제가 존재하기는 하지만 대부분의 운영체제는 C, C++ 같은 고급 언어로 작성된다.
사실 운영체제는 하나 이상의 언어로 구현될 수 있다. 커널의 가장 낮은 단계는 어셈블리어로 구현될 수 있다. Linux 배포판을 검사하면 아마도 언급한 모든 언어로 구현되어 있다는 것을 알 수 있을 것이다.
운영체제를 고급 언어나 최소한 시스템 구현 언어를 사용함으로써 생기는 장점은 생산성 면에서도 있지만, 이식이 훨씬 쉽다는 부분도 있다.
예컨대 MS-DOS는 Intel 8088 어셈블리어로 작성되었기 때문에 Intel x86 계열의 CPU에서만 수행된다. 대조적으로 Linux 운영체제는 대부분 C로 작성되었으며 Intel x86, Sun SPARC, IBM PowerPC 등 다수의 다른 CPU에서 사용 가능하다.
운영체제를 고급 수준 언어로 구현하는 것은 속도가 느리고 저장 장치가 많이 소요된다는 단점이 있지만, 현재의 시스템에서는 주된 문제가 아니다. 다른 시스템에서도 잘 알려진 사실이지만, 운영체제의 주요 성능 향상은 우수한 어셈블리어 코드보다는 좋은 자료 구조와 알고리즘의 결과일 가능성이 크다.
게다가 운영체제가 크긴 하지만 단지 소량의 코드만이 고성능이 중요하다. 아마도 인터럽트 핸들러, 입출력 관리자, 메모리 관리자와 CPU 스케줄러가 가장 긴급한 루틴일 것이다. 시스템이 작성되어 정확히 작동하면 병목 루틴을 확인할 수 있고, 동등한 어셈블리어로 대체될 수 있다.

운영체제 구조

현대의 운영체제와 같이 크고 복잡한 시스템은 적절하게 동작하고 쉽게 변경될 수 있으려면 한 개의 일관된 시스템보다는 태스크를 작은 구성 요소로 분할 하는 것이 낫다.

간단한 구조(Simple Structure)

잘 정의된 구조를 갖지 못한 운영체제는 처음에는 소형이면서 간단하고 제한된 시스템으로 시작되었지만, 원래의 범위 이상으로 발전된 경우가 많다.
대표젹인 예가 MS-DOS로 MS-DOS는 원래 단지 몇 사람들에 의해 설계, 구현되었고 그렇게 대중화 될 것이라고는 생각하지 못했다.
그것은 최소의 공간에 최대의 기능들을 제공하도록 구현되었기 때문에 신중하게 모듈별로 구분되지 않았다. 그림 2.11은 MS-DOS의 구조를 보인다.
MS-DOS은 인터페이스와 기능 계층이 잘 분리되어 있지 않다. 예컨대 응용 프로그램은 기본 입출력 루팅을 통해 디스플레이와 디스크 드라이브에 직접 쓰기가 가능하다.
이러한 자유는 MS-DOS를 오류가 있는 프로그램으로부터 취약하게 만들었다. 따라서 사용자 프로그램이 고장나면 시스템 전체가 고장 나게 된다.
제한적인 구조의 또 다른 예는 최초의 UNIX 운영체제이다. MS-DOS와 마찬가지로 UNIX는 처음에는 하드웨어 기능에 의해 제한 받는 또 다른 시스템이었다.
UNIX는 두 부분, 커널과 시스템 프로그램으로 구성되어 있다. 커널은 여러 가지 인터페이스와 장치 드라이버로 다시 분리되는데, 이들은 UNIX가 발전해 오면서 여러 해 동안 추가되고 확장된 것이다.
전통적인 UNIX 운영체제는 그림 2.12에서 보여주는 바와 같이 계층들로 이루어졌다고 볼 수 있다. 시스템 호출 인터페이스 아래와 물리적 하드웨어 위의 모든 것이 커널이다. 커널은 시스템 호출을 통해 파일 시스템, CPU 스케줄링, 메모리 관리 그리고 다른 운영체제 기능을 제공한다.
전체를 감안하면 그것은 하나의 계층으로 결합하기에는 엄청나게 많은 기능이다. 이 모놀리식(monolithic) 구조는 구현하기 어렵고 또 유지보수 하기도 어려웠다. 그러나 이 구조는 성능 측면에서는 분명히 장점을 가지는 구조이다. 시스템 호출 인터페이스나 커널 안에서 통신하는 경우에는 오버헤드가 거의 없다. 우리는 이러한 간단하고 모놀리식 구조의 흔적은 여전히 UNIX, Linux 및 Windows 운영체제에서 발견할 수 있다.

계층적 접근(Layered Approach)

적절한 하드웨어 지원이 있을 경우 운영체제는 원래의 MS-DOS 또는 UNIX 시스템에 의해 허용되던 것보다 작고 적절한 조각으로 분할될 수 있다.
운영체제는 컴퓨터와 그 컴퓨터를 사용하는 응용에 대해 훨씬 더 큰 제어를 유지할 수 있다. 구현자들은 시스템의 내부 동작과 모듈식 운영체제의 생성에 변화를 줄 수 있는 보다 큰 자유를 갖게 되었다.
하향식(top-down) 접근법 하에서는 전체적인 기능과 특징이 결정되고 그리고 구성 요소로 분리된다. 정보의 은폐 또한 중요하다. 왜냐하면 그것은 프로그래머가 저수준 루틴을, 그 루틴의 외부 인터페이스가 변경되지 않고 그리고 루틴 자체가 공시된 일을 수행한다면, 그들이 적절하다고 생각하는 대로 자유스럽게 구현할 수 있게 하기 때문이다.
시스템은 다양한 방식으로 모듈화 될 수 있다. 한 가지 방식이 계층적 접근 방식인데 이 방식에서는 운영체제가 여러 개의 층으로 나누어진다. 최하위 층은(층 0) 하드웨어이고 최상위 층(층 N)은 사용자 인터페이스이다. 이 구조가 그림 2.13에 나와있다.
운영체제 층은 데이터와 이를 조작하는 연산으로 구성된 추상된 객체의 구현이다. 전형적인 운영체제 층(편의상 층 M이라고 하자)은 자료 구조와 상위 층에서 호출할 수 있는 루틴의 집합으로 구성된다. 층 M은 다시 하위 층에 대한 연산을 호출할 수 있다.
계층적 접근 방식의 주된 장점은 구현과 디버깅의 간단함에 있다. 층들은 단지 자신의 하위 층들의 서비스와 기능(연산)들만 사용하도록 선택된다. 이러한 접근 방법은 시스템의 검증과 디버깅 작업을 단순화 한다.
첫 번째 층은 정의에 의해 하드웨어(하드웨어는 정확하다고 가정함)만을 사용하여 이 층의 기능을 구현하기 때문에, 나머지 시스템에 아무런 신경을 쓰지 않고 디버깅할 수 있다.
첫 번째 층의 디버깅이 끝나면 두 번째 층을 디버깅 하는 동안 그것이 정확하게 동작한다고 가정될 수 있으며, 이러한 과정이 반복된다.
만일 어느 층의 디버깅 중 오류가 발견되면, 그 하위의 층은 이미 디버깅 되었기 때문에 오류는 반드시 그 층에 있다. 따라서 시스템을 계층으로 나누면 시스템의 설계나 구현이 간단해진다.
각 층은 자신보다 하위 수준의 층에 의해 제공된 연산들만 사용해 구현한다. 한 층은 이러한 연산들이 어떻게 구현되는지 알 필요 없고, 다만 이러한 연산들이 무엇을 하는지만 알면 된다. 그러므로 각 층은 특정 데이터 구조, 연산, 그리고 하드웨어의 존재를 상위 층에 대해 숨기게 된다.
계층적 접근 방법의 가장 어려운 점은 여러 층을 적절히 정의하는 것을 포함한다. 각 층은 자신의 하위에 있는 계층들만 사용할 수 있기 때문에 신중한 계획이 필요하다.
예컨대 예비 저장 장치(backing store)(가상 메모리 알고리즘에 의해 사용되는 디스크 공간)를 위한 장치 드라이버는 메모리 관리가 예비 저장 장치를 사용할 수 있는 능력을 필요로 하기 때문에 메모리 관리 루틴들보다 하위 층에 있어야 한다.
다른 요구 조건들은 그렇게 명확하지는 않다. 예비 저장 장치 드러이버는 통상적으로 CPU 스케쥴러 위에 존재하는데, 이는 드라이버가 입출력을 위해 기다려야 하고, CPU가 그 동안 재스케쥴될 수 있기 때문이다.
그러나 대형 시스템의 CPU 스케쥴러는 활동 중인(active) 모든 프로세스들에 대해 메모리에 적재할 수 있는 것보다 더 많은 정보를 가질 수 있다. 따라서 이러한 정보들은 메모리에서 스왑인 또는 스왑아웃 될 필요가 있을 수 있으며, 이는 예비 저장 장치 드라이버 루틴이 CPU 스케쥴러 아래에 놓일 것을 요구한다.
계층적 구현 방법의 마지막 문제점은 다른 유형의 구현 방법보다 효율성이 낮다는 것이다.
예컨대 사용자 프로그램이 입출력 연산을 수행할 경우, 프로그램이 시스템 호출을 수행하여 입출력 층으로 트랩되고, 입출력 층은 메모리 관리 층을 호출하고, 메모리 관리 층은 이어 CPU 스케줄링 층을 호출하며, 마지막으로 하드웨어로 전달된다.
각 층에서 매개변수들이 변경된다든지, 데이터가 전달될 필요가 있다든지 하는 일이 있을 수 있다. 각 층은 시스템 호출에 오버헤드를 추가하며 그 결과 계층적 구조가 아닌 시스템보다 시스템 호출의 수행 시간이 더 오래 걸리게 된다.
이러한 제한이 근년에 계층화에 대한 약간의 부정론을 낳고 있다. 모듈화 코드가 갖는 대부분의 장점을 가지면서 반면 계층 정의나 상호 작용의 어려운 문제점들을 피할 수 있는 층은 거의 설계되고 있지 않다.

마이크로커널(Microkernels)

우리는 이미 UNIX가 확장함에 따라 커널이 커지고 관리하기 어려워진 것을 목격하였다. 1980년대 중반에 카네기 멜론 대학교의 연구자들이 마이크로커널 접근 방식을 사용하여 커널을 모듈화한 Mach라는 운영체제를 개발하였다.
이 방법은 모든 중요치 않은 구성 요소를 커널로부터 제거하고, 그들을 시스템 및 사용자 수준 프로그램으로 구현하여 운영체제를 구성하는 방법이다.
결과는 보다 작은 커널이다. 어느 서비스가 커널에 남아 있어야 하고, 어느 서비스가 사용자 공간에 구현되어야 할지에 대해서는 의견이 일치하지 않는다.
그러나 통상 마이크로커널은 통신 설비 외에 추가로 최소한의 프로세스와 메모리 관리를 제공한다. 그림 2.14에 전형적인 마이크로커널의 구조가 도시되어 있다.
마이크로커널의 주 기능은 클라이언트 프로그램과 역시 사용자 공간에서 수행되는 다양한 서비스 간에 통신을 제공하는 것이다. 통신은 2.4.5절에 기술된 메시지 전달에 의해 제공된다.
예컨대 만일 클라이언트 프로그램이 파일을 접근하기를 원한다면, 파일 서버와 반드시 상호작용해야 한다. 클라이언트 프로그램과 서비스는 결코 직접 상호 작용하지 않는다. 오히려 그들은 마이크로커널과 메시지를 교환함으로써 간접적으로 상호작용한다.
마이크로커널 접근법의 한 가지 장점은 운영체제의 확장이 용이하다는 것이다. 모든 새로운 서비스는 사용자 공간에 추가되며, 따라서 커널의 변경을 필요로 하지 않는다. 커널이 변경되어야만 할 때는 마이크로커널이 작은 커널이기 때문에 변경할 대상이 비교적 적은 경향이 있다. 결과적으로 만들어지는 운영체제는 한 하드웨어로부터 다른 하드웨어로 이식이 쉽다.
마이크로커널은 대부분의 서비스가 커널이 아니라 사용자 프로세스로 수행되기 때문에 또한 보다 높은 보안성과 신뢰성을 제공한다. 만일 한 서비스가 잘못되더라도 운영체제의 다른 부분은 아무런 영향을 받지 않는다.
다수의 현대의 운영체제가 마이크로커널 접근 방법을 사용하고 있다. Tru64 UNIX는 사용자에게 UNIX 인터페이스를 제공하지만, Mach 커널로 구현되어 있다. Darwin이라고 알려진 Mac OS X 커널도 Mach 마이크로커널에 기반을 두고 있다.
또 다른 예는 실시간 운영체제인 QNX이다. QNX Neutrino 마이크로커널은 메시지 전달과 프로세스 스케쥴링을 위한 서비스를 제공한다. QNX는 또한 저 수준의 네트워크 통신과 하드웨어 인터럽트를 처리한다.
안타깝게도 마이크로커널은 가중된 시스템 기능 오버헤드 때문에 성능이 나빠진다.
Windows NT의 첫 번째 버전은 마이크로 커널 구조를 가졌는데 이 버전의 성능은 Windows 95에 비교될 정도로 성능이 떨어졌다. Windows NT 4.0은 계층들을 사용자 공간으로부터 커널 공간으로 옮기고 그들을 보다 긴밀히 통합함으로써 성능 문제를 부분적으로 개선하였다. Windows 구조는 Windows XP가 설계될 때까지 마이크로 커널보다는 모놀리식에 가까운 구조였다.

모듈(Modules)

운영체제를 설계하는데 이용되는 최근 기술 중 최선책은 아마도 적재기능 커널 모듈(load-able kernel modules) 기법의 사용일 것이다. 이 접근법에서는 커널은 핵심적인 구성요소의 집합을 가지고 있고 부팅 때 또는 실행 중에 부가적인 서비스들을 모듈을 통하여 링크한다.
이러한 유형의 설계는 Solaris, Linux, Mac OS X, alc, Windows 등 현대 UNIX를 구현하는 일반적인 추세이다.
설계의 주안점은 커널은 핵심 서비스를 제공하고 다른 서비스들은 커널이 실행되는 동안 동적으로 구현하는 것이다. 서비스를 동적으로 링크하는 것은 새로운 기능을 직접 커널에 추가하는 것보다 바람직하다.
후자의 경우 수정 사항이 생길 때마다 커널을 다시 컴파일 해야 한다. 예컨대 CPU 스케쥴링과 메모리 관리 알고리즘은 커널에 직접 구현하고 다양한 파일 시스템을 지원하는 것은 적재가능 모듈을 통하여 구현할 수 있다.
전체적인 결과는 커널의 각 부분이 정의 되고 보호된 인터페이스를 가진다는 점에서 계층 구조를 닮았다. 그러나 모듈에서 임의의 다른 모듈을 호출할 수 있다는 점에서 계층 구조보다 유연하다.
중심 모듈은 단지 핵심 기능만을 가지고 있고 다른 모듈의 적재 방법과 모듈들과 어떻게 통신하는지 안다는 점에서는 마이크로 커널과 유사하다. 그러나 통신하기 위하여 메시지 전달을 호출할 필요가 없기 때문에 더 효율적이다.
그림 2.15에 나와 있는 Solaris 운영체제 구조는 핵심 커널과 7가지 유형의 적재가능 커널 모듈로 구성된다.
1.
스케쥴링 클래스
2.
파일 시스템
3.
적재가능 시스템 호출
4.
실행 파일 형식
5.
STREAMS 모듈
6.
기타 잡다한 기능들
7.
장치 및 버스 드라이버
Linux도 장치드라이버와 파일 시스템을 지원하기 위해 적재 가능 커널 모듈을 사용한다.

혼용 시스템(Hybrid Systems)

사실 엄격하게 정의된 하나의 구조를 채택한 운영체제는 거의 존재하지 않는다. 대신 다양한 구조를 결합하여 성능, 보안 및 편리성 문제를 해결하려는 혼용 구조로 구성된다.
예컨대 Linux와 Solaris는 운영체제 전부가 하나의 주소 공간에 존재하여 효율적인 성능을 제공하기 때문에 모놀리식 구조이다. 그러나 이 운영체제들은 모듈을 사용하기 떄문에 새로운 기능을 동적으로 커널에 추가할 수 있다.
역시 성능 상의 이유로 Windows도 대체적으로 모놀리식 구조라 할 수 있지만 사용자 모드 프로세스로서 실행되는 분리된 서비시스템을 지원하는 등 전형적인 마이크로커널의 형태를 유지하고 있다. Windows 시스템은 또한 동적으로 적재가능한 커널 모듈도 지원한다.

Mac OS X

Apple Mac OS X 운영체제는 혼용 구조를 사용한다. 그림 2.16에 보이는 것처럼 Mac OS X는 계측 구조 시스템이다.
상위 층들은 Aqua 인터페이스(그림 2.4), 응용 환경과 서비스를 제공한다. 특히 Cocoa 환경은 Objective-C 프로그래밍 언어를 위한 API를 명시한다. Objective-C 언어는 Mac OS X 응용을 개발하는데 이용된다.
이 층들의 아래에 커널 환경이 위치하는데 커널 환경은 주로 Mach 마이크로커널과 BSD UNIX 커널로 이루어져 있다. Mach는 메모리 관리, 메시지 전달과 함께 원격 함수 호출(remote procedure calls, RPC)과 프로세스 간 통신 설비의 지원 그리고 스레드 스케쥴링을 제공한다.
BSD 구성요소는 BSD 명령어 라인 인터페이스와 네트워킹과 파일 시스템 지원 그리고 Pthreads를 포함한 POSIX API의 구현을 제공한다.
Mach와 BSD 이외의 커널 환경은 장치 드라이버와 동적 적재가능 모듈(Mac OS X는 커널 확장이라고 부른다)의 개발을 위한 입출력 도구 세트를 제공한다.
그림 2.16에서 보이는 것처럼 BSD 응용 환경은 BSD 설비를 직접 사용할 수 있다.

iOS

iOS는 자사의 스마트폰에서 실행시키기 위하여 설계된 모바일 운영체제로, Mac OS X 운영체제 상에서 구현되어 모바일 장치에 특화된 기능을 추가로 제공하지만 Mac OS X 응용을 직접 실행하지는 않는다. iOS의 구조가 그림 2.17에 나와 있다.
Cocoa Touch는 iOS 장치 상에서 실행될 응용을 개발하기 위한 많은 프레임워크를 제공하는 Objective-C를 위한 API이다. 앞서 언급한 Cocoa와 Cocoa Touch의 근본적인 차이점은 후자가 터치스크린과 같은 모바일 장치의 고유한 하드웨어 기능을 지원한다는 것이다.
미디어 서비스 층은 그래픽, 오디오 및 비디오 서비스를 제공한다.
핵심 서비스 층은 클라우드 컴퓨팅과 데이터베이스 지원과 같은 다양한 기능을 제공한다.
바닥 층은 핵심 운영체제를 나타내는데 그림 2.16에 보인 커널 환경에 기반을 두고 있다.

Android

Android 운영체제는 Android 스마트폰과 태블릿을 위해 개발되었다. iOS가 Apple의 모바일 장치에서 실행하기 위해 설계되었고 소스가 공개되지 않는데 반해 Android는 다양한 모바일 플랫폼에서 실행되며 오픈소스이다. 오픈소스라는 이유가 빠르게 인기가 높아졌던 이유 중 일부를 차지한다. Android의 구조가 그림 2.18에 나와 있다.
Android는 모바일 응용을 개발하기 위한 풍부한 프레임워크를 제공하는 소프트웨어의 계층 구조로 이루어져 있다는 점에서 iOS와 유사하다.
소프트웨어 스택의 제일 바닥에는 Google에 의해 수정되었으며 일반적인 Linux 릴리즈 배포와는 별도로 이루어지기는 하지만 Linux 커널이 존재한다.
Linux는 프로세스, 메모리 및 하드웨어를 위한 장치 드라이버 지원에 주로 이용되며 전력 관리 기능이 추가되었다. Android 실행 환경은 핵심적인 라이브러리 집합과 Dalvik 가상 기계를 포함한다.
Android 장치의 소프트웨어 설계자는 Java를 사용하여 응용을 개발한다. 그러나 표준 Java API를 이용하는 대신 Google은 Java 개발을 위한 별도의 Android API를 설계하였다. Java 클래스 파일은 먼저 Java 바이트 코드로 컴파일 된 후 Dalvik 가상기계에서 실행 가능한 실행 파일로 변환된다.
Dalvik 가상기계는 Android를 위해 설계되었으며 제한된 메모리와 CPU 처리 능력을 가진 모바일 장치에 최적화 되었다.
Android 응용에게 제공되는 라이브러리 집합에는 웹브라우저(webkit), 데이터베이스 지원(Sqlite) 및 멀티미디어 응용을 개발하기 위한 프레임워크가 포함된다. libc 라이브러리는 표준 C 라이브러리와 유사하나 크기가 훨씬 작으며, 모바일 장치의 특징인 저속 CPU를 위하여 설계되었다.

운영체제 디버깅

넓게는 디버깅은 하드웨어와 소프트웨어에서의 시스템의 오류를 발견하고 수정하는 행위이다. 성능 문제는 버그로 간주되므로 시스템에서 처리 중에 발생하는 병목 현상을 제거하여 성능을 향상 시키려는 성능 조정(performance tuning)도 디버깅에 포함된다.

장애 분석(Failure Analysis)

만일 프로세스가 실패한다면, 대부분의 운영체제는 시스템 구동자 또는 문제를 발생시킨 사용자에게 문제가 발생했다는 것을 경고하기 위해 오류 정보를 로그 파일에 기록한다.
운영체제는 또한 프로세스가 사용하던 메모리를 캡처한 코어 덤프(core dump)를 취하고 차후 분석을 위해 파일로 저장한다(초창기 시절에 메모리를 ‘코어’라고 칭했다.)
실행 중인 프로그램과 코어 덤프는 프로그래머가 프로세스의 코드와 메모리를 분석할 수 있도록 설계된 도구인 디버거에 의해 검사될 수 있다.
사용자 수준 프로세스 코드를 디버깅한느 것은 도전적인 일이다. 커널의 크기와 복잡도, 하드웨어 제어 및 사용자 수준 디버깅 도구가 없기 때문에 운영체제 커널을 디버깅하는 것은 훨씬 복잡하다.
커널 장애는 충돌(crash)라고 불린다. 프로세스 장애와 마찬가지로 오류 정보가 로그 파일에 저장되고 메모리의 상태가 충돌 덤프(crash dump)에 저장된다.
운영체제 디버깅과 프로세스 디버깅은 종종 두 태스크의 근본적인 차이에 의해 서로 다른 도구와 기법을 사용한다.
파일 시스템 코드 때문에 발생한 커널 장애는 재부팅 전에 커널의 상태를 파일 시스템에 저장하려는 시도를 위험하게 한다. 일반적인 기법은 커널의 메모리 상태를 이 용도를 위해 예약된 파일 시스템을 가지지 않은 디스크의 특정 부분에 저장하는 것이다.
커널이 복구 불가능한 오류를 탐지하면 메모리의 전체 내용 또는 적어도 시스템 메모리의 커널이 소유한 부분만이라도 이 디스크 영역에 저장한다.
시스템이 재부팅 되면 프로세스는 이 영역으로부터 데이터를 수집하고 분석을 위해 파일 시스템의 충돌 덤프 파일에 기록한다. 분명하게 이러한 전략은 보통의 사용자 수준 프로세스를 디버깅 할 때는 필요하지 않다.

성능 조정(Performance Tuning)

앞서 성능 조정은 처리 병목 지점을 제거함으로써 성능을 향상시키려 한다고 언급하였다. 병목 지점을 발견하기 위해 시스템 성능을 감시할 수 있다. 따라서 시스템 동작을 측정하고 표시할 수 있는 방법을 가지고 있어야 한다.
많은 시스템에서 운영체제는 이 작업을 위하여 시스템 동작의 추적 목록을 생상한다. 모든 관심 있는 사건은 시간과 중요 매개변수와 함께 기록되며 파일에 기록된다.
후에 분석 프로그램이 로그 파일을 처리하여 시스템 성능을 결정하고 병목 지점과 비효율성을 발견한다. 이 동일한 추적은 개선된 시스템의 모의실험을 위해 입력으로 사용될 수 있다. 추적은 또한 운영체제 동작의 오류를 발견하는데 도움을 줄 수도 있다.
성능 조정을 하는 또 다른 접근법은 사용자와 관리자가 병목을 찾기 위해 시스템의 다양한 구성요소들의 상태를 살펴보기 위한 목적을 가진 대화형 도구를 사용하는 것이다.
그러한 도구 중 하나인 UNIX 명령어인 top은 시스템에서 사용 중인 자원을 표시하고 그와 함께 가장 많은 자원을 사용하는 프로세스의 순위를 보여준다. 다른 도구들은 디스크 입출력, 메모리 할당 및 네트워크 통신량의 상태를 표시해 준다.
Windows의 작업 관리자는 Windows 시스템에서 유사한 작업을 하는 도구이다. 그림 2.19 참조

DTrace

DTrace는 실행 중인 시스템, 사용자 프로세스와 커널 모두에 동적으로 탐색점을 추가할 수 있는 설비이다. 이 탐색점들은 커널, 시스템 상태 및 프로세스 활동에 관한 놀라울 저옫의 정보를 얻기 위해 D 프로그래밍 언어를 이용하여 질의할 수 있다.
예컨대 그림 2.20은 응용이 시스템 호출(ioctl())을 실행하고 시스템 호출을 실행하면서 호출한 커널 내부 함수들을 보여주고 있다.
사용자 수준과 커널 코드의 상호 작용을 디버깅하는 것은 양쪽의 코드를 이해하고 상호작용을 계측할 수 있는 도구의 집합 없이는 거의 불가능하다.
그런 도구 집합이 정말로 유용하려면 디버깅을 염두에 두지 않고 작성된 부분을 포함한 시스템의 어느 부분도 디버깅할 수 있어야 하며 그 작업을 시스템의 안정성을 해치지 않고 할 수 있어야 한다.
이 도구는 또한 이상적으로는 사용하지 않을 경우에는 성능에 전혀 영향을 주지 않고 사용 중일 때는 비례하게 성능에 영향을 줄 수 있도록 성능에 미치는 영향을 최소로 해야 한다.
DTrace 도구는 이러한 요구 조건을 만족시키면서 동적이고, 안전하며 낮은 영향력을 미치는 디버깅 환경을 제공한다.
DTrace는 컴파일러, 구조, 구조 안에서 작성된 검사점 제공자 그리고 그 검사점의 소비자로 구성된다.
DTrace 제공자는 검사점을 생성한다. 제공자가 생성한 모든 검사점을 추적하기 위한 커널 구조가 존재한다.
검사점은 해시 테이블 자료구조에 저장되고, 이 자료구조는 이름으로 해싱되고 유일한 검사점 식별자에 따라 인덱싱 된다. 검사점이 활성화 되면 검사될 영역의 약간의 코드가 dtrace_probe(probe identifier)를 호출하도록 재작성되고 코드의 원래 연산을 계속 진행한다.
다른 제공자는 다른 종류의 검사점을 생성한다.
DTrace 커널에서 실행되는 바이트 코드를 생성할 수 있는 컴파일러를 특지응로 한다. 이 코드는 컴파일러에 의해 ‘안전’하다고 보장된다. 예컨대 루프를 만들 수 없으며, 명시적인 요청을 통해 특정 커널 상태만을 변경할 수 있다.
커널 전용 데이터를 검색할 수 있고 요청이 있을 경우에는 데이터를 수정할 수 있기 때문에 오직 DTrace 특권을 가진 사용자 또는 루트 사용자만이 DTrace를 사용할 수 있다. 생성된 코드는 커널에서 실행되고 검사점을 활성화 한다.
또한 사용자 모드의 소비자를 활성화하고 둘 간의 통신을 가능하게 한다.
DTrace 소비자는 검사점과 그 결과에 관심 있는 코드를 말한다. 소비자는 생산자가 하나 이상의 검사점을 생성하도록 요청한다. 검사점이 시작되면 검사점은 커널에 의해 관리되는 데이터를 방출한다.
검사점이 시작되면 커널 안에서 제어 블록 활성화(enabling control blocks, ECB)라고 불리는 작업이 실행된다. 하나 이상의 소비자가 검사점을 요구할 때 한 검사점은 여러 ECB가 실행되도록 할 수 있다. 각 ECB는 ECB를 걸러낼 수 있는 술어 논리(”if 문”)를 포함하고 있다. 그렇지 않다면 ECB의 작업 목록이 실행된다.
가장 일밙거인 작업은 검사점 실행 시점의 변수의 값과 같은 데이터의 어떤 비트를 캡쳐하는 것이다. 그런 데이터를 수집함으로써 사용자 또는 커널 활도으이 완전한 그림이 생성된다.
게다가 사용자 공간과 커널 양쪽에서 시작된 검사점은 어떻게 사용자 수준 활동이 커널 수준 반응을 일으키는지를 보여줄 수 있다. 그런 데이터는 성능 감시와 코드 최적화를 위한 귀중한 정보이다.
검사점 소비자가 종료하면 해당 ECB는 제거된다. 검사점을 소비하는 ECB가 존재하지 않으면 검사점은 제거된다.
이 제거는 dtrace_probe() 호출을 제거하여 원래 코드로 되돌아 갈 수 있도록 코드를 재작성하는 것을 포함한다. 따라서 검사점이 생성되기 전 그리고 검사점이 파괴된 후의 시스템은 마치 검사가 일어나지 않은 것처럼 동일한 상태를 유지한다.
DTrace는 검사점이 너무 많은 메모리 또는 CPU 용량을 사용하지 않는 것을 보장하기 위해 신경을 쓴다. 많은 메모리와 CPU 용량을 사용하게 되면 실행 중인 시스템에 피해를 줄 수 있다.
검사 결과를 저장하기 위해 사용되는 버퍼는 디폴트와 최대 제한점을 넘지 않도록 감시된다. 검사점 실행을 위한 CPU 시간도 역시 감시된다.
제한점을 넘게 되면 소비자는 종료되고 따라서 제한을 넘은 검사점도 같이 제거된다. 경쟁과 데이터 손실을 방지하기 위하여 버퍼는 CPU 마다 할당된다.
D code의 예와 그 출력이 그 유틸리티를 보인다. 다음 프로그램은 스케줄러 검사점을 활성화하고 검사점이 활성화 된 기간 동안, 즉 프로그램이 실행되는 동안, 사용자 ID 101번의 각 프로세스가 사용한 CPU 시간을 기록하는 DTrace 코드를 보이고 있다.
sched:::on-cpu uid == 101 { self->ts = timestamp } sched:::off-cpu self->ts { @time[execname] = sum(timestamp - self->ts); self->ts = 0; }
C++

운영체제 생성

한 사이트에서 하나의 특정 기계를 위하여 운영체제를 설계, 코드 작성, 구현하는 것이 가능하다. 그러나 일반적으로 운영체제는 다양한 주변 구성을 가진 여러 사이트에 있는 여러 부류의 기계에서 수행되도록 설계되는 것이 보다 일반적이다. 그 경우 시스템은 각 특정 컴퓨터 사이트를 위해 구성되거나 또는 생성되어야 하는데, 이 절차를 시스템 생성(SYSGEN)이라 한다.
시스템을 생성하기 위해 우리는 특수한 프로그램을 사용한다. SYSGEN 프로그램은 하드웨어 시스템의 특정 구성에 관한 정보를 운영자에게 요구하거나 주어진 파일로부터 읽어 들이거나 또는 어느 구성 요소가 있는지 결정하기 위해 하드웨어를 직접 시험한다. 아래와 같은 종류의 정보들이 반드시 결정되어야 한다.
무슨 CPU가 사용되는가? 설치된 옵션(확장된 명령 집합, 부동소수점 연산 등)은 무엇인가? 다중 CPU 시스템들에 대해서는 각 CPU를 반드시 기술해야 한다.
부트 디스크는 어떻게 포맷될 것인가? 얼마나 많은 섹션 또는 파티션으로 분할되어야 하고 각 파티션에는 어떤 내용들이 저장되어야 하는가?
사용 가능한 메모리의 크기를 얼마인가? 어떤 시스템은 불법 주소(illegal address) 폴트가 발생할 때까지 메모리 위치를 차례대로 참조함으로써 시스템 스스로가 메모리의 크기를 정한다. 이 절차는 최종의 합법적 주소와 사용 가능한 메모리의 크기를 정의한다.
어떠한 주변 장치가 사용 가능한가? 시스템은 각 장치를 어떻게 가리킬 지(장치의 번호), 장치 인터럽트 번호, 장치 유형과 모델 그리고 특별한 장치의 특성 등을 알 필요가 있다.
어떠한 운영체제 옵션이 필요한지, 또는 어떤 매개변수 값이 사용되어야 하는가? 이들 옵션이나 값들이 어떠한 크기의 버퍼를 몇 개나 필요로 할지, 요구되는 CPU 스케줄링 알고리즘의 타입은 무엇인지, 지원될 프로세스의 최대 개수는 몇 개인가 등을 포함할 수 있다.
일단 이러한 정보들이 결정되면 이들은 여러 가지로 사용될 수 있다. 극단적으로는 시스템 관리자가 운영체제의 원천 프로그램의 사본을 변경하는데 사용할 수 있다.
그런 다음 운영체제가 완전히 컴파일 된다. 데이터 선언, 초기화 그리고 상수들은 조건부 컴파일과 함께 기술도니 시스템에 맞게 개별화된 운영체제의 목적 버전을 만들어낸다.
약간 덜 개별화된 수준에서는 시스템 기술은 미리 컴파일 된 라이브러리로부터 모듈을 선택하거나 테이블 생성을 유발할 수도 있다. 이러한 모듈들이 함께 링크되어 최종적으로 생성된 운영체제를 만든다.
선택은 라이브러리가 모든 지원되는 입출력 장치를 위한 장치 드라이버를 갖는 것은 허용하지만, 실제 필요한 것만 운영체제에 링크된다. 시스템이 다시 컴파일 되지 않기 때문에 시스템 생성은 더 빠르다. 그러나 결과적으로 만들어지는 시스템은 지나치게 일반적일 수 있다.
다른 방향의 극단으로 완전히 테이블 방식에 의해 시스템을 구성할 수도 있다. 모든 코드는 항상 시스템의 일부분이며, 선택이 컴파일이나 링크 시간이 아니라 실행 시에 일어난다. 시스템 생성은 단순히 시스템을 기술하기 위해 적절한 테이블을 생성하는 일을 포함한다.
대부분의 현대 운영체제는 이러한 방법으로 만들어진다.
이들 접근 방법들의 주요 차이점은 생성된 시스템의 크기와 일반성, 그리고 하드웨어 구성의 변화에 따른 변경의 용이성이다.

시스템 부트

운영체제가 생상된 후에 하드웨어의 의해 사용 가능해야 한다. 그러나 하드웨어는 커널이 어디에 있는지 어떻게 적재해야 하는지를 어떻게 알 수 있는가?
커널을 적재하여 컴퓨터를 시동하는 절차는 시스템을 부팅하는 것으로 알려져 있다. 대부분의 컴퓨터 시스템에는 부트스트랩 프로그램 또는 부트스트랩 로더로 알려져 있는 작은 크기의 코드가 커널을 찾고, 그것을 주 메모리에 적재하고 수행을 시작한다.
PC와 같은 일부 컴퓨터 시스템은 단순한 부트스트랩 로더가 더욱 복잡한 더욱 복잡한 부트 프로그램을 디스크로부터 적재하고, 이 부트 프로그램이 다시 커널을 적재하는 두 단계 절차를 사용한다.
컴퓨터가 전원을 키거나 재부팅 등의 리셋 사건을 받으면 명령 레지스터는 미리 지정된 메모리 위치를 가리키게 되고 그곳에서부터 실행을 시작한다.
그 위치에는 최초의 부트스트랩(bootstrap) 프로그램이 존재한다. RAM은 시스템 시작 시에 알 수 없는 상태가 되기 떄문에 이 프로그램은 ROM(read-only memory) 안에 저장된다. ROM은 초기화할 필요가 없고, 바이러스 같은 것을 염려하지 않아도 되기 때문에 편리하다.
부트 프로그램은 다양한 작업을 수행한다. 가장 흔한 작업 중 하나는 기계의 상태를 진단하는 작업이다. 이 진단 작업을 통과하면 프로그램은 부팅 절차를 계속 진행한다. CPU 레지스터, 장치 제어기, 주 메모리의 내용 등 시스템 전반에 걸쳐 초기화된다. 조만간 부트스트랩 프로그램은 운영체제를 시작 시킨다.
휴대전화, 태블릿, 게임 콘솔 드으이 시스템들은 운영체제 전체를 ROM에 저장한다. 운영체제를 ROM에 저장하는 것은 운영체제의 크기가 작거나 간단한 하드웨어를 지원하거나 험한 환경에서 실행되는 시스템에 적합하다.
이 방식의 문제점은 부트스트랩 코드가 변경되면 ROM 하드웨어 칩을 교체해야 한다는 것이다. 몇몇 시스템은 이 문제를 EPROM(erasable programmable read-only memory)를 사용하여 해결하였다. 이 EPROM은 쓰기 가능하도록 만드는 명령어가 주어지기 전에는 읽기 전용 상태를 유지한다.
하드웨어와 소프트웨어의 중간적 특성을 가지기 때문에 ROM의 모든 형태를 firmware라고 부른다. Firmware의 일반적인 문제는 RAM에서 실행시킬 때보다 실행 속도가 떨어진다는 것이다. 그래서 몇몇 시스템은 운영체제를 firmware에 저장하고 실행할 때는 RAM으로 복사하여 실행한다. Firmware의 마지막 문제는 가격이 비싸서 용량이 크지 않다는 것이다.
Windows, Mac OS X 및 Linux 같은 일반적인 운영체제를 포함하여 대용량의 운영체제 또는 자주 변경되는 시스템에서는 부트스트랩 로더는 firmware에 있고 운영체제는 디스크에 존재한다.
이 경우 부트스트랩은 진단 절차를 수행하고 고정된 위치(예컨대 블록 0)의 디스크 블록 하나를 읽어 메모리에 적재하고 그 위치로부터 실행시킬 수 있는 코드를 가진다. 이 블록을 부트 블록이라고 한다. 부트 블록에 저장된 프로그램은 운영체제 전부를 메모리에 적재하고 실행을 시작할 수 있을만큼 복잡할 수 있다.
더 일반적으로는 한 블록에 저장되어야 하기 때문에 나머지 부트 프로그램의 디스크 상의 주소와 길이만 알고 있는 간단한 코드이다. GRUB은 Linux 시스템을 위한 오픈소스 부트스트랩 프로그램의 한 예이다.
디스크 상의 부트스트랩과 운영체제는 새 버전을 디스크에 기록함으로써 쉽게 변경할 수 있다. 부트 파티션을 가지고 있는 디스크는 부트 디스크 또는 시스템 디스크라 불린다.
모든 부트 프로그램이 적재되면 파일 시스템을 탐색하여 운영체제 커널을 찾아내고 메모리로 적재한 후 실행을 시작한다. 시스템이 실행 중이라고 할 수 있는 시점은 바로 이 시점이다.