지난주 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
1745 x86 interrupt & instruction table file 백창우 2007.02.23 9190
1744 x86 linux booting 과정 file 백창우 2007.02.23 12734
1743 아키텍쳐에 관련된 내용입니다. [6] file 원민수 2007.02.25 7234
1742 논의사항 [7] 백창우 2007.02.26 4657
1741 Power architecture roadmap이라네요. [17] 정필섭 2007.02.26 4817
1740 첫째날에 공부한 내용 정리한번 해보는게 어떨까요?? 여러분의 의견을 묻습니다. [8] file 김정수 2007.02.26 7255
1739 [re] 제본 신청 하실분 댓글 달아 주세요. [17] 정필섭 2007.02.28 4870
1738 Power ISA 2.03 외 기타 문서. [6] 김형진 2007.02.28 5294
1737 [수정] 스터디 문서 분량을 나누겠습니다. [3] 정필섭 2007.03.02 5190
1736 제송.... 제가 이번에 스터디 그룹에 참여하는데 아직 공부할 부분을 할당받지 못했는데요... [2] 강형석 2007.03.02 5261
1735 power architecture의 역사와 미래 [2] file 백창우 2007.03.03 5179
1734 3월 3일 LKSAS 두번째 모임 즐거웠습니다. [7] file 김정수 2007.03.04 5046
1733 x86 아키텍쳐에 관한 동영상 강좌 링크입니다. [6] 남현우 2007.03.04 4413
1732 EM64T_Guide [2] file 이종우 2007.03.04 3502
1731 "만들면서 배우는 OS 커널의 구조와 원리/김범준 저"에 오늘 공부했던 내용에 대해 쉽게 적혀있네요. [4] 김정수 2007.03.04 5380
1730 메신저 주소 공유합시다!! [8] 김정수 2007.03.04 3672
1729 [질문] POSIX 표준 함수들의 소스 [4] 김강년 2007.03.05 5602
1728 분담2 MOVLHPS ~ MOVUPD 까지입니다 선준규 2007.03.06 4851
1727 asm 강좌 문서 file 맥주 2007.03.06 4147
1726 lilo 공부하실때 참고 할만한 자료 입니다. [5] 강형석 2007.03.07 3034
XE Login