Search

[2장 프로세스 관리(기초)]5.프로세스 생성 - fork(), execve()

Publish Date
Category
Status
Done
1 more property
ecve()
프로세스 생성
리눅스에서 fork()execve()를 활용하여 새로운 프로세스를 생성하고, 동일 프로그램의 병렬 처리를 실현하거나 새로운 프로그램을 실행하는 방법을 이해한다.

프로세스 생성

프로세스 생성 목적

프로세스를 생성하는 데는 두 가지 주요 목적이 있다.
목적1: 동일한 프로그램 병렬처리
동일한 프로그램을 여러 프로세스로 나누어 병렬로 처리하기 위함이다.
이 경우 fork()함수를 사용하여 처리한다.
목적2: 새로운 프로그램 실행
이 경우는 새로운 프로그램을 실행하기 위해서 프로세스를 생성하는 것이다.
fork(), execve() 함수 모두 사용한다.

프로세스 생성 방법

리눅스에서 fork(), exec() 함수를 사용하여 프로세스를 생성한다.
fork() 함수:
부모 프로세스의 복사본(자식 프로세스)를 생성한다.
내부적으로 clone() 시스템 콜을 호출한다.
exec()함수:
자식 프로세스를 새로운 프로그램으로 치환한다.
내부적으로 execvel() 시스템 콜을 호출한다.

같은 프로세스를 두개로 분열 시키는 fork()함수

fork() 함수는 현재 실행 중인 프로세스의 복사본을 생성하는 함수로, 호출 시 원본 프로세스와 복사된 프로세스 모두에서 fork() 함수 호출이 반환된다. 원본 프로세스를 부모 프로세스(parent process)라 하고, 생성된 복사본을 자식 프로세스(child process)라 한다.
fork() 함수의 동작 순서:
1.
부모 프로세스가 fork() 함수를 호출한다.
2.
새로운 메모리 공간을 할당받은 자식 프로세스가 생성되며, 부모 프로세스의 메모리 내용이 자식 프로세스로 복사한다.
하지만, 실제로 부모 프로세스의 데이터를 자식 프로세스로 복사하는 작업은 카피 온 라이트(Copy-on-Write) 기술로 인해 효율적으로 처리된다. 이 기술 덕분에 동일한 프로그램을 여러 프로세스로 나누어 처리할 때 발생하는 오버헤드는 최소화된다.
3.
부모와 자식 프로세스 동일한 코드를 실행하지만, 반환값에 따라 프로세스를 구분한다.
부모 프로세스는 자식 프로세스의 ID(PID)가 반환한다.
자식 프로세스는 0을 반환한다.

프로세스 생성 시 자원

자원 할당:
자식 프로세스는 운영체제로부터 직접 자원을 받거나 부모 프로세스의 자원을 공유받을 수 있다.
초기화 데이터:
부모 프로세스는 자식 프로세스에 초기 데이터를 전달할 수 있다. (예: 파일 이름, 입출력 장치)
일부 운영체제에서는 부모가 자식에 열린 파일이나 환경 블록을 전달한다.

실습: fork()함수 사용

다음은 fork() 함수를 활용하여 부모 프로세스와 자식 프로세스의 동작을 확인하는 Python 코드 예제이다.
fork.py
#!/usr/bin/python3 import os, sys ret = os.fork() if ret == 0: print("자식 프로세스: pid={}, 부모 프로세스의 pid={}".format(os.getpid(), os.getppid())) exit() elif ret > 0: print("부모 프로세스: pid={}, 자식 프로세스의 pid={}".format(os.getpid(), ret)) exit() sys.exit(1)
Bash
복사
os.fork()는 프로세스를 분기한다.
반환값을 통해 부모 프로세스와 자식 프로세스를 구분하여 각기 다른 처리를 실행한다.
PID가 75272인 부모 프로세스가 새로운 자식 프로세스(PID: 75273)를 생성한다. fork() 호출 이후, 부모 프로세스와 자식 프로세스가 각각 실행되며, 반환값에 따라 분기 처리된다.

다른 프로그램을 기동하는 exec()함수

execve() 함수는 기존의 자식 프로세스를 완전히 새로운 프로그램으로 치환하는 역할을 한다. fork() 함수로 생성된 프로세스는 복사본을 만드는 데 그치지만, exec() 함수는 해당 프로세스의 메모리와 실행 상태를 새 프로그램으로 덮어쓴다.
execve() 함수의 작동 과정:
1.
자식 프로세스가 exec() 함수 호출을 시작한다.
2.
지정된 실행 파일에서 프로그램을 읽어온다.
3.
현재 프로세스의 메모리 공간을 새로운 프로그램 데이터로 덮어쓴다. (메모리 치환)
4.
새 프로그램의 시작 지점(entry point)에서 실행을 시작한다. (명령 실행 시작)
자식 프로세스는 기존 프로세스의 상태를 완전히 잃고 새로운 프로그램을 메모리에 로드한다. 이 호출로 기존 메모리 이미지를 덮여쓰며, 오류가 발생하지 않는 한 exec()는 제어를 반환하지 않는다.

실습: fork()와 exec() 함께 사용하기

