brunch

You can make anything
by writing

C.S.Lewis

by 정주홍 Aug 17. 2017

개념 정리 - (7) 운영 체제 편

우리가 배운 개념이 어디서 어떻게 쓰이는지 알아보자

운영 체제 또는 오퍼레이팅 시스템(Operating SystemOS)은 시스템 하드웨어를 관리할 뿐 아니라 응용 소프트웨어를 실행하기 위하여 하드웨어 추상화 플랫폼과 공통 시스템 서비스를 제공하는 시스템 소프트웨어이다. 최근에는 가상화 기술의 발전에 힘입어 실제 하드웨어가 아닌 하이퍼바이저 위에서 실행되기도 한다. 학교 강의에서 교수님께서는 운영 체제는 리소스 관리자라고 많이 말씀하셨던 기억이 난다. CPU, 메모리, 보조 기억 장치, 네트워크 등의 자원들을 잘 관리하여 응용 소프트웨어들에게 제공해주는 역할을 하는 소프트웨어인 것이다. 

하드웨어 추상화 플랫폼 또한 중요한 개념이다. 추상화는 프로그래밍에 있어 매우 중요하고 유용한 개념이다. 응용 소프트웨어를 개발할 때 파일 접근이 필요하면 보조 기억 장치가 어떤 장치인지 몰라도 이용할 수 있고, 물리 메모리가 부족하여 스왑핑이 일어나야 하더라도 직접 보조 기억 장치에 페이지 내용을 복사하는 작업 등을 할 필요가 없다. 단지 OS에게 요청하기만 하면 알아서 잘 처리된다.



개요

아래 사진은 OS가 제공하는 몇 가지 주요 서비스에 대해 그림으로 나타낸 것이다. 사용자가 운영 체제와 맞닿아 있는 부분을 사용자 인터페이스(User interface)라고 하며 GUI 또는 CUI로 나뉜다. 실제 서비스를 받기 위한 방법으로 시스템 콜(System calls)들이 제공된다. 윈도우에서는 WinAPI라는 이름으로 불린다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/2_Structures.html


학부 운영 체제 강의는 크게 프로세스 관리, 메모리 관리, 스토리지 관리, 보호와 보안 네 부분을 배운다. 네트워크도 중요하지만 다른 강의에서 따로 다루기 때문에 주요 부분에서 제외되어 있다. 보통 리눅스를 바탕으로 설명하는데, 각 부분이 리눅스에서 어떻게 운영되고 있는지 설명하는 식이다. 프로세스 관리에서는 현대 운영 체제의 기본적인 기능인 멀티프로그래밍(multiprogramming)을 지원하기 위한 방법들을 다루고, 메모리 관리에서는 거의 대부분이 가상 메모리에 대한 내용이다. 스토리지 관리에서는 대용량 스토리지들의 유형과 파일 시스템에 대해 다루고 보호와 보안에서는 보호의 방식과 암호화 등의 내용을 다룬다. 운영 체제가 하는 일이 많기 때문에 한 학기 동안의 강의에서 모든 내용을 깊게 다루기는 어려우므로 여러 분야를 얉게 알아보는 편이다.



프로세스 생애주기

프로세스는 프로그램의 인스턴스로 운영 체제에서 가장 기본적인 실행 단위이다. 각 프로세스는 메모리를 차지하고, 일정 상태 주기를 가진다. 아래 사진은 프로세스의 메모리 구조를 간략하게 나타낸 것이다. 어떻게 프로그램을 실행되게 만들었느냐에 따라 다르지만 보통 지역 변수나 함수 호출로 인한  같은 것은 스택 영역에서 메모리가 할당되고, 동적 메모리 할당은 힙 영역에서 메모리가 할당된다. 

출처 : https://commons.wikimedia.org/wiki/File:Process-in-memory.jpg

프로세스는 보통 5가지 상태 중 하나의 상태로 존재한다. 처음 프로그램을 실행하기 위해 OS에게 요청하면 프로세스를 new 상태로 생성하고 실행 가능한 상태가 되면 ready 상태가 된다. ready 상태의 프로세스는 스케쥴러에 의해 running 상태가 되어 실행되다가 I/O 혹은 이벤트 대기에 의해 waiting 상태가 되거나 인터럽트에 의해 ready 상태가 된다. waiting 상태였던 프로세스는 I/O나 이벤트가 완료됨에 따라 다시 ready 상태가 된다. 실행 중이던 프로그램이 종료되면 terminated 상태가 되고, 곧 OS에 의해 자원이 회수된다. 

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/3_Processes.html

위의 프로세스 메모리 구조는 프로세스 자체의 인스턴스이고, 운영 체제에서는 각 프로세스를 관리하기 위한 PCB(Process Control Block)이라는 별도의 자료 구조로 프로세스들을 관리한다. PCB에는 프로세스가 실행되는 동안 필요한 정보인 Program Counter나 레지스터 값들, 가상 메모리를 위한 페이지 테이블, 열었던 파일 목록 등이 포함된다. 아래 사진은 프로세스 실행 도중 다른 프로세스가 실행되는 흐름을 나타내는 다이어그램이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/3_Processes.html



스케쥴링(Scheduling)

앞서 현대 OS의 기본적인 기능으로 멀티프로그래밍이 있다고 하였다. 싱글 코어 프로세서일 경우 동시에 여러 프로그램을 실행할 수 없기 때문에 실제로는 시분할을 통해 각 시간 분할마다 다른 프로그램을 실행하는 것으로 동시에 여러 프로그램을 실행하고 있는 것 같은 효과를 준다. 이때 어떤 프로그램이 프로세서에 의해 실행되게 할지 결정해주는 역할을 하는 것이 스케쥴러이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/3_Processes.html

스케쥴러는 크게 장기 스케쥴러(Long-term scheduler), 단기 스케쥴러(Short-term scheduler)로 나뉜다. 장기 스케쥴러는 새로 프로세스를 실행하는 것과 같이 덜 주기적이어도 되는 작업에 적용되며, 훨씬 복잡한 알고리즘으로 만들어질 수 있다. 단기 스케쥴러는 실행 중이던 프로세스를 멈추고 다른 프로세스를 실행하려는 것과 같이 매우 짧은 주기로 실행되는 스케쥴러이다. 매우 자주 실행되기 때문에 단순한 알고리즘이어야 한다. 어떤 프로그램은 I/O 사용이 많고, 어떤 프로그램은 CPU 사용이 많은 프로그램이기 때문에 둘을 적절히 섞어서 실행해야 CPU를 최대한 활용할 수 있다.

어떤 프로세스가 실행되다가 다른 프로세스가 실행되어야 할 때 문맥 전환(Context switching)이 일어난다. 문맥 전환을 하고 나면 완전히 다른 프로세스가 실행 가능해야 한다. 어떻게 이것이 가능할까 싶지만 Program Counter를 포함한 레지스터 값들과 파이프라이닝 장치들, 컨트롤 장치의 값 등이 복구되면 완전히 다른 프로세스를 실행하는 게 된다. 그러나 값들을 다시 복사해야 한다는 점에서 적지 않은 오버헤드다. 문맥 전환은 매우 매우 자주 일어나기 때문에 빠르면 빠를수록 좋다.

스케쥴링 방법은 선점형(preemtive)과 비선점형(non-preemtive)으로 나뉜다. I/O 작업이나 wait 시스템 콜로 waiting 상태가 되는 경우 비선점형 스케쥴링 방식이라고 하며, 프로세스가 실행 중에 timeout 등으로 인해 waiting 상태로 변경될 수 있다면 선점형 스케쥴링이라고 한다. Dispatcher는 ready 상태의 프로세스 중 하나를 dispatch 하여 running 상태로 만든다. 

스케쥴링 방법은 여러 가지 존재할 수 있으며, 각 방법을 평가할 수 있기 위해서 평가 척도가 필요하다. 여러 가지 평가 척도가 있겠지만 CPU 사용량(CPU utilization), 단위 시간당 완료하는 프로세스량(Throughput), 소요 시간(Turnaround time), 대기 시간(Waiting time), 응답 시간(Response time) 정도를 포함한다. 각 스케쥴링 알고리즘에 대한 자세한 설명은 각 링크를 참고.


비선점 프로세스 스케줄링

FCFS 스케줄링(First Come First Served Scheduling)

SJF 스케줄링(Shortest Job First Scheduling)

HRRN 스케줄링(Highest Response Ratio Next Scheduling)


선점 프로세스 스케줄링

RR 스케줄링(Round Robin Scheduling)

SRTF 스케줄링(Shortest Remaining-Time First Scheduling)

다단계 큐 스케줄링(Multilevel Queue Scheduling)

다단계 피드백 큐 스케줄링(Multilevel Feedback Queue Scheduling)

RM 스케줄링(Rate Monotonic Scheduling)

EDF 스케줄링(Earliest Deadline First Scheduling)



프로세스 연산들

주요 프로세스 연산은 역시 프로세스 생성과 종료이다. 리눅스에서 프로세스를 생성하는 방법은 보통 fork류 시스템 콜을 호출하여 프로세스를 복사한 뒤, 자식 프로세스로서의 실행을 하거나 exec류 시스템 콜을 호출하여 다른 프로그램으로 바뀌어 실행하는 방법이다. 다른 방법들은 세부적인 사항에서 몇 가지 차이가 있겠지만 큰 흐름상의 차이는 없다. fork 시스템 콜 호출 뒤 자식 프로세스는 fork 시스템 콜 다음부터 실행된다. 만약 부모 프로세스가 자식 프로세스의 종료를 기다리고 싶다면 wait 시스템 콜을 호출하면 된다. main 함수가 종료되기 전에 임의로 프로세스를 종료하고 싶다면 exit 시스템 콜을 호출한다. exit 시스템 콜은 정수 인자를 받는다. 이는 main 함수에서 반환하는 값과 같은 의미인데 프로세스의 종료 결과를 알려주는 방법이다. 예를 들어, 프로세스가 0을 반환하고 종료하지 않은 경우 에러가 발생했다는 의미가 약속되어 있다.



프로세스 간 통신

종종 여러 프로세스가 협업할 필요가 있는 경우가 있다. 예를 들어 브라우저는 렌더러(Renderer) 프로그램과 각 플러그 인 별 프로세스를 가지는 프로세스다. 렌더러 프로세스는 HTML/CSS를 읽어 렌더링하고 인터프리터로 JS 코드를 실행해야 한다. 각 역할별 프로그램을 분리하여 모듈화 한 것이다. 이런 실행 환경에서는 프로세스 간의 통신이 가능해야 한다.

프로세스 간 통신(Interprocess Communication)은 운영 체제의 도움이 필요하다. 프로세스는 보안 등의 문제로 각자 독립된 메모리 공간을 가지기 때문에 단순 메모리 값 변경은 다른 프로세스에게 데이터를 전달하는 방법으로 사용될 수 없다. 

첫 번째로 공유 메모리(Shared-Memory Systems)를 사용하는 방법이 있다. OS에게 어떤 영역의 메모리를 다른 프로세스와 공유하고 싶다는 요청을 하는 것이다. 두 번째로 메세지 전달(Message-Passing Systems)을 사용하는 방법이 있다. 프로세스와 프로세스 간에 메세지를 전달할 수 있는 방법을 만드는 것이다. 메세지 전달 방법은 OS에서 내부적으로 메세지 큐를 두는 방법, 파이프를 만드는 방법, 소켓을 이용하는 방법 등 여러 가지 방법으로 구현될 수 있다. 



쓰레드(Threads)

쓰레드는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위이다. 쓰레드마다 다른 실행 흐름을 가지기 때문에 각자 레지스터 집합과 스택 영역을 가지며 힙 영역은 공유된다. 여러 쓰레드가 동시에 실행하는 방법은 I/O 작업과 같이 시간이 오래 걸리는 작업 중에 CPU 사용이 많은 작업을 하고 싶을 때와 같은 상황에 유용하게 쓰일 수 있다. 또한, 서버 프로그램과 같이 워커(Worker)가 여럿 필요한 경우에도 유용하다. 만약 멀티 프로세서 환경에서 어떤 작업들이 서로 의존성이 없다면 동시에 실행함으로써 처리 속도를 향상할 수도 있다. 쓰레드는 실행 흐름은 독립적이지만 메모리 공간은 공유하고 있기 때문에 경량 프로세스(Light Weight Process)라고 부르기도 한다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html

멀티쓰레딩을 지원하는 방법으로는 다대일, 일대일, 다대다 방법이 있다. 일반적으로 사용자가 생성하는 쓰레드는 유저 수준 쓰레드라고 분류하고 실제 시스템에서 실행되는 쓰레드를 커널 수준 쓰레드라고 하는데, 유저 수준 쓰레드와 커널 수준 쓰레드를 어떤 방식으로 맵핑하느냐의 차이이다. 다대일 모델은 여러 유저 수준 쓰레드를 하나의 커널 수준 쓰레드에 사상하고, 일대일 모델은 각 유저 수준 쓰레드마다 커널 수준 쓰레드에 사상한다. 다대다 모델은 여러 유저 수준 쓰레드를 여러 커널 수준 쓰레드에 사상한다. 한 유저 수준 쓰레드에서 blocking 연산을 하는 경우 다른 모든 유저 수준 쓰레드가 멈춘다는 점에서 다대일 모델은 통용되기 어렵다. 커널 수준 쓰레드는 생성하기에 오버헤드가 있으므로 일대일 모델보다는 현실적으로 다대다 모델이 사용된다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html

