30년 버틴 init, 5년 만에 갈아치워진 표준

리눅스에서 PID 1로 부팅 직후 가장 먼저 실행되는 프로세스가 init이에요. 이 자리는 SysVinit이 30년 가까이 지켰지만 2010년대 초반부터 갑자기 풍경이 바뀌었어요. 2011년 Fedora 15에서 등장한 systemd가 5년 만에 거의 모든 메이저 배포판의 기본 init을 차지했고, 그 사이에는 Upstart가 잠깐 등장했다 사라졌죠.

이 글은 세 init 시스템을 기능 비교 대신 연표로 다룹니다.

  • 어떤 배포판이 언제 SysVinit을 버렸는가
  • Upstart는 왜 짧게 살았는가
  • systemd 채택을 둘러싼 Debian 투표, Devuan 분기, Ubuntu의 입장 변화는 어떻게 흘러갔는가
  • 지금도 systemd를 쓰지 않는 배포판은 어디인가

서버에 SSH로 들어가서 ps -p 1 -o comm=을 한 번 실행했을 때, 출력 한 줄이 어떤 역사 위에 서 있는지 정리해 보겠습니다.

세 세대, 한눈에

세대이름등장부팅 모델설정 단위대표 배포판 (전환 시점)
1세대SysVinit1983 (System V)직렬 / 런레벨/etc/inittab + /etc/init.d/*.sh거의 모든 리눅스 (~ 2010s)
2세대Upstart2006이벤트 기반/etc/init/*.confUbuntu 6.10 ~ 14.04, RHEL 6
3세대systemd2010병렬 / 의존성 + cgroup*.service, *.socket, *.timer, *.targetFedora 15+, RHEL 7+, Debian 8+, Ubuntu 15.04+, Arch, openSUSE 12.1+

세 시스템의 본질적 차이는 한 줄로 줄일 수 있습니다. SysVinit은 셸 스크립트를 순서대로 돌리고, Upstart는 이벤트가 발생하면 잡(job)을 돌리며, systemd는 의존성 그래프를 따라 유닛(unit)을 병렬로 돌린 뒤로도 계속 지켜봅니다.

1세대: SysVinit (1983~)

뿌리는 AT&T가 1983년에 낸 Unix System V입니다. 리눅스에는 1990년대 초 Miquel van Smoorenburg(미컬 반 스모렌부르흐)가 포팅한 sysvinit 패키지로 흘러들어왔고, 그 후 약 20년간 거의 모든 리눅스 배포판의 기본 init이 됐습니다.

핵심 구조는 단순합니다.

/etc/inittab          → 어느 런레벨에 어떤 스크립트를 돌릴지 선언
/etc/init.d/*         → 각 서비스의 start/stop/restart 스크립트
/etc/rc{0..6}.d/      → 런레벨별 심볼릭 링크 (S20foo, K80foo …)

런레벨(runlevel)은 시스템의 모드 번호입니다. 관습적으로 7개를 씁니다.

런레벨의미
0halt (종료)
1 (S)single user mode
2multi-user, 네트워크 없음 (Debian 계열은 네트워크 포함)
3multi-user, 네트워크 있음 (서버 기본값)
4미정의/사용자 정의
5multi-user + GUI (데스크톱 기본값)
6reboot

런레벨 진입 시 /etc/rcN.d/S로 시작하는 링크를 번호 순으로 실행하고, 빠져나갈 때 K 링크를 실행합니다. 부팅이란 곧 “셸 스크립트를 정해진 순서대로 한 줄씩 돌리는” 일이었습니다.

SysVinit의 한계

SysVinit이 30년을 살아남은 건 단순함 덕이고, 5년 만에 밀려난 것도 그 단순함 탓입니다.

가장 큰 문제는 직렬 부팅이었습니다. S20foo가 끝나야 S21bar가 시작되니, CPU가 놀고 있어도 두 서비스가 서로 무관해도 순서를 바꿀 수 없었고 이게 부팅이 느린 결정적 이유였습니다. 의존성 표현이 약한 것도 여기 얽힙니다. “DB가 떠 있어야 web이 뜬다” 같은 관계를 번호(S20, S21)로만 나타냈고, LSB 헤더가 의존성 기술을 거들긴 했지만 여전히 선형 정렬이 전제였습니다.

나머지 둘은 프로세스 관리 쪽입니다. 스크립트가 &로 백그라운드에 넘긴 자식을 init이 직접 추적하지 않아 PID 파일이 진실의 원천이 되는데, 이게 어긋나면 service foo status가 거짓말을 하고 자식이 더블 fork로 도망가면 더 곤란해집니다. 서비스가 죽었을 때 되살리는 것도 init 밖의 일이라, respawn을 inittab에 직접 걸거나 monit/supervisord 같은 외부 도구를 끌어와야 했습니다.

이 한계들이 2000년대 후반 “부팅이 빨라야 하는 노트북"과 “의존성 많은 데스크톱 환경"의 시대와 충돌했습니다. 다음 두 세대는 모두 이 중 하나 이상을 풀려고 출발합니다.

2세대: Upstart (2006~)

Canonical의 Scott James Remnant(스콧 제임스 렘넌트)가 만들었습니다. 첫 출시는 **Ubuntu 6.10 “Edgy Eft” (2006-10)**입니다. 이때부터 Ubuntu의 기본 init은 SysVinit이 아니라 Upstart였습니다(호환 모드로 SysV 스크립트도 돌렸습니다).

Upstart의 출발점은 “부팅은 단순한 순서가 아니라 일련의 사건들이다"라는 발상입니다. USB가 꽂히고 네트워크가 올라오며 디스크가 마운트되는 등, 이벤트가 발생할 때마다 그에 맞는 잡이 트리거되도록 모델을 짰습니다.

설정은 /etc/init/*.conf에 한 잡씩 둡니다.

description "Hello Web"

start on runlevel [2345]
stop on runlevel [!2345]

respawn
respawn limit 10 5

exec /usr/local/bin/hello-web --port 8080

respawn 한 줄로 자동 재시작이 해결되고, start on filesystem and net-device-up 식으로 이벤트 조합도 가능했습니다. SysVinit에 비하면 진짜 진보였습니다.

채택도 빨랐습니다. **Ubuntu 9.10 “Karmic Koala” (2009-10)**에서 SysV 호환 레이어 없이 native Upstart 부팅을 기본화했고, **RHEL 6 (2010-11)**이 정식 채택했습니다. Google의 ChromeOS와 일부 Fedora 릴리스도 한때 Upstart를 썼습니다.

Upstart가 짧게 살았던 이유

그런데 RHEL 6 이후 Upstart의 채택은 거기서 멈춥니다. 2010년대 초반 새로 등장한 systemd로 흐름이 갈아탔고 Ubuntu 본진마저 결국 같은 길을 갔는데, 단순히 “더 좋은 게 나왔다"로는 설명이 부족합니다.

가장 자주 꼽히는 건 Canonical의 CLA 정책입니다. Upstart 코드에 기여하려면 Canonical이 요구하는 Contributor License Agreement에 동의해야 했는데, 받은 코드를 상용으로 재배포할 권리를 회사에 양도하는 형태라 다른 진영이 부담스러워했습니다. 같은 시기 Linux 커널을 비롯한 여러 핵심 프로젝트가 CLA 없이 기여를 받던 흐름과 대조됐습니다.

기술적 한계도 겹쳤습니다. “A 이후 B” 같은 단순 의존이 이벤트 모델로는 어색했던 반면, systemd가 들고 나온 의존성 그래프와 Wants= / Requires= / After= 선언이 더 자연스럽다는 평가가 빠르게 자리 잡았습니다. 자식 프로세스 추적도 Upstart는 SysVinit과 비슷한 한계 안에 머물러, cgroup을 fork-bomb에도 끄떡없는 추적 수단으로 쓴 systemd에 비하면 약했습니다. 여기에 Fedora와 RHEL이 systemd를 적극적으로 밀면서 큰 생태계 플레이어 하나가 Upstart 진영에서 빠져나갔고, RHEL 6의 Upstart는 단명한 뒤 RHEL 7부터 systemd로 갔습니다.

결정타는 Debian과 Ubuntu가 차례로 systemd로 넘어간 시점입니다. 그 부분은 뒤에서 따로 정리합니다.

3세대: systemd (2010~)

2010년 4월, Red Hat의 Lennart Poettering(레나르트 푀터링)과 Kay Sievers(카이 지버스)가 발표한 글 한 편(“Rethinking PID 1”)으로 등장합니다. 핵심 아이디어는 세 가지였습니다.

첫째는 의존성 기반 병렬 부팅입니다. 유닛(*.service, *.socket, *.target 등)이 의존성을 명시하고, 의존성이 풀린 것부터 병렬로 출발합니다. 둘째는 소켓 활성화(socket activation)로, 데몬을 미리 띄우는 대신 systemd가 먼저 listen 소켓을 열어두고 첫 연결이 들어올 때 데몬을 깨웁니다. macOS의 launchd에서 영감을 받은 모델입니다. 셋째는 cgroup 기반 프로세스 추적입니다. 서비스가 fork한 자식과 손자까지 cgroup에 묶여 정확히 추적/정리되므로 PID 파일 위조나 더블 fork로 도망갈 수 없습니다.

이 세 가지가 동시에 풀린 게 결정적이었습니다. 이전 세대가 풀지 못한 한계를 한꺼번에 정리했고, 부팅 시간 단축이라는 가시적 효과도 따라왔습니다.

다만 systemd는 init만이 아니라 logind, journald, networkd, resolved, timedated 등 시스템 영역의 여러 컴포넌트를 흡수해 갔고, “Unix 철학과 어긋난다"는 비판도 같은 시기에 나왔습니다. 이 글은 그 논쟁에 한 발 들이지 않고, 채택 흐름만 따라갑니다.

systemd 채택 연표

발표 시점부터 메이저 배포판들의 채택 시점을 연도순으로 보면 흐름이 분명해집니다.

연/월사건
2010-04systemd 첫 발표 (Lennart Poettering, Kay Sievers)
2011-05Fedora 15: 메이저 배포판 중 첫 systemd 기본 채택
2011-11openSUSE 12.1: systemd 기본
2012-05Mageia 2: systemd 기본
2012-10Arch Linux: SysVinit에서 systemd로 전환
2013CoreOS: 출범부터 systemd가 핵심 (컨테이너 호스트 OS)
2014-06RHEL 7 / CentOS 7: 엔터프라이즈 표준이 바뀐 분기점
2014-11Debian Technical Committee 투표: systemd를 Jessie의 기본 init으로 결정
2014-11-27Devuan 분기 발표: “Veteran Unix Admins” 명의
2015-04Debian 8 “Jessie” 정식 출시 (systemd 기본)
2015-04-23Ubuntu 15.04 “Vivid Vervet”: Upstart에서 systemd로
2017-05-25Devuan 1.0 “Jessie”: Debian 기반, systemd 없는 첫 안정판

4년 (2011~2015) 만에 Fedora, RHEL, Debian, Ubuntu, openSUSE, Arch가 모두 systemd로 정렬됩니다. 이 정도 속도로 init 같은 핵심 컴포넌트가 통일된 적은 리눅스 역사에 거의 없습니다.

Debian의 투표와 Devuan 분기

Debian은 의사결정에 시간이 오래 걸리는 프로젝트입니다. systemd 채택도 예외가 아니어서 2013년 후반부터 Technical Committee 안에서 격론이 오갔고, 결국 2014년 11월 표결로 Debian 8 “Jessie"의 기본 init은 systemd로 결정됩니다.

표결 자체는 그것대로 정리됐지만, 같은 결의에 끼어 있던 또 다른 항목, 즉 “패키지가 systemd 의존성을 강제로 걸 수 있느냐"가 분기를 불렀습니다. 결의는 “다른 init 시스템 지원이 권장되지만 의무는 아니다(recommended, but not mandatory)“로 나왔습니다. 패키지가 systemd 외에 안 돌게 만들어도 막지 않는다는 의미였습니다.

이 결과에 반발한 일부 Debian 사용자/개발자가 “Veteran Unix Admins” 이름으로 2014년 11월 27일 Devuan 분기를 발표합니다. 약 2년 반의 패키지 감사/수정 끝에 2017년 5월 25일 Devuan 1.0 “Jessie"가 나왔습니다. Debian 8을 베이스로 systemd 훅을 모두 들어내고 SysVinit(또는 OpenRC)을 기본 init으로 하는 버전입니다.

Devuan은 이후로도 Debian을 한 단계씩 따라가며 출시를 이어가고 있습니다. 규모는 작지만 “systemd 없이도 Debian 생태계를 쓰고 싶다"는 수요에 답하는 진영으로 살아남았습니다.

Ubuntu가 자존심을 접은 결정

Ubuntu에게 init은 단순한 부품이 아니었습니다. Upstart는 Canonical의 자체 프로젝트였고, 거의 10년간 Ubuntu의 기본 init이었습니다. 그런데 Debian이 systemd로 결정한 직후, Mark Shuttleworth(마크 셔틀워스)는 “Ubuntu도 upstream(Debian)과 보조를 맞추겠다"고 발표합니다.

마이그레이션은 비교적 부드러웠습니다. Ubuntu 15.04 “Vivid Vervet”(2015-04-23)에서 기본 init이 systemd로 전환됐고, Ubuntu Touch(모바일)만 예외였습니다. 이후 15.04부터 16.10까지는 부팅 시 GRUB에서 Upstart와 systemd를 고를 수 있는 듀얼 부팅 기간을 유지했는데, 회귀가 생겼을 때 도망갈 길을 일정 기간 열어둔 운영적 선택이었습니다. 16.10 이후 Upstart 옵션이 제거되면서 이때부터 Ubuntu는 완전히 systemd 단독으로 갔습니다.

Canonical 입장에서는 자기 프로젝트를 접고 경쟁 프로젝트를 받아들인 결정이었지만, 그 무렵엔 systemd가 사실상 표준이 된 상태였고 Debian과 다른 init을 유지하는 비용이 더 커졌습니다.

systemd를 안 쓰는 배포판들

2026년 현재도 systemd가 아닌 init을 기본으로 쓰는 배포판이 남아 있습니다. 컨테이너 베이스 이미지나 임베디드, 보수적 운영을 위한 선택지로 의외로 자주 등장합니다.

배포판기본 init비고
Alpine LinuxOpenRC컨테이너 베이스 이미지 점유율이 높음. musl + busybox + OpenRC 조합
Void Linuxrunit단순함과 빠른 부팅이 강점
GentooOpenRC (기본) / systemd 옵션profile 선택으로 둘 다 사용 가능
DevuanSysVinit / OpenRCDebian 8 분기 후 독자 노선
SlackwareBSD-style init15.0 (2022) 시점에도 SysV가 아닌 BSD 스타일 유지
Artix LinuxOpenRC / runit / s6 / dinitArch 기반의 systemd-free 분기

여기서 중요한 점이 하나 있습니다. Alpine은 컨테이너 이미지 시장에서 표준급 점유율을 가지고 있습니다. “제 노트북은 Ubuntu고 systemd만 만져봤다"고 해도, 컨테이너에 FROM alpine:... 한 줄을 넣는 순간 OpenRC 기반 시스템과 만납니다. 다만 컨테이너 안에서는 init이 거의 의미를 갖지 않는다는 별도의 이슈가 있는데, 그건 마지막에서 다시 봅니다.

같은 작업, 세 가지 표현

세 init 시스템의 차이를 가장 빨리 느끼는 방법은 같은 서비스를 세 가지 형식으로 옆에 두고 보는 것입니다. hello-web이라는 가상의 HTTP 서버를 부팅 시 자동 기동하고 죽으면 자동 재시작하도록 등록한다고 해보겠습니다.

SysVinit: /etc/init.d/hello-web

#!/bin/sh
### BEGIN INIT INFO
# Provides:          hello-web
# Required-Start:    $network $remote_fs
# Required-Stop:     $network $remote_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: hello web server
### END INIT INFO

DAEMON=/usr/local/bin/hello-web
PIDFILE=/var/run/hello-web.pid

case "$1" in
  start)
    start-stop-daemon --start --background \
      --make-pidfile --pidfile $PIDFILE \
      --exec $DAEMON
    ;;
  stop)
    start-stop-daemon --stop --pidfile $PIDFILE
    rm -f $PIDFILE
    ;;
  restart)
    $0 stop; sleep 1; $0 start
    ;;
  status)
    [ -f $PIDFILE ] && kill -0 $(cat $PIDFILE) 2>/dev/null \
      && echo "running" || echo "stopped"
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|status}"; exit 1
    ;;
esac

그 후 update-rc.d hello-web defaults(Debian 계열) 또는 chkconfig hello-web on(RHEL 계열)으로 런레벨 링크를 만들어야 합니다. 자동 재시작은 별도 도구가 필요합니다.

Upstart: /etc/init/hello-web.conf

description "hello web server"

start on runlevel [2345]
stop on runlevel [!2345]

respawn
respawn limit 10 5

exec /usr/local/bin/hello-web --port 8080

스크립트가 아니라 선언문입니다. start-stop-daemon, PID 파일, status 분기, 런레벨 링크가 전부 사라졌습니다. respawn 한 줄로 자동 재시작도 끝납니다.

systemd: /etc/systemd/system/hello-web.service

[Unit]
Description=hello web server
After=network.target

[Service]
ExecStart=/usr/local/bin/hello-web --port 8080
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

등록은 systemctl enable --now hello-web 한 줄. 의존성을 After=로 명시하고, 재시작 정책도 Restart=on-failure로 명확합니다. 자식 프로세스 추적은 cgroup이 알아서 해줍니다.

같은 의도가 41줄에서 9줄, 다시 12줄로 짧아집니다. 줄어든 만큼 init이 책임지는 부분과 사용자가 짜야 하는 부분의 경계가 옮겨갔다는 뜻입니다.

컨테이너 시대의 init: PID 1 문제

여기까지가 “호스트 OS의 init” 이야기였습니다. 그런데 컨테이너 시대로 들어오면 init의 의미가 한 번 더 뒤집힙니다.

도커 컨테이너 안에서 PID 1은 실제 init 시스템 대신 사용자가 실행한 프로세스입니다. CMD ["node", "server.js"]면 node가 PID 1이 됩니다. 그런데 PID 1에는 두 가지 특별한 책임이 있습니다.

하나는 좀비 자식 프로세스 수확입니다. 자식이 죽으면 부모가 wait()으로 거둬야 좀비가 정리되는데, PID 1이 이걸 안 하면 좀비가 영구히 쌓입니다. 다른 하나는 시그널 처리입니다. 커널은 PID 1에게 기본 시그널 핸들러를 붙여주지 않아서, 명시적으로 처리하지 않으면 SIGTERM이나 SIGINT가 무시됩니다.

대부분의 애플리케이션은 이 두 가지를 신경 쓰고 만들어지지 않았습니다. 그래서 컨테이너 생태계에는 경량 init들이 등장했습니다.

tini는 Docker가 --init 플래그로 채택한 사실상 표준입니다. dumb-init은 Yelp가 만든 alternatives이고, s6-overlay는 컨테이너 안에서 멀티 프로세스를 다룰 때 씁니다.

Kubernetes는 이 문제를 한 단계 더 위에서 다룹니다. Pod 종료 시 컨테이너 PID 1에 SIGTERM을 보내고 grace period 후 SIGKILL로 가는데, 앱이 SIGTERM을 안 잡으면 매번 강제 종료가 발생합니다. (이 흐름은 SIGINT, SIGTERM, SIGHUP, SIGKILL: 쿠버네티스 시대의 유닉스 시그널에서 깊게 다뤘습니다.)

호스트의 init은 의존성과 부팅 속도를 고민하지만, 컨테이너의 PID 1은 시그널 전파와 좀비 수확이라는 더 원초적인 책임으로 돌아갑니다.

한 줄로

SysVinit의 30년은 단순함이 호환성을 만든 시대였고, systemd의 5년은 의존성과 cgroup이 표준을 만든 시대였어요. 그 사이의 Upstart는 좋은 아이디어 한 가지로 잠깐 빛났지만 큰 생태계의 흐름을 못 이긴 사례로 남았고, PID 1 자리에 무엇이 앉느냐는 결국 그 시대 운영체제가 무엇을 가장 중요하게 여기는지를 비추는 거울이에요.


→ 다음 편: systemd 유닛이라는 언어: 11종을 한 바퀴. 이 글에서 살짝만 비춘 유닛 11종이 cron, fstab, inetd, autofs, inotify 같은 옛 도구를 어떻게 흡수했는지 한 바퀴 돕니다.

참고