본문으로 건너뛰기

운영체제 구조


운영체제는 프로그램 실행환경을 제공한다. 운영체제는 프로그램과 그 프로그램의 사용자에게 특정 서비스를 제공한다.

  • 사용자 인터페이스(user interface): 거의 모든 운영체제는 사용자 인터페이스(UI)를 제공한다. 이 인터페이스는 여러 형태로 제공될 수 있다. 가장 일반적으로 그래픽 사용자 인터페이스(GUI)가 사용된다. 여기서 인터페이스는 윈도 시스템으로 I/O를 지시하고, 메뉴에서 선택하고, 화면을 선택하는 포인팅 장치인 마우스와 텍스트를 입력할 키보드를 가지고 있다. 또 다른 옵션은 명령어 라인 인터페이스(CLI)이다. 이 인터페이스는 명령을 사용하여 이를 입력할 방법이 사용된다.
  • 프로그램 수행(program execution): 시스템은 프로그램을 메모리에 적재해 실행할 수 있어야 한다. 프로그램은 정상적이든, 혹은 비정상적이든 실행을 끝낼 수 있어야 한다.
  • 입출력 연산(I/O operation): 수행 중인 프로그램은 입출력을 요구할 수 있다. 이러한 입출력에는 파일 혹은, 입출력 장치가 연관될 수 있다. 특정 장치에 대해서는 특수한 기능(네트워크 인터페이스에서 읽거나 파일 시스템에 쓰기 등)이 요구될 수 있다. 효율과 보호를 위해, 사용자들은 통상 입출력 장치를 직접 제어할 수 없다. 따라서 운영체제가 입출력 수해으이 수단을 제공해야 한다.
  • 파일 시스템 조작(file system manipulation): 파일 시스템은 특히 중요한 분야이다. 명핵히, 프로그램은 파일을 읽고 쓸 필요가 있다. 프로그램은 또한 이름에 의해 파일을 생성하고 삭제할 수 있고 지정된 파일을 찾을 수 있어야 하고 파일의 정보를 열거할 수 있어야 한다. 마지막으로 몇몇 프로그램은 파일 소유권에 기반을 둔 권한 관리를 이용하여 파일이나 디렉터리 접근을 허가하거나 거부할 수 있게 한다. 많은 운영체제들은 떄로는 개인의 선택에 따라 그리고 때로는 특정 특성과 성능 특성을 제공하기 위하여 다양한 파일 시스템을 제공한다.
  • 통신(communication): 한 프로세스가 다른 프로세스와 정보를 교환해야 할 필요가 있는 상황이 있다. 통신은 공유 메모리를 통해서 구현될 수도 있고, 메시지 전달(message passing) 기법을 사용해서 구현될 수 있는데, 후자의 경우 정보의 패킷들이 운영체제에 의해 프로세스들 사이를 이동한다.
  • 오류 탐지(error detection): 운영체제는 모든 가능한 오류를 항상 의식하고 있어야한다. 오류는 CPU, 메모리 하드웨어, 입출력 장치, 또는 사용자 프로그램에서 일어날 수 있다. 운영체제는 올바르고 일관성 있는 계산을 보장하기 위해 각 유형의 오류에 대해 적당한 조치를 해야 한다.
  • 자원 할당(resource allocation): 다수의 프로세스나 다수의 작업이 동시에 진행될 때, 그들 각각에 자원을 할당해 주어야 한다. 운영체제는 여러 가지 다른 종류의 자원을 관리한다.
  • 로깅(logging): 우리는 어떤 프로그램이 어떤 종류의 자원을 얼마나 많이 사용하는지 추적할 수 있길 원한다. 이와 같은 기록 관리는 회계, 또는 단순히 사용 통계를 내기 위해 사용된다. 사용 통계는 컴퓨팅 서비스를 개선하기 위해 시스템을 재구성하고자 하는 시스템 관리자에게 중요한 자료가 될 수 있다.
  • 보호(protection)와 보안(security): 다중 사용자 컴퓨터 시스템 또는 네트워크로 연결된 컴퓨터 시스템에 저장된 정보의 소유자는 그 정보의 사용을 통제하길 원한다. 서로 다른 여러 프로세스가 병행하게 수행될 때, 한 프로세스가 다른 프로세스나 운영체제 자체를 방해해서는 안 된다. 보호는 시스템 자원에 대한 모든 접근이 통제되도록 보장하는 것을 필요로 한다. 외부로부터의 시스템 보안 또안 중요하다. 이러한 보안은 각 사용자가 자원에 대한 접근을 원할 때 통상 패스워드를 사용해서 자기 자신을 인증하는 것으로부터 시작된다. 보안은 네트워크 어댑터 등과 같은 외부 입출력 장치들을 부적합한 접근 시도로부터 지키고, 침입의 탐지를 위해 모든 접속을 기록하는 것으로 범위를 넓힌다.

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

명령 인터프리터(Command Interpreter)

Linux, UNIX 및 Windows를 포함한 운영체제 대부분은 명령 인터프리터를 프로세스가 시작하거나 사용자가 (대화형 시스템상에서) 처음 로그온 할 때 수행되는 특수한 프로그램으로 취급한다. 선택할 수 있는 여러 명령 인터프리터를 제공하는 시스템에서 이 해석기는 셸(shell)이라고 불린다.

명령 인터프리터의 중요한 기능은 사용자가 지정한 명령을 가져와서 그것을 수행하는 것이다. 이 수준에서 제공된 많은 명령은 파일을 조작한다. 즉 생성, 삭제, 리스트, 프린트, 복사, 수행등을 합니다. UNIX 시스템에서 사용 가능한 다양한 셸은 이런 방식으로 실행된다. 이 명령어들은 두가지 일반적인 방식으로 구현될 수 있다.

한 가지 방법은 명령 인터프리터 자체가 명령을 실행할 코드를 가지는 경우이다. 예를 들면, 한 파일을 삭제하기 위한 명령은 명령 인터프리터가 자신의 코드의 한 부분으로 분기하고, 그 코드 부분이 매개변수를 설정하고 적절한 시스템 콜을 한다. 이 경우 제공되는 명령의 수가 명령 인터프리터의 크기를 결정하는데, 그 이유는 각 명령이 자신의 구현 코드를 요구하기 때문이다. 여러 운영체제 중 UNIX에 의해 사용되는 다른 대안의 접근 방법은, 시스템 프로그램에 의해 대부분의 명령을 구현하는 것이다. 이러한 경우 명령 인터프리터는 전혀 그 명령을 알지 못한다. 단지 메모리에 적재되어 있는 실행될 파일을 식별하기 위해 명령을 사용한다. 따라서 파일을 삭제하는 다음의 UNIX 명령은 rm file.txt rm이라고 불리는 파일을 찾아서, 그 파일을 메모리에 적재하고, 그것을 매개변수 file.txt로 수행한다. rm 명령과 관련된 로직은 rm이라는 파일 내의 코드로 완전하게 정의된다. 이러한 방법으로 프로그래머는 적합한 프로그램 로직을 가진 새로운 파일을 생성함으로써 시스템에 새로운 명령을 쉽게 추가할 수 있다. 명령 인터프리터 프로그램은 이제 아주 작아질 수 있으며, 새로운 명령을 추가하기 위해 변경될 필요가 없다.

그래픽 기반 사용자 인터페이스

운영체제와 접촉하는 두 번째 방식은 사용자 친화적인 그래픽 기반 사용자 인터페이스 또는 GUI를 통하는 방식이다. 이 방식에서는 명령어 라인 인터페이스를 통하여 사용자가 직접 명령어를 입력하는 것이 아니라 데스크톱이라고 특정지어지는 마우스를 기반으로 하는 윈도 메뉴 시스템을 사용한다.

사용자는 마우스를 움직여 마우스 포인터를 프로그램, 파일, 시스템 기능등을 나타내는 화면상의 이미지에 위치시킨다. 마우스 포인터의 위치에 따라, 마우스 버튼을 누름으로써 프로그램을 호출하거나 파일 혹은 디렉터리를 선택할 수도 있고, 또는 명령을 포함한 메뉴를 잡아당길수도 있다.

터치 스크린 인터페이스