Java와 같이 VM 위에서 실행되는 환경이 아니라면 쓰레드를 생성하는 방법도 고려할 점이 된다. POSIX 표준을 지키는 OS라면 표준 pthread 라이브러리를 이용하면 된다. 윈도우에서는 WinAPI에서 제공되는 쓰레드 생성 API를 이용해야 한다. 쓰레드도 프로세스와 마찬가지로 다른 쓰레드의 종료를 기다리는 함수 등을 제공한다.

쓰레드 개념이 추가되면서 고려해야 할 점들이 생긴다. 만약 fork나 exec 시스템 콜을 호출하는 경우 쓰레드는 어떻게 되어야 하는가? 프로세스에 신호(signal)가 전달되는 경우 어떤 쓰레드가 전달받도록 해야 하는가? 쓰레드 취소는 즉시 되어야 하는가? 쓰레드에 대한 스케쥴링은 어떻게 이루어져야 하는가? 이러한 점들은 시스템마다 다르게 구현된다.



동기화(Synchronization)

프로세스 내에 여러 실행 흐름이 존재할 수 있게 되면서 동기화 문제가 대두된다. 여러 쓰레드가 공유 자원을 동시에 접근하려는 경우 발생하는 경쟁 상태(Race condition) 때문이다. 이러한 경쟁 상태를 일으키는 코드 영역을 임계 구역(Critical section)이라고 한다. 예를 들어 두 쓰레드가 한 변수에 서로 다른 값을 동시에 대입하려 한다면 문제가 생기게 된다. 

경쟁 상태가 일어나지 않게 하기 위해서는 다른 쓰레드가 이미 작업 중인 경우 작업이 끝날 때까지 기다릴 수 있도록 지원해줘야 한다. 현실적으로 별도의 하드웨어 지원을 받지 않는 이상 어떤 알고리즘도 경쟁 상태로부터 자유로울 수 없다. 하드웨어 지원을 통해 어떤 변수의 값 수정을 원자적으로 가능하게 함으로써 뮤텍스 록(Mutex locks)이나 세마포어(Semaphore)와 같은 해결 방법들을 제안한다. 

그러나 록킹 방법은 데드록(Deadlock)을 일으킬 수 있다. 서로 공유 자원을 점유하고 있는 상황에서 서로의 공유 자원을 얻으려 할 때 데드록이 발생하며, 이러한 문제로 가장 유명한 것이 식사하는 철학자들 문제다. 데드록에 대한 해결 방법이 몇 가지 제시되는데, 크게 예방, 회피, 감지, 복구 방법이 있다. 자세한 내용은 다음 문서를 참고.

록을 얻는 방법은 결국 잠재적으로 데드록이 일어날 가능성을 가진다. 프로그래머가 실수를 하게 되는 경우 데드록이 발생할 수 있기 때문이다. 모니터(Monitor) 방법은 한 번에 하나의 프로세스만 모니터에서 활동하도록 보장해준다. 어떤 공유 데이터에 대해 모니터를 지정해놓으면, 프로세스는 그 데이터를 접근하기 위해 모니터에 들어가야만 한다. 즉, 모니터 내부에 들어간 프로세스에게만 공유 데이터를 접근할 수 있는 기능을 제공하는 것이다. 또한 프로세스가 모니터에 들어가고자 할 때 다른 프로세스가 모니터 내부에 있다면 입장 큐에서 기다려야 한다. Java에서 synchronized를 명시한 메서드가 모니터의 실제 사례 중 하나라고 볼 수 있다.




프로그램 로딩

모든 프로세스는 메모리에 로드하여 실행한다. 현대 OS에서 거의 모든 작업은 메모리와 연관되기 때문에 메모리 관리를 잘 하는 것이 중요하다는 것은 굳이 더 설명할 필요 없을 것이다. 만약 하나의 프로그램만이 실행되는 시스템이라면 물리 메모리 전체를 그 단일 프로그램이 모두 사용해도 되겠지만, 현실은 수많은 프로그램을 동시에 실행하는 환경이다. 또한, 어떤 프로세스가 함부로 커널 메모리 영역을 건드린다던가 하는 일은 없어야 할 것이다. 따라서 프로세스는 각자 독립적인 메모리 공간을 갖고자 한다. 그전에 프로그램의 실행 과정을 살펴보면서 메모리 관리를 어떻게 해야 할지 생각해보자.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/8_MainMemory.html

고수준 언어로 작성된 소스 코드는 컴파일러와 어셈블러에 의해 목적 프로그램으로 컴파일된다. 위 사진에서 object module이 컴파일된 단계에 해당된다. 목적 프로그램은 linkage editor라는 프로그램에 의해 여러 다른 목적 프로그램과 연결된다. 예를 들어 각 소스 파일별로 목적 프로그램이 만들어지게 되는데, 다른 소스에 있는 함수를 참조하려면 링킹(linking)이 필요한 것이다. 연결이 완료되고 나면 loader라는 프로그램에 의해서 실제로 물리 메모리에 로딩된다. 이때, 동적 로드하기로 되어 있는 시스템 라이브러리 등이 동적 링킹 된다. 최종적으로 로딩된 프로세스는 CPU에 의해서 실행된다.



주소 변환

앞서 말한 이유로 인해 프로세스에서 물리 메모리로 직접 접근 가능하도록 하지는 않는다. 가장 기본적인 주소 변환 방법은 base와 limit 레지스터를 이용한 하드웨어를 사용하는 방법이다. 어떤 주소에 대해 base보다 높고 base + limit보다는 낮은 주소를 참조해야만 하도록 하는 하드웨어다. 만약 base보다 낮은 주소나 base + limit보다 높은 주소를 참조하려고 하면 trap을 작동시켜 에러가 나도록 한다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/8_MainMemory.html



스왑핑(Swapping)

만약 물리 메모리에 공간이 부족하다면 어떡할까? 실행 중이던 프로그램이 손상되어선 안 되므로 OS에서는 스왑핑이라는 기법으로 실행 중이던 프로그램의 메모리를 보조 기억 장치에 저장시키고 메모리를 비운 뒤 사용하는 방법을 이용한다. 보조 기억 장치는 매우 느리기 때문에 스왑핑이 일어나는 것은 성능에 큰 영향을 끼친다. 예를 들어 프로세스가 전체 물리 메모리를 다 사용하도록 하는 시스템이라면 다른 프로세스가 실행될 때마다 물리 메모리 전체를 보조 기억 장치에 복사하는 작업을 수행해야 할 것이다. 하드 디스크가 20MB/s 정도의 쓰기 속도를 보이므로 4GB 메모리를 복사한다고 하면 약 200초가 걸린다. 이건 당연히 말이 안 된다.

