- 출처: Operating Systems - Three Easy Pieces
개요
Program을 실행한다는 것은 무엇인가? 많은 정의가 있을 수 있지만 그중 가장 직관적인 정의를 꼽으라면 "명령어를 수행하는것"이라고 할 수 있을것이다. OS는 Program을 수행할 때 다음과 같은 과정을 수행한다.
- Processor가 Memory로 부터 명령어(instruction)을 가져온다.
- 이를 해석(decode)하고 실행(execute)한다.
- 위 과정을 반복한다.
OS의 가장 근본적인 책임은 많은 application 을 동시에 잘 수행될 수 있도록 하는것이다. 컴퓨터의 1 application이 1개의 memory만 점유하는것이 아니라 여러 개의 application이 memory를 공유한다. 또한 application이 device를 쉽게 사용할 수 있도록 하는 역할도 한다. 이를 각각의 관점에서 본다면 아래와 같이 생각해볼 수 있다.
- 가상화(Virtualization): OS는 물리적인 자원을 application이 사용할 수 있도록 가상의 형태로 변환한다. 그래서 OS를 다른말로 가상머신(virtual machine) 이라고 표현할 수 있다.
- 표준 라이브러리(Standard library): OS는 application을 구동하고, 메모리를 할당하며, application이 파일에 접근할 수 있도록 해준다. 이런 동작들을 가능하게 하기 위해 OS는 application에게 "System Call"을 노출한다.
- 자원 관리자(Resource manager): CPU가 1개이거나 memory가 1개 여도 우리는 많은 application을 동시에 실행할 수 있다. 또한 disk에 여러 application이 접근할 수도 있다. 이런 관점에서 볼 때 OS를 자원 관리자라고 표현할 수 있다.
CPU 가상화
#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;
}
대뜸 C 코드를 들고와서 당황스러웠을 수 있지만 이해하지 못해도 상관없다. 나도 대학교때 이후로 C 언어를 써본적은 없지만 간략히 설명하자면 아래와 같이 program 인자로 문자열을 받아서 1초 마다 무한으로 출력하는 예제이다.
prompt> gcc -o cpu cpu.c -Wall
prompt> ./cpu "A"
A
A
A
A
ˆC prompt>
CPU가 1개인 상태에서 위의 프로그램 여러 개를 동시에 실행하면 1 번째 프로그램이 끝날때까지 기다려야 할까?
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
C
B
D
위와 같이 OS는 물리 CPU가 1개여도 application 에게는 마치 자신만을 위해 일하는 전용 CPU가 있는것처럼 가상의 CPU를 할당한다.
Memory 가상화
memory는 한마디로 bytes 배열이라고 할 수 있다. 특정 주소를 지정하면 해당 주소에 읽기/쓰기를 수행할 수 있는 공간이다. application의 모든 데이터뿐만 아니라 OS가 실행하는 명령어(instruction) 또한 메모리에 존재한다.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
int main(int argc, char *argv[]) {
int *p = malloc(sizeof(int));
assert(p != NULL);
printf("(%d) address pointed to by p: %p\n",
getpid(), (void *)p);
*p = 0;
while (1) {
Spin(1);
*p = *p + 1;
printf("(%d) p: %d\n", getpid(), *p);
}
return 0;
}
위의 프로그램은 int 형 공간을 할당하고 해당 메모리 주소를 출력한 후 점점 1씩 증가하는 p를 pid와 함께 출력하는 예제이다. 이 프로그램을 compile 하고 동시에 2개 실행하면 어떻게 될까?
prompt> ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) memory address of p: 00200000
(24114) memory address of p: 00200000
(24113) p: 1
(24114) p: 1
(24114) p: 2
(24113) p: 2
(24113) p: 3
(24114) p: 3
(24113) p: 4
(24114) p: 4
pid 24113과 24114가 같은 메모리 주소를 출력했고 각각 증가시키고 있다. 같은 메모리 주소의 값을 증가 시키면 충돌이 날것 같은데 마치 application 마다 자신의 메모리 공간이 있는것처럼 아무 문제 없이 application당 증가된 숫자를 잘 출력하고 있다.
위의 결과에서 출력된 주소값은 물리 메모리의 주소값이 아니라 가상 메모리의 주소값이기 때문이다. OS는 물리 메모리를 application에게 제공할 때 마치 application 마다 자신만의 메모리가 있는것처럼 할당해준다.
Concurrency
위에서 설명했듯이 OS는 한꺼번에 많은 일을 수행한다. 하지만 application 자체도 예외는 아니다. 요즘에는 점점 application이 복잡해지는 추세이기 때문에 multi thread 를 거의 필수적으로 사용하고 있다.
#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;
}
위 프로그램은 worker를 2개 생성해서 프로그램 인자로 받은 만큼 counter를 증가시킨다. 만약 1000을 입력한다면 2개의 worker가 1000만큼 counter를 증가시키므로 최종결과는 2000이 될것이라고 예상할 수 있다. 정말 그럴까?
prompt> ./thread 100000
Initial value : 0
Final value : 143012
prompt> ./thread 100000
Initial value : 0
Final value : 137298
위와 같이 compile후 실행하면 이상한 값이 나오는것을 알 수 있다. 이렇게 나오는 이유는 C에서 증가 연산자가 마치 1단계 연산 같지만 CPU가 실행하는 명령어(instruction) 관점에서 볼때는 3단계의 연산이기 때문이다.
- 메모리로 부터 register로 불러온다
- register에서 +1 증가연산
- register에서 메모리로 저장한다
위의 3단계 연산은 원자적(atomic)하게 일어나지 않고, 2개의 worker가 각각 일할 때 충돌을 불러와서 프로그램 인자로 넘긴 수의 2배수의 값이 정확하게 나오질 않는다.
영속성(Persistence)
OS에서 디스크를 관리하는 S/W를 우리는 파일시스템이라고 한다. 파일 시스템은 CPU와 메모리와는 다른점이 있는데, application 마다 전용자원인것처럼 제공하던 형태와 달리 여러 application에서 같은 파일을 공유한다.
예를 들어 IDE에서 main.c를 작성한 후 이를 gcc -o main main.c를 통해 compile한 후 결과 파일을 실행했다고 가정해보자. 이 과정에서 main.c는 여러 process 간에 공유되었다고 생각할 수 있다.
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
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;
}
위의 코드는 /tmp/file을 생성한 후 "hell world"를 파일에 쓰고 닫는 예제이다. 위의 예제에서 open, write, close 함수를 통해 각각 파일을 열거나 생성하고, 내용을 쓰고, 더이상 쓸 내용이 없다는 의미로 파일을 닫는다. 이 함수들을 "System call" 이라고 한다.
"System call" 연산은 간단해 보이지만 사실 깊게 들어가면 상당히 복잡하다. 그래서 OS는 이런 "System call"을 통해 application 에게 장치(device)를 다루는 표준적이고 쉬운 방법을 제공한다. 그래서 OS는 표준 라이브러리(standard library)를 제공한다고도 할 수 있다.
도대체 얼마나 복잡하기에 그런가? 우선 간단히 살펴보자면 파일에 쓰기 연산을 할 때 단순하게 내용을 곧바로 추가하는것이 아니다. 성능상의 이유로 파일 시스템은 쓰기 작업을 지연시켜서 더 큰 작업 단위로 grouping 한다. 또한 쓰기 연산이 수행되는 중간에 시스템 자체가 망가질(crash)수 있기 때문에 파일 시스템은 이런 문제를 다루기 위해 journaling이나 copy-on-write 같은 프로토콜도 포함하고 있다.
마치면서
결국 OS는 CPU, 메모리, Disk와 같은 물리 자원을 가상화 및 추상화하여 application에게 제공한다고 볼 수 있다.
또한 application 이 다른 application에게 영향을 주어서 망가뜨리거나 application이 OS를 망가뜨리지 않도록 보호(protection)한다. 이렇게 보호하기 위해서 격리(isolation)하는것도 OS의 또다른 핵심 이라고 볼 수 있다.
'Concepts > OS' 카테고리의 다른 글
File System (0) | 2024.11.03 |
---|---|
Limited Direct Execution (0) | 2024.05.25 |
Process API (0) | 2024.05.15 |
Process Abstraction (0) | 2024.05.05 |
댓글