OS공부를 하다가...

georgek의 이미지

안녕하세요. 프로그래밍 질문은 아닌것같아서 자유게시판에 글을 올리게되었습니다.ㅜ

OS공부를 하다가 궁금한게 생겼습니다.

pid_t pid;
pid = fork();
if (pid == 0) { /* child process */
fork();
thread_create( . . .);
}
fork();

이런 코드에서 process는 총 6개 생성되고, thread는 8개 만들어지는거 맞는지 궁금합니다.

제가 생각한 것인데 한번 봐주실 수 있으신가요?

첫번째 fork()에서 부모,자신 process로 나뉘고 그 다음에 자식process에서 fork()가 호출되어서

총 3개의 process가 생기게 된 상태에서 thread_create(~~~)가 호출되어 Thread는 총 4개가 생성된 상태입니다.(여기까지 process 3개, thread4개)

마지막에 괄호 밖에서 호출된 fork()에 의해, process는 총 6개가 되고 thread는 8개가 되는게 맞는지 궁금합니다.
(구글링과 책을 통해 찾아본 결과 fork()다음에 바로 exec()가 호출되지 않는다면 모든 thread도 다 복사된다고해서 이렇게 생각했습니다.)

결과적으로 process는 총 6개 생성되고, thread는 8개 만들어지는거 맞는지 궁금합니다.(제가 생각한 과정도 맞는지 궁금합니다.)

chanik의 이미지

우선 pthread_create() 이후의 상태는 프로세스 3개, 스레드 4개가 아니고,
프로세스 갯수로는 3개, 스레드 갯수로는 5개가 되죠?

그리고, 리눅스의 fork()는 해당 프로세스의 모든 스레드를 복제하는 것이 아니고
fork()를 호출한 스레드 하나만 복제합니다.

       *  The  child  process  is  created  with  a single thread—the one that
          called fork().

따라서 마지막 fork() 이후에는 스레드 1개짜리 프로세스 3개가 추가되므로
프로세스 갯수로는 6개, 스레드 갯수로는 8개인 상태가 될 것입니다.
chanik의 이미지

찾아보니 오래전에 KLDP에서 다뤄진 주제였군요.
아래 글에 내용이 풍성합니다.

다중쓰레드가 돌고있는 프로세스를 포크시키면 어떻게 될까..

georgek의 이미지

감사합니다. 앞으로는 좀 더 검색하고!! 궁금한게 있으면 올려야 겠네요.
정말 감사합니다!
****근데 쓰레드가 왜 5개 되는지는 아직 이해가 잘 안되네요 ㅜㅜ
그리고 pthread_create 와 thread_create는 같은건가요?

chanik의 이미지

아래와 같이 진행되기 때문입니다.

  1. 첫번째 fork() 직전 : 스레드 1개짜리 프로세스 1개. (프로세스 1, 스레드 1)
  2. 첫번째 fork() 직후 : 스레드 1개짜리 프로세스 2개. (프로세스 2, 스레드 2)
  3. 두번째 fork() 직후 : 스레드 1개짜리 프로세스 3개. (프로세스 3, 스레드 3)
  4. thread_create() 후 : 스레드 1개짜리 프로세스 1개. 스레드 2개짜리 프로세스 2개. (프로세스 3, 스레드 5)
  5. 세번째 fork() 직후 : 스레드 1개짜리 프로세스 4개. 스레드 2개짜리 프로세스 2개. (프로세스 6, 스레드 8)

올려주신 코드조각에 약간의 살을 붙여 샘플을 만들어봤습니다.

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
 
void *t_function(void *data)
{
    printf("Thread Start\n");
    sleep(100);
    printf("Thread end\n");
}
 
int main()
{
    pthread_t p_thread;
 
    pid_t pid;
    pid = fork();
    if (pid == 0) { /* child process */
        fork();
        pthread_create(&p_thread, NULL, t_function, (void *)NULL);
    }
    fork();
 
    printf("forked\n");
 
    sleep(100);
    return 0;
}

위의 코드를 빌드해서 실행하고 프로세스/스레드 현황을 보시면 확인 가능할 것입니다.
PID(프로세스 아이디)는 6개, LWP(스레드 아이디)는 8개의 unique 아이디가 보이죠.