그러나 물리 메모리가 언제나 여유 있는 것은 아니므로 물리 메모리의 내용을 보조 기억 장치에 저장시키는 스왑핑 기법 자체는 매우 유용하다. 그렇다면 스왑핑이 물리 메모리 전체에 대해서가 아닌 일부에 대해서만 일어나게끔 개량하는 방법을 생각할 수 있을 것이다.



연속적인 메모리 할당

메모리 할당 방법에 대해 생각해보자. 보통 메모리를 딱 한 워드만 요구하지는 않기 때문에 연속적인 공간을 할당해줄 수 있어야 한다. 물리 메모리에 여유 공간이 있다고 할 때 어떤 공간을 할당해줘야 할까? 크게 First fit, Best fit, Worst fit 세 가지 방법이 있다. First fit은 여유 공간 중 첫 번째로 충분한 공간을 선택해서 할당하는 방법, Best fit은 딱 맞는 여유 공간을 선택해서 할당하는 방법, Worst fit은 가장 여유 있는 공간을 선택해서 할당하는 방법이다. 시뮬레이션에 의하면 First fit이나 Best fit 방법이 시간과 스토리지 사용량에 대해서 나은 결과를 보였다고 한다. First fit과 Best fit 둘 다 스토리지 사용량은 비슷하나 First fit이 할당할 공간을 더 빠른 시간 내에 찾았다. 따라서 First fit 방법을 이용하는 게 낫다고 볼 수 있을 것이다.

할당 방법에 따라 파편화가 생긴다. 어떤 공간에 메모리를 할당하게 되면 할당한 공간을 제외한 부분이 남게 된다. 이것을 파편화라고 하는데, 항상 딱 맞게 할당하는 것이 아니기 때문에 곳곳에 파편화된 메모리 공간들이 존재하게 된다. 언젠간 파편화가 너무 심해져서 더 이상 할당을 할 수 없게 되기 때문에 압축(compaction) 작업은 불가피하다.



세그먼테이션(Segmentation)

어차피 사용자들은 프로그램이 메모리에 연속적으로 존재한다고 생각하지 않는다. 오히려 각 함수나 변수마다 하나의 메모리 조각(segment)으로 생각하는 경향이 있다. 예를 들어 C 컴파일러는 유저 코드, 라이브러리 코드, 전역 변수, 스택, 힙을 각각의 조각으로 분리할 수도 있다. 이러한 관점과 앞서 기본적인 주소 변환 하드웨어를 결합한 것이 세그먼테이션 방법이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/8_MainMemory.html

세그먼테이션 방법에서는 각 세그먼트마다 limit과 base가 존재하여 메모리 곳곳에 메모리 조각을 둘 수 있다. 그러나 세그먼테이션 방법에서도 각 메모리 조각은 연속된 공간에 할당되어야 하며, 그에 따라 파편화 문제는 해결되지 않는다.



페이징(Paging)

페이징 방법은 연속적인 할당조차 필요 없게 만들어준다. 페이지 테이블을 구성하고 메모리를 페이지라는 단위로 나누어 논리 주소를 물리 주소로 변환한다. 메모리 주소의 앞쪽 bit들은 페이지 번호로, 뒤쪽 bit들은 페이지 내의 offset으로 사용된다. 

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/8_MainMemory.html

페이지를 찾는 작업은 메모리에 접근할 때마다 발생하므로 매우 빠르게 수행되어야 한다. 이를 위해 하드웨어 지원을 받는데, TLB 캐시를 둠으로써 주소 변환을 빠르게 한다. 만약 TLB에 접근하는데 20 나노초가 걸리고 메인 메모리에 접근하는데 100 나노초가 걸리며 TLB 히트율이 80%라고 하면 데이터를 가져오는데 평균적으로 0.80 * 100 + 0.20 * 200 = 120 나노초가 걸린다. 이는 20% 정도 성능 저하가 된다는 것을 뜻하고, 만약 TLB 히트율이 99%가 되면 101 나노초만이 걸리는 것으로 오버헤드가 매우 적음을 알 수 있다.

페이지 테이블에는 유효 bit를 둔다. 프로세스가 모든 메모리를 항상 참조하는 것은 아니기 때문에 어떤 페이지는 물리 메모리에서 내려둘 수도 있다. 이때는 원래 페이지가 다른 프로세스에 의해 점유될 것이므로 그 메모리 보호를 위해서는 페이지 테이블의 유효 bit만을 고쳐주면 된다. 만약 접근하려는 페이지의 유효 bit가 유효하지 않다고 설정되어 있다면 해당 페이지만을 다시 스왑핑하면 될 것이다. 이는 요구 페이징과도 연관된다.

또한, 페이지를 공유할 수 있다. 시스템 라이브러리와 같이 거의 대부분의 프로세스들이 공유하게 될 메모리는 애초에 공유 페이지로 설정되어 굳이 프로세스마다 중복되게 페이지를 로드하지 말고 이미 로드되어 있는 메모리를 참조하게 하는 것으로 메모리를 절약한다.



페이지 테이블 구조

현대 OS는 최소 주소를 32bit로 표현한다. 페이지 크기는 보통 4KB 정도로 설정하는데, 그러면 페이지 테이블은 2의 20승만큼의 항목을 갖게 되고, 항목당 4byte만을 가진다고 해도 페이지 테이블의 크기는 4MB가 된다. 이는 또다시 1024개의 페이지로 구성되어야 함을 뜻한다. 프로세스가 전환될 때마다 페이지 테이블도 전환되어야 하는데 한 번에 이렇게 큰 용량을 전환하는 것은 비용이 너무 크다.

페이지 테이블을 두 단계로 나누는 방법이 있다. 첫 10bit를 외부 페이지 주소로, 다음 10bit를 내부 페이지 테이블 주소로 설정하여 일부 내부 페이지 테이블만을 로드하는 방법이다. 64bit 운영 체제는 52bit를 페이지 번호로 지정하는데, 이 경우 두 단계로 페이지 테이블을 나눈 것도 너무 크기 때문에 세 단계로 나누기도 한다.

다른 접근 방법으로는 페이지 번호를 해싱하여 해시값을 페이지 주소로 사용하는 방법이 있다. 또는, 역 페이지 테이블 접근 방법도 있다. 논리 주소에 프로세스 id를 사용하고, 페이지 테이블에서 pid를 이용하여 페이지를 찾게 한 다음 그 페이지의 테이블상 위치 값을 이용하여 주소를 변환하는 방법이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/8_MainMemory.html



수정 시 복사(Copy on Write)

잠깐 다시 프로세스를 생각해보자. 새로운 프로세스를 실행하는 방법은 보통 fork 시스템 콜을 사용한다고 앞서 설명하였다. 그런데 페이징 방법을 고려하면 프로세스를 복사하는 것이 만만찮은 작업임을 알 수 있다. 심지어 어떤 프로세스는 복사되자마자 exec 시스템 콜을 호출하여 다른 프로세스로 바뀌어버린다. 이는 페이지나 페이지 테이블을 복사하는 것이 낭비가 될 수 있음을 뜻한다.

