운영체제는 사용자가 컴퓨터를 쉽게 다루게 해주는 인터페이스이다. 운영체제가 있기에 사용자는 한정된 메모리나 시스템 자원을 분배하는 일을 신경쓰지 않아도 된다.
운영체제의 역할은 다음과 같다.
CPU 스케쥴링과 프로세스 관리
- CPU 소유권을 어떤 프로세스에 할당할지 결정하고, 실제로 CPU를 프로세스에 할당한다.
- 프로세스의 생성과 삭제, 자원(CPU) 할당 및 반환을 관리한다.
- 프로세스 간 공유 자원 접근과 통신 등을 관리한다.
저장장치 관리
- 프로세스에 메모리를 할당하고, 해제한다.
- 가상 메모리 기능을 제공하고, 이를 통해 각 프로세스에게 독립된 메모리 공간을 제공한다.
- 파일 데이터 관리를 위한 파일 시스템을 OS에서 관리한다.
네트워킹
- TCP/IP 기반의 인터넷 연결이나 네트워크 사용을 위한 네트워크 프로토콜을 지원한다.
디바이스 드라이버
- 컴퓨터 시스템의 여러 하드웨어를 운영체제에서 인식하고 관리하여 응용 프로그램이 하드웨어를 사용할 수 있게 한다.
- 응용 프로그램이 하드웨어를 사용하기 위해 필요한 하드웨어 추상화 계층인 디바이스 드라이버를 관리한다.
사용자가 컴퓨터와 상호작용할 수 있도록 하는 사용자 인터페이스로, 그래픽적 요소(아이콘 클릭 등)가 들어가있는 GUI와 커맨드 명령어로 처리하는 CUI가 있다.
시스템콜이란 운영체제가 커널에 접근하기 위한 인터페이스이며 사용자 프로그램이 운영체제의 서비스를 받기 위해 커널함수를 호출할 때 사용한다.
유저 프로그램이 시스템콜을 요청할 경우, 해당 시스템콜이 커널 모드로 변환되어 실행된다. 커널 모드에서 유저 프로그램이 요청한 시스템콜에 맞는 커널 코드가 실행된 후, 다시 유저 모드로 돌아가 그 뒤에 있는 유저 프로그램의 로직을 수행한다.
시스템콜이 작동될 때 modebit를 참고해서 유저 모드와 커널 모드를 구분한다. 0 또는 1의 값을 가지는 modebit는 0은 커널 모드, 1은 유저 모드를 뜻하는데, 시스템콜을 통해 커널 코드를 실행시키고자할 때 modebit를 1에서 0으로 바꿔 커널 모드로 변경한 후 커널 코드를 실행하고, 그 이후 modebit를 0에서 1로 바꿔 유저 모드로 변경하고 이후 로직을 수행한다.
운영체제의 핵심 부분이자 시스템콜 인터페이스를 제공하며 보안, 메모리, 프로세스, 파일 시스템, I/O 디바이스, I/O 요청 관리 등 운영체제의 중추적인 역할을 한다. 컴퓨터를 부팅했을 때, 메모리에 항상 상주해야하는 운영체제의 서비스들이 대부분 커널에 해당한다.
운영체제의 대표적인 할 일 중 하나가 메모리 관리이다.
컴퓨터 내의 한정된 메모리를 효율적으로 활용하기 위해 운영체제는 가상 메모리 시스템을 제공한다.
가상 메모리란 프로그램이 혼자 메모리를 사용하는 것처럼 메모리를 가상화한 것을 말한다.
멀티태스킹이 없던 옛날에는 항상 하나의 프로그램이 모든 메모리를 직접 사용한다는 전제하에 만들었다. 이후 멀티태스킹이 도입될 때 문제가 되던 것 중에 하나가 각자의 프로그램이 동시에 작동한다고 했을때 메모리를 어떻게 분배할지에 대한 것이 있었다.
일반적으로 프로그램이 메모리에 데이터를 읽거나 쓸 때 실행 파일에 저장된 특정 위치에 특정 데이터를 저장한다. 이때 만약 같은 프로그램을 2개 실행한다고 하면 두 프로그램이 메모리의 같은 위치에 데이터를 읽고 쓰게 되고, 두 프로그램이 서로 충돌해 오동작하게 된다.
이를 해결하기 위한 여러 방법들 중 하나가 각 프로그램이 별도의 메모리를 혼자 사용하는 것처럼 가상화하는 것이다.
가상 메모리는 컴퓨터가 실제로 이용 가능한 물리 메모리 자원을 추상화하여 이를 사용하는 사용자들에게 본인만 사용하는 별도의 매우 큰 메모리 공간이 있는 것처럼 보이게 만드는 것을 말한다.
32bit 시스템에서 운영체제가 제공하는 가상 메모리의 크기는 약 4GB(2^32 Bytes)에 해당한다. 4GB의 가상메모리는 커널 영역과 사용자 영역으로 나뉘게 되는데, 커널 영역에는 운영체제의 커널에서 사용하는 코드와 데이터가 적재되어있고, 사용자 프로그램의 코드 및 데이터는 사용자 영역에 적재되어있다. 우리가 사용하는 프로그램들은 가상 메모리의 사용자 영역에 올라가서 실행되는 것이다.
4GB의 메모리는 일반적으로 아래와 같이 두 영역으로 나뉜다.
리눅스의 경우 1GB(커널 영역) / 3GB(사용자 영역)
윈도우의 경우 2GB(커널 영역) / 2GB(사용자 영역)
가상 메모리의 주소 공간은 페이지(Page)라는 것으로 일정한 크기로 분할되어 있다. 페이지의 크기는 하드웨어에 의해 결정이 되며 운영체제는 이러한 페이지들을 페이지 테이블 안에 집어 넣어서 관리하고 있다. 프로세스마다 자신만의 페이지 테이블을 가지고 있다(유저 메모리). 물론 운영체제도 역시 자신만의 페이지 테이블을 가지고 있다(커널 메모리). 페이지 테이블은 운영체제에서 관리한다.
프로세스는 각자 독립된 가상 주소를 가진다. 예를 들어 A 프로세스는 0x1000라는 주소를 가질 수도 있고 동시에 B 프로세스는 0x1000라는 주소를 가질수도 있다. 만약 두 프로세스가 각자 0x1000라는 주소에 접근할 경우 CPU는 프로세스(컨텍스트)마다 가지는 페이지 테이블을 참조한다.
페이지 테이블은 가상 주소와 물리 메모리의 실제 주소를 연결시켜주는 테이블이다. 가령 A 프로세스의 페이지 테이블에서 0x1000 주소에 대응되는 실제 주소는 0x30000 으로 나오고 B 프로세스의 페이지 테이블에서는 0x1000 주소에 대응되는 실제 주소가 0x40000인 것이다. 이렇게 되면 두 프로세스는 동일한 메모리 주소를 참조했지만 실제로는 MMU가 프로세스의 페이지 테이블을 참조해서 실제 물리 메모리 주소를 얻고 그 위치를 참조해서 가져오거나 쓰기를 한 것이다. 따라서 두 프로세스의 주소 공간이 겹치지 않고 독립적으로 실행될 수 있으며 프로그래머는 다른 프로세스의 주소 공간을 생각할 필요가 없어진다.
가상 메모리 공간은 비페이징 공간(Nonpaged Space)과 페이징 공간(Paged Space)으로 나뉘어져 있다. 비페이징 공간은 항상 물리 메모리에 존재하는 공간으로 페이징 파일로 옮겨지지 않는다. 반면 페이징 공간은 운영체제가 필요하지 않을 경우 물리 메모리에서 페이징 파일로 옮길 수도 있는 공간이다. 따라서 비페이징 공간은 페이지 폴트가 절대로 일어나지 않는다.
각 프로세스에게 주어진 가상 메모리의 주소를 가상 주소라하고, 실제 물리 메모리상에 있는 주소는 물리 주소라고한다. 현대의 CPU들은 모두 메모리 관리 유닛(MMU, Memory Management Unit)이라는 것을 내장하고 있는데, 가상 주소는 이 MMU에 의해 물리 주소로 변환되며, 덕분에 사용자는 물리 주소를 의식할 필요 없이 가상 주소를 이용하면 된다. CPU도 실제 연산을 수행할 때, 물리 주소가 아닌 가상 주소 를 이용한다.
MMU는 가상 주소를 물리 주소로 변환할 때 프로세스의 주소 매핑 정보가 들어있는 페이지 테이블과 속도 향상을 위한 TLB를 사용한다.
TLB 란?
- MMU가 주소를 변환하기 위해서는 페이지 테이블에 있는 주소 매핑 정보를 알아야 하는데, 이 페이지 테이블 또한 물리 메모리에 적재되어 있다. 그래서 CPU가 특정 가상 주소에 있는 데이터를 갖고 오기 위해서는 메모리에 총 2번(페이지 테이블 접근 1번 + 변환된 물리주소로 데이터를 갖고오기 위한 접근 1번) 접근해야한다. 메모리에 총 2번 접근하기때문에 발생하는 성능저하를 줄이기 위해 TLB는 메모리와 CPU 사이에서 캐시의 역할을 한다. TLB에서는 페이지 테이블에 있는 주소 매핑 정보를 보관하고, MMU가 주소를 변환하기 위해 주소 매핑 정보를 얻고자 할 때 TLB에 해당 주소 매핑 정보가 있는지 먼저 확인한 후, 없다면 그제서야 물리 메모리에 적재된 페이지 테이블에 접근하여 주소 매핑 정보를 가져온다.
만약 우리가 사용하고자하는 프로그램이 사용하는 메모리 용량이 가상메모리의 사용자 영역의 크기를 넘어간다면, 해당 프로그램을 동작시키는데 필요한 코드 또는 데이터가 메모리에 올라가있지 않은 상황이 벌어질 수 있고 결국 프로그램은 동작할 수 없다.
다행스럽게도 가상 메모리 시스템은 사용자 프로그램의 당장 사용하지 않는 영역을 SSD, HDD와 같은 보조기억장치로 옮겨 필요할 때 다시 메모리로 갖고오고, 사용하지 않으면 다시 보조기억장치로 내려 물리 메모리를 효과적으로 관리한다. 이때 올리고 내리는 행위를 스와핑이라고도 하며, 스와핑의 단위가 페이지인 경우를 페이징이라고 생각하면 된다.
만약 프로세스의 주소 공간에는 존재하지만 물리 메모리에는 없는 데이터를 접근하는 경우, 페이지 폴트가 발생한다. 이 경우 운영체제는 페이지 폴트가 발생했다는 트랩을 발생시켜 보조기억장치에 있는 필요한 페이지를 물리 메모리로 가져와서 프로그램이 정상적으로 실행될 수 있게 한다. 이 과정에서 메모리로 가져온 페이지에 대한 페이지 테이블도 최신화된다.
페이지 교체 알고리즘은 가상 메모리에서 Page Fault가 발생했을 때 어떤 페이지를 메모리로 적재할지를 결정하는 알고리즘이다.
가장 간단한 페이지 교체 알고리즘으로, 가장 먼저 들어온 페이지를 먼저 교체한다. 큐(queue)를 사용하여 페이지를 관리하며, 가장 앞에 있는 페이지가 교체 대상이 된다.
페이지 중에서 가장 오랫동안 사용(참조)되지 않은 페이지를 교체한다. 각 페이지에 접근할 때마다 해당 페이지의 사용 시간을 갱신하고, 교체할 때는 사용(참조)한지 가장 오래된 페이지를 선택한다.
해당 알고리즘은 페이지를 최근에 사용(참조)되었는지 여부와 수정되었는지 여부에 다라 분류하고, 이를 기반으로 교체할 페이지를 선택하는 알고리즘이다. 참조여부 비트 1개와 수정여부 비트 1개, 총 2개의 비트로 4가지의 상태로 분류하고, 각 4개의 상태에 우선순위를 두어 우선순위가 가장 낮은 상태의 페이지를 교체 대상으로 선택한다.
페이지 중에서 사용 횟수가 가장 적은 페이지를 교체한다. 각 페이지에 접근할 때마다 해당 페이지의 사용 횟수를 갱신하고, 교체할 때는 사용 횟수가 가장 적은 페이지를 선택한다.
페이지를 원형 큐로 관리하며, 시계 바늘이 특정 페이지를 가리키도록 한다. 페이지 부재가 발생하면 시계 바늘이 가리키는 페이지를 검사하며, 사용 비트를 확인하여 교체할 페이지를 선택한다. 비트의 값이 1일 경우 최근에 참조되었다는 뜻이고, 0일 경우 참조되지 않았음을 뜻한다. 시계 방향으로 돌면서 비트의 값이 0인 페이지를 찾고 0을 찾은 순간 해당 페이지를 교체하고, 교체된 페이지의 비트를 1로 바꾼다.