아래는 fork()execve()를 조합하여 새로운 프로그램을 실행하는 Python 예제이다.
fork-and-exec.py
#!/usr/bin/python3 import os, sys ret = os.fork() # os.fork()로 부모와 자식 프로세스를 생성 if ret == 0: # 자식 프로세스는 execve()를 호출하여 새로운 프로그램 /bin/echo를 실행 print("자식 프로세스: pid={}, 부모 프로세스 pid={}".format(os.getpid(), os.getppid())) os.execve("/bin/echo", ["echo", "pid={}에서 안녕".format(os.getpid())], {}) exit() elif ret > 0: # 부모 프로세스는 자식 프로세스의 PID를 출력하고 종료 print("부모 프로세스: pid={}, 자식 프로세스 pid={}".format(os.getpid(), ret)) exit() sys.exit(1) # fork 실패 시 종료
Python
복사
부모 프로세스(PID: 92386)는 자식 프로세스(PID: 92387)를 생성한 후 종료한다.
자식 프로세스는 exec()를 호출하여 /bin/echo 프로그램으로 변환되며, 메시지를 출력하고 종료한다.

ELF(Executable and Linking Format)

execve() 함수가 새로운 프로그램을 실행하려면, 실행 파일은 다음과 같은 데이터를 포함해야 한다.
코드 영역 정보: 파일 내 코드 섹션의 오프셋, 크기, 메모리 맵 시작 주소
데이터 영역 정보: 파일 내 데이터 섹션의 오프셋, 메모리 맵 시작 주소
엔트리 포인트: 프로그램 실행을 시작할 메모리
리눅스 실행 파일은 보통 ELF(Executable and Linking Format) 포맷을 사용하며, 위 정보를 담고 있다. 이를 확인하려면 readelf 명령어를 사용한다.

ELF 헤더 확인 readelf -h

1장에서 사용한 pause 프로그램을 아래 명령어를 사용해서 빌드한다. ASLR 기능을 비활성화 하려면 -no-pie 옵션을 추가한다.
# cc -o pause -no-pie pause.c
Bash
복사
readelf -h 명령어를 사용하여 ELF 헤더 정보를 확인한다.
# readelf -h pause
Bash
복사
0x400490 값이 프로그램의 엔트리 포인트(프로그램 실행이 시작되는 메모리 주소)이다.

ELF 섹션 정보 확인 readelf -S

readelf -S 명령어를 실행하여 실행 파일의 섹션 정보를 확인한다.
readelf -S pause
Bash
복사
root@1bfb5d7bf12d:/app/01-# readelf -S pause There are 29 section headers, starting at offset 0x1cd8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align ... [13] .text PROGBITS 0000000000400490 00000490 0000000000000194 0000000000000000 AX 0 0 8 [23] .data PROGBITS 0000000000411020 00001020 0000000000000010 0000000000000000 WA 0 0 8 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), p (processor specific)
Bash
복사
.text: 코드 섹션 - 메모리 주소 0x400490, 파일 오프셋 0x490, 크기 0x194.
.data: 데이터 섹션 - 메모리 주소 0x411020, 파일 오프셋 0x1020, 크기 0x10.

메모리 맵 확인 /proc/<pid>/maps

실행 중인 프로그램의 메모리 맵은 /proc/<pid>/maps 파일에서 확인할 수 있다.
pause 프로그램을 백그라운드에서 실행한다.
# ./pause & [2] 5547
Bash
복사
/proc/<pid>/maps 파일을 열어 메모리 맵을 확인한다.
# cat /proc/5547/maps
Bash
복사
코드 영역:
ELF의 .text 섹션에 해당하며, 메모리 맵의 r-xp 속성(읽기/실행 가능, 쓰기 불가)을 가진다.
00400000-00401000 코드 영역
데이터 영역:
ELF의 .data 섹션에 해당하며, 메모리 맵의 rw-p 속성(읽기/쓰기 가능, 실행 불가)을 가진다.
00411000-00412000: 데이터 영역

프로그램 종료

실습이 끝난 후 실행 중인 pause 프로세스를 종료한다.
# kill <PID>
Shell
복사
실행 파일은 코드와 데이터 외에도 프로그램 실행에 필요한 메타데이터를 포함한다.
execve() 함수는 ELF 포맷에 정의된 정보를 바탕으로 새로운 프로그램을 실행한다.
readelf/proc/<pid>/maps를 사용하여 실행 파일의 구조와 메모리 맵을 분석할 수 있다.

마무리

리눅스의 fork()exec() 함수는 효율적이고 유연한 프로세스 관리를 제공한다.
fork()는 동일 프로세스 복사본을 생성하여 병렬 처리를 가능하게 하고,
exec()는 새로운 프로그램을 실행하여 기능 확장을 지원한다.
구분
fork()
exec()
기능
부모 프로세스의 복사본(자식 프로세스) 생성
새로운 프로그램으로 프로세스 치환
목적
동일 프로그램의 병렬 처리
다른 프로그램 실행
반환값
부모: 자식 프로세스 ID 자식: 0
없음 (새로운 프로그램으로 대체)
동작 방식
메모리 복사(Copy-on-Write) 사용
기존 메모리를 새로운 프로그램 데이터로 덮어쓰기
사용 사례
다중 요청 처리 (예: 웹서버)
새로운 프로그램 실행 (예: Bash에서 명령 실행)
또한 ASLR(Address Space Layout Randomization)과 PIE(Position Independent Executable) 같은 보안 기능은 실행 파일의 안전성을 강화한다.

Q&A

fork()와 execve()의 차이점은?
fork() 호출 시 반환값의 의미는?
execve() 호출 후 부모 프로세스는 어떻게 되는가?
오프셋(Offset), 메모리 맵(Memory Map)이란?
PIE와 ASLR의 역할은?
readelf 명령어로 무엇을 확인할 수 있는가?
/proc/<pid>/maps에서 확인 가능한 정보는?
no-pie 옵션의 역할은?
ASLR이 비활성화된 환경에서의 장단점은?
Search
Main PageCategoryTagskkogggokkAbout MeContact