지난주 B조에서 리눅스 커널 내부구조라는 책으로 스터디를 하다가 53페이지부터 나오는 fork()함수에 대한 질문이 있었습니다. 
"fork() 함수를 사용한 결과값에 따라 분기가 나눠지는데 어떻게 부모와 자식 모두의 분기가 2번 수행되는가?"

 

저는 개념적으로 fork()함수는 내부적으로 return을 두번하게 구현되어 있다고만 답변을 하였고 세부적인 정보를 제공할 수 없었습니다. 그래서 아래와 같이 교육용 코드를 가져다가 부연 설명을 하기로 하였습니다. 우리는 리눅스 커널을 연구하고 있지만 fork()함수 개념을 설명하기에는 교육용 소스코드가 편리하기 때문에 MIT OS 수업에 사용하는 XV6(교육용 Unix) 소스코드를 활용해 설명합니다. 제가 개인적으로 소스코드를 해석한 것이기 때문에 다른 의견이나 해석은 댓글을 통해서 의견을 달아주시기 바랍니다. 

 

소스코드 링크 : https://pdos.csail.mit.edu/6.828/2014/xv6/xv6-rev7.pdf
OS과목 홈페이지 링크 : https://pdos.csail.mit.edu/6.828/2014/schedule.html

 

<proc.c 내부의 fork()함수 정의>

2303 int
2304 fork(void)
2305 {
2306     int i, pid;
2307     struct proc *np; //새로운 proc 구조체 선언(리눅스의 task_struct와 같음)
2308
2309     // Allocate process.
2310     if((np = allocproc()) == 0) //새로운 구조체가 할당될 수 있는 배열을 탐색하여 할당함
2311         return −1;
2312
2313     // Copy process state from p.
2314     if((np−>pgdir = copyuvm(proc−>pgdir, proc−>sz)) == 0){  //신규 자식의 proc 구조체에 현재 proc의 내용을 복사
2315         kfree(np−>kstack);
2316         np−>kstack = 0;
2317         np−>state = UNUSED;
2318     return −1;
2319     }
2320     np−>sz = proc−>sz; 
2321     np−>parent = proc;  //신규 np의 부모를 proc으로 설정
2322     *np−>tf = *proc−>tf;  //신규 np의 trapframe(레지스터 등 정보 저장)에 부모의 것을 복사
2323
2324     // Clear %eax so that fork returns 0 in the child.
2325     np−>tf−>eax = 0;  //자식 np의 트랩 프레임의 %eax값을 0으로 지정함
2326                                  //자식 프로세스는 forkret 후 trapret으로 돌아갈 때 리턴값으로 eax 값을 사용하게됨
2327     for(i = 0; i < NOFILE; i++)
2328         if(proc−>ofile[i])
2329             np−>ofile[i] = filedup(proc−>ofile[i]);  //부모의 열려진 파일 목록을 자식에게 복사
2330     np−>cwd = idup(proc−>cwd);  //부모의 현재디렉토리 관련 정보를 자식에게 복사
2331
2332     pid = np−>pid;  //자식의 pid를 부모에게 전달
2333     np−>state = RUNNABLE;  //자식의 프로세스 상태를 RUNNABLE로 지정
2334     safestrcpy(np−>name, proc−>name, sizeof(proc−>name));  //부모 프로세스 이름을 자식에게 복사
2335     return pid;  //리턴함, 이때 부모는 자식 프로세스의 pid를 가지고 리턴함 (fork가 trap에 걸려서 온 것이니 trapret함)
2336 }

 

 

여기에서 2335라인의 return pid를 보고 부모와 자식 모두 "결국 똑같은 값"이 리턴되어야 하는것 아닌가? 라고 생각하실 수 있습니다. 하지만 최초 2310라인에서 allocproc()함수를 사용하여 신규 프로세스 구조를 할당하는 부분에 우리가 원하는 내용이 있습니다. 아래의 소스코드를 통해 allocproc()함수의 내용을 확인하면 child 프로세스 proc 구조체 내부의 커널 스택을 수동으로 설정하여 forkret과 trapret을 거치게 하는 것을 볼 수 있습니다. forkret으로 돌아가는 과정이 forkret() 함수 호출 이후 원래의 위치로 돌아가는 것이 아니라, 인공적으로 마치 forkret 함수가 호출했던 것처럼 스택구조를 설정하는 것 입니다. 또한 trapframe을 수동으로 복사함으로써 아직까지 한번도 실행된 적이 없었던 신규 child 프로세스가 스케줄러에서 swtch될 때 과거의 실행된 부분이 context_switch되는 것처럼 묘사합니다. 

 

 

