지난주 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 2197
1545 [커널 리눅스 내부구조] load_balance()가 수행되는 원리는? 유니악 2016.05.28 187
1544 [리눅스 커널 내부구조] 그림 3.14 : 태스크 문맥 용어 설명? 유니악 2016.05.28 169
1543 [리눅스 커널 내부구조] 그림 3.16 : 태스트 상태 전이 ? 유니악 2016.05.28 255
1542 안녕하세요. 5월 28일부터 커널 스터디 C조에 참석하는 김진수입니다. killinggun 2016.05.27 196
1541 아키텍쳐 북스터디 관련. [6] woos 2016.05.05 562
1540 x86팀 001주차 - 2016.05.21(토) 장소 및 교재 공지 n5de 2016.05.18 186
1539 천승환님 x86 커널 분석 자료를 우측 메뉴에 추가했습니다. [2] woos 2016.05.15 239
1538 리눅스 커널 강의 추천 [8] 민준홍 2016.05.01 3960
1537 A조 인원 정리 및 오픈채팅 개설 [40] file 김형원 2016.04.26 776
1536 디엔트리 객체에 대한 질문 반짝이는물오름달 2016.05.10 200
1535 3주차 질문내용 [5] heeee 2016.05.08 577
1534 VFS inode에 대해서 돌고래너구리 2016.05.08 365
1533 혹시 x86 아키텍쳐 스터디를 위해 오신분 계신가요? [10] woos 2016.05.03 504
» fork() 함수가 리턴을 두번하는 이유 설명 [2] 커널B조 2016.05.07 30230
1531 A조 5월 7일 참석인원 조사 [29] tjgus5 2016.04.30 440
1530 C조 5월7일 오프라인모임 참석자 조사 [39] file 뒷북 2016.04.30 592
1529 cortex-a 시리즈 PM(자료 좋네요.) file woos 2016.05.06 228
1528 커널 스터디 모임에 참여하고 싶습니다 [1] 꼬마툴 2016.05.05 323
1527 커널 스터디 모임에 참여하고 싶습니다 꼬마툴 2016.05.05 139
1526 Kernel 스터디 모임에 참여하고 싶습니다. 꼬마툴 2016.05.05 153
XE Login