해법은 페이지에 수정을 가하는 경우에만 실제로 복사 작업을 하는 것이다. 어차피 수정 사항이 없는 페이지는 원본 페이지를 참조하여도 문제가 발생하지 않기 때문에 효과적이라고 할 수 있다. 단, 시스템 콜에 여러 옵션을 줘서 이를 사용자가 조절할 수 있도록 한다.



페이지 교체(Page Replacement)

원하는 페이지가 물리 메모리에 존재하지 않으면 어쩔 수 없이 어떤 페이지는 내려놔야 한다. 원하는 페이지가 물리 메모리에 없으면 페이지 폴트(Page fault)가 일어났다고 하고, 물리 메모리에서 내려갈 페이지를 Victim이라고 한다. Victim을 선택하는 알고리즘은 여러 가지가 있다.

FIFO 알고리즘은 먼저 참조되었던 페이지를 victim으로 결정하는 알고리즘이다. 단순하고 쉬우나 언제나 최적의 결과를 보여주지는 않는다. 심지어 Belady's anomaly라는, 더 많은 프레임을 사용할 수 있는 상황일 때 오히려 페이지 폴트를 더 일으키는 이상 현상을 보이기도 하는 문제가 있다. 그러나 하드웨어의 지원이 하나도 없는 시스템이라면 FIFO 알고리즘만이 남은 선택지다.

OPR(Optimal Page-Replacement) 알고리즘은 실제로 페이지 폴트를 가장 적게 일으키는 방식으로 victim을 결정하는 알고리즘이다. 당연히 현실적으로는 구현 불가능한 알고리즘이다. 대신, OPR 알고리즘을 기준으로 성능을 비교할 수 있기 때문에 유의미하다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html

LRU(Least Recently Used) 가장 과거에 참조했던 페이지를 victim으로 결정하는 알고리즘이다. 오랫동안 참조하지 않았으면 앞으로도 참조하지 않을 가능성이 높다는 차원에서 그럴듯하다. 문제는 LRU를 구현하는 방법이다. Counter 방법이나 Stack 방법으로 가능한데, Counter 방법은 페이지에 접근할 때마다 counter를 올리고, victim을 찾을 때 counter가 가장 작은 것을 찾는 방법이다. Stack 방법은 페이지에 접근할 때마다 스택에 페이지 번호를 push 한다. 만약 스택에 이미 그 페이지 번호가 있었다면 꺼내어 다시 top에 푸시한다. 결국 스택의 바닥에 가장 과거에 참조했던 페이지 번호가 위치하게 될 것이다. 문제는 이러한 작업들이 메모리 접근을 할 때마다 이뤄져야 한다는 것이다. 만약 소프트웨어 수준에서 이 방법을 지원하기 위해서는 성능 저하가 너무 심해질 것이다. 따라서 하드웨어의 지원 없이는 불가능하다.

유사 LRU(LRU-Approximation) 알고리즘은 LRU 방식을 일부 적용하는 알고리즘이다. 기본적으로 참조 bit를 따로 둬서 덜 참조된 페이지를 victim으로 결정하는 방식이다. 

Additional-Reference-Bits Algorithm : 참조 bit를 더 많이 두고 bit들을 매번 오른쪽으로 shift하면서 페이지에 접근할 때마다 가장 왼쪽의 bit를 1로 변경한다. 참조 bit들을 정수로 읽었을 때 값이 가장 작은 것이 LRU에 가깝게 될 것이다. 

Second Chance Algorithm : 순환 큐에 페이지 항목들과 참조 bit를 둔다. 참조 bit가 설정되지 않은 페이지는 바로 victim으로 결정되고, 참조 bit가 설정된 페이지는 참조 bit를 초기화한다. 

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html

Enhanced Second-Chance Algorithm : 참조 bit 뿐만 아니라 수정(Dirty) bit를 둔다. 수정된 페이지는 보조 기억 장치에 쓰기 작업이 필요하므로 선택 우선순위에서 떨어진다. 

참조 횟수가 가장 적은 페이지를 victim으로 결정하는 LFU 알고리즘(Least Frequently Used)과 참조 횟수가 가장 많은 페이지를 victim으로 결정하는 MFU 알고리즘(Most Frequently Used)도 있다. MFU 알고리즘은 가장 작은 참조 횟수를 가진 페이지가 가장 최근에 참조됐기 때문에 가장 적게 참조됐고, 앞으로 사용될 것이라는 판단에 근거한 알고리즘이다. 그러나 이 알고리즘은 빠르게 수행되게끔 구현이 어려울뿐더러 OPT 알고리즘에 가까운 성능을 보이지도 못하기 때문에 잘 사용되지 않는다.

페이지 버퍼링(Page-Buffering) 알고리즘은 풀링(Pooling) 방식을 적용한 알고리즘이다. 사용 가능한 물리 메모리 프레임을 풀에 보관하다가 필요하면 꺼내 쓰고 victim은 보조 기억 장치에 복사한 뒤 다시 풀에 추가한다.



쓰레싱(Thrashing)

동시에 더 많은 프로세스를 실행한다고 해서 CPU 사용량이 계속 증가하기만 하는 것은 아니다. 아무리 좋은 페이지 교체 알고리즘을 사용하더라도 프로세스들이 메모리를 많이 쓰면 페이지 폴트가 일어나 스왑핑을 할 수밖에 없고, 스왑핑을 위한 IO 작업이 많이 발생하면 CPU 사용량이 떨어진다. 이때, CPU 사용량이 급감하기 시작하는 것을 쓰레싱이라고 한다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html



커널 메모리 할당

프로세스가 유저 모드에서 실행되면서 추가적인 메모리를 요청하면 커널이 비어있는 페이지 중 하나를 할당해준다. 페이지 하나를 덜컥 할당해주는 것이기 때문에 내부 파편화 문제는 어쩔 수 없이 발생한다. 이는 메모리 할당을 빠르게 해주기 위해 어쩔 수 없다. 메모리 할당은 매우 빈번하게 일어나고 매번 복잡한 알고리즘으로 수행한다면 성능이 떨어질 것이다. 커널 메모리는 유저 모드 프로세스의 메모리 할당과 다르게 메모리 풀을 이용해서 메모리를 할당한다. 커널 메모리를 다른 방식으로 할당하는 이유는 아래 두 가지가 중요하다.

커널에서 사용하는 자료 구조들은 크기가 다양하고 페이지보다 용량이 적다. 조심스럽게 메모리 할당을 하지 않으면 파편화로 인해 메모리를 많이 낭비하게 된다. 많은 OS가 커널 코드나 데이터에는 페이징을 적용하지 않고 있다.

