OSTEP - 1 [가상화]

김태훈·2026년 5월 31일

OSTEP 스터디 정리: 프로세스, fork/exec, 좀비 프로세스, 그리고 CPU 가상화

  • 폰 노이만 구조와 하버드 구조
  • 프로세스와 가상 주소 공간
  • fork()exec()
  • 고아 프로세스와 좀비 프로세스
  • 컨테이너에서 PID 1이 중요한 이유
  • Limited Direct Execution
  • trap, interrupt, context switch
  • 스케줄링과 Work Stealing

1. 폰 노이만 구조와 하버드 구조

스터디 초반에는 폰 노이만 구조와 하버드 구조 이야기가 나왔다.

단순히 외우면 이렇게 말할 수 있다.

구조특징
폰 노이만 구조명령어와 데이터를 같은 메모리에 저장한다
하버드 구조명령어 메모리와 데이터 메모리를 분리한다

그런데 여기서 더 중요한 건, 폰 노이만 구조가 단순히 “메모리를 하나로 쓴다”는 점이 아니다.

폰 노이만 구조의 진짜 의미는 프로그램 자체를 메모리에 저장할 수 있게 되었다는 점이다.

과거에는 어떤 계산을 하려면 물리적으로 배선을 바꾸는 방식에 가까웠다.
즉, 프로그램이 소프트웨어라기보다 하드웨어의 배선 상태에 가까웠다.

하지만 폰 노이만 구조에서는 명령어의 순서를 메모리에 저장한다.
CPU는 메모리에 있는 명령어를 하나씩 가져와 실행한다.

즉, 프로그램을 바꾸고 싶으면 물리적인 배선을 바꾸는 게 아니라, 메모리에 올라간 명령어 데이터를 바꾸면 된다.

이게 소프트웨어라는 개념을 가능하게 만든 핵심이다.


2. 현대 CPU는 폰 노이만인가, 하버드인가?

대화 중에 “현대 CPU는 하버드 구조에 가까운 것 아니냐”는 이야기가 나왔다.

정리하면 이렇게 보는 게 적절하다.

운영체제나 프로그래머가 바라보는 메모리 모델은 대체로 폰 노이만 구조에 가깝다.
하지만 CPU 내부 구현, 특히 캐시 계층에서는 하버드 구조의 아이디어를 일부 사용한다.

예를 들어 현대 CPU에는 보통 다음이 분리되어 있다.

Instruction Cache  → 명령어 가져오기
Data Cache         → 데이터 읽기/쓰기

명령어를 가져오는 경로와 데이터를 접근하는 경로가 분리되어 있으면, 파이프라인에서 다음 명령어를 가져오는 작업과 현재 명령어의 데이터 접근 작업을 더 효율적으로 겹칠 수 있다.

여기서 스터디 중 헷갈렸던 지점이 하나 있다.

단일 코어인데 어떻게 동시에 명령어 fetch와 데이터 load가 일어나지?

이건 “코어가 하나니까 모든 일이 완전히 순차적으로만 일어난다”라고 생각하면 헷갈린다.

CPU 내부에는 명령어를 가져오는 유닛, 디코딩하는 유닛, 실행하는 유닛, 메모리에 접근하는 유닛 등이 있다.
하나의 코어 안에서도 회로 수준에서는 여러 단계가 파이프라인처럼 겹쳐서 동작할 수 있다.

즉, 단일 코어라는 말은 “하나의 실행 흐름만 존재한다”는 뜻에 가깝지, CPU 내부의 모든 전기적 동작이 한 줄로만 순차 실행된다는 뜻은 아니다.


3. 구조적 해저드와 메모리 버스

폰 노이만 구조에서는 명령어와 데이터가 같은 메모리 경로를 공유한다.

파이프라인을 생각해보자.

명령어 A: 데이터를 메모리에서 읽는다
명령어 B: 다음에 실행될 명령어다

이때 CPU는 한편으로는 명령어 A의 데이터 메모리 접근을 해야 하고, 다른 한편으로는 명령어 B를 fetch해야 한다.

그런데 명령어와 데이터가 같은 메모리 버스나 같은 메모리 포트를 공유한다면, 둘을 동시에 처리하기 어렵다.

이런 식으로 여러 명령어가 파이프라인에서 동시에 진행되려 할 때, 같은 하드웨어 자원을 두고 충돌하는 상황을 구조적 해저드(structural hazard)라고 한다.

해결 방법은 크게 두 가지다.

