2017년 2월 8일 수요일

fork, vfork, clone 차이점과 exec 함수

앞에서 fork 함수의 프로세스 생성 과정을 확인해보았다. 이번에는 리눅스 시스템에서의 프로세스 생성을 위한 fork, vfork, clone 시스템콜의 차이점과 exec 함수가 갖는 의미를 살펴보겠다.

1. fork

fork는 새로운 프로세스를 만들 때 기존 프로세스를 복제하는 방식을 사용하며 이때 fork를 호출한 부모 프로세스(parent process)를 원본으로 삼아 새로 복제하여 자식 프로세스(child process)를 만든다. (fork를 호출한 프로세스가 부모 프로세스, fork에 의해서 생성된 새로운 프로세스를 자식 프로세스)

fork는 자식 프로세스 생성과 동시에 필요한 메모리 영역을 할당하고 부모 프로세스의 메모리 영역을 모두 자식 프로세스의 메모리 공간으로 복사합니다. 하지만 fork가 부모 프로세스 메모리 공간을 모두 복사 하는 것은 비효율적인 오래 전 방식이며 현재 리눅스의 fork는 copy-on-write 기법을 사용하도록 개선되었다. Copy-on-write 기법은 자식 프로세스가 부모 프로세스의 메모리 공간을 모두 복사하는 것이 아니고, 동일 메모리 공간을 참조하고 있다가 변경사항이 발생할 경우에만 복사 하도록 구현되어 있다. 현재 리눅스의 fork가 Copy-on-write 기법을 도입하면서, 아래 설명할 vfork 의 장점이 많이 사라진 상태이다.

2. vfork

위에서 설명할fork 함수의 사용법을 살펴보면, 왜 vfork가 탄생하게 되었는지를 이해할 수 있게 된다. fork 함수는 주로 fork함수 실행 직후에 exec 계열 함수를 호출하는 방식으로 사용된다. exec 계열 명령은 현재 프로세스 실행 이미지를 다른 파일로 교체 시키는 기능이다. exec가 실행되면 기존 메모리는 모두 해제되고, file, mask, pid 등의 정보만 유지된 채 다른 실행 파일 이미지로 교체된다. 쉽게 말해 다른 실행 파일이 실행 되는 것이다. 이렇게 fork 후에 exec가 연속해서 호출되는 과정을 fork-and-exec모델이라고 부른다. 그런데 fork를 하면서 복제되었던 메모리가 곧바로 exec를 하면서 해제 되므로 무의미한 메모리 복제과정, 즉 오버헤드가 존재 하게 된다. fork-and-exec 방식의 오버헤드를 제거하려면 메모리 복제를 피하도록 설계 해야만 했다. 하지만 문제는 fork후에 exec 호출할 것인지 아닌지를 예측할 수 있는 방법은 없으며, 결국 프로그래머 용도에 fork-and-exec인 경우라면 메모리 복제를 하지 않는 새로운 함수를 쓰도록 대안이 제시되었다. 그 결과 실험적인 유닉스인 BSD3에서 vfork라는 함수가 탄생되었다.

자식 프로세스가 생성되면서 부모 프로세스 메모리, 파일 기술자 테이블 등을 복사하여 프로세스를 구성한다. 하지만, 자원을 복사하는데 걸리는 시간 때문에 프로세스 생성까지 오랜 시간이 걸렸고, 우리는 vfork가 fork의 무의미한 메모리 복제과정을 회피하기 위해 고안된 것이라는 것을 알고 있다. vfork 는 부모 프로세스의 메모리를 복제하는 대신에, 메모리를 공유하는 방법을 선택했다. fork는 광역과 지역 메모리 공간까지 똑같은 복사본을 만들고 부모와 자식이 서로 다른 메모리 공간을 사용하지만, 단점을 보완하기 위해서 vfork는 프로세스만 만들고 부모의 메모리 공간을 그대로 이용한다.(부모와 자식이 메모리 공간을 공유)

