운영체제는 그 속에서 프로그램이 실행되는 환경을 제공해 줍니다.
운영체제의 역할에 대해서 알기 쉽게 접근하기 위해서 바라볼 수 있는 관점이 있습니다.
먼저, 첫 번째 관점은 운영체제가 제공하는 서비스에 초점을 맞추는 것
입니다.
이전에 사용자들이 프로그램을 사용하는 것은 문제를 해결하기 위함이라고 했고, 운영체제는 사용자들이 더 쉽고, 편리하게 프로그램을 사용할 수 있도록 도와준다고 했습니다.
운영체제는 이러한 목적을 수행하기 위해서 특정한 기능들을 가지고 사용자들에게 제공합니다. 이러한 기능들을 서비스라고 합니다.
두 번째 관점은 운영체제가 사용자와 프로그래머에게 제공하는 인터페이스에 초점을 맞추는 것
입니다. 이러한 기능들을 제공하기 위해서 함수들이 구현되어 있어야 할 것입니다. 자세한 동작은 캡슐화가 되어있어서 내부적으로 숨겨져 있고, 그것의 최상위 추상화 계층인 인터페이스를 호출하여 특정 동작을 수행하여 기능들을 제공할 것입니다.
세 번째 관점은 시스템의 구성요소와 그들의 상호 연결에 초점을 맞추는 것
입니다. 컴퓨터에서 프로그램은 여러 프로그램이 돌아가고 이러한 멀티 태스킹 환경에서 운영체제는 적절하게 자원(리소스)를 분배해주고, 관리해야 합니다. 하드웨어 자원들을 적절하게 분배해주면서, 격리해주고 잘 동작하기 위해서 운영체제는 중간 계층에서 자원들을 관리하면서 사용자와 하드웨어 자원들을 연결시켜주는 작업을 수행합니다.
운영체제가 하는 일에 대해서 살펴봅시다.
그래픽 사용자 인터페이스(GUI)
혹은 터치 스크린 인터페이스
, 명령어 라인 인터페이스(CLI)
를 사용자들에게 제공하여 명령을 입력할 수 있도록 도와줍니다.rwx
와 같은 동작들을 수행할 수 있도록 해줍니다.이러한 동작들은 사용자에게 도움을 주는 것이 주 목적입니다.
운영체제는 시스템 자체의 효율적인 보장을 보장하기 위해서 제공해주는 기능들도 존재합니다.
앞서 사용자가 운영체제와 접촉하는 방식에 3가지 방식을 언급했습니다. 이에 대해서 살펴볼 것입니다.
운영체제 대부분은 명령 인터프리터(Command Interpreter, CLI)를 프로세스가 시작되거나 사용자가(대화형 시스템상에서) 처음 로그온 할 때 수행되는 특수한 프로그램으로 취급합니다.
선택할 수 있는 여러 명령 인터프리터를 제공하는 시스템에서 이 해석기는 셸(shell)이라고 불립니다.
명령 인터프리터의 중요한 기능은 사용자가 지정한 명령을 가져와서
그것을 수행
하는 것입니다. 이때, 두가지 방식으로 수행할 수 있습니다.
여러 운영체제 중 UNIX에 의해 사용되는 방식은 2번입니다.
예를 들어서 설명해보자면,
rm file.txt
이 명령어는 사실 어떻게 동작하나면,
즉, rm 명령과 관련된 로직은 rm이라는 파일 내의 코드로 완전하게 정의되어 있는 것입니다. 사용자는 그것을 이용하기만 하면 되는 것입니다.
사용자 친화적인 그래픽 기반 사용자 인터페이스(Graphical User Interface, GUI) 방식은 CLI 명령어을 사용하는 것이 아니라 데스크톱이라고 특정지어지는 마우스를 기반으로 하는 윈도 메뉴 시스템을 사용합니다.
대부분의 모바일 시스템에는 터치스크린 인터페이스를 사용하여 상호 작용을 합니다.
무엇을 선택하는지는 개인의 선호도에 달려 있습니다.
예를 들어, 일반 컴퓨터 사용자의 경우 CLI의 사용은 불편하게 느껴질 것입니다. 이러한 경우, GUI 혹은 터치스크린 인터페이스가 적합할 것입니다.
또한, CLI는 다른 인터페이스보다 더 많은 기능들을 제공할 수 있는데 시스템 기능들이 필요한 사용자들의 경우 CLI를 사용하면 될 것입니다.
CLI는 반복적으로 해야 하는 작업은 셸 스크립트(shell scripts)를 이용하여 프로그래밍해 놓을 수 있습니다.
정리하자면, 인터페이스의 선택은 어떤 장치에 사용되는지, 사용자가 필요로 하는지에 따라 선택하면 될 것입니다.
시스템 콜(System Call)은 운영체제에 의해 사용 가능하게 된 서비스에 대한 인터페이스를 제공합니다.
쉽게 정리하자면, 사용자(프로세스)에게 운영체제 수준의 기능을 제공해준다고 생각하면 됩니다.
보통 하나의 CLI 명령에는 여러 시스템콜이 호출되서 동작하는데 그것을 잠깐 살펴보고 갑시다.
예를 들어서,
cp in.txt out.txt
이 CLI 명령을 실행한다고 생각해봅시다.
우선 프로그램은 반드시 입력 파일을 오픈하고 출력 파일을 생성한 후 오픈합니다.
각각 이러한 연산은 또 다른 시스템 콜을 필요로 하며, 각 시스템 콜에서 오류가 발생하면 처리되어야 합니다.
해당 동작들이 성공적으로 진행되어 두 개의 파일이 준비되었다면, 입력 파일로부터 읽어서(하나의 시스템 콜), 출력 파일에 기록(또 다른 시스템 콜)하는 루프에 들어가게 됩니다. 이 과정에서도 에러가 나올 수 있으므로 그에 대한 시스템 콜이 준비되어 있어야 합니다.
전체 파일이 복사된 후, 프로그램은 두 개의 파일을 닫고(2개의 시스템 콜), 콘솔 또는 윈도에 메시지를 기록하고(추가의 시스템 콜들), 결국 정상으로 종료(마지막 시스템 콜)하게 됩니다.
위에서 봤듯이, 간단한 프로그램이라도 운영체제의 기능(시스템 콜)을 아주 많이 사용하게 됩니다.
하지만, 응용 개발자들은 이러한 시스템콜들을 직접 사용하는 것일까요? 당연히 아닙니다. 응용 개발자들은 응용 프로그래밍 인터페이스(Application programming interface, API)에 따라 프로그램을 설계합니다.
API는 각 함수에 전달되어야 할 매개변수들
과 프로그래머가 기대할 수 있는 반환 값
을 포함하여 응용 프로그래머가 사용 가능한 함수의 집합을 명시합니다.
프로그래머는 운영체제가 제공하는 코드의 라이브러리를 통하여 API를 활용합니다.
응용 프로그래머가 시스템 콜보다 API를 활용하는 이유를 정리하자면 다음과 같습니다.
추상화
의 이점을 가져가게 된다는 것입니다.⚡️ 실행시간 환경(RTE) ⚡️
컴파일러 또는 인터프리터를 포함하여 특정 프로그래밍 언어로 작성된 응용 프로그램을 실행하는데 필요한 전체 소프트웨어 제품군과 라이브러리 또는 로더와 같은 다른 소프트웨어를 함께 가르킨다.
시스템 콜은 필요에 따라 매개변수
를 필요로 할 수 있는데, 운영체제에 매개변수를 전달하기 위해서 세 가지 일반적인 방법을 제공합니다.
시스템 콜은 다섯 가지의 중요한 범주, 즉 프로세스 제어, 파일 조작, 장치 조작, 정보 유지와 통신, 보안 등으로 묶을 수 있습니다.
핵심적인 기능들이 많기 때문에 여기서는 간단하게 정리를 해보도록 하겠습니다.
프로세스를 제어하기 위해서 제공되는 기능들은 굉장히 많습니다.
간단하게만 살펴봐도, 프로세스를 실행될 수 있도록 적재하는 작업, 그것을 실행할 수 있는 작업이 요구될 것입니다.
또 다른 프로세스의 생성, 종료 작업이 필요로 할 수 있고 여러 작업들이 충분히 실행될 수 있도록 시간을 부여하기도 해야합니다.
천천히 하나씩 정리를 해보도록 하겠습니다.
우선 실행 중인 프로그램은 수행을 정상적으로 또는 비정상적으로 멈출 수 있어야 합니다.
실행 과정에서 문제가 발생해 오류 트랩(trap)을 유발할 경우, 때때로 메모리 덤프가 행해지고 오류 메시지가 생성됩니다. 메모리 덤프는 디버거에 의해 검사될 수 있습니다.
오류 처리를 하는 방식도 여러 가지인데,
사용자의 응답이 요구되는 오류인 경우 대화식 시스템(GUI)를 이용해서 오류를 알리고 지시를 기다릴 것입니다.
일부 시스템에서는 오류가 발생할 경우 특별한 복구 행위를 지시하는 제어 카드를 허용할 것입니다.
오류 수준을 정의할 수 있는데 이를 통해 정상 종료, 비정상 종료를 활용할 수 있습니다.
한 프로그램을 실행하고 있는 프로세스가 다른 프로그램을 적재하고 실행하기를 원할 수 있습니다.
멀티 태스킹을 위한 프로세스 생성이 필요로 할 수 있고, 프로세스 속성들을 결정하고 설정할 수 있는 능력이 필요로 할 수 있습니다.
잘못되었거나 필요하지 않는 프로세스를 종료하기를 원할 수도 있습니다.
여기까지만 봐도 굉장히 많은 기능들이 존재합니다. 여기서부터는 명령어들을 나열하는 것으로 정리하겠습니다. 설명이 필요한 기능들에만 추가적인 설명을 적어놓겠습니다.
둘 이상의 프로세스들은 데이터를 공유할 수 있습니다. 잠글 수 있는 시스템 콜이 필요합니다.
프로세스가 백그라운드에서 실행되고 있을 경우, 해당 프로세스는 키보드로부터 직접 입력을 받을 수 없는데, 이는 셸이 그 자원을 사용하고 있기 때문입니다.
프로세스를 생성하기 위해 fork()
라는 명령어도 존재하는데 이에 관련해서는 3장에서 다루겠습니다.
여기서도 필요한 명령어에만 설명을 적어놓고, 나머지는 명령어들만 나열해놓도록 하겠습니다.
파일 시스템이 파일을 조작하기 위해 디렉토리 구조를 가질 수 있는데, 이렇게 되어 있는 경우 이에 대해서도 연산 집합이 필요할 수 있습니다.
프로세스는 작업을 계속 수행하기 위해 추가 자원이 필요할 수 있습니다. 이러한 추가 자원은 주 기억장치, 디스크 드라이브, 파일에의 접근 등이 될 수 있습니다.
운영체제에 의해 제어되는 다양한 자원들은 장치로 간주될 수 있는데, 물리 장치와 추상적(가상적) 장치로 구분됩니다.
예를 들어서 구분하자면, 디스크 드라이브 / 파일 과 같은 경우입니다.
다수의 사용자가 동시에 사용하는 시스템은 독점적인 장치 사용을 보장받기 위해 그 장치를 요구하는 작업이 필요합니다. 또한, 장치의 사용이 끝나면 반드시 방출해야 합니다.
장치를 요청하고 할당받게 되면, 파일과 마찬가지로 그 장치를 읽고, 쓰고, 위치 변경할 수 있습니다.
사실 입출력 장치와 파일 간에는 유사성이 매우 많기 때문에, UNIX를 포함한 많은 운영체제가 이들 둘을 통합된 파일-장치 구조(file device structure)
로 결합하였습니다.
이와 같은 기능들은 여러 상황에서 쓰이겠지만 특히 디버깅에 유용합니다.
통신 모델에는 두 가지 일반적인 모델이 있습니다.
메시지 전달 모델 에서는 통신하는 두 프로세스가 정보를 교환하기 위하여 서로 메시지를 주고받습니다. 이때, 서로를 식별하기 위해서 호스트 이름과 프로세스 이름이 요구됩니다.
이러한 것들은 connection에서 사용될 매개변수로 전달될 수 있습니다.
수신 프로세스는 통상 통신이 일어날 수 있도록 허가(permission)를 제공해야 합니다.
연결을 받아들일 프로세스들의 대부분은 특수 목적의 디먼(daemon)으로서, 이들은 그러한 목적을 위해 제공된 시스템 프로그램들이다. 그들은 연결을 위해 대기 호출을 수행하고 연결이 이루어질 때 깨어납니다.
wait_for_connection()
read_message()
write_message()
공유 메모리 모델에서는, 프로세스는 다른 프로세스가 소유한 메모리 영역에 대한 접근을 위한 기능들이 필요로 합니다.
정상적인 운영체제들은 서로 다른 프로세스가 각자의 메모리에 접근하는 것을 막으려 하고, 공유 메모리는 그 제한을 제거하는 것이라고 보면 됩니다.
보호는 컴퓨터 시스템이 제공하는 자원에 대한 접근을 제어하기 위한 기법을 지원합니다.
자원에 접근하기 위한 권한을 이용한다고 보면 되는데, 그것을 위한 기능들을 제공합니다.
특정 사용자에 대한 접근 권한도 부여할 수 있습니다.
시스템 서비스는, 시스템 유틸리티(system utility)로도 알려진, 프로그램 개발과 실행을 위해 더 편리한 환경을 제공합니다.
여기서는 다루는 내용이 좀 많습니다. 일단, 링커와 로더의 역할을 알아보기 위해서 아래의 그림을 참고합시다.
위에서부터 하나씩 살펴볼 것입니다.
먼저, 소스 프로그램은 디스크에 이진 실행 파일
로 존재합니다. (예: a.out 또는 prog.exe)
이것을 컴파일러가 메모리에 적재하기 위해서 오브젝트 파일로 컴파일합니다. 이러한 형식을 재배치 가능 오브젝트 파일이라고 합니다.
링커는 이러한 재배치 가능 오브젝트 파일을 (다른 오브젝트 파일 또는 라이브러리와 함께) 하나의 이진 실행 파일로 결합합니다.
로더는 이진 실행 파일을 메모리에 적재하는 데 사용되며, CPU 코어에서 실행할 수 있는 상태가 됩니다. 이 상태까지 도달했으면 메모리의 프로그램 형태로 프로세스로 만들어 진 것입니다.
여기서 짚고 넘어가야할 개념이 보입니다.
재배치는 프로그램 부분에 최종 주소 할당 후, 프로그램 코드와 데이터를 해당 주소와 일치하도록 조정하는 작업입니다.
즉, 함수의 집합으로 구성되어 있던 프로그램 코드에서 데이터를 가져와 사용할 수 있도록 맞춰주는 작업입니다.
해당 작업은 프로그램이 실행될 때 코드가 라이브러리 함수를 호출하고 변수에 접근할 수 있도록 해줍니다.
아직 위에서 언급되지 않은 단어가 보입니다. 동적 링크 라이브러리(dynamically linked library, DLL)
현재까지 설명한 과정에서는 라이브러리가 실행 파일에 결합되어서 이진 실행 파일로 만들어 진다고 했고, 이것이 메모리에 올라간다고 했습니다.
하지만, 여기서 말하는 라이브러리는 프로세스를 실행시키기 위한 필수적인 라이브러리로 그 외에 동작하는 과정에서 필요할 수 있는 라이브러리를 말하는 것이 아닙니다.
따라서, 시스템 대부분에서는 프로그램이 적재될 때 라이브러리를 동적으로 링크할 수 있게 합니다. 이러한 기능의 라이브러리가 DLL인 것입니다.
❗️ 로더의 동작
로더는 어떻게 실행파일을 실행시켜서 메모리에 적재하는 것일까요?
UNIX 시스템(예: ./main)의 명령어 라인에 프로그램 이름을 입력하면 셸은 먼저
fork()
시스템 콜을 사용하여 프로그램을 실행하기 위한 새 프로세스를 생성합니다. 그런 다음 셸은exec()
시스템 콜로 로더를 호출하고exec()
에 실행 파일 이름을 전달합니다.
그런 다음 로더는 새로 생성된 프로세스의 주소 공간을 사용하여 지정된 프로그램을 메모리에 적재합니다.
GUI 인터페이스를 사용하는 경우에도 유사한 매커니즘을 사용하여 로더가 호출됩니다.
여기까지가 프로세스의 생성 과정이라고 보면 됩니다. 이것은 운영체제 내부적으로 수행되는 동작입니다. 여기서 저는 좀 더 나아가서 프로세스 생성을 구분해서 설명해볼까 합니다.
해당 내용은 조금 길어질 여지가 있으므로 소단원하나를 만들겠습니다.
UNIX 계열의 시스템에서 프로세스 생성을 할 때 fork()
명령어를 사용한다는 것을 위에서 언급했습니다.
Windows 계열인 경우에는 create_process()
를 사용합니다.
그렇다면, 이 둘은 어떠한 차이가 있는 것이고 무엇보다 위에서 설명했던 프로세스 생성 과정과 이 두 명령어의 관계는 어떻게 되는 것일까요? 해당 명령어도 위의 동작을 수행하는 것일까요?
먼저, 확실히 잡아두고 가야되는 것은 위에서 언급한 컴파일 -> 링킹 -> 로딩
과정을 통해서 생성되는 프로세스는 대체로 '초기 프로세스'라고 생각하시면 됩니다.
해당 과정을 통해 운영체제는 필요한 자원을 할당하고 프로세스를 초기화합니다. 이 과정은 사용자가 명령어를 통해 직접적으로 제어하는 것이 아니라, 운영체제 내부 매커니즘에 의해 자동으로 이루어집니다. 따라서 '초기 프로세스' 생성은 운영체제의 기본 동작으로 볼 수 있습니다.
그렇다면, fork()
와 create_process()
는 어떠한 목적으로 사용될까요?
해당 함수들은 프로그램(사용자)에 의해 호출되어 자식 프로세스를 생성하는 함수입니다.
fork()
UNIX 계열 시스템
에서 사용됩니다.create_process()
fork()
와 달리 새 프로세스에 대해 완전히 새로운 실행 환경을 설정하며, 부모 프로세스의 상태를 복사하지 않습니다. 주로 Windows와 같은 다른 운영체제
에서 사용됩니다.특히, fork()에서 언급되고 있는 복제에 대해서 좀 더 이야기할 필요가 있습니다.
fork()
가 호출될 때, 메모리의 실제 복사가 즉시 발생하지 않음을 의미합니다. 대신, 부모 프로세스와 자식 프로세스는 메모리의 동일한 부분을 공유하게 됩니다. 메모리에 대한 변경이 발생할 때만 실제 복사가 이루어집니다. 이 방법으로 메모리 사용량과 성능 오버헤드를 최소화할 수 있습니다.fork()
를 사용하여 생성된 자식 프로세스는 자신만의 실행 경로를 가집니다. 이는 자식 프로세스가 독립적인 작업을 수행할 수 있음을 의미합니다. 자식 프로세스는 exec()
계열 함수를 사용하여 완전히 새로운 프로그램을 로드하고 실행할 수 있습니다. fork()
는 서버에서 새로운 연결을 처리하거나, 병렬 처리를 수행하거나, 복잡한 계산 작업을 분리하는 데 유용합니다. 이를 통해 각각의 자식 프로세스가 독립적으로 특정 작업을 수행할 수 있습니다.여기서 또 의문점이 생길 수 있는데, 자식 프로세스가 무엇이길래 부모 프로세스로부터 메모리의 복사를 수행할 필요가 있는가에 대한 의문입니다.
좀 더 명확히 이해해보기 위해서 "크롬"을 예로 들어가며 설명해 보겠습니다.
먼저, 크롬을 처음에 실행하면 위에서 말했던 "초기 프로세스" 생성 과정을 거쳐가며 생성될 것입니다.
이후 크롬은 다중 프로세스 아키텍처를 사용하여 여러 개의 프로세스를 구성합니다.
사실 이러한 경우에 fork()
와 같은 전통적인 프로세스 복제 방식을 사용하기 보다는 운영체제의 API를 활용하여 새로운 프로세스를 생성하고 관리하는 방식을 채택한다고 합니다.
fork()
를 통해 직접적으로 부모 프로세스의 복사본으로 생성되지 않습니다. 대신, 크롬은 메인 프로세스(부모)가 직접 새로운 프로세스를 생성하고 관리하는 매커니즘을 사용합니다.createProcess()
(Windows) 또는 유사한 API(다른 운영체제)가 사용될 수 있습니다. 이러한 API는 새 프로세스를 생성하고, 필요한 실행 파일을 로드합니다.이렇게 자식 프로세스의 동작이 명확히 구분되는 경우에는 fork()
를 사용하지는 않습니다.
결국 fork()
의 사용의 대표적인 예시는 다음과 같습니다.
이러한 경우에는 기존 작업과 비슷한 로직들로 동작하는 격리된 환경의 프로세스가 필요할 수 있기 때문에 fork()
를 사용합니다.
각 운영체제는 고유한 시스템 콜 집합을 제공합니다. 시스템 콜은 어느 정도 같더라도 다른 장벽으로 인해 응용 프로그램을 다른 운영체제에서 실행하기 어렵습니다.
하지만, 우리는 여러 운영체제에서 같은 응용 프로그램을 사용하는 것을 볼 수 있습니다. 아래에는 그것을 가능케 하는 3가지 방법이 제시됩니다.
결국, 응용 프로그램에서 운영 체제에 맞게 설계를 다 하든가, 아니면 가상 머신을 활용하던가 또는 컴파일러의 존재로 여러 운영체제에서 동일한 응용 프로그램을 실행할 수 있도록 해주는 것입니다.
하지만 이것은 말처럼 쉬운 것이 아닙니다.
일반적으로 응용 프로그램의 이동성이 부족한 데에는 여러 가지 원인이 있으며, 이로 인해 여전히 크로스 클랫폼 응용 프로그램을 개발하는 것이 어려운 작업입니다.
실제로, 하나의 API 집합(예: iOS에서 사용가능한 API)를 호출하도록 설계된 응용 프로그램은 해당 API를 제공하지 않는(예: Android) 운영체제에서는 작동하지 않습니다.
다음을 포함하여 시스템의 낮은 수준에는 다른 어려운 점이 존재합니다.
위에서 언급했듯이 API는 응용 프로그램 수준에서 특정 기능을 지정합니다.
아키텍처 수준에서 이진 코드의 여러 구성요소가 주어진 아키텍처에서 특정 운영체제와 상호 작용할 수 있는 방법을 정의하는 데 ABI(Application Binary Interface)가 사용됩니다.
이것은 특정 아키텍처에 대해 명시되어 있으며 아키텍처 수준의 API입니다. 따라서, 특정 운영체제에 대한 ABI는 플랫폼 간 호환성을 거의 제공하지 않습니다.
운영체제를 설계하고 구현할 때 생기는 문제점에 대해 완전한 해결책이 있는 것은 아니지만, 성공적인 접근 방법들은 있습니다.
시스템을 설계하는 데에 첫째 문제점은 시스템의 목표와 명세를 정의하는 일이빈다.
시스템 설계는 최상위 수준에서는 하드웨어와 시스템 유형(일괄처리, 시분할, 단일 사용자, 다중 사용자, 분산, 실시간 혹은, 범용)의 선택에 의해 영향을 받을 것입니다.
이러한 요구 조건을 일일히 명시하는 것은 어렵지만 근본적으로 이러한 요구 조건은 사용자 목적과 시스템 목적의 두 가지 기본 그룹으로 나눌 수 있습니다.
이전에도 말했지만, 운영체제의 목적은 사용자에게 프로그램 사용의 목적 달성을 하기위한 편의성을 제공하기 위한 동작들을 제공합니다.
이것을 바탕으로 사용자 목적에 맞게 설계를 하고, 그에 맞게 시스템을 설계하는 것입니다.
이러한 시스템도 사람들에 의해 정의되기 때문에 이 또한 유지 보수성이나 적응성, 신뢰성, 무오류, 효율성들을 고려해서 설계가 될 것입니다.
한 가지 중요한 원칙은 기법(mechanism)으로부터 정책을 분리하는 것입니다.
기법은 어떤 일을 어떻게 할 것인가를 결정하는 것이고, 정책은 무엇을 할 것인가를결정하는 것입니다.
예를 들면, 타이머 구조는 CPU 보호를 보장하기 위한 기법이지만, 특정 사용자를 위해 타이머를 얼마나 오랫동안 설정할지를 결정하는 것은 정책적 결정입니다.
목적성과 환경에 맞게 정책이 결정될 것이고, 이 정책을 실현시키기 위해서 기법을 사용하는 것입니다.
초기 운영체제는 어셈블리 언어로 작성되었습니다. 이제 대부분은 C나 C++와 같은 고급 언어로 작성되며, 극히 일부의 시스템이 어셈블리 언어로 작성됩니다. 실제로, 둘 이상의 고급 언어가 종종 사용됩니다.
커널은 대부분 약간의 어셈블리 언어를 함께 사용하고 C로 작성되었습니다. 대부분의 Android 시스템 라이브러리는 C 또는 C++로 작성되며 시스템에 개발자 인터페이스를 제공하는 응용 프로그램 프레임워크는 대부분 JAVA로 작성됩니다.
그렇다면 왜 이러한 고급언어를 사용해서 작성하는 것일까요? 실제로 저수준 언어로 작성하는 것이 더 빠른 성능을 기대할 수 있다는 것은 흔히 알려져 있는 사실인데도 불구하고 말이죠.
고수준 언어를 사용함으로써 얻을 수 있는 이점은 다음과 같습니다.
추가로, 컴파일러 기술의 향상은 단순한 재컴파일에 의해 전체 운영체제를 위해 생성된 코드를 향상시킬 것입니다.
단점은 다음과 같습니다.
현대에서는 이러한 문제들은 주된 문제가 아닙니다.
실제로, 현대의 컴파일러는 대규모 프로그램을 위해 복잡한 분석을 수행하고 정교한 최적화를 적용하여 우수한 코드를 생산할 수 있습니다. 현대의 처리기는 깊은 파이프라이닝과 다수의 기능 장치를 가지며 이들 기능 장치는 인간이 할 수 있는 것보다 훨씬 쉽게 복잡한 의존성의 상세 사항들을 처리할 수 있습니다.
이제 운영체제의 주요 성능 향상은 우수한 어셈블리어 코드보다는 좋은 자료구조와 알고리즘의 결과일 가능성이 큽니다.
현대의 운영체제와 같이 크고 복잡한 시스템은 적절하게 동작하고 쉽게 변경될 수 있어야 합니다.
일반적인 접근 방법은 한 개의 일관된 시스템 보다는 태스크를 작은 구성 요소 즉, 모듈로 분할하는 것입니다.
운영 체제를 구성하는 가장 간단한 구조인 모놀리식 구조는 커널의 모든 기능을 단일 주소 공간에서 실행되는 실행되는 단일 정적 이진 파일에 넣는 것입니다.
아래는 전통적인 UNIX 시스템 구조를 보여줍니다.
이러한 계층 구조는 커널과 하드웨어의 집합들을 구분해 줍니다.
시스템 콜 인터페이스 아래와 물리적 하드웨어 위의 모든 것이 커널입니다.
Linux 구조는 조금 다른데 아래 사진과 같습니다.
UNIX에 기반을 두고 있으며, 차이점은 응용 프로그램이 일반적으로 커널에 대한 시스템 콜 인터페이스와 통신할 때 glibc 표준 C 라이브러리
를 사용한다는 점입니다.
Linux 커널은 단일 주소 공간에서 커널 모드로 전부 실행된다는 점에서 모놀리식이지만, 런타임 중에 커널을 수정할 수 있는 모듈식 설계를 갖추고 있습니다.
모놀리식의 장단점은 다음과 같습니다.
즉, 모놀리식 구조는 밀접하게 결합된 시스템으로 구성되어 있습니다.
밀접하게 결합된 시스템의 단점을 보완하기 위해서 느슨하게 결합된 시스템을 설계할 수 있습니다.
이러한 시스템은 기능이 특정 기능 및 한정된 기능을 가진 개별적이며 작은 구성요소로 나뉩니다. 이 모든 구성요소가 합쳐져 커널을 구성합니다.
이 설계의 이점은 다음과 같습니다.
간단하게 정리하자면, 구현과 디버깅의 간단함을 주된 장점으로 생각할 수 있는 것입니다.
이러한 이점을 가지고 있음에도 불구하고 순수한 계층 접근 방식을 사용하는 운영체제는 비교적 적다고 합니다. 그 이유는 다음과 같습니다.
따라서, 어느 정도의 계층화만 활용하여 가지고 있던 이점을 이용합니다.
마이크로커널 접근 방식을 사용하여 커널을 모듈화한 운영체제를 Mach라고 합니다.
이 방법의 설계는 기본적으로 다음과 같습니다.
전형적인 마이크로커널의 구조는 아래와 같습니다.
마이크로커널의 주 기능은 클라이언트 프로그램과 역시 사용자 공간에서 수행되는 다양한 서비스 간에 통신을 제공하는 것입니다.
통신은 메시지 전달에 의해 제공됩니다.
예를 들어서, 만일 클라이언트 프로그램이 파일에 접근하기를 원한다면, 파일 서버와 반드시 상호 작용 해야합니다.
해당 방식의 장점 중 하나는 운영체제의 확장이 쉽다는 것입니다.
모든 새로운 서비스는 사용자 공간에 추가되며, 따라서 커널을 변경할 필요가 없습니다. 만일 커널이 변경되어야 할 때는, 마이크로커널은 작은 커널이기 때문에 변경할 대상이 비교적 적은 경향이 있습니다.
결과적으로 가져오게 되는 이점은 이러한 방식으로 설계된 운영체제는 한 하드웨어로부터 다른 하드웨어로 이식이 쉽다는 점입니다.
즉, 서비스의 대부분은 사용자 프로세스로 수행되기 때문에 이식성이 좋고 이로 인해 더욱 높은 보안성과 신뢰성을 제공합니다.
마이크로커널 운영체제의 가장 잘 알려진 실례는 macOS
및 iOS
운영체제의 커널 구성요소인 Darwin
입니다. 실제로 Darwin
은 두 개의 커널로 구성되며 그중 하나는 Mach
마이크로커널입니다.
이 방식의 단점은 가중된 시스템 기능 오버헤드 때문에 성능이 나쁘다는 점입니다.
운영체제를 설계하는 데 이용되는 최근 기술 중 최선책은 아마도 적재가능 커널 모듈(loadable kernel modules, LKM) 기법의 사용일 것입니다. 이 모듈은 동적 로딩을 통하여 링크될 수 있습니다.
앞서 언급되었던 구조들의 특징들을 엮어서 활용하면 아래와 같은 특징들을 가지게 됩니다.
위에서 보았듯이, 현대의 운영체제는 다양한 구조를 결합하여 성능, 보안 및 편리성 문제를 해결하려는 혼용 구조로 구성됩니다.
간단하게 예시를 들어보자면,
Linux는 모놀리식 구조와 LKM을 같이 사용합니다.
Windows는 모놀리식 구조와 전형적인 마이크로커널 + LKM을 사용합니다.
여기서 Windows는 마이크로커널의 형태를 사용자 모드 프로세스로서 실행되는 분리된 서브시스템, 운영체제의 인격이라고 알려진, 지원하는 방식 등으로 유지하고 있습니다.
사실 8.5에서는 macOS
와 iOS
에 대해 언급하면서 Android
와 비교하면서 구조에 대해서 설명을 하고 있습니다.
하지만, 이부분은 읽어본 결과 온전히 받아들이기 어렵게 느껴져서 정리는 나중에 하는 것으로 하겠습니다.
또한, 운영체제 빌딩과 부팅, 운영체제 디버깅에 대해서도 다루는데 이 부분들 또한 추후에 다루도록 하겠습니다.