[OS] 2. Introduction

급식·2022년 3월 22일
0

OSTEP

목록 보기
2/24
post-thumbnail

보자, 어떻게 시작해야 할까. 일단 제일 눈에 띄는 질문부터 시작하는 게 좋겠다.
영어를 못해서 그냥 이해한 대로 쓸 것이다.

프로그램이 실행될 때 어떤 일이 벌어지는가?

프로그램을 실행한다는 것은, 단지 여러 명령(instruction)들을 실행하는 것을 의미한다.
프로세서는 메모리로부터 명령을 가져오고(fetch), 해석하고(decode, 개인적으로 그냥 decode라고 쓰는 게 더 좋은 것 같다.), 실행하고(execution), 다음 명령어로 넘어가는 일을 프로그램이 완전히 끝날 때까지 초당 몇백만, 몇십억 번 수행한다.

이것이 폰 노이만 컴퓨팅 모델(Von Neumann model of computing)에 대한 아주 간단한 설명인데, 운영 체제 강의에서는 시스템을 사용자가 사용하기 쉽게 만들기 위해 일어나는 여러 일들에 대해 배운다고 한다.


OS (Operating System)

프로그램을 쉽게 실행할 수 있도록 해준다든지(ex. 여러 프로그램들을 동시에 실행하는 일들), 프로그램들이 메모리를 공유하게 해준다든지, 프로그램이 장치(device)와 상호작용한다든지 하는 일들을 맡는 소프트웨어를 의미한다. OS가 있어 시스템을 쉽고, 정확하고, 효율적인 방법으로 다룰 수 있는 것이다. 뭔가 OS가 물 밑에서 열심히 발길질 하고 있는걸 몰랐던 벌을 3학점에 걸쳐서 듣는 기분이다.

Virtualization (가상화)

OS가 시스템을 쉽고, 정확하고, 효율적으로 다루기 위해 주로 사용하는 방법인데, 프로세서, 메모리, 디스크 따위의 물리적인 자원들을 더 일반적이고, 강력하고, 사용하기 쉬운 형태로 변환하는 것을 의미한다. 이 때문에 OS를 종종 Virtual machine이라고 부르기도 하나보다.

열심히 가상화를 수행했으니, OS는 사용자가 이러한 자원을 활용할 수 있도록 API로써 System call이라는 것을 제공한다. 사용자가 System call을 통해 프로그램 실행, 메모리 액세스 등의 작업을 할 수 있기 때문에 이를 OS가 사용자에게 Standard Library를 제공한다고 표현하기도 한단다.

또 다른 측면에서 보면, OS 위에서 돌아가는 여러 프로그램이 OS를 통해 동시에 명령과 데이터에 접근하기도 하고(메모리 공유), 외부 장치에 접근하기도 때문에 OS를 Resource manager라 부르기도 한다. 여기서 Resource란 CPU, 메모리, 디스크 등을 의미하는데, OS는 이러한 자원들을 효율적으로, 공정하게 각각의 프로세스가 나누어 쓰도록 도와준다.

이제 책 전반에 걸쳐 공부하게 될 CPU, 메모리 가상화, 그리고 Concurrency, Persistence가 무엇인지 살짝 찍어서 맛만 한번 봐 보자.


2.1. Virtualizing The CPU

제일 먼저 예제로 나온 프로그램은 별 거 없다.

실행 결과는 아래와 같다.

살짝 멈칫한 부분이 있기는 한데(뒤에서 코드를 자세히 살펴볼 것이다!), 프로그램에 인자(argv)로 주어진 문자열 "A"를 반복적으로 실행하기만 하는 별 볼 일 없는 프로그램이다. 그런데 이걸 동시에 실행하면 어떤 결과가 나올까?
아, 그 전에, 저기 보이는 Ampersand(&) 기호를 사용하면 Background에서 프로그램을 순차적으로 실행한다는 의미라고 한다.

얼라리? 뭔가 이상하다. (사실 아는 게 없어서 별로 이상하지 않았는데 일단 이상한 척해본다.)
분명 내 노트북에 CPU가 4개 씩이나 달려 있는 건 아닐 텐데, 4개의 프로그램이 동시에 실행되고 있음을 알 수 있다.
이는 OS가 하나, 또는 적어도 일반적으론 실행되고 있는 프로세스보단 적은 갯수의 CPU를 가지고 거의 무한한 것처럼 보이는 숫자의 CPU를 굴리고 있는 것처럼 CPU 가상화를 하고 있기 때문에 한 번에 여러 프로그램이 동시에 실행되고 있는듯한 착각하게 되는 것이다. 프로그램 역시 자신이 CPU 전체를 점유하고 있는 것처럼 착각한다. (교수님께서는 이걸 '좋은 착각'이라고 표현하셨다.)

