지난주 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 2191
1745 setup.c 파일의 cacheid_init 함수 [1] file HyunGyu 2013.11.05 72244
1744 Vol.1의 CMPS ~ CVTPD2PS 입니다. 늦어서 죄송합니다. file 지현구 2007.03.10 64189
1743 as86(1) - Linux man page 입니다. 김민석 2010.04.30 36679
1742 lilo.c에서 !! 관한 토론? [6] 오시리스 2011.07.25 34354
1741 [ARM중] 1차 분석 복습 [5] file 홍문화 2011.08.08 33698
1740 ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM (미완성) 구본규 2013.10.15 32758
» fork() 함수가 리턴을 두번하는 이유 설명 [2] 커널B조 2016.05.07 30218
1738 task_struct 구조체입니다. [1] file 아폴로 2013.04.30 29852
1737 ARM 프로세서 모드 [7] 홍문화 2011.06.08 26499
1736 BIOS 를 통하여 PCI configuration space를 액세스하는 방법 지현구 2007.08.12 22858
1735 파이프라인과 익셉션의 관계 관련 블로그 주소입니다. 이한울 2012.05.26 22081
1734 buildroot 사용법 [1] 구본규 2012.07.20 20238
1733 [x86] 스터디때 나왔던 cpu_dev 문제 [2] file pororo 2012.02.19 18422
1732 페이지 테이블에 주소 변환 정보가 채워지는 원리 [16] 홍문화 2011.07.12 16325
1731 odroid bootlog 입니다 박장운 2010.08.14 15560
1730 명령어 정리 - 늦어서 죄송.. 송형주 2007.03.09 14521
1729 Linux booting 과정 (start_kernel() 함수 전까지) 관련 참고자료들 모음 file 지현구 2007.04.27 14327
1728 분석 환경 구축 실습 [11] file 권석민 2013.05.19 14203
1727 [x86] 가족번호 [2] pororo 2012.02.27 13911
1726 LVM에 대해 간략하게 정리했습니다. [2] file 조성진 2013.05.07 13824
XE Login