$ gcc -o sample sample.c -lpthread
$ ./sample &
$ ps -LF | grep [s]ample
UID        PID  PPID   LWP  C NLWP    SZ   RSS PSR STIME TTY          TIME CMD
test01    2328  2247  2328  0    1  1458   428   1 10:29 pts/0    00:00:00 ./sample
test01    2329  2328  2329  0    2  4052   396   0 10:29 pts/0    00:00:00 ./sample
test01    2329  2328  2332  0    2  4052   396   1 10:29 pts/0    00:00:00 ./sample
test01    2330  2329  2330  0    2  4052   396   0 10:29 pts/0    00:00:00 ./sample
test01    2330  2329  2331  0    2  4052   396   1 10:29 pts/0    00:00:00 ./sample
test01    2333  2328  2333  0    1  1458   228   1 10:29 pts/0    00:00:00 ./sample
test01    2334  2329  2334  0    1  4052   168   0 10:29 pts/0    00:00:00 ./sample
test01    2335  2330  2335  0    1  4052   168   0 10:29 pts/0    00:00:00 ./sample

그리고, thread_create()는 어떤 라이브러리에서 제공되는 함수인지 알 수 없어서
흔히 쓰이는 pthread_create()를 대신 쓴 것입니다.

만약 커널에서 인지되는 커널스레드가 아닌, 유저스레드를 구현한 라이브러리가 있고
thread_create()가 그 라이브러리에서 제공되는 것이라면
멀티스레드 프로세스에서 fork() 실행할때 어떤 일이 벌어질 지는
그 라이브러리의 문서를 참조하셔야 할 것 같고요.

georgek의 이미지

예제 정말 감사합니다.
왜 5개가 됐는지 이해가 됐습니다. 그런데 최종적으로 왜 8개가 되는지는 제가 생각했던것이 잘못되었군요..

그런데, 마지막에 fork()가 실행되면 모든 프로세스가 duplicate되면서 최종적으로 쓰레드가 10개가 되어야 하는거 아닌가요?.
(쓰레드 1개 가진 프로세스 2개 , 쓰레드 2개 가진 프로세스 4개해서 총 10개의 쓰레드....)

제가 책에서 본 부분은 이 부분입니다.
"
If exec() is called immediately after forking, then duplicating all threads is
unnecessary, as the program specified in the parameters to exec() will replace
the process. In this instance, duplicating only the calling thread is appropriate.
If, however, the separate process does not call exec() after forking, the separate
process should duplicate all threads."

그리고 궁금한게 있습니다. fork()를 통해 thread도 복사가 되는데 복사가되도 각각의 thread는 자신만의 id를 갖고있는데 그렇다면 8개의 thread는 unique thread로 취급 되는건가요?

정말정말 감사합니다. 아직 부족한게 많네요 ㅜㅜ

jick의 이미지

어떤 책의 어떤 문맥에서 나온 이야기인지 잘 모르겠습니다만 fork는 쓰레드를 복사해주지 않습니다.
그러니까 마지막에 the separate process *should* duplicate all threads라는 얘기는 프로세스가 *알아서* 쓰레드를 복제해야 한다는 이야기이지, exec를 안 부르면 fork가 그렇게 해준다는 얘기가 아닙니다.

* 상식적으로 fork가 점쟁이도 아니고 자기를 부르는 프로세스가 좀 있다 exec을 부를지 말지 어떻게 알겠습니까.

georgek의 이미지

위에 예제와 설명에 미루어 보아 맞는 말씀이시네요. 감사합니다.

그럼 결과적으로 duplicate된게 아니어서 전체적으로 unique thread가 8개 만들어진 것이군요. 감사합니다.!!

chanik의 이미지

fork() 실행시 콜러 스레드 하나만 복제해줄지 아니면 해당 프로세스의 스레드 모두를 복제해줄지는 선택의 문제인 것 같습니다.

우선 리눅스의 fork()는 앞서 답변드린대로 콜러 스레드 하나만을 복제해 주도록 구현되어 있습니다. 매뉴얼에도 그렇게 나와있고 샘플로 테스트해봐도 그런 결과가 나왔습니다.

하지만 솔라리스에서는 좀 달랐던 모양입니다. 매뉴얼을 보시면, fork(), fork1(), forkall() 등 세 가지의 fork 계열 시스템콜이 나옵니다. fork()는 POSIX 표준이고 나머지 둘은 솔라리스만의 변종 시스템콜인 것 같은데요,

fork1()은 그 이름처럼 콜러 스레드 하나만을 복제해준답니다. forkall()은 역시 그 이름처럼 프로세스에 속한 모든 스레드를 복제해준다고 하고요. 그런데 fork()는 그 동작방식에 약간의 역사적 변동이 있었습니다. 솔라리스 10 이후부터는 무조건 fork1()과 마찬가지로 동작하지만, 그 전 솔라리스에서는 -lpthread (POSIX Threads)로 링크된 경우에는 forkall()처럼 동작하고 -lthread (Solaris Threads)로 링크된 경우에는 fork1()처럼 동작했다고 합니다.

매뉴얼에는 fork1()의 동작방식이 바로 POSIX에 정의된 fork() 방식(POSIX-specified behavior of fork())이라고도 나옵니다.