<proc.c 내부의 allocproc()함수 정의>
2204 static struct proc*
2205 allocproc(void)  //이 함수는 신규 생성되는 child 프로세스에 해당하는 부분임
2206 {
2207     struct proc *p;
2208     char *sp;
2209
2210     acquire(&ptable.lock);  //lock을 걸고
2211     for(p = ptable.proc; p < &ptable.proc[NPROC]; p++) //proc배열에 신규 프로세스를 할당할 공간이 있는지 조사
2212         if(p−>state == UNUSED)  //사용하지 않고 있는 것이 있다면
2213             goto found;  //found로 이동함
2214     release(&ptable.lock);  //lock을 해제
2215     return 0;
2216
2217 found:
2218     p−>state = EMBRYO;  //사용하고 있지 않은 배열공간이 있으면 우선 해당 공간을 EMBRYO 상태로 정의함
2219     p−>pid = nextpid++;  //nextpid (전역적으로 관리되고 있는 번호)를 1증가시킨 후 현재의 pid로 지정함
2220     release(&ptable.lock);  //lock을 해제
2221
2222     // Allocate kernel stack.
2223     if((p−>kstack = kalloc()) == 0){  //신규 proc 구조체에 커널스택 메모리를 할당
2224         p−>state = UNUSED;  //실패하면 proc구조를 다시 원래 UNUSED상태로 변경
2225         return 0;
2226     }
2227     sp = p−>kstack + KSTACKSIZE;  //stack포인터 위치 지정
2228
2229     // Leave room for trap frame.   //여기부터는 return을 위한 스택을 수동으로 넣어주는 부분임
2230     sp −= sizeof *p−>tf;  //trapframe 사이즈만큼 stack포인터 이동
2231     p−>tf = (struct trapframe*)sp;  
2232
2233     // Set up new context to start executing at forkret,
2234     // which returns to trapret.
2235     sp −= 4;
2236     *(uint*)sp = (uint)trapret;  //trapret 주소 넣는다. 아래의 forkret이 수행된 이후 trapret으로 return될 것이다.
2237
2238     sp −= sizeof *p−>context;
2239     p−>context = (struct context*)sp;
2240     memset(p−>context, 0, sizeof *p−>context);
2241     p−>context−>eip = (uint)forkret;  //신규 proc context의 eip에 forkret 주소를 넣음
2242                                                          //이렇게 되면 child는 fork함수 종료 후 forkret => trapret으로 이동함
2243     return p;
2244 }

 


결론을 다시 정리하면 아래와 같습니다.

1. fork에서 부모 프로세스 메모리와 proc구조체(리눅스의 task_struct)를 자식에게 복사함
  - 그리고 부모는 자식 프로세스는 pid를 가지고 리턴 => 이후 원래 호출된 부분으로 돌아가 wait()
2. 자식 프로세스를 위한 proc구조체를 만드는 과정에서 커널스택에 수동으로 trapframe 내용, trapret 주소, forkret 주소를 넣음
   (또한 자식 프로세스의 trapframe 내부의 eax값을 0으로 지정)

  - 자식 프로세스도 proc 구조체가 runnable 상태이기 때문에 이제는 스케줄러에 의해 실행될 수 있는 상태가 되었음

  - 부모가 wait() 상태가 되어 스케줄러에서 자식 프로세스가 실행됨
  - 자식은 forkret과 trapret을 거쳐서 원래 함수를 호출한 부분으로 복귀. 이때 eax값이 0이어서 리턴값은 0이 됨. 

 

 

번호 제목 글쓴이 날짜 조회 수
공지 [공지] 커널 스터디 관련 Q&A 게시판 입니다. [5] woos 2016.04.09 2196
» fork() 함수가 리턴을 두번하는 이유 설명 [2] 커널B조 2016.05.07 30229
31 그림입니다. [3] pororo 2013.06.30 2786
30 The 3.11 kernel is out !!! [4] 아폴로 2013.09.03 2513
29 안정버전 패치 branch는 어떻게 가져오는거죠? [2] 아폴로 2013.07.26 2340
28 [커널 15차 B팀] 2주차 결과 TUN 2018.05.06 875
27 커널 스터디 B조 의견접수 글입니다. [43] JIHOONS 2016.04.23 775
26 커널 B조 4월 30일 오프라인 참석자수 조사 [35] 황금돌고래 2016.04.25 545
25 [커널 16차 B팀] 1주차 장소 공지 및 참석인원 조사 [35] 승현 2019.05.22 537
24 운영체제 기초 강의 입니다. [2] file Lolki 2016.05.01 477
23 [커널 16차 B팀] B팀 공지사항 입니다. [5] 승현 2019.05.18 443
22 [커널 15차 B팀]4주차(2018-05-19) 스터디 결과 및 5주차(2018-05-26) 장소 공지 [8] HeyJin 2018.05.20 381
21 [커널 15차 B팀] 1주차 결과 [11] TUN 2018.04.29 377
20 [커널 15차 B팀] 스터디 장소 관련 [10] dudu 2018.04.26 371
19 커널 B조 실시간 의견 공유를 위해 오픈채팅 개설합니다. [6] psionic 2016.04.25 338
18 [커널 15차 B팀] 3주차(2018-0512) 결과 및 장소 공지 [7] TUN 2018.05.13 305
17 [커널 16차 B팀] 2주차 장소 공지 및 참석인원 조사 [16] 승현 2019.05.29 262
16 [커널16차 B조] 12주차(2019/08/24) 참석인원 조사 [5] 승현 2019.08.19 189
15 [커널16차 B조] 23주차(2019/11/16) 참석인원 조사 mcsmonk 2019.11.10 167
14 [커널16차 B조] 13주차(2019/09/07) 참석인원 조사 [6] 승현 2019.09.04 155
13 [커널16차 B조] 3주차 장소 공지 및 참석인원 조사 [11] 승현 2019.06.06 149
XE Login