이 글은 "운영체제 아주 쉬운 세 가지 이야기" 책을 읽고 공부한 내용들을 두고두고 보기 위해 정리하는 글이다.
읽을 때마다 그 날의 내용들을 꾸준히 이어서 업데이트 할 예정이다.
🗓️ 2023.06.26 작성 ▽
책에서 다룰 내용들을 간단히 소개해 줄 운영체제 개요
후에 집중적으로 다룰 내용들
❗️ 프로그램이 실행될 때
프로세스는 명령어를 실행
명령어를 반입(fetch), 해석(decode), 실행(excute)
명령어 작업 완료 후 프로세서는 다음 명령어, 또 다음 명령어로 프로그램이 완전히 종료될 때 까지 실행을 반복
❗️ 운영체제
1. 프로그램을 쉽게 실행 (심지어 여러 프로그램 동시 실행)
2. 프로그램 간의 메모리 공유를 가능하게
3. 장치와 상호작용을 가능하게
위와 같은 일을 할 수 있게 해주는 소프트웨어
시스템을 사용하기 편리하게, 정확하고 올바르게 동작시킬 책임이 있음
위의 언급한 일을 하기 위해 가상화(virtualization)라고 불리는 기법 사용
물리적인 자원을 이용하여 일반적이고, 강력하고, 사용이 편리한 가상 형태의 자원을 생성
-> 운영체제를 가상 머신이라고 부르기도 함
이 가상화를 통해 많은 프로그램들이 CPU를 공유하고, 동시에 실행될 수 있게함
또한 프로그램들이 각자 명령어와 데이터를 접근할 수 있게하고 프로그램들이 디스크 장치를 공유할 수 있게 함
-> 운영체제를 자원 관리자라고 부르기도 함
🗓️ 2023.06.27 작성 ▽
❓ 핵심 질문: 자원을 어떻게 가상화 시키는가
- 가상화 목적
시스템을 사용하기 편리하게 만들기 위해- 가상화 방법에 초점
-> 가상화 효과를 얻기 위하여 운영체제가 구현하는 기법과 정책은?
-> 운영체제는 이들을 어떻게 효율적으로 구현하는지?
-> 어떤 하드웨어 지원이 필요한지?
❗️ 예시
// cpu.c라는 이름으로 저장
#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); // 1초 동안 실행된 후 리턴하는 함수 Spin()
printf("%s\n", str); // 1초 마다 str을 출력
}
return 0;
}
gcc -o cpu cpu.c -Wall 로 cpu.c를 cpu라는 이름으로 컴파일
./cpu "A" 명렁어로 컴파일된 cpu의 argv에 "A"를 담아 실행
-> 1초마다 A가 출력. "ctr+c"를 누르면 프로그램 종료
./cpu A & ; ./cpu B & ; ./cpu C & ; ./cpu D & 명령어 실행
-> 한 줄마다 A B D C A B D C A C B D ... 출력 (결과 순서는 다를 수 있음)
프로세스가 하나밖에 없음에도 프로그램 4개 모두 동시에 실행되는 것처럼 보임
하드웨어의 도움을 받아 운영체제가 시스템에 매우 많은 수의 가상 CPU가 존재하는 듯한 환상을 만들어 낸 것
위의 예시처럼 하나의 CPU 또는 소규모 CPU 집합을 무한개의 CPU가 존재하는 것처럼 변환하여 동시에 많은 수의 프로그램을 실행시키는 것을 CPU 가상화라고 함
컴퓨터에서의 물리 메모리 모델은 바이트 배열
메모리를 읽기 위해서는 데이터에 주소를 명시해야 함
메모리에 쓰기(혹은 갱신) 위해서는 주소와 데이터를 명시해야 함
❗️ 예시
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
int main(int argc, char *agrv[]) {
int *p = malloc(sizeof(int)); // a1. p라는 주소에 메모리 할당
assert(p != NULL); // p가 NULL이 아니여야 한다는 조건 부여
printf("(%d) memory address of p: %p\n", getpid(), (unsigned) p); // a2. getpid()는 실행중인 프로세스 아이디
*p = 0 // a3. p라는 주소의 값에 0 대입
while (1) {
Spin(1);
*p = *p + 1; // 1초마다 p 값이 1씩 늘어난다
printf("(%d) p: %d\n", getpid(), *p); // a4
}
return 0;
}
위 예시를 mem이란 이름으로 컴파일 후 실행하면
prompt> ./mem
(2134) memory address of p: 00200000
(2134) p: 1
(2134) p: 2
(2134) p: 3
^C
여기서 2134는 실행중인 프로세스 ID. 00200000은 p의 주소
1초마다 p의 값이 1씩 늘어가며 출력된다
프로세스가 수행하는 작업들
1. 메모리를 할당받는다. (위의 a1)
2. 메모리의 주소를 출력한다. (a2)
3. 새로 할당받은 메모리의 첫 슬롯에 숫자 0을 넣는다 (a3)
4. 루프(while)로 진입하여 1초 대기 후, 변수 p가 가리키는 주소에 저장되어 있는 값을 1 증가시키고 PID와 함께 출력한다. (a4)
prompt> ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) memory address of p: 0x200000
(24114) memory address of p: 0x200000
(24113) p: 1
(24114) p: 1
(24113) p: 2
(24114) p: 2
(24113) p: 3
(24114) p: 3
^C
[1] 24113은 먼저 실행된 프로세스 ID, [2] 24114는 두 번째 실행된 프로세스 ID를 나타냄
두 프로그램은 같은 주소의 메모리(0x200000)를 할당받지만 각각이 독립적으로 0x200000 번지의 값을 갱신(각각 1, 2, 3 순으로 늘었음)
각 프로그램이 물리 메모리를 다른 프로그램과 공유하는 것이 아닌 자신의 메모리를 가지고 있는 것처럼 보임
-> 운영체제의 메모리 가상화 덕분
각 프로세스는 자신만의 가상 주소 공간을 소유
운영체제가 이 주소 공간을 컴퓨터의 물리 메모리로 매핑
하나의 프로그램이 수행하는 각종 메모리 연산은 다른 프로그램의 주소 공간에 영향을 주지 않음
-> 실행 중인 프로그램 입장에서는 자기 자신만의 물리 메모리를 갖는 셈
🗓️ 2023.06.28 작성 ▽
프로그램이 한 번에 많은 일을 하려 할 때 발생하는, 그리고 반드시 해결해야 하는 문제들을 처리할 때
❗️ 예시
// 멀티 쓰레드 프로그램 (threads.c)
#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); // 두 개의 쓰레드 생성, worker 루틴 실행
Pthread_join(p1, NULL);
Pthread_join(p2, NULL);
printf("Final value : %d\n", counter);
return 0;
}
prompt> gcc -o thread thread.c -Wall -pthread
prompt> ./thread 1000
Initial value : 0
Final value : 2000
loops 값을 1000으로 지정 후 프로그램을 실행시키면 각 쓰레드가 1000번씩 counter값을 증가시켰으므로 counter의 최종 값이 2000이 됨
loops값이 N이면 최종 값이 2N이라고 생각하기 쉬움
prompt> gcc -o thread thread.c -Wall -pthread
prompt> ./thread 100000
Initial value : 0
Final value : 143012
prompt> gcc -o thread thread.c -Wall -pthread
prompt> ./thread 100000
Initial value : 0
Final value : 137298
이번에는 loops값을 100000으로 주었더니 최종 값이 200000이 아님
또한 실행 마다 최종 값이 다름
원인은 명령어가 한 번에 하나씩만 실행된다는 것과 관련이 있음
위의 프로그램에서 counter 값을 증가시키는 부분은 세 개의 명령어로 이루어짐
counter 값을 메모리에서 레지스터로 탑재하는 명령어(1), 레지스터를 1 증가시키는 명령어(2), 레지스터의 값을 다시 메모리에 저장하는 명령어(3)
이 세 개의 명령어가 한 번에 세 개 모두 실행되지는 않기 때문에 발생하는 일
-> 병행성(corcurrency)의 문제
❓ 핵심 질문: 올바르게 동작하는 병행 프로그램은 어떻게 작성해야 하는가
- 같은 메모리 공간에 다수의 쓰레드가 동시에 실행한다고 할 때,
-> 올바르게 동작하는 프로그램을 작성하는 방법은?
-> 운영체제로부터 어떤 기본 기법들을 제공받아야 하는지?
-> 하드웨어는 어떤 기능을 제공해야 하는지?
-> 병행성 문제를 해결하기 위해서 기본 기법들과 하드웨어 기능을 어떻게 이용할 수 있는지?
DRAM과 같은 장치는 데이터를 휘발성 방식으로 저장하기 때문에 메모리의 데이터는 쉽게 손실될 수 있음
전원 공급이 끊어지거나 시스템이 갑자기 고장나면 메모리의 모든 데이터는 사라짐
-> 데이터를 영속적으로 저장할 수 있는 하드웨어와 소프트웨어가 필요
하드웨어는 입력/출력 혹은 I/O 장치 형태로 제공
장기간 보존할 정보를 저장하는 장치로는 일반적으로 하드 드라이브가 사용됨
디스크를 관리하는 운영체제 소프트웨어를 파일 시스템이라고 부름
사용자가 생성한 파일을 시스템의 디스크에 안전하고 효율적인 방식으로 저장할 책임
CPU나 메모리 가상화와는 달리 운영체제는 프로그램 별로 가상 디스크를 따로 생성하지 않음
❓ 핵심 질문: 데이터를 영속적으로 저장하는 방법은 무엇인가
- 파일 시스템은 데이터를 영속적으로 관리하는 운영체제의 일부분
-> 올바르게 일하기 위해 필요한 기법은?
-> 이러한 작업의 성능을 높이기 위해 어떤 기법과 정책이 필요?
-> 하드웨어와 소프트웨어가 실패하더라도 올바르게 동작하려면?
❗️ 예시
// 입출력을 수행하는 프로그램 (io.c)
#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); // hello world 쓰기
assert(rc == 13);
close(fd);
return 0;
}
여기서 프로그램은 운영체제를 세 번 호출
open() 콜로 파일을 생성(1), write()콜로 파일에 데이터 쓰기(2), close()콜로 파일 닫기(3).
위의 시스템 콜은 운영체제의 파일 시스템으로 전달
데이터를 디스크에 쓰기 위해 파일 시스템은 많은 작업을 해야 함
새 데이터가 디스크의 어디에 저장될 지 결정하고, 파일 시스템이 관리하는 다양한 자료 구조를 통하여 데이터의 상태를 추적한다. -> 이를 위해 저장 장치로부터 기존 자료 구조를 읽거나 갱신
장치가 무언가 하게 하는 일은 복잡한 작업 -> 저수준의 장치 인터페이스와 그 시맨틱에 대한 이해 필요
운영체제는 시스템 콜이라는 표준화된 방법으로 장치들을 접근할 수 있게 함
-> 운영체제는 표준 라이브러리처럼 보이기도 함
대부분의 파일 시스템은 성능 향상을 위해 응용 프로그램들이 요청한 쓰기 요청들을 모아서 한 번에 처리
-> 요청받은 쓰기의 내용이 실제 저장장치에 기록될 때까지 일정 시간의 지연이 발생
-> 그 사이 정전 등의 문제가 발생하면 내용이 손실될 수 있음
위와 같은 상황에 대비해 파일 시스템들은 저널링(journaling), Copy-On-Write와 같은 기법을 사용
-> 고장이 발생해도 시스템이 정상 상태로 복구될 수 있게 함(내용, 기록 순서 보장)
🗓️ 2023.06.29 작성 ▽
운영체제는 CPU, 메모리, 디스크와 같은 물리 자원을 가상화하고, 병행성과 관련된 복잡한 문제를 처리하고, 파일을 영속적으로 저장하여 안전한 상태에 있게 함
이러한 시스템을 구현하려면 몇 가지 목표를 세워야 함
이러한 목표는 설계와 구현에 집중하고, 필요한 경우 절충안을 찾는 데 필수적임
시스템 개발 시 적절한 절충안을 찾는 것은 매우 중요
개념 정의
가장 기본적인 목표
시스템을 편리하고 사용하기 쉽게 만드는 데 필요한 개념들을 정의
성능
운영체제 설계와 구현에 중요한 목표
다른 말로 표현하자면 오버헤드를 최소화하는 것
시간이나 공간 혹은 둘 다 최소로 하는 해결책을 찾는 것
보호
응용 프로그램 간의 보호, 운영체제와 응용 프로그램 간의 보호
한 프로그램의 악의적인 혹은 의도치 않은 행위가 다른 프로그램에게 피해를 주지 않는다는 것을 보장
응용 프로그램이 운영체제에게 해를 끼치지 않기를 원함
보호는 운영체제 원칙 중 하나인 고립 원칙의 핵심
신뢰성
운영체제는 계속 실행되어야 함
운영체제가 실패하면 그 위의 모든 응용 프로그램도 실패
그 외
에너지 효율성, 악의적 응용 프로그램에 대한 보안, 이동성 등