1.7 The Operating System Manages the Hardware
1.7 운영 체제가 하드웨어를 관리한다
우리의 hello 예시로 돌아가 보겠습니다.
쉘이 hello 프로그램을 로드하고 실행했을 때, 그리고 hello 프로그램이 실행하여 메시지를 출력했을 때, 두 프로그램(쉘 프로그램과 hello 프로그램) 모두 키보드, 디스플레이, 디스크, 메인 메모리에 직접 접근하지 않았습니다.
▶ hello 프로그램이 운영 체제의 서비스를 이용하여 하드웨어 자원에 접근하고 있습니다.
- 1. 프로세서가 메모리에서 명령어를 읽고, 명령어를 실행하는 과정이 이루어집니다.
- 2. 프로그램 카운터(PC)는 메모리 내에서 첫 번째 명령어의 위치를 가리키며, 프로세서는 차례대로 명령어를 실행합니다.
- 3. hello 프로그램이 디스플레이에 메시지 출력 등의 작업을 할 때, 운영 체제의 서비스를 통해 하드웨어 자원에 접근합니다.
- 4. hello 프로그램이 실행을 마치고 종료되면, 운영 체제는 쉘 프로그램으로 제어를 반환합니다. 쉘은 다시 사용자 입력을 기다리는 상태로 돌아갑니다.
우리는 운영 체제를 애플리케이션 프로그램과 하드웨어 사이에 놓인 소프트웨어 계층으로 생각할 수 있습니다.
이는 그림 1.10에서 보여집니다.

애플리케이션 프로그램이 하드웨어를 조작하려는 모든 시도는 반드시 운영 체제를 거쳐야 합니다.
운영 체제는 두 가지 주요 목적을 가지고 있습니다.
- 1. 하드웨어를 보호하는 것, 즉 프로그램이 시스템 자원을 무분별하게 사용하는 것을 방지하는 것입니다.
- 2. 애플리케이션이 다양한 하드웨어를 일관된 방식으로 다룰 수 있도록하는 방식을 제공합니다.
운영 체제는 그림 1.11에 나오는 세 가지 기본적인 추상화(프로세스, 가상 메모리, 파일)를 통해 이 두 가지 목표를 달성합니다.