대부분의 모바일 시스템에는 명령 라인 인터페이스나 마우스 및 키보드 시스템이 실용적이지 않기 때문에 스마트폰 및 휴대용 태블릿 컴퓨터는 일반적으로 터치스크린 인터페이스를 사용한다. 사용자는 터치스크린에서 손가락을 누르거나 스와이프하는 등의 제스처를 취하여 상호작용한다.

인터페이스의 선택

명령어 라인 또는 그래픽 기반 인터페이스를 사용할 것인지는 개인의 선호에 달려 있다. 컴퓨터를 관리하는 시스템 관리자와 시스템에 대해 깊게 알고 있는 파워 유저들은 명령어-라인 인터페이스를 사용한다. 그들로서는 하고자 하는 작업에 대해 더 빨리 접근할 수 있으므로 명령어-라인 인테퍼이스가 더 효율적이다. 사실 몇몇 시스템에서는 GUI를 통해서는 시스템 기능의 일부만을 이용할 수 있고 자주 쓰이지 않는 나머지 기능은 명령어-라인을 사용할 수 있는 사용자만이 이용할 수 있다.

셸 스크립트는 UNIX와 Linux와 같이 명령어-라인 인터페이스에 기반을 둔 시스템에서는 매우 흔한 형태이다.

시스템 콜(System Calls)

시스템 콜은 운영체제에 의해 사용 가능하게 된 서비스에 대한 인터페이스를 제공한다. 특정 저수준 작업(예를 들면 하드웨어를 접근해야 하는 작업)은 어셈블리 명령을 사용하여 작성되어야 하더라도 이러한 호출은 일반적으로 C와 C++ 언어로 작성된 함수 형태로 제공된다.

예제

운영체제가 어떻게 시스템 콜을 사용할 수 있게 만드는지에 대해 논의하기 전에 시스템 콜이 어떻게 사용되는지 설명하는 예를 보도록 하자.

한 파일로부터 데이터를 읽어서 다른 파일로 복사하는 간단한 프로그램을 작성한다고 해보자. 프로그램이 필요로 하는 첫번째 입력은 두 개의 파일, 즉 입력 파일과 출력 파일의 이름일 것이다. 이 이름들은 운영체제의 설계에 따라 여러 가지 방법으로 지정할 수 있다. 한 가지 방법은 명령의 일부로 두 파일의 이름을 전달하는 것이다.(예: UNIX cp 명령)

cp in.txt out.txt

이 명령은 입력 파일 in.txt를 출력하일 out.txt에 복사한다. 두 번째 방법은 프로그램이 사용자에게 이름을 요청하는 것이다. 대화형 시스템에서 이 방법은 일련의 시스템 콜이 필요하다. 먼저 화면에 프롬프트 메시지를 작성한 다음 키보드에서 두 파일의 이름을 지정하는 문자를 읽는다. 마우스 기반 및 아이콘 기반 시스템에서 파일 이름 메뉴는 일반적으로 창에 표시된다. 그런 다음 사용자는 마우스를 사용하여 소스 이름을 선택할 수 있으며 대상 이름을 지정할 수 있는 창을 열 수 있다. 이 일련의 작업을 위해서는 많은 I/O 시스템 콜이 필요하다.

일단 두개의 파일 이름이 얻어지면, 프로그램은 반드시 입력 파일을 오픈하고 출력 파일을 생성한 후 오픈한다. 각각의 이러한 연산은 또 다른 시스템 콜을 필요로 하며 각 시스템 콜에서 오류가 발생하면 처리되어야 한다. 예를 들어 프로그램이 입력 파일을 오픈하려고 했을 때, 그 이름을 갖는 파일이 존재하지 않거나 그 파일에 대한 접근이 금지되어 있는 것을 발견할 수 있다. 이러한 경우 프로그램은 에러 메시지를 출력하고(또 다른 일련의 시스템 콜이다) 비정상적으로 종료(또 다른 시스템 콜이다)한다. 만약 입력 파일이 존재하면 새로운 출력 파일을 생성해야 한다. 이때 동일한 이름을 가진 출력 파일이 이미 존재하는 경우가 있다. 이러한 상황은 프로그램을 중단(abort)(하나의 시스템 콜임)하게 하거나, 또는 우리가 기존 파일을 삭제(다른 시스템 콜임)한 후, 새로운 파일을 생성(다른 시스템 콜임)할 수도 있다. 대화형 시스템에서 또 다른 방법은 기존의 파일을 대체할 것인지, 혹은 프로그램을 중단할 것인지를 사용자에게 물어보는(프롬프트 메시지의 출력과 터미널로부터 응답을 읽기 위한 일련의 시스템 콜) 것이다.

이제 두 개의 파일이 준비되면, 입력 파일로부터 읽어서, 출력 파일에 기록하는 루프에 들어가게 된다. 각 읽기와 쓰기는 가능한 여러가지 오류 상황의 정보를 반환해야 한다. 입력에서 프로그램 이 파일의 끝에 도달하거나 읽기 중에 하드웨어 오류(이를테면 패리티 오류)가 발생할 수도 있다. 쓰기 연산시 출력 장치에 따라 여러 가지 오류들(예를 들어 디스크 공간의 부족)이 발생할 수도 있다.

마지막으로 전체 파일이 복사된 후, 프로그램은 두 개의 파일을 닫고, 콘솔 또는 윈도에 메시지를 기록하고, 결국 정상적으로 종료하게된다.

응용 프로그래밍 인터페이스

간단한 프로그램이라도 운영체제의 기능을 아주 많이 사용하게 된다. 종종 초당 수천 개의 시스템 콜을 수행하게 된다. 대부분의 응용 프로그램 개발자들은 응용 프로그래밍 인터페이스에 따라 프로그램을 설계한다. API는 각 함수에 전달되어야 할 매개변수들과 프로그래머가 기대할 수 있는 반환 값을 포함하여 응용 프로그래머가 사용 가능한 함수의 집합을 명시한다.

UNIX와 Linux 시스템에서 C 언어로 작성된 프로그램을 위해서 제공되는 라이브러리는 libc로 불린다. 모든 운영체제는 고유의 시스템 콜 이름을 가진다.

API를 구성하는 함수들은 통상 응용 프로그래머를 대신하여 실제 시스템 콜을 수행한다. 예를 들어 Windows 함수 CreateProcess()는 실제로 Window 커널의 NTCreateProcess() 시스템 콜을 부른다.

왜 응용 프로그래머는 실제 시스템 콜을 부르는 것보다 API에 따라 프로그래밍하는 것을 선호하는가? 그렇게 하는데에는 몇 가지 이유가 있다. 한가지 이점은 프로그램의 호환성과 관련 있다. API에 따라 프로그램을 설계하는 응용 프로그래머는 자신의 프로그램이 같은 API를 지원하는 어느 시스템에서건 컴파일되고 실행된다는 것을 기대할 수 있다. 게다가 실제 시스템 콜은 종종 더 자세한 명세가 필요하고 프로그램상에서 작업하기가 응용 프로그래머에게 가용한 API보다 어렵다. 그럼에도 불구하고 API 함수를 호출하는 것과 커널의 관련된 시스템 콜을 호출하는 것에는 강한 상관관계가 존재한다. 사실 대부분의 POSIX와 Windows API는 UNIX, Linux 및 Winodws 운영체제가 제공하는 고유의 시스템 콜과 유사하다.

시스템 콜을 처리하는데 있어 중요한 또 다른 요소는 런타임 환경이다. 컴파일러 또는 인터프리터를 포함하여 특정 프로그래밍 언어로 작성된 응용 프로그램을 실행하는 데 필요한 전체 소프트웨어 제품군과 라이브러리 또는 로더와 같은 다른 소프트웨어를 함께 가리킨다. RTE는 운영체제가 제공하는 시스템 콜에 대한 연결고리 역할을 하는 시스템 콜 인터페이스를 제공한다. 이 시스템 콜 인터페이스는 API 함수의 호출을 가로채어 필요한 운영체제 시스템 콜을 부른다. 통상 각 시스템 콜에는 번호가 할당되고 시스템 콜 인터페이스는 이 번호에 따라 색인되는 테이블을 유지한다. 시스템 콜 인터페이스는 의도하는 시스템 콜을 부르고 시스템 콜의 상태와 반환 값을 돌려준다.

