Li Chen의 스폰 템플릿 제안과 pidfd 기반 새 프로세스 생성 인터페이스 논의를 통해 Linux가 fork()+exec()를 넘어설 수 있을지 살펴본다.
* [LWN FAQ](https://lwn.net/op/FAQ.lwn)
* [기고하기](https://lwn.net/op/AuthorGuide.lwn)
사용자: 비밀번호: | |
Linux 커널을 LWN만큼 다루는 곳은 없습니다 ...하지만 이를 계속 이어가기 위해서는 여러분의 지원이 필요합니다. LWN 구독자는 LWN Kernel Source Database 접근을 포함한 여러 혜택을 받으며, 이 독특한 보도를 계속 살아 있게 하는 결정적인 지원을 제공합니다.
LWN을 한번 이용해 보세요: 1개월 무료 체험 구독 받기, 의무도 없고, 속임수도 없고, 신용카드도 필요 없습니다.
기사로 이동
작성 Jonathan Corbet
2026년 6월 5일
Unix의 아주 초기 시절부터, 프로세스 중심 시스템 호출의 핵심 둘은 부모의 복사본으로 자식 프로세스를 만드는 fork()와, 현재 프로세스를 대신해 새 프로그램을 실행하는 exec()였다. Linux 커널에서는 이 시스템 호출들이 clone()과 execve()로 더 잘 알려져 있지만, 핵심 기능은 그대로다. 이 프로세스 생성 모델에는 우아한 면이 있지만, 단점도 있다. Li Chen이 커널에 “스폰 템플릿”을 추가하자는 최근 제안은 현재 형태로는 받아들여지지 않겠지만, 앞으로 새로운 프로세스 생성 원시 기능으로 가는 길을 가리킬 수도 있다. fork()는 비교적 비용이 큰 시스템 호출이다. 자식 프로세스를 위해 전체 프로세스 상태를, 메모리를 포함해, 복사해야 하기 때문이다. 수년에 걸쳐 많은 최적화가 이루어졌지만, fork는 여전히 근본적으로 비용이 큰 연산이다. 더 나쁜 점은, fork() 호출 뒤에 바로 exec()가 오는 경우가 많아서 자식을 위해 공들여 복사했던 메모리가 곧바로 모두 버려진다는 것이다. 이런 경우를 최적화하려는 시도(vfork() 같은 것)가 수년에 걸쳐 있었지만, 이 패턴은 여전히 가능했던 것보다 더 비싸다.
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를 통해 전달되는데, 이는 다음 배열에 대한 포인터다:
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%의 향상을 보여준다. 그다지 커 보이지 않을 수도 있지만, 예상된 패턴에 들어맞는 애플리케이션에는 차이를 만들 수 있다.
이 작업에 대한 가장 자세한 리뷰는 Mateusz Guzik이 게시한 것으로, 그는 이렇게 말했다. “이 문제는 내게 각별한 주제이며, 꽤 오랫동안 간헐적으로 고민해 왔다. fork + exec 관용구 전체가 끔찍하며 퇴장해야 한다.” 그는 이 패치 세트의 초점이 문제의 fork() 부분을 건드리지 않은 점에서 다소 이상하다고 지적했다. 비용의 대부분이 바로 거기에 있으므로, 최적화 노력은 그것을 그림에서 제거하는 방향이어야 한다는 것이다. 현재 프로세스를 복사하는 대신, “오염되지 않은 새 프로세스를 만드는 것이 가야 할 길”이라고 했다.
Christian Brauner는 목표에 대해 호의적이었다. 그는 “exec를 위한 builder api를 갖는다는 생각은 그리 엉뚱하지 않다”고 말했다. 다만 그의 제안은 새로운 API를 기존 pidfd 추상화 위에 구축하자는 것이었다. 세부 사항까지 들어가지는 않았지만, 올바른 접근은 pidfd_open()에 빈 프로세스를 생성하는 옵션을 추가하는 것이라고 말했다. 그런 다음 새로운 pidfd_config() 시스템 호출을 여러 차례 호출해 이 새 프로세스를 원하는 대로 구성하고, 환경, 실행할 이미지 등을 설정하게 된다는 것이다. 따라서 pidfd_config()는 fsconfig()와 유사한 것이 된다.
Brauner에 따르면 새 인터페이스의 중요한 목표 가운데 하나는 사용자 공간에서 posix_spawn() 구현을 지원할 수 있어야 한다는 점이다. posix_spawn()은 fork()/exec() 패턴의 대체물로 매우 적합하다. 개발자들은 현재 구현과 달리 내부에서 fork()와 exec()를 숨기지 않는 네이티브 구현을 반길 가능성이 크다. Chen도 동의했다. Brauner가 대략적으로 그려낸 API가 더 좋아 보이며, 앞으로의 작업은 그 방향으로 갈 것이라고 말했다. 따라서 Linux 커널에 스폰 템플릿이 들어가지는 않겠지만, Chen의 향후 작업이 결실을 맺는다면 Linux는 마침내 제대로 된 posix_spawn() 구현을 얻게 될지도 모른다.
이 기사가 마음에 드셨나요?? 지금 무료 체험 구독을 신청하세요더 많은 이런 기사를 읽을 수 있습니다.
게시일 2026년 6월 5일 14:29 UTC (금) 작성자 smcv (구독자, #53363) [링크] (응답 1개)
현재 구현과 달리 내부에서 fork()와 exec()를 숨기지 않는 네이티브 구현
posix_spawn()는 적어도 때때로는 fork/exec의 복사 문제를 피한다. 이를 위한 전용 시스템 호출이 없다면, 아마 vfork()와 exec()를 사용하는 것 아닐까? 나는 최근 GLib가 가능하면 g_spawn_async()와 비슷한 함수들의 백엔드로 posix_spawn()를 사용하려 한다는 것을 알고 있고, 이는 메모리가 부족한 시스템에서 fork/exec가 사용자에게 실제로 일으키는 문제를 피하기 위해서였으며, 분명 성공적이었던 것 같다.
(불행히도 posix_spawn()에서 할 수 있는 일은 다소 제한적이어서, 다소 예측 가능한 실행 환경에서 하위 프로세스를 실행하려는 라이브러리이면서도 자기 호스트 프로세스의 실행 환경을 100% 제어하지는 못하는 경우에는 언제나 이를 사용할 수는 없다. 예를 들어 개별 fd를 닫기 위해 posix_spawn_file_actions_addclose()를 쓸 수는 있지만, close_range()에 해당하는 일을 할 수는 없다. 그래서 GLib는 라이브러리 사용자가 GLib 자체 API에 넘긴 옵션에 따라 많은 경우 여전히 fork/exec를 사용해야 한다.)
게시일 2026년 6월 5일 16:49 UTC (금) 작성자 bluca (구독자, #118303) [링크]
맞다. CLONE_VM과 CLONE_VFORK를 사용해서 부모가 자식이 exec될 때까지 멈추게 하므로, 메모리가 COW 없이 완전히 공유되어 많은 메모리를 절약한다.
게시일 2026년 6월 5일 14:45 UTC (금) 작성자 josh (구독자, #17465) [링크] (응답 12개)
나는 이전에도 이것을 말한 적이 있고, 이에 대한 발표도 했다. “액션”을 위한 올바른 메커니즘은 io_uring이라고 생각한다. 새 빈 프로세스를 만들고, 그 안에서 링을 실행해 파일 디스크립터 수신/설치 같은 일을 하며, 링의 끝을 하나 이상의 exec 시도로 마무리하고, exec 없이 링의 끝에 도달하면 그 프로세스에 SIGKILL을 보내는 방식이다.
또한 이것을 어떤 식으로든 “스폰 템플릿” 메커니즘과 결합하는 것이 합리적이라고 생각한다. 한 번 ELF를 로드하고 나서 그것을 반복 실행하고 싶을 수도 있기 때문이다. 예를 들어 gcc 호출을 만드는 경우처럼.
게시일 2026년 6월 5일 15:06 UTC (금) 작성자 krisman (구독자, #102057) [링크]
동의한다. 실제로, https://lwn.net/ml/all/87fr31xdz3.fsf@mailhost.krisman.be/
게시일 2026년 6월 5일 15:32 UTC (금) 작성자 bluca (구독자, #118303) [링크] (응답 5개)
큰 차이점 하나는 seccomp 지원이 없다는 점일 텐데, 프로세스 생성을 허용하는 무언가에겐 꽤 큰 단점일 것이다. 그 외의 일반적인 LSM 이야기는 최신 상황을 잘 모르겠지만, 한때는 기본적으로 “iouring 차단” 아니면 “iouring 허용” 정도로 귀결됐던 것으로 기억한다. 다만 그 부분은 이후 진전이 있었을지도 모른다.
게시일 2026년 6월 5일 15:41 UTC (금) 작성자 krisman (구독자, #102057) [링크] (응답 4개)
이제는 더 이상 그렇지 않다. 이제 io_uring에는 연산별 bpf 기반 필터링이 있다.
게시일 2026년 6월 5일 15:55 UTC (금) 작성자 bluca (구독자, #118303) [링크] (응답 1개)
그렇다면 여전히 seccomp는 없고, 자체 맞춤형 필터링이 필요하다는 뜻인가? pidfd_spawn를 많이 쓰는 사용자 공간 개발자 입장에서는 fsconfig()처럼 일반 시스템 호출 기반 접근이 훨씬 낫다. 훨씬 더 깔끔하고, 기존 샌드박싱 생태계와도 훨씬 잘 통합된다. fsconfig/opentree/movemount 등은 정말 멋진 API 계열이다. 설계도 잘 되어 있고 사용하기도 좋다.
게시일 2026년 6월 5일 16:09 UTC (금) 작성자 josh (구독자, #17465) [링크]
어떤 필터링 메커니즘을 쓰든, io_uring의 연산은 시스템 호출과 1:1로 대응하지 않는다. io_uring의 BPF 기반 필터링은 io_uring의 개념들에 맞게 필터링을 매핑한 합리적인 방식이라고 생각한다. seccomp를 사용하더라도 seccomp를 상당히 손봐야 할 것이며, 기존 필터는 그냥 바로 동작하지 않을 것이다.
게시일 2026년 6월 7일 3:30 UTC (일) 작성자 DemiMarie (구독자, #164188) [링크]
샌드박싱 도구가 이를 강제하고 자식 프로세스가 만든 모든 링에 자동으로 적용할 수 있는가?
Google이 Android에서 서드파티 앱에 io_uring을 활성화하기 전까지는 나는 회의적인 입장을 유지하겠다.
게시일 2026년 6월 7일 3:30 UTC (일) 작성자 DemiMarie (구독자, #164188) [링크]
필터링을 샌드박싱 도구가 강제하고 자식 프로세스가 만든 모든 링에 자동으로 적용할 수 있는가? eBPF가 필요한가, 아니면 cBPF만으로 되는가?
Google이 Android에서 서드파티 앱에 io_uring을 활성화하기 전까지는 나는 회의적인 입장을 유지하겠다
게시일 2026년 6월 5일 17:49 UTC (금) 작성자 calvin (구독자, #168398) [링크] (응답 2개)
왜 더 나아가지 않는가? 빈 FD 집합, 주소 공간 등을 가진 프로세스를 생성한 뒤 실행 파일을 직접 로드할 수 있게 하자. 이렇게 하면 exec() 로더의 책임을 더 많이 사용자 공간으로 옮길 수도 있다.
게시일 2026년 6월 5일 19:56 UTC (금) 작성자 josh (구독자, #17465) [링크]
많은 경우에는 그렇게 동작하겠지만, suid 같은 경우를 위해서는 여전히 커널 안에서 exec를 처리해야 한다.
그렇긴 해도, 그것이 반드시 고성능 경로일 필요는 없다. 모든 것을 위한 사용자 공간 메커니즘이 있고, 권한 상승 시에는 그냥 동작하지 않는 정도면 괜찮을 것이다.
게시일 2026년 6월 6일 4:53 UTC (토) 작성자 IAmLiterallyABee (구독자, #144892) [링크]
그건 기본적으로 Fuchsia가 동작하는 방식이다.
게시일 2026년 6월 5일 19:36 UTC (금) 작성자 khim (구독자, #9252) [링크] (응답 1개)
io_uring을 어떤 “특별한 방식”으로 확장하고 싶다는 건가… 왜? 그게 무엇을 가져다줄까? 정말로 그렇게 많은 프로세스를 생성해서 일반 시스템 호출만으로는 부족한가?
그 망할 템플릿을 수행할 코드를 그냥 기존 메커니즘을 사용해 실행하면 된다. (정말 필요하다면 io_uring도 물론 포함해서 말이다.)
커널에서 아무것도 바꿀 필요가 없다. 모든 것은 사용자 공간에서 할 수 있다.
게시일 2026년 6월 5일 19:58 UTC (금) 작성자 josh (구독자, #17465) [링크]
나는 이후 반복 재사용을 위해 실행 파일을 로드하는 시스템 호출을 추가하고, 그다음 그 시스템 호출에 대한 바인딩을 io_uring에 별도로 제공하자는 뜻이다. 어느 쪽으로든 사용할 수 있고, 다만 io_uring을 통해서는 더 빠를 것이라는 말이다.
게시일 2026년 6월 5일 18:01 UTC (금) 작성자 alkbyby (구독자, #61687) [링크] (응답 10개)
하지만 이 패턴은 여전히 가능했던 것보다 더 비싸다.
나는 이런 종류의 진술에 대한 근거가 얼마나 있는지 궁금하다. 물론 vfork(또는 CLONE_VFORK를 쓴 clone)와 일련의 “스폰 액션” 시스템 호출(fd 닫기, 시그널 액션/마스크 얻기 등)은 오버헤드가 없지 않다. 예를 들어 사용자 공간/커널 공간 경계를 넘나드는 것과 몇몇 프로세스 구조체를 복사하는 비용이 있다. 하지만 이야기하는 규모는 아마도 수십 마이크로초 정도일 수 있다. 반면 “자연스러운” exec 오버헤드, 즉 프로세스 시작 시 동적 링커, 재배치 등은 아마 그보다 한 자릿수 이상 더 큰 규모일 가능성이 있다.
사람들이 exec에 좌절하는 것은 이해한다. 분명 사용자 공간 API로는 posix_spawn이 맞다. 하지만 그 API를 위해서는 우리는 이미 커널 공간에서 필요한 모든 것을 가지고 있다. 그리고 사용자 공간도 적어도 대부분은 이미 제대로 되어 있다.
예전에는 예를 들어 glibc가 pthread_atfork 문제와 OOM 위험을 모두 안고 “일반적인” fork를 하던 시절이 있었지만, 그것은 오래전에 고쳐졌다.
그렇다면 더 많은 복잡성과 보안 위험 등을 감수할 이유가 무엇인가?
게시일 2026년 6월 5일 18:36 UTC (금) 작성자 alkbyby (구독자, #61687) [링크] (응답 9개)
아마도 vfork+exec 오버헤드의 가장 큰 문제는 그것이 부모 프로세스의 파일 디스크립터 수와 VMA 수에 선형적으로 비례한다는 점일 것이다. 더 큰 프로세스에서는 이것이 꽤 크게 누적될 수 있다. 하지만 그래도 수치에 기반한 논의가 있는 편이 더 낫다.
게시일 2026년 6월 5일 19:14 UTC (금) 작성자 Cyberax (✭ supporter ✭, #52523) [링크] (응답 8개)
[v]fork()+exec는 멀티스레드 앱에서는 끔찍하다. 몇 밀리초 뒤 버려질 막대한 페이지를 COW로 만들거나, 아니면 모든 것을 멈추게 하게 된다.
게시일 2026년 6월 5일 20:25 UTC (금) 작성자 alkbyby (구독자, #61687) [링크] (응답 7개)
vfork는 어떤 COW도 유발하지 않으며 멀티스레드 프로세스에서도 적절히 확장 가능해야 한다. (VMA 복사 오버헤드를 제외하면, 각 스레드는 1개 또는 2개의 vma에 해당하지만, 그것들이 아주 크지는 않을 것이다.)
게시일 2026년 6월 5일 20:44 UTC (금) 작성자 Cyberax (✭ supporter ✭, #52523) [링크] (응답 6개)
대신 vfork는 프로세스를 “얼어붙게” 만드는데, 지금 vfork()가 끝나기를 기다리는 수백 개의 스레드가 있다면 오히려 더 나쁠 수도 있다.
어느 쪽이든 좋지 않다.
게시일 2026년 6월 5일 22:03 UTC (금) 작성자 alkbyby (구독자, #61687) [링크] (응답 5개)
아니다. vfork는 호출한 스레드만 멈춘다. 그것도 posix_spawn 동안에만. 이는 캐시 라인 바운싱과 경합을 줄이는 데 도움이 될 가능성이 있다.
그리고 위에서 내가 실수했다. VMA는 vfork/CLONE_VFORK에서 복사되지 않는다. 그러니 vfork의 유일한 “확장성 없는” 부분은 파일 디스크립터 복제뿐인 것처럼 보인다. 그중 대부분은 가장 일반적인 사용에서는 명시적으로 닫히거나 O_CLOEXEC를 통해 닫힌다. 어떤 프로세스는 그런 것들을 수백만 개 갖기도 한다. 하지만 대부분의 경우 그렇게 비싸지 않다.
다시 말하지만, 여기 있는 사람들(나 자신 포함)은 비용에 대해 추측하고 있다는 점을 지적하고 싶다. 비용이 결정을 이끄는 요인이라면, 구체적인 수치를 얻어야 한다.
내 감각으로는 이렇다. a) fork에는 실제 문제가 있다 b) 사람들은 그 문제의 상당 부분을 (잘못해서) vfork 탓으로 돌리는 경향이 있다 c) vfork+작은 집합의 시그널 안전 액션+exec 전체는 매우 위험한 함정투성이다 d) 그래서 커널 개발자들이 더 나은 무언가를 제안하고 싶어 하는 유혹을 받는다
하지만 내 생각에 libc가 고품질 posix_spawn 구현을 제공해 그 함정 문제를 처리해 준다면, 무슨 차이가 있는가?
게시일 2026년 6월 6일 19:22 UTC (토) 작성자 quotemstr (구독자, #45331) [링크] (응답 4개)
하지만 내 생각에 libc가 고품질 posix_spawn 구현을 제공해 그 함정 문제를 처리해 준다면, 무슨 차이가 있는가?
당신 말이 맞다. 간단한 공유 접착 코드를 사용자 모드에 조금 두는 것이, 더 나쁜 접착 코드를 커널에 추가하지 않아도 되게 해주는 많은 영역 중 하나를 정확히 짚었다.
이런 승리 가능성은 내가 Go 진영, Zig 진영, 그리고 다른 이들이 “비대해진” libc를 우회해 “순수한” 시스템 호출을 하려는 노력에 그렇게 강하게 반대하는 이유 중 하나다. libc를 우회하는 것은 어리석은 잘못된 절약이다. 하드웨어의 권한 분리가 강제하는 협력 프로토콜을 세상의 Zig들이 받아들이려 하지 않기 때문에, 조정 지점을 값싼 사용자 모드 코드에서 비싼 커널 코드로 밀어 올리게 된다.
예를 들어 Go는 Windows에서는 user32/kernel32/ntdll을 기꺼이 쓰고, macOS에서는 libc를 기꺼이 쓴다. 그런데 Linux에서는 somehow 직접 시스템 호출을 하는 것이 절대적으로 중요하다고 여기며, 그 결과 CPU 권한 전환을 수반하지 않는 협력적 공유지를 만들려는 시도를 무력화한다. 왜 그런가? libc 시스템 호출 래퍼는 느리지 않다. 비대하지도 않다. 그것을 우회한다고 프로그램이 초능력을 얻는 것도 아니다. 내가 보기에는 일부 언어 런타임 작성자들이 libc를 우회하려는 충동은 분위기 같은 것에 기반해 있다. 개별적으로는 미미한 이익, 공동체에는 큰 비용이다.
허영이여, 그대 이름은 정적 링크이니.
게시일 2026년 6월 6일 21:01 UTC (토) 작성자 Cyberax (✭ supporter ✭, #52523) [링크]
Go/Zig 쪽 사람들도 아마 GLIBC가 하는 일을 그냥 복제할 수 있을 것이다. 그리고 glibc를 프로세스에 로드하는 비용도 사실 그렇게 크지 않다. 다른 건 피하면서 posix_spawn용으로만 쓰면 된다.
게시일 2026년 6월 7일 0:43 UTC (일) 작성자 koflerdavid (구독자, #176408) [링크]
Go도 시스템 호출을 직접 호출하려다가 OpenBSD로부터 많은 반발을 샀다. 이제 OpenBSD는 그 관행을 아예 금지했고, 모두가 대신 libc를 사용하도록 요구한다.
https://marc.info/?l=openbsd-tech&m=170647326029909&...
게시일 2026년 6월 7일 0:45 UTC (일) 작성자 intelfx (구독자, #130118) [링크] (응답 1개)
잘 말했다.
그리고 정확히 짚었다. 이 모든 것은 정적 링크라는 이름으로 이루어지고 있다. 모든 것을 컨테이너에 감싸는 것만으로도 부족한지, 이제는 모든 것이 단일 파일로 이루어진 컨테이너에 감싸지기를 원한다.
게시일 2026년 6월 7일 1:57 UTC (일) 작성자 Cyberax (✭ supporter ✭, #52523) [링크]
문제는 glibc의 심볼 버전 관리가 끔찍하다는 점이다. 더 나중 버전의 glibc에서 앱을 컴파일하면, 더 이전 버전에서는 배포할 수 없다. 그리고 이를 고칠 쉬운 방법도 없다.
반대로 user32.dll과 macOS libc는 극도로 안정적이다. Windows 11에서 앱을 컴파일해도, 누락된 함수만 사용하지 않는다면 Windows XP에서도 아마 동작할 것이다.
심볼 버전 관리가 버전 간에 함수 동작이 약간 다른 데서 생길 수 있는 미묘한 버그를 이론적으로 막아준다는 것은 이해한다. 하지만 우리가 말하는 건 하필 libc다!
게시일 2026년 6월 5일 18:54 UTC (금) 작성자 joib (구독자, #8541) [링크] (응답 4개)
프로세스 생성에서 fork+exec 접근의 문제를 설명하는 유명한(?) 논문이 있다: https://www.microsoft.com/en-us/research/wp-content/uploa...
그 논문은 “빈” 프로세스를 만든 뒤, pid 인자(또는 오늘날 Linux에 구현한다면 아마 pidfd)를 받는 변형을 여러 시스템 호출에 확장하여 새 프로세스를 시작 전에 설정하는 대안적 접근을 제안한다.
게시일 2026년 6월 5일 21:33 UTC (금) 작성자 gutschke (구독자, #27910) [링크] (응답 3개)
마지막으로 이 대화가 약 2년 전 나왔을 때, 기존 시스템 호출만으로도 이런 일을 모두 할 수 있다는 제안이 있었다. 시스템 호출을 창의적으로 조합하면, 필요에 따라 자신을 설정하는 “빈 프로세스”를 생성할 수 있다는 것이다.
나는 회의적이었지만 궁금했고, https://github.com/gutschke/safeexec/blob/main/safeexec.c가 그 조사 결과다.
이 시스템 호출들은 사실 이런 목표를 염두에 두고 설계된 것이 아니어서 조금 보기 흉하지만, 실험을 시작하기엔 충분할 만큼은 괜찮다. 적어도 빈 프로세스에서 시작한다는 기본 개념은 검증할 수 있을 것이다. 사용자 공간에서 할 수 있다면 전통적인 fork()/exec()의 여러 대안을 훨씬 더 쉽게 실험할 수 있다. 커널 추가를 제안해야 하는 것보다 훨씬 낫다.
우리가 무엇을 하고 싶은지, 실제 애플리케이션이 실제로 무엇을 요구하는지에 대한 합의가 생기면, 커널에 추가해야 할 구체적 문제를 식별할 수 있다.
현재 상태로는 우리는 그저 제자리걸음을 하고 있다. 몇 년마다 새로운 제안이 나온다. 어느 정도 관심을 얻는다. 하지만 애플리케이션이 이미 가진 것을 완전히 대체하지 못하거나, 다른 커널 서브시스템과의 큰 장애물을 만나면서 결국 흐지부지된다.
오늘날의 fork()/exec() 또는 다양한 spawn() 구현을 쓰면서 측정 가능한 문제를 겪는 두세 개의 주요 애플리케이션을 누군가 찾아내고, 내가 만든 코드로 시험 삼아 붙여서 그들이 중요하게 생각하는 벤치마크 수치가 바뀌는지 봤으면 좋겠다. 덧붙이자면, 새 템플릿 제안을 내가 올바르게 이해했다면, 그것 역시 내 POC 사용자 공간 구현으로 시뮬레이션할 수 있을 것이라고 믿는다.
추신: 내 해킹성 코드가 절대 생산 환경에서 사용되어야 한다고 주장하는 것은 아니다. 하지만 새로운 API 제안이 정말 차이를 만드는지, 아니면 대체로 사용되지 않을지를 보려면, 다양한 프로세스 실행 전략을 시험하는 실제 애플리케이션 사례가 훨씬 더 많이 필요하다고 믿는다.
게시일 2026년 6월 5일 22:57 UTC (금) 작성자 malmedal (구독자, #56172) [링크] (응답 2개)
해킹성이라면, 그냥 CLONE_VM에 CLONE_VFORK 없이 하면 안 되는가?
약간 성가신 점은 부모 프로세스에서 스택을 할당해야 하고 자식이 exec를 호출했음을 확실히 알기 전까지는 그 영역을 재사용할 수 없다는 점이지만, 극복 불가능한 것은 아니다.
게시일 2026년 6월 5일 23:11 UTC (금) 작성자 gutschke (구독자, #27910) [링크] (응답 1개)
내가 마지막으로 CLONE_VM을 실험한 지 너무 오래되어 아마 세부 사항을 다 기억하지 못할 것이다. 하지만 C 코드에서 쓰는 것이 사실상 불가능할 정도로 호출 규약이 매우 어색했던 것으로 생각한다.
언젠가 내 개념 증명이 어떤 형태로든 제품화된다면 또 하나의 어셈블리 래퍼를 추가하는 것은 당연히 가능하다. 하지만 나는 의도적으로 이런 종류의 저수준 코드로 할 수 있는 한 최대한 높은 수준이고 이식 가능한 상태를 유지하려 했다.
그리고 맞다, 이것은 보기 흉하고, 적어도 어느 정도의 노력 없이는 전혀 이식 가능하지 않다. 이런 API의 본질이 그렇다.
게시일 2026년 6월 5일 23:35 UTC (금) 작성자 malmedal (구독자, #56172) [링크]
어떻게 생길지 궁금해서 코드를 짜 봤다. 내가 뭔가 놓친 게 아니라면, 그렇게까지 나쁘지는 않다.
각 프로세스의 스택은 exec를 호출하자마자 재사용할 수 있다. 그걸 가장 잘 감지하는 방법은 잘 모르겠다. FD_CLOEXEC 같은 것, 또는 자식의 /proc/<pid>/exe가 바뀌는 시점 같은 방법이 있을 수 있다.
#include <fcntl.h>
#include <linux/sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */);
int target(void *arg) {
char fname[256];
int i = *(int *)arg;
snprintf(fname, sizeof(fname), "file%d.txt", i);
int fd = open(fname, O_CREAT | O_RDWR, 0777);
dup2(fd, 1);
dup2(fd, 2);
execl("/usr/bin/bash", "bash", "-c", "sleep 100 ; date", NULL);
return -1;
}
double dtime() {
struct timeval tv;
gettimeofday(&tv, NULL);
return (double)tv.tv_sec + tv.tv_usec / 1000000.0L;
}
int main(int argc, char **argv, char **envp) {
const int STACK_SIZE = 65536;
const int STACKS = 200;
int pids[STACKS];
int args[STACKS];
char *stack = malloc(STACK_SIZE * STACKS);
const long SIZE = 20 * 1024L * 1024 * 1024;
char *buffer = malloc(SIZE);
memset(buffer, 1, SIZE);
double start = dtime();
for (int i = 0; i < STACKS; i++) {
args[i] = i + 1;
pids[i] = clone(target, stack + STACK_SIZE * (i % STACKS + 1), CLONE_VM,
&args[i]);
}
fprintf(stderr, "Avg %d %f\n", 0, (dtime() - start) / 200.0);
for (int i = 0; i < STACKS; i++) {
int status;
int ret = waitpid(pids[i], &status, 0);
if (status != 0 || ret < 0) {
printf("status %d %d %d %d\n", i, pids[i], status, ret);
}
}
printf("done\n");
}
게시일 2026년 6월 5일 18:55 UTC (금) 작성자 clugstj (구독자, #4020) [링크] (응답 2개)
큰 프로세스를 반복해서 생성하고 있다면, 이미 잘못하고 있는 것이다. 해결책은 커널이 아니라 사용자 공간에 있다.
게시일 2026년 6월 5일 21:54 UTC (금) 작성자 roc (구독자, #30627) [링크] (응답 1개)
나도 이 말을 하러 왔다!! 사용자 공간 개발자로서 정말 성능이 중요하다면, 오늘날에도 fork+exec 비용을 완화할 수 있는 도구가 많이 있으며, 그중 일부는 새 프로세스를 만드는 것이 결코 따라갈 수 없는 훨씬 높은 성능 수준에 도달한다.
당연히 가장 효율적인 것은 다른 프로세스를 쓰지 않는 것이다. 기능을 라이브러리로 가져와 현재 프로세스에서 호출하라.
어떤 이유로든 별도 프로세스가 필요하다면(예: 자원 관리나 샌드박싱), 지속적인 하위 프로세스를 하나 만들고 여러 번 재사용하라.
어떤 이유로든 많은 개별 하위 프로세스가 필요하다면, zygote 패턴을 사용하라. 하나의 zygote 프로세스를 fork+exec한 뒤, 새 하위 프로세스가 필요할 때마다 그 zygote를 fork하라.
이런 방법이 적용되지 않는 예외적 경우도 있지만, 예외적 경우에서 작은 이득을 주기 위해 복잡한 새 커널 인터페이스를 추가하는 것은 좋은 생각으로 보이지 않는다.
게시일 2026년 6월 5일 22:07 UTC (금) 작성자 Cyberax (✭ supporter ✭, #52523) [링크]
많은 소프트웨어가 하위 프로세스를 회피하고 있을 수도 있다. 바로 그것들이 너무 끔찍하기 때문이다. 내 경우, 우리는 큰 Java 앱에서 포킹을 피하려고 한 프로젝트에서 실제로 “프로세스 러너” 서버를 사용했다. Microsoft 문서에서 텍스트를 추출하고 이미지 파싱/리사이징을 해야 했기 때문이다.
게시일 2026년 6월 5일 23:49 UTC (금) 작성자 ejr (구독자, #51652) [링크]
말장난은 아주 많이 의도했고, 이건 참혹하게 깨문 혀를 참혹하게 깨문 뺨 안에 넣은 듯한 반쯤 농담이다.
우리 중 얼마나 많은 이가 첫 운영체제 수업에서 fork()의 반환값을 확인하지 않아서 생기는 “서비스 거부”를 배웠는가? 공유 자원의 시대에는 그것이 아주, 아주 빠르고 사회적으로도 강제된 교훈이었다. 나중에 나는 학계에서 시스템 관리자로 일했고, 우리는 그런 일에 대비해야 한다는 것을 알고 있었다.
학생들(콜록 에이전트들)이 배워야 하는 “나쁜 생각” 에뮬레이션 환경이 필요할지도 모른다. vfork+exec를 없애는 것은 환상적으로 들린다. 하지만 그 선택이 왜 환상적인지를 N+4세대에게 어떻게 가르칠 것인가? 프로그래밍 언어 자체는 어느 것도 이를 도와주지 못한다. 그렇지 않았다면 우리 모두 Pascal / Modula-{2,3,...} / Ada를 쓰고 있었을 것이다.
이제 교육 현장을 조금 벗어나 있는 입장에서, 나는 정말 걱정된다. 우리는 이미 사람들이 Matlab만 알고, 그다음에는(?) Python만 아는 문제를 겪고 있다… 한 학생은 정수의 텍스트 표현이 서로 다른 인코딩과 다르다는 것을 이해하지 못했다. 나는 여전히 아키텍처, 프로그래밍, 응용 과정 없이 그 차이를 설명하는 데 큰 어려움을 느낀다. 그 차이에서 출발한다면, 물론 그냥 거기 있는 것이다. 하지만 그 차이를 한 번도 경험하지 못한 채 대학 수준 교육을 5년 넘게 받았다면, 음, 나는 아직도 그들의 입장에 충분히 서서 잘 설명하는 데 실패하고 있다.
미안하다. 횡설수설했다.
그 외에는, init가 좀비 자식을 수거하는 책임을 계속 지게 될 것이라고 생각한다. 어쩌면 용어도 바꿀 수 있을 것이다?
Copyright © 2026, Eklektix, Inc.
댓글과 공개 게시물의 저작권은 각 작성자에게 있습니다.
Linux는 Linus Torvalds의 등록 상표입니다