이 그림에서 말하는 것처럼,
- 파일은 입출력 장치(I/O devices)에 대한 추상화입니다.
- 가상 메모리는 메인 메모리와 디스크 입출력 장치에 대한 추상화입니다.
- 프로세스는 프로세서, 메인 메모리, 입출력 장치에 대한 추상화입니다.
추상화는 운영 체제가 하드웨어 자원을 단순화하여 소프트웨어가 쉽게 자원을 사용할 수 있게 만드는 기법입니다.
프로세스, 가상 메모리, 파일은 각기 하드웨어 자원에 대한 추상화를 제공하여, 사용자가 복잡한 세부 사항을 신경 쓰지 않고도 자원들을 효율적으로 관리하고 사용할 수 있게 합니다.
운영 체제는 하드웨어와 소프트웨어 간의 복잡한 상호작용을 단순화하고 관리하기 쉽게 만들어주는 추상화 계층을 제공합니다.
예를 들어, 사용자가 프로세스를 관리하거나 메모리를 사용할 때, 하드웨어의 복잡한 세부 사항을 신경 쓰지 않아도 되도록 추상화된 인터페이스를 제공합니다.
1.7.1 프로세스 (Processes)
hello와 같은 프로그램이 현대 시스템에서 실행될 때, 운영 체제는 해당 프로그램이 시스템에서 유일하게 자신이 혼자 실행되고 있는 것처럼 보입니다.
즉, 프로그램은 마치 프로세서, 메인 메모리, 그리고 I/O 장치를 자신만이 독점적으로 사용하는 것처럼 보입니다.
프로세서는 프로그램의 명령어들을 마치 연속적으로 자신만의 명령을 실행하는 것처럼 보입니다.
또한, 해당 프로그램의 코드와 데이터만이 시스템 메모리에 존재하는 것처럼 보입니다.
이러한 환상(추상화)은 ‘프로세스(process)’라는 개념을 통해 제공되며, 이는 컴퓨터 과학에서 가장 중요하고 성공적인 개념 중 하나입니다.
결론은, 운영 체제는 여러 프로그램이 동시에 실행되더라도, 각각의 프로그램이 마치 시스템 전체를 혼자 쓰는 것처럼 “착각하게” 만듭니다. 이 착각(추상화)을 만들어주는 것이 바로 "프로세스"입니다.
프로세스란 운영 체제가 프로그램에게 "당신이 시스템 전체를 혼자 쓰고 있어요!"라고 착각하게 만들어주는 일종의 가상 실행 환경입니다.
덕분에 프로그램은 안정적이고 독립적으로 실행될 수 있고, 사용자는 여러 프로그램을 동시에 실행하면서도 그 사실을 거의 느끼지 못합니다.
결국, 실행 중인 프로그램을 운영 체제가 다루기 쉽도록 구조화한 개념이 프로세스입니다.
하나의 시스템에서 여러 개의 프로세스가 동시에 실행될 수 있으며, 각 프로세스는 자신이 하드웨어를 독점적으로 사용하는 것처럼 보입니다.
실제로는 하드웨어를 나눠 쓰지만, 각 프로세스는 자신만 쓰는 줄 알고 실행됩니다.
즉, 실제로는 프로세스 간 전환을 통해 하나의 프로세서가 하나씩 실행합니다.
여기서 동시(concurrently)란, 한 프로세스의 명령어들과 다른 프로세스의 명령어들이 서로 번갈아가며(interleaved) 실행된다는 뜻입니다.
대부분의 시스템에서는 실행해야 할 프로세스 수가 CPU 수보다 많습니다.
전통적인 시스템은 한 번에 하나의 프로그램만 실행할 수 있었지만, 요즘의 멀티코어 프로세서는 여러 프로그램을 동시에 실행할 수 있습니다.
하지만 두 경우 모두, 하나의 CPU라도 마치 여러 프로세스를 동시에 실행하는 것처럼 보일 수 있습니다.
이는 프로세서가 위에서 언급했다 시피, 여러 프로세스 사이를 빠르게 전환(switch)하면서 실행하기 때문입니다.
운영 체제는 이런 명령어 간의 전환(interleaving)을 컨텍스트 스위칭(context switching)이라는 기법을 통해 수행합니다.
남은 설명을 간단히 하기 위해, 여기서는 CPU가 하나인 단일 프로세서 시스템(uniprocessor)만 고려하겠습니다.
멀티프로세서 시스템에 대한 논의는 1.9.2절에서 다시 다룰 예정입니다.
유닉스(Unix), POSIX, 그리고 표준 유닉스 명세(Standard Unix Specification)
1960년대는 IBM의 OS/360, 하니웰(Honeywell)의 Multics 시스템처럼 거대하고 복잡한 운영 체제들이 등장하던 시대였습니다.
OS/360은 역사상 가장 성공적인 소프트웨어 프로젝트 중 하나였지만, Multics는 수년간 개발이 지연되었고, 광범위하게 사용되지는 못했습니다.
벨 연구소(Bell Labs)는 Multics 프로젝트의 초기 참여자였지만, 프로젝트의 복잡성과 진전 부족에 대한 우려로 1969년에 탈퇴했습니다.
Multics에서의 불쾌한 경험에 대한 반작용으로, Bell Labs의 연구자들인 Ken Thompson, Dennis Ritchie, Doug McIlroy, Joe Ossanna는 1969년, PDP-7 컴퓨터용으로 더 단순한 운영 체제를 만들기 시작했고, 이는 전부 머신 언어로 작성되었습니다.
계층형 파일 시스템, 사용자 수준 프로세스로서의 셸(shell)과 같은 많은 아이디어들은 Multics에서 차용했지만, 이를 더 작고 단순한 구조로 구현했습니다.
1970년, 브라이언 커니핸(Brian Kernighan)은 ‘Multics’의 복잡성을 풍자하여 이 새 운영 체제에 'Unix'라는 이름을 붙였습니다.
1973년, 커널은 C 언어로 다시 작성되었고, 1974년에 Unix는 외부에 공식적으로 소개되었습니다.
유닉스의 확산과 POSIX 표준화
Bell Labs가 Unix 소스 코드를 대학에 자유롭게 제공한 덕분에, Unix는 대학에서 큰 인기를 얻게 되었습니다.
1970년대 후반과 1980년대 초반, UC 버클리에서의 작업이 가장 큰 영향을 끼쳤습니다.
이들은 가상 메모리, 인터넷 프로토콜 등을 추가하여 Unix 4.xBSD라는 이름으로 배포했습니다.
동시에, Bell Labs도 자체적인 Unix 버전을 출시했고, 이는 System V Unix로 알려지게 되었습니다.
썬 마이크로시스템즈(Sun Microsystems)의 Solaris와 같은 다른 업체들의 버전도 대부분 BSD와 System V에서 파생된 것입니다.
POSIX 및 표준 유닉스 명세
1980년대 중반, 다양한 Unix 업체들이 자신들만의 기능을 추가하며 서로 호환되지 않는 기능들이 생겨나 문제가 발생했습니다.
이런 혼란을 막기 위해, IEEE가 나서서 Unix 표준화 작업을 주도했고, 리처드 스톨먼(Richard Stallman)은 이를 ‘POSIX’라 이름 붙였습니다.
그 결과, POSIX 표준이라는 일련의 규약이 만들어졌고, 이 표준은 C 언어 시스템 콜 인터페이스, 셸과 유틸리티, 스레드, 네트워크 프로그래밍 등을 포함합니다.
최근에는 표준 유닉스 명세(Standard Unix Specification)라는 별도의 표준화 노력과 POSIX가 통합되어, 하나의 통일된 Unix 시스템 표준이 만들어졌습니다.
이러한 표준화 노력 덕분에, 오늘날에는 Unix 버전 간의 차이점이 거의 사라졌습니다.
운영 체제에서 프로세스 관리와 컨텍스트 스위칭(Context Switching)
운영 체제는 프로세스가 실행되기 위해 필요한 모든 상태 정보를 관리합니다.
즉, 프로그램이 언제든지 중단되었다가 다시 실행되더라도 이어서 실행할 수 있도록, 그 프로그램(프로세스)의 현재 상태를 운영 체제가 기억하고 있다는 뜻입니다.
이 상태 정보를 컨텍스트(context)라고 부르며, 여기에 포함되는 것은 다음과 같습니다
- 프로그램 카운터(PC): 현재 어떤 명령어를 실행 중인지 체크, 프로그램 실행 흐름을 제어하며, 현재 실행 중인 명령어의 주소를 추적
- 레지스터 파일들: 프로세서의 고속 데이터 저장소로, CPU가 즉시 사용할 수 있는 값들을 저장
- 메인 메모리:프로세스의 코드와 데이터를 저장하는 큰 저장 공간으로, CPU와의 속도 차이를 메꾸기 위해 빠른 캐시가 함께 사용되기도 함
이 정보를 기반으로 프로그램은 다시 실행될 때 이전 상태 그대로 이어서 실행할 수 있습니다.
단일 CPU 시스템(uniprocessor)은 한 순간에 오직 하나의 프로세스만 실행할 수 있습니다.
CPU는 한 번에 하나의 일만 하므로, 동시에 여러 프로세스를 "진짜로" 실행하는 건 불가능합니다.
대신 빠르게 전환하면서 돌아가며 실행시킵니다.
운영 체제가 현재 실행 중인 프로세스를 멈추고, 다른 프로세스로 전환하기로 결정하면, 다음 단계를 수행합니다
- 1. 현재 프로세스의 컨텍스트를 저장
- 2. 새 프로세스의 컨텍스트를 복원
- 3. CPU 제어권을 새 프로세스로 넘김
이렇게 전환된 새로운 프로세스는 자신이 멈췄던 그 지점에서부터 정확히 이어서 실행됩니다.
그림 1.12는 이 내용을 hello 예제를 기반으로 시각적으로 설명한 것입니다.

커널은 운영 체제의 핵심 부분으로, 중앙 처리 장치(CPU), 메모리, I/O 장치와 같은 하드웨어 자원을 관리, 제어하는 역할을 합니다.
커널 코드는 이러한 하드웨어 자원에 대한 접근을 효율적이고, 안전하게 관리하는 저수준 코드입니다.
운영체제 동작 방식
우리의 예시 시나리오에는 동시에 존재하는 두 개의 프로세스가 있습니다.
하나는 쉘(shell) 프로세스, 다른 하나는 hello 프로세스입니다.
처음에는 쉘 프로세스만 실행 중이며, 사용자가 입력하기를 커맨드라인에서 대기하고 있습니다.
우리가 hello 프로그램을 실행하라고 입력하면, 쉘은 ‘시스템 콜(system call)’이라는 특별한 함수를 호출하여, 운영 체제에게 제어권을 넘깁니다.
- 시스템 콜은 사용자 프로그램이 운영 체제의 기능을 요청하는 수단입니다.
운영 체제는 다음과 같은 작업을 합니다.
- 현재 실행 중인 쉘 프로세스의 상태(context)를 저장
- 새로운 hello 프로세스와 그에 대한 컨텍스트(context)를 생성
- 제어권을 hello 프로세스로 넘김
hello 프로그램이 종료되면, 운영 체제는 쉘 프로세스의 이전 상태(context)를 복원하고
제어권을 다시 쉘로 넘겨서, 다음 명령어 입력을 기다리는 상태로 되돌립니다.
정리
- 1. 사용자는 쉘에서 명령어를 입력합니다.
- 2. 쉘은 hello를 실행하기 위해 운영 체제에 시스템 콜을 보냅니다.
- 3. 운영 체제는 쉘의 상태(context)를 임시로 저장하고, 새로운 hello 프로세스를 생성, 실행시킵니다.
- 4. hello가 실행을 마치고 종료되면, 운영 체제는 저장해뒀던 쉘의 상태를 복원하고, 제어권을 다시 쉘로 돌려보냅니다.
- 5. 쉘은 다음 명령을 기다리는 상태로 돌아갑니다.
그림 1.12에서 볼 수 있듯이, 한 프로세스에서 다른 프로세스로 전환하는 작업은 운영 체제의 커널(kernel)이 관리합니다.
- → 여러 프로세스를 번갈아 실행하는 컨텍스트 스위칭은 커널의 역할입니다.
커널은 운영 체제의 핵심 코드 부분으로, 항상 메모리에 상주해 있습니다.
- → 시스템이 켜져 있는 동안, 커널은 절대 메모리에서 내려가지 않으며, 언제든 시스템 자원을 관리할 수 있어야 하기 때문입니다.
애플리케이션 프로그램이 운영 체제의 도움이 필요한 작업(예: 파일 읽기/쓰기)을 요청하면, 특별한 명령어인 시스템 콜(system call)을 실행하여 커널로 제어권을 넘깁니다. ( 사용자 프로그램이 운영 체제의 핵심 부분인 커널에게, 특정 작업을 수행하도록 요청하고, 이 요청을 처리하기 위해 커널이 제어권을 얻는 과정을 의미한다. )
- → 일반 프로그램은 하드웨어를 직접 다룰 수 없기 때문에, 반드시 커널을 통해 요청해야 합니다.
커널은 요청된 작업을 수행한 뒤, 다시 애플리케이션 프로그램으로 제어권을 돌려줍니다.
- → 예: 파일을 읽어오고, 그 결과를 프로그램에게 넘겨줌.
중요한 점은, 커널은 독립된 프로세스가 아니라는 것입니다.
커널은 운영 체제가 모든 프로세스를 관리하기 위해 사용하는 코드와 데이터 구조들의 집합일 뿐입니다.
사용자 프로그램과 달리 커널은 실행 중인 독립적인 프로세스로 존재하지 않으며, 시스템 콜이나 인터럽트가 발생했을 때 그에 대한 실행을 담당하는 코드의 집합입니다.
커널은 프로세스 스케쥴링, 메모리 관리, 디스크 및 파일 시스템 관리, 입출력 관리 등 시스템 자원을 관리하는 기능을 담당합니다. 이 말은 즉, 커널 자체는 하드웨어 자원을 관리하고, 프로세스나 메모리 등 다양한 시스템 자원을 효율적으로 제어하는 역할을 합니다.
프로세스는 이런 운영 체제 커널이 관리하고 구현하게 됩니다.
프로세스라는 추상화를 구현하려면, 하드웨어(예: CPU, 메모리)와 소프트웨어 간의 밀접한 협력이 필요합니다.
- → 예를 들어, 컨텍스트 스위칭을 하려면 레지스터 값 저장/복원, 메모리 접근 제한, 타이머 인터럽트 등이 필요하므로 하드웨어와 운영 체제가 함께 작동해야 합니다.
운영 체제가 여러 프로세스를 동시에 실행하는 것 처럼 보이게 하기 위해서, 실제로 CPU가 여러 프로세스를 빠르게 전환하면서 처리하고 있다는게 중요한 포인트입니다.
그렇다면 그 과정을 좀 세분화해서 알아봅시다.
1. 현재 프로세스의 상태(context) 저장:
- CPU는 현재 실행 중인 프로세스의 상태( 즉, 레지스터 값, 프로그램 카운터(PC) 등)를 커널 메모리에 저장합니다.
- 이 상태 정보는 현재 프로세스가 실행 중이던 위치( 어떤 명령어를 실행하고 있었는지)를 포함하며, 나중에 해당 프로세스가 다시 실행될 때 이 정보를 복원할 수 있도록 합니다.
2. 다음 프로세스 상태(context) 복원:
- 커널은 다음에 실행할 프로세스의 상태( 즉, 이전에 저장한 레지스터 값과 PC 값)를 CPU에 복원합니다.
- 이 복원된 상태 정보는 CPU가 이 프로세스를 계속 실행하도록 해줍니다.
3. 프로세스 전환:
- CPU는 현재 실행 중인 프로세스를 중단하고, 새로운 프로세스를 실행하기 위해 상태를 복원합니다.
- 이 과정에서 프로세서는 다음에 실행될 프로세스의 명령어를 실행하고, 이전에 중단된(현재) 프로세스는 나중에 다시 실행될 수 있도록 저장된 상태로 복원합니다.
이런 과정을 거치면서,
레지스터 값 저장 및 복원(PC, 스택 포인터, 각종 데이터 레지스터 등)을 통해, 데이터를 복원하고
커널은 메모리 접근 제한으로 각 프로세스의 메모리 접근을 제어하고, 다른 프로세스의 메모리에 접근하지 못하게 합니다.
타이머 인터럽트를 통해 운영 체제가 멀티태스킹을 효율적으로 처리하고 프로세스 전환(컨텍스트 스위칭)을 주기적으로 수행하도록 돕습니다.
이런 구조가 어떻게 작동하는지, 그리고 애플리케이션이 직접 프로세스를 생성하고 제어하는 방법에 대해서는
8장에서 더 자세히 다룰 예정입니다.
1.7.2 Threads
보통 우리는 하나의 프로세스가 하나의 흐름(제어 흐름, control flow)만 가진다고 생각하지만,
현대 시스템에서는 하나의 프로세스가 여러 개의 실행 단위(스레드, threads)로 구성될 수 있습니다.
이 스레드들은 하나의 프로세스 내부에서 실행되며, 같은 코드와 전역 데이터를 공유합니다.
스레드는 네트워크 서버에서 동시에 많은 요청을 처리해야 하는 경우 등,
동시성(concurrency)이 요구되는 환경에서 점점 더 중요한 프로그래밍 모델이 되고 있습니다.
스레드 간 데이터 공유는 프로세스 간 데이터 공유보다 훨씬 쉽기 때문입니다.
- 프로세스끼리는 각각 독립된 메모리를 사용합니다.
- 스레드는 같은 메모리 공간을 공유하므로 데이터 교환이 간단합니다.
또한, 일반적으로 스레드는 프로세스보다 더 효율적입니다.
- 새 스레드를 만드는 것은 새로운 프로세스를 만드는 것보다 더 빠르고 비용이 적게 듭니다.
멀티코어 CPU가 있는 경우, 멀티스레딩은 프로그램을 더 빠르게 실행할 수 있는 중요한 방법 중 하나입니다.
이 내용은 1.9.2절에서 더 자세히 설명할 예정입니다.
12장에서는 동시성(concurrency)의 기본 개념과 함께, 스레드를 사용하는 프로그램을 작성하는 방법에 대해 배우게 될 것입니다.
1.7.3 Virtual Memory
가상 메모리(Virtual Memory)는 각 프로세스가 메인 메모리를 독점해서 사용하는 것처럼 보이게 해주는 추상화 개념입니다.
이를 통해 프로세스 간의 격리와 메모리 보호가 가능해지며, 물리적 메모리가 부족한 상황에서도 디스크 공간을 메모리처럼 사용할 수 있게 됩니다.
- 실제로는 여러 프로세스가 같은 물리 메모리를 공유하지만, 각 프로세스는 자신만의 메모리 공간을 가진 것처럼 보이며, 다른 프로세스의 메모리에 접근할 수 없습니다. 이는 프로세스 간의 보호를 가능하게 합니다.
가상 메모리 시스템에서, 운영 체제는 프로세스가 사용할 특정 메모리 공간을 제공합니다. 이는, 모든 프로세스는 동일하고 일관된 형태의 메모리 구조를 보이는데, 이를 가상 주소 공간(virtual address space)이라고 합니다.
가상 주소 공간은 프로세스가 사용하는 논리적 주소로, 프로세스가 직접 사용하는 주소입니다.
→ 예: 모든 프로그램은 가상 주소 0번지부터 시작하는 메모리를 사용하는 것처럼 보입니다. 실제로는 이 가상 주소는 운영체제에 의해 물리 메모리 주소로 운영 체제가 뒤에서 매핑해줍니다.
모든 프로세스는 가상 주소 공간을 같은 방식으로 사용하는데, 이는 보통 텍스트 영역, 데이터 영역, 힙 영역, 스택 영역으로 나누어집니다.
이런 메모리 영역의 구분은 모든 프로세스에서 동일하게 적용되며, 모든 프로세스는 가상 주소 공간 내에서 같은 구조로 메모리를 사용합니다.
다만 각 프로세스마다 고유한 가상 주소 공간을 가지므로, 실제 물리 메모리에서 위치는 다릅니다.
실제 물리적 메모리는 RAM을 의미하고, 여러 프로세스가 이 물리 메모리를 공유하고 있습니다.
각 프로세스는 가상 주소 공간을 사용하고, 운영 체제는 이 가상 주소를 실제 물리적 주소로 매핑하여 프로세스가 필요한 데이터를 실제 물리적 메모리에서 처리할 수 있게 합니다.
가상 메모리의 작동 원리
1. 가상 주소와 물리 주소 매핑
가상 메모리는 운영 체제가 제공하는 메모리 관리 기법입니다.
이 기법의 목표는 각 프로세스에 독립적인 메모리 공간을 제공하고, 메모리를 효율적으로 사용할 수 있도록 돕는 것입니다.
즉, 운영 체제가 가상 메모리를 통해 주소 공간을 관리하며, MMU가 실제 주소 변환을 수행합니다.
이는 아래에서 더 자세히 설명합니다.
위의 설명대로 한다면, 각 프로세스는 가상 주소를 사용하고, 이 가상 주소는 운영 체제에 의해 물리 주소로 변환됩니다.
이 주소 변환 과정은 페이지 테이블(Page Table)이라는 데이터 구조를 사용하여 이뤄집니다. 페이지 테이블은 가상 주소가 어떤 물리 주소에 매핑되는지를 기록한 표입니다. 즉, 가상 페이지 번호를 물리 페이지 번호로 매핑하는 일종의 사전입니다.
2. 페이징(Paging)
가상 주소 공간과, 물리 메모리는 동일한 크기의 블록으로 나뉘는데, 이를 가상 메모리에서 페이지(Page)라고 하며, 물리 메모리에서는 프레임(frame)으로 부릅니다.
페이징은 이 페이지들을 프레임에 대응시켜 매핑하는 메모리 관리 기법을 말합니다.
운영 체제가 페이지 테이블을 이용해 어떤 가상 페이지가 어떤 물리 프레임에 대응되는지 기록합니다.
이렇게 되면, 페이지들은 연속되지 않은 프레임에 자유롭게 매핑이 가능하게 되며, 연속된 물리 메모리 공간이 필요없습니다. 덕분에 연속성을 요구하는 외부 단편화는 사라지게 되고, 메모리 단편화를 줄이고, 디스크를 사용하거나 프로세스를 메모리에서 넣거나 뺄 때 큰 이동 없이 조정할 수 있으며, 자유로운 매핑의 특징으로 인해 유연한 메모리 사용을 가능하게 합니다.
페이징은 가상 주소와 물리 주소를 분리하는 구조이기에, 모든 프로세스는 가상 주소의 시작 주소로 0부터 시작해도 페이지 테이블을 통해 서로 다른 물리 프레임에 매핑됩니다. 즉, 가상 주소 공간을 추상화하여 물리 주소와 무관하게 프로세스가 실행가능하게 만듭니다.
각 프로세스는 자신만의 페이지 테이블을 갖고 있고, 운영 체제는 다른 프로세스의 페이지에 접근할 수 없도록 보호합니다. 이 구조 덕분에 다른 프로세스 간 메모리 간섭을 방지하는 보안을 강화시킬 수 있습니다.
여기서 연속된 공간이 없어도 된다는 것이 페이징의 핵심 목적입니다.
페이지 교체(Page Replacement)는 물리 메모리의 프레임이 가득 찼을 때, 새로운 페이지를 로드하기 위해 기존 페이지 중 선택해 디스크로 내보내는 방식입니다.
이는 새로운 페이지를 메모리에 로딩하려 할 때, 이미 공간이 부족한 경우일 때 이 방식이 사용됩니다.
어떤 페이지를 쫒아낼지 결정하는 것이 페이지 교체 알고리즘입니다.
이는 메모리가 부족한 순간에 작동하는 보조 메커니즘입니다.
요구 페이징(Demand Paging)은 필요한 데이터를 메모리에 올리고, 그 전까지 디스크에 남겨두는 가상 메모리 기법 입니다. 프로그램 실행 시 전체 메모리를 한 번에 로드하지 않으며, 실제로 접근하는 페이지만 메모리에 로딩하고, 메모리를 디스크의 캐시처럼 동작하게 하는 방식입니다.
옛날에는 스와핑(Swapping) 방식을 사용했지만, 현대 운영 체제는 페이지 방식을 사용하고 있습니다.
리눅스 프로세스의 가상 주소 공간 구조는 그림 1.13에 나와 있으며, 다른 유닉스 시스템들도 유사한 구조를 사용합니다.
리눅스에서 가상 주소 공간의 최상단 영역은, 모든 프로세스가 공통적으로 사용하는 운영 체제의 코드와 데이터를 위해 예약된 공간입니다.
→ 예: 커널 코드, 시스템 호출 테이블 등