vfork는 부모프로세스 와 주소공간을 공유하고, 메모리 주소 공간에 대한 복사가 이루어지지 않기 때문에, 기존 fork보다 빠르게 프로세스를 생성할 수 있다. 하지만 자원을 공유하기 때문에, 자식 프로세스가 실행되어 변수들을 수정하기 시작하면, 부모 프로세스는 영향을 받게된다. 자원에 대한 race condition이 발생하게 되는 것이다. vfork는 이 문제를 해결하기 위해서 부모 프로세스는 자식프로세스가 exit 하거나 execve가 호출되지 전까지 SLEEP 상태로 대기시킨다. 즉 자식프로세스가 실행이 완료되어 exit를 실행하거나 exec를 호출하여 다른 프로그램을 실행하고 종료될 때까지 부모 프로세스는 계속 기다리고 있게 된다. vfork 를 사용하는 경우에 execve를 바로 호출 하지 않는 경우라면 자식 프로세스에서 수정한 광역변수나 지역변수들이 부모 프로세스에 영향을 미치기 때문에, 이를 고려하여 코딩 하여야 한다. 이점은 많이 이들이 vfork 가 위험하다고 보는 이유이기도 하다.

위에 언급한 것과 같이fork가 전체 메모리 주소 공간을 복사한다는 것은 예전 방식이고 현재 리눅스에서는 fork는 copy-on-write방식을 사용하여 프로세스 생성 시 모든 자원을 복사하는 것이 아니고 변경사항이 생길 경우에만 복사하도록 구현되어 있다. 따라서 현재 vfork가 갖는 장점은 부모 프로세스 페이지 테이블을 복사 하지 않는 것 뿐이다. 또한 성능에 대한 장점도 fork 함수에 copy-on-write 방식이 구현된 후로는 차이가 없어져서 현실적으로 vfork는 이점은 없어졌고, 자식 프로세스가 종료되거나 exec로 교체되기 전까지 부모 프로세스가 대기 상태로 기다려야만 하는 부작용 등만 남게 되었다.

따라서, 많은 관련 문서에서는 더 이상 vfork의 사용을 중지하기를 권고하고 하고 있다. POSIX 표준에서는 fork-and-exec 방식을 이용하여 표준 함수posix_spawn를 만들었다. 아직 많은 부분에서 vfork는 사용되고 있지만, 시간이 흘러감에 따라 점차 vfork는 퇴출되거나 호환성을 위해 남겨두지만 fork 로 완전 대체될 가능성도 있다.

3. clone

clone의 경우는 용도나 특성에 있어서 위의 두 함수와 살짝 차이가 존재한다. 이미 process descriptor 인 task_struct구조체를 살펴보면서, 리눅스 커널에서는 스레드를 위한 구조체가 따로 존재하지 않고 프로세스와 스레드의 구분을 위해 약간 다른 처리를 한다고 확인했다. clone은 프로세스를 생성하긴 하나, 용도 상 프로세스 생성보다는 스레드 생성의 의미에 가깝다. 리눅스에는 실제 스레드라는 것이 없고, clone함수를 이용하여 프로세스를 생성하면, 자식 프로세스가 부모 프로세스의 메모리 영역을 공유 한다. 즉 fork는 별도의 공간을 할당한 뒤 부모 프로세스 영역의 값을 복사하는 형식인 것에 반해 clone은 메모리 공간 자체를 공유한다. 물론 코드 영역도 공유한다. 이것이 가지는 의미는 자식 프로세스에서 어떤 메모리 영역의 값을 변경시키면 부모 프로세스에서도 그 영역을 공유하므로 부모 프로세스 직접적으로 영향을 준다는 사실이며, 이것이 곧 스레드의 특성과 매우 흡사합니다. 대개, 리눅스에서는 fork로 프로세스를 생성하고 clone 으로 스레드를 생성한다고 받아드려도 되겠다.

4. exec

