[Operating System] – “운영체제란?”
[Operating System] – “운영체제란?”
이 글은 도서 Operating Systems: Three Easy Pieces 를 읽고, 제가 이해한 방식대로 정리한 것임을 밝힙니다.
blog-driven-development 나 blog-driven-learning 은 위험합니다. 저의 글을 끊임없이 의심하고, 검증하고, 혹시라도 틀린 내용이 있거나, 논리적 비약이 있다면 가감없이 알려주시면 감사하겠습니다 :)
운영체제의 역할과 책임
우리는 컴퓨터를 사용합니다. 필자의 첫 번째 컴퓨터는 동네 찜질방의 pc방이었습니다. 그 때만 하더라도, 담배 냄새가 많이 났고, 사람들은 뭐에 홀린 듯이 헤드셋을 착용한 채로 컴퓨터 화면에 집중하고 있었죠.
초등학생이었던 저는 다른 사람이 하던 게임을 눈여겨보게 되었고, 그 때부터 카트라이더라는 게임에 빠져들었고 컴퓨터와 친해지게 되었더랍니다. 전원 버튼을 누르면 나오는, 하지만 이제는 들을 수 없는 평화로운 부팅음, 펄럭거리는 듯한 깃발 모양의 아이콘, 꿈에서 나올 것만 같은 초원 사진의 바탕 화면. 알아채셨겠지만, 저의 첫 번째 운영체제는 Windows xp 였습니다.
이처럼, 운영체제가 무엇인지는 모르지만 운영체제는 현재 컴퓨터를 사용하기 위해서 필수적인 요소 중 하나가 되었습니다. 컴퓨터 관련 학과에 입학하기 전의 필자도, 운영체제가 대충 무엇을 의미하는지는 알고 있었으니 말이죠. 하지만 개발자가 되기로 선택한 그 시점부터, 컴퓨터를 다뤄 큰 일을 도모하고 싶은 순간부터, 우리는 운영체제가 무엇이고, 어떤 일을 하고, 어떻게 그 일들을 하는지에 대해 알아야 합니다.
다시 게임 이야기로 돌아가 봅시다. 게임 프로그램이 실행되면, 어떤 일이 일어나죠? 컴퓨터가 알아들을 수 있는 명령어의 집합인 프로그램이 실행되면 컴퓨터는 단순한 일들을 수행합니다. 프로세서는 프로그램을 읽고, 프로그램에 적혀 있는 명령어들을 반입하고, 명령어를 해석하고 실행합니다. 그리고 이 과정은 프로그램이 종료될 때까지 반복되죠.
운영체제는 프로그램을 쉽게 실행하고, 메모리 공유를 가능케 하고, 동시에 여러 프로그램을 실행하는 등의 여러 일을 수행합니다. 그리고 게임과 같은 중요한 프로그램들을 정확하고 올바르게 동작시킬 책임을 가지며, 프로세서나 메모리, 디스크와 같은 물리적 자원들을 사용자가 직접 하드웨어를 다루지 않고도 편리하게 다룰 수 있도록 가상화를 수행하기도 하죠. 이는 운영체제가 컴퓨터의 자원을 효율적으로, 공정하게 관리하는 역할을 한다는 것을 의미합니다.
운영체제의 역할 1: 가상화
운영체제는 프로세서가 하나밖에 없음에도, 프로그램 여러 개가 동시에 실행되는 것만 같은 마법을 부립니다. 실제로, 아래의 프로그램을 살펴보고 실행한다면 그 마법이 실재한다는 것을 알 수 있죠.
// common.h
#ifndef __common_h__
#define __common_h__
#include <sys/time.h>
#include <sys/stat.h>
#include <assert.h>
double GetTime()
{
struct timeval t;
int rc = gettimeofday(&t, NULL);
assert(rc == 0);
return (double)t.tv_sec + (double)t.tv_usec / 1e6;
}
void Spin(int howlong)
{
double t = GetTime();
while ((GetTime() - t) < (double)howlong)
;
}
#endif
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <assert.h>
#include "common.h"
/**
* 사용자에게서 문자열을 입력받아, 1초마다 출력하는 프로그램
*/
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "usage: cpu <string>\n");
exit(1);
}
char *str = argv[1];
while (1)
{
Spin(1);
printf("%s\n", str);
}
return 0;
}
./cpu "Sup, OS~1" & ./cpu "Sup, OS~2" & ./cpu "Sup, OS~3" & ./cpu "Sup, OS~4"
위의 C 프로그램을 컴파일하고 실행한다면, 아래와 같은 결과를 볼 수 있습니다:
이처럼, 어떤 방법인지는 모르지만 운영체제는 CPU를 가상화하여 사용자가 여러 개의 CPU 가 존재하는 듯한 환상을 만들어냅니다. 과연 운영체제는 어떤 상황에서 어떤 프로그램을 실행해야 하고, 어떤 상황에서 이 프로그램을 종료해야 하고, 어떤 상황에서 프로세서를 할당시켜 작업을 수행할까요?
또한 운영체제는 메모리를 가상화하여 각 프로세스가 자신만의 가상 주소 공간을 가지도록 합니다. 이렇게 함으로서 각 프로그램이 수행하는 메모리 연산들은 다른 프로그램의 메모리 주소 공간에 영향을 주지 않는데, 운영체제는 어떻게 각 프로그램들이 자신만의 메모리 공간을 가지고 있는 것처럼 보이도록 하는 걸까요?
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
/**
* 메모리를 동적으로 할당하고 1초마다 1씩 증가시키는 프로그램
*/
int main(int argc, char *argv[])
{
int *p = malloc(sizeof(int));
assert(p != NULL);
printf("(%d) address pointed to by p: %p\n", getpid(), (unsigned)p);
*p = 0;
while (1)
{
Spin(1);
*p = *p + 1;
printf("(%d) p: %d\n", getpid(), *p);
}
return 0;
}
다음 글에서, 어떻게 운영 체제가 위의 작업들을 수행하는지 알아볼 것입니다.
해당 주제의 모든 포스트들을 아래에서 확인하세요!
CPU 가상화
운영체제의 역할 2: 병행성
위에서 운영체제가 수행하는 마법
실습 코드를 실행해 보았다면, 그 예에서 운영체제가 한 번에 여러 프로세스를 실행시켜 한 번에 많은 일들을 해왔다는 것을 알아채셨을 것입니다. 어떤 방법으로 하는지는 몰라도 말입니다.
하지만, 프로그램이 동시에 많은 일들을 해야 할 때에 프로그램에는 여러 문제가 발생하게 됩니다.
// common_threads.h
#ifndef __common_threads_h__
#define __common_threads_h__
#include <pthread.h>
#include <assert.h>
#include <sched.h>
#ifdef __linux__
#include <semaphore.h>
#endif
#define Pthread_create(thread, attr, start_routine, arg) assert(pthread_create(thread, attr, start_routine, arg) == 0);
#define Pthread_join(thread, value_ptr) assert(pthread_join(thread, value_ptr) == 0);
#define Pthread_mutex_lock(m) assert(pthread_mutex_lock(m) == 0);
#define Pthread_mutex_unlock(m) assert(pthread_mutex_unlock(m) == 0);
#define Pthread_cond_signal(cond) assert(pthread_cond_signal(cond) == 0);
#define Pthread_cond_wait(cond, mutex) assert(pthread_cond_wait(cond, mutex) == 0);
#define Mutex_init(m) assert(pthread_mutex_init(m, NULL) == 0);
#define Mutex_lock(m) assert(pthread_mutex_lock(m) == 0);
#define Mutex_unlock(m) assert(pthread_mutex_unlock(m) == 0);
#define Cond_init(cond) assert(pthread_cond_init(cond, NULL) == 0);
#define Cond_signal(cond) assert(pthread_cond_signal(cond) == 0);
#define Cond_wait(cond, mutex) assert(pthread_cond_wait(cond, mutex) == 0);
#ifdef __linux__
#define Sem_init(sem, value) assert(sem_init(sem, 0, value) == 0);
#define Sem_wait(sem) assert(sem_wait(sem) == 0);
#define Sem_post(sem) assert(sem_post(sem) == 0);
#endif // __linux__
#endif // __common_threads_h__
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
#include "common_threads.h"
volatile int counter = 0;
int loops;
void *worker(void *arg)
{
int i;
for (i = 0; i < loops; i++)
{
counter++;
}
return NULL;
}
/**
* 두 개의 스레드를 생성한 후, 공유 변수 counter 값을 증가시키는 프로그램
*/
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "usage: threads <loops>\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;
}
위의 프로그램을, 심호흡 두 번 정도 하고 살펴봅시다. 프로그램에서는 Pthread_create
를 통해 쓰레드 두 개를 만들고, worker
를 통해서 루프를 반복하며 카운터의 값을 증가시킵니다.
예컨대, loops
가 1000 이라면 어떤 결과가 나타날까요?
// counter 는, 위에서 1로 초기화되었으니 0임이 자명하고..
printf("Initial value : %d\n", counter);
// 두 개의 쓰레드를 생성하고, worker 안에서 loops 값만큼 counter 의 값을 증가시키니,
Pthread_create(&p1, NULL, worker, NULL);
Pthread_create(&p2, NULL, worker, NULL);
Pthread_join(p1, NULL);
Pthread_join(p2, NULL);
// Final Value 는 2000이 되겠구나!
printf("Final value : %d\n", counter);
하지만, 결과는 뭔가 다르게 나타납니다.
왜 이런 일이 나타나는 걸까요? 과연 멀티 쓰레드 환경에서 우리가 예측한 것처럼 올바르게 동작하는 프로그램은 어떻게 작성되어야 마땅할까요? 운영체제는 이런 병행성 문제를 해결하기 위해서 어떤 기법들을 준비해 두었을까요?
운영체제의 역할 3: 영속성
모든 컴퓨터에서 “자료를 저장하는 것” 은 필수적인 기능 중 하나입니다. 그것은 우리가 작성한 코드일 수도 있고, 게임 실행에 필요한 바이너리 파일들일 수도 있고, 회사에 제출하기 위한 소중한 사직서가 될 수도 있죠. 운영체제는 파일 시스템이라는 소프트웨어를 사용해 컴퓨터의 디스크에 파일을 저장합니다.
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
/**
* /tmp/file 위치에 파일을 생성하고, hello world 문자열을 쓰는 프로그램
*/
int main(int argc, char *argv[])
{
int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
assert(fd >= 0);
char buffer[20];
sprintf(buffer, "hello world\n");
int rc = write(fd, buffer, strlen(buffer));
assert(rc == (strlen(buffer)));
fsync(fd);
close(fd);
return 0;
}
운영체제는 사용자들에게 메모리나 디스크 접근 등 여러 가지 기능을 제공하기 위해 API 를 제공합니다. 그리고 응용 프로그램들이 사용 가능한 API 들을 시스템 콜이라고 하는데, 그렇기 때문에 우리는 위의 예제 프로그을 사용하여 컴퓨터를 뜯어 메모리의 값이나 프로세서, 디스크를 조작하지 않고도 딸깍 한 번에 파일을 쓰거나, 삭제하거나, 조작할 수 있는 것이죠. 과연 운영체제의 파일 시스템은 데이터를 어떻게 저장할까요? 데이터를 저장하기 위해서 운영체제가 사용하는 기법은 어떤 것일까요?
요약
우리는 위에서 예제 코드들과 함께, 운영체제가 어떤 복잡한 일들을 처리하는지 간략하게 살펴보았습니다.
- 운영체제는 CPU, 메모리와 같은 물리 자원을 가상화합니다.
- 운영체제는 병행성과 관련된 여러 문제들을 처리합니다.
- 운영체제는 파일들을 영속적으로 저장하여, 안전하게 보관할 책임도 가집니다.
다음 포스트에서, 우리는 프로세스의 개념을 살펴보고, 운영체제의 첫 번째 역할에서 논의했던 “CPU 가 여러 개 존재한다는 환상을, 운영체제는 어떻게 만들어내는가?” 에 대해 알아볼 것입니다.