운영체제는 무엇이고, 왜 필요할까?
첫 수업은 운영체제가 무엇이며, 어떤 핵심적인 역할을 수행하는지에 대한 이야기로 시작되었다. 가장 단순하게 생각하면, 프로그램은 그저 일련의 명령어 집합이다. CPU는 메모리에서 이 명령어들을 하나씩 가져와(Fetch), 해석하고(Decode), 실행하는(Execute) 과정을 반복할 뿐이다.
What happens when a program runs? |
하지만 만약 운영체제가 없다면, 모든 프로그램은 하드웨어 자원(CPU, 메모리, 디스크 등)에 직접 접근하고 제어해야 하는 엄청난 부담을 갖게 된다. 이는 마치 가게에 키오스크나 점원 없이 손님들이 마음대로 주방에 들어와 조리 기구를 사용하는 것과 같다. 금세 엉망이 될 것이다.
1. 자원 관리자 (Resource Manager)
운영체제의 첫 번째 핵심 역할은 자원 관리자이다. 컴퓨터의 물리적 자원은 한정되어 있는데, 수많은 프로그램이 동시에 이 자원을 사용하려고 경쟁한다. OS는 이 혼란을 중재하고, 모든 프로그램이 자원을 공정하고 효율적으로 나눠 쓸 수 있도록 관리한다.
CPU: 어떤 프로그램이 CPU를 사용할지 스케줄링한다.
메모리: 각 프로그램에 필요한 메모리 공간을 할당하고, 다른 프로그램의 영역을 침범하지 않도록 보호한다.
디스크: 파일 생성, 삭제, 읽기, 쓰기 등 디스크 접근을 관리한다.
2. 시스템 콜 (System Call)
OS는 프로그램이 하드웨어에 직접 접근하는 것을 막는 대신, 시스템 콜이라는 표준화된 인터페이스를 제공한다. 프로그램이 파일 읽기, 화면 출력, 네트워크 통신 등 특별한 권한이 필요한 작업을 하고 싶을 때, OS에게 정식으로 요청하는 창구다. 이는 가게에서 손님이 주방에 들어가는 대신 키오스크를 통해 주문하는 것과 같다. OS는 이 요청을 받아 안전하다고 판단될 때만 해당 작업을 대신 수행해주고 결과를 반환한다. 이를 통해 시스템 전체의 안정성과 보안을 유지할 수 있다.
정리하자면, 교재의 표현을 빌려 OS는 한정된 물리적 자원을 관리하고, 사용자 프로그램에게는 사용하기 쉬운 '가상'의 형태를 제공해주는 소프트웨어다. 오늘은 그 핵심 개념인 가상화(Virtualization), 동시성(Concurrency), 영속성(Persistence)에 대해 배운 내용을 정리해보고자 한다.
1. 가상화 (Virtualization): 어떻게 자원을 '내 것처럼' 쓸까?
컴퓨터에는 물리적인 CPU나 메모리가 한정적으로 존재하지만, 우리는 수십 개의 프로그램을 동시에 실행하는 데 익숙하다. 마치 각 프로그램이 자신만의 CPU와 메모리를 점유하고 있는 것처럼 말이다. 이렇게 물리적 자원을 가상의 형태로 변환하여 프로그램에게 제공하는 것을 가상화라고 한다. OS는 이 과정을 통해 복잡한 하드웨어 제어를 숨기고, 프로그램이 쉽고 안전하게 자원을 사용하도록 돕는다.
CPU 가상화
하나의 CPU를 여러 개인 것처럼 보이게 만드는 기술이다. 수업에서 들었던 '아이스크림 가게' 비유가 인상 깊었다. 직원은 한 명인데 주문은 수십 개가 밀려드는 상황과 같다. 이때 현명한 가게 주인(OS)은 새 직원을 계속 뽑는 대신, 한 명의 직원(CPU)이 여러 주문(프로그램)을 아주 빠른 속도로 번갈아 가며 처리하도록 만든다. 이 '시분할(Time-multiplexing)' 기법 덕분에 사용자 입장에서는 모든 아이스크림이 동시에 만들어지는 것처럼 느끼게 된다.
[Example: cpu.c] 아래는 1초에 한 번씩 주어진 문자열을 무한히 출력하는 간단한 C 코드다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: cpu <string>\n");
exit(1);
}
char *str = argv[1];
while (1) {
sleep(1);
printf("%s\n", str);
}
return 0;
}
이 프로그램을 4개 동시에 실행시키면 결과는 다음과 같다.
prompt> ./cpu A &; ./cpu B &; ./cpu C &; ./cpu D &
[1] 7353
[2] 7354
[3] 7355
[4] 7356
A
B
D
C
A
B
D
C
...
분명 CPU는 하나인데, A, B, C, D가 번갈아 가며 출력된다. 이는 OS가 각 프로그램에 CPU 시간을 매우 짧게 할당하고 빠르게 전환(Context Switching)하기에 가능한 일이다.
메모리 가상화
CPU와 마찬가지로, OS는 각 프로그램이 물리 메모리 전체를 독점적으로 사용하는 듯한 착각을 준다. 각 프로그램은 자신만의 '가상 주소 공간(Virtual Address Space)'을 갖게 된다. '아이스크림 가게' 비유를 다시 가져와 보자. 여러 아이스크림(프로그램)을 만드는데 그릇(메모리)이 하나뿐이라면 레시피가 섞여 엉망이 될 것이다. 이때 주인(OS)은 각 아이스크림마다 별도의 그릇을 제공하는 것처럼 보이게 한다. 실제로는 한정된 물리적 그릇들을 사용하지만, 어떤 아이스크림이 어떤 그릇을 쓰고 있는지 주인이 정확히 기억하고 관리(mapping)해주는 것이다.
[Example: mem.c] 아래 코드는 메모리를 동적으로 할당받아 그 주소를 출력하고, 1초마다 해당 메모리의 값을 1씩 증가시키는 프로그램이다.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main(int argc, char *argv[]) {
int *p = malloc(sizeof(int));
assert(p != NULL);
printf("(%d) address of p: %p\n", getpid(), p);
*p = 0;
while (1) {
sleep(1);
*p = *p + 1;
printf("(%d) p: %d\n", getpid(), *p);
}
return 0;
}
이 프로그램을 두 개 동시에 실행시킨 결과다.
prompt> ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) address of p: 0x1e8a010
(24114) address of p: 0x1e8a010
(24113) p: 1
(24114) p: 1
(24114) p: 2
(24113) p: 2
...
놀랍게도 두 프로그램 모두 완전히 동일한 메모리 주소(예: 0x1e8a010
)를 할당받았다. 하지만 각자의 p
값은 서로에게 영향을 주지 않고 독립적으로 증가한다. 이는 각 프로그램이 물리 주소가 아닌 자신만의 가상 주소를 보고 있기 때문이다. OS가 이 가상 주소를 실제 물리 메모리의 다른 위치로 매핑해주기 때문에 서로 충돌 없이 실행될 수 있는 것이다.
2. 동시성 (Concurrency): 어떻게 여러 작업을 '겹쳐서' 처리할까?
동시성은 여러 작업을 동시에 다루는 OS의 능력을 의미한다. CPU 가상화에서 언급된 '빠르게 번갈아 가며 처리'하는 것과 깊은 관련이 있다. 만약 100개의 아이스크림을 주문한 손님(오래 걸리는 작업) 때문에 뒤에 있는 1개 주문 손님(짧은 작업)이 하염없이 기다려야 한다면 가게의 평판은 나빠질 것이다. OS는 이런 상황을 막기 위해 작업들을 잘게 쪼개고 교대로 실행함으로써 시스템의 전반적인 응답성과 효율을 높인다.
하지만 이 과정에서 문제가 발생하기도 한다. 바로 여러 작업이 '공유 자원'에 동시에 접근할 때다.
[Example: thread.c]
아래 코드는 두 개의 스레드(Thread)가 하나의 공유 변수 counter
를 loops
만큼씩 증가시키는 코드다.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
volatile int counter = 0;
int loops;
void *worker(void *arg) {
for (int i = 0; i < loops; i++) {
counter++;
}
return NULL;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: threads <value>\n");
exit(1);
}
loops = atoi(argv[1]);
pthread_t p1, p2;
printf("Initial value: %d\n", counter);
pthread_create(&p1, NULL, worker, NULL);
pthread_create(&p2, NULL, worker, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
printf("Final value: %d\n", counter);
return 0;
}
loops 값에 1000을 주었을 때는 Final value: 2000
으로 정확한 결과가 나오지만, 100000을 주면 결과는 예측 불가능해진다.
prompt> ./thread 100000
Initial value : 0
Final value: 143012 // huh??
prompt> ./thread 100000
Initial value : 0
Final value: 137298 // what the??
그 이유는 counter++
라는 코드가 원자적(atomic)으로 실행되지 않기 때문이다. 이 한 줄의 코드는 내부적으로 1) 메모리에서 값을 가져오고, 2) 1을 더하고, 3) 다시 메모리에 저장하는 3단계로 나뉜다. 만약 한 스레드가 1, 2단계를 마친 직후 OS가 다른 스레드로 실행을 전환하면, 두 번째 스레드는 아직 갱신되기 전의 옛날 값을 읽어가게 된다. 결국 하나의 증가 연산이 누락되는 '경쟁 상태(Race Condition)'에 빠지는 것이다. 이러한 동시성 문제를 해결하는 것이 OS의 중요한 과제 중 하나다.
(자세한 내용은 지난 스레드에 관한 글을 참고하자.)
3. 영속성 (Persistence): 어떻게 데이터를 '영원히' 보관할까?
지금까지 다룬 메모리(DRAM)는 전원이 꺼지면 모든 데이터가 사라지는 휘발성(volatile) 저장 장치다. 우리가 작업한 파일이나 데이터를 영구적으로 보관하기 위해서는 비휘발성 저장 장치인 디스크(HDD, SSD)가 필요하다. 이처럼 데이터를 전원이 꺼져도 유지되도록 하는 것을 영속성이라고 한다. OS는 파일 시스템(File System)이라는 소프트웨어를 통해 영속성을 관리한다.
'아이스크림 가게' 비유에서는 포장 주문 손님이 나타나지 않을 때, 아이스크림이 녹지 않도록 냉장고(영속적 저장소)에 보관하는 것에 해당한다.
[Example: file_io.c]
아래 코드는 시스템 콜을 사용하여 /tmp/file
이라는 파일을 생성하고 "hello world"라는 문자열을 쓰는 예제다.
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
// O_WRONLY: 쓰기 전용, O_CREAT: 없으면 생성, O_TRUNC: 있으면 내용 삭제
// S_IRWXU: 소유자에게 읽기/쓰기/실행 권한 부여
int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
assert(fd > -1);
int rc = write(fd, "hello world\n", 13);
assert(rc == 13);
close(fd);
return 0;
}
사용자가 파일을 생성(open
), 쓰고(write
), 닫는(close
) 등의 시스템 콜을 호출하면, 파일 시스템은 이 요청을 해석해서 디스크의 어느 위치에 어떻게 데이터를 기록할지 결정하고, 실제 장치에 I/O 요청을 보낸다. 또한 파일 시스템은 데이터를 쓰는 도중 전원이 나가는 등의 예기치 않은 시스템 충돌이 발생해도 데이터가 손상되지 않도록 저널링(Journaling), 카피-온-라이트(Copy-on-Write) 같은 기법을 사용하여 데이터의 무결성을 보장하는 중요한 역할도 수행한다.
결론: 추상화를 제공하는 자원 관리자
정리하자면, 운영체제는 물리 하드웨어를 직접 다루는 복잡함은 숨기고, 가상화, 동시성, 영속성이라는 핵심 개념을 통해 프로그램에게 편리하고 강력한 추상화(Abstraction)를 제공하는 자원 관리자이다. 첫 수업이었지만, 앞으로 우리가 만들 모든 소프트웨어의 기반이 되는 이 시스템의 원리를 이해하는 것이 얼마나 중요한지 다시금 깨닫는 시간이었다.
추천글: