앞에서 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 함수를 실행해서 새로운 프로세스를 실행하는 것이다. 이 방식을 사용하여 부모 프로세스와 자식 프로세스가 모두 실행이 가능한 것이다.
댓글 없음:
댓글 쓰기