유저 모드 프로세스에게 할당된 메모리는 물리적으로 연속적이지 않아도 큰 상관이 없다. 그러나 커널 메모리 공간은 하드웨어 장치로부터 직접적으로 물리 메모리에 접근되기 때문에 물리적으로 연속적일 필요가 있다.

이러한 이유 때문에 커널 메모리는 다른 전략으로 관리된다. 그중 Buddy System과 Slab Allocation 방법을 알아보자. Buddy System은 물리적으로 연속된 메모리 공간을 2의 지수로 쪼개어 사용하는 방법이다. 커널에서 요구하는 메모리 크기보다 같거나 큰 덩어리를 할당한다. 예를 들어 21KB를 요청받으면 32KB 덩어리를 할당한다. 형제 노드를 buddy라고 하는데, 형제 노드가 모두 메모리 해제되면 둘은 합병(Coalescing)된다. 이로써 나중에 더 큰 물리적으로 연속된 메모리를 할당할 수 있게 된다. Buddy System 전략으로 메모리를 할당하면 외부 단편화를 해결할 수 있다.

출처 : Operating System Concepts 9th edition

Slab Allocation 방법은 슬랩 단위로 메모리를 관리하면서 내부 단편화를 해결하면서 초기화 속도 등을 향상하는 방법이다. 기본적인 아이디어는 메모리를 커널에서 사용하고 있는 자료 구조들의 객체로 취급하는 것이다. Slab cache 체인은 각각 slab 목록을 포함하는데, 일반적으로 slab 목록은 물리적으로 연속된 메모리이다. slab은 완전히 할당이 완료되었거나(slabs_full), 일부만 할당되었거나(slabs_partial), 비어있는(slabs_empty) 상태 세 가지로 나뉜다. 커널의 자료 구조에 대한 메모리 할당을 요청받으면 Slab allocator는 미리 메모리를 할당해둔 slab 속의 메모리를 준다. 미리 할당해둔 메모리를 주기 때문에 물리 메모리 상의 빈 공간을 찾기 위한 시간이 들지 않고, 메모리를 반환할 때도 Slab allocator에게 반환하여 후에 재활용하기 때문에 실제로 메모리 할당을 위한 비용이 줄어들게 된다. 

출처 : Operating System Concepts 9th edition



대용량 저장 장치(Mass-Storage)

메모리를 넘어 스토리지 관리에 대해 알아본다. 메모리는 크기가 작고 휘발성이기 때문에 비휘발성 저장 장치가 필요하다. 가장 익숙한 것은 HDD(Hard Disk Drive)일 것이다. 이제는 SSD(Solid State Disk)가 많이 대중화되었지만 과거엔 HDD가 시장을 지배하고 있었다. HDD는 물리적으로 회전하고 이동하는 장치에 의해 작동되는 방식이기 때문에 어떤 데이터를 접근할지 잘 스케쥴링하는 것이 성능에 중요한 영향을 끼쳤다. 암이 이동하고 디스크가 회전하는 시간이 밀리초 단위이기 때문에 먼 곳으로 이동하기 위해선 긴 시간이 걸리기 때문이다. 디스크 스케쥴링에 대한 자세한 항목은 다음 문서를 참고.

보통 저장 장치는 SATA 등의 방식으로 물리적으로 연결되어 있지만, 네트워크상으로 연결될 수도 있다. 지금도 많이 쓰고 있는 NAS(Network-Attached Storage), SAN(Storage-Area Network)과 같은 것이 네트워크상으로 저장 장치를 연결하는 방법이다. 

OS와 같은 프로그램은 휘발되면 안 되기 때문에 저장 장치에 저장되어 있다. 컴퓨터를 처음 켜면 ROM에 있는 부트스트랩(bootstrap) 프로그램이 디스크에서 MBR(Master Boot Record)에 접근하여 부팅하도록 한다. 조금 다르긴 하지만 OS가 프로세스를 실행시켜주는 것과 비슷하다고 볼 수 있다.

은행과 같이 저장된 데이터가 매우 매우 중요한 곳은 저장 자체를 여분으로 더 많이 해둔다. RAID(Redundant Array of Inexpensive Disks)는 널리 사용되는 중복 저장 및 유효성 검사 방식이다. RAID 방식도 여러 가지가 있기 때문에 자세한 내용은 링크된 문서를 확인. 데이터베이스 서버는 데이터가 손상되지 않게 잘 저장해야 하는 시스템이기 때문에 RAID로 구성하는 것이 매우 중요하다.



파일 시스템(File System)

파일 시스템은 파일이라는 단위로 대용량 저장 장치를 잘 관리해주는 시스템이다. 앞서 언급한 바와 같이 저장 장치는 물리적으로 연결되어 있을 수도, 네트워크로 연결되어 있을 수도 있다. 이를 추상화하여 파일을 다루는 서비스를 제공하는 것이 파일 시스템이다. 가장 널리 알려진 파일 시스템으로는 FAT, NTFS 등이 있다.

OS마다 차이가 있겠지만 기본적으로 파일은 이름과 식별자, 종류, 위치, 크기 등을 포함한다. 파일에 대한 연산에는 파일 생성, 파일 쓰기, 파일 읽기, 파일 내 위치 이동, 파일 삭제, 파일 내용 초기화가 있다. 거의 대부분의 OS는 파일에 대한 연산을 수행하기 전에 그 파일을 여는 작업을 먼저 하도록 한다. 그러한 연산이 일어나는 곳은 프로세스이므로 프로세스는 열린 파일 테이블(Open file table)이라는 것을 가진다. 열린 파일에 대한 연산은 File pointer라는 것으로 다뤄진다. 파일은 여러 프로세스에 의해 동시에 접근될 수도 있기 때문에 어떤 시스템은 동기화를 위해 파일 락킹(File locking) 기능을 제공한다. 예를 들어 함께 파일을 열어 읽을 수 있는 락킹 모드(shared lock)라던가, 남이 파일을 열 수 없게 하는 락킹 모드(exclusive lock) 등을 제공한다.

널리 알려진 파일 종류는 크게 다음과 같다. 엄밀히 말해서 리눅스에서 파일 종류는 크게 디렉터리 파일과 일반 파일, 디바이스 파일로 나뉜다. 아래 파일 종류는 어떠한 규칙으로 저장된 일반 파일에 대해 프로그램이 각자 해석하는 것이지 시스템상으로 차이가 있진 않다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/11_FileSystemInterface.html

저장 장치 상에서 접근 방법이 여러 가지 존재할 수 있다. 순차 접근(Sequential Access)은 시작점부터 종료점까지 위치를 순차적으로 이동하는 방법이다. 오직 되감기를 하거나 다음으로 이동만 가능하다. 자기 테이프와 같은 저장 장치에서는 이 방식이 유효하다. 직접 접근(Direct Access)은 저장 장치상의 특정 위치로 직접 이동할 수 있다. 저장 장치의 작동 방식에 따라 직접 접근도 접근에 시간이 걸릴 것이다. 어떤 파일이 어디에 위치해있는지 확인할 수 있도록 인덱스를 구성하기도 하고, 파일이 아주 많은 경우에는 인덱스의 인덱스를 구성하기도 한다. 인덱스를 잘 활용하는 대표적 프로그램으로 데이터베이스 관리 시스템(DBMS)이 있다.

