운영체제 - 아주 쉬운 3가지 이야기
책에서 말하는 3가지 이야기란 가상화, 병행성, 영속성을 말한다.
책 읽기에 앞서 왜 운영체제를 공부해야 되는 지 의문이 들었다. 이전에도 조금 배웠었지만 배운 내용들을 개발할 때 사용해본 적이 없다. 하드웨어 위에서 소프트웨어가 돌아간다고 해도 동시성 처리나 스케줄링 같은 것들은 운영체제가 알아서 해주기 때문에 몰라도 상관없지 않을까?
하지만 앞으로도 이러한 지식이 필요하지 않을거라는 보장이 없으므로 책을 공부해야 되는 동기는 모르겠지만 목표를 하나 정해두고 시작하려고 한다. 목표는 '지금 배운 지식을 가지고 실제 나의 문제를 1가지라도 해결하기'이다. 이론만 머릿속에 넣어서는 아무 의미가 없다고 생각하기 때문에 실제로 나의 문제를 해결하고, 필요성에 대해서 깨닫고 싶다.
가상화라는 개념(?)이 왜 탄생했을까? 뭐 때매 탄생했을까? 궁금했다.
처음에 생각한 바는 멀티 프로세스와 관련이 있나 싶었다. cpu는 하나의 프로세스만 점유할 수 있기 때문에 둘 이상의 프로세스를 동작시키기 위해서는 둘 이상의 cpu가 존재해야 한다. 하지만 이렇게 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, "사용법: cpu <문자열>\n");
exit(1);
}
char *str = argv[1];
while (1) {
Spin(1);
printf("%s\n", str);
}
return 0;
}
위 코드를 딱 봤을 때 처음 든 생각은 가상화와 무슨 상관이 있지? 라는 생각이였다. 딱 봐도 while()을 돌면서 1초마다 문자열을 출력하는 코드이기 때문이다.
그런데 cpu는 위 프로그램을 동작시키면서 다른 작업을 할 수 있다. 이게 가능한 이유는 타이머 인터럽트 때문이라고 한다. 타이머 인터럽트는 하나의 프로세스가 cpu를 독점하지 못하도록 일정 주기로 cpu 제어권을 다른 프로세스에게 넘겨준다. 이 덕분에 하나의 프로세스가 cpu를 독점하고 있는 것처럼 보이지만 그렇지 않고, 실행시키는 와중에도 영상이나 다른 작업을 할 수 있다.
하나의 cpu로 마치 여러 개의 프로세스가 동시에 실행되는 것처럼 보여지는 cpu 가상화, 여러 프로세스가 순서에 맞게 작동하도록 스케줄링 기법.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
int main(int argc, char *argv[]) {
int *p = malloc(sizeof(int)); // 정수형 포인터 p에 메모리 할당
assert(p != NULL); // p가 NULL이 아닌지 확인
printf("(%d) p의 메모리 주소: %08x\n", getpid(), (unsigned) p); // 프로세스 ID와 p의 메모리 주소 출력
*p = 0; // p가 가리키는 값을 0으로 초기화
while (1) {
Spin(1); // 1초 대기
*p = *p + 1; // p가 가리키는 값을 1 증가
printf("(%d) p: %d\n", getpid(), *p); // 프로세스 ID와 p가 가리키는 값 출력
}
return 0;
}
위 코드의 핵심은 malloc으로 메모리 공간을 할당하고, 넘겨준 주소는 가상 주소라는 것. 위 프로그램은 하나의 프로세스에서 고유한 가상 주소를 갖게 되지만, 말 그대로 가상 주소이기 때문에 또다른 터미널에서 위 프로그램을 실행 하더라도 똑같은 가상 주소가 나올 수 있음.
결론은 운영체제는 가상 메모리 공간에 값을 저장함으로써 다른 프로세스에서 위 주소로 접근하더라도 실제 값은 변경되지 않는다.
예를 들어, 메인 프로그램이 Pthread_create() 함수를 사용하여 두 개의 쓰레드를 만들고, 각 쓰레드가 worker() 함수를 실행하여 카운터 값을 증가시키는 상황을 생각해 볼 수 있습니다. 만약 각 쓰레드가 카운터를 1000번 증가시키도록 설정된다면, 최종 카운터 값은 2000이 될 것으로 예상할 수 있습니다.
위 상황에서 2000이 증가될 것 같지만 그렇지 않은 경우가 발생한다. 만약 스레드A가 접근한 값이 0이라고 했을 때 그와 동시에 스레드B가 접근한다면 둘다 0이라는 값을 1 증가시키는 상황이 발생할 수 도 있기 때문이다.
위와 같은 문제는 팀 프로젝트를 진행하면서도 발생했던 문제다. 댓글 하위의 좋아요 버튼이 있는데 나와 다른 유저가 동시에 좋아요 버튼을 누른다면 어떻게 처리해줘야 할까?
일반적으로 DRAM에 저장된 데이터는 휘발된다는 것을 알고 있을 것이다.
그래서 휘발되면 안되는 정보는 디스크에 저장하고, 파일 시스템은 디스크에 저장된 정보에 접근해서 데이터를 읽어올 수 있도록 도와준다.
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/types.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
if (fd == -1) {
perror("open");
exit(1);
}
assert(fd > -1); // 파일 열기를 확인
int rc = write(fd, "hello world\n", 13); // "hello world\n" 문자열 쓰기
assert(rc == 13); // 쓰기 작업을 확인
printf("파일 쓰기 완료");
close(fd); // 파일 닫기
fd = open("/tmp/file", O_RDONLY); // 읽기용으로 다시 열기
if (fd == -1) {
perror("open for read");
exit(1);
}
char buf[100];
int n = read(fd, buf, sizeof(buf) - 1);
buf[n] = '\0'; // 문자열 종료
printf("읽은 내용: %s\n", buf);
close(fd);
return 0;
}
디스크에 데이터를 쓰는 건 굉장히 복잡한 일이지만 운영체제는 시스템 콜이라는 표준화된 기법을 통해 우리가 손쉽게 디스크에 데이터를 저장할 수 있도록 도와준다.