exec 함수는 위의 fork 함수를 살펴보면서 보면서 이미 그 의미를 살짝 확인했다. (보통 Exec 함수는 execve를 포함한 exec함수군으로 표현한다.) exec함수는 사용하던 공간을 해제하고 새로운 공간을 실행 가능한 바이너리를 로드하고 실행한다. 새로운 스택을 초기화하고 excutable entry point를 전달하여 실행한다. exec 함수는 보통 fork-and-exec 방식(UNIX process management model에서 일컫는)으로 사용된다. 어떤 프로세스가 exec를 호출하면 이 프로세스 메모리 공간을 해제하고 새 실행 파일을 로드하고 실행한다. 이렇게 되어버리면 exec를 호출한 프로세스 공간은 새 프로세스로 교체되고, exec를 호출한 프로세스는 실행이 중지된다. 기존 프로세스의 실행 중지를 회피하기 위한 방법이 fork-and-exec 방식인 것이다. 새로운 프로세스를 생성하고 그 프로세스에서 exec 함수를 실행해서 새로운 프로세스를 실행하는 것이다. 이 방식을 사용하여 부모 프로세스와 자식 프로세스가 모두 실행이 가능한 것이다.

Navigation of this blog:이 블로그 한눈에 보기

2017년 2월 4일 토요일

fork 함수와 process 생성

리눅스는 새로운 프로세스와 스레드 생성을 fork (=_do_fork)함수를 이용하여 수행한다. fork 는 현재 태스크(프로세스)를 복제하여 자식 프로세스를 만든다. fork를 수행하는 프로세스의 task_struct 자료구료를 복사하여 새로운 프로세스(혹은 스레드)를 만드는 것이다. 이런 경우 원래 프로세스(fork 를 수행한 프로세스)는 부모 프로세스(parent process)가 되며, 새로운 프로세스는 자식 프로세스(child process)가 된다.

이렇게 만들어진 프로세스는 새로운 pid를 부여받으며, 이때 부모의 아이디는 ppid에 저장된다. 한 프로세스는 자식 프로세스를 여러 개 생성할 수 있으며, 자식 프로세스들끼리는 부모가 같은 형제프로세스(sibling process)가 된다. 자식 프로세스는 부모 프로세스의 task_struct 를 복사하여 만들어진다. 따라서 상속되지 않는 지연된 시그널과 같은 일부 자원과 통계수치를 제외하고는 그 부모와 동일한 값을 갖는다.

프로세스 생성 fork 커널 thread

user space 레벨에서 프로세스 생성을 위하여 커널 단에서는 세가지 시스템 콜을 제공하며, 이를 통하여 프로세스 생성을 가능케 한다. 3가지 시스템콜은 fork, vfork, clone이 되겠다. kernel space 레벨에서도 특정한 작업을 위하여 커널 스레드(kernel thread)를 만드는 경우가 있는데, 이를 위해 kernel_thread 함수를 제공하고 있다 kernel_thread 함수는 커널 함수인 _do_fork 함수를 직접 호출하여 스레드를 생성 실행한다.

user space 에서 프로세스 생성은 fork, vfork, clone 3가지 시스템콜 중에 하나로 수행되며, kernel space 에서 프로세스 생성은 kernel_thread 함수가 _do_fork 를 직접 호출하여 수행된다. 4가지 방식은 모두 _do_fork 함수를 호출하는 점은 동일하며, 호출할때 arg 설정으로 그 차이점이 발생한다.

pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
    return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
        (unsigned long)arg, NULL, NULL, 0);
}

SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
    return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
    /* can not support in nommu mode */
    return -EINVAL;
#endif
}

SYSCALL_DEFINE0(vfork)
{
    return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
            0, NULL, NULL, 0);
}

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         unsigned long, tls)
{
    return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}

위의 4가지 방식의 _do_fork 함수 호출 소스를 살펴보자. #define 관련한 소스는 상당부분 삭제하였다.

