운영체제 OStudy

[OStudy] 1주차 - 아주 쉬운 세가지 이야기

Zeka_P 2025. 9. 1. 21:17

새 포스팅 카테고리가 생겼습니다!

정글에서 뵈었던 형님 누님들과 "운영체제 아주 쉬운 세가지 이야기" 도서를 통한 운영체제 스터디를 진행하게 되었습니다.

아무래도 운영체제의 전반적인 내용을 다루다보니, 일부는 기존 CSAPP 파트와 겹치기도 합니다. 하지만 모든 전산학 파트를 전반적으로 다루는 기존 CSAPP와는 다르게 해당 파트에선 운영체제의 개요와 그 동작 원리, 알고리즘에 집중합니다. 더 딥하게 공부할 기회가 될 것이라 생각합니다! 

매주 월요일, 꾸준히 포스팅 됩니다. 

 

운영체제의 역할

 운영체제는, 쉽게 생각하면 컴퓨터 내에서, 모든 프로그램이 원활히 동작할 수 있도록 이를 상위 레벨에서 전반적으로 관리하는 일종의 관리자 역할을 수행합니다. 말은 정말 쉽지만, 이를 구현한다는 것은 정말 어렵습니다. 당장 PintOS 때를 생각해봐도 충분하죠. 반입(fetch)/해석(decode)/실행(execute)의 명령어가 초에 수백만 또는 수십억번까지도 동작 가능한 프로그램들이, 하나도 아니고 수십개까지도 컴퓨터 안에서 동작할 수 있습니다. 단순히 명령어 수행만 맡지도 않습니다. 이를 위한 자원 할당, 분배, 수거도 필요합니다. 메모리는 하나만 존재하니, 서로 침범되지 않고, 효율적으로 관리될 수 있도록 높은 효율성이 보장되는 방법론에 근거한 정책이 존재해야 합니다. 심지어는 휘발되지 않도록 하기 위해, 이를 어딘가에 저장하기도 해야하죠. 

이런 전반적인 작업을 위해 채택된 기법은 가상화(Virtualization)라는 기법이었습니다.  물리적 자원인 프로세서, 메모리, 디스크 등을 가상 형태의 자원으로 치환시켜, 보다 사용이 편리하고, 강력한 효율을 보일 수 있는, 일종의 논리적 할당 정책을 구축했습니다. 가상화라는 기법 자체가, 한정된 자원의 할당, 유지의 효율성을 극대화시키기 위해 고안된 기법이니 만큼, 깊게 들여다볼수록, 굉장히 심오하게 고안되어 있습니다. 모든 프로그램을 관리하는 운영체제의 기법이니만큼, 무조건적인 정교성이 요구되기 때문에 그런 것도 있죠. 그러니 가상화를 이어 설명하겠습니다.

 

가상화

 결국 가상의 자원을 임의로 형성시킨다는 것은, 물리적으로 한계가 존재하는 하드웨어의 자원을 보다 효율적이며, 그러면서도 모순점이 없는 자원 관리 시스템을 구축한다는 것을 뜻합니다. 

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <assert.h>
#include "common.h"

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "사용법: cpu <문자열>\n");
        exit(1);
    }
    char *str = argv[1];
    while (1) {
        Spin(1);
        printf("%s\n", str);
    }
    return 0;
}

 

사용자의 입력 문자열을, 1초 단위로 반복 출력하는 코드가 존재합니다. 단독 시행하면, 그냥 평범한 무한루프 코드죠. 근데 이를 여러개 띄워 동시 시행한다면 어떻게 될까요? 예를 들어, 4개를 띄우고, A B C D로 나눠 입력을 준다면, A B C D A C B D C B C .... 와 비슷한 출력이 나오게 될 것입니다. 사실 이런 기능들을 당연하게 써온 우리들 입장에선 별 것 아닌 것처럼 보일지도 모릅니다.(유튜브도 보면서 사냥도 하고, 웹툰도 동시에 보잖아요?) 근데 여기서 짚고 가야할 것이 있습니다. 우리가 사용하는 CPU는 단 하나입니다. 즉, 이 A B C D를 출력하는 네 프로그램이, 하나의 CPU를 공유하며 사용하고 있고, 과정이야 어찌됐건(뒤에 배우게됩니다.) 결국 사용자 입장에선 이들이 '동시에' 작동한 것처럼 보이게 만듭니다. 물론 이렇게 출력으로 찍어보면, 동시가 아니란 것을 알 수 있습니다. 운영체제의 가상화는 이렇듯, 일종의 환상을 제공합니다.

하나의 CPU만 존재하는 하드웨어 환경에서도, 마치 여러개의 가상 CPU가 있는 것처럼, 프로그램을 속입니다. 프로그램에게 마치 그 프로그램만의 공간인 것처럼 말하며, CPU를 내어주고, 일정 시간이나 이벤트가 발생하면, 바로 방망이 루실을 가져와 그 프로그램을 기절시킵니다. 그리고 그 프로그램이 사용하며 만들어진 CPU 환경을 기억해둡니다. 다음 프로그램에게 똑같이 그 프로그램만의 것인 양 소개시켜줍니다. 다시 기절시킵니다. 원래 프로그램이 깨어날 때 즈음에, 다시 기억해둔 그 환경으로 되돌립니다. 운영체제는 방 하나로 수십 명을 동시에 묵게 할 수 있는, 숙박업의 귀재라고 할 수 있습니다.

 

