티스토리 툴바


fork()

fork() 의 실행 예제
 ...
x = 10000;
y = fork();
if ( y == 0 )
{
    x = x + 10000;
    printf("child: %d \n", x);
}
else
{
    sleep(5);
    x = x - 10000;
    printf("parent: %d %n", x);
    wait(&status);
}

[실행결과]
child: 20000
parent: 0

위 예제에서 중요한 것은 if 조건문 때문에 "child: 20000" 혹은 "parent: 0" 의 둘 중 하나만 찍히는 것이 아니고, 둘 다 찍힌다는 것이다. fork()가 프로그램을 복제하라는 시스템 콜이기 때문에 fork()가 수행되고 나면 위 프로그램이 복제되어 두개의 프로그램이 메모리에 뜨게 되고, 각자 스케쥴되어 실행되면 각자 if 조건문을 수행하여 원래의 프로그램(부모 프로그램)은 "parent: 0"를 찍게 되고 복제된 프로그램(자식 프로그램)은 "child: 20000"을 찍게 된다. 부모 프로그램이 sleep(5) 시스템 콜을 호출하여 잠시 블록킹되므로 자식이 먼저 실행되어 위에서처럼 결과를 보이게 된다.

fork()는 크게 두가지 스텝으로 나뉜다. 즉 부모 프로세스의 core image(메모리에 떠있는 코드, 데이터, 스택)을 복제하고 task_union(프로세스 디스크립터)을 복제한다. 프로세스 디스크립터는 복제 후 자식에게 특정한 정보는 변경한다. 최종적으로 부모에게는 자식의 pid를, 자식에게는 0이 리턴되도록 각 프로세스의 커널모드 스택(KMS)을 세팅한다. 복제 알고리즘은 아래와 같다.

- core image 복제, task_union 복제
- 자식의 task_struct에 자식에게 특정한 정보 조정 : 복제된 core image 위치, pid, 등등
- 자식 프로세스를 프로세스 큐, 런 큐에 삽입
- 자식에게는 0, 부모에게는 자식의 pid를 리턴
   (자식의 KMS.eax = 0, 부모의 KMS.eax = 자식의 pid)

이 알고리즘이 수행되는 과정을 설명해본다.


<그림 4.1> fork() 직전의 메모리 모습

fork() 호출

fork()가 호출된 시점(그러나 아직 복제는 일어나지 않은 시점)의 메모리 상태를 <그림 4.1> 에 보여주고 있다. 부모 프로그램이 메모리에 로드된 모습과 부모의 task_union에 아래쪽에는 task_struct, 위쪽에는 커널모드 스택이 있는 것을 볼 수 있다. 메모리 주소는 아래쪽이 낮은 자리를 의미하도록 그렸기 때문에 부모 코드가 거꾸로 뒤집혀 들어 가 있다. fork()는 시스템 콜이므로 어셈블리어 코드 레벨에서는 int 0x80으로 번역된다. 따라서 fork()가 호출되고 인터럽트 처리 과정에 의해 우선 cpu가 주요 레지스터 (ss, esp, eflag, cs, eip)를 지정된 장소에 저장하고 인터럽트 0x80 번의 ISR1으로 점프한다. 리눅스에 cpu가 주요 레지스터를 저장하는 장소는 인터럽트 발생시 수행되고 있던 프로세스의 커널모드 스택이다. 따라서 그림에 부모 프로세스의 커널모드 스택에 ss, esp, .. 등이 저장되어 있는 것을 볼 수 있다. 특히 eip = 100 으로 인터럽트를 발생시킨 주소를 갖고 있음을 그림에 표시하였다. 리눅스에서 인터럽트 0x80번의 ISR1은 system_call 이다. system_call에서는 나머지 레지스터를 마저 저장하고 ISR2로 점프한다. 그림에서는 ... 으로 나머지 레지스터가 저장된 모습을 표현하였다. 리눅스에서 fork() 시스템 콜의 ISR2는 sys_fork() 이다.

<그림 4.2> fork() 직후의 메모리 모습

fork() 처리 후

sys_fork()는 do_fork()를 호출하고, do_fork()에서는 위에 제시한 fork() 알고리즘이 수행된다. 즉 부모의 몸을 복제하고 부모의 task_union을 복제하며 자식에게 특정한 정보를 수정한다. 위의 <그림 4.2>에서 메모리에 자식의 코드와 데이터부분이 부모로부터 복제된 것을 볼 수 있다. 다만 코드는 별도의 복제본을 만들지 않고 부모와 자식이 공유한다. 자식의 task_union도 부모의 그것을 복제한다. task_struct 뿐만 아니라 커널모드 스택도 복제한 것을 볼 수 있다. 다만, 자식에 특정한 정보는 수정한다. 그림에서는 자식의 pid = 41로 수정된 것을 볼 수 있다. 커널모드 스택은 그 프로세스가 지난번 중지되었을 시점을 레지스터 값(컨텍스트)을 저장하고 있다. 자식 프로세스가 이것도 복제하므로 자식 프로세스의 실행이 시작되면 부모가 중단되었던 코드지점에서부터 실행이 재개된다. 부모의 커널모드 스택의 eax에는 자식의 pid(즉 41)을 저장하고 자식의 커널모드 스택의 eax에는 0을 저장한 후 do_fork()에서 리턴하게 된다.
부모가 fork()를 호출하면 부모 프로세스를 중단하고 (eip = 100인 지점) 운영체제 안으로 cpu가 점프한다. 운영체제 안에서 fork() 시스템 콜을 처리하고 나면 메모리에는 부모의 몸을 복제한 자식의 몸이 메모리에 뜨게 되고, 또한 부모의 프로세스 디스크립터를 복제한 자식의 task_union이 메모리에 생기게 된다.