디렉터리 구조도 여러 가지 방법으로 결정할 수 있다. 하나의 루트 디렉터리에 모든 파일이 모여있는 단일 수준 디렉터리(Single-Level Directory), 루트 디렉터리 아래에 하나의 디렉터리가 더 존재할 수 있는 이 단계 디렉터리(Two-Level Directory), 트리와 같은 구조로 디렉터리가 존재할 수 있는 트리 구조 디렉터리(Tree-Structured Directories), 비순환 그래프 구조의 디렉터리(Acyclic-Graph Directories), 아예 순환도 존재할 수 있는 일반 그래프 구조 디렉터리(General Graph Directory)가 있다. 디렉터리 구조에 따라 디렉터리 순회 방법에 대한 고려가 필요하다.

파일 시스템은 마운팅(Mounting) 될 수도 있다. 어떤 디렉터리를 마운트 포인트(mount point)로 보고 마운트 포인트에 다른 파일 시스템을 장착하는 식이다. 네트워크로 연결된 저장 장치도 마운트 한 뒤 파일 접근을 통해 사용할 수 있으므로 쉽고 강력한 기능이라고 볼 수 있다. 여러 저장 장치를 한 시스템에 연결해서 사용한다면 각 저장 장치를 볼륨이라는 이름으로 이미 마운트 해서 쓰고 있는 것이기도 하다.

현대 OS에서는 여러 유저가 시스템을 사용할 수 있도록 다중 사용자 기능을 제공한다. 이 경우 유저 간의 파일 공유가 문제가 될 수 있다. 또는, 네트워크 상으로 연결됐을 때 파일을 공유해줄 수 있도록 하는가도 고려 사항이 된다. 다른 사용자로부터 내 파일을 접근하지 못하도록 하고 싶을 수 있다. 따라서 파일에 대한 보호 기능이 제공된다. 파일 연산에 대해 권한을 나누어 권한별 사용이 가능하게 하는 것이 Access Control이다. 보통 읽기, 쓰기, 실행 권한 정도로 나눈다. 이에 대해서는 추후 보호 부분에서 더 다룬다.



파일 시스템 구현

파일 시스템은 볼륨마다 boot block, master file table(superblock)을 두고 파일마다 FCB(File Control Block)을 둔다. FCB는 파일에 대한 자세한 정보가 있는 테이블이다. UNIX에서는 inode에 FCB를 저장한다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/12_FileSystemImplementation.html

시스템에서는 메모리에 마운트 테이블, 최근 접근한 디렉터리 정보 캐시, 시스템 차원의 열린 파일 테이블과 프로세스당 열린 파일 테이블을 관리한다. 아래 사진은 열린 파일 테이블이 어떻게 사용되는지에 대한 그림이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/12_FileSystemImplementation.html

리눅스에서는 VFS(Virtual File System)이라는 추상화 계층을 둔다. 파일 시스템 인터페이스 하에 추상 계층을 두고 추상 계층을 통해 각 파일 시스템이 이용된다. 리눅스의 VFS는 inode 객체, 파일 객체, superblock 객체, directory 객체를 기반으로 운영된다. 아래 그림은 VFS이 어떤 형태인지 보여주는 그림이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/12_FileSystemImplementation.html



저장 장치의 공간 할당

메모리 때와 마찬가지로 저장 장치의 공간을 할당하는 방법도 고민의 대상이다. 기본적으로 저장 장치는 블록이라는 단위로 나뉜다. 파일을 블록들을 물리적으로 연속하여 위치하도록 하면 당연히 좋겠지만 현실적으로 모든 블록을 연속적으로 두는 것은 어렵다. 각 디스크 블록을 연결된 리스트로 두는 방법도 있다. 하지만 이 방법은 중간 접근을 위해 각 블록을 순차적으로 읽어야 하기 때문에 성능 저하를 일으키게 된다. 곧바로 임의 위치 블록을 찾아 이동할 수 있도록 하기 위해 인덱스를 할당하는 방법이 대안이다. 다만 인덱스를 두는 블록을 어떻게 관리하고 구현할지가 문제가 된다. Linked Scheme, Multi-Level Index, Combined Scheme 등의 접근 방식이 존재한다. 디스크 블록을 읽어 들이는 작업은 매우 느리기 때문에 무엇보다도 디스크 접근을 최소화하는 방법이 성능상의 우위를 보이게 될 것이다. 아래 사진은 UNIX의 inode 관리를 위해 사용되는 Combined Scheme 방식을 보여준다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/12_FileSystemImplementation.html

가용 공간을 어떻게 관리할지도 생각할만한 부분이다. 저장 장치를 탐색하면서 저장 공간을 관리하는 것은 너무 오래 걸리기 때문에 따로 지속적으로 저장 공간 관리를 한다. 대표적으로 bit 벡터 방식과 연결된 리스트 방식이 있다. bit 벡터는 bit당 블록 하나로 쳐서 bit가 1이면 사용 가능하고, 0이면 사용 중인 것으로 간주한다. 매우 적은 용량과 빠른 알고리즘으로 가용 공간을 관리할 수 있다. 연결된 리스트 방법은 가용 블록들을 연결된 리스트로 관리하여 할당하기에 적합한 블록을 찾는 방법이다. 그 외, 그룹핑이나 카운팅, 공간 맵 방법 등이 있다. 아래 사진은 가용 블록들을 연결된 리스트로 관리하는 예시이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/12_FileSystemImplementation.html



성능

파일 시스템에 있어서 성능은 무조건 보조 기억 장치에 접근을 최소화하는 것이다. 파일 시스템에서 읽어온 데이터를 캐시 해두고 있다면 저장 장치에 접근하지 않아도 되기 때문에 성능상에 큰 이득을 볼 수 있게 된다. 디스크 블록을 캐시 해두는 것을 buffer cache, buffer cache를 페이지처럼 쓸 수 있게 따로 연결해두는 것을 page cache라고 한다. buffer cache와 page cache 둘이 중복으로 저장해두는 것은 메모리 낭비를 일으키므로 이를 해결하기 위한 unified buffer cache 방식으로 캐시 하기도 한다. unified buffer cache 방식에서는 memory-mapped I/O와 일반 I/O를 수행할 때 통합된 buffer cache에 접근하게 된다.