1. 기다린다
   → 파이프라인에 bubble을 넣는다
   → 성능 손해가 있다

2. 하드웨어 자원을 분리한다
   → instruction cache / data cache 분리
   → 충돌 가능성을 줄인다

그래서 현대 CPU는 전체적으로는 폰 노이만 모델 위에 있지만, 내부적으로는 하버드 구조의 장점을 일부 섞어서 쓴다고 볼 수 있다.


4. 프로세스와 가상 주소 공간

다음으로 프로세스 이야기가 나왔다.

프로세스를 이해할 때 중요한 개념은 프로세스마다 자기만의 가상 주소 공간을 가진다는 점이다.

예를 들어 두 개의 프로세스에서 같은 주소를 출력했다고 해보자.

printf("%p\n", &value);

두 프로세스에서 우연히 같은 주소값이 나올 수 있다.

하지만 그 주소가 실제 물리 메모리의 같은 위치를 의미하는 것은 아니다.

프로세스가 보는 주소는 가상 주소다.
운영체제와 MMU가 이 가상 주소를 실제 물리 주소로 변환한다.

그래서 프로세스 A의 0x1000과 프로세스 B의 0x1000은 겉으로는 같은 주소처럼 보여도, 실제로는 서로 다른 물리 페이지를 가리킬 수 있다.

물론 예외도 있다.
공유 메모리나 공유 라이브러리처럼 의도적으로 같은 물리 페이지를 여러 프로세스가 매핑할 수도 있다.

하지만 기본적인 사고방식은 이거다.

프로세스가 보는 주소는 절대적인 물리 주소가 아니라, 자기 가상 주소 공간 안에서의 위치다.


5. 프로그램이 실행될 때 필요한 것들

프로그램 실행을 단순히 이렇게 생각하기 쉽다.

실행 파일을 메모리에 올린다
CPU가 실행한다

틀린 말은 아니지만 너무 많이 생략되어 있다.

실제로 프로세스를 실행하려면 운영체제가 많은 것을 준비해야 한다.

  • 코드 영역 매핑
  • 정적 데이터 영역 매핑
  • 힙 영역 준비
  • 스택 영역 준비
  • 파일 디스크립터 설정
  • 레지스터 초기값 설정
  • 프로그램 카운터 설정
  • argc, argv, envp 전달

즉, 프로세스는 단순히 코드 덩어리가 아니다.

프로세스는 실행 중인 프로그램을 표현하기 위한 운영체제의 관리 단위다.
코드뿐 아니라 주소 공간, 파일 디스크립터, 레지스터 상태, 스택, 힙, 부모/자식 관계 등 여러 상태를 함께 가진다.

JVM도 결국은 하나의 프로세스다.

JVM 내부에 Java Heap, Thread Stack, Metaspace 같은 런타임 구조가 있더라도, 운영체제 입장에서는 JVM 프로세스의 가상 주소 공간 안에서 동작하는 것이다.


6. 왜 fork 후 exec를 할까?

스터디에서 가장 재밌던 질문 중 하나가 이거였다.

새 프로그램을 실행하려면 그냥 run(program) 같은 시스템 콜 하나면 되지 않을까?
왜 굳이 fork()로 프로세스를 복제한 다음 exec()로 프로그램을 갈아끼울까?

직관적으로는 이상해 보인다.

셸에서 ls를 실행한다고 해보자.

ls

실제로는 대략 이런 흐름이다.

1. shell 프로세스가 fork()를 호출한다
2. child 프로세스가 생긴다
3. child 프로세스가 exec("ls")를 호출한다
4. child 프로세스의 프로그램 이미지가 ls로 교체된다
5. parent shell은 wait()로 child가 끝나길 기다린다

처음 보면 비효율적으로 보인다.
왜 셸을 복사한 뒤에 바로 ls로 갈아끼우는 걸까?

핵심은 fork와 exec 사이에 자식 프로세스의 실행 환경을 조작할 수 있다는 점이다.

예를 들어 리다이렉션을 생각해보자.

ls > result.txt

이 경우 셸은 대략 이런 일을 한다.

1. fork()로 자식 프로세스를 만든다
2. 자식 프로세스에서 stdout을 result.txt로 바꾼다
3. exec("ls")를 호출한다

파이프도 마찬가지다.

cat file.txt | grep hello

각 프로세스가 어떤 stdin/stdout을 바라볼지 exec() 전에 조정할 수 있다.