fork() 처리 리눅스 코드

fork()를 호출하게 되면,

fork() -> mov $2, eax; int 0x80 -> sys_fork

와 같은 진행이 일어난다. int 0x80 명령어 때문에 사용자 프로그램으로부터 운영체제 안으로의 long jump가 발생한다. 운영체제 안의 sys_fork()는 아래의 do_fork()를 호출한다.

do_fork()의 알고리즘
do_fork(long clone_flags, ... )
{
    p = alloc_task_struct();    // child의 task_union 할당
    *p = *current;    // parent의 task_struct 복사
    p->pid = get_pid(clone_flags);    // 새로운 pid 할당

    // clone_flags가 지정하는 공유 정도에 따라 fs, mm, signal handlers 복제여부 결정
    copy_files(clone_flags, p);
    copy_fs(clone_flags, p);
    copy_sighand(clone_flags, p);
    copy_mm(clone_flag, p);

    copy_thread( ... );    // 부모의 KMS를 child의 KMS에 복사.
                                    // 단, child KMS의 eax 위치에 0 저장

    SET_LINKS(p);    // 프로세스 큐에 삽입(제일 뒤에)

    wake_up_process(p);    // run queue에 삽입(제일 앞에)

SET_LINKS()의 알고리즘
SET_LINKS(p)
{
    p->next_task = &init_task;
    p->prev_task = init_task.prev_task;
    init_task.prev_task->next_task = p;
    init_task.prev_task = p;
}

wake_up_process()의 알고리즘
wake_up_process(p)
{
    p->state = TASK_RUNNING;
    add_to_runqueue(p);

add_to_runqueue()의 알고리즘
add_to_runqueue(p)
{
    next = init_task.next_run;
    p->prev_run = &init_task;
    init_task.next_run = p;
    p->next_run = next;
    next->prev_run = p;
    return retval;    // child의 pid return
}

주요 작업은 자식을 위한 task_union을 할당하고 그 안의 task_struct에 부모의 task_struct를 복사하는 것이다. pid, p_cptr, p_pptr 등 자식에 특정적인 정보는 변경된다. file, memory, signal 등 부모가 사용하던 시스템 자원은 자식에게 상속된다. 마지막으로 자식 프로세스를 프로세스 큐와 런 큐에 삽입한다. 코드에서 보다시피 부모의 몸에 대한 복제는 실제로는 일어나지 않는다. 자식 프로세스가 스케쥴되어 자신의 데이터 영역에 데이터를 쓰는 등 실제 몸이 필요한 때가 되면 그 시점에서 부모의 몸을 복제한다.


프로세스와 쓰레드

프로세스는 부모와 자식간에 코드만을 공유하고 모든 자료구조를 독립적으로 사용한다. 따라서 fork() 후에 부모와 자식은 전혀 별개의 프로세스로서 데이터 교환이 필요하면 시스템에 제공하는 IPC(InterProcess Communication) 서비스를 사용해야 한다. 이 공유 정도를 조절함으로써 여러 가지 유용한 일을 할 수 있다. 여러 레벨에서 공유정도가 다른 프로세스들을 통틀어 쓰레드라고 한다. 하지만 일반적으로 쓰레드라고 하면 스택을 제외한 모든 것을 공유한 프로세스를 일컫는다. 쓰레드는 데이터를 공유하고 있으므로 복제 후에도 부모와 자식이 같은 데이터 세그먼트를 접근하게 된다. 따라서 부모와 자식은 IPC를 통하지 않고도 데이터 교환이 가능하다.


kernel 쓰레드

쓰레드 중에서 커널이 자신을 복제하는 것을 커널 쓰레드라고 한다. 커널 쓰레드는 kernel_thread()에 의해 만들어지며 kernel_thread()를 호출할 때마다 자식 프로세스가 생겨서 커널 코드의 실행 경로를 복수개로 만든다. 즉 각 커널 쓰레드마다 커널의 다른 경로를 실행하되 쓰레드도 프로세스이므로 각자 스케쥴 될 때 마다 자신이 맡은 부분을 수행한다. 커널 쓰레드를 만드는 이유는 커널이 여러 작업을 동시에 수행하고 있어야 하기 때문이다. 예를 들어 주 쓰레드는 커널의 초기화 작업을 마저 끝낸 후 무한 루프에서 대기해야 하고, 복제된 쓰레드는 정기적으로 메모리에 올라와 있는 페이지 중 오랫동안 비활성인 경우 디스크로 옮겨야 하고, 또 다른 쓰레드는 전력관련 작업을 정기적으로 수행해야 하고 등 커널이 정기적으로 수행해야 하는 작업들마다 쓰레드를 만들어 해당 일을 수행하게 한다.

'IT > Operating System' 카테고리의 다른 글

fork()함수의 처리과정, 프로세스와 쓰레드  (1) 2012/01/06
프로세스(process)  (0) 2012/01/04
인터럽트(interrupt)  (1) 2011/12/21
start_kernel()  (0) 2011/12/19
Reading Linux Kernel  (0) 2011/12/17
부팅과정  (0) 2011/12/15

이 글에 관한 여러분의 의견을 남겨 주세요