이는 메모리에서도 마찬가지입니다. 메모리 가상화 기법에서는, 각 프로그램이 본인들만의 메모리 공간을 가지고 있다고 믿게 만듭니다. 물리적 메모리 공간보다 많은 가상주소 공간을 임의로 할당하며, 프로그램 입장에선, 타 프로그램과 메모리가 공유되지 않는 것처럼 느끼게 만듭니다. 처음엔 이게 낯설게 다가올 수 있습니다. 속인다고 해서, 무슨 이점을 얻을 수 있을까요? 바로, 자원의 배제입니다. 메모리 공간은 한정되어 있으나, 우리의 프로그램들은, 그리고 우리가 동작시키는 규모는 분명 그 메모리의 할당량보다 클 가능성이 높습니다. 이를 위해서, 그때그때 자원할당을 위해 이미 적재된 데이터들을 내보내야하는 일이 생깁니다. 하지만 마냥 내보내면 꼬일 수밖에 없겠죠. 하지만 우리에겐 가상주소가 있습니다. 이 가상주소를 일종의 마킹으로 사용해서 해당 자원이 다시 필요해졌을 때, 즉시 다시 메모리로 적재시키면서, 프로그램 입장에선, 그 자원이 메모리로부터 삭제되지 않았고, 계속 그상태 그대로 존재했다는 것처럼 인식시킬 수 있습니다. 심지어 배제와 적재의 과정에서, 실제 메모리 주소가 달라졌는데도 말이죠. 이는, 물리 메모리 주소가 달라졌어도, 해당 자원의 가상 주소는 동일하기 때문에 가능한 일입니다. 이러고도 과연, 컴퓨터가 외계인의 산물이 아니라 할 수 있을까요? 이걸 사람이 만들었다고??? 진짜???

 

병행성과 영속성

 하지만, 동시에 시행한다는 것은, 꽤 많은 골머리를 앓게 만들기도 합니다. 그중 하나가 이 병행성(Concurrency)입니다. 운영체제만의 이야기가 아닌, 멀티 쓰레드 프로그램에서도 발생하는 이 문제는, 여러 접근 주체들이, 하나의 자원에 접근하면서, 일종의 경쟁 문제가 발생함을 말합니다. 

#include <stdio.h>
#include <stdlib.h>
#include "common.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, "사용법: threads <값>\n");
        exit(1);
    }
    loops = atoi(argv[1]);
    pthread_t p1, p2;
    printf("초기 값 : %d\n", counter);

    Pthread_create(&p1, NULL, worker, NULL);
    Pthread_create(&p2, NULL, worker, NULL);
    Pthread_join(p1, NULL);
    Pthread_join(p2, NULL);
    printf("최종 값 : %d\n", counter);
    return 0;
}

 

위는, 두 쓰레드가 loops 변수를 이용하여 카운터 값을 반복 증가시키는 코드입니다. 100,000과 같은 숫자를 넣었을 때 어떤 값이 나와야 할까요? 두 쓰레드니까 200,000? 하지만 결과는 143,012 같이 엉뚱한 값이 나오게 됩니다. 왜 그럴까요? 해당 프로그램은 단순히 카운터만 증가시키지 않습니다. 카운터 값을 증가시키라는 명령어는, 카운터 값을 메모리에서 레지스터로 복사하라는 명령 하나, 그 레지스터 값을 1 증가시키라는 명령 하나, 그리고 해당 값을 다시 메모리로 옮기라는 명령 하나로 총 셋으로 나누어 존재하게 됩니다. 근데, 이 모든 명령어가 다 동작하기도 채 전에, 이 쓰레드를 방망이로 기절시켜 버린겁니다. 레지스터 값을 1더하고.. 윽!

이런 문제를 해결하기 위해, 이들을 원자적으로(atomically) 처리될 수 있도록 해주어야 합니다. 이는, 우리가 기존에 배웠던, lock이나 semaphore 같은 논리적 잠금장치를 통해 해결할 수 있습니다. 

그리고 고려해야할 것은 메모리의 휘발성 또한 존재합니다. 이는, 예기치 못한 문제가 발생했을 시, 기대한 값과는 다른 값으로 저장되거나, 아예 소실될 수도 있음을 의미합니다. 결국 디스크와 같은 비휘발성 저장장치가 요구되며, 결국, 이런 저장장치가 복수 존재한다는 것은 이에대한 시스템 정책관리가 요구됨을 의미합니다. Copy on write라 하는, 지연쓰기 기법이나, 저널링 등의 기법을 통해, 우리의 컴퓨터는 메모리의 휘발성 문제로 발생하는 크래시 문제 등을 가능한 회피하여, 데이터 일관성을 유지할 수 있습니다. 

 

설계 목표

 그렇다면 이런 운영 체제는 어떤 목표를 가지고 설계되어야 할까요. CPU나 메모리, 디스크 등의 물리 자원에 대한 가상화나, 병행성 문제도 해결해야하고, 이를 영구적으로 저장해야하며, 나아가 해당 모든 과정에서 취약점이 발견되지 않아야하고, 그 모든 논리의 신뢰성이 보장되어야 합니다. 이를 쉽게 정리하면 다음과 같습니다.

 

1. 제각각인 하드웨어 자원들을 일관되게 관리할 수 있는 추상화 기능 제공

2. 모든 자원 활용에 있어서, 할당과 소거의 최적화와 전반적인 오버헤드 최소화를 위한 성능 향상

3. 타 프로그램과의 간섭 방지나, 기타 취약성을 배제할 수 있는 보안

4. 시스템이 항상 안정적으로, 논리적으로 실행될 수 있음을 보장할 수 있는 신뢰성