호출자는 시스템 콜이 어떻게 구현되고 실행 중 무슨 작업을 하는지 아무것도 알 필요가 없다. 호출자는 단지 API를 준수하고 시스템 콜의 결과로서 운영체제가 무엇을 할 것인지만 이해하면 된다. 따라서 운영체제 인터페이스에 대한 대부분의 자세한 내용은 API에 의해 프로그래머로부터 숨겨지고 RTE에 의해 관리된다.

시스템 콜의 유형(Types of System Calls)

시스템 콜은 다섯 가지의 중요한 범주, 즉 프로세스제어, 파일 조작, 장치 조작, 정보 유지 보수와 통신과 보호등으로 묶을 수 있다.

  • 프로세스 제어(process control)
    • 끝내기(end), 중지(abort)
    • 적재(load), 수행(execute)
    • 프로세스 생성, 프로세스 종료
    • 프로세스 속성(attributes) 획득, 프로세스 속성(attributes) 설정
    • 시간을 기다림
    • 이벤트를 기다림(wait event), 이벤트를 알림(signal event)
    • 메모리 할당 및 자유화
  • 파일 조작(File Manipulation)
    • 파일 생성(create file), 파일 삭제(delete file)
    • 열기(open), 닫기(close)
    • 읽기, 쓰기, 위치 변경(reposition)
    • 파일 속성 획득 및 설정
  • 장치 관리(Device Management)
    • 장치를 요구(request devices), 장치를 방출(release devices)
    • 읽기, 쓰기, 위치 변경(reposition)
    • 장치 속성 획득, 장치 속성 설정
    • 장치의 논리적 부착(attach) 또는 분리(detach)
  • 정보 유지(Information Maintenance)
    • 시간과 날짜의 설정과 획득
    • 시스템 데이터의 설정과 획득
    • 프로세스, 파일, 장치 속서의 획득
    • 프로세스, 파일, 장치 속성의 설정
  • 통신(Communication)
    • 통신 연결의 생성, 제거
    • 메시지의 송신, 수신
    • 상태 정보 전달
    • 원격 장치의 부착(attach) 및 분리(detach)
  • 보호(protection)
    • get file permissions
    • set file permissions

실행 중인 프로그램은 수행을 정상적으로(end()) 또는 비정상적으로(abort()) 멈출 수 있어야 한다. 만약 현재 실행 중인 프로그램을 비정상적으로 중지하기 위해 시스템 콜이 호출되거나 프로그램에 문제가 발생해 오류 트랩(trap)을 유발할 경우, 때때로 메모리 덤프가 행해지고 오류 메시지가 생성된다. 이 덤프는 특별한 로그 파일이나 디스크에 기록되고 문제의 원인을 결정하기 위해 디버거에 의해 검사될 수 있다.


한 프로그램을 실행하고 있는 프로세스가 다른 프로그램을 적재(load())하고 실행(execute()) 하기를 원할 수 있다. 이 기능은 명령 인터프리터가 사용자 명령 또는 마우스 클릭(click)을 통해서 지시된 프로그램을 실행하는 것을 허용한다.

여기서 주목할 점은 적재된 프로그램이 종료되었을 때 어디로 제어를 되돌려 주느냐 하는 것이다. 이 질문은 기존 프로그램이 유실될지, 보관될지, 새로운 프로그램과 병행하게 실행을 계속 허용할 것인지 하는 문제와 관련 있다.

만약 우리가 새로운 잡이나 프로세스, 혹은 잡들이나 프로세스들의 집할을 생성한다면, 우리는 그들의 실행을 제어할 수 있어야 한다. 이러한 제어는 잡의 우선순위, 최대 허용 실행 시간 등을 포함하여 잡 혹은 프로세스의 속성들을 결정하고 재설정(reset)할 수 있는 능력이 필요하다.(get_process_attributes(), set_process_attributes()) 또한 생성한 잡이나 프로세스가 잘못되었거나 더 이상 필요없다면 종료하기를 원할 수 있다.(terminate_process())

새로운 프로세스를 실행한 후에는 우리는 이들의 실행이 끝나기를 기다려야 할 필요가 있을 수 있다.(wait_time()) 그리고 더욱 가능성이 큰 경우는 우리가 특정 이벤트가 일어날 때까지 기다리는 것이다.(wait_event()) 그 경우 프로세스들은 그 이벤트가 일어나면 신호를 보내야 한다.(signal_event())

빈번하게 둘 이상의 프로세스는 데이터를 공유한다. 공유되는 데이터의 일관성을 보장하기 위해서 운영체제는 종종 프로세스가 공유 데이터를 잠글 수 있는 시스템 콜을 제공한다. 그러면 잠금이 해제될 때까지는 어느 프로세스도 데이터에 접근할 수 없게 된다. 통상 그런 시스템은 acquire_lock()과 release_lock() 시스템 콜을 제공한다.

통신(communication)

통신 모델에는 메시지 전달과 공유 메모리의 두 가지 일반적인 모델이 있다. 메시지 전달 모델에는 통신하는 두 프로세스가 정보를 교환하기 위하여 서로 메시지를 주고받는다. 메시지는 두 프로세스 사이에 직접 교환되거나 우편함을 통하여 간접적으로 교환될 수 있다. 통신이 이루어지기 전에 연결이 반드시 열려야 한다. 상대 통신자(communicator)가 동일한 CPU에 있는 프로세스이든지 또는 통신 네트워크에 의해 연결된 다른 컴퓨터에 있는 프로세스이든지 간에 그 이름을 반드시 알고 있어야 한다. 네트워크의 각 컴퓨터는 호스트 이름을 가지며, 각 컴퓨터는 이들 이름으로 일반적으로 알려져 있다. 마찬가지로 프로세스는 프로세스 이름을 가지고 있으며, 이 이름은 운영체제에 의해 동등한 식별자로 변환되고, 이 식별자는 운영체제가 그 프로세스를 가리키는 데 사용할 수 있다. get_hostid()와 get_processid() 시스템 콜은 이러한 변환을 수행한다. 이들 식별자는 그 후 시스템의 통신 모델에 따라 파일 시스템에 의해 제공되는 범용의 open과 close 호출에 전달되거나, 특정 connection()과 close_connection() 시스템 콜에 전달된다. 수신 프로세스는 통상 통신이 일어날 수 있도록 accept_connection() 호출에 자신의 허가(permission)을 제공한다. 연결을 받아들일 프로세스 대부분은 특수 목적의 데몬(daemon)으로서, 이들은 그러한 목적을 위해 제공된 시스템 프로그램들이다. 그들은 연결을 위해 대기(wait_for_conneciton()) 호출을 수행하고 연결이 이루어질 때 깨어난다. 클라이언트(client)로 알려진 통신의 출발지와 서버(server)로 알려진 수신 데몬은 이어 read_message()와 write_message() 시스템 콜에 의해 메시지를 교환한다. close_connection() 호출은 통신을 종료한다.

공유 메모리 모델에서, 프로세스는 다른 프로세스가 소유한 메모리 영역에 대한 접근을 위해 shard_memeory_create()와 shared_memory_attach() 시스템 콜을 사용한다. 정상적으로 운영체제는 한 프로세스가 다른 프로세스의 메모리에 접근하는 것을 막으려고 한다는 것을 기억하라. 공유 메모리는 두개 이상의 프로세스가 이러한 제한을 제거하는데 동의할 것을 필요로한다. 그런 후, 이들 프로세스는 이러한 공유 영역에서 데이터를 읽고 씀으로써 정보를 교환할 수 있다. 데이터 형식은 운영체제의 제어하에 있는 것이 아니라 이들 프로세스에 의해 결정된다. 프로세스는 또한 동일한 위치에 동시에 쓰지 않도록 보장할 책임을 진다.

이러한 두가지 방법은 운영체제에서 보편적이며, 시스템 대부분은 둘 다 구현한다. 메시지 전달은 소량의 데이터를 교환할 때 유용한데, 이는 피해야할 충돌이 없기 때문이다. 메시지 전달은 또한 컴퓨터 간의 통신을 위해 메모리 공유보다 구현하기가 쉽다. 공유 메모리는 한 컴퓨터 안에서는 메모리 전송 속도로 수행할 수 있으므로 최대 속도와 편리한 통신을 허용한다. 하지만 보호와 동기화 부분에서는 여러 문제점을 가지고 있다.

보호(Protection)

보호는 컴퓨터 시스템이 제공하는 자원에 대한 접근을 제어하기 위한 기법을 지원한다. 역사적으로 보호는 다수의 사용자를 가지는 다중 프로그램 시스템에서만 고려되는 문제였다. 그러나 네트워킹등 인터넷의 출현으로 서버에서 휴대용 컴퓨터까지 모든 컴퓨터 시스템에서 보호를 고려하여야 한다.

통상 보호를 지원하는 시스템 콜은 set_permission()과 get_permission()을 포함하는데, 파일과 디스크와 같은 자원의 허가 권한을 설정하는데 이용된다.

시스템 서비스(System Services)

시스템 서비스는, 시스템 유틸리티로도 알려진 프로그램 개발과 실행을 위해 더 편리한 환경을 제공한다. 그들 중 몇몇은 단순히 시스템 콜에 대한 사용자 인터페이스이며, 반면에 나머지는 훨씬 더 복잡하다. 이들은 다음 몇 가지 범주로 분류할 수 있다.

  • 파일 관리: 이들 프로그램은 파일과 디렉터리를 생성, 삭제, 복사, 개명(rename), 인쇄, 열거하고, 그리고 일반적으로 조작한다.
  • 상태 정보: 어떤 프로그램은 단순히 시스템에게 날짜, 시간, 사용 가능한 메모리와 디스크의 양, 사용자 수, 혹은 비슷한 상태 정보를 묻는다. 복잡한 프로그램들은 상세한 성능, 로깅 및 디버깅 정보를 제공한다. 통상 이 프로그램들은 정보를 단말기나 다른 출력 장치 혹은 파일로 포맷하여 인쇄하거나 GUI의 윈도우에 표시한다. 몇몇 시스템은 환경 설정 정보를 저장하고 검색할 수 있는 등록(registry) 기능을 지원하기도 한다.
  • 파일 변경: 디스크나 다른 저장 장치에 저장된 파일의 내용을 생성하고 변경하기 위해 다수의 문장 편집기(text editor)를 사용할 수 있다. 파일의 내용을 검색하거나 변환하기 위한 특수 명령어가 제공되기도 한다.
  • 프로그래밍 언어 지원: 일반적인 프로그래밍 언어들에 대한 컴파일러, 어셈블러, 디버거 및 해석기가 종종 운영체제와 함께 사용자에게 제공되거나 별도로 다운로드 받을 수 있다.
  • 프로그램 적재와 수행: 일단 프로그램이 어셈블되거나 컴파일된 후, 그것이 수행되려면 반드시 메모리에 적재되어야 한다. 시스템은 절대 로더(absolute loader), 재배치 가능 로더(relocatable loader), 링키지 에디터(linkage editor)와 중첩 로더 (overlay loader)등을 제공할 수 있다. 또한 고급어나 기계어를 위한 디버깅 시스템도 필요하다.
  • 통신: 이들 프로그램은 프로세스, 사용자, 그리고 다른 컴퓨터 시스템들 사이에 가상 접속을 이루기 위한 기법을 제공한다.
  • 백그라운드 서비스: 모든 범용 시스템은 부트할 때 특정 시스템 프로그램을 시작시킬 수 있는 방법을 가지고 있다. 이러한 프로세스 중 일부는 자신들의 한 일을 완수하면 종료하는 반면에, 일부는 시스템이 정지될 때까지 계속해서 실행되는 프로세스도 존재한다. 항상 실행되는 시스템 프로그램 프로세스는 서비스, 서브 시스템 또는 데몬으로 알려져있다. 전형적인 시스템은 수십개의 데몬을 가지고 있다. 게다가 운영체제가 중요한 활동을 커널 문맥에서가 아니라 사용자 문맥에서 실행해야 하는 경우, 데몬을 이용해서 이 작업을 수행할 수 있다.

링커와 로더(Linkers and Loaders)

일반적으로 프로그램은 디스크에 이진 실행 파일(예: a.out 또는 prog.exe)로 존재한다. CPU에서 실행하려면 프로그램을 메모리로 가져와 프로세스 형태로 배치되어야 한다.

소스 파일은 임의의 물리 메모리 위치에 적재되도록 설계된 오브젝트 파일로 컴파일된다. 이러한 형식을 재배치 가능 오브젝트 파일이라고 한다. 링커는 이러한 재배치 가능 오브젝트 파일을 하나의 이진 실행 파일로 결합한다. 링킹 단계에서 표준 C 또는 수학 라이브러리와 같은 다른 오브젝트 파일 또는 라이브러리도 포함될 수 있다.

로더는 이진 실행 파일을 메모리에 적재하는 데 사용되며, CPU는 코어에서 실행할 수 있는 상태가 된다. 링크 및 로드와 관련된 활동은 재배치로, 프로그램 부분에 최종 주소를 할당하고 프로그램 코드와 데이터를 해당 주소와 일치하도록 조정하여 프로그램이 실행될 때 코드가 라이브러리 함수를 호출하고 변수에 접근할 수 있게 한다.


UNIX 시스템의 명령어 라인에 프로그램 이름을 입력하면 셸은 먼저 fork() 시스템 콜을 사용하여 프로그램을 실행하기 위한 새 프로세스를 생성한다. 그런 다음 셀은 exec() 시스템 콜로 로더를 호출하고 exec()에 실행 파일 이름을 전달한다. 그런 다음 로더는 새로 생성된 프로세스의 주소 공간을 사용하여 지정된 프로그램을 메모리에 적재한다. GUI 인터페이스를 사용하는 경우 실행 파일과 연관된 아이콘을 두 번 클릭하면 유사한 메커니즘을 사용하여 로더가 호출된다.

지금까지 설명한 과정에서 모든 라이브러리가 실행 파일에 링크되어 메모리에 적재된다고 가정한다. 실제로 시스템 대부분에서는 프로그램이 적재될 때 라이브러리를 동적으로 링크할 수 있게 한다. 여러 프로세스가 동적으로 링크된 라이브러리를 공유할 수 있어서 메모리 사용이 크게 절약될 수 있다.

오브젝트 파일 및 실행 파일은 일반적으로 표준화된 형식을 가진다. 이 표준 형식은 컴파일된 기계 코드 및 프로그램에서 참조되는 함수 및 변수에 대한 메타데이터를 포함하는 기호 테이블을 포함한다. UNIX 및 Linux 시스템의 경우 이 표준 형식을 ELF(Executable and Linkable Format)라고 한다. 재배치 가능 파일과 실행 파일 각각을 위한 별도의 ELF 형식이 사용된다. 실행 가능 파일의 ELF 파일의 정보 중 하나는 프로그램의 시작점이며, 프로그램을 실행할 때 실행할 첫 번째 명령어의 주소가 저장되어 있다.

응용 프로그램이 운영체제마다 다른 이유

각 운영체제는 고유한 시스템 콜 집합을 제공한다. 시스템 콜은 응용 프로그램이 사용할 수 있도록 운영체제가 제공하는 서비스 집합의 일부이다. 시스템 콜은 어느 정도 같더라도 다른 장벽으로 인해 응용 프로그램을 다른 운영체제에서 실행하기 어렵다.

다음 세 가지 방법 중 한 가지를 사용하여 응용 프로그램이 여러 운영체제에서 실행될 수 있게 만들 수 있다.

  1. 응용 프로그램은 운영체제마다 인터프리터가 제공하는 인터프리터 언어로 작성도리 수 있다. 인터프리터는 소스 프로그램의 각 라인을 읽고 상응하는 기계어 명령을 실행하고, 해당 운영체제의 시스템 콜을 호출한다. 기계어 코드로 구성된 응용 프로그램에 비해 성능이 떨어지고, 인터프리터는 각 운영체제 기능의 일부만 제공하므로 관련 응용 프로그램의 기능도 제한될 수 있다.
  2. 응용 프로그램은 실행 중인 응용 프로그램을 포함하고 있는 가상 머신을 가진 언어로 작성될 수 있다. 가상 머신은 언어의 RTE 중 일부이다. 이 방법의 예로 Java를 들 수 있다. Java는 로더, 바이트 코드 검증기 및 Java 응용 프로그램을 Java 가상 머신으로 적재하는 기타 구성요소를 RTE로 가지고 있다. 이 RTE는 메인 프레임에서 스마트폰에 이르기까지 많은 운영체제에 이식 또는 개발되었으며 이론적으로 모든 Java 앱은 RTE가 제공되는 어디서나 실행될 수 있다. 이런 종류의 시스템은 위에서 설명한 인터프리터 시스템과 유사한 단점을 가진다.
  3. 응용 프로그램 개발자는 컴파일러가 기기 및 운영체제 고유의 이진 파일을 생성하는 표준 언어 또는 API를 사용할 수 있다. 응용 프로그램은 실행될 각 운영체제로 이식되어야 한다. 이 이식은 많은 시간이 소요될 수 있으며 많은 시험과 디버깅을 거쳐 응용 프로그램의 새 버전마다 수행되어야 한다.

이론적으로 세가지 접근 방식은 다양한 운영체제에서 실행될 수 있는 응용 프로그램을 개발하기 위한 간단한 솔루션을 제공한다. 그러나 일반적으로 응용 프로그램의 이동성이 부족한데는 여러가지 원인이 있다. 이로 인해 여전히 크로스 플랫폼 응용 프로그램을 개발하는 것이 어려운 작업이다. 응용 프로그램 수준에서 운영체제와 함께 GUI 인터페이스와 같은 기능을 제공하는 라이브러리가 포함되어 있으며, 하나의 API 집합 를 호출하도록 설계된 응용 프로그램은 해당 API를 제공하지 않는 운영체제에서는 작동하지 않는다.

  • 각 운영체제에는 헤더, 명령어 및 변수의 배치를 강제하는 응용 프로그램 이진 형식이 있다. 이러한 구성요소는 명시된 구조 형태로 실행 파일 내의 특정 위치에 있어야 운영체제가 파일을 열고 응용 프로그램을 적재하여 올바르게 실행할 수 있다.
  • CPU는 다양한 명령어 집합을 가지며 해당 명령어가 포함된 응용 프로그램만 올바르게 실행할 수 있다.
  • 운영체제는 응용 프로그램이 파일 생성과 네트워크 연결 열기와 같은 다양한 활동을 요청할 수 있는 시스템 콜을 제공한다. 이러한 시스템 콜은 사용되는 피연산자, 피연산자 순서, 응용 프로그램이 시스템 콜을 호출하는 방법, 시스템 콜 번호, 의미, 및 반환 결과를 포함하여 여러 측면에서 운영체제마다 다르다.

이러한 구조적 차이점을 완전히 해결하지는 못했지만 해결하는 데 도움이 되는 몇가지 방법이 있다. 예를 들어, Linux와 거의 모든 UNIX 시스템에서 이진 실행 파일은 ELF 형식을 채택하였다. ELF는 Linux 및 UNIX 시스템에서 공통 표준을 제공하지만 ELF 형식은 특정 컴퓨터 아키텍쳐와 연관되어 있지 않으므로 실행 파일이 다른 하드웨어 플랫폼에서 실행될 것이라는 보장은 없다.

이진 코드의 여러 구성요소가 주어진 아키텍처에서 특정 운영체제와 상호작용 할 수 있는 방법을 정의하는데 ABI(application binary interface)가 사용된다. ABI는 주소 길이, 시스템 콜에 매개변수를 전달하는 방법, 런타임 스택 구성, 시스템 라이브러리의 이진 형식 및 데이터 유형의 크기 등의 하위 수준의 세부 정보를 명시한다. 일반적으로 ABI는 특정 아키텍처에 대해 명시된다. 따라서 ABI는 아키텍처 수준의 API이다. 이진 실행 파일이 특정 ABI에 따라 컴파일되고 링크된 경우 해당 ABI를 지원하는 다른 시스템에서 실행될 수 있어야 한다. 그러나 특정 아키텍처에서 실행되는 특정 운영체제에 대해 ABI가 정의되어 있기 때문에 ABI는 플랫폼 간 호환성을 거의 제공하지 않는다.

요약하면, 이러한 모든 차이점은 특정 CPU 유형의 특정 운영체제에서 인터프리터, RTE 또는 이진 실행 파일을 작성하고 컴파일하지 않으면 응용 프로그램이 실행되지 않는다는 것을 의미한다.

운영체제의 설계 및 구현

설계 목표

시스템 설계는 최상위 수준에서는 하드웨어와 시스템 유형(일괄 처리, 시분할, 단일 사용자, 다중 사용자, 분산, 실시간 혹은, 범용)의 선택에 의해 영향을 받을 것이다. 이 최상위 설계 수준을 넘어서면 요구 조건을 일일이 명시하는 것이 훨씬 어려워 진다. 이러한 요구 조건은 근본적으로 사용자 목적과 시스템 목적의 두가지 기본 그룹으로 나눌 수 있다.

시스템은 사용하기 쉽고 편리하며, 배우기 쉽고, 믿을 수 있고, 안전하고, 신속해야 한다. 물론 이러한 특징을 구현하는 방법에 대해 일반저긍로 합의된 사항이 없기 때문에 시스템을 설계할 때 이러한 명세가 쓸모 있지는 않다.

운영체제에 대한 요구를 정의하는 문제를 해결하는 유일한 방법은 없다. 많은 시스템에서 다른 요구 조건들이 다른 환경을 위해 다양한 해결 방법을 낳을 수 있음을 보여준다.

기법과 정책

한 가지 중요한 원칙은 기법으로부터 정책을 분리하는 것이다. 기법은 어떤 일을 어떻게 할 것인가를 결정하는 것이고, 정책은 무엇을 할 것인가를 결정하는 것이다. 예를 들어 타이머 구조는 CPU를 보호하기 위한 기법이지만, 특정 사용자를 위해 타이머를 얼마나 오랫동안 설정할지를 결정하는 것은 정책적 결정이다.

정책과 기법의 분리는 융통성을 위해 아주 중요하다. 정책은 장소가 바뀌거나 시간의 흐름에 따라 변경될 수 있다. 여러 정책에서 사용되기에 충분한 융통성이 있는 일반적인 기법이 더 바람직하다.

정책 결정은 모든 자원 할당 문제에 있어 중요하다. 자원의 할당 여부를 결정할 필요가 있을 때마다 정책 결정을 해야 한다. 질문이 무엇이 아니라 어떻게일때마다, 반드시 결정되어야 하는 것은 기법이다.

구현

운영체제의 설계가 완료되면 구현되어야 한다. 운영체제는 많은 사람에 의해 오랫동안 개발된 많은 프로그램의 집합체이기 때문에 구현 방법에 대한 일반적인 언급을 하는 것은 어렵다.

초기 운영체제는 어셈블리 언어로 작성되었다. 이제 대부분은 C 또는 C++와 같은 고급 언어로 작성되며, 극히 일부의 시스템이 어셈블리 언어로 작성도니다. 실제로, 둘 이상의 고급 언어가 종종 사용된다. 커널의 최하위 레벨은 어셈블리 언어 및 C로 작성될 수 있다. 상위 레벨 루틴은 C 및 C++로 작성될 수 있으며 시스템 라이브러리는 C++ 또는 상위 레벨 언어로 작성될 수 있다.

운영체제를 구현하기 위해 고급언어나 최소한 시스템 구현 언어를 사용함으로써 생기는 장점은 그 언어가 응용 프로그램을 위해 사용될 때 생기는 장점과 마찬가지이다. 즉, 코드를 빨리 작성할 수 있으며, 더욱 간결하고, 이해하기 쉽고, 디버그하기도 쉽다. 추가로, 컴파일러 기술의 향상은 단순한 재 컴파일에 의해 전체 운영체제를 위해 생성된 코드를 향상시킬 것이다. 마지막으로, 운영체제가 고급 언어로 작성된 경우 다른 하드웨어로 이식하는 것이 훨씬 쉽다.

운영체제를 고급 언어로 구현하는 것에 대해 주로 주장되는 단점은 속도가 느리고 저장 장치가 많이 소요되는 것이다. 그러나 이것은 현재의 시스템에서는 주된 문제가 아니다. 점누적인 어셈블리어 프로그래머는 효율적인 작은 루틴을 생산할 수 있지만 현대의 컴파일러는 대규모 프로그램을 위해 복잡한 분석을 수행하고 정교한 최적화를 적용하여 우수한 코드를 생산 할 수 있다.

다른 시스템에서도 알려진 사실이지만, 운영체제의 주요 성능 향상은 우수한 어셈블리어 코드보다는 좋은 자료구조와 알고리즘의 결과일 가능성이 크다. 게다가 운영체제가 크긴 하지만, 단지 소량의 코드만이 고성능이 중요하다. 아마도 인터럽트 핸들러, 입출력 관리자, 메모리 관리자와 CPU 스케줄러가 가장 긴급한 루틴일 것이다.

운영체제 구조

현대의 운영체제와 같이 크고 복잡한 시스템은 적절하게 동작하고 쉽게 변경될 수 있으려면 신중히 제작되어야 한다. 일반적인 접근 방법은 한 개의 일관된 시스템보다는 태스크를 작은 구성요소로 분할하는 것이다. 이들 모듈 각각은 신중히 정의된 인터페이스와 기능들을 가진, 시스템의 잘 정의된 부분이어야 한다. 이 절에서는 운영체제를 구성하는 구성요소들이 어떤 방법으로 상호 연결되고 하나의 커널로 결합되는지 알아본다.

모놀리식 구조

운영체제를 구성하는 가장 간단한 구조는 구조가 아예 없는 것이다. 즉, 커널의 모든 기능을 단일 주소 공간에서 실행되는 단일 정적 이진 파일에 넣는 것이다. 제한적인 구조를 가진 운영체제의 예는 최초의 UNIX 운영체제로 커널과 시스템 프로그램의 두 부분으로 구성된다. 커널은 여러 가지 인터페이스와 장치 드라이버로 다시 분리되는데, 이들은 UNIX가 발전해오면서 여러 해 동안 추가되고 확장된 것이다.

전통적인 운영체제는 위 그림과 같이 계층들로 이루어졌다고 볼 수 있다. 시스템 콜 인터페이스 아래와 물리적 하드웨어 위의 모든 것이 커널이다. 커널은 시스템 콜을 통해 파일 시스템, CPU 스케줄링, 메모리 관리 그리고 다른 운영체제 기능을 제공한다. 요약하면, 그것은 하나의 주소 공간으로 결합하기에는 엄청나게 많은 기능이다.

Linux 운영체제는 UNIX에 기반을 두고 있다.

응용 프로그램은 일반적으로 커널에 대한 시스템 콜 인터페이스와 통신할 때 glibc 표준 C 라이브러리를 사용한다. Linux 커널은 단일 주소 공간에서 커널 모드로 전부 실행된다는 점에서 모놀리식이지만, 런타임 중에 커널을 수정할 수 있는 모듈식 설계를 갖추고 있다.

모놀리식 커널의 명백한 단순성에도 불구하고 이 구조는 구현 및 확장하기 어렵다. 그러나 모놀리식 커널은 성능 면에서 뚜렷한 이점이 있다. 시스템 콜 인터페이스에는 오버헤드가 거의 없고 커널 안에서 통신 속도가 빠르다. 따라서 모놀리식 커널의 단점에도 불구하고, 속도의 효율성은 이 구조의 증거를 여전히 UNIX, Linux 및 Windows 운영체제에서 발견할 수 있는 이유를 설명한다.

계층적 접근

시스템의 한 부분을 변경하면 다른 부분에 광범위한 영향을 줄 수 있으므로 모놀리식 접근은 종종 밀접하게 결합된 시스템으로 불린다. 대안으로 느슨하게 결합된 시스템을 설계할 수 있다. 이러한 시스템은 기능이 특정 기능 및 한정된 기능을 가진 개별적이며 작은 구성요소로 나뉜다. 이 모든 구성요소가 합쳐져서 커널을 구성한다. 이 모듈 방식의 장점은 한 구성요소의 변경이 해당 구성요소에만 영향을 미치고 다른 구성요서에는 영향을 미치지 않으므로 구현자가 시스템의 내부 작동을 더 자유롭게 생성하고 변경할 수 있다는 것이다.

시스템은 다양한 방식으로 모듈화 될 수 있다. 한 가지 계층적 접근 방식인데 이 방식에서는 운영체제가 여러 개의 층으로 나누어진다. 최하위 층은 하드웨어고 최상위 층은 사용자 인터페이스이다.

운영체제의 층은 데이터와 이를 조작하는 연산으로 구성된 추상된 객체의 구현이다. 전형적인 운영체제 층(편의상 M층이라고 하자)은 자료구조와 상위층에서 호출할 수 있는 루틴의 집합으로 구성된다. 층 M은 다시 하위층에 대한 연산을 호출할 수 있다.

계층적 접근 방식의 주된 장점은 구현과 디버깅의 간단함에 있다. 층들은 단지 자신의 하위층들의 서비스와 기능(연산)들만을 사용하도록 선택된다. 이러한 접근 방법은 시스템의 검증과 디버깅 작업을 단순화한다. 첫 번째 층은 정의엥 의해 하드웨어만을 사용하여 이 층의 기능을 구현하기 때문에, 나머지 시스템은 아무런 신경을 쓰지 않고 디버깅할 수 있다. 첫 번째 층의 디버깅이 끝나면, 두 번째 층을 디버깅하는 동안 그것이 정확하게 동작한다고 가정될 수 있으며, 이러한 과정이 반복된다. 만일 어느 층의 디버깅 중 오류가 발견되면 그 하위의 층은 이미 디버깅되었기 때문에 오류는 반드시 그 층에 있다. 따라서 시스템을 계층으로 나누면 시스템의 설계나 구현이 간단해진다.

각 층은 자신보다 하위 수준의 층에 의해 제공된 연산들만 사용해 구현한다. 한 층은 이러한 연산들이 어떻게 구현되는지 알 핖요가 없고, 다만 이러한 연산들이 무엇을 하는지만 알면 된다. 그러므로 각 층은 특정 데이터 구조, 연산, 그리고 하드웨어의 존재를 상위층에 대해 숨기게 된다.

계층화된 시스템은 컴퓨터 네트워크(예: TCP/IP) 및 웹 응용 프로그램에서 성공적으로 사용됐다. 그럼에도 불구하고 순수한 계층 접근 방식을 사용하는 운영체제는 비교적 적다. 한 가지 이유는 각 계층의 기능을 적절히 정의해야 하는 문제와 관련이 있다. 또한 이러한 시스템의 전반적인 성능은 운영체제 서비스를 얻기 위해 사용자 프로그램이 여러 계층을 통과해야 하는 오버헤드로 인해 열악하다. 그러나 어느 정도의 계층화는 현대 운영체제에서 공통적이다. 일반적으로 이러한 시스템은 더 많은 기능을 가진 더 적은 개수의 층을 가지므로 층 기능의 정의 및 상호작용의 문제를 피하면서 모듈화된 코드의 장점을 최대한 활용할 수 있다.

마이크로 커널

우리는 이미 UNIX가 모놀리식 구조를 가진다는 것을 보았다. UNIX가 확장함에 따라, 커널이 커지고 관리하기 힘들어졌다. 1980년대 중반에, Carnegie-Mellon 대학교 연구자들이 마이크로 커널 접근 방식을 사용하여 커널을 모듈화한 Mach라 불리는 운영체제를 개발하였다. 이 방법은 모든 중요치 않은 구성요소를 커널로부터 제거하고, 그들을 별도의 주소 공간에 존재하는 사용자 수준 프로그램으로 구현하여 운영체제를 구성하는 방법이다. 결과는 더 작은 커널이다. 어느 서비스가 커널에 남아 있어야 하고, 어느 서비스가 사용자 공간에 구현되어야 할지에 대해서는 의견이 일치하지 않는다.

마이크로커널의 주 기능은 클라이언트 프로그램과 역시 사용자 공간에서 수행되는 다양한 서비그 간의 통신을 제공하는 것이다.

예를 들어, 만일 클라이언트 프로그램이 파일에 접근하기를 원한다면, 파일 서버와 반드시 상호작용을 해야한다. 클라이언트 프로그램과 서비스는 결코 직접 상호작용하지 않는다. 오히려, 그들은 마이크로커널과 메시지를 교환함으로써 간접적으로 상호 작용한다.

마이크로 커널 접근법의 한 가지 장점은 운영체제의 확장이 쉽다는 것이다. 모든 새로운 서비스는 사용자 공간에 추가되며, 따라서 커널을 변경할 필요가 없다. 커널이 변경되어야만 할 때는 마이크로 커널이 작은 커널이기 때문에 변경할 대상이 비교적 적은 경향이 있다. 결과적으로 만들어지는 운영체제는 한 하드웨어로부터 다른 하드웨어로 이식이 쉽다. 마이크로커널은 서비스 대부분이 커널이 아니라 사용자 프로세스로 수행되기 때문에 또한 더욱 높은 보안성과 신뢰성을 제공한다. 만일 한 서비스가 잘못되더라도, 운영체제의 다른 부분은 아무런 영향을 받지 않는다.

마이크로 커널 운영체제의 가장 잘 알려진 실례는 macOS 및 iOS 운영체제의 커널 구성요소인 Darwin이다. 실제로 Darwin은 두 개의 커널로 구성되며 그 중 하나는 Mach 마이크로커널이다.

안타깝게도 마이크크로 커널은 가중된 시스템 기능 오버헤드 때문에 성능이 나빠진다. 두 개의 사용자 수준 서비스가 통신해야 하는 경우 별도의 주소 공간에 서비스가 존재하기 때문에 메시지가 복사되어야 한다. 또한 운영체제는 메시지를 교환하기 위해 한 프로세스에서 다음 프로세스로 전환해야 할 수도 있다. 메시지 복사 및 프로세스 전환과 관련된 오버헤드는 마이크로커널 기반 운영체제의 성장에 가장 큰 장애였다.

모듈

운영체제를 설계하는 데 이용되는 최근 기술 중 최선책은 아마도 적재가능 커널 모듈(loadable kernel modules, LKM) 기법의 사용일 것이다. 이 접근법에서는 커널은 핵심적인 구성요소의 집합을 가지고 있고 부팅 때 또는 실행 중에 부가적인 서비스들을 모듈을 통하여 링크할 수 있다. 이러한 유형의 설계는 Linux, Mac OS X, Solaris 및 Windows 등의 현대 UNIX를 구현하는 일반적인 추세이다.

설계의 주안점은 커널은 핵심 서비스를 제공하고 다른 서비스들은 커널이 실행되는 동안 동적으로 구현하는 것이다. 서비스를 동적으로 링크하는 것은 새로운 기능을 직접 커널에 추가하는 것보다 바람직하다. 후자의 경우 수정 사항이 생길 때마다 커널을 직접 다시 컴파일해야 한다. 예를 들어 CPU 스케줄링과 메모리 관리 알고리즘은 커널에 직접 구현하고 다양한 파일 시스템을 지원하는 것은 적재가능 모듈을 통하여 구현할 수 있다.

전체적인 결과는 커널이 각 부분이 정의되고 보호된 인터페이스를 가진다는 점에서 계층 구조를 닮았다. 그러나 모듈에서 임의의 다른 모듈을 호출할 수 있다는 점에서 계층 구조보다 유연하다. 중심 모듈은 단지 핵심 기능만을 가지고 있고 다른 모듈의 적재 방법과 모듈들과 어떻게 통신하는지 안다는 점에서 마이크로 커널과 유사하다. 그러나 통신하기 위하여 메시지를 전달을 호출할 필요가 없기 때문에 더 효율적이다.

Linux는 주로 장치 드라이버와 파일 시스템을 지원하기 위해 적재가능 커널 모듈을 사용한다. LKM은 시스템이 시작되거나 USB 장치가 실행 중인 시스템에 접속되는 경우와 같이 런타임 중에 커널에 "삽입"될 수 있다. Linux 커널에 필요한 드라이버가 없으면 동적으로 적재할 수 있다. LKM은 런타임 중에 커널에서 제거될 수도 있다. Linux의 경우 LKM은 동적 및 모듈식 커널을 허용하면서 모놀리식 시스템의 성능 이점을 유지한다.

하이브리드 시스템

사실 엄격하게 정의된 하나의 구조를 채택한 운영체제는 거의 존재하지 않는다. 대신 다양한 구조를 결합하여 성능, 보안 및 편리성 문제를 해결하려는 혼용 구조로 구성된다. 예를 들어, Linux는 운영체제 전부가 하나의 주소 공간에 존재하여 효율적인 성능을 제공하기 때문에 모놀리식 구조이다. 그러나 이 운영체제들은 모듈을 사용하기 때문에 새로운 기능을 동적으로 커널에 추가할 수 있다.

Apple의 macOS 운영체제는 주로 데스크톱 및 랩톱 컴퓨터 시스템에서 실행되도록 설계되었으며 iOS는 iPhone 스마트폰 및 iPad 태블릿 컴퓨터용으로 설계된 모바일 운영체제이다. 구조적으로 macOS와 iOS는 공통점이 많으므로 공유하는 내용과 서로 다른 점을 강조하여 함께 제시한다.

  • 사용자 경험 층: 이 층은 사용자가 컴퓨팅 장치와 상호 작용할 수 있는 소프트웨어 인터페이스를 정의한다. macOS는 마우스 또는 트랙패드 용으로 설계된 Aqua 사용자 인터페이스를 사용하는 반면, iOS는 터치 장치용으로 설계된 Springboard 사용자 인터페이스를 사용한다.
  • 응용 프로그램 프레임워크 층: 이 층에는 Cocoa 및 Cocoa Touch 프레임워크가 포함되며 Objective-C 및 Swift 프로그래밍 언어에 대한 API를 제공한다. Cocoa와 Cocoa Touch의 주요 차이점은 전자는 macOS 응용프로그램 개발에 사용되고 후자는 iOS가 터치 스크린과 같은 모바일 장치에 고유한 하드웨어 기능을 지원하는데 사용한다는 것이다.
  • 코어 프레임워크: 이 층은 Quicktime 및 OpenGL을 포함한 그래픽 및 미디어를 지원하는 프레임워크를 정의한다.
  • 커널 환경. Darwin이라고도 불리는 이 환경에는 Mach 마이크로커널과 BSD UNIX 커널이 포함된다.

응용 프로그램은 사용자 경험 기능을 이용하거나 이 기능을 우회하여 응용 프로그램 프레임워크 또는 핵심 프레임워크와 직접 상호작용하도록 설계될 수 있다. 또한 응용 프로그램은 프레임워크를 완전히 버리고 커널 환경과 직접 통신할 수 있다.( 이 후자의 경우의 예는 사용자 인터페이스 없이 POSIX 시스템 콜을 수행하도록 작성된 C 프로그램이다.)

macOS와 iOS의 중요한 차이점은 다음과 같다.

  • macOS는 데스크톰 및 랩톱 컴퓨터 시스템이므로 Intel 아키텍처에서 실행되도록 컴파일된다. iOS는 모바일 장치용으로 설계되었으므로 ARM 기반 아키텍처용으로 컴파일된다. 마찬가지로 iOS 커널은 전원 관리 및 공격적인 메모리 관리와 같은 모바일 시스템의 특정 기능과 요구를 해결하기 위해 약간 수정되었다. 또한 iOS에는 macOS보다 더 엄격한 보안 설정이 있다.
  • iOS 운영체제는 일반적으로 macOS보다 개발자에게 훨씬 더 제한적이며 더 폐쇄적일수도 있다. 예를 들어 iOS는 iOS에서 POSIX 및 BSD API에 대한 엑세스를 제거하는 반면 macOS에서 개발자는 공개적으로 사용할 수 있다.

Darwin은 주로 Mach 마이크로커널과 BSD UNIX 커널로 구성된 계층화된 시스템이다. Darwin의 구조는 아래와 같다.