주소 공간의 하단 영역은 사용자 프로세스가 정의한 코드와 데이터가 저장되는 영역입니다.
→ 예: 사용자가 작성한 함수, 변수, 스택, 힙 등
참고로, 그림 속 주소는 아래에서 위로 갈수록 커지는 방향으로 그려져 있습니다.
즉, 주소값이 위로 올라갈수록 가상 주소값이 증가합니다.
각 프로세스가 바라보는 가상 주소 공간은 여러 개의 명확히 정의된 구역으로 구성되어 있으며, 각 구역은 특정한 목적을 가지고 있습니다.
Heap(힙)은 아래에서 위로 주소가 커지고,
동적 메모리 할당이 이뤄질 때, 더 높은 주소 쪽으로 확장됩
Stack(스택)은 위에서 아래로 주소가 감소합니다.
함수 호출이 일어날 때마다 더 낮은 주소 쪽으로 스택 프레임이 쌓입니다.
이 방식은 힙과 스택이 중간에 서로 반대 방향으로 성장하게 하여 충돌을 늦추거나, 방지하기 위함입니다.
서로 함수 호출이 깊어지고, 동적 메모리를 할당하게 되어, 두 영역이 중간에서 마주칠 때, 운영 체제가 스택 오버플로우( 힙 오버플로우)를 감지하여 방지할 수 있습니다.
이 구역들에 대해 더 자세한 내용은 책 뒷부분에서 배우게 되지만, 지금은 가장 낮은 주소 영역부터 위로 올라가며 간단히 개요를 살펴보겠습니다.
1. 프로그램 코드와 데이터 (Program code and data)
모든 프로세스에서 코드는 고정된 시작 주소에서 시작되며, 그 뒤를 이어서 전역 C 변수에 해당하는 데이터 영역이 배치됩니다.
이 코드와 데이터 영역은 실행 파일(예: hello 실행 파일)의 내용을 기반으로 초기화됩니다.
즉, 컴파일된 코드가 그대로 메모리로 로딩되어 실행됩니다.
이 영역에 대해 더 자세한 내용은 7장에서 링킹과 로딩을 배울 때 학습합니다.
2. 힙 (Heap)
코드와 데이터 영역 다음에는 런타임 힙(heap)이 위치합니다.
코드와 데이터 영역은 프로세스가 시작될 때 크기가 고정되지만, 힙 영역은 실행 도중 동적으로 크기가 늘어나거나 줄어들 수 있습니다.
→ 이는 C의 malloc(), free() 같은 표준 라이브러리 함수 호출에 따라 변합니다.
힙의 작동 원리에 대해서는 9장에서 가상 메모리 관리를 다룰 때 자세히 배웁니다.
3. 공유 라이브러리 (Shared libraries)
가상 주소 공간의 중간쯤에는, C 표준 라이브러리, 수학 라이브러리 등과 같은 공유 라이브러리의 코드와 데이터가 저장되는 구역이 있습니다.
공유 라이브러리(shared library)는 매우 강력한 개념이지만 약간 복잡한 구조를 가지고 있습니다.
공유 라이브러리는 여러 프로그램이 하나의 라이브러리 코드를 함께 사용함으로써 메모리 절약과 유지 관리의 효율성을 높이는 기술입니다.
이 라이브러리가 어떻게 작동하는지는 7장에서 동적 링킹(dynamic linking)을 배울 때 자세히 다룹니다.
4. 스택 (Stack)
사용자 가상 주소 공간의 가장 위쪽에는, 컴파일러가 함수 호출을 처리하기 위해 사용하는 사용자 스택(user stack)이 존재합니다.
힙(heap)처럼, 스택도 실행 중에 동적으로 크기가 커졌다 작아졌다 합니다.
함수를 호출하면 → 스택이 커지고, 함수에서 반환(return)하면 → 스택이 줄어듭니다.
스택은 함수 호출 정보를 저장하는 공간이며, 함수 호출 시 지역 변수, 매개변수, 반환 주소 등이 스택에 쌓입니다.
이 스택이 컴파일러에 의해 어떻게 사용되는지는 3장에서 배울 것입니다.
5. 커널 가상 메모리 (Kernel virtual memory)
가상 주소 공간의 가장 최상단 영역은 운영 체제 커널을 위해 예약된 영역입니다.
일반 애플리케이션 프로그램은 이 영역의 내용을 직접 읽거나 쓸 수 없으며, 커널 코드에 정의된 함수를 직접 호출할 수도 없습니다.
대신, 프로그램은 시스템 콜(system call)을 통해 운영 체제에게 요청을 보내고, 운영 체제가 해당 작업을 대신 수행합니다.
커널 메모리는 일반 사용자 접근이 차단된 보호된 영역이며, 시스템 자원을 다루려면 반드시 운영 체제를 통해 간접적으로 접근해야 합니다.
가상 메모리(Virtual Memory)가 제대로 작동하려면?
하드웨어와 운영 체제 소프트웨어 간의 정교한 상호 작용이 필요합니다.
여기에는 특히, 프로세서가 명령어를 실행하면서, 메모리에 접근하기 위해 필요한 가상 주소를 하드웨어가 변환(주소 매핑)하는 작업이 포함됩니다.
즉, CPU가 접근하려는 가상 주소를 실제 물리 주소로 자동으로 변환해주는 장치가 필요합니다.
이를 위한 하드웨어가 MMU (Memory Management Unit)입니다.
MMU가 물리 주소로 변환해주고, 이 과정을 주소 매핑(address translation) 또는 주소 변환이라고 합니다.
이러한 매핑 정보는 페이지 테이블에 저장되어 있습니다.
가상 메모리의 기본 아이디어는 다음과 같습니다.
프로세스의 가상 메모리 내용 전체를 디스크에 저장해두고, 실제로 실행할 때는 메인 메모리를 디스크의 캐시(cache)처럼 사용하는 것입니다.
즉, 필요한 데이터만 메모리에 불러오고, 나머지는 디스크에 보관해두는 방식입니다.
이 덕분에 프로그램은 실제 메모리보다 훨씬 큰 주소 공간을 사용하는 것처럼 작동할 수 있습니다.
이 방식은 가상 메모리의 일반적인 페이지 기반 페이징(Paging) 기법입니다.
디스크에 저장된 데이터를 필요할 때 메모리로 불러오고, 메모리가 부족해지면, 다시 디스크로 일부를 내보내는 방식으로, 이로써 물리 메모리보다 훨씬 큰 주소 공간을 사용할 수 있는 착시(추상화)를 제공합니다.
9장에서는 이 가상 메모리 시스템이 어떻게 작동하는지, 그리고 왜 이것이 현대 컴퓨터 시스템에서 매우 중요한 기능인지에 대해 설명할 것입니다.
1.7.4 Files
파일은 바이트의 연속에 불과합니다. 그 이상도 그 이하도 아닙니다.
파일은 단순히 바이트들이 모여 있는 형태이며, 그 자체로 복잡한 구조를 가지고 있지 않습니다.
모든 입출력 장치(I/O 장치), 예를 들어 디스크, 키보드, 디스플레이, 심지어 네트워크까지도 파일로 모델링됩니다.
이는 Unix 계열 시스템의 핵심 철학 중 하나입니다.
유닉스 및 유닉스 계열 시스템의 추상화 모델을 기반으로 파일 추상화를 설명하겠습니다.
하드웨어와 소프트웨어 간의 데이터 전달은 파일을 통해 이루어지므로, 모든 장치가 파일로 취급됩니다.
이는 운영 체제는 하드웨어 장치를 특별하게 취급하지 않고, 읽고 쓰기가 가능한 파일처럼 추상화 합니다.
이는 프로그래머 입장에서 모든 장치와의 통신이 일관된 방식으로 사용가능하게 할 수 있습니다.
구조적으로 본다면, 운영 체제는 I/O 장치마다 장치 드라이버를 통해 하드웨어를 직접 제어하지만,
애플리케이션에게는 그걸 파일처럼 보이게 합니다.
예를들어, /dev/tty는 실제 물리 장치가 아닌, 커널 내부에서 구현해 제공하는 장치를 가리키는 특수 파일로, 장치 파일입니다. 다른 말로 가상 장치 파일(Virtual Device File), 캐릭터 디바이스 파일(Character Device File)이라 불립니다.
이와 같은 장치 파일은 장치 번호(major/minor)정보가 존재하고, 어느 핸들러(드라이버 코드)로 연결하고, 해당 핸들러 안에서 구체적으로 어떤 논리 장치인지 지정하여 식별시키고, 이를 특정 커널 코드에 연결해 동작을 제공합니다.
여기서 커널이 장치 번호(major/minor)를 등록하고, 캐릭터 디바이스는 커널의 함수 테이블과 연결되며, 이후 cdev라는 캐릭터 디바이스의 커널 객체를 통해 커널에 등록됩니다. 유저 프로그램이 read()와 같은 시스템 콜을 호출하면, 커널은 해당 연결된 장치 번호를 통해 해당 캐릭터 디바이스를 찾아 등록된 함수 테이블의 read() 핸들러를 실행합니다.
시스템 내에서 모든 입력과 출력은 파일을 읽고 쓰는 방식으로 수행됩니다.
터미널 입력(키보드), 디스플레이 출력(모니터), 디스크, 네트워크, 어댑터 장치 등
입출력 장치도 역시, 전부 다 운영 체제 관점에서는 파일처럼 취급됩니다.
이때 사용되는 시스템 콜은 Unix I/O 시스템 콜 또는 POSIX I/O 라는 작은 집합에 속합니다.
프로그램은 파일을 읽고 쓰는 방식으로 데이터를 처리하며, 이를 위해 시스템 콜을 사용합니다.
파일이라는 이 개념을 통해, 시스템에 존재할 수 있는 다양한 I/O 장치들을 동일한 방식으로 애플리케이션에 적용하여 다룰 수 있습니다.
즉, 파일을 사용하면 다양한 입출력 장치를 구체적인 하드웨어 차이를 신경 쓰지 않고도 동일한 방식으로 다룰 수 있습니다.
운영체제는 I/O 장치를 파일 형태로 추상화하여, 개발자는 이를 통해 하드웨어의 복잡한 작동 방식을 몰라도 쉽게 접근할 수 있습니다.
예를 들어, 디스크 파일의 내용을 조작하는 애플리케이션 프로그래머는 디스크 기술(저장 장치의 내부 구조와 동작 방식) 이 무엇인지에 대해 알지 못해도 작업을 진행할 수 있습니다.
파일을 다루는 방식은 하드웨어 세부 사항과 독립적으로 작동하므로( 운영 체제는 하드웨어 저장 장치의 세부 구현을 감추고, 사용자와 개발자에게 '파일'이라는 일관된 인터페이스만 제공한다는 의미), 개발자는 어떤 디스크 기술을 사용하는지 몰라도 동일하게 작업을 할 수 있습니다.
또한, 같은 프로그램이 서로 다른 디스크 기술을 사용하는 시스템에서도 동일하게 실행될 수 있습니다.
이처럼 파일 시스템을 통한 입출력 처리는 하드웨어에 독립적( 디스크 종류, 버스 종류, 저장 방식, 위치 등에 영향을 받지 않고 동일하게 처리할 수 있다 )이기 때문에, 여러 시스템 간에 호환성이 보장됩니다.
Unix I/O에 대한 자세한 내용은 10장에서 배울 것입니다.