만약 fork + exec가 아니라 “새 프로그램 실행”이라는 단일 시스템 콜만 있었다면, 리다이렉션, 파이프, 파일 디스크립터 조작, 환경 변수 설정 같은 옵션을 시스템 콜 파라미터에 전부 욱여넣어야 했을 것이다.

그렇게 되면 시스템 콜은 점점 복잡해지고 덜 유연해진다.

반대로 fork()exec()를 분리하면 구조가 단순해진다.

fork()
→ 자식 프로세스 생성

자식 프로세스에서 필요한 환경 설정
→ 파일 디스크립터 변경
→ 권한 변경
→ 환경 변수 변경

exec()
→ 새 프로그램으로 교체

이게 Unix 계열 시스템에서 fork/exec 모델이 강력한 이유다.


7. fork는 진짜 메모리를 전부 복사할까?

여기서 또 하나의 의문이 생긴다.

부모 프로세스가 엄청 무거우면 fork도 엄청 무거운 것 아닌가?

현대 운영체제에서는 일반적으로 fork()가 부모의 메모리 내용을 즉시 전부 복사하지 않는다.

대신 Copy-on-Write를 사용한다.

처음에는 부모와 자식이 같은 물리 페이지를 공유한다.
그러다가 둘 중 하나가 해당 페이지를 수정하려고 할 때 그때 실제 복사가 일어난다.

fork 직후:
부모와 자식이 같은 페이지를 공유

자식이 특정 페이지 수정:
그 페이지를 복사해서 자식에게 별도 페이지 제공

그래서 fork()는 생각보다 가볍게 동작할 수 있다.

물론 그래도 프로세스 메타데이터, 페이지 테이블 등의 비용은 있다.
그래서 vfork(), posix_spawn() 같은 대안도 존재한다.

방식특징
fork부모 프로세스를 복제한다. 보통 COW로 최적화된다
exec현재 프로세스 이미지를 새 프로그램으로 교체한다
vfork자식이 exec/exit 하기 전까지 부모 주소 공간을 빌려 쓰는 식의 최적화
posix_spawnfork+exec 패턴을 더 효율적으로 제공하기 위한 API

다만 블로그에 쓸 때는 너무 단정적으로 “Linux의 모든 프로세스 생성은 무조건 fork+exec다”라고 쓰면 위험하다.

셸에서 명령을 실행하는 대표적인 흐름은 fork+exec가 맞다.
하지만 내부적으로는 clone, vfork, posix_spawn 등 여러 방식이 있고, 런타임이나 libc 구현에 따라 달라질 수 있다.


8. PID 1, 고아 프로세스, 좀비 프로세스

스터디 중에 고아 프로세스와 좀비 프로세스가 섞여서 이야기됐다.

이 둘은 반드시 구분해야 한다.

8.1 고아 프로세스

고아 프로세스는 부모 프로세스가 먼저 종료됐지만 자식 프로세스는 아직 살아 있는 상태다.

parent 종료
child는 계속 실행 중

이 경우 자식 프로세스는 새로운 부모에게 입양된다.

전통적으로는 PID 1 프로세스가 그 역할을 한다.
Linux에서는 subreaper가 설정된 프로세스가 있다면 그쪽으로 reparent될 수도 있다.

중요한 점은, 고아 프로세스 자체가 곧 문제는 아니라는 것이다.

부모가 없어졌을 뿐, 프로세스는 아직 정상적으로 실행 중이다.

8.2 좀비 프로세스

좀비 프로세스는 다르다.

좀비 프로세스는 자식 프로세스가 이미 종료됐는데, 부모 프로세스가 wait 계열 시스템 콜로 종료 상태를 회수하지 않은 상태다.

프로세스가 종료되면 대부분의 자원은 정리된다.

- 사용자 메모리 대부분 해제
- 열린 파일 정리
- 실행은 끝남

하지만 부모에게 전달해야 하는 최소한의 정보는 남아 있어야 한다.

- PID
- 종료 코드
- 자원 사용 정보 일부
- 프로세스 테이블 엔트리

이 상태가 좀비다.

그래서 좀비 프로세스는 이미 죽은 프로세스다.

kill -9를 보내도 사라지지 않는다.
이미 실행 중인 프로세스가 아니기 때문이다.

좀비를 없애려면 부모가 wait()를 호출해야 한다.
또는 부모가 죽어서 좀비가 PID 1이나 subreaper에게 reparent되고, 그 프로세스가 wait()을 해줘야 한다.


9. 컨테이너에서 PID 1이 중요한 이유

