0%

OSTEP

01. Intro - Introduction code

Von Neumann 모델 - 프로세서가 프로그램이 완료될때까지 메모리로부터 명령어를 fetch하여, decode 후, execute 하는 과정을 반복한다.

OS - 프로그램을 실행하고, 프로그램이 메모리를 공유하며, 다른 장치와 상호작용할 수 있도록 도와주는 소프트웨어를 뜻한다.


THE CRUX OF THE PROBLEM

리소스 가상화하기
어떻게 OS는 리소스를 가상화(virtualize) 시키는 것일까?
이유는 당연히 시스템을 더 쉽게 사용할 수 있도록 만들기 위해서일 것이다.
결국, OS가 어떤 매커니즘과 정책으로 가상화를 진행하는지, 어떻게 효율적으로 진행하며 무슨 H/W 지원이 필요한가를 논해야한다.


OS는 가상화(Virtualization)을 통해, 시스템이 정확하고 효율적으로 작동하는 부분을 책임진다. 가상화란 OS가 물리적 리소스(프로세서, 메모리, 디스크 등)를 보편적이고, 사용하기 쉬운 가상의 형태로 변환시킨다.
여러 프로그램이 실행되고, 동시에 서로의 명령어와 데이터, 장치에 접근할 수 있게 해주며, CPU, 메모리, 디스크와 같은 리소스를 관리해주는 OS = resource manager 라 할 수 있다.

예제를 하나 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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, "usage: cpu <string>\n");
exit(1);
}
char *str = argv[1];
while (1) {
Spin(1);
printf("%s\n", str);
}
return 0;
}

Figure 2.1 : 간단한 루프와 출력을 실행하는 코드 (cpu.c)

CPU 가상화 (Virtualizing the CPU)

위의 코드는 반복적으로 시간을 확인하고 1초 동안 실행되면 리턴하는 함수, Spin() 을 호출한다. 만약, 싱글 프로세서(CPU)에서 cpu.c를 컴파일하여 실행하면 아래와 같은 결과를 볼 수 있다.

1
2
3
4
5
6
7
8
prompt> gcc -o cpu cpu.c -Wall
prompt> ./cpu "A"
A
A
A
A
^C
prompt>

^C는 ctrl+C 또는 cmd+C 로 실행을 멈춘 것

다르게 실행을 해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
A
...

Figure 2.2 : 여러 프로그램을 동시에 실행

비록 싱글 프로세서를 사용하고 있지만, 4개의 프로그램이 동시에 실행되는 것처럼 보인다. 이는 OS가 하나의 CPU를 거의 무한에 가까운 여러개의 가상 CPU로 만드는 착각을 만드는 것이다. 이를 CPU 가상화라고 한다.
이상하게도 결과를 보면, 실행 순서가 규칙적이지 않다. 이는 OS의 정책마다 실행 순서가 다르기 때문이다. 실제로 다른 OS에서 실행을 해보면 출력 결과가 다르게 나올 것이다. 이러한 부분은 뒤에서 다룬다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int main(int argc, char *argv[]) {
int *p = malloc(sizeof(int)); // a1
assert (p != NULL);
printf("(%d) address pointed to by p: %p\n", getpid(), p); // a2
*p = 0; // a3
while (1) {
Spin(1);
*p = *p + 1;
printf("(%d) p: %d\n", getpid(), *p); // a4
}
return 0;
}

Figure 2.3 : 메모리에 접근하는 프로그램 (mem.c)

메모리 가상화 (Virtualizing Memory)

이제는 메모리를 가상화 해보자. 물리적 메모리 개념은 쉽다. 그저, bytes의 배열이고, 메모리에 데이터가 저장된 주소를 명시하여 읽어들인 후, 메모리에 저장(또는 수정)한다.
메모리는 프로그램이 실행되는 모든 시간마다 접근하며, 명령어를 메모리에 저장하고, 실행될 때 호출과 저장을 번갈아가며 실행한다.
위 코드의 실행 결과를 보자.

1
2
3
4
5
6
7
8
prompt> ./mem
(2134) address pointed to by p: 0x200000
(2134) p: 1
(2134) p: 2
(2134) p: 3
(2134) p: 4
(2134) p: 5
^C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
prompt> ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) address pointed to by p: 0x200000
(24114) address pointed to by p: 0x200000
(24113) p: 1
(24114) p: 1
(24113) p: 2
(24114) p: 2
(24114) p: 3
(24113) p: 3
(24113) p: 4
(24114) p: 4
...

Figure 2.4 : 메모리 프로그램을 여러번. 실행