page cache를 관리하는 전략으로 Free-behind 방식과 Read-ahead 방식이 있다. 둘은 LRU에 기반한 아이디어로 Free-behind는 다음 페이지를 접근하자마자 이전의 페이지를 내려놓는 방식이다. 이미 읽은 데이터를 다시 필요로 할 가능성이 낮기 때문이다. Read-ahead 방식은 다음 페이지를 미리 읽는 방식이다. 순차적으로 데이터를 읽고 있는 상황에서 다음 페이지에 접근하게 될 가능성이 높다고 예상되기 때문이다.

디스크에 쓰기 작업을 하는 것도 동기 방식과 비동기 방식으로 나뉠 수 있다. 디스크에 쓰기 하려고 할 때 바로 쓰기 작업을 수행하는 것을 동기, 쓰기 요청을 캐시 해뒀다가 좀 더 효율적인 스케줄로 쓰기 작업을 수행하는 것을 비동기 방식이라고 한다. 시스템은 상황에 따라 적절한 방법을 선택할 수 있도록 기능을 제공한다.



I/O 시스템

I/O 장치를 관리하는 것도 OS의 중요한 부분 중 하나이다. 키보드, 마우스, 저장 장치 등의 I/O 장치들을 잘 관리해야 한다. 각 I/O 서브시스템들은 장치 드라이버라는 것을 통해 OS로부터 조종되며, I/O 하드웨어들은 port를 통해 연결되고 버스를 통해 데이터를 전송한다. 장치로부터 입출력을 하는 방법은 폴링(Polling) 방식과 인터럽트(Interrupts) 방식으로 나뉜다. 

폴링 방식은 주기적으로 장치를 확인하여 데이터를 입출력하는 방법이다. 장치를 위해 busy waiting을 하게 돼서 시간을 많이 낭비하게 된다. 반면 인터럽트 방식은 입출력이 수행된 뒤 인터럽트를 통해 작업 종료를 알려주게 되므로 주기적으로 다시 확인할 필요가 없다. 아래 방식은 인터럽트 주도 I/O 작업의 주기이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/13_IOSystems.html

장치가 직접 메모리에 접근하게 하는 DMA(Direct Memory Access) 방식도 있다. 잠시 버스를 점유하여 메모리를 사용하는 것을 제외하면 장치가 직접 메모리에 접근하므로 효율적이다.



보호(Protection)

OS에서의 보호는 사용자나 프로그램의 고의적인 악용을 막는 것을 말한다. 보호를 통해 OS는 공유된 자원을 시스템의 정책이나 설계자, 관리자에 의해 정해진대로 쓰고 문제가 있는 프로그램으로 인한 손상을 최소화하고자 한다.

최소 권한의 원칙은 보호에 있어 중요한 원칙이다. 사용자나 프로그램은 최소한의 권한만을 부여받아 일을 수행할 수 있게 한다. 최소 권한의 원칙에 따르면 사용자나 프로그램이 다른 무슨 일을 하려고 해도 권한이 부족하기 때문에 해를 입히기 어렵게 된다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/14_Protection.html

컴퓨터는 프로세스들과 객체들의 모음으로 볼 수 있고, 도메인이란 객체에 대한 권한의 모임이다. Need to know 원칙은 어떤 일을 수행하기 위해 알아야 하는 객체만 알려주는 원칙을 말한다. Need to know 원칙에 따라 최소한의 객체만을 포함하는 도메인을 제공하고자 한다. UNIX에서는 유저를 하나의 도메인으로 볼 수 있다. 

다른 일을 수행하기 위해 다른 권한이 필요하면 도메인 전환이 필요하다. 접근 제어 행렬 방법에서는 객체와 도메인 간의 권한을 테이블로 만들어 관리한다. 아래 사진은 접근 제어 행렬 방법에서의 테이블의 예이다. 어떤 도메인이 객체에 대해 어떤 권한이 있는지, 어떤 도메인에서 다른 도메인으로 전환 가능한지 표시를 해두는 식이다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/14_Protection.html

접근 제어 행렬 방법을 구현하는 방법이 몇 가지 있다. Global Table 방법은 전체 객체, 도메인, 권한에 대한 표를 만들어두는 방법이지만, 너무 테이블이 크기 때문에 현실적이지 않다. Access Lists 방법은 객체에 대한 도메인의 목록, Capability Lists 방법은 도메인이 가진 객체 권한 목록을 나열하는 방법이다. 



접근 제어(Access Control)

역할 기반 접근 제어(Role-Based Access Control, RBAC) 방법은 사용자에게 권한이나 역할을 부여하는 방법이다. RBAC는 최소 권한의 원칙을 지원한다. 접근 권한을 파기하는 것이 Access Lists 방법에서는 쉽다. 객체의 도메인 목록에서 도메인을 제거해주기만 하면 된다. 하지만 Capability Lists 방법에서는 시스템 전체에 퍼져있는 접근 권한을 찾아 제거해야 하기 때문에 어렵다. Capability Lists 방법에서 접근 권한 파기 방법으로는 Reacquisition, Back-pointers, Indirection, Keys 방법 등이 있다.



보안(Security)

보호는 파일과 자원을 사용자 간에 보호하는 것이었다면 보안은 시스템을 내외부의 공격으로부터 막는 것을 다룬다. 물리적, 인간적(피싱이나 비밀번호 크래킹 등), 네트워크 상의 공격, OS 자체의 보안 결함으로부터 보호해야 한다. 

프로그램적인 위협으로는 트로이 목마, 트랩 도어, 논리 폭탄, 스택과 버퍼 오버플로우, 바이러스 등이 존재한다. 시스템이나 네트워크 상의 위협으로는 , 포트 스캔, 서비스 거부 공격(DOS)이 있다. 

데이터의 보안을 위해서는 암호화가 중요하다. 대칭 키 암호화 방식에서 이제는 비대칭 키 암호화 방식이 대세가 되었다. 비대칭 키 암호화 알고리즘으로 가장 유명한 것이 RSA이다. 안전한 소켓 통신을 위한 SSL(Secure Socket Layer) 통신의 키 교환과 인증에 RSA가 쓰일 수 있다.

방화벽은 네트워크 상의 공격을 막는 장치 혹은 방법이다. 외부로부터의 인터넷 접근을 제어하여 내부 컴퓨터를 보호한다. 이제는 많이 대중화되어 개인용 방화벽 소프트웨어나 OS에 기본적으로 설치되어 있기도 한다.

출처 : https://www2.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/15_Security.html



이것으로 운영 체제의 주요 주제들을 살펴보았다. 넓고 얕게 많은 부분을 다루다 보니 양이 정말 많았다. 책을 정리하기엔 너무 양이 많아서 일리노이 대학교 시카고의 운영 체제 강의 노트를 참고했다. 사진 출처도 대부분 강의 노트 페이지인데, 강의 노트에서 사용된 사진도 대부분 엄밀히 말하면 Operating System Concepts 책에서 가져온 것으로 보인다.

다음 편에서는 네트워크를 다루겠다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari