운영체제 10th Edition, OS? Oh Yes!, 이화여대 반효경 교수님의 운영체제 강의 내용을 바탕으로 정리를 진행했습니다. 관련 강의와 책을 보고 싶으시다면 링크를 클릭해주세요.
운영체제(Operating System)
운영체제는 컴퓨터 사용자와 하드웨어 사이에서 중개자 역할을 합니다. 사용자가 프로그램을 편리하고 효율적이게 수행할 수 있는 환경을 제공해주면서 하드웨어를 관리해주는 소프트웨어 입니다.
또한, 응용 프로그램을 위한 기반을 제공해줍니다.
컴퓨터 시스템 구성 요소
운영체제는 컴퓨터 시스템에서 자원 할당자의 역할을 맡습니다.
시스템이 효율적이고 공정하게 운영될 수 있도록 어느 요청에 자원을 할당할 지에 대한 결정을 운영체제가 담당합니다. 자원 할당뿐만 아니라 입출력 장치의 제어와 작동에도 운영체제가 관여하게 됩니다.
운영체제의 다양한 역할을 쉽게 이해하기 위해서는 먼저 컴퓨터 시스템이 어떻게 구성되어 있는지 알아야 합니다.
CPU, 메모리, 입출력 장치 등의 하드웨어 구조를 이해하고 나서 운영체제에 대해 좀 더 알아보도록 하겠습니다.
컴퓨터 시스템 구조
컴퓨터 시스템 구조는 CPU와 메모리로 구성된 컴퓨터와 입출력 장치들로 구성되어 있습니다.
하드웨어 구조
공통 버스(Bus)를 통해서 CPU, 메모리, 입출력 장치는 연결됩니다. CPU와 입출력 장치가 공통 버스를 통해서 연결되기 때문에 메모리 사이클을 놓고 경쟁하게 됩니다.
⓵ 저장 장치 구조(Storage Structure)
원하는 프로그램이 실행되기 위해서는 해당 프로그램이 메인 메모리에 먼저 적재되어야 합니다. CPU는 메인 메모리에 있는 프로그램만을 실행하기 때문이죠.
그럼, 프로그램과 데이터를 영구히 메인 메모리에 저장하면 되지 않을까요?
그건 불가능합니다. 모든 필요한 프로그램과 데이터를 영구히 저장하기에 메인 메모리는 너무 작습니다. 또한, 메인 메모리는 전원 공급이 되지 않으면 내용을 잃어버리는 휘발성 저장 장치이기 때문에 영구히 저장하는건 불가능합니다.
그렇다면, 프로그램과 데이터를 어떻게 영구히 저장할 수 있나요?
대부분의 컴퓨터 시스템은 메인 메모리의 확장으로 보조저장장치를 제공합니다.
보조저장장치는 대량의 데이터를 영구히 보존할 수 있고, 프로그램과 데이터 모두를 위한 저장소를 제공해줍니다. 메인 메모리에 적재되지 못한 프로그램들은 메인 메모리에 올라가기 전까지 보조저장장치에 있게 됩니다.
보조저장장치는 메인 메모리보다 훨씬 느리기 때문에 CPU에서 프로그램을 빠르게 실행시키기 위해서는 접근 속도가 빠른 메인 메모리에 프로그램을 적재해야 하는 겁니다.
하드 디스크 드라이브(HDD)나 비휘발성 메모리 장치(NVM)같은 2차 저장 장치도 있지만, CD-ROM, Blu-ray, 자기 테이프 등의 3차 저장장치도 존재합니다. 3차 저장장치는 다른 장치에 저장된 자료의 백업 사본을 저장하기 위한 특수 목적으로만 사용합니다. 3차 저장장치는 저장 공간이 크지만 그만큼 속도가 느립니다.
보시다시피, 저장 장치 간의 차이점은 바로, 속도, 크기, 휘발성 입니다.
크기와 속도 사이에 상충하는 측면이 있어서 메모리가 작을수록 빠르고 CPU에 가까이 있게 됩니다.
메모리 구조
위에 있는 레지스터, 캐시 메모리, 메인 메모리는 1차 저장 장치로 용량은 작지만 액세스 시간이 빠릅니다. 하지만, 휘발성 저장장치이기 때문에 전원이 공급되지 않으면 저장할 수 없습니다.
아래 있는 2차, 3차 저장장치는 용량은 크지만 액세스 시간이 느립니다. 하지만, 비휘발성 저장장치이기 때문에 안전하게 데이터나 프로그램을 저장할 수 있습니다.
각 저장 장치에 대해서 조금 더 자세하게 알아봅시다.
레지스터(Register)
레지스터는 메인 메모리보다 빠른 기억 장치로 크기가 작습니다. 시스템과 사용 목적에 따라서 8, 16, 32비트 등의 크기를 가집니다.
메인 메모리보다 빠른 기억 장치인 레지스터는 CPU가 여러 개 가지고 있습니다.
그 중에서 데이터 레지스터는 연산을 위해 사용하게 됩니다. 레지스터에서 진행되는 연산은 메인 메모리에서 일어나는 데이터 연산보다 더 빠릅니다. 데이터 레지스터말고도 다양한 레지스터가 있는데, 주소 레지스터는 데이터, 명령어의 메모리 주소 저장, 계산에 사용됩니다.
CPU의 연산을 제어하기 위해 사용되는 MBR, MAR, IR, PC 등의 레지스터도 있습니다. 또한, 모든 CPU는 현재 상태 정보를 저장하는 PSW 레지스터를 가집니다. PSW 레지스터는 인터럽트 가능, 불가능 비트, 현재 실행 모드를 나타내는 비트에 대한 값을 가집니다.
메인 메모리(주기억 장치)
메인 메모리(RAM)는 시스템에서 중요한 부분입니다.
CPU에서 실행되기 위한 프로그램들은 반드시 주기억 장치(메인 메모리)에 있어야 하기 때문이죠. 메인 메모리에 있던 명령어는 실행을 위해서 CPU 레지스터에 적재가 되고 일부분은 캐시에 저장됩니다.
메인 메모리와 CPU는 특정 메모리 주소들에 대한 적재 또는 저장 명령을 통해서 상호 작용합니다.
적재 명령은 메인 메모리로부터 CPU 내부의 레지스터로 한 바이트 또는 한 워드를 옮기는 일입니다. 반대로, 저장 명령은 레지스터의 내용을 메인 메모리로 옮기게 됩니다.
적재, 저장 외에도 CPU는 프로그램 카운터(PC)에 저장된 위치부터 실행하기 위해서 메인 메모리에서 명령을 자동으로 적재하게 됩니다.
폰 노이만 구조에서 메인 메모리와 CPU 사이의 명령-실행 사이클은 이러합니다.
⓵ 메인 메모리로부터 명령어를 인출하고, ⓶ 해당 명령을 CPU에 있는 명령 레지스터에 저장하게 됩니다. ⓷ 저장된 명령은 해독됩니다. 이때, 메모리로부터 피연산자를 인출하여 내부 레지스터에 저장하도록 유발합니다. ⓸ 피연산자에 대한 명령을 실행한 후에는 결과가 메모리에 다시 저장될 수 있습니다.
CPU와 상호작용하는 메인 메모리는 아쉽게도 휘발성 저장 장치입니다.
즉, 영구히 데이터나 프로그램을 저장할 수 없는 저장 장치입니다. 전원을 끄면 정보가 날아가버리기 때문이죠.
그렇기 때문에, 컴퓨터 전원을 키면 가장 먼저 실행되는 부트스트랩 프로그램도 휘발성 저장 장치인 메인 메모리에 저장할 수 없습니다. 해당 프로그램이 운영체제를 메모리에 적재해주어야 하는데, 날아가버린다면 큰일이겠죠?
따라서, 부트스트랩 프로그램은 EEPROM 및 기타 형태의 펌웨어를 사용합니다.
위에서 부트스트랩 프로그램이 운영체제를 메인 메모리에 적재한다고 했는데, 적재하고 나서 전원이 들어와 있는 동안 항상 메모리에 상주하는 부분이 있습니다. 이 부분을 커널이라고 부릅니다.
커널은 항상 메모리에 상주하고 있어야 합니다. 사용자와 실행 프로그램을 위해 매우 빈번하게 사용되는 부분들이 있기 때문에 이를 디스크에 올려두게 되면 디스크에서 메인 메모리로 올리는 시간(입출력 시간)이 많이 요구됩니다. 이로 인해 시스템 성능이 저하될 수 있기 때문에 항상 메인 메모리에 올려두는 겁니다.
그럼, 전체 프로그램을 커널로 만들면 항상 메인 메모리에 올라가 있을테니 좋지 않을까요?
그러면 사용자 프로그램들이 올라갈 공간이 없어지게 됩니다.
메인 메모리 공간은 한정되어 있기 때문에 운영체제가 메모리의 대부분을 차지하게 되면 사용자 프로그램은 메모리에 올라올 수 없게 되고, 사용자 프로그램을 효과적으로 실행할 수 없게 만듭니다.
⓶ 입출력 장치(I/O Device)
입출력 작성 구조
입출력 장치들은 장치들을 전담하는 작은 CPU같은 것들을 가집니다. 이 작은 CPU를 Device controller 라고 부릅니다.
Device controller는 각 입출력 장치를 통제하게 됩니다. 디스크의 헤드(head)가 어떻게 움직일지, 어떤 데이터를 읽을지는 CPU가 아니라 Device controller가 통제하게 됩니다.
Device controller는 제어 정보를 위해서 Control Register와 Status Register를 가집니다. Control Register를 통해서 해당 버퍼에 있는 값을 프로그램으로 넘겨주거나 프로그램에 있는 데이터를 화면에 출력하기 위해 버퍼에 담은 값을 어떻게 해야 하는지 지시하게 됩니다.
또한, 입출력 장치는 장치 드라이버를 가집니다.
입출력 장치를 사용해야하는 상황이 오면 장치 드라이버를 사용해서 명령을 하게 됩니다. 드라이버는 CPU가 입출력 장치를 실행하기 위해서 필요한 코드를 담고 있습니다. 따라서, 일을 하기 위한 메뉴얼, 동작을 하기 위해서 무엇을 해야하는지에 대한 인터페이스를 제공해주게 됩니다.
이러한 입출력 장치는 입출력이 발생했을때 CPU로 신호를 보내주어야 합니다.
어떤 방식으로 신호를 보낼 수 있을까요?
가장 간단하게 생각할 수 있는 방식은 CPU가 신호를 계속 확인하는 방식이겠죠?
CPU가 해당 입출력이 완료될 때까지 확인하는 방식은 입출력이 완료되면 CPU가 바로 신호를 받고 작업을 이어갈 수 있다는 장점이 있지만, CPU가 입출력 진행동안에 입출력에 매여 있게 되기 때문에 CPU를 낭비하게 됩니다.
그러면, CPU는 자기 일을 하고 있으면서 간접적으로 신호를 받을 수 있는 방법이 없을까요?
인터럽트를 통해서 입출력이 완료되었는지 확인할 수 있습니다.
입출력 장치가 공유하고 있는 시스템 버스를 통해서 CPU에게 인터럽트 신호를 보내는겁니다. 인터럽트를 통해서 CPU에게 입출력 장치에 대한 일을 처리해달라고 알림을 보내는거죠.
물론, CPU가 신호를 계속 확인하는 방식처럼 입출력이 끝나고 바로 작업을 이어갈 수는 없겠지만, 인터럽트를 통해서 입출력이 완료되었는지 알 수 있기 때문에 CPU는 다른 일을 할 수 있게 됩니다. 즉, CPU 낭비가 사라집니다.
하지만, 인터럽트를 통한 입출력도 문제가 있습니다.
입출력 장치에서 인터럽트 신호를 보내서 인터럽트를 처리하는 방식은 소량의 데이터를 이동하는데 좋지만, 비휘발성 저장장치 I/O와 같은 대량 데이터 이동 시에는 높은 오버헤드를 유발할 수 있습니다.
버퍼 크기에 비해 입출력 데이터가 클수록 잦은 인터럽트 처리를 요구하게 되고, 결국 시스템 오버헤드를 발생시키게 되는겁니다.
그럼, 대량 데이터 이동 시에는 CPU가 계속 확인하는 방식밖엔 사용하지 못하는 건가요?
인터럽트 문제로 인해 나온 해결책이 바로, 직접 메모리 액세스(DMA) 방식입니다.
DMA는 주변 장치(하드 디스크, 그래픽 카드)들이 메모리에 직접 접근하여 읽거나 쓰도록 하는 기능입니다. 따라서, CPU의 개입없이 입출력 장치와 기억 장치 사이의 데이터를 전송할 수 있게 됩니다. 메모리로부터 자신의 버퍼 장치로, 또는 버퍼로부터 메모리로 데이터 블록 전체를 전송하게 됩니다. 블록 전송이 완료되면 인터럽트를 발생시킵니다.
CPU가 개입하지 않기 때문에 장치 컨트롤러가 전송 작업을 수행하는 동안에 CPU는 다른 작업을 수행할 수 있게 됩니다.
인터럽트(Interrupt)
운영체제가 자원을 효율적으로 관리하기 위해선 현 자원의 상황을 알아야 합니다.
현 자원의 상황을 파악할 수 있는 방법 중 하나가 바로, 폴링(Polling)입니다. 폴링은 CPU가 일정한 시간 간격을 두고 각 자원들의 상태를 주기적으로 확인하는 방법입니다.
즉, 선생님이 반 아이들 한명 한명에게 가서 질문이 있는지 물어보는겁니다.
이런 상황에서 선생님은 학생에게 질문을 물어보는 시간을 적절히 정해야 합니다. 또한, 질문이 없던 친구가 선생님이 지나가고 나서 질문이 생기더라도 선생님이 다시 한 바퀴를 돌아올 때까지 질문을 말 할 수 없습니다. 그리고 선생님은 질문이 없는 친구들에게도 굳이 질문이 있냐고 물어봐야 합니다. 비효율적이죠.
그러면 어떤 방식이 효율적일까요?
질문이 있는 친구가 알아서 손을 들고 질문을 하는 방식입니다.
이렇게 능동적으로 자신의 상태 변화를 CPU에게 알리는 방식이 바로, 인터럽트(Interrupt)입니다.
각 자원들은 인터럽트로 상황 발생을 CPU에게 즉시 알릴 수 있기 때문에 CPU가 각 자원의 상태를 묻느라 시간 낭비하지 않아도 됩니다.
그렇다면, 인터럽트는 어떤 과정을 거쳐서 발생하게 될까요?
먼저, ⓵ 하드웨어가 시스템 버스를 통해서 CPU에 신호를 보내서 인터럽트를 발생시킵니다. ⓶ CPU는 실행중인 명령어 실행을 완료하고서 인터럽트 신호를 확인합니다. ⓷ 현재 실행중이던 프로그램을 인터럽트 처리 후 다시 실행하기 위해서 프로그램의 현 상태 정보를 시스템(제어) 스택에 저장합니다. ⓷부분을 좀 더 자세하게 봅시다.
인터럽트 실행 이전의 CPU와 메인 메모리가 있습니다. CPU는 인터럽트를 처리하기 전에 현재 실행하던 프로그램의 정보를 메인 메모리의 제어 스택에 저장해둡니다.
범용 레지스터와 프로그램 카운터에 있던 값을 제어 스택에 저장하고 스택 포인터에 제어 스택의 마지막 부분 주소를 저장합니다. 프로그램 카운터에는 인터럽트 처리 루틴의 시작 주소를 넣습니다.
인터럽트 처리 루틴 시작 주소가 프로그램 카운터에 들어가면 ⓸ 인터럽트 처리 루틴을 실행시킵니다. 처리 루틴은 먼저 CPU에 있는 레지스터들의 값을 저장합니다. 그리고 인터럽트 고유 핸들러를 호출하게 됩니다.
⓹ 처리가 끝나면 프로그램 카운터에 인터럽트 처리 루틴의 마지막 주소가 들어 있습니다. 이제 이전에 저장했던 레지스터 값으로 다시 재저장합니다.
⓺ PSW, PC 값이 인터럽트 이전에 실행 중이던 프로그램에서 실행할 명령어 위치로 재저장되었기 때문에 이어서 프로그램을 실행할 수 있게 됩니다.
인터럽트는 매우 빈번하게 발생하기 때문에 빠르게 처리되어야 합니다. 인터럽트 루틴에 대한 포인터들의 테이블을 이용하게 되면 필요한 속도를 제공받을 수 있게 됩니다.
테이블을 사용하게 되면 중간 루틴이 필요없게 되기 때문이죠. 테이블은 하위 메모리에 저장되어 있으면서 여러 장치에 대한 인터럽트 서비스 루틴 주소를 저장하기 때문에 간접적으로 인터럽트 루틴을 호출할 수 있습니다.
근데, 인터럽트 처리 중에 또 인터럽트 신호가 온다면 어떻게 처리되나요?
인터럽트 처리 중에 인터럽트가 중첩되는 상황이 발생할 수 있습니다.
현재 처리하던 인터럽트를 마저 처리 완료하고 차례대로 다음 인터럽트를 처리하는 순차적 처리를 할 수도 있지만, 나중에 들어온 인터럽트의 우선순위가 높다면 중간에 다른 인터럽트를 먼저 처리할 수도 있습니다.
하지만, 인터럽트가 계속 중첩되게 된다면 제어 스택에 인터럽트 전 상태들이 쌓여서 제어 스택의 크기를 증가시키게 됩니다.
입출력 장치가 인터럽트를 보낸다는 건 알겠는데, CPU는 어떻게 인터럽트를 받는건가요?
CPU 하드웨어는 인터럽트 요청 라인이 존재합니다.
각 장치 컨트롤러는 해당 라인에 신호를 보내서 인터럽트가 발생했다는 정보를 알립니다. CPU는 하나의 명령어 실행이 완료되고 나면 요청 라인을 감지해서 인터럽트가 있는지 확인합니다.
CPU가 인터럽트를 감지하면 인터럽트 번호를 읽습니다. 해당 번호는 인터럽트 벡터(인터럽트 서비스 루틴의 주소를 제공하기 위한 주소 배열)의 인덱스로 사용하게 됩니다. 해당 인덱스를 사용해서 인터럽트 핸들러 루틴으로 점프하게 되고, 해당 인덱스 관련 주소에서 실행을 시작하게 됩니다.
지금까지 얘기한 인터럽트는 하드웨어 인터럽트를 말합니다.
하지만, 하드웨어 인터럽트말고도 또 다른 형태의 인터럽트가 있습니다. 트랩입니다. 트랩은 오류, 사용자 프로그램 특정 요청(시스템 콜)때문에 발생하는 소프트웨어 인터럽트입니다.
트랩 중 하나인 시스템 콜은 운영체제에 의해 사용 가능하게 된 서비스에 대한 인터페이스를 제공해줍니다. 따라서, 시스템 콜 API는 운영체제마다 다르게 나타나게 됩니다. 즉, 같은 작업이 운영체제마다 다른 API로 나타나는거죠.
사용자가 사용자 프로그램 특정 요청을 발생시킬때에는 해당 시스템 콜이 무슨 작업을 하는지, 구현이 어떻게 되어 있는지 알 필요가 없습니다. 단지, API를 준수하고 시스템 콜의 결과로 운영체제가 어떤 일을 하는지만 알면 됩니다.
범용 프로세서 수에 따른 컴퓨터 시스템 구조
범용 프로세서의 수에 따라서 컴퓨터 시스템의 구조는 다르게 나타나게 됩니다.
간단하게 한 개의 프로세서가 있다고 한다면, 해당 시스템을 단일 프로세서 시스템이라고 합니다.
단일 프로세서 시스템은 단일 처리 코어를 가진 하나의 CPU를 포함하는 프로세서입니다. 여기서 말하는 코어는 명령을 실행하고 로컬로 데이터를 저장하기 위한 레지스터를 포함하는 구성 요소라고 보면 됩니다.
하지만, 현대에는 단일 프로세서 시스템을 가진 컴퓨터는 거의 없습니다.
대부분 다중의 무언가를 가진 시스템입니다.
다중 프로세서 시스템
각각의 단일 코어 CPU가 있는 두 개 이상의 프로세서를 가진 시스템입니다.
다중 프로세서 시스템 구조
일반적인 다중 프로세스 시스템은 각 CPU 프로세서가 운영체제 기능 및 사용자 프로세스를 포함한 모든 작업을 수행하는 대칭형 다중 처리(SMP)구조입니다. 각 CPU 프로세서는 개별 또는 로컬 캐시뿐만 아니라 자체 레지스터 세트를 가집니다. 하지만, 컴퓨터 버스, 클록, 메모리 및 주변 장치는 공유하게 됩니다. 자원을 다양한 프로세서 간에 동적으로 공유할 수 있어서 프로세서 간의 작업 부하 분산을 낮출 수 있게 됩니다.
다중 프로세서 시스템은 많은 프로세스를 동시에 실행 가능하다는 장점이 있습니다.
N개의 CPU가 있으면 성능이 크게 저하하지 않으면서 N개의 프로세스를 실행할 수 있습니다. 따라서, 더 적은 시간으로 더 많은 작업을 수행할 수 있기 때문에 처리량이 증가하게 됩니다.
하지만, CPU가 독립적이기 때문에 하나는 유휴 상태이고 다른 하나만 과부하에 걸리는 상황이 생길 수 있습니다. 해당 문제는 특정 자료구조를 공유해서 해결할 수 있습니다.
다중 코어 시스템
여러 개의 코어가 단일 프로세서(칩)에 상주하는 시스템입니다. 다중 코어 시스템은 다중 프로세서 시스템이 발전한 형태입니다.
다중 코어 시스템 구조
여러 개의 코어가 같은 프로세서 내부에 존재하기 때문에 칩 내 통신이 가능해집니다. 프로세서(칩) 내 통신이 프로세서 간 통신보다 빠르기 때문에 단일 코어를 가지는 여러 프로세서보다 효율적입니다. 훨씬 적은 전력을 사용할 수 있기 때문이죠.
NUMA(Non-Uniform Memory Access)
다중 프로세스 시스템은 CPU를 추가하면 컴퓨팅 성능이 향상되지만, CPU를 너무 많이 추가하게 되면 시스템 버스에 대한 경합이 병목 현상이 되어서 성능이 저하되기 시작합니다.
따라서, 병목 현상 문제를 해결하기 위해 각 CPU에 작고 빠른 로컬 버스를 통해 액세스 되는 자체 로컬 메모리를 제공합니다. 로컬 메모리 제공 시, 모든 CPU가 공유 시스템 연결로 연결되어 하나의 물리 주소 공간을 공유하게 됩니다. 이 방식은 NUMA 라고 합니다.
NUMA 시스템 구조
NUMA는 더 많은 프로세서가 추가될수록 더 효과적으로 확장 가능합니다.
하지만, 원격 메모리 액세스 시에는 지연 시간이 증가하여 성능 저하가 발생할 수 있습니다. 각 CPU가 자신이 가진 자체 로컬 메모리에 접근하듯이, 다른 CPU의 로컬 메모리를 접근하지 못하기 때문에 발생하는 성능 저하 문제입니다. 이는 운영체제의 신중한 CPU 스케줄링과 메모리 관리를 통해서 최소화할 수 있습니다.
운영체제의 구조
위에서 컴퓨터 시스템의 구조가 어떻게 구성되어 있는지 알아보았습니다. 그렇다면, 운영체제는 어떤 구조로 구성되어 있을까요?
모놀리식 구조
가장 간단한 구조로 구조가 아예 없는 구조입니다.
구조가 아예 없기때문에 커널의 모든 기능을 단일 주소 공간에서 실행하는 단일 정적 이진 파일에 넣습니다. 따라서, 단일 주소 공간에 엄청나게 많은 기능들이 들어가게 됩니다. 즉, 구현 및 확장이 어려운 구조입니다.
하지만, 성능면에서 뚜렷한 이점이 존재합니다. 구조가 없기 때문에 커널 안에서 통신 속도가 빠릅니다.
계층 구조
모놀리식 구조가 밀접하게 결합된 시스템이라면 계층적 접근은 느슨하게 결합된 시스템입니다.
특정 기능, 한정된 기능을 가진 개별적이고 작은 구성 요소들을 합쳐서 하나의 커널 구성으로 만들게 됩니다. 개별적인 요소들이기 때문에 서로 영향을 끼치지 않고 자유롭게 생성 및 변경이 가능해집니다.
계층 구조에서는 운영체제가 여러 층으로 나뉘어지게 됩니다.
각 층은 하위층 연산을 호출할 수 있으며, 해당 연산이 어떻게 구현되는지 알 필요가 없습니다. 구현과 디버깅이 간단해지게 되는거죠. 하지만, 사용자 프로그램이 여러 계층을 통과해야 하기에 오버헤드가 발생할 수 있습니다.
마이크로커널
마이크로커널은 중요치 않은 구성 요소를 커널로부터 제거한 후에 해당 구성 요소들을 별도의 주소 공간에 존재하는 사용자 수준 프로그램으로 구현하여 운영체제를 구성하는 구조입니다.
즉, 더 작은 커널을 별도로 만든 구조입니다.
마이크로 커널은 운영체제 확장이 쉽습니다. 모든 새로운 서비스는 사용자 공간에 추가하기 때문에 커널을 변경할 필요가 없습니다. 변경하더라도 작은 커널에서 변경되기에 변경할 대상이 적습니다. 확장이 쉽기 때문에 다른 하드웨어로의 이식도 쉽습니다.
서비스 대부분이 사용자 프로세스로 수행되기 때문에 높은 보안성과 신뢰성을 제공하고 한 서비스가 잘못되어도 다른 부분에 영향을 주지 않습니다.
하지만, 마이크로커널은 가중된 시스템 기능 오버헤드 때문에 성능이 나빠질 수 있습니다.
마이크로커널은 두 개의 사용자 수준 서비스 통신 시에 별도의 주소 공간에 서비스가 존재하기 때문에 메시지 복사가 일어나게 됩니다. 또한, 메시지 교환을 하기 위해 한 프로세스에서 다음 프로세스로 전환해야 할 수도 있습니다.
이 과정에서 오버헤드가 발생하게 됩니다.
그렇다면, 오버헤드 문제를 해결할 수 없는건가요?
Mach 마이크로커널은 위의 오버헤드 성능 문제를 해결했습니다.
macOS, iOS에서 사용하는 커널 구성요소인 Darwin은 두 개의 커널(하이브리드 시스템)로 구성되어 있습니다. 두 개의 커널 중 하나가 바로, Mach 마이크로커널입니다.
Mach 마이크로커널은 Mach, BSD, I/O 키트, 모든 커널 확장을 단일 주소 공간으로 결합하여 오버헤드 성능 문제를 해결했습니다. 단일 주소 공간으로 결합했기에 메시지 전달 발생 시에 메시지를 복사할 필요가 없어졌습니다. 동일한 주소 공간에 있기 때문에 메시지 복사없이도 접근 가능하기 때문이죠.
따라서, 위에서 발생하는 오버헤드를 해결했습니다.
user mode? kernel mode?
커널 구조 이미지에서 보이는 user mode, kernel mode는 뭘까요?
컴퓨터 시스템에서 사용자와 운영체제는 시스템 하드웨어와 소프트웨어 자원을 공유하게 됩니다.
사용자와 운영체제 사이에서 자원이 공유되기 때문에 운영체제 코드 실행과 사용자-정의 코드 실행이 구분 가능해야 합니다. 즉, 두 개의 독립된 연산 모드가 필요합니다.
두 개의 독립된 모드는 사용자 모드(1)와 커널 모드(0)입니다.
처음 시스템 부트 시에는 ⓵ 커널 모드로 시작합니다. 커널 모드에서 운영체제를 적재하고 나면, ⓶ 사용자 모드에서 사용자 프로세스가 시작됩니다. 그러던 중에, 응용 프로그램에서 운영체제로 서비스 요청 즉, ⓷ 시스템 콜을 하게 되면 사용자 모드에서 커널 모드로 전환됩니다. 운영체제가 제어를 얻으면 커널 모드가 된다는 것만 기억하면 됩니다.
적재가능 커널 모듈
적재 가능 커널 모듈(LKM) 기법은 커널이 핵심적인 구성요소 집합을 가지고 있고 부팅, 실행 중에 부가적인 서비스를 모듈로 링크하는 방법입니다.
커널은 핵심 서비스를 제공하며, 다른 서비스는 커널 실행동안에 동적으로 구현됩니다.
서비스를 동적으로 링크하는 것은 새로운 기능을 직접 커널에 추가하는 것보다 바람직합니다. 직접 커널에 추가할 시에 수정 사항이 발생하면 커널을 다시 컴파일해야 하기 때문이죠.
적재가능 커널 모듈 기법은 커널의 각 부분이 정의되고 보호된 인터페이스를 가진다는 점에서 계층 구조와 비슷하지만 더 유연합니다. 또한, 중심 모듈이 핵심 기능만 가지고 있고, 다른 모듈의 적재 방법과 모듈들과 어떻게 통신하는지 안다는 점에서 마이크로커널과도 유사합니다. 하지만, 모듈 기법은 통신하기 위해서 메시지 전달을 하지 않아도 된다는 점에서 마이크로커널보다 효율적입니다.
운영체제의 작동
운영체제는 프로그램이 실행되는 환경을 제공합니다. 실행 환경은 여러 경로를 거쳐 구성되기 때문에 운영체제마다 큰 차이가 있습니다. 하지만, 그 속에서도 공통적으로 지니는 작동 방식들이 존재합니다.
이전 운영체제는 공통적으로 작업 요청의 일정량을 모아서 한꺼번에 처리했습니다.(일괄 처리)
이 방식은 작업이 완전히 종료될 때까지 기다려야 했기 때문에 중간에 사용자 개입이 허용되지 않았습니다. 따라서, 상호 작용할 수 없는 시스템이 되었고 CPU나 입출력 장치를 바쁘게 유지할 수 없었습니다.
CPU를 바쁘게 유지할 수 없다는 건, CPU 자원을 낭비하는 일입니다. 따라서, 여러 프로그램을 실행 가능하게 만들어서 CPU를 바쁘게 유지할 필요가 있었습니다.
다중 프로그래밍
CPU가 항상 한 개의 프로그램은 실행할 수 있도록 구성하여 CPU 이용률을 높이고 사용자 만족도도 높이는 방식입니다.
항상 한 개의 프로그램을 실행할 수 있도록 여러 프로그램들을 메인 메모리에 두어 CPU를 받을 준비를 시킵니다. 운영체제가 여러 프로세스를 동시에 메모리에 유지시키기 때문에, CPU가 유휴 상태에 들어가지 않고 프로세스를 계속 실행할 수 있습니다. 거듭 얘기하지만, CPU가 유휴 상태인 상황은 발생해선 안됩니다. I/O 실행동안 CPU가 다른 프로그램을 실행하게끔하여 유휴 상태에 들어가지 않도록 해야 합니다.
다중 프로그래밍에서는 여러 프로그램들이 동시에 실행되기 때문에 최종 결과물을 반환하는 시간이 줄어들고, 성능이 좋아집니다.
프로그램? 프로세스?
실행중인 프로그램은 프로세스로 불립니다. 프로세스는 CPU에 의해 명령이 실행되지 않으면 아무것도 할 수 없습니다. 이러한 프로세스는 프로그램이 아닙니다.
프로그램은 디스크에 저장된 파일의 내용과 같은 수동적인 개체지만, 프로세스는 다음 수행할 명령을 지정하는 프로그램 카운터를 가진 능동적인 개체이기 때문입니다.
다중 태스킹
CPU가 여러 프로세스를 전환하며 프로세스를 실행하는 방식입니다.
프로세스 간의 전환이 자주 발생하기 때문에 사용자에게 빠른 응답 시간을 제공해줍니다. I/O 실행동안에 CPU를 쉬지 않고 다른 사용자 프로그램으로 신속하게 전환하여 실행하게 합니다.
다중 태스킹에서는 운영체제가 적절한 응답 시간을 보장해주어야 합니다. 적절한 응답 시간을 보장하는 일반적인 방법이 바로, 가상 메모리입니다. 이 기법을 사용하게 되면 프로그램이 물리 메모리의 크기보다 커도 괜찮습니다. 따라서, 메모리 저장 장치 한계로부터 자유로워지게 됩니다.
시분할 시스템
시분할 시스템은 CPU가 처리해줄 수 있는 시간을 작업 수에 맞춰 분할합니다.
시간을 각자 일정량만큼 분배하여 번갈아가며 처리하게 됩니다. 번갈아가며 처리해야 하기 때문에 분배받을 프로그램들이 같이 메모리에 올라와 있어야 합니다. 즉, 다중 프로그래밍이 전제되어 있어야 합니다. 그래야지만, CPU를 다음 프로그램으로 바로바로 넘길 수 있습니다.
CPU는 타임을 나눠서 프로그램 한 개를 잠깐동안 실행하게 됩니다. CPU가 잠깐이라도 해당 프로그램을 실행하기 때문에 모든 사용자는 CPU가 나만을 위해서 일했다고 생각하게 됩니다.
시분할 시스템에서 사람이 느끼기에 빠르다고 느끼면서 동시에 주어진 자원을 최대한 활용하는 것이 중요합니다. 단순히 정확한 시간을 지키는 것이 시분할 시스템의 목적이 아닙니다.
운영체제의 자원 관리
운영체제는 CPU, 메모리 공간, 파일-저장 공간 및 I/O 장치 같은 자원의 관리자 역할을 합니다.
자원의 관리자로서 어떤 일을 하는지는 계속 진행되는 챕터에서 자세하게 알 수 있지만, 이번 섹션에서 어떤 일을 하는지 대충 훑고 가봅시다.
CPU 관리자
위에서 프로그램을 CPU가 실행하기 위해서는 메인 메모리에 있어야 한다는 얘기를 했습니다. 그렇다면, 메인 메모리에 있는 프로그램 중에 어떤 프로그램을 CPU에 할당해야 할까요?
어떤 프로그램을 CPU에 할당할지 CPU 스케줄링을 하는 것도 운영체제의 역할 중 하나입니다.
먼저 온 순서대로 처리하게 되면 하나의 프로그램이 오랜 시간 CPU를 점유하게 되어서 다른 프로그램들이 하염없이 기다려야 하는 상황이 생길 수 있습니다. 예를 들어서, 먼저 온 프로그램이 가장 오랜 시간동안 CPU를 점유하는 프로그램이라면, 이미 실행을 마쳤을 수도 있는 프로그램들이 하염없이 기다리는 상황이 생깁니다. CPU라는 자원을 효율적으로 사용하지 못한 방식입니다.
따라서, CPU를 가장 짧게 사용할 프로그램에게 먼저 CPU를 주는 방식으로 문제를 해결할 수 있게 됩니다.
짧게 사용할 프로그램들이 먼저 CPU를 점유하게 되면 나머지 프로그램들의 응답시간이 짧아지기 때문에 평균적인 성능이 좋아집니다.
메모리 관리자
메인 메모리는 CPU와 입출력 장치에 의해 공유되는, 빠른 접근이 가능한 데이터 저장소입니다.
CPU가 데이터를 처리하기 위해서는 메인 메모리로 데이터가 먼저 전송되어야 합니다. 즉, CPU에서 명령을 수행하게 만들기 위해선 명령이 메인 메모리에 있어야 하는거죠.
CPU 이용률, 컴퓨터 응답 속도 개선을 위해서는 메모리에 여러 개의 프로그램을 유지해야 합니다. 하지만, 메모리의 공간은 한정되어 있고, 여러 프로그램들이 동시에 메모리에 올라가고 싶어할 수도 있습니다.
운영체제는 어떤 프로그램에게 메모리 공간을 얼만큼 할당해주어야 하나요?
프로그램에게 메모리 공간을 할당해주는 것도 운영체제의 역할 중 하나입니다.
공평하게 N분의 1의 공간을 프로그램에게 나눠준다면 어떨까요? 이 방식은 프로그램이 점점 많아지게 된다면 각 프로그램이 가지는 메모리 공간이 점점 작아져서 프로그램을 원활하게 실행할 수 없게 만듭니다.
그렇기 때문에, 일부 프로그램만 어느 정도 메인 메모리 공간에 할당하고 나머지 프로그램은 전부 디스크로 쫓아내어야 합니다. 메인 메모리 공간에 있는 프로그램이라도 CPU에서 원활하게 실행하기 위해서 입니다.
그럼, 어떤 프로그램들이 메모리에 올라와 있어야 하고, 어떤 프로그램들이 디스크로 내려가야 할까요?
이 부분도 운영체제가 해주는 역할입니다. 뒤에서 더 자세하게 알아볼게요.