리눅스와 솔라리스를 함께 정리해보면, POSIX는 콜러 스레드만 복제하는 방식을 선택했고, 리눅스 및 최신 솔라리스는 모두 POSIX의 방식을 따르고 있다고 해두면 될 것 같습니다.

georgek의 이미지

pid_t pid;
pid = fork();
if (pid == 0) { /* child process */
fork();
thread_create( . . .);
}
fork();

(이루어지는 과정은 이해가 되는데 아래 설명이 맞는것인지 혼란스러워서 다시한번 올려봅니다 ㅜ)
전체적으로 8개의 unique한 프로세스가 생성되고(만약 처음에 1개의 process가 있다고 가정시), 8개의 unique한 쓰레드가 생성되는게 맞나요?
- fork()다음에 exec()가 안나와서 기존 프로세스에 있는 thread가 duplicate 된다고 해도 LWP가 다 다르니 결국 unique한 thread로 취급되는건가요?

제 설명이 맞는지 궁금합니다.

chanik의 이미지

이전 답변의 내용을 반복하게 될 것 같은데요,

스레드는 모두 8개가 맞지만, 프로세스는 6개만 만들어집니다.
(스레드 1개짜리 프로세스 4개, 스레드 2개짜리 프로세스 2개)

그리고, fork()시 콜러스레드 하나만 복제됩니다.
이 부분을 아직 오해하시는 것 같습니다.

fork()는 해당 프로세스로부터 콜러스레드 하나만을 복제하고
exec()는 해당 프로세스의 모든 스레드를 파괴하고 그 공간에 새로운 실행파일을 읽어들입니다.

fork()는 fork()일 뿐이고 exec()는 exec()일 뿐,
fork()이후 exec()가 실행되든 안 되든 fork()는 정해진 행동을 하고
exec()역시 무조건 정해진 행동을 합니다.

참고로, POSIX fork() 문서와 exec()문서를 인용해 둡니다.

http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html

Quote:

A process shall be created with a single thread. If a multi-threaded process calls fork(), the new process shall contain a replica of the calling thread and its entire address space, possibly including the states of mutexes and other resources. Consequently, to avoid errors, the child process may only execute async-signal-safe operations until such time as one of the exec functions is called. Fork handlers may be established by means of the pthread_atfork() function in order to maintain application invariants across fork() calls.

http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html

Quote:

A call to any exec function from a process with more than one thread shall result in all threads being terminated and the new executable image being loaded and executed. No destructor functions or cleanup handlers shall be called.

이전 답변의 내용을 다시 한 번 찬찬히 살펴보시고,
올려드린 샘플을 수정해 가면서 몇 번 실험을 해보시면 도움이 될 것입니다.

georgek의 이미지

프로세스 6개 맞습니다. 제가 잘못 썼습니다.

내용바탕으로 더 확인해 봐야겠네요.
정말 감사합니다.

chanik의 이미지

앞서 댓글에서 올렸던 샘플의 실행결과를 그림으로 표현해봤습니다.
이해에 도움이 되기를 바랍니다. 만들다보니 저도 더 생각하게 되네요.

(마지막 fork()결과의 PID간 관계(forker-forkee 짝짓기?)는 사실과 다를 수 있습니다.
이를테면 P2328의 fork() 결과물은 꼭 P2333은 아니며, P2334일수도 P2335일수도 있는 것이죠)

                fork()                fork()                    pthread_create()  fork()
                ─┬─                ─┬─                    ─────┬──  ─┬─
                  ↓                    ↓                                ↓        ↓
 
┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐
│P2328 ─ T2328├┬─┤P2328 ─ T2328├──┤P2328 ─ T2328├──┤P2328 ─ T2328├┬─┤P2328 ─ T2328│
└───────┘│  └───────┘    └───────┘    └───────┘│  └───────┘
                  │                                                                │  ┌───────┐
                  │                                                                └─┤P2333 ─ T2333│
                  │                                                                    └───────┘
                  │  ┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐
                  └─┤P2329 ─ T2329├┬─┤P2329 ─ T2329├──┤P2329 ┬ T2329├┬─┤P2329 ┬ T2329│
                      └───────┘│  └───────┘    │      └ T2332││  │      └ T2332│
                                        │                        └───────┘│  └───────┘
                                        │                                          │  ┌───────┐
                                        │                                          └─┤P2334 ─ T2334│
                                        │                                              └───────┘
                                        │  ┌───────┐    ┌───────┐    ┌───────┐
                                        └─┤P2330 ─ T2330├──┤P2330 ┬ T2330├┬─┤P2330 ┬ T2330│
                                            └───────┘    │      └ T2331││  │      └ T2331│
                                                                  └───────┘│  └───────┘
                                                                                    │  ┌───────┐
                                                                                    └─┤P2335 ─ T2335│
                                                                                        └───────┘