컨테이너에서는 이 문제가 더 현실적으로 다가온다.

컨테이너 안에서 Java 애플리케이션이 PID 1로 실행되는 경우가 많다.

컨테이너 namespace 안:
PID 1 = java

그런데 PID 1은 일반 프로세스와 조금 다르게 취급된다.

특히 컨테이너 안에서 PID 1이 자식 프로세스를 만들고, 그 자식이 종료됐는데 제대로 wait()하지 않으면 좀비가 남을 수 있다.

Java 애플리케이션이 원래 init 시스템처럼 자식 프로세스를 회수하도록 설계된 것은 아니다.

그래서 컨테이너에서 백그라운드 프로세스를 띄우거나, nohup으로 스크립트를 실행하거나, ProcessBuilder로 외부 프로세스를 실행하는 경우 조심해야 한다.

여기서 nohup도 정확히 정리해야 한다.

nohup은 부모를 PID 1로 바꾸는 명령이 아니다.

기본적으로는 SIGHUP을 무시하도록 만들어서, 터미널이 끊겨도 프로세스가 계속 실행되게 하는 도구다.

다만 셸이 종료되면 그 셸의 자식 프로세스는 고아가 되고, 이후 PID 1이나 subreaper에게 reparent될 수 있다.

즉, 아래처럼 이해하면 부정확하다.

nohup 자체가 reparent를 수행한다

좀 더 정확히는 이렇다.

nohup으로 실행한 프로세스가 셸 종료 후에도 살아남는다
→ 부모 셸이 죽는다
→ 고아가 된다
→ PID 1 등으로 reparent된다

컨테이너에서 이런 문제를 줄이려면 보통 이런 방법을 쓴다.

- 컨테이너에 tini, dumb-init 같은 init 프로세스를 둔다
- Docker라면 --init 옵션을 고려한다
- Java에서 외부 프로세스를 띄웠다면 Process.waitFor()로 회수한다
- 불필요한 백그라운드 프로세스를 컨테이너 안에서 만들지 않는다

운영 환경에서 kubectl exec로 들어가서 nohup script.sh & 같은 걸 실행하는 습관은 생각보다 위험할 수 있다.


10. Limited Direct Execution

다음으로 Limited Direct Execution 이야기가 나왔다.

처음에는 “Direct Execution이 무슨 뜻이지?”가 헷갈릴 수 있다.

Direct Execution은 말 그대로 사용자 프로그램을 CPU에서 직접 실행한다는 뜻이다.

운영체제가 모든 명령어를 하나씩 해석해서 실행하는 게 아니다.
사용자 프로그램의 일반 명령어는 CPU가 직접 실행한다.

하지만 완전히 직접 실행만 허용하면 문제가 생긴다.

사용자 프로그램이 마음대로 이런 일을 할 수 있기 때문이다.

- 커널 메모리 접근
- 디스크 직접 제어
- 네트워크 장치 직접 제어
- 다른 프로세스 메모리 접근
- 무한 루프로 CPU 독점

그래서 운영체제는 두 가지를 동시에 만족해야 한다.

1. 성능
   → 가능한 한 사용자 프로그램을 CPU에서 직접 실행하게 둔다

2. 통제
   → 위험한 작업이나 자원 접근은 커널이 개입한다

이 절충안이 Limited Direct Execution이다.

일반적인 계산은 user mode에서 직접 실행한다.
하지만 파일 I/O, 네트워크 I/O, 프로세스 생성, 메모리 매핑 같은 작업은 시스템 콜을 통해 kernel mode로 들어간다.

user mode 실행
→ system call
→ trap
→ kernel mode 진입
→ 커널 코드 실행
→ 다시 user mode 복귀

즉, 운영체제는 항상 프로그램의 모든 명령에 개입하지 않는다.
그랬다면 너무 느렸을 것이다.

대신 필요한 순간에만 개입한다.


11. 트랩, 인터럽트, 컨텍스트 스위칭

사용자 프로그램이 시스템 콜을 호출하면 trap이 발생한다.

trap은 의도적으로 커널에 진입하기 위한 메커니즘이다.

반면 인터럽트는 외부 장치나 타이머 등에 의해 비동기적으로 발생할 수 있다.

둘 다 결과적으로 CPU 실행 흐름을 커널 쪽으로 넘긴다는 점에서는 비슷하지만, 발생 원인이 다르다.

trap
→ 사용자 프로그램이 의도적으로 커널 기능을 요청
→ 예: read(), write(), open()

