fork()와 exec()를 넘어서
Source: Hacker News
[LWN 구독자 전용 콘텐츠]
LWN.net에 오신 것을 환영합니다
다음 구독자 전용 콘텐츠는 LWN 구독자가 여러분에게 제공한 것입니다. 수천 명의 구독자가 Linux와 자유 소프트웨어 커뮤니티의 최고의 뉴스를 얻기 위해 LWN에 의존하고 있습니다. 이 글이 마음에 드신다면, LWN 구독을 고려해 주세요. LWN.net을 방문해 주셔서 감사합니다!
Unix 초창기부터 두 가지 핵심 프로세스 지향 시스템 콜이 존재했습니다. 부모 프로세스를 복사해 자식 프로세스를 만드는 fork()와 현재 프로세스를 새로운 프로그램으로 교체하는 exec()입니다. Linux 커널에서는 이 시스템 콜들을 각각 clone()과 execve()으로 더 잘 알려져 있지만, 핵심 기능은 동일합니다. 이 프로세스 생성 모델에는 우아함이 있지만, 단점도 존재합니다. Li Chen이 커널에 “spawn 템플릿”을 추가하자는 최근 제안은 현재 형태로는 받아들여지지 않을 것이지만, 향후 새로운 프로세스 생성 원시 연산자를 향한 길을 제시할 수도 있습니다.
fork()는 비교적 비용이 많이 드는 시스템 콜입니다; 자식 프로세스를 위해 전체 프로세스 상태(메모리 포함)를 복사해야 하기 때문입니다. 수년간 여러 최적화가 이루어졌지만, fork()는 여전히 근본적으로 비용이 큰 연산입니다. 상황을 더 악화시키는 점은 fork() 호출 직후에 exec()가 따라오는 경우가 많아, 자식에게 복사해 놓은 메모리를 모두 버리게 된다는 것입니다. 이러한 경우를 최적화하려는 시도(vfork() 등)가 있었지만, 이 패턴은 여전히 기대보다 비쌉니다.
Spawn 템플릿
Chen의 패치 세트는 fork()와 exec() 패턴을 최적화하기 위한 흥미로운 접근 방식을 제시합니다. 이는 동일한 실행 파일을 반복적으로 실행하는 애플리케이션에 초점을 맞추고 있습니다. 예를 들어, 저장소의 내용을 파악하기 위해 Git을 여러 번 실행해야 하는 프로그램을 생각해 보세요. 이런 경우 프로그램은 템플릿을 만들어 호출 비용을 여러 작업에 걸쳐 분산시킬 수 있습니다. 템플릿은 spawn_template_create() 시스템 콜을 통해 생성됩니다:
struct spawn_template_create_args {
__aligned_u64 flags;
__s32 execfd;
__u32 exec_flags;
__aligned_u64 filename;
/* Some fields elided */
};
int spawn_template_create(struct spawn_template_create_args *args, size_t args_size);
이 호출은 실행 파일에 대한 템플릿을 나타내는 파일 디스크립터를 반환합니다. 템플릿은 파일 디스크립터(execfd) 혹은 절대 경로(filename) 중 하나로 지정할 수 있으며, 두 개를 동시에 지정할 수는 없습니다. 템플릿을 만들 때 커널은 지정된 파일을 열고, 향후 해당 파일을 더 빠르게 실행할 수 있도록 여러 정보를 캐시합니다.
대상 애플리케이션은 같은 실행 파일을 여러 번 실행할 수 있지만, 각 실행은 여러 면에서 다릅니다. 특정 실행에 대한 세부 사항은 다음 구조체 인스턴스에 넣어야 합니다:
struct spawn_template_spawn_args {
__aligned_u64 flags;
__aligned_u64 pidfd;
__aligned_u64 argv;
__aligned_u64 envp;
__aligned_u64 actions;
__aligned_u64 actions_len;
__aligned_u64 reserved[4];
};
argv 필드는 프로그램에 전달될 인자 리스트를 가리키는 포인터이고, envp는 환경을 가리킵니다. 파일 디스크립터와 시그널 처리의 변경은 actions를 통해 전달됩니다. actions는 다음 구조체 배열을 가리킵니다:
struct spawn_template_action {
__u32 type;
__u32 flags;
__s32 fd;
__s32 newfd;
__aligned_u64 arg;
};
예를 들어, 자식 프로세스에서 파일 디스크립터 4를 닫아야 한다면, 해당 spawn_template_action 구조체의 type을 SPAWN_TEMPLATE_ACTION_CLOSE로, fd를 4로 설정합니다. 파일 디스크립터 복제, 파일 열기, 작업 디렉터리 변경, 시그널 처리 변경 등을 위한 다른 액션도 존재합니다.
spawn_template_spawn_args 구조체를 채운 뒤에는 다음과 같이 새 프로세스를 실행할 수 있습니다:
int spawn_template_spawn(int template_fd,
struct spawn_template_spawn_args *args, int args_size);
내부적으로 이 시스템 콜은 일반적인 fork()/exec() 흐름과 거의 동일하게 동작합니다. Chen은 새로운 파일을 실행할 때 적용되는 모든 정상적인 검사들이 그대로 유지된다고 강조합니다. 하지만 템플릿에 캐시된 정보 덕분에 전체 과정이 이전보다 더 빨라집니다.
얼마나 빨라졌을까요? 커버 레터에 제시된 벤치마크 결과는 약 2% 정도의 향상을 보여줍니다. 눈에 띄게 큰 수치는 아니지만, 기대 패턴에 맞는 애플리케이션에서는 차이를 만들 수 있습니다.
posix_spawn()을 향하여
이 작업에 대한 가장 상세한 리뷰는 Mateusz Guzik이 작성했으며, 그는 “이 문제는 제게 매우 친숙하고, 오랫동안 고민해 왔습니다. 전체 fork + exec 관용구는 끔찍하고 폐기돼야 합니다”라고 말했습니다. 그는 패치 세트가 fork() 부분을 그대로 두고 있다는 점이 다소 이상하다고 지적했습니다. 비용의 대부분이 fork()에 집중돼 있기 때문에, 최적화 노력은 이를 제거하는 방향으로 가야 한다는 것이 그의 주장입니다. “현재 프로세스를 복사하는 대신, 깨끗한 프로세스를 생성하는 것이 올바른 접근”이라고 강조했습니다.
Christian Brauner는 목표에 대해 긍정적인 입장을 보이며, “exec용 빌더 API 아이디어는 전혀 터무니없는 것이 아니다”라고 말했습니다. 하지만 그는 새로운 API가 기존 pidfd 추상화 위에 구축돼야 한다고 제안했습니다. 구체적인 구현 세부 사항에 들어가지 않고, 그는 pidfd_open()에 빈 프로세스를 생성하는 옵션을 추가하는 것이 올바른 접근이라고 말했습니다. 이후 일련의 pidfd_config() 시스템 콜을 통해 새 프로세스의 환경, 실행 이미지 등을 원하는 대로 설정할 수 있습니다. pidfd_config()는 fsconfig()와 유사한 역할을 하게 됩니다.
Brauner가 강조한 새로운 인터페이스의 중요한 목표 중 하나는 사용자 공간에서 posix_spawn() 구현을 지원하는 것입니다. posix_spawn()은 fork()/exec() 패턴을 대체하기에 적합한 함수이며, 현재 구현이 fork()와 exec()를 내부에서 숨기고 있는 것과 달리, 네이티브 구현이 제공된다면 개발자들에게 큰 환영을 받을 것입니다.
Chen은 Brauner가 제시한 API가 더 나은 방향이라고 동의했으며, 앞으로의 작업이 그 방향으로 진행될 것이라고 밝혔습니다. 따라서 Linux 커널에 “spawn 템플릿”은 도입되지 않겠지만, Chen의 향후 작업이 실현된다면 Linux는 마침내 적절한 posix_spawn() 구현을 갖추게 될 가능성이 있습니다.
Index entries for this article
KernelSystem calls/clone()
KernelSystem calls/execve()