fork 시스템콜을 보면 SIGCHLD flag 설정을 제외하면, 추가적인 설정은 없다. SIGCHLD flag의 의미는 자식 프로세스가 종료 될 때 부모 프로세스에게 SIGCHLD 시그널을 보내라는 설정이다. 즉, 해당 flag 설정이 되어 있으면, 프로세스 exit 수행 시에 SIGCHLD 시그널이 부모 프로세스에게 전달된다.

kernel_thread 함수와 vfork 함수는 CLONE_VM가 설정된다. CLONE_VM는 새로 생성되는 자식 프로세스와 부모 프로세스 간에 메모리 공간을 공유하겠다는 설정이다. kernel thread가 각자의 메모리 공간이 존재하지 않는 점은 해당 flag 설정 때문이다. 또한, clone에는 CLONE_VM 설정이 없지만, clone_flags 를 arg 로 받아오기 때문에, 얼마든지 설정이 가능하고, 많은 곳에서 그렇게 사용하고 있다. 4가지 호출 방식의 비교는 뒤에서 다시 세부적으로 하기로 하겠다.

fork vfork clone kernel_thread _do_fork

_do_fork 함수는 크게 copy_process함수와 wake_up_new_task함수 호출 부분으로 나뉜다. copy_process함수에서는 프로세스 생성을 위한 초기화 작업들이 이뤄진다. 부모 프로세스의 task_struct 와 thread_info 구조체를 복사하고 필요한 부분은 초기화하여, execution 을 위한 준비를 한다. wake_up_new_task 함수는 새로이 생성한 task_struct 를 scheduler 쪽에 알려주는 역할을 한다. 새로운 process 생성을 완료하고 scheduler에 알려서, runqueue에 등록/실행될 수 있도록 하는 것이다.

copy_process 함수가 하는 일을 크게 나누어 보면 아래와같다.
1. Check clone_flags : 인자로 넘어온 clone_flags를 확인한다. 리눅스 구현 상 허용되지 못하는 flag 설정이 시스템콜을 통하여 전달될 수 있다. 이를 걸러내기 위한 확인 작업이 되겠다.
2. dup_task_struct : 프로세스의 기본 구성이 되는 task_struct 와 thread_info 구조체를 생성하고, 부모 프로세스의 구조체로부터 복사한다. 커널은 task_struct 와 thread_info 구조체를 슬랩 할당자(slab allocator)를 통해 메모리를 할당한다. 할당된 메모리에 부모 프로세스의 정보를 복사하고, 공유될 수 없는 정보들은 초기화한다.
3. check max number of processes : rlimit (Resource limit ID)값을 확인하여, 프로세스를 생성하는 user의 최대허용 프로세스 개수를 넘지 않도록 한다.
4. Initializing time stamps and irqflags : 프로세스 관련 정보 중 time stamp 통계 값이나 irqflags 등을 초기화한다. 이런 정보들은 프로세스 간에 공유가 불가능하다.
5. Scheduler related setup & Copy process info : sched_fork함수를 호출하여 scheduler 쪽에서 해당 task_struct 에 대한 초기화 작업을 한다. Scheduler 에서는 해당 프로세스를 TASK_NEW state로 설정하고, priority 값 등을 초기화하는 작업이 이뤄진다. copy_fs 는 file system 정보를 복사하며, copy_files 는 현재 오픈되어 있는 file 정보를 복사한다. 각 함수 안에서는 clone_flags를 확인하여, 각 CLONE_FS, CLONE_FILES 비트가 설정되어 있어야 실제 복사가 이뤄진다. 프로세스 메모리 공간의 복사를 수행하는 copy_mm 함수도 호출되며, signal 복사도 이뤄지는 등 여러 함수가 호출되니, 한번 쭉 보기 바란다.

copy_process 함수는 마무리 작업을 완료한 후 새로이 생성된 task_struct 구조체를 반환한다. 반환된 task_struct 구조체는 위에서 이야기한대로 scheduler의 wake_up_new_task 함수에 인자로 담아서 전달하면, runqueue에 등록/실행될 수 있도록 준비하는 것이다.

Navigation of this blog:이 블로그 한눈에 보기