interrupt
→ 외부 이벤트나 타이머에 의해 발생
→ 예: 키보드 입력, 네트워크 패킷 도착, timer interrupt

컨텍스트 스위칭은 보통 타이머 인터럽트와 깊게 연결된다.

만약 타이머 인터럽트가 없다면, CPU를 잡은 프로세스가 자발적으로 시스템 콜을 호출하거나 종료할 때까지 계속 실행될 수 있다.

이러면 운영체제가 CPU를 공정하게 나눠주기 어렵다.

그래서 하드웨어 타이머가 주기적으로 인터럽트를 발생시킨다.
운영체제는 이 시점에 제어권을 되찾고, 현재 프로세스를 계속 실행할지, 다른 프로세스로 바꿀지 결정한다.

프로세스 A 실행
→ timer interrupt
→ kernel mode 진입
→ scheduler 실행
→ 프로세스 B 선택
→ context switch
→ 프로세스 B 실행

여기서 스케줄러는 “누구를 다음에 실행할지”를 결정하는 알고리즘이다.

타이머 인터럽트는 “언제 커널이 제어권을 다시 가져올지”와 관련된 메커니즘이다.

둘은 연결되어 있지만 같은 개념은 아니다.


12. 커널 스택과 유저 스택

스터디 중에 유저 모드 스택과 커널 모드 스택 이야기도 나왔다.

프로세스가 user mode에서 실행될 때 사용하는 스택과, kernel mode에 진입했을 때 사용하는 스택은 일반적으로 분리된다.

이유는 보안과 안정성 때문이다.

만약 커널이 사용자 스택 위에서 그대로 실행된다면, 사용자가 스택 내용을 조작해서 커널 실행에 영향을 줄 수 있다.

그래서 일반적으로는 프로세스 또는 스레드마다 커널 스택이 따로 존재한다.

시스템 콜이나 인터럽트로 커널에 진입하면 CPU는 커널 스택으로 전환하고, 그 위에서 커널 코드를 실행한다.

개념적으로 보면 이런 흐름이다.

user mode
- user stack 사용
- 사용자 코드 실행

trap / interrupt 발생

kernel mode
- kernel stack 사용
- 커널 코드 실행
- 필요한 레지스터 저장
- 스케줄링 또는 시스템 콜 처리

return to user mode
- user stack으로 복귀
- 사용자 코드 재개

이 부분은 나중에 context switch, trap frame, PCB, thread_info 같은 구조를 볼 때 더 명확해진다.


13. 스케줄링: Round Robin, MLFQ, Lottery Scheduling

스터디 후반에는 스케줄링 이야기가 나왔다.

Round Robin은 가장 직관적인 방식이다.

각 프로세스에게 같은 시간 조각을 준다
순서대로 돌아가며 실행한다

CPU-bound 작업만 있다면 꽤 단순하고 괜찮게 동작한다.

하지만 현실의 애플리케이션은 CPU 작업만 하지 않는다.
I/O를 기다리는 작업도 많다.

I/O-bound 작업은 CPU를 오래 쓰지 않고 자주 대기 상태로 빠진다.

이런 작업까지 단순히 Round Robin으로만 다루면 응답 시간이 나빠질 수 있다.

그래서 MLFQ 같은 방식이 나온다.

MLFQ는 대략 이런 방향성을 가진다.

- 짧게 실행되고 자주 양보하는 작업은 우선순위를 높게 유지
- CPU를 오래 독점하는 작업은 점점 낮은 큐로 이동
- interactive한 작업의 응답성을 좋게 만든다

Lottery Scheduling은 접근이 조금 다르다.

프로세스마다 추첨권을 나눠주고, 매번 랜덤 추첨으로 다음 실행 대상을 고른다.

A 프로세스: ticket 75장
B 프로세스: ticket 25장

그러면 장기적으로 A가 CPU를 약 75%, B가 약 25% 정도 가져갈 가능성이 높아진다.

장점은 단순하다는 것이다.
정교하게 누적 실행 시간을 계산하지 않아도 비율 기반의 공정성을 표현할 수 있다.


14. 멀티코어 스케줄링과 Work Stealing

단일 CPU만 생각하면 하나의 run queue를 두고 스케줄링하면 된다.

하지만 멀티코어에서는 문제가 생긴다.

모든 코어가 하나의 전역 큐를 공유하면 어떻게 될까?

Core 1
Core 2
Core 3
Core 4
  ↓
Global Run Queue

