프로세스 생성과 복사
Win32 API vs UNIX
createProcess() | fork(), exec() |
ExitProcess() | exit() |
waitForSingleObject() | wait() |
가상메모리 애기를 하면 항상 따라다니는 놈이 process다. process단위로 OS가 접근제어를 하는데 파일이나 여러 자원에 대한 접근제어를 의미한다. 어떤 프로세스에 대해 OS가 접근허가를 하면 그 권한을 쓰레드들은 다 공유하게 된다. 그리고 가상메모리라는 공간은 여러 프로세스가 실행되고 있어도 이 가상 메모리 공간은 독립적인 공간으로서 보장을 받는다. 그리고 그 thred들이 가상 메모리를 사용이 가능하다.
우리가 OS내에서 새로운 프로세스를 생성해야 한다고 보면 새로운 프로세스가 생성이 될때마다 독립적인 가상메모리 공간을 가져야 하는데 가상메모리 공간을 프로세스마다 OS가 할당해줘야 한다.
만약 어떤 프로세스가 있다고 해보자. 이 프로세스는 자기만의 고유의 PCB도 갖고 가상메모리 공간을 갖는다. 이 프로세스가 새로운 프로세스를 생성을 하면 이 2개의 프로세스의 관계가 설정이 되는데 기존 프로세스를 부모 프로세스, 새로 생긴 프로세스를 자식 프로세스라고 한다.
그런데 신기한 것이 위의 표를 보면 윈도우 경우 프로세스 생성 함수가 createProcess로 하나지만 UNIX는 fork(), exec() 2개가 존재한다. 왜 그런것일까?
- 프로세스가 생성이 되면 이 프로세스를 쓰기 위해서 해당 프로세스의 메모리영역에 text영역에 존재하는 실행코드를 복사 후 이 실행코드에 대한 PCB가 생성이 되고, 가상메모리 공간이 확보가 된다.
- 이 가상메모리 공간 속에 heap영역, Stack영역, Data라는 정적 static 영역등이 들어가져 있다
우리가 C/C++을 배워서 실행파일을 만들면 Unix계열은 a.out으로 Window는 a.exe로 떨어진다. 이때, 윈도우 계열 실행파일은 PE format을 하고 있고 유닉스 계열은 ELF format을 하고 있다. 이때, PE format과 ELF format은 서로 비슷하지만 PE format이 좀 더 복잡하다. 아래 그림을 보면 이해될 것이다.
- PCB 생성 및 메모리 할당: 프로세스 제어 블록을 생성하고, 프로세스가 사용할 가상 메모리 공간을 할당한다.
- 포맷 분석: PE나 ELF 포맷의 실행 파일을 분석하여, 필요한 섹션(특히 텍스트 섹션)을 메모리에 복사한다.
- 실행 준비 완료: 모든 준비가 끝나면 프로세스가 CPU에서 실행을 시작한다.
이 과정이 굉장히 복잡하고 오래 걸려서 이 작업을 효율적으로 하다보니 Window에서는 createProcess() 하나지만 Linux에서는 fork(), exec() 2개로 구성이 된다. 그럼 본론으로 가서 왜 2개일까?
이 복잡한 과정을 효율적으로 처리하기 위해서이긴 한데, fork()는 새로운 프로세스(자식)를 생성하고
- 그리고 메모리는 Copy on Write 방식을 사용하여 실제 메모리를 전부 복사하지 않고, 부모와 자식이 동일한 물리 메모리 페이지를 참조하다가 어느 한 쪽이 데이터를 변경할려고 할때에만 해당 페이지를 복사하는 구조이다.
- 그리고 레지스터나 프로그램 카운터 값도 복제한다. 그래서 사실상 동일한 코드 지점을 가지고 있지만, fork() 성공된 후 각자 독립된 흐름으로 실행된다.
반면, exec()는 현재 프로세스의 메모리 공간(텍스트, 데이터, 힙, 스택 등)을 새로운 프로그램의 코드, 데이터로 완전히 갈아끼우지만, 기존 PCB를 재활용하고 새로운 프로그램을 로딩하는 시스템 호출이다.
- 새로운 프로그램 로드
- exec()를 호출하면, 운영체제는 지정된, 새로운 실행 파일(예: ELF)을 열고, 해당 바이너리의 텍스트(.text) 섹션, 데이터(.data) 섹션, BSS(.bss) 섹션 등을 새롭게 매핑한다.
- 이 과정에서 기존에 사용하던 메모리(코드/데이터/힙/스택)는 완전히 해제되고, 새로운 프로그램 이미지로 대체됩니다.
- PCB 재활용
- exec()는 기존의 PCB를 버리지 않고, 프로세스 ID(PID)도 그대로 유지된다.
- 다만, PCB 안에 있는 “메모리 관리 정보(코드, 데이터, 스택, 힙 정보)”나 “CPU 레지스터 상태” 등이 새로운 프로그램 기준으로 다시 세팅된다.
그럼 왜 2개일까?
- 역사적: 초기 Unix에서 “프로세스 생성”과 “새 프로그램 로딩”을 분리한 전통이 이어져 왔고,
- 설계 철학: “한 가지 함수는 한 가지 일만 한다”라는 Unix 철학과 “쉘이 부모로서 계속 살아 있어야 한다”는 요구사항,
- 유연성: 부모·자식이 분기된 시점에 필요한 조작(파일 디스크립터, 환경 변수, 파이프 설정 등)을 할 수 있고, 이후 exec()로 프로그램을 로딩하기 때문에, 다양한 시나리오를 쉽게 지원,
- 효율성 보완: 현대 OS에서는 fork() 복제 비용을 Copy-on-Write로 크게 줄였으므로, 2단계 구조라도 성능상 치명적이지 않음.
이 구조 덕분에, Unix 계열에서는 **프로세스 제어(부모-자식 관계, 파이프, 리다이렉션 등)**가 매우 직관적으로 이뤄지며, 운영체제 내부 구현도 “프로세스 복제”와 “프로그램 로딩”을 비교적 모듈화해서 다룰 수 있게 된 것.
그리고 wait와 waitForSingleObject는 어떤 하나의 Object를 기다린다는 말로 가끔 프로세스가 종료될때까지 기다려야 하는 상황에 사용한다. 이 함수들을 사용하면 프로세스 상태가 대기상태로 빠지게 된다.
출처
곰책으로 쉽게 배우는 최소한의 운영체제론 강의 | 널널한 개발자 - 인프런
널널한 개발자 | '넓고 얕게 외워서 컴공 전공자 되기' 강의를 끝낸 분들이 운영체제에 대해 좀 더 깊이 있는 공부를 할 수 있도록 제공되는 강의입니다., 운영체제도 널널한 개발자와 함께! 👨
www.inflearn.com
'운영체제' 카테고리의 다른 글
CPU 스케줄링 (0) | 2025.01.13 |
---|---|
프로세스와 쓰레드 (0) | 2025.01.12 |
프로세스 휴식, 보류 상태와 문맥 (0) | 2025.01.12 |
프로세스의 상태 변화 (0) | 2025.01.12 |
CPU 예측이 가져올 수 있는 문제점 (0) | 2025.01.12 |