나만,, 이게 떠오르나,,?

여기서 또 하나, 가상화를 통해 마치 여러 개의 CPU가 있는 것처럼 여러 프로그램이 착각하고 있긴 하지만, 물리적으로는 하나의 CPU만 존재한다. 그럼 두 프로그램이 동시에 실행되기를 원할 때, 어떤 걸 실행시켜 주어야 하는가? 이걸 결정하는 게 뒤에서 배울 Policy이다. 보기에는 마법처럼 와다다다 여러 프로그램을 실행시켜 주는 것 같지만, 정해진 영리한 규칙대로 이걸 다 해내고 있었던 것이다. 박수!


2.2. Virtualizing Memory

데이터가 저장되는 공간인 물리적 메모리(Physical memory)는 단지 특정 주소에 있는 메모리를 읽고, 쓰고, 갱신하는 기능을 가진 일종의 byte로 구성된 배열(array)일 뿐인데, 우리 똑똑한 OS는 이 메모리에도 가상화를 수행한다.

이제 예제 프로그램을 통해 메모리 가상화가 어떻게 일어나는지 살펴보자.

..큰일났다. 위에서 대충 넘기기는 했는데 다 까먹은 언어라 줄줄 읽어 내릴 수가 없다.
사실 작년에 C++ 프로그래밍 교과목을 수강하기는 했는데, 맘에 썩.. 들지는 않아서 Python이랑 Java 같은 언어만 편식하다 보니까 배운 걸 거의 다 잊어버렸다. 반성,,

암튼 잘 모르겠으니 위에서부터 천천히 해석해보자.


	int *p = malloc(sizeof(int));

이건 int 자료형의 크기 = 4byte = 32bit 만큼 메모리를 Heap 영역으로부터 할당받아 주소를 저장하는 포인터 변수 p에 해당 값이 저장된 메모리의 시작 주소를 저장하겠다는 의미이다.

	*p = 0;
    
    ...
    
    *p = *p + 1;

저 Asterisk 기호는 포인터 변수 p가 가리키는 메모리 공간의 값을 의미하는데, 이를 0으로 초기화 한다는 것은 포인터 변수 p가 가리키는 값을 0으로 초기화 하겠다는 의미이다. 그 아래도 같다.

		Spin(1);

책에는 계속 시간을 체크하다가, 인수로 주어진 만큼(sec) 시간이 지나면 반환하는 함수라고 나와 있다. sleep 비슷한 건가 보다.

		printf("(%d) p: %d\n", getpid(), *p);

저 getpid는 프로세스를 구분하기 위해 프로세스별로 고유하게 매겨진 PID 값을 받아오는 함수이다.


대충 소스 코드가 어떻게 생겼는지 파악 했으니, 실행 결과를 살펴보자면 아래와 같다.


하나의 프로그램만 실행했을 때는 아까와 마찬가지로 별 볼 일이 없다. 그냥 PID가 2134인 프로세스가 하나 만들어져서, Heap 공간의 0x200000에 해당하는 주소에 값을 초기화하고, 1초마다 1씩 올려주고 있다.


그럼 그렇지. 두 개의 프로그램을 동시에 실행시켜 보니 뭔가 이상한 점이 보인다.
프로세스가 각각 생성된 것까지는 좋은데, p가 가리키는 heap 영역의 할당된 메모리 영역의 시작 주소가 0x200000로 같다!

그럼 같은 메모리 공간을 가리키고 있다고 하니, 해당 메모리 공간에 값을 1씩 올려준다면 두 프로그램이 힘을 합쳐 p가 가리키는 메모리의 값을 아까보다 2배 빠르게 증가시켜야 할 것 같은데, 왜인지 각각 p를 0부터 착실하게 1씩 증가시키고 있다. 무슨 일일까?

사실 표준 출력을 통해 보여지는 0x200000는 실제 Physical memory 주소가 아니라, 각각의 프로세스가 서로 간섭할 수 없이 고유하게 부여되는 Virtual address space 위의 주소이다. OS가 이 address space 위의 가상화된 주소와 실제 physical memory 사이를 연결하기 위한 mapping 정보를 가지고 있기 때문에 가능한 일이다.


2.3. Concurrency

고마워요! 네이버 영어사전!

Concurrency. 사전에는 '동시 실행'이라 나와 있다. 무슨 의미일까?
책에는 '같은 프로그램 안에서 여러 작업들이 동시에 실행될 때 발생하여 해결해야 하는 문제' ..정도로 나와 있는 것 같다. 멀리 갈 것도 없이 아까 앞에서 언급했듯이 OS가 자원을 가상화할 때 프로세스의 실행 따위의 것들을 수행하는 과정에서도 concurrency 문제가 발생한다.

