운영체제 학습 노트 목차

운영체제 — 하드웨어를 길들이는 관리자

컴퓨터를 켜면 수십 개의 프로그램이 동시에 도는 것처럼 보인다 — 브라우저, 음악, 메신저, 백그라운드 업데이트. 그런데 CPU 코어는 몇 개뿐이고, 메모리도 디스크도 하나다. 이 한정된 하드웨어를 수많은 프로그램이 서로 망가뜨리지 않고 공유하게 만드는 게 **운영체제(OS)**다. OS가 하는 일을 한마디로 줄이면 — 하드웨어를 추상화하고, 자원을 관리하고, 프로그램들을 보호한다. 이 노트 전체가 그 세 가지의 전개이고, 첫 페이지에서는 그 토대OS가 무엇을 가려 주고, 어떻게 권한을 통제하는지 — 를 본다.

OS는 무엇을 가려 주나 — 네 가지 추상화

프로그래머로서 우리가 하드웨어를 직접 다룬다고 상상해 보자 — 어느 메모리 번지가 비었는지 직접 관리하고, 디스크의 몇 번째 섹터에 쓸지 정하고, CPU를 다른 프로그램과 어떻게 나눌지 협상해야 한다. 끔찍하다. OS는 이 복잡함을 깔끔한 추상으로 가려 준다.

다이어그램 로딩 중…

CPU는 **프로세스**로 추상화된다 — 코어가 몇 개든, 각 프로그램은 자기가 CPU를 독차지한 것처럼 돈다(실은 OS가 빠르게 번갈아 준다). 메모리가상 메모리로 추상화된다 — 각 프로그램은 0번지부터 시작하는 깨끗한 자기 메모리가 있는 줄 안다(실은 OS가 물리 메모리로 번역한다). 디스크파일로(섹터·블록을 몰라도 이름으로 읽고 쓴다), 네트워크 카드소켓으로 추상화된다. 우리가 프로그램에서 변수에 값을 담고 소켓으로 통신할 수 있는 건 — 그 아래 OS가 이 추상들을 깔아 줬기 때문이다.

아무나 하드웨어를 못 만지게 — 커널 모드와 유저 모드

추상화만큼 중요한 게 보호다. 한 프로그램의 버그가 다른 프로그램이나 OS 자체를 망가뜨리면 안 된다. 그래서 CPU는 두 개의 권한 등급을 하드웨어로 강제한다 — **커널 모드(ring 0)**는 하드웨어에 직접 접근하는 모든 권한을 가진 OS의 영역이고, **유저 모드(ring 3)**는 제한된 권한만 가진 일반 프로그램의 영역이다.

핵심은 — *유저 모드 코드가 특권 명령(하드웨어 직접 제어, 남의 메모리 접근)을 시도하면, CPU가 *예외(trap)를 일으켜 막는다는 것이다. (출처: User mode vs Kernel mode.) 그래서 내 프로그램이 폭주해도 — 기껏해야 그 프로그램만 죽고 OS와 다른 프로그램은 멀쩡하다. 이 하드웨어가 강제하는 경계프로세스 격리와 권한 분리의 뿌리다.

부탁할 땐 시스템 콜 — 유저에서 커널로

그런데 일반 프로그램도 파일을 읽거나 네트워크로 보내려면 결국 하드웨어를 써야 한다. 직접은 못 하니, OS에게 부탁한다 — 그 창구가 **시스템 콜(system call)**이다. read·write·open·fork·socket 같은 게 모두 시스템 콜로, *유저 모드에서 커널 모드로 안전하게 전환(trap)*해 OS가 대신 일을 하고 결과를 돌려준다.

다이어그램 로딩 중…

왜 알아야 하나 — 이 모드 전환공짜가 아니다. 시스템 콜마다 유저↔커널 전환 비용이 들어서, 시스템 콜을 자주 부르면 느려진다. 그래서 작은 read를 수천 번 부르는 대신 한 번에 모아(버퍼링) 시스템 콜 횟수를 줄이는 게 성능의 기본이다. *"비싼 전환을 아껴라"*는 이 원칙은 — 뒤에서 볼 문맥 전환·I/O까지 OS 전반을 관통한다.

실제로 눈으로 보기

시스템 콜을 직접 들여다본다

strace -f ls /tmp 2>&1 | head -12
# execve("/usr/bin/ls", ["ls", "/tmp"], 0x7ffd...) = 0
# openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
# mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f...
# openat(AT_FDCWD, "/tmp", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
# getdents64(3, 0x55..., 32768) = 512
# write(1, "file1  file2\n", 13) = 13

ls라는 평범한 명령 하나가 커널에 이만큼 부탁하고 있다. 파일을 여는 것도(openat), 목록을 읽는 것도(getdents64), 화면에 찍는 것도(write) *전부 시스템 콜*이다 — 프로그램은 하드웨어를 직접 못 만지므로 매번 커널에게 부탁해야 한다.

유저 모드와 커널 모드에 쓴 시간이 따로 나온다

time ./myapp
# real  0m3.214s     ← 실제 흐른 시간(대기 포함)
# user  0m0.412s     ← 유저 모드에서 쓴 CPU 시간
# sys   0m2.688s     ← 커널 모드에서 쓴 CPU 시간

sysuser보다 훨씬 크면 일보다 부탁이 많다는 뜻이다. 대개 시스템 콜을 잘게 너무 자주 부르는 경우다.

strace -c -f ./myapp
# % time     seconds  usecs/call     calls    errors syscall
# ------ ----------- ----------- --------- --------- ----------------
#  71.42    1.882014           3    588012           write
#  18.03    0.475122           4    112004           read

write를 58만 번 부르고 있다. 한 번에 1바이트씩 쓰면 이렇게 된다 — 버퍼에 모아 한 번에 쓰면 호출 수가 수백 배 준다. 시스템 콜 하나하나가 *모드 전환*이라는 값을 치르기 때문에, 이 숫자가 곧 성능이다.

모드 전환이 정말 비싼가

getpid()처럼 아주 가벼운 시스템 콜커널로 안 들어가고 처리되기도 한다. 리눅스는 vDSO라는 영역을 프로세스 주소 공간에 매핑해 두고, 시간 조회 같은 일부 호출을 유저 모드에서 그대로 끝낸다.

cat /proc/self/maps | grep vdso
# 7ffd8b3f4000-7ffd8b3f6000 r-xp 00000000 00:00 0    [vdso]

이런 장치가 있다는 것 자체가 모드 전환 비용이 무시할 수 없다는 증거다. 비싸지 않다면 굳이 피해 갈 이유가 없다.

정리하면, OS는 한정된 하드웨어를 수많은 프로그램이 안전하게 공유하게 만드는 관리자다 — CPU·메모리·디스크·네트워크를 프로세스·가상 메모리·파일·소켓으로 추상화해 복잡함을 가리고, 커널/유저 모드라는 하드웨어 경계로 프로그램을 서로 보호하며, 시스템 콜유저가 커널에 안전하게 일을 부탁하게 한다. 이 토대 위에서 — CPU를 어떻게 나누고(프로세스·스케줄링), 메모리를 어떻게 속이고(가상 메모리), 공유 자원의 함정을 어떻게 피하는지(동시성) 가 펼쳐진다. 그 첫걸음, 프로세스부터 본다.

프로세스 — 주소공간·fork/exec·컨텍스트 스위치