모든 코어가 같은 큐에 접근하므로 락 경합이 생길 수 있다.
스케줄링 자체가 병목이 될 수 있다.

그래서 코어별로 큐를 나누는 전략이 나온다.

Core 1 → Run Queue 1
Core 2 → Run Queue 2
Core 3 → Run Queue 3
Core 4 → Run Queue 4

이 방식은 장점이 있다.

- 전역 큐 락 경합을 줄일 수 있다
- CPU cache locality를 살릴 수 있다
- 각 코어가 자기 큐를 중심으로 빠르게 스케줄링할 수 있다

하지만 단점도 있다.

어떤 코어의 큐는 비었고, 다른 코어의 큐에는 작업이 많이 쌓일 수 있다.

Core 1 → 일이 없음
Core 2 → 일이 많음

이때 놀고 있는 코어가 다른 코어의 큐에서 작업을 가져오는 전략이 Work Stealing이다.

이 개념은 Java의 ForkJoinPool과도 닮아 있다.

ForkJoinPool도 워커 스레드마다 work queue를 가지고, 자기 큐가 비면 다른 워커의 큐에서 작업을 훔쳐올 수 있다.

다만 여기서도 너무 단순화하면 안 된다.

Work stealing은 공짜가 아니다.

- 다른 큐를 확인하는 비용
- 동시성 제어 비용
- CAS 또는 lock 비용
- cache locality 저하 가능성

그래서 work stealing은 “항상 훔쳐오면 된다”가 아니라, 내 큐가 비었을 때 부하를 맞추기 위한 보조 전략에 가깝다.


15. 이번 스터디에서 정리된 핵심

이번 대화에서 가장 좋았던 부분은 단순히 개념을 외운 게 아니라, 계속 “왜 이렇게 만들었지?”를 물었다는 점이다.

정리하면 핵심은 이렇다.

폰 노이만 구조
→ 프로그램을 메모리에 저장할 수 있게 되면서 소프트웨어의 유연성이 생겼다.

하버드 구조
→ 명령어와 데이터 경로를 분리해 파이프라인/캐시 효율을 높일 수 있다.

프로세스
→ 실행 중인 프로그램이며, 가상 주소 공간과 실행 문맥을 가진다.

fork/exec
→ 새 프로세스를 만든 뒤, exec 전에 실행 환경을 자유롭게 조작할 수 있게 해준다.

좀비 프로세스
→ 이미 종료됐지만 부모가 wait하지 않아 최소한의 종료 정보가 남아 있는 상태다.

PID 1
→ 고아 프로세스를 입양하고, 종료된 자식 프로세스를 회수하는 책임을 가진다.

Limited Direct Execution
→ 사용자 코드는 가능한 직접 실행하되, 위험한 작업은 커널이 trap을 통해 통제한다.

Timer Interrupt
→ 운영체제가 CPU 제어권을 주기적으로 되찾아 스케줄링할 수 있게 해준다.

Work Stealing
→ 멀티코어/멀티스레드 환경에서 큐 불균형을 완화하기 위한 전략이다.

16. 마무리

운영체제 공부를 하다 보면 처음에는 용어가 너무 많아서 각각을 따로 외우게 된다.

process
fork
exec
trap
interrupt
context switch
scheduler
zombie
orphan
PID 1

그런데 이번에 다시 느낀 건, 이 개념들이 따로 떨어져 있지 않다는 점이다.

프로세스를 만들려면 fork/exec를 알아야 하고,
프로세스가 끝나면 wait와 좀비 프로세스를 알아야 하고,
프로세스를 공정하게 실행하려면 timer interrupt와 scheduler를 알아야 하고,
사용자 프로그램을 안전하게 실행하려면 user mode/kernel mode와 trap을 알아야 한다.

결국 운영체제가 하는 일은 이런 것이다.

프로그램이 CPU를 직접 쓰는 것처럼 보이게 만들되,
실제로는 운영체제가 언제든 제어권을 되찾을 수 있게 만드는 것.

이 관점으로 보면 프로세스, 시스템 콜, 인터럽트, 스케줄링이 하나의 흐름으로 이어진다.

마지막으로 스스로 점검해볼 질문은 이거다.

forkexec가 분리되어 있지 않았다면, shell의 pipe와 redirection은 어떤 식으로 구현해야 했을까?

이 질문에 답할 수 있으면 이번 스터디의 프로세스 파트는 꽤 잘 잡힌 거라고 봐도 될 것 같다.

profile
기록하고, 공유합시다

0개의 댓글