📌 매뉴얼 (Linux)
NAME
fork - create a child process
: 하위 프로세스 생성하기
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
DESCRIPTION
fork() creates a new process by duplicating the calling process. The new process is referred
to as the child process. The calling process is referred to as the parent process.
The child process and the parent process run in separate memory spaces. At the time of fork()
both memory spaces have the same content. Memory writes, file mappings (mmap(2)), and unmap‐
pings (munmap(2)) performed by one of the processes do not affect the other.
The child process is an exact duplicate of the parent process except for the following points:
* The child has its own unique process ID, and this PID does not match the ID of any existing
process group (setpgid(2)) or session.
* The child's parent process ID is the same as the parent's process ID.
* The child does not inherit its parent's memory locks (mlock(2), mlockall(2)).
* Process resource utilizations (getrusage(2)) and CPU time counters (times(2)) are reset to
zero in the child.
* The child's set of pending signals is initially empty (sigpending(2)).
* The child does not inherit semaphore adjustments from its parent (semop(2)).
* The child does not inherit process-associated record locks from its parent (fcntl(2)). (On
the other hand, it does inherit fcntl(2) open file description locks and flock(2) locks
from its parent.)
* The child does not inherit timers from its parent (setitimer(2), alarm(2), timer_cre‐
ate(2)).
* The child does not inherit outstanding asynchronous I/O operations from its parent
(aio_read(3), aio_write(3)), nor does it inherit any asynchronous I/O contexts from its
parent (see io_setup(2)).
The process attributes in the preceding list are all specified in POSIX.1. The parent and
child also differ with respect to the following Linux-specific process attributes:
* The child does not inherit directory change notifications (dnotify) from its parent (see
the description of F_NOTIFY in fcntl(2)).
* The prctl(2) PR_SET_PDEATHSIG setting is reset so that the child does not receive a signal
when its parent terminates.
* The default timer slack value is set to the parent's current timer slack value. See the
description of PR_SET_TIMERSLACK in prctl(2).
* Memory mappings that have been marked with the madvise(2) MADV_DONTFORK flag are not inher‐
ited across a fork().
* Memory in address ranges that have been marked with the madvise(2) MADV_WIPEONFORK flag is
zeroed in the child after a fork(). (The MADV_WIPEONFORK setting remains in place for
those address ranges in the child.)
* The termination signal of the child is always SIGCHLD (see clone(2)).
* The port access permission bits set by ioperm(2) are not inherited by the child; the child
must turn on any bits that it requires using ioperm(2).
Note the following further points:
* The child process is created with a single thread—the one that called fork(). The entire
virtual address space of the parent is replicated in the child, including the states of mu‐
texes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be
helpful for dealing with problems that this can cause.
* After a fork() in a multithreaded program, the child can safely call only async-signal-safe
functions (see signal-safety(7)) until such time as it calls execve(2).
* The child inherits copies of the parent's set of open file descriptors. Each file descrip‐
tor in the child refers to the same open file description (see open(2)) as the correspond‐
ing file descriptor in the parent. This means that the two file descriptors share open
file status flags, file offset, and signal-driven I/O attributes (see the description of
F_SETOWN and F_SETSIG in fcntl(2)).
* The child inherits copies of the parent's set of open message queue descriptors (see
mq_overview(7)). Each file descriptor in the child refers to the same open message queue
description as the corresponding file descriptor in the parent. This means that the two
file descriptors share the same flags (mq_flags).
* The child inherits copies of the parent's set of open directory streams (see opendir(3)).
POSIX.1 says that the corresponding directory streams in the parent and child may share the
directory stream positioning; on Linux/glibc they do not.
: fork()는 호출 프로세스를 복제하여 새 프로세스를 생성합니다.
새 프로세스를 자식 프로세스(child process)라고 하며
호출 프로세스를 부모 프로세스(parent process)라고 합니다.
자식 프로세스와 부모 프로세스는 별도의 메모리 공간에서 실행됩니다.
fork() 시점에서 두 메모리 공간은 모두 동일한 내용을 갖습니다.
프로세스 중 하나에 의해 수행되는 메모리 쓰기, 파일 매핑(mmap(2)) 및 매핑 해제(munmap(2))는
다른 프로세스에 영향을 미치지 않습니다.
자식 프로세스는 다음 사항을 제외하고 부모 프로세스를 정확히 복제한 것입니다:
* 자식에는 고유한 프로세스 ID가 있으며 이 PID는 기존 프로세스 그룹(setpgid(2)) 또는 세션의 ID와 일치하지 않습니다.
* 자녀의 부모 프로세스 ID는 부모 프로세스 ID와 동일합니다.
* 자식은 부모의 메모리 잠금(mlock(2), mlockall(2))을 상속받지 않습니다.
* 프로세스 리소스 사용률(getrassage(2)) 및 CPU 시간 카운터(time(2))는 하위 항목에서 0으로 재설정됩니다.
* 자식의 보류 신호 세트가 처음에는 비어 있습니다(sigpending(2)).
* 자식은 부모로부터 세마포어 조정을 상속받지 않습니다(semop(2)).
* 자식은 부모로부터 프로세스 관련 레코드 잠금을 상속받지 않습니다(fcntl(2)).
(반면 부모로부터 fcntl(2) 열린 파일 설명 잠금과 flock(2) 잠금을 상속받습니다.)
* 자식은 부모로부터 타이머를 상속받지 않습니다(setitimer(2), alarm(2), timer_create(2)).
* 자식은 상위(aio_read(3), aio_write(3))에서 미결 비동기 I/O 작업을 상속하지 않으며,
상위에서 비동기 I/O 컨텍스트를 상속하지도 않습니다(io_setup(2) 참조).
이전 목록의 프로세스 특성은 모두 POSIX.1에 지정되어 있습니다.
부모와 자식은 또한 다음과 같은 리눅스 고유의 프로세스 특성과 관련하여 다릅니다:
* 자식이 부모로부터 디렉토리 변경 알림(dnotify)을 상속받지 않습니다(fcntl(2)의 F_NOTIFY 설명 참조).
* prctl(2) PR_SET_PDESSIG 설정은 부모가 종료될 때 자식이 신호를 받지 않도록 재설정됩니다.
* 기본 타이머 슬랙 값은 부모의 현재 타이머 슬랙 값으로 설정됩니다.
prctl(2)의 PR_SET_TIMERSLACK에 대한 설명을 참조하십시오.
* madvise(2) MADV_DONTFORK 플래그로 표시된 메모리 매핑은 포크()에서 상속되지 않습니다.
* madvise(2) MADV_WIPEONFORK 플래그로 표시된 주소 범위의 메모리는 포크() 이후 자식에서 영점으로 표시됩니다.(MADV_WIPEONFORK 설정은 자식의 해당 주소 범위에 대해 그대로 유지됩니다.)
* 자식의 종료 신호는 항상 SIGCHLD입니다(clone(2) 참조).
* ioperm(2)에서 설정한 포트 액세스 권한 비트는 자식에게 상속되지 않으며,
자식은 ioperm(2)을 사용하여 필요한 모든 비트를 켜야 합니다.
다음 사항에 주의하십시오:
* 자식 프로세스는 fork()라는 단일 스레드를 사용하여 생성됩니다. 부모의 전체 가상 주소 공간은 음소거 상태,
조건 변수 및 기타 pthread 개체를 포함하여 자식에서 복제됩니다. pthread_atfork(3)을 사용하면
이러한 문제를 해결하는 데 도움이 될 수 있습니다.
* 멀티 스레드 프로그램에서 포크()를 사용한 후, 아이는 execve(2)를 호출할 때까지
비동기 신호 안전 기능(signal-safety(7))만 안전하게 호출할 수 있습니다.
* 자식은 부모의 오픈 파일 디스크립터 집합의 복사본을 상속합니다. 자식의 각 파일 디스크립터는
부모의 해당 파일 디스크립터와 동일한 오픈 파일 디스크립터(open(2))를 참조합니다.
이는 두 파일 디스크립터가 오픈 파일 상태 플래그, 파일 오프셋 및 신호 기반 I/O 속성을
공유한다는 것을 의미합니다(fcntl(2)의 F_SETOWN 및 F_SETSIG 설명 참조).
* 자식은 부모의 열린 메시지 큐 디스크립터 집합의 복사본을 상속합니다(mq_overview(7)).
자식의 각 파일 디스크립터는 부모의 해당 파일 디스크립터와 동일한 열린 메시지 큐 디스크립터를 나타냅니다.
이는 두 파일 디스크립터가 동일한 플래그(mq_flags)를 공유한다는 것을 의미합니다.
* 자식은 부모의 열린 디렉터리 스트림 집합의 복사본을 상속합니다(opendir(3) 참조).
POSIX.1은 부모 및 자식의 해당 디렉토리 스트림이 디렉토리 스트림 포지셔닝을 공유할 수 있지만
Linux/glibc에서는 공유하지 않는다고 말합니다.
RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in the
child. On failure, -1 is returned in the parent, no child process is created, and errno is
set appropriately.
: 성공 시 자식 프로세스의 PID는 부모에서 반환되고 0은 자식에서 반환됩니다.
실패 시 -1은 부모에서 반환되고 자식 프로세스는 생성되지 않으며 errno는 적절하게 설정됩니다.
📌 함수 설명
fork 함수는 함수를 호출한 프로세스를 복사하는 기능을 가지며,
원래 진행되던 프로세스를 '부모 프로세스' (parent), 복사된 프로세스를 '자식 프로세스' (child) 라 한다.
(프로세스에 대한 추가 내용 : 2024.06.02 - [프로그래밍/C] - [study] 프로세스 (Process))
프로세스의 id를 pid라 하며, fork 함수가 반환하는 값이 이 pid이다.
부모 프로세스는 자식 pid를 반환하고, 자식 프로세스는 0 반환, 함수 실행 실패 시 -1 반환이다.
그림을 통해 보면, fork는 기존 Parent Process를 Parent Process의 진행과 함께
해당 프로세스를 복사한 Child Process를 만들어주는 함수로 쓰인다.
코드를 통해 확인해보면 더욱 쉽게 알 수 있다.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
fork();
printf("hello\n");
return 0;
}
자, fork()를 한번 실행한다고 했을 때, 진행상황을 살펴보도록 하겠다.
이 프로그램이 시작하여 프로세스가 만들어지면, main문이 실행이 된다.
그러하여, fork()를 처음 만나게 되는데, 이 상황에서 기존 프로세스는 부모 프로세스가 되어, 자식 프로세스를 복사한다.
그럼 fork() 이후에 실행될 코드들을 가진 두 개의 프로세스가 생기게 된 것이고, printf()가 각각 한번씩 실행 되는 것이다.
코드의 진행을 그림으로 보면 이런형태를 띌 수 있다.
fork 이후 부터의 코드가 진행되는 프로세스가 2가지가 생겨난 것이다.
(하나는 parent process의 기존 진행, 하나는 복사된 Child Process의 진행)
결과 - std_output)
hello hello |
이렇게 두 프로세스가 실행되어 결과가 2개의 hello를 출력하는 상황이 나온다.
이 두 프로세스는 pid로 구분이 가능하고 (부모는 자식의 pid, 자식은 0) pid의 값을 저장하여 fork한다면,
그 값에 따라 구분하여 원하는 실행 결과를 얻을 수도 있다. 다음 코드가 그러한 상황을 보여준다.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return (0);
}
else if (pid == 0) {
printf("Hello from the child process!\n");
return (0);
}
else {
printf("Hello from the parent process!\n");
return (0);
}
return (0);
}
결과 - std_output)
Hello from the parent process! Hello from the child process! |
pid의 값이 0이 된 상황에서는 child 프로세스이므로, child의 결과값이 나오도록 했고,
pid의 값이 0이나 -1이 아니면, child 프로세스의 pid를 가지고 있는 parent 프로세스이므로 parent의 결과값이 나오도록 했다.
이 실행 순서에 관해서는 '스케줄링'에 따라 OS가 판별하여 둘 중 하나든 먼저 실행될 수 있다.
그렇다면 종료 순서는? 마찬가지로 '스케줄링'에 따르겠지만 일반적으로는 자식 프로세스가 종료될 때까지 부모가 기다린다.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("Line 0\n");
fork();
printf("Line 1\n");
fork();
printf("Line 2\n");
fork();
printf("Line 3\n");
return 0;
}
더 쉬운 이해를 위해, 이번에는 fork를 무려 3번 쓴 코드가 나왔다.
그리고 중간중간 printf가 존재하는데 어떻게 동작할지 예측해보자.
그림으로 보면, printf("Line 0")은 첫 parent process에서 한번 실행되고 그 이후에 fork를 하면
하위의 코드들로 이뤄진 process가 둘이 생기고, 그럼 printf("Line 1") 은 총 두 번 실행이 된다. 그 이후 같은 과정으로
fork 되고 printf("Line 2")가 있는 4개의 프로세스가 생기고, 그렇게 꼬리에 꼬리를 물어, 1 + 2 + 4 + 8, 15개의 줄이 생길 것이다.
결과 - std_output)
언급했던 대로, 스케줄링에 의해 실행 때마다 프로세스 실행 순서가 바뀐 상황이다.
하지만 당연하게도, Line 2가 한번도 나오지 않았는데 Line 3이 나오는 경우는 발생하지 않는다.
이와 같이 결과로 나올 수 없는 불가능한 경우는 'Infeasible output' 이라 하고,
사진과 같이 정상적으로 나올 수 있는 가능한 경우는 'Feasible output' 이라 한다.
이 fork에 대해 추가적인 설명을 덧붙이도록 하겠다.
[ fork의 용도 ]
1. 같은 프로그램에서 새로운 제어 흐름을 만드는 용도
2. 다른 프로그램을 실행하는 새 프로세스를 만드는 용도
대개 2번의 용도로 사용하는데, 쉘에서 명령어를 통해 프로세스를 실행하는 경우이다.
그리고, 자식 프로세스가 생성되면 부모 프로세스의 Stack, Heap, Data 를 복사하고,
부모 프로세스의 PCB(Process Control Block)도 복사한다. Text 영역은 복사대신 공유한다.
이 PCB에는 CPU에서 수행되던 레지스터 값들과 Program Counter 정보가 존재하므로
fork 이후의 실행상태를 그대로 복사하여 실행된다.
그리고, COW(Copy On Write) 정책 이라는 '쓰기 작업 시 복사'가 일어나는 최적화 기술을 사용하는데,
말 그대로 이 Resource를 수정할 때만 실제로 Copy를 하고,
수정하지 않고 사용할 때는 그냥 공유해서 쓰는 것을 의미한다.
그리하여 복사한 자식 프로세스의 메모리 가상 주소가 동일하며,
내용이 변경될 시 새로운 물리 메모리 주소를 가리키는 상황이 생긴다.
결국, 모든 데이터를 새롭게 복사하는 것이 아니기 때문에 복제 비용이 거의 들지 않고, 관리가 용이하다.
프로세스를 생성하는 것보다 fork를 통해 복사하는 것에 대한 의의가 바로 이런 것이다.
참고)
https://velog.io/@jungbumwoo/fork-%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
'프로그래밍 > C' 카테고리의 다른 글
[function] unlink 함수 알아보기 (2) | 2024.06.07 |
---|---|
[function] pipe 함수 알아보기 (0) | 2024.06.06 |
[function] exit 함수 알아보기 (0) | 2024.05.31 |
[function] execve 함수 알아보기 (0) | 2024.05.30 |
[function] dup2 함수 알아보기 (2) | 2024.05.28 |