운영체제는 시스템 내의 자원을 효율적으로 사용하고, 사용자가 편리하게 컴퓨터를 사용할 수 있게 하기 위해 만들어졌다.
크게 나누면 운영체제는 CPU, Memory, I/O장치들을 관리 한다.
CPU:
-
현대 컴퓨터는 multi-process를 기반으로 되어있다. 따라서 Memory 위에 동시에 여러개의 프로세스가 올라가게 되고, 운영체제는 어떤프로세스에게 어떻게 CPU를 배당해야 효율적인 시스템이 될 것인가에 대한 문제를 처리한다.
여기서, 각각의 프로세스들은 CPU를 사용하는 패턴이 모두 다르기 때문에, CPU 스케줄링필요하게 된다. CPU 스케줄링 방식은 비선점형 (nonpreemptive)와 선점형(preemptive)로 나뉜다. 두 방식의 차이는 cpu를 자발적으로 반납할 때 까지 기다리느냐, 강제로 빼앗느냐에 따라 달라진다. -
CPU를 사용하고 있던 프로세스가 다른 프로세스에 cpu를 넘겨줄 때, 문맥 교환 (context switch)이 발생하게 된다. 여기서 문맥이란 cpu가 연산하던 프로세스의 상태 값으로, PC(program counter), register 값 등이 포함된다. 연상중이던 프로세스의 문맥은 블록 형태(PCB)로 운영체제 커널 데이터 영역에 저장된다.
CPU를 넘겨 받는 프로세스는 PCB에서 자신의 문맥 값을 꺼내와 본래 진행하던 연산을 계속해서 이어나가게 된다.
Memory:
-
운영체제는한정된 메모리 공간을 어떻게 나누어 사용하는가에 대한 문제를 관리한다.
-
운영체제의 핵심 영역이라 할 수 있는 커널 영역은 항상 메모리에 상주하게 된다. 운영체제는 메모리의 어느 부분이 어떤 프로그램에 의해 사용되고 있는지 같은 정보들을 주소를 통해 관리하기도 하고, 각각의 프로세스가 함부로 다른 프로세스의 영역을 침범하지 못하게도 하고 입출력 요청 등의 권한이 필요한 명령어를 사용자 프로그램에게서 System call을 받아 대신 처리하기도 한다.
-
프로세스란 실행중인 프로그램이라는 의미이며 메모리위에 올라와있는 프로그램들을 말한다. 각각의 프로그램들은 모두 0부터 시작하는 가상 메모리를 지닌다. 사실상 CPU가 읽는 주소 값들도 가상 메모리의 주소들이다.
프로그램이 아무리 크더라도 프로그램은 가상 메모리를 기준으로 실행되기 때문에, 물리적 메모리의 크기에 상관 없이 실행이 가능하다. 이런 방법이 가능한 이유는 가상메모리를 작게 나누어서 사용하는 영역만 물리적 메모리에 올리고, 사용하지 않는 영역은 Swap out 시켜 보조저장장치의 Swap영역에 내리게 된다. Swap영역에 저장된 부분은 사용될 때 다시 Swap in 되어 물리적 메모리에 올라가게 된다. 작게 나누어진 가상 메모리가 물리적 메모리에 올라가기 위해선 물리적 메모리 주소와 Mapping 되어야 한다. 여기에는 대표적으로, 일정 단위로 나누어 맵핑하는 Paging 기법과 의미단위로 나누어 맵핑하는 Segmentation 기법이 있다. -
프로세스는 더 작은 실행단위인 Thread(a light process)를 가진다. Thread는 기본적으로 프로세스의 데이터를 서로 공유하기 때문에(cpu 이용 정보는 별도로 지닌다) Thread 간에 협력이 용이하고 오버헤드도 없다.
Thread와 비교하면 프로세스는 각자의 고유 주소를 지니기 때문에, 프로세스간 협력을 위해선 서로 직접 링크를 연결해주거나, 메일박스를 두어 통신하는 등 번거로운 일이 생긴다. 또 프로세스간 스위치가 발생할 때 문맥교환은 케쉬메모리가 flush 되는 등의 상당한 오버헤드가 발생하기 때문에 성능 저하가 발생할 수 있다.
이처럼 Thread는 상당한 장점을 지니지만, 하나의 Thread가 죽으면 프로그램 전체가 죽을 수 있는 리스크를 지닌다.
I/O Devices
-
입출력 장치는 조금 특이한 방식으로 관리된다. 입출력 장치가 CPU 사용을 원할 때는 신호를 보내게 되는데, 이를 인터럽트라고 한다. 인터럽트가 발생하게 되고 cpu가 인터럽트를 감지하면 cpu는 하던 일을 잠시 멈추고 인터럽트 요청을 처리한 후에 본래 하던 일로 되돌아 간다.
-
여기서, 동기 처리방식으로 인터럽트를 처리한다면 cpu가 처리할 때 연산되고 있던 프로세스는 봉쇄되고 I/O요청 처리를 기다리게 된다. CPU와 입출력 장치들 간에 처리속도가 상당히 많이 나기 때문에 CPU가 그냥 기다린다면 엄청난 비효율이 발생하게 된다.
따라서 이때는 다른 프로세스가 cpu를 이용하게 되고, I/O요청 처리가 끝나면 본래 프로세스가 다시 cpu를 이용하게 된다. 비동기로 처리된다면, 프로세스는 봉쇄 되지않고 I/O요청 처리가 필요없는 작업부터 연산된다. -
개인적으로 동기,비동기를 이해하는데 가장 와닿았던 설명은 왜 파이어베이스가 비동기를 기반으로 만들어졌는가에 대한 글이었다.
Why are the Firebase API asynchronous? 대충 요약해보면, 안드로이드를 예를 들어, 안드로이드 같은 경우는 UI를 조작할 수 있는 것은 메인쓰레드 하나뿐이다. 따라서 메인쓰레드가 Block된다면 반응이 느린다던가 하는 사용자로선 좋지 않은 경험을 하게된다.
파이어베이스 같은 경우에는 요청을 하고 Callback을 받는 형식으로 구현되는데, 리턴값을 바로 받아도, 콜백함수가 바로 실행되지 않고 메인쓰레드 다음에 큐에 들어가 최대한 메인쓰레드를 건드리지 않는다. 만약 동기 방식으로 구현됐다면, 쓰레드를 블락시키고, 바로 Callback을 받는 구조가 되었을 것이다. -
인터럽트를 너무 자주 발생시키면 오버헤드가 큰 문맥교환이 자주 일어날 수 있고, cpu 사용 효율이 떨어질 수 있다. 이를 보완하기 위해 있는 것이 DMA인데, 일반적으로 메모리에 직접 접근할 수 있는 것은 cpu뿐이지만 DMA는 메모리 접근이허락된다.
인터럽트가 발생하면, 바로 cpu에게 전달하지 않고, DMA가 블록 단위로 메모리에 올린 후, cpu에 인터럽터를 전달함으로서 cpu가 인터럽터를 받는 경우를 줄인다.