운영체제 대부분은 UNIX 및 Linux 시스템에서 표준 C 라이브러리를 통하는 것처럼 커널에 대해 하나의 시스템 콜 인터페이스를 제공하는 반면 Darwin은 Mach 시스템 콜(트랩이라고 함)과 BSD 시스템 콜(POSIX 기능 제공)두 개의 시스템 콜 인터페이스를 제공한다. 이러한 시스템 콜에 대한 인터페이스는 표준 C 라이브러리뿐만 아니라 네트워킹, 보안 및 프로그래밍 언어 지원을 제공하는 라이브러리를 포함하는 풍부한 라이브러리 집합이다.

시스템 콜 인터페이스 아래에서 Mach는 메모리 관리, CPU 스케줄링 및 메시지 전달 및 원격 프로시저 호출(remote procedure call, RPC)과 같은 프로세스간 통신(IPC) 기능을 포함한 기본 운영체제 서비스를 제공한다. Mach에서 제공하는 대부분의 기능은 커널 추상화를 통해서 사용 가능하다. 이 추상화에는 태스크, 스레드, 메모리 객체 및 포트가 포함된다. 예를 들어, 응용 프로그램은 BSD POSIX fork() 시스템 콜을 사용하여 새 프로세스를 생성할 수 있다. Mach는 커널 내에서 프로세스를 나타내기 위하여 태스크 커널 추상화를 사용한다.

Mach 및 BSD 외에도 커널 환경은 장치 드라이버 및 동적 적재가능 모듈(macOS는 커널 확장 또는 kext라고 함) 개발을 위한 I/O 키트를 사용한다.

운영체제 빌딩과 부팅

하나의 특정 기기 구성에 맞게 운영체제를 설계, 코딩할 수 있다. 그러나 보다 일반적으로 운영체제는 다양한 주변장치 구성을 가진 모든 종류의 컴퓨터에서 실행되도록 설계된다.

Linux를 위한 Windows 서브 시스템

Windows는 다른 운영체제 환경을 에뮬레이트 하기 위해 서브시스템을 제공하는 하이브리드 구조를 사용한다. 이러한 사용자 모드 서브시스템은 실제 서비스를 제공하기 위해 Windows 커널과 통신한다. Winodws 10에는 Linux 용 Windows 서브시스템(WSL)이 추가되어 네이티브 Linux 응용 프로그램(ELF 바이너리 형식)을 Windows 10에서 실행할 수 있다. 통상 사용자가 Windows 응용 프로그램 bash.exe를 시작하고 Linux를 실행하는 bash 셸을 사용자에게 제시한다. 내부적으로 WSL은 init 프로세스로 구성된 Linux 인스턴스를 생성하고 네이티브 Linux 응용 프로그램 /bin/bash를 실행하는 bash 셸 프로세스를 생성한다. 이러한 각 프로세스는 Winodows Pico 프로세스에서 실행된다. 이 특별한 프로세스는 네이티브 Linux 바이너리를 프로세스의 자체 주소 공간에 적재하여 Linux 응용 프로그램을 실행할 수 있는 환경을 제공한다.

Pico 프로세스는 커널 서비스 LXCore 및 LXSS와 통신하여 가능한 한 Linux 시스템 콜을 네이티브 Windows 시스템 콜로 변환한다. Linux 응용 프로그램이 상응하는 Winodws 시스템과 콜로 변환한다. Linux 응용 프로그램이 상응하는 Windows 시스템 콜이 없는 요청을 한다면 LXSS 서비스는 동등한 기능을 제공해야 한다. Linux와 Windows 시스템 콜 간에 일대일 관계가 있는 경우 LXSS는 Linux 시스템 콜을 Windows 커널의 동등한 시스템 콜로 직접 전달한다. 상황에 따라 Linux와 Windows의 시스템 콜은 비슷하지만 동일하지는 않다. 이 경우 LXSS는 일부 기능을 제공하고 나머지 기능을 제공하기 위해 유사한 Windows의 시스템 콜을 호출한다. Linux의 fork()는 이러한 사례를 보여준다. Windows의 createProcess() 시스템 콜은 fork()와 유사하지만 완전히 똑같은 기능을 제공하지는 않는다. WSL에 fork()가 호출되면 LXSS 서비스는 fork()의 초기 작업 중 일부를 수행한 다음 CreateProcess()를 호출하여 나머지 작업을 수행한다.

운영체제 생성

운영체제를 처음부터 생성(또는 빌딩)하는 경우 다음 절차를 밟아야 한다.

  1. 운영체제 소스 코드를 작성한다(또는 이전에 작성된 소스코도를 확보한다).
  2. 운영체제가 실행될 시스템의 운영체제를 구성한다.
  3. 운영체제를 컴파일한다.
  4. 운영체제를 설치한다.
  5. 컴퓨터와 새 운영체제를 부팅한다.

시스템 부트

운영체제가 생성된 후는 하드웨어에서 새 운영체제를 사용할 수 있어야 한다. 그러나 하드웨어는 커널의 위치 또는 커널을 적재하는 방법을 어떻게 알 수 있을까? 커널을 적재하여 컴퓨터를 시작하는 과정을 시스템 부팅이라고 한다.

부팅 과정은 다음과 같다.

  1. 부트 스트랩 프로그램 또는 부트 로더라고 불리는 작은 코드가 커널의 위치를 찾는다.
  2. 커널이 메모리에 적재되고 시작된다.
  3. 커널은 하드웨어를 초기화한다.
  4. 루트 파일 시스템이 마운트된다.

일부 컴퓨터 시스템은 다단계 부팅 과정을 사용한다. 컴퓨터 전원을 처음 켜면 BIOS라고 하는 비휘발성 펌웨어에 있는 소형 부트 로더가 실행된다. 이 초기 부트 로더는 일반적으로 부트 블록이라고 하는 디스크의 정해진 위치에 있는 두 번째 부트 로더를 적재하는 작업만 한다. 부트 블록에 저장된 프로그램은 전체 운영체제를 메모리에 적재하고 실행을 시작하기에 충분히 정교할 수 있다. 더 일반적으로, 이 부트 로더는 간단한 코드로서(하나의 디스크 블록에 저장되어야 하기 때문에) 디스크의 주소와 부트스트랩 프로그램 나머지의 길이만 알고 있다.

Windows, Linux 및 macOS와 iOS 및 Android를 비롯한 대부분의 운영체제의 부트 로더는 하드웨어 문제 진단, 손상된 파일 복구 및 운영체제 재설치 등의 작업을 수행할 수 있는 복구 모드 또는 단일 사용자 모드로 부팅할 수 있는 기능을 제공한다.

운영체제 디버깅

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

장애 분석(Failure Analysis)

만일 프로세스가 실패한다면, 운영체제 대부분은 시스템 관리자 또는 문제를 발생시킨 사용자에게 문제가 발생했다는 것을 경고하기 위해 오류 정보를 로그 파일에 기록한다. 운영체제는 또한 프로세스가 사용하던 메모리를 캡처한 코어 덤프(core dump)를 취하고 차후 분석을 위해 파일로 저장한다.(초창기 시절에 메모리를 코어라고 칭했다.) 실행 중인 프로그램과 코어 덤프는 디버거에 의해 검사될 수 있으며, 이는 장애 발생시 프로그래머가 프로세스의 코드와 메모리를 분석할 수 있도록 한다.

사용자 수준 프로세스 코드를 디버깅하는 것은 도전적인 일이다. 커널의 크기와 복잡도, 하드웨어 제어 및 사용자 수준 디버깅 도구가 없기 때문에 운영체제 커널을 디버깅하는 것은 훨씬 복잡하다. 커널 장애는 크래시(crash)라고 불린다. 프로세스 장애와 마찬가지로 오류 정보가 로그 파일에 저장되고 메모리의 상태가 크래시 덤프(crash dump)에 저장된다.

운영체제 디버깅과 프로세스 디버깅은 종종 두 태스크의 근본적인 차이에 의해 서로 다른 도구와 기법을 사용한다. 파일 시스템 코드 때문에 발생한 커널 장애는 재부팅 전에 커널의 상태를 파일 시스템에 저장하려는 시도를 위험하게 한다.