OS 뿐만 아니라 현대 프로그래밍 업계에서 널리 사용되는 Multi-threaded 기법에서도 concurrency 문제가 발생하는데, 아래 프로그램을 통해 이를 살펴보자.

이번에도 모르는 코드가 나와서, 잠깐 간단하게 짚어보고 넘어가려 한다.


volatile int counter = 0;

이건 사실 제대로 이해하지 못했는데, '컴파일러가 메모리 최적화에서 해당 변수를 제외하여 항상 메모리에 접근할 수 있도록 함' ..이라고 필기해 두었다. 아마 두 스레드가 방해 없이 counter 변수에 접근할 수 있도록 하기 위한 keyword 같다.

또, 그냥 그런가 보다 하고 넘어갔는데 아래 두 전역 변수 선언은 컴파일의 결과인 실행 파일에 전혀 다른 형태로 저장된다고 한다.

volatile int counter = 0;
int loops;

volatile 때문이 아니라, 초기화 여부가 둘의 기록 형태(?)를 결정한다.
위의 counter의 경우 컴파일 시점에 값을 알 수 있기 때문에 실행 파일의 data 영역에 'counter라는 식별자를 가진 변수의 값은 0임' 하는 식으로 저장이 되는데, loops의 경우 초기화되지 않았기 때문에 어떤 값인지 알 수 없어 BSS라는 영역에 'loops는 int 크기 만큼 메모리를 점유할 것임' 이라고만 저장해놓고, 실행 시점에 그냥 비트가 0으로 초기화되어서 자동으로 값까지 0으로 초기화되는 것이라고 한다. 이거 퀴즈도 나왔는데... 사실 아직 메모리 구조가 머릿속에 완전하게 그려지지는 않는다.