위의 프로그램을 보면, a1에서 메모리를 할당한다. 그리고, a2에서 메모리 주소를 출력하고, a3에서 새로 할당된 메모리에 0을 저장한다. 마지막으로, 반복하며, p에 저장된 주소를 1초마다 증가시킨다. 여기서 getpid()를 통해 process identifier를 같이 출력한다. 새로 할당된 주소는 0x200000이며 프로그램이 실행되며 천천히 값을 업데이트하며 출력한다.
Figure 2.4 에서 여러 프로세스를 동시에 실행하는데, 두개의 프로세스 모두 같은 주소인 0x200000에 할당되어 서로 독립적으로 값을 증가하는 걸 볼 수 있다. 이를 통해, 두개의 프로그램이 같은 물리 메모리를 공유하는게 아니라 독립적인 메모리를 가지고 실행된다는 걸 알 수 있다.

OS에서 이러한 현상을 메모리 가상화라고 한다. 각 프로세스는 OS가 물리 주소로 매핑한 개인 소유의 가상 주소(주소 공간)를 가진다는 말이다. 메모리는 다른 프로세스의 공간에 영향을 주지 않는 독립된 공간을 쓰는것처럼 보인다. 하지만, 물리 주소는 공유 자원이며, OS에서 이를 관리하여 독립된 공간을 제공해주는 것이다.

위의 예제를 통해, 이 책의 첫 부분인 가상화 내용을 알 수 있다.

동시실행(Concurrency)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

volatile int counter = 0;
int loops;

void *worker(void *arg) {
int i;
for (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;
}

Figure 2.5 : 멀티쓰레드 프로그램 (threads.c)

이 책의 다른 핵심 주제는 바로 동시실행(concurrency)이다. 이는 같은 프로그램에서 많은 일을 한번에 처리할 때 생기는 문제와 부딪힐 때 사용하는 용어이다. 위의 가상화 예제에서 보듯, OS는 많은 일을 한번에 처리한다. 시간이 지날 수록, 이는 여러 문제로 이어진다. 이러한 문제는 OS 뿐 아니라, 멀티쓰레드 프로그램에서도 보인다. Figure 2.5 멀티쓰레드 프로그램을 보자.
모든 부분을 잉해할 수는 없어도, 프로그램에서 Pthread_create()로 2개의 쓰레드를 생성한다. 쓰레드는 1개 이상의 다른 함수와 같은 메모리 공간을 공유하며 작동하는 함수로 이해하면 된다. 예를 들어, 각 쓰레드는 worker()라는 루틴에서 시작하여 loop 숫자만큼 반복하며 counter를 증가시킨다.

loops를 1000으로 설정하여 실행해보자.

1
2
3
4
prompt> gcc -o thread thread.c -Wall -pthread
prompt> ./thread 1000
Initial value : 0
Final value : 2000

예상했던대로 2개의 쓰레드가 실행되면 각 쓰레드가 1000번씩 더하여 총 2000의 값을 나타낸다. loops가 N의 입력값을 받는다면, 결과는 2N이 된다. 하지만, 만약 더 큰 숫자로 실행하면 어떻게 나올까?

1
2
3
4
5
6
prompt> ./thread 100000
Initial value : 0
Final value : 143012 // 음?
prompt> ./thread 100000
Initial value : 0
Final value : 137298 // 왓더?

이번 실행에서 입력값을 100,000으로 주었지만 우리가 예상한 값인 200,000이 아니라 143,012, 137,298 등 엉뚱한 출력값을 보여준다. 값이 틀릴 뿐 아니라, 전혀 다른 값이 출력되는데, 무엇이 문제일까?
이는, 명령어가 동시에 실행될 때, 명령어가 어떻게 처리되는지와 연관되어 있다. 3개의 명령어가 처리된다: 메모리에서 레지스터로 값을 불러오기, 그 값을 더하기, 수정된 값을 다시 메모리에 저장하기. 이 3개의 명령어가 atomic하게(하나씩) 처리되지 않기 때문에, 이상한 결과가 출력되는 것이다. 이런 동시성 문제는 2번째 챕터에서 다루자.


THE CRUX OF THE PROBLEM

올바른 동시성 프로그램 작성하기
같은 메모리를 공유하며 동시 실행되는 여러 쓰레드가 있을 때, 어떻게 올바른 프로그램을 빌드할 수 있을까? OS에서 어느 요소가 필요할까? H/W의 무슨 매커니즘이 제공되어야 할까? 우리는 어떻게 이러한 동시성 문제를 해결할 수 있을까?


지속성 (Persistence)

OS (Operating System) 정리

이전에 학교에서 배웠던 교재를 기반으로 정리하기로 했습니다.

재목은 Remzi H. 교수의 Operating Systems: Three Easy Pieces (이후 OSTEP으로 명칭)

모든 자료는 오픈되어 있으며

아래 링크로 들어가면 모든 자료가 열람 가능합니다.

OSTEP


목차

목차는 본 교재와 같은 순으로 구성되게끔 번역해볼 예정이며

Dialogue는 링크로만 첨부, 각 챕터 별 내용을 정리하여 올릴 예정입니다.

Intro

Introduction Code

Virtualization

Concurrency

Persistence

Appendices