# fork() + exec()를 넘어

> Clean Markdown view of GeekNews topic #30243. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30243](https://news.hada.io/topic?id=30243)
- GeekNews Markdown: [https://news.hada.io/topic/30243.md](https://news.hada.io/topic/30243.md)
- Type: GN+
- Author: [xguru](https://news.hada.io/@xguru)
- Published: 2026-06-07T09:33:21+09:00
- Updated: 2026-06-07T09:33:21+09:00
- Original source: [lwn.net](https://lwn.net/SubscriberLink/1076018/16f01bbbb8e0d1f0/)
- Points: 2
- Comments: 1

## Topic Body

- **spawn templates**는 같은 실행 파일을 반복 실행하는 애플리케이션에서 커널이 실행 파일 정보를 캐시해 이후 프로세스 시작을 빠르게 하려는 Linux 커널용 프로세스 생성 제안
- **fork()** 는 자식 프로세스를 위해 메모리를 포함한 전체 프로세스 상태를 복사해야 하고, 바로 뒤따르는 exec()가 그 메모리를 폐기하는 경우가 많아 기존 패턴의 비효율 발생
- **spawn_template_create()** 는 execfd 또는 절대 경로 filename 중 하나로 실행 파일을 지정해 템플릿 파일 디스크립터를 반환하고, 커널은 해당 파일을 열어 빠른 실행에 필요한 정보 캐시
- **spawn_template_spawn()** 은 일반 fork()/exec() 경로와 가까운 방식으로 동작하며 새 파일 실행 시 적용되는 검사는 유지하고, 커버레터의 벤치마크는 약 2% 개선 기록 {p:2}
- **pidfd** 기반 빈 프로세스 생성과 pidfd_config() 구성이 더 나은 접근으로 평가되며, 목표는 사용자 공간의 posix_spawn() 구현 지원

---

### Unix 프로세스 생성 모델의 한계
- Unix 초기부터 fork()는 부모의 복사본으로 자식 프로세스를 만들고, exec()는 현재 프로세스의 자리에 새 프로그램을 실행하는 핵심 프로세스 지향 시스템 호출
- Linux 커널에서는 같은 핵심 기능이 [clone()](https://man7.org/linux/man-pages/man2/clone.2.html)과 [execve()](https://man7.org/linux/man-pages/man2/execve.2.html)로 더 잘 알려져 있음
- 이 프로세스 생성 모델에는 우아함과 단점이 모두 있으며, Li Chen의 [spawn templates 제안](https://lwn.net/ml/all/20260528095235.2491226-1-me@linux.beauty)은 현재 형태로 Linux 커널에 수용되지 않을 예정이지만 미래의 새 프로세스 생성 원시 연산으로 이어질 수 있음
- fork()는 자식 프로세스를 만들기 위해 메모리를 포함한 전체 프로세스 상태를 복사해야 하는 상대적으로 비싼 시스템 호출
- 수년간 여러 최적화가 있었지만 fork()는 근본적으로 비용이 큰 작업
- fork() 호출 뒤에 exec()가 바로 이어지는 경우가 많고, exec()는 자식을 위해 복사된 메모리를 모두 폐기
- [vfork()](https://man7.org/linux/man-pages/man2/vfork.2.html) 같은 최적화 시도가 있었지만 fork() 다음 exec() 패턴은 여전히 가능한 수준보다 더 비싼 구조

### 스폰 템플릿(Spawn templates)
- Li Chen의 패치 세트는 fork()와 exec() 패턴을 최적화하기 위해 같은 실행 파일을 반복 실행하는 애플리케이션에 초점
- 예시로 저장소 콘텐츠 정보를 얻기 위해 Git을 반복 실행해야 하는 프로그램이 해당 사례
- 이런 경우 프로그램은 여러 실행에 설정 비용을 분산하기 위해 템플릿을 만들고, 그 템플릿으로 호출 가속
- 템플릿 생성은 `spawn_template_create()` 시스템 호출 사용
  - `int spawn_template_create(struct spawn_template_create_args *args, size_t args_size);` 형태의 시그니처
- 이 호출은 실행 파일 템플릿을 나타내는 파일 디스크립터 반환
- 실행 파일은 파일 디스크립터 `execfd` 또는 절대 경로 `filename` 중 하나로 지정해야 하며, 둘을 동시에 사용할 수 없음
- 커널은 지정된 파일을 열고, 이후 그 파일을 더 빠르게 실행하는 데 필요한 여러 정보 캐시
- 각 실행은 서로 다른 인수, 환경, 파일 디스크립터 변경, 신호 처리 변경을 가질 수 있음
- 구체적인 실행 정보는 `spawn_template_spawn_args` 구조체에 배치
  - `argv`는 프로그램에 전달할 인수 목록을 가리키는 포인터
  - `envp`는 프로그램 환경을 가리키는 포인터
  - `actions`는 파일 디스크립터와 신호 처리 변경을 전달하는 `spawn_template_action` 배열 포인터
- `spawn_template_action`은 `type`, `flags`, `fd`, `newfd`, `arg` 필드로 구성
  - 자식에서 파일 디스크립터 4를 닫아야 하는 경우 `type`은 `SPAWN_TEMPLATE_ACTION_CLOSE`, `fd`는 4로 설정
  - 다른 액션은 파일 디스크립터 복제, 파일 열기, 작업 디렉터리 변경, 신호 처리 변경 지원
- 실행 정보가 채워진 뒤 `spawn_template_spawn()`으로 새 프로세스 실행
  - `int spawn_template_spawn(int template_fd, struct spawn_template_spawn_args *args, int args_size);` 형태의 시그니처
- 내부 동작은 일반 fork()/exec() 경로에 가까운 방식
- 새 파일 실행 때 적용되는 일반 검사는 모두 그대로 유지
- 템플릿에 캐시된 정보로 전체 생성 흐름의 속도 향상
- 커버레터의 벤치마크 결과는 약 2% 개선이며, 예상 패턴에 맞는 애플리케이션에는 차이가 될 수 있는 수치 {p:2}

### posix_spawn()을 향해
- [Mateusz Guzik](https://lwn.net/ml/all/vealb52tv5suireenkke4lul2l3wbnaul2rp3ea545ly5wa5ty@yk3aksvp7skt)은 “전체 fork + exec 관용구는 끔찍하며 퇴출되어야 한다”는 평가
- 패치 세트의 이상한 지점은 fork() 부분을 그대로 둔다는 점이며, 비용 대부분이 거기에 있다는 판단
- 최적화는 현재 프로세스 복사를 제거하고 “깨끗한(pristine) 프로세스”를 만드는 방식이어야 함
- [Christian Brauner](https://lwn.net/ml/all/20260528-madig-fachrichtung-fehlinformation-61117ba640da@brauner)는 exec를 위한 builder API 구상이 “그렇게 이상한 것은 아니다”라는 입장
- 다만 새 API는 기존 [pidfd](https://lwn.net/Articles/794707/) 추상화 위에 구축하는 접근을 선호
- 구체적 세부 사항은 없지만 [pidfd_open()](https://man7.org/linux/man-pages/man2/pidfd_open.2.html)에 빈 프로세스를 만드는 옵션을 추가하는 방식이 올바른 접근
- 이후 새 `pidfd_config()` 시스템 호출을 여러 번 호출해 환경, 실행할 이미지 등 원하는 설정을 새 프로세스에 적용
- `pidfd_config()`는 [fsconfig()](https://man7.org/linux/man-pages/man2/fsconfig.2.html)와 유사한 역할
- 새 인터페이스의 중요한 목표는 사용자 공간에서 [posix_spawn()](https://man7.org/linux/man-pages/man3/posix_spawn.3.html) 구현 지원
- posix_spawn()은 fork()/exec() 패턴의 대체재에 적합
- 현재 구현은 내부에서 fork()와 exec()를 숨기며, 네이티브 구현은 그 구조와 다른 구현
- Li Chen은 Brauner가 넓게 그린 API가 더 나아 보인다는 데 동의했고, 향후 작업을 그쪽으로 진행할 계획
- Linux 커널에는 spawn templates가 들어가지 않지만, 향후 작업이 결실을 맺으면 Linux가 적절한 posix_spawn() 구현을 갖게 될 수 있음

## Comments



### Comment 59057

- Author: neo
- Created: 2026-06-07T09:33:22+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=48425528) 
- 관련 논의로 **A fork() in the road** 논문이 있음: [https://www.microsoft.com/en-us/research/wp-content/uploads/...](<https://www.microsoft.com/en-us/research/wp-content/uploads/2019/04/fork-hotos19.pdf>)  
  초록에서는 Unix의 `fork()`+`exec()` 조합이 영감 어린 설계라는 통념과 달리, 1970년대 기계와 프로그램에는 영리한 해킹이었지만 이제는 현대 프로그래머에게 나쁜 추상화이고 운영체제 구현도 제약한다고 주장함  
  운영체제의 1급 원시 기능으로 남겨두기보다 역사적 유물로 가르치고, 학생들이 처음 배우는 프로세스 생성 방식이 되지 않게 해야 한다는 입장임
  - `fork()`+`exec()`가 그렇게 된 이유는 **부모 프로그램과 함께 메모리에 들어가지 못할 만큼 큰 프로그램**을 실행할 수 있게 하려는 것이었음  
    원래 구현은 `fork()` 호출 시 포크하는 프로그램을 디스크로 스왑아웃하고, 제어가 돌아오기 전 프로세스 테이블 항목을 복제·조정해서 메모리에 있는 프로세스와 스왑아웃된 프로세스가 생기게 했고, 메모리에 있는 쪽이 제어를 받아 `exec()`를 호출할 수 있었음  
    이 방식 덕분에 작은 **PDP-11** 기계에서도 큰 프로그램을 실행할 수 있었고, 메모리가 매우 비싸던 시대에는 필요했음  
    QNX는 흥미롭게도 프로그램 로딩이 운영체제 안에 없고 라이브러리에 있음. 실행 파일 헤더를 읽고 메모리를 할당하고 프로그램을 로드해 실행 준비를 한 뒤 시작하는 `.so`에 링크하며, 프로그램 로더는 권한 없는 사용자 공간에서 돌아감. 아마 이쪽이 올바른 방식에 가까움
  - `fork()`를 쓰지 않는 가장 널리 쓰이는 “큰” 운영체제인 **Windows**의 프로세스 생성이 매우 느리다는 점은 흥미로움  
    `fork()`가 아닌 원시 기능이 있어야 한다는 데는 동의하지만, 성능이 최고의 논거인지는 잘 모르겠음
  - 이 논문도 좋고, 참고문헌 [29]도 `fork()`를 포함한 **확장 가능한 인터페이스**의 미묘한 부분을 다뤄서 특히 좋았음: The Scalable Commutativity Rule: Designing Scalable Software for Multicore Processors [https://people.csail.mit.edu/nickolai/papers/clements-sc.pdf](<https://people.csail.mit.edu/nickolai/papers/clements-sc.pdf>)
  - 당시 논의는 여기 있음: [https://news.ycombinator.com/item?id=19621799](<https://news.ycombinator.com/item?id=19621799>) - A fork() in the road (2019-04-10, 178 comments)
  - `fork()`는 **zygote 패턴**에는 훌륭함  
    그만큼 효율적이고 우아한 최적화를 떠올리기 어려움

- 최근에 포크된 프로세스에서 더 많은 **파일 디스크립터**를 닫아야 해서 생긴 모호한 버그를 겪었음  
  내 경험상 “현재 프로세스의 복제본을 원한다”보다 “완전히 새 프로세스를 원한다”가 훨씬 흔한데, 후자를 직접 표현할 방법이 없고 복제한 다음 사후에 고치는 식으로만 근사해야 한다는 게 이상하게 느껴짐
  - 보통 그 프로세스와 통신하고 싶으니, 예를 들어 파일 디스크립터 같은 것을 설정해야 하고 부모 프로세스의 정보를 넘겨야 함
  - 그건 `O_CLOEXEC`로 해결되는 것 아닌가?
  - “후자를 직접 표현할 방법”이라면 그게 **`posix_spawn`** 의 용도 아닌가?
  - “완전히 새 프로세스”가 정확히 무슨 뜻인가?

- “`fork()`는 상대적으로 비싼 시스템 호출이고, 자식 프로세스를 위해 메모리를 포함한 전체 프로세스 상태를 복사해야 한다. 수년간 많은 최적화가 있었지만 근본적으로 비용이 큰 작업이다. 더 나쁜 것은 `fork()` 호출 뒤에 곧바로 `exec()`가 따라와서, 자식을 위해 정성껏 복사한 메모리를 전부 버리는 경우가 많다는 점이다”라고 하면서 **쓰기 시 복사(copy-on-write)** 를 언급하지 않는 건 이상함  
  실제 메모리 전체를 복사하지 않게 해주는 최적화인데 빠져 있음
  - 글에서는 암묵적으로 처리했지만, 여기서 프로세스 상태 복사는 **메모리 관리 구조**를 뜻함. 주로 페이지 테이블과 VMA임  
    실제 페이지가 가리키는 메모리는 공유되더라도, 이 구조들의 복사본을 담기 위해 새 페이지를 할당해야 함. 그리고 그 구조들을 전부 순회해 복사하는 것 자체가 여전히 비쌈
  - Redis는 이 비용이 크게 중요한 프로세스 유형임. `fork()`가 메모리 자체를 복사하지는 않지만 **페이지 테이블**은 여전히 복사해야 함  
    수십 GB RAM을 들고 있는 프로세스라면 `fork()`가 오래 걸릴 수 있고, Redis가 `.rdb` 파일을 덤프하거나 바이너리 로그인 AOF를 다시 쓸 때마다 한 번씩 발생함  
    2012년에도 이 작업의 높은 비용을 보여준 글이 있었음: [https://redis.io/blog/testing-fork-time-on-awsxen-infrastruc...](<https://redis.io/blog/testing-fork-time-on-awsxen-infrastructure/>)  
    약 25GB RAM을 쓰는 `m2.xlarge`에서 `fork()`에 5.67초가 걸렸음. Redis 클라이언트가 보통 대부분의 작업에서 한 자릿수 밀리초 지연을 겪는다는 점을 생각하면 긴 정지 시간임. 이건 페이지 테이블 복사 시간뿐임  
    huge page를 언급하지 않는 건 놀랍고, 여기서는 핵심 고려사항처럼 보임. 14년 뒤 하드웨어가 빨라졌겠지만 Redis 인스턴스도 더 많은 RAM을 쓸 가능성이 높으니, 이 벤치마크를 다시 해보면 흥미로울 듯함
  - 이런 논문의 의도된 독자층에게 **쓰기 시 복사**는 기본 지식이라 생략된 듯함
  - 쓰기 시 복사가 있어도 `fork()`는 그 설정 비용을 치러야 함. 부모 프로세스에 바쁜 스레드가 많으면, 예를 들어 **Java**에서는 `exec()`가 실행되기 전에 불필요한 쓰기 시 복사가 많이 발생할 수 있음
  - 본문은 “상태”라고 했음. 쓰기 시 복사여도 내용을 복사하지 않을 뿐 **페이지 테이블 항목 수**에 비례하는 비용은 남음  
    큰 가상 메모리 크기를 가진 프로그램을 포크하는 것이 느리다는 건 잘 알려진 문제임

- `fork()`+`exec()` 모델의 우아함은 `fork()` 이후에 일반 API를 그대로 써서 모든 종류의 설정을 할 수 있다는 데 있음  
  지금까지 본 결합 호출 방식의 대체안들은 근본적으로 빈약해 보였는데, 모든 설정 옵션을 호출 매개변수로 추가해야 하고, 나중에 확장 가능하면서도 난장판이 되지 않게 만들어야 하기 때문임
  - 약간 동의하지 않지만 유용성은 보임. `fork()`/`exec()`가 어떤 경우에는 유용할 수 있어도, API들이 **`pidfd` 인자**를 받으면 꽤 괜찮을 것 같음. 0은 현재 프로세스를 뜻하게 할 수 있음  
    문제는 `setuid`/`setgid` 바이너리 정도일 텐데, 이 경우는 `exec`에서 특별 처리하는 편이 나을 수도 있음  
    예를 들어 `pidfd_t ps = spawn();`으로 정지된 프로세스를 만들고, `setuid(ps, 33);`, `capset(ps, ...);`, `socket(ps, ...);`, `mmap(ps, ...);`, `process_vm_writev(ps, ...);`, `exec(ps, ...);`, `signal(ps, SIGCONT);`처럼 구성할 수 있음  
    “내가 접근 권한을 가진 다른 프로세스에 이 작업을 하고 싶다면?”을 보통의 시스템 호출 API가 충분히 고려하지 않는다는 비판이기도 함. 이렇게 하면 `fork()`에서 스레드 안전성도 어느 정도 가능해짐  
    다만 수많은 매개변수를 받는 `CreateProcess` 같은 방식이 사용자 공간 API로 훌륭하진 않다는 데는 동의함
  - 완전히 반대 생각임. UNIX식 모델의 큰 실수는 프로세스 생성 시 너무 많은 **상태가 보존**된다는 점임  
    예를 들어 어떤 객체가 파일 디스크립터 번호 4가 되도록 하는 API들이 있고, 프로그램을 실행해서 그 프로그램이 4번 디스크립터에서 그 객체를 찾게 만들 수 있음. 이건 이상함  
    Windows는 수많은 결점에도 `fork()`+`exec()`를 쓰지 않고, 대신 프로세스 생성 방법에 대한 옵션을 주로 제공함. 우아하진 않았지만 방향은 맞았음
  - 그걸 우아하다고 부르는 건 `fork()`+`exec()` 역사의 **경로 의존성**임  
    `fork()`+`exec()`가 없던 다른 세계라면, 그런 “일반 API” 중 다수는 다른 프로세스의 설정을 바꿀 수 있게 명시적 `pid` 인자를 가졌을 것임. Fuchsia가 대략 그런 방식임  
    이 세계에는 장점이 많음. 가장 분명한 건 설정 오류를 보고하려고 별도의 IPC 체계를 마법처럼 만들어낼 필요가 없다는 점이고, 자식의 속성을 조정하는 관리자 프로세스를 둘 수 있다는 점도 꽤 유용함. 디버거들이 특히 좋아할 만함
  - `fork()`를 없애는 올바른 방법은 프로세스 상태를 바꾸는 일반 API들이 명시적인 **프로세스 핸들**을 받게 하는 것임  
    그러면 같은 API로 빈 프로세스를 설정할 수 있고, IPC나 디버깅 같은 다른 방식으로도 조합 가능함
  - 순서는 **spawn, configure, exec**가 되어야 함  
    프로세스가 `ptrace` 연결 상태이고 스레드가 없게 시작하면 설정 단계에서 시스템 호출을 강제로 수행하게 할 수 있음. Linux에는 “스레드가 없는 프로세스” 개념조차 없으니 아마 더미 스레드가 필요할 것임

- `fork()`가 싸다는 오해가 이상할 정도로 흔한데, 프로세스 크기에 대해 **O(N)** 이고 항상 그랬음  
  맞다, 쓰기 시 복사이긴 함. 하지만 프로세스 크기와 이를 표현하는 데 필요한 페이지 테이블 항목 수 사이에는 선형 관계가 있음

- Chen의 패치가 거절된 건 놀랍지 않음. 너무 특수한 사용 사례라 지원할 가치가 낮음  
  셸 개발자 관점에서는 “개발자들이 현재 구현처럼 내부에서 `fork()`와 `exec()`를 숨기지 않는 **네이티브 구현**을 반길 가능성이 높다”는 결론에 동의함
  - 특정 구현이 아니라 그 개념 자체에는 관심이 있어 보임

- `fork()`는 처음 배웠을 때부터 개념적으로 끔찍해 보였음. 어떤 하나의 작업, 즉 프로세스 시작을 하고 싶다면, 그와 다른 무관한 작업인 현재 프로세스 포크를 하는 **수수께끼 같은 주문**을 거쳐야 해서는 안 됨  
  글의 예시처럼 한 프로세스가 많은 `git` 하위 프로세스를 띄우는 상황을 가장 잘 처리하는 방법이 궁금함. 오래 실행되는 부모 작업 중에 `git`을 반복해서 처음부터 시작하는 건 말이 안 되는 것 같은데, 같은 결과를 내는 저비용 추상화는 무엇일까?
  - `fork()`는 개념적으로 단순함. 다른 계층을 끌어오지 않으면, 존재한다고 확실히 아는 단 하나인 **자기 자신**으로 프로세스를 시작하는 것임  
    그렇지 않으면 프로세스를 만들고, 실행할 무언가로 채우고, 실행되게 배치하는 여러 단계가 필요함. 아니면 Win32처럼 파일시스템, 객체 로더, 링커 같은 다른 계층과 영구히 뭉개서 합쳐야 함
  - Windows에서 출발한 사람으로서 `fork()`+`exec()` 모델은 전혀 이해되지 않았음. 이제는 그저 **역사적 특이점**이라는 걸 알지만, 아직도 `fork()`+`exec()`가 실제로 좋은 것인 척하는 사람들이 있음
  - `libgit2`가 있음. 파이프나 소켓으로 어떤 `gitd`와 통신하는 방식을 상상할 수는 있지만, 그게 왜 좋은 아이디어인지는 모르겠음. 그게 아니면 프로세스를 띄워야 함

- `exec`/`fork`를 대체하기 어려운 이유는 새 프로세스를 보통 설정해야 하기 때문임. 예를 들어 시그널 핸들러 설정, 파일 디스크립터 닫기나 열기, 네임스페이스 전환, `seccomp` 설정, 권한 조정이 필요함  
  그런데 이를 위한 시스템 호출들은 현재 프로세스에만 적용되므로 대체 수단이 필요함. 글의 제안은 이를 위한 새 API를 만드는 것이었음  
  내 생각에는 `spawn` 같은 새 시스템 호출이 빈 프로세스를 만들고, 그 안에 가벼운 **로더**를 올린 뒤 임의의 설정 데이터를 넘길 수 있음. 로더가 프로세스를 설정하고 주 프로그램을 `exec()`하는 방식임  
  이렇게 하면 메모리를 포크하지 않으면서 기존 API를 유지할 수 있지만, 파일 디스크립터와 다른 것들은 여전히 복제해야 함
  - 다행히 누군가 타임머신을 타고 이 글을 보고 **POSIX.1-2001**에 추가해둔 듯함 :)  
    농담이 아니었다면 미안하지만, `posix_spawn()`은 이미 존재하고 glibc에서 `fork`는 그냥 `clone()`의 별칭임  
    원래 제안과 정확히 같지는 않아도 `fork()`/`exec()`는 정말 레거시에 가까움

- `fork`와 `exec`가 쓰기 시 복사 성격을 넘어 지속적이고 **대수적인 동작**을 보일 수 있다면, 더 유용할 뿐 아니라 사용하기도 더 흥미로울 것임. 예를 들어 지연 평가에 쓸 수 있음

- 이 오래된 API에 대한 논의는 Hacker News에 많이 있었고, 예를 들면 [https://news.ycombinator.com/item?id=31739794](<https://news.ycombinator.com/item?id=31739794>)가 있음