void *worker(void *arg) {

*worker 부분을 제대로 모르고 있었는데, 함수의 실행 흐름이 있는 곳의 시작 주소라고 한다. 강의를 듣다 보니 너무 당연해져서 아까 쓰려다 말았는데, 디스크에 저장된 실행 파일이 메모리로 loading 되어야 비로소 프로세스가 되는 것이고, 명령 역시 메모리의 어딘가에 저장되어 있기 때문에 주소를 가진다.
여하튼, 스레드를 만들 때 어떤 작업을 할지 명시해주는 과정에서 함수 자체를 넘겨주기 위한 문법.. 정도로 이해하고 넘어가련다. 나중에 필요하면 또 나오겠지.

	Pthread_create(&p1, NULL, worker, NULL);
    Pthread_create(&p2, NULL, worker, NULL);

스레드를 생성하는 코드이다. 생성만 했고 아직 실행은 안 한 건데, 해당 스레드를 실행하기 위한 데이터들을 모아 놓은 activation record를 생성한 것이라고 한다. 특히 두 번째 인자는 스레드에서 사용할 stack의 크기를 의미하는데, NULL로 주면 시스템이 알아서 정한다고 한다.


휴! 부분적으로 살펴봤으니 전체적인 흐름을 읽어보면..

  1. 몇 번 루프를 돌지 argv를 통해 전달받고
  2. p1, p2라는 2개의 thread를 만들고
  3. counter가 0으로 초기화되어 있음을 보여준 뒤
  4. p1, p2 각각의 thread가 counter라는 전역 변수에 1씩 더해준다.
  5. 그럼? 아마도 count의 값은 (loop * 2)가 될 것 같다.

실행 결과를 살펴보자.

loop를 1000으로 주었더니, 예상한 대로 counter의 값이 1000이 되었다.

loop의 값을 좀 더 크게 줘볼까? 100000 정도면 적당할 것 같다.

아잇씨 진짜,,,,

예상과 달리 counter 값이 200000도 아니고, 심지어 실행할 때마다 값이 다르게 나온다.
교수님께서 이걸 '컴퓨터는 시킨 대로 잘 한 건데, 사람이 잘못한 것'이라고 하셨다. 인간이 기계에게 굴복할 날이 머지않은 것 같다고 생각했었다.

여하튼 왜인고 하니, 용어부터 들이밀어 보자면 우리가 원했던 작업(counter++;)이 atomic 하지 않아서 그렇다. 배웠다시피, 어차피 기계는 자연어나 자연어에 가까운 High-level 프로그래밍 언어가 아니라 기계어의 집합을 읽어 명령을 수행하는데, C언어에서는 한 statement인 저 작업이 기계어 수준에서는 하나의 명령이 아니라고 한다. 기계어 수준에서 하나의 명령을 atomic 하다고 할 수 있겠다.
(뭉뚱그려 생각해 봐도 메모리로부터 값을 읽어와 레지스터에 저장하는 작업, 값이 저장된 레지스터의 값을 하나 올려주는 작업, 다시 메모리에 쓰는 작업, 최소 세 개의 작업이 필요할 것이다.)


가능한 시나리오로 예를 들어보자면, 위처럼 p1이 먼저 counter에 저장된 값 1을 읽어와 더하는 것까지 했는데, 이를 메모리에 반영하기도 전에 p2가 counter에 저장된 값 1을 읽어와 1을 더해 메모리에 저장해 버리면 p1 역시 ADD 연산이 수행되었기 때문에 그 결과인 2를 메모리에 덮어쓰기 때문에 우리가 기대한 3이 아니라 2가 저장될 수도 있다.

뭔가 컴퓨터 구조 강의에서 배웠던 Hazard 느낌이 나는데.. OS에선 이런 상황을 Race condition이라 부른다고 한다. 이를 해결하기 위해 우리가 원하는 작업이 atomic하게 수행되도록 해주어야 concurrency 문제를 해결할 수 있다. C++ 강의 후반부에 세마포어니, 뮤텍스니 배웠던 것 같은데 아마 여기서 쓰일 것 같다.


2.4. Persistence

시스템의 메모리는 DRAM과 같은 장치 위에 올라가 있기 때문에, 시스템이 뻗는다든지, 갑자기 전원이 나가버린다든지 하면 정보가 쉽게 유실된다(휘발성). 따라서, 시스템이 이러한 상황에서도 최대한 지속적으로(persistently) 데이터를 저장하도록 보장해야 한다.
..잘 이해가 안 되어서 구글링을 해봤는데, '메모리에 올라가 있는 프로세스와는 별개로 데이터는 독립적으로 별도의 기억 장치(HDD, SSD 등)에 계속 유지되도록 보장하는 것' 정도인 것 같다.

OS에서 디스크를 관리하는 소프트웨어를 보통 File System이라 부르는데, 이 SW는 사용자가 문제없이, 효율적으로 디스크에 파일을 저장할 수 있도록 해준다. 이 file들은 앞의 address space를 통해 프로세스에게 각각 할당된 메모리와는 달리, 시스템을 사용하는 사용자들이 공유할 수 있다.

즉 CPU, 메모리와 마찬가지로 파일에 대한 입출력(I/O) 역시 System call의 형태로 사용자에게 제공되는데, 그 예시를 아래 소스 코드를 통해 확인해보자.

	int fd = open("/tmp/file", O_WRONGLY|O_CREAT|O_TRUNC, S_IRWXU);

여기서 fd는 file descriptor를 의미하는데, open System call을 통해 사람이 보기 쉬운 형태인 "/tmp/file"이라는 file path를 인자로 받아 파일을 열어 컴퓨터가 보기 쉬운 형태숫자 ID(file descriptor)로 프로세스 안에서 파일을 각각 식별하기 위해 부여한 것이다.

	assert(fd > -1);

만약 파일을 읽는데 실패한다면 open system call이 -1을 반환한다고 한다.

	int rc = write(fd, "hello world\n", 13);
    assert(rc == 13);

윗줄은 write system call을 통해 fd가 가리키는 파일에 "hello world\n" 문자열을 쓰는 코드이다. 저 13이 무슨 의미인지 모르겠어서 찾아봤는데, 문자열이 끝났음을 의미하는 null character까지 포함하면 "hello world\n"는 (12+1) byte이고, write 함수가 파일에 쓴 바이트 수를 반환하므로 assert statement에서 파일에 제대로 문자열이 쓰였음을 확인하는 것이었다!

...

뭐 그렇다고 한다. 이 I/O 매커니즘에 대해서는 아주 자세히 다뤄주시지는 않아서 이정도로 정리하고 나중에 해당 chapter에 가서 깊이 공부해볼 것이다.

아, 그리고 1학년 때 처음으로 라즈베리파이에 우분투 계열 OS를 올려서 리눅스 실습을 했던 기억이 나는데, 그때 외부 장치가 마치 파일처럼 다루어지는 걸 보고 '음 그냥 그렇군!' 하고 넘겼던 것 같다. 이게 OS 지원해주는 일종의 추상화인데, 예를 들어 네트워크 카드에서 패킷을 읽는 작업을 마치 '네트워크 카드' 라는 파일의 특정 파일을 읽는 것처럼 처리할 수 있다고 한다. 신기해라!


마무리

Introduction인 만큼, OS에서의 CPU virtualization, Memory virtualization, Concurrency, Persistence가 대강 어떤 의미인지만 넓고 얕게 훑어 보았다.
아직 강의를 Virtualization 파트 밖에 듣질 않아서 concurrency, 특히 persistence에선 어떤 이슈를 다루게 될지 감이 잘 안 오지만 아직까진 재밌어서 다행이다. 마음먹은 김에 남는 시간에 딴짓하지 말고 열심히 써봐야겠다.

profile
뭐 먹고 살지.

0개의 댓글