SysVinit (init.d) 종합 가이드
Unix System V에서 유래한 전통적 init 시스템인 SysVinit의 설계 철학, /etc/inittab 런레벨 구조, /etc/init.d/ 스크립트 작성법(LSB 헤더), rcN.d 심볼릭 링크 체계, 서비스 관리 도구, 부팅 시퀀스, 대안 init 시스템 비교, 현대 리눅스에서의 활용까지 체계적으로 정리합니다.
/sbin/init)를 실행하는 과정을 이해해야 SysVinit의 역할을 파악할 수 있습니다.
핵심 요약
- PID 1 — 커널이 실행하는 최초 사용자 프로세스,
/sbin/init이 SysVinit의 핵심 바이너리 - 런레벨(Runlevel) — 시스템 상태를 0~6과 S로 구분, 각 런레벨마다 실행할 서비스 집합이 다름
- /etc/inittab — init 프로세스의 동작을 정의하는 핵심 설정 파일
- /etc/init.d/ — 서비스 시작/정지 셸 스크립트가 모이는 디렉터리
- rcN.d 심볼릭 링크 — 런레벨별로 어떤 서비스를 시작(S)하고 정지(K)할지 결정하는 링크 체계
단계별 이해
- PID 1과 init의 관계 이해
커널이/sbin/init을 실행하는 과정과 PID 1의 특수한 책임을 파악합니다. - 런레벨과 inittab 학습
시스템 상태(런레벨)가 어떻게 정의되고 전환되는지 이해합니다. - init.d 스크립트 구조 파악
서비스 제어 스크립트의 표준 인터페이스와 LSB 헤더를 학습합니다. - rcN.d 링크 체계 이해
런레벨별 서비스 시작/정지 순서가 결정되는 메커니즘을 파악합니다.
SysVinit 개요 및 설계 철학
SysVinit(System V init)은 AT&T Unix System V에서 유래한 init 시스템으로, 리눅스에서 가장 오랫동안 사용된 전통적 초기화 방식입니다. 커널이 초기화를 완료한 후 PID 1로 실행하는 최초의 사용자 공간 프로세스이며, 이 전환 과정의 상세 내용은 부팅 과정을 참고하세요. 1983년 System V Release 1(SVR1)에서 도입된 이후, BSD init과 함께 유닉스 계열 운영 체제의 두 가지 주요 init 모델 중 하나로 자리잡았습니다. 리눅스는 초기부터 SysVinit을 채택했으며, Miquel van Smoorenburg가 리눅스용 SysVinit 구현(sysvinit 패키지)을 작성하여 2010년대 초반까지 대부분의 배포판에서 기본 init 시스템으로 사용되었습니다.
SysVinit의 핵심 설계 원칙
- 단순성 — init 프로세스 자체는 최소한의 기능만 담당합니다.
/etc/inittab을 파싱하고 지정된 프로세스를 실행하는 것이 전부입니다. 서비스 관리 로직은 셸 스크립트에 위임합니다. - 셸 스크립트 기반 — 모든 서비스 제어가
/etc/init.d/의 셸 스크립트로 이루어집니다. 관리자가 스크립트를 직접 읽고 수정할 수 있어 투명성이 높습니다. - 순차 실행 — 서비스를 번호 순서대로 하나씩 실행합니다. 이전 서비스가 완료(또는 백그라운드 전환)되어야 다음 서비스가 시작됩니다.
- 런레벨 기반 상태 관리 — 시스템 상태를 런레벨(0~6, S)로 구분하고, 런레벨마다 실행할 서비스 집합을 정의합니다.
- PID 파일 기반 프로세스 추적 — 서비스 프로세스의 PID를
/var/run/에 파일로 기록하여 추적합니다.
Unix init 시스템의 두 가지 전통
| 항목 | BSD init | System V init |
|---|---|---|
| 설정 파일 | /etc/rc (단일 스크립트) | /etc/inittab + /etc/init.d/ |
| 상태 관리 | 단일 모드/다중 모드 2가지 | 런레벨 0~6, S |
| 서비스 추가 | /etc/rc.local에 직접 추가 | /etc/init.d/에 스크립트 + 심볼릭 링크 |
| 채택 | FreeBSD, OpenBSD, NetBSD | 리눅스, Solaris, HP-UX, AIX |
| 장점 | 극도의 단순성 | 개별 서비스 제어 가능 |
배포판 채택 역사
- 1992~2006 — 대부분의 리눅스 배포판이 SysVinit을 기본으로 사용
- 2006 — Ubuntu 6.10이 Upstart를 도입 (이벤트 기반 init)
- 2010 — Fedora 15가 systemd를 최초 도입
- 2011~2012 — openSUSE, Arch Linux가 systemd로 전환
- 2014 — Debian이 systemd 채택 결정 (논쟁적인 투표)
- 2015 — Ubuntu가 Upstart에서 systemd로 전환 (15.04)
- 현재 — Devuan(systemd-free Debian), Slackware, 일부 임베디드 배포판에서 계속 사용
SysVinit 소스 코드 구조
리눅스용 SysVinit 패키지(sysvinit)는 비교적 작은 규모의 C 프로그램입니다. 핵심 구성 요소는 다음과 같습니다.
| 바이너리 | 역할 |
|---|---|
/sbin/init | PID 1 프로세스, inittab 파싱 및 프로세스 관리 |
/sbin/telinit | 런레벨 전환 명령 (init에 시그널 전송) |
/sbin/shutdown | 안전한 시스템 종료/재부팅 |
/sbin/halt | 시스템 정지 |
/sbin/reboot | 시스템 재부팅 |
/sbin/runlevel | 현재/이전 런레벨 조회 |
/bin/login | 사용자 로그인 처리 (선택적) |
/sbin/sulogin | 단일 사용자 모드 로그인 |
/* SysVinit init.c — 핵심 동작 흐름 (의사 코드) */
int main(int argc, char **argv)
{
/* PID 1으로 실행 확인 */
if (getpid() != 1)
exec_telinit(argc, argv); /* PID 1이 아니면 telinit으로 동작 */
/* 콘솔 초기화, /dev/console 열기 */
console_init();
/* /etc/inittab 파싱 */
read_inittab();
/* sysinit 항목 실행 (파일시스템 마운트 등) */
boot_transitions();
/* 메인 루프: 프로세스 감시 + 런레벨 전환 처리 */
for (;;) {
pid = waitpid(-1, &status, 0);
check_init_fifo(); /* telinit 명령 수신 */
process_signals(); /* SIGCHLD, SIGHUP 등 */
start_if_needed(); /* respawn 항목 재시작 */
}
}
내부 데이터 구조 (CHILD 구조체)
SysVinit의 init 프로세스는 내부적으로 CHILD 구조체의 단일 연결 리스트로 모든 자식 프로세스를 관리합니다. /etc/inittab의 각 항목이 하나의 CHILD 노드에 대응됩니다.
/* SysVinit init.h — CHILD 구조체 (핵심 자료구조) */
typedef struct _child_ {
struct _child_ *next; /* 다음 CHILD 노드 (연결 리스트) */
int pid; /* 현재 실행 중인 프로세스의 PID (0=미실행) */
char id[8]; /* inittab의 id 필드 (1~4자) */
short rlevel; /* 적용 런레벨 비트마스크 */
short action; /* 액션 타입 (RESPAWN, WAIT, ONCE 등) */
short flags; /* 상태 플래그 (RUNNING, WAITING, XECUTED 등) */
char process[128]; /* 실행할 명령어 문자열 */
time_t tm; /* 마지막 실행/종료 시각 (respawn 보호용) */
int count; /* respawn 재시작 횟수 (빠른 재시작 감지) */
} CHILD;
/* 전역 연결 리스트 헤드 */
CHILD *family = NULL; /* inittab의 모든 항목 */
/* 액션 타입 상수 */
#define RESPAWN 1 /* 종료 시 자동 재시작 */
#define WAIT 2 /* 실행 후 완료 대기 */
#define ONCE 3 /* 한 번만 실행 */
#define BOOT 4 /* 부팅 시에만 실행 */
#define BOOTWAIT 5 /* 부팅 시 실행 + 대기 */
#define SYSINIT 6 /* 최초 시스템 초기화 */
#define CTRLALTDEL 7 /* Ctrl+Alt+Del 이벤트 */
/* 상태 플래그 */
#define RUNNING 0x01 /* 프로세스 실행 중 */
#define WAITING 0x02 /* 완료 대기 중 */
#define XECUTED 0x04 /* 이미 실행됨 (once/wait) */
#define DEMAND 0x08 /* 재실행 요청됨 */
/etc/inittab이 변경되어도(telinit q) 기존 노드를 삭제하지 않고 새 노드를 추가하는 방식으로 동작합니다. 삭제된 inittab 항목의 CHILD 노드는 플래그만 변경되어 다음 런레벨 전환 시 정리됩니다. 이 설계 덕분에 실행 중인 프로세스가 inittab 편집에 영향받지 않습니다.
/etc/inittab 구조와 런레벨(Runlevel)
/etc/inittab은 SysVinit의 핵심 설정 파일로, init 프로세스가 부팅 시와 런레벨 전환 시 어떤 프로세스를 실행할지 정의합니다. 각 항목은 콜론(:)으로 구분된 4개 필드로 구성됩니다.
# /etc/inittab 항목 형식
id:runlevels:action:process
# 필드 설명:
# id — 1~4자의 고유 식별자
# runlevels — 이 항목이 적용되는 런레벨 (예: 2345, 또는 비워두면 모든 런레벨)
# action — init이 이 항목을 처리하는 방식
# process — 실행할 명령 또는 스크립트
런레벨(Runlevel) 체계
런레벨은 시스템의 동작 상태를 숫자로 구분하는 개념입니다. 각 런레벨은 실행되어야 할 서비스의 집합을 정의합니다.
| 런레벨 | 의미 | 설명 | systemd 등가 |
|---|---|---|---|
0 | Halt | 시스템 종료 | poweroff.target |
1 / S | Single-user | 단일 사용자 모드, 네트워크 없음, 루트 셸만 활성 | rescue.target |
2 | Multi-user | 다중 사용자 (Debian: 기본값, NFS 없음) | multi-user.target |
3 | Multi-user + Network | 다중 사용자 + 네트워크 (Red Hat: 텍스트 기본값) | multi-user.target |
4 | 미사용 | 관리자가 자유롭게 정의 가능 | — |
5 | Graphical | 다중 사용자 + 그래픽 (Red Hat: GUI 기본값) | graphical.target |
6 | Reboot | 시스템 재부팅 | reboot.target |
액션(Action) 타입
| 액션 | 설명 |
|---|---|
initdefault | 기본 런레벨 지정 (process 필드 무시) |
sysinit | 부팅 초기에 실행, 모든 boot/bootwait 항목 전에 실행 |
boot | 부팅 시 실행 (런레벨 무시), 완료를 기다리지 않음 |
bootwait | 부팅 시 실행, 완료를 기다림 |
wait | 해당 런레벨 진입 시 실행, 완료를 기다림 |
once | 해당 런레벨 진입 시 한 번 실행, 완료를 기다리지 않음 |
respawn | 프로세스 종료 시 자동 재시작 (getty 등에 사용) |
ctrlaltdel | Ctrl+Alt+Del 키 조합 시 실행 |
powerfail | UPS 전원 실패 시그널(SIGPWR) 수신 시 실행 |
powerwait | powerfail과 동일하나 완료를 기다림 |
powerokwait | 전원 복구 시 실행 |
kbrequest | 키보드 특수 조합 시 실행 |
respawn 항목이 짧은 시간 내에 반복적으로 종료되면, init은 "respawning too fast" 경고를 출력하고 해당 항목의 재시작을 5분간 중단합니다. 이는 설정 오류나 바이너리 누락으로 인한 무한 재시작 루프를 방지하는 안전장치입니다. /var/log/syslog에서 "id \"XX\" respawning too fast: disabled for 5 minutes" 메시지를 확인하세요.
systemd에서의 등가 개념(Target 유닛)은 systemd 부팅 순서를 참고하세요.
inittab 파싱 흐름
init 프로세스가 /etc/inittab을 파싱하는 과정은 액션 타입에 따라 다른 처리 경로를 따릅니다.
실전 inittab 예제
# /etc/inittab — 전형적인 Debian 스타일
# 기본 런레벨 (2 = 다중 사용자)
id:2:initdefault:
# 시스템 초기화 스크립트 (부팅 최초 실행)
si::sysinit:/etc/init.d/rcS
# 런레벨별 rc 스크립트 실행
l0:0:wait:/etc/init.d/rc 0
l1:1:wait:/etc/init.d/rc 1
l2:2:wait:/etc/init.d/rc 2
l3:3:wait:/etc/init.d/rc 3
l4:4:wait:/etc/init.d/rc 4
l5:5:wait:/etc/init.d/rc 5
l6:6:wait:/etc/init.d/rc 6
# Ctrl+Alt+Del 처리
ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now
# UPS 전원 실패 처리
pf::powerwait:/etc/init.d/powerfail start
pn::powerfailnow:/sbin/shutdown -f -h +2 "Power failure; system shutdown"
po::powerokwait:/sbin/shutdown -c "Power restored"
# 가상 콘솔에 getty 생성 (respawn: 종료 시 자동 재시작)
1:2345:respawn:/sbin/getty 38400 tty1
2:23:respawn:/sbin/getty 38400 tty2
3:23:respawn:/sbin/getty 38400 tty3
4:23:respawn:/sbin/getty 38400 tty4
5:23:respawn:/sbin/getty 38400 tty5
6:23:respawn:/sbin/getty 38400 tty6
# 시리얼 콘솔 (서버/임베디드 환경)
T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100
# /etc/inittab — Red Hat/CentOS 스타일
# 기본 런레벨 (3 = 텍스트 다중 사용자, 5 = GUI)
id:3:initdefault:
# 시스템 초기화
si::sysinit:/etc/rc.d/rc.sysinit
# 런레벨별 실행
l0:0:wait:/etc/rc.d/rc 0
l1:1:wait:/etc/rc.d/rc 1
l2:2:wait:/etc/rc.d/rc 2
l3:3:wait:/etc/rc.d/rc 3
l4:4:wait:/etc/rc.d/rc 4
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6
# Ctrl+Alt+Del
ca::ctrlaltdel:/sbin/shutdown -t3 -r now
# 가상 콘솔
1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6
# 런레벨 5에서 X11 디스플레이 매니저
x:5:respawn:/etc/X11/prefdm -nodaemon
telinit 명령
telinit은 실행 중인 init 프로세스에 런레벨 전환을 요청하는 명령입니다. 실제로는 init 프로세스와 동일한 바이너리이며, PID 1이 아닌 경우 telinit 모드로 동작합니다.
# 런레벨 전환
sudo telinit 3 # 런레벨 3(텍스트 모드)으로 전환
sudo telinit 5 # 런레벨 5(GUI 모드)으로 전환
sudo telinit 1 # 단일 사용자 모드로 전환
# inittab 재읽기 (HUP 시그널)
sudo telinit q
# 현재/이전 런레벨 확인
runlevel
# N 3 (이전: 없음(N), 현재: 3)
who -r
# run-level 3 2026-03-23 10:30
/dev/initctl FIFO 통신 프로토콜
telinit은 /dev/initctl 이라는 명명된 파이프(Named Pipe, FIFO)를 통해 PID 1(init)과 통신합니다. 이 FIFO는 init이 부팅 시 생성하며, telinit은 struct init_request 구조체를 FIFO에 기록하여 명령을 전달합니다.
/* initreq.h — telinit↔init 통신 프로토콜 */
#define INIT_MAGIC 0x03091969 /* 매직 넘버 (Miquel van Smoorenburg 생일) */
#define INIT_CMD_START 0 /* 사용 안 함 */
#define INIT_CMD_RUNLVL 1 /* 런레벨 전환 */
#define INIT_CMD_POWERFAIL 2 /* UPS 전원 실패 */
#define INIT_CMD_POWEROK 3 /* UPS 전원 복구 */
#define INIT_CMD_SETENV 6 /* 환경 변수 설정 */
struct init_request {
int magic; /* INIT_MAGIC (검증용) */
int cmd; /* 명령 타입 (INIT_CMD_*) */
int runlevel; /* 전환할 런레벨 ('0'~'6', 'S', 'Q') */
int sleeptime; /* 대기 시간 (shutdown에서 사용) */
char data[368]; /* 추가 데이터 (SETENV 시 환경 변수) */
};
/* 구조체 크기: 384바이트 (고정) */
/dev/initctl FIFO의 소유자는 root이고 권한은 600(-rw-------)입니다. 따라서 root 권한 없이는 init에 명령을 전달할 수 없습니다. 하지만 magic 번호가 하드코딩되어 있고 인증 메커니즘이 없으므로, root 권한을 획득한 공격자는 임의의 런레벨 전환을 요청할 수 있습니다. 이는 SysVinit이 설계된 1990년대에는 크게 문제되지 않았으나, 현대적 보안 관점에서는 systemd의 D-Bus 기반 인증이 더 안전합니다.
/etc/init.d/ 스크립트 구조
/etc/init.d/ 디렉터리에는 개별 서비스를 제어하는 셸 스크립트가 위치합니다. 각 스크립트는 표준화된 인터페이스(start, stop, restart, status 등)를 제공해야 하며, LSB(Linux Standard Base) 규격에 따라 헤더와 종료 코드를 준수해야 합니다.
LSB 헤더(LSB Init Script Header)
LSB 헤더는 스크립트 상단에 특수 주석으로 작성하며, 의존성 관리 도구(insserv, update-rc.d)가 서비스 간 시작 순서를 결정하는 데 사용합니다.
#!/bin/sh
### BEGIN INIT INFO
# Provides: myservice
# Required-Start: $remote_fs $syslog $network
# Required-Stop: $remote_fs $syslog $network
# Should-Start: $named
# Should-Stop: $named
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: My Application Server
# Description: Starts the My Application Server daemon
# which provides REST API endpoints.
### END INIT INFO
| 필드 | 설명 |
|---|---|
Provides | 이 스크립트가 제공하는 시설(Facility) 이름 |
Required-Start | 이 서비스 시작 전에 반드시 시작되어야 하는 시설 |
Required-Stop | 이 서비스 정지 후에 정지되어야 하는 시설 |
Should-Start | 가능하면 먼저 시작되어야 하는 시설 (선택적) |
Should-Stop | 가능하면 나중에 정지되어야 하는 시설 (선택적) |
Default-Start | 기본으로 시작할 런레벨 (update-rc.d defaults에 사용) |
Default-Stop | 기본으로 정지할 런레벨 |
Short-Description | 한 줄 서비스 설명 |
Description | 여러 줄 상세 설명 (계속행은 # + 공백으로 시작) |
insserv/update-rc.d가 의존성을 파악할 수 없어 순서 번호를 임의로 할당합니다. 결과적으로 네트워크가 활성화되기 전에 네트워크 서비스가 시작되는 등 의존성 위반이 발생합니다. 반드시 모든 init.d 스크립트에 LSB 헤더를 포함하세요.
LSB 가상 시설(Virtual Facilities)
가상 시설은 $ 접두사가 붙은 특수 이름으로, 구체적 서비스가 아니라 시스템 상태를 나타냅니다.
| 시설 | 의미 | 제공하는 서비스 예 |
|---|---|---|
$local_fs | 모든 로컬 파일시스템이 마운트됨 | checkroot, mountall |
$remote_fs | 모든 원격 파일시스템(NFS 등)이 마운트됨 | mountnfs |
$syslog | 시스템 로그 데몬이 동작 중 | rsyslog, syslog-ng |
$network | 저수준 네트워크가 활성화됨 | networking |
$named | DNS 이름 해석이 가능 | bind9, dnsmasq |
$portmap | SunRPC 포트매퍼(Portmapper)가 활성 | rpcbind |
$time | 시스템 시간이 정확하게 설정됨 | ntp, chrony |
$all | 모든 서비스가 시작된 후 (Required-Start에만 사용) | — |
LSB 종료 코드
| 코드 | 의미 | 사용 상황 |
|---|---|---|
0 | 성공(Success) | 명령이 정상적으로 완료 |
1 | 일반 오류(Generic Error) | 명시되지 않은 오류 |
2 | 잘못된 인자(Invalid Arguments) | 사용법 오류 |
3 | 미구현(Not Implemented) | 해당 액션을 지원하지 않음 |
4 | 권한 부족(Insufficient Privilege) | root 권한 필요 |
5 | 미설치(Not Installed) | 서비스 바이너리가 없음 |
6 | 미설정(Not Configured) | 설정 파일이 없거나 잘못됨 |
7 | 미실행(Not Running) | status 명령에서 서비스가 동작하지 않을 때 |
PID 파일과 start-stop-daemon
SysVinit에서 서비스 프로세스를 추적하는 핵심 메커니즘은 PID 파일입니다. 서비스가 시작되면 /var/run/service.pid에 자신의 PID를 기록하고, 정지 시 이 파일의 PID를 읽어 프로세스를 종료합니다.
# PID 파일 기반 서비스 관리 (수동 방식)
start() {
/usr/sbin/mydaemon --config /etc/mydaemon.conf &
echo $! > /var/run/mydaemon.pid
}
stop() {
if [ -f /var/run/mydaemon.pid ]; then
kill $(cat /var/run/mydaemon.pid)
rm -f /var/run/mydaemon.pid
fi
}
# start-stop-daemon 사용 (Debian 권장 방식)
start() {
start-stop-daemon --start --quiet \
--pidfile /var/run/mydaemon.pid \
--exec /usr/sbin/mydaemon \
--make-pidfile --background \
-- --config /etc/mydaemon.conf
}
stop() {
start-stop-daemon --stop --quiet \
--pidfile /var/run/mydaemon.pid \
--retry=TERM/30/KILL/5
}
서비스 생명주기와 보안
init.d 스크립트로 관리되는 서비스의 생명주기와 보안 고려사항을 정리합니다.
보안 고려사항
SysVinit 환경에서는 systemd의 ProtectSystem, NoNewPrivileges 같은 자동 보안 강화 기능이 없습니다. 따라서 init.d 스크립트 작성 시 수동으로 보안을 적용해야 합니다.
# 보안 강화 패턴
# 1. 비특권 사용자로 실행
start-stop-daemon --start --chuid myapp:myapp \
--exec /usr/sbin/myapp
# 2. 리소스 제한 (ulimit)
ulimit -n 65536 # 파일 디스크립터 제한
ulimit -u 1024 # 프로세스 수 제한
ulimit -v 2097152 # 가상 메모리 2GB 제한
# 3. umask 설정 (파일 생성 권한 제한)
umask 027 # 그룹 읽기, 기타 접근 불가
# 4. chroot 격리 (선택적)
start-stop-daemon --start --chroot /srv/myapp \
--exec /usr/sbin/myapp
로그 로테이션 연동
# /etc/logrotate.d/mydaemon
/var/log/mydaemon/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 mydaemon mydaemon
postrotate
# init.d 스크립트의 reload를 호출하여 HUP 시그널 전송
[ -f /var/run/mydaemon.pid ] && kill -HUP $(cat /var/run/mydaemon.pid) 2>/dev/null || true
endscript
}
/etc/rcN.d/ 심볼릭 링크 체계
SysVinit의 서비스 시작/정지 순서는 /etc/rcN.d/ 디렉터리의 심볼릭 링크로 결정됩니다. 여기서 N은 런레벨 번호(0~6)이고, rcS.d/는 시스템 초기화 단계입니다. 각 링크는 /etc/init.d/의 실제 스크립트를 가리킵니다.
링크 명명 규칙
# 형식: [S|K][순서번호][서비스이름] -> ../init.d/서비스이름
/etc/rc3.d/
├── K01gdm -> ../init.d/gdm # 런레벨 3에서 gdm 정지 (순서 01)
├── S10sysklogd -> ../init.d/sysklogd # syslog 시작 (순서 10)
├── S20ssh -> ../init.d/ssh # SSH 시작 (순서 20)
├── S40networking -> ../init.d/networking # 네트워크 시작 (순서 40)
├── S55cron -> ../init.d/cron # cron 시작 (순서 55)
├── S91apache2 -> ../init.d/apache2 # Apache 시작 (순서 91)
└── S99rc.local -> ../init.d/rc.local # 사용자 커스텀 (마지막)
| 접두사 | 의미 | 실행 방법 |
|---|---|---|
S (Start) | 해당 런레벨 진입 시 서비스 시작 | /etc/init.d/서비스 start |
K (Kill) | 해당 런레벨 진입 시 서비스 정지 | /etc/init.d/서비스 stop |
실행 순서 규칙
- K 스크립트 먼저 실행 — 런레벨 전환 시, 먼저 K 링크를 번호 순서대로 실행하여 이전 런레벨의 서비스를 정지합니다.
- S 스크립트 순서 실행 — K 스크립트가 모두 완료된 후, S 링크를 번호 순서대로 실행하여 새 런레벨의 서비스를 시작합니다.
- 번호가 작을수록 먼저 실행 — S10은 S20보다 먼저, K01은 K20보다 먼저 실행됩니다.
- 순차 실행 — 각 스크립트는 이전 스크립트가 완료(또는 백그라운드 전환)될 때까지 대기합니다.
insserv를 사용하면 LSB 헤더의 의존성 정보를 기반으로 번호가 자동 할당됩니다.
# /etc/init.d/rc 의 핵심 동작 (의사 코드)
run_rc() {
RUNLEVEL=$1
# 1단계: K 스크립트 실행 (정지)
for script in /etc/rc${RUNLEVEL}.d/K*; do
$script stop
done
# 2단계: S 스크립트 실행 (시작)
for script in /etc/rc${RUNLEVEL}.d/S*; do
$script start
done
}
런레벨 전환 시 실행 순서
런레벨 3(텍스트 모드)에서 런레벨 5(GUI 모드)로 전환할 때의 실제 실행 순서를 시간축으로 보여줍니다. 먼저 rc5.d의 K 스크립트로 불필요한 서비스를 정지하고, 이어서 S 스크립트로 새 서비스를 시작합니다.
번호 할당 관례
| 번호 범위 | 용도 | 예 |
|---|---|---|
01~09 | 시스템 핵심 서비스 정지/시작 | K01gdm, S01hostname |
10~19 | 기반 인프라 (로깅, 시간동기화) | S10sysklogd, S15ntp |
20~39 | 기본 네트워크 서비스 | S20ssh, S30nfs-common |
40~59 | 네트워크 및 시스템 서비스 | S40networking, S50samba, S55cron |
60~89 | 애플리케이션 서비스 | S70mysql, S80nginx |
90~99 | 최종 단계 서비스 | S91apache2, S99rc.local |
서비스 관리 도구
SysVinit 환경에서 서비스를 관리하는 주요 도구는 배포판에 따라 다릅니다.
update-rc.d (Debian/Ubuntu)
update-rc.d는 Debian 계열에서 /etc/rcN.d/ 심볼릭 링크를 관리하는 도구입니다. LSB 헤더를 읽어 의존성을 고려한 번호를 할당합니다.
# 기본 설정으로 심볼릭 링크 생성 (LSB 헤더의 Default-Start/Stop 사용)
sudo update-rc.d myservice defaults
# 수동으로 시작/정지 번호 지정
sudo update-rc.d myservice defaults 90 10
# S90myservice (시작 순서 90), K10myservice (정지 순서 10)
# 서비스 비활성화 (S 링크를 K 링크로 변경)
sudo update-rc.d myservice disable
# 서비스 재활성화
sudo update-rc.d myservice enable
# 특정 런레벨에서만 활성화/비활성화
sudo update-rc.d myservice enable 3 5
sudo update-rc.d myservice disable 2 4
# 심볼릭 링크 완전 제거 (init.d 스크립트 삭제 후)
sudo update-rc.d myservice remove
chkconfig (Red Hat/CentOS)
chkconfig는 Red Hat 계열에서 서비스의 런레벨별 활성화 상태를 관리합니다.
# 서비스 등록
sudo chkconfig --add myservice
# 런레벨별 상태 확인
chkconfig --list myservice
# myservice 0:off 1:off 2:on 3:on 4:on 5:on 6:off
# 전체 서비스 상태 조회
chkconfig --list
# 특정 런레벨에서 활성화/비활성화
sudo chkconfig --level 35 myservice on
sudo chkconfig --level 2 myservice off
# 서비스 제거
sudo chkconfig --del myservice
service 명령
service는 /etc/init.d/ 스크립트를 실행하는 래퍼(Wrapper) 명령입니다. 환경 변수를 정리하고 스크립트를 호출합니다.
# 서비스 제어
sudo service ssh start
sudo service ssh stop
sudo service ssh restart
sudo service ssh reload
sudo service ssh status
# 위 명령은 다음과 동일:
sudo /etc/init.d/ssh start
# 전체 서비스 상태 조회
sudo service --status-all
# [ + ] cron — 실행 중
# [ - ] gdm — 정지됨
# [ ? ] networking — 상태 미지원
invoke-rc.d (Debian)
invoke-rc.d는 /etc/init.d/ 스크립트를 호출하되, 정책(Policy)을 확인하는 래퍼입니다. /usr/sbin/policy-rc.d가 존재하면 해당 스크립트를 실행하여 서비스 시작을 허용할지 결정합니다. chroot 환경이나 컨테이너에서 서비스 자동 시작을 방지하는 데 사용합니다.
# invoke-rc.d 사용 (update-rc.d, dpkg 등이 내부적으로 사용)
sudo invoke-rc.d ssh start
# 정책으로 서비스 시작 차단 (chroot/컨테이너에서 유용)
cat > /usr/sbin/policy-rc.d <<'POLICY'
#!/bin/sh
# 모든 서비스 시작을 거부 (101 = 실행 금지)
exit 101
POLICY
chmod +x /usr/sbin/policy-rc.d
apt-get install이 서비스를 자동 시작하는 것을 방지하려면, policy-rc.d를 설정하여 모든 invoke-rc.d 호출을 거부합니다. 공식 Debian Docker 이미지는 이 기법을 기본 적용합니다.
insserv (LSB 의존성 기반)
insserv는 LSB 헤더의 의존성 정보를 분석하여 /etc/rcN.d/ 심볼릭 링크의 순서 번호를 자동으로 결정합니다. update-rc.d가 내부적으로 insserv를 호출합니다.
# insserv 직접 사용 (의존성 기반 자동 번호 할당)
sudo insserv myservice
# 제거
sudo insserv -r myservice
# 의존성 순환 확인
insserv --showall
도구 비교 요약
| 기능 | update-rc.d (Debian) | chkconfig (Red Hat) | insserv |
|---|---|---|---|
| 의존성 해석 | insserv에 위임 | chkconfig 헤더 기반 | LSB 헤더 기반 자동 |
| 번호 할당 | 자동 (insserv) 또는 수동 | 수동 (--level) | 자동 (의존성 그래프) |
| 런레벨별 제어 | enable/disable [런레벨] | --level [레벨] on/off | LSB Default-Start 기반 |
| 순환 감지 | insserv 경고 | 미지원 | 경고 + 중단 |
| 스크립트 헤더 | LSB 헤더 권장 | chkconfig 헤더 | LSB 헤더 필수 |
# Red Hat chkconfig 헤더 형식 (LSB 헤더와 공존 가능)
#!/bin/sh
# chkconfig: 2345 90 10
# description: My Application Daemon
# 필드 의미: chkconfig: [런레벨] [시작번호] [정지번호]
# 위 예: 런레벨 2345에서 S90으로 시작, K10으로 정지
SysVinit 부팅 시퀀스
커널이 초기화를 완료한 후 /sbin/init이 PID 1로 실행되면, SysVinit은 다음 순서로 시스템을 초기화합니다.
/etc/inittab파싱 — init은 설정 파일을 읽어 기본 런레벨과 실행할 프로세스 목록을 결정합니다.sysinit항목 실행 —/etc/init.d/rcS(Debian) 또는/etc/rc.d/rc.sysinit(Red Hat)를 실행합니다. 이 단계에서 파일시스템 점검(fsck), 루트 파일시스템 재마운트(read-write),/proc//sys마운트, 호스트명 설정, 커널 모듈 로드 등이 수행됩니다.boot/bootwait항목 실행 — 부팅 시에만 한 번 실행되는 작업입니다.- 기본 런레벨 진입 —
initdefault에 지정된 런레벨로 진입합니다. /etc/init.d/rc N실행 — 해당 런레벨의 K/S 스크립트를 순서대로 실행합니다.respawn항목 시작 —getty등 지속적으로 실행되어야 하는 프로세스를 시작합니다.- 메인 루프 진입 — init은 자식 프로세스를 감시하며,
respawn항목이 종료되면 재시작하고,telinit명령(FIFO 또는 시그널)을 대기합니다.
/etc/rc.local
/etc/rc.local은 부팅 과정의 마지막 단계에서 실행되는 스크립트입니다. 일반적으로 S99rc.local로 심볼릭 링크되어 모든 서비스가 시작된 후 마지막으로 실행됩니다. 관리자가 서비스로 등록하기 어려운 일회성 명령을 넣는 용도로 사용됩니다.
#!/bin/sh -e
# /etc/rc.local — 부팅 완료 후 마지막으로 실행
# 예: 커널 파라미터 조정
echo 1 > /proc/sys/net/ipv4/ip_forward
# 예: 임시 디렉터리 생성
mkdir -p /var/run/myapp
chown myapp:myapp /var/run/myapp
# 예: 네트워크 인터페이스 추가 설정
ip route add 10.0.0.0/8 via 192.168.1.1
exit 0 # 반드시 0을 반환해야 부팅이 정상 완료됨
종료 시퀀스
시스템 종료는 부팅의 역순으로 진행됩니다.
shutdown명령 실행 — 사용자에게 경고 메시지를 브로드캐스트하고, 런레벨 0(halt) 또는 6(reboot)으로 전환합니다./etc/init.d/rc 0(또는rc 6) 실행 — K 스크립트를 번호 순서대로 실행하여 모든 서비스를 정지합니다.SIGTERM전송 — 남아있는 모든 프로세스에 종료 요청 시그널을 보냅니다.- 대기 (5~10초) — 프로세스가 정리 작업을 수행할 시간을 줍니다.
SIGKILL전송 — 여전히 살아있는 프로세스를 강제 종료합니다.- 파일시스템 언마운트 — 모든 파일시스템을 언마운트하거나 읽기 전용으로 재마운트합니다.
- 시스템 호출 —
reboot(RB_HALT_SYSTEM)또는reboot(RB_AUTOBOOT)를 호출하여 하드웨어를 정지하거나 재부팅합니다.
# 시스템 종료 명령
sudo shutdown -h now # 즉시 종료
sudo shutdown -r now # 즉시 재부팅
sudo shutdown -h +10 # 10분 후 종료
sudo shutdown -r 23:00 # 23시에 재부팅
sudo shutdown -c # 예약된 종료 취소
# telinit으로 직접 전환
sudo telinit 0 # 런레벨 0 (종료)
sudo telinit 6 # 런레벨 6 (재부팅)
# 직접 명령 (shutdown 우회)
sudo halt # 시스템 정지
sudo reboot # 시스템 재부팅
sudo poweroff # 시스템 전원 차단
shutdown -h는 시스템을 정지(halt)하지만 ACPI 전원 차단이 보장되지 않습니다. 일부 구형 시스템에서는 "System halted" 메시지 후 수동으로 전원 버튼을 눌러야 합니다. poweroff(또는 shutdown -P)는 ACPI를 통해 전원을 완전히 차단합니다. 서버 원격 관리 시 반드시 -P 플래그 또는 poweroff를 사용하세요.
init 스크립트 작성 패턴
LSB 호환 init 스크립트의 완전한 템플릿과 주요 패턴을 정리합니다.
완전한 LSB 호환 데몬 스크립트 템플릿
#!/bin/sh
### BEGIN INIT INFO
# Provides: mydaemon
# Required-Start: $remote_fs $syslog $network
# Required-Stop: $remote_fs $syslog $network
# Should-Start: $named
# Should-Stop: $named
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: My Application Daemon
# Description: Starts the My Application Daemon
# which provides REST API endpoints on port 8080.
### END INIT INFO
# LSB 헬퍼 함수 로드
. /lib/lsb/init-functions
# 변수 정의
NAME="mydaemon"
DESC="My Application Daemon"
DAEMON="/usr/sbin/mydaemon"
DAEMON_ARGS="--config /etc/mydaemon.conf"
PIDFILE="/var/run/${NAME}.pid"
SCRIPTNAME="/etc/init.d/${NAME}"
USER="mydaemon"
GROUP="mydaemon"
# 환경 변수 파일 로드 (존재하면)
[ -r /etc/default/${NAME} ] && . /etc/default/${NAME}
# 바이너리 존재 확인
[ -x "$DAEMON" ] || exit 5
do_start() {
# 이미 실행 중인지 확인
start-stop-daemon --start --quiet \
--pidfile "$PIDFILE" \
--exec "$DAEMON" \
--test > /dev/null 2>&1 \
|| return 1 # 이미 실행 중
# 데몬 시작
start-stop-daemon --start --quiet \
--pidfile "$PIDFILE" \
--exec "$DAEMON" \
--chuid "$USER:$GROUP" \
--make-pidfile --background \
-- $DAEMON_ARGS \
|| return 2 # 시작 실패
}
do_stop() {
start-stop-daemon --stop --quiet \
--pidfile "$PIDFILE" \
--retry=TERM/30/KILL/5 \
--exec "$DAEMON"
RETVAL="$?"
rm -f "$PIDFILE"
return "$RETVAL"
}
do_reload() {
start-stop-daemon --stop --quiet \
--pidfile "$PIDFILE" \
--signal HUP \
--exec "$DAEMON"
}
case "$1" in
start)
log_daemon_msg "Starting $DESC" "$NAME"
do_start
case "$?" in
0) log_end_msg 0 ;;
1) log_progress_msg "already running" ; log_end_msg 0 ;;
*) log_end_msg 1 ;;
esac
;;
stop)
log_daemon_msg "Stopping $DESC" "$NAME"
do_stop
case "$?" in
0|1) log_end_msg 0 ;;
*) log_end_msg 1 ;;
esac
;;
restart|force-reload)
log_daemon_msg "Restarting $DESC" "$NAME"
do_stop
case "$?" in
0|1)
do_start
case "$?" in
0) log_end_msg 0 ;;
*) log_end_msg 1 ;;
esac
;;
*) log_end_msg 1 ;;
esac
;;
reload)
log_daemon_msg "Reloading $DESC" "$NAME"
do_reload
log_end_msg $?
;;
status)
status_of_proc -p "$PIDFILE" "$DAEMON" "$NAME" && exit 0 || exit $?
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload|status}" >&2
exit 3
;;
esac
exit 0
/lib/lsb/init-functions 주요 헬퍼
| 함수 | 용도 | 예 |
|---|---|---|
log_daemon_msg | 데몬 동작 메시지 출력 | log_daemon_msg "Starting" "sshd" |
log_end_msg | 성공/실패 표시 ([OK]/[FAIL]) | log_end_msg 0 |
log_progress_msg | 진행 메시지 | log_progress_msg "already running" |
log_warning_msg | 경고 메시지 | log_warning_msg "config missing" |
log_failure_msg | 실패 메시지 | log_failure_msg "cannot start" |
status_of_proc | 프로세스 상태 확인 | status_of_proc -p $PID $DAEMON $NAME |
start_daemon | 데몬 시작 (Red Hat 스타일) | start_daemon -p $PID $DAEMON |
killproc | 프로세스 종료 | killproc -p $PID $DAEMON |
pidofproc | PID 조회 | pidofproc -p $PID $DAEMON |
환경 변수 파일 패턴 (/etc/default/)
# /etc/default/mydaemon — 환경 변수와 옵션 설정
# 이 파일은 init 스크립트에서 source됩니다.
# 데몬 활성화 여부 (0=비활성, 1=활성)
ENABLED=1
# 데몬 옵션
DAEMON_ARGS="--port 8080 --workers 4"
# 리소스 제한
ULIMIT="-n 65536"
# 로그 레벨
LOG_LEVEL="info"
# init 스크립트에서 환경 파일 사용
ENABLED=0
[ -r /etc/default/mydaemon ] && . /etc/default/mydaemon
case "$1" in
start)
if [ "$ENABLED" != "1" ]; then
log_warning_msg "$NAME is disabled in /etc/default/$NAME"
exit 0
fi
# ulimit 적용
[ -n "$ULIMIT" ] && ulimit $ULIMIT
do_start
;;
esac
/etc/default/ 파일은 conffile로 관리되어, 패키지 업그레이드 시 관리자의 수정 사항이 보존됩니다. 따라서 사이트별 데몬 옵션, 환경 변수, 리소스 제한 등을 이 파일에 두는 것이 안전합니다. init.d 스크립트 자체를 수정하면 패키지 업그레이드 시 충돌이 발생할 수 있습니다.
다중 인스턴스 패턴
# /etc/init.d/myapp@.sh — 인스턴스별 실행
# 심볼릭 링크로 인스턴스 생성:
# ln -s myapp@.sh /etc/init.d/myapp@instance1
# ln -s myapp@.sh /etc/init.d/myapp@instance2
INSTANCE=$(basename "$0" | sed 's/myapp@//')
PIDFILE="/var/run/myapp-${INSTANCE}.pid"
DAEMON_ARGS="--instance $INSTANCE --config /etc/myapp/${INSTANCE}.conf"
대안 init 시스템 비교
SysVinit 이후 다양한 init 시스템이 등장했습니다. 각 시스템은 SysVinit의 한계를 다른 방식으로 극복하고자 설계되었습니다.
주요 init 시스템
| init 시스템 | 설계 방식 | 주요 배포판 | 특징 |
|---|---|---|---|
| SysVinit | 순차 셸 스크립트 | Devuan, Slackware | 단순, 투명, 느린 부팅 |
| systemd | 의존성 DAG + 병렬 | Debian, Ubuntu, Fedora, RHEL, Arch | 소켓 활성화, cgroup, 통합 에코시스템 |
| OpenRC | 의존성 기반 셸 스크립트 | Gentoo, Alpine | SysVinit 호환, 병렬 시작 옵션 |
| runit | 3단계 감시 모델 | Void Linux | 극도의 단순성, daemontools 계열 |
| s6 | 감시 트리(Supervision Tree) | — | 엄격한 프로세스 감시, execline |
| Upstart | 이벤트 기반 | Ubuntu 9.10~14.10 (역사적) | 이벤트 구동, SysVinit 호환 레이어 |
| BusyBox init | 최소 inittab | 임베디드 (Buildroot 등) | 초소형, /etc/inittab 호환 (제한적) |
기능 비교
| 기능 | SysVinit | systemd | OpenRC | runit | s6 |
|---|---|---|---|---|---|
| 의존성 관리 | 번호 순서 | DAG (Requires/After) | 셸 기반 의존성 | 디렉터리 순서 | s6-rc 의존성 DB |
| 병렬 시작 | 제한적 | 완전 병렬 | 선택적 병렬 | 서비스 디렉터리 | 완전 병렬 |
| 프로세스 감시 | PID 파일 | cgroup | PID 파일 | supervise | supervise |
| 자동 재시작 | respawn (inittab) | Restart= | 선택적 | 기본 동작 | 기본 동작 |
| 소켓 활성화 | inetd 별도 | 네이티브 | 미지원 | 미지원 | s6-ipcserver |
| cgroup 통합 | 미지원 | v2 네이티브 | 선택적 | 미지원 | 미지원 |
| 로깅 | syslog | journald (바이너리) | syslog | svlogd (파일) | s6-log (파일) |
| 설정 형식 | 셸 스크립트 | INI 유닛 파일 | 셸 스크립트 | 디렉터리/파일 | execline/파일 |
| 바이너리 크기 | ~50 KB | ~1.5 MB | ~200 KB | ~40 KB | ~100 KB |
부팅 시간 비교
init 시스템의 선택은 부팅 시간에 직접적인 영향을 미칩니다. 다음은 동일한 서비스 구성(~30개 서비스)에서의 대략적인 부팅 시간 비교입니다.
| init 시스템 | 일반 서버 (HDD) | 일반 서버 (SSD) | 임베디드 (eMMC) | 주요 병목 |
|---|---|---|---|---|
| SysVinit | 40~90초 | 15~40초 | 8~20초 | 순차 실행, 셸 인터프리터 오버헤드 |
| systemd | 15~30초 | 5~15초 | 3~8초 | 소켓 활성화로 지연 시작 가능 |
| OpenRC | 25~50초 | 10~25초 | 5~12초 | 선택적 병렬, 셸 기반 |
| runit | 20~40초 | 8~20초 | 4~10초 | 서비스 디렉터리 감시 방식 |
| s6 | 15~35초 | 6~18초 | 3~8초 | s6-rc 의존성 기반 완전 병렬 |
/sbin/init 실행) 후부터 로그인 프롬프트(또는 default.target 도달)까지의 시간입니다. 실제 부팅 시간은 하드웨어, 서비스 수, 디스크 속도, 네트워크 DHCP 대기 시간 등에 따라 크게 달라집니다. SysVinit 환경에서 부팅 시간을 측정하려면 time /etc/init.d/rc 3 또는 각 서비스별 time /etc/init.d/서비스 start를 사용합니다. systemd 환경에서는 systemd-analyze와 systemd-analyze blame으로 상세 분석이 가능합니다.
init 시스템 선택 가이드
| 환경 | 권장 init | 이유 |
|---|---|---|
| 프로덕션 서버 (Debian/RHEL) | systemd | 프로세스 관리, 보안, 리소스 제어, 로깅 통합 |
| 임베디드 (자원 제한) | BusyBox init / SysVinit | 최소 크기, 단순성, glibc 미의존 |
| 컨테이너 (단일 프로세스) | tini / dumb-init | 좀비 수거 + 시그널 전달만 필요 |
| 컨테이너 (다중 프로세스) | s6-overlay | 경량 감시 + 의존성 관리 |
| Gentoo / Alpine | OpenRC | 셸 스크립트 기반 의존성, musl 호환 |
| 미니멀 / 교육 | SysVinit / runit | 투명한 동작, 학습 용이 |
| systemd-free 선호 | Devuan (SysVinit/OpenRC/runit) | Init Freedom 철학 |
OpenRC
OpenRC는 Gentoo의 기본 init 시스템이자 Alpine Linux에서도 사용됩니다. SysVinit의 셸 스크립트 방식을 유지하면서 의존성 관리를 개선했습니다.
# OpenRC 서비스 스크립트 예 (/etc/init.d/myservice)
#!/sbin/openrc-run
name="myservice"
description="My Application Service"
command="/usr/sbin/mydaemon"
command_args="--config /etc/mydaemon.conf"
command_user="mydaemon:mydaemon"
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"
depend() {
need net
after logger
use dns
}
start_pre() {
checkpath --directory --owner mydaemon:mydaemon /var/run/mydaemon
}
# OpenRC 명령
# rc-update add myservice default # 기본 런레벨에 추가
# rc-service myservice start # 서비스 시작
# rc-status # 전체 서비스 상태
Upstart (역사적)
Upstart는 Canonical(Ubuntu)이 개발한 이벤트 기반 init 시스템으로, 2006~2015년 Ubuntu의 기본 init이었습니다. SysVinit의 순차 실행과 systemd의 의존성 그래프 사이의 과도기적 설계입니다.
# /etc/init/myservice.conf — Upstart job 파일 예제
description "My Application Service"
author "Admin"
start on (filesystem and net-device-up IFACE!=lo)
stop on runlevel [016]
respawn
respawn limit 5 60
env CONFIG=/etc/myapp/config.yaml
env LOG_LEVEL=info
pre-start script
mkdir -p /var/run/myapp
chown myapp:myapp /var/run/myapp
end script
exec start-stop-daemon --start --chuid myapp \
--exec /usr/bin/myapp -- --config $CONFIG
post-stop script
rm -f /var/run/myapp/myapp.pid
end script
Upstart의 핵심 개념은 이벤트(Event)입니다. start on/stop on으로 특정 이벤트 발생 시 서비스를 시작/정지합니다. 이 모델은 하드웨어 핫플러그, 네트워크 연결 등 동적 이벤트에 유연하게 대응할 수 있었지만, 서비스 간 복잡한 의존성 표현에는 한계가 있었습니다.
runit
runit은 3단계 부팅 모델과 감시(Supervision) 방식으로 프로세스를 관리합니다. 서비스 하나당 디렉터리 하나로 구성됩니다.
# runit 서비스 구조
/etc/sv/myservice/
├── run # 실행 스크립트 (필수)
├── finish # 종료 시 실행 (선택)
└── log/
└── run # 로그 수집 스크립트
# /etc/sv/myservice/run
#!/bin/sh
exec chpst -u mydaemon /usr/sbin/mydaemon --config /etc/mydaemon.conf
# 서비스 활성화: 심볼릭 링크 생성
# ln -s /etc/sv/myservice /var/service/
# runit 명령
# sv start myservice # 시작
# sv stop myservice # 정지
# sv status myservice # 상태 확인
s6
s6는 Laurent Bercot(skarnet.org)이 개발한 프로세스 감시 시스템입니다. s6-rc와 결합하면 의존성 기반 서비스 관리가 가능합니다.
# s6 서비스 정의 (디렉터리 구조)
/etc/s6/sv/myservice/
├── type # "longrun" 또는 "oneshot"
├── run # 실행 스크립트
├── dependencies # 의존성 목록
└── producer-for # 파이프라인 연결 (선택)
# /etc/s6/sv/myservice/run
#!/bin/execlineb -P
fdmove -c 2 1
s6-setuidgid mydaemon
/usr/sbin/mydaemon --config /etc/mydaemon.conf
현대적 의의
systemd가 대부분의 주요 배포판을 장악한 현재에도, SysVinit 방식의 init은 여러 영역에서 여전히 활용되고 있습니다.
임베디드 리눅스
Buildroot, Yocto/OpenEmbedded 등 임베디드 리눅스 빌드 시스템에서는 SysVinit(또는 BusyBox init)이 기본 선택지입니다. systemd의 바이너리 크기(~1.5 MB + 의존 라이브러리)와 D-Bus 의존성은 자원이 제한된 임베디드 환경에서 부담이 됩니다.
# Buildroot init 시스템 선택지
BR2_INIT_BUSYBOX=y # BusyBox init (기본, 최소 크기)
BR2_INIT_SYSV=y # SysVinit (전통적 방식)
BR2_INIT_OPENRC=y # OpenRC (의존성 관리)
BR2_INIT_SYSTEMD=y # systemd (glibc 필수, 크기 큼)
BR2_INIT_NONE=y # init 없음 (직접 구현)
컨테이너 환경의 경량 init
Docker/OCI 컨테이너에서 PID 1 프로세스는 좀비 프로세스 수거와 시그널 전달이라는 핵심 책임을 져야 합니다. 애플리케이션 프로세스가 이 역할을 직접 수행하지 못하는 경우, SysVinit의 PID 1 개념에서 영감을 받은 경량 init이 사용됩니다.
| 경량 init | 크기 | 역할 |
|---|---|---|
tini | ~28 KB | 시그널 전달 + 좀비 수거 |
dumb-init | ~50 KB | 시그널 전달 + 좀비 수거 + 프로세스 그룹 |
s6-overlay | ~200 KB | s6 기반 컨테이너 init (다중 프로세스) |
PID 1 문제의 본질
Docker 컨테이너에서 PID 1 문제가 발생하는 근본적인 이유는 리눅스 커널의 PID 1 특수 규칙 때문입니다.
- 시그널 기본 동작 무시 — PID 1은 등록하지 않은 시그널의 기본 동작을 무시합니다. 즉, 애플리케이션이
SIGTERM핸들러를 등록하지 않으면docker stop의 종료 시그널이 무시되고, Docker는 10초 후SIGKILL로 강제 종료합니다. - 좀비 수거 책임 — PID 1은 고아 프로세스를
waitpid()로 수거해야 합니다. 일반 애플리케이션은 이 로직이 없으므로 좀비가 누적됩니다.
docker stop이 graceful shutdown을 수행하지 못합니다. 10초 타임아웃 후 SIGKILL로 강제 종료되어 데이터 손실, 불완전한 트랜잭션, 리소스 누수가 발생할 수 있습니다.
# 방법 1: tini 사용 (가장 일반적)
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y tini
ENTRYPOINT ["tini", "--"]
CMD ["/usr/bin/myapp"]
# 방법 2: Docker 내장 --init 플래그
# docker run --init myimage
# 방법 3: exec 형식 ENTRYPOINT (시그널 직접 처리하는 앱)
ENTRYPOINT ["/usr/bin/myapp"]
# 주의: 셸 형식 (ENTRYPOINT /usr/bin/myapp) 사용 시
# sh가 PID 1이 되어 시그널이 앱에 전달되지 않음
# 방법 4: s6-overlay (다중 프로세스 컨테이너)
FROM ubuntu:22.04
ADD https://github.com/just-containers/s6-overlay/releases/download/v3.1.6.2/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ENTRYPOINT ["/init"]
임베디드 리눅스 실전 예제
Yocto/OpenEmbedded에서 SysVinit 기반 서비스를 배포하는 실전 패턴입니다.
# Yocto 레시피에서 init.d 스크립트 설치
# recipes-core/myapp/myapp.bb
inherit update-rc.d
INITSCRIPT_NAME = "myapp"
INITSCRIPT_PARAMS = "defaults 90 10"
do_install_append() {
install -d ${D}${sysconfdir}/init.d
install -m 0755 ${WORKDIR}/myapp.init ${D}${sysconfdir}/init.d/myapp
}
# Buildroot에서 init 스크립트 설치
# package/myapp/S90myapp
#!/bin/sh
case "$1" in
start)
printf "Starting myapp: "
start-stop-daemon -S -b -m -p /var/run/myapp.pid \
-x /usr/bin/myapp
[ $? = 0 ] && echo "OK" || echo "FAIL"
;;
stop)
printf "Stopping myapp: "
start-stop-daemon -K -p /var/run/myapp.pid
echo "OK"
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
systemd-sysv-generator
systemd 환경에서도 /etc/init.d/의 SysVinit 스크립트는 systemd-sysv-generator에 의해 자동으로 systemd 유닛으로 변환되어 실행됩니다. 이 호환 레이어 덕분에 systemd로 전환된 시스템에서도 기존 init.d 스크립트가 계속 동작합니다.
# systemd-sysv-generator가 생성한 유닛 확인
ls /run/systemd/generator.late/
# mylegacy.service → /etc/init.d/mylegacy 기반 자동 생성
# 생성된 유닛 내용 확인
systemctl cat mylegacy.service
# [Unit]
# SourcePath=/etc/init.d/mylegacy
# Description=LSB: My Legacy Service
# [Service]
# Type=forking
# ExecStart=/etc/init.d/mylegacy start
# ExecStop=/etc/init.d/mylegacy stop
systemd-sysv-generator의 내부 동작
systemd-sysv-generator는 부팅 초기에 /etc/init.d/ 디렉터리를 스캔하여 다음 규칙으로 유닛 파일을 생성합니다.
- LSB 헤더 파싱 —
Provides,Required-Start,Default-Start등을 읽어 유닛의 의존성과 Install 섹션을 결정합니다. - Type=forking 기본 할당 — SysVinit 스크립트는 대부분 데몬을 포크(fork)하므로
Type=forking으로 설정됩니다. - ExecStart/ExecStop 매핑 —
/etc/init.d/서비스 start와/etc/init.d/서비스 stop으로 매핑됩니다. - 우선순위 — 같은 이름의 네이티브
.service유닛이 존재하면, generator가 생성한 유닛보다 네이티브 유닛이 우선합니다.
# generator 동작 확인
systemd-sysv-generator /tmp/normal /tmp/early /tmp/late
ls /tmp/late/ # 생성된 .service 파일 확인
# 특정 init.d 스크립트의 변환 결과 확인
systemctl cat mylegacy.service
# SourcePath=/etc/init.d/mylegacy
# Description=LSB: My Legacy Service
# After=remote-fs.target network-online.target
# Wants=remote-fs.target network-online.target
# generator가 무시하는 경우:
# - 같은 이름의 .service 파일이 /etc/systemd/system/ 또는 /usr/lib/systemd/system/에 존재
# - 스크립트에 실행 권한이 없음
# - 스크립트 이름이 .dpkg-dist, .dpkg-old 등 백업 확장자
Devuan과 systemd-free 운동
Devuan은 2015년 Debian의 systemd 채택에 반대하여 분기(Fork)된 배포판입니다. SysVinit, OpenRC, runit 중 선택할 수 있으며, "Init Freedom"을 핵심 가치로 내세웁니다. 이는 init 시스템의 다양성과 사용자 선택권이 리눅스 생태계에서 여전히 중요한 가치임을 보여줍니다.
Devuan은 Debian의 패키지 저장소를 그대로 사용하되, systemd 의존성이 있는 패키지를 포크(fork)하거나 패치하여 제공합니다. elogind(systemd-logind의 독립 포크)를 사용하여 polkit, NetworkManager 등 systemd-logind에 의존하는 소프트웨어도 동작합니다.
# Devuan에서 init 시스템 선택
# 설치 시 선택하거나, 나중에 변경:
sudo apt-get install sysvinit-core # SysVinit
sudo apt-get install openrc # OpenRC
sudo apt-get install runit-init # runit
# elogind (systemd-logind 대체)
sudo apt-get install elogind libpam-elogind
커널 인터페이스
SysVinit의 /sbin/init은 커널과 직접 상호작용하는 최초의 사용자 공간 프로세스입니다. 이 상호작용에서 사용되는 핵심 커널 인터페이스를 정리합니다.
PID 1의 특수성
커널은 PID 1에 대해 특별한 규칙을 적용합니다.
- 시그널 무시 — PID 1은 핸들러를 등록하지 않은 시그널을 무시합니다. 따라서
kill -9 1(SIGKILL)도 PID 1을 종료하지 못합니다. 이는 커널(kernel/signal.c)에서 하드코딩된 보호입니다. - 고아 프로세스 수거 — 부모 프로세스가 종료되면, 그 자식 프로세스는 PID 1에 재배정(reparenting)됩니다. PID 1은
waitpid()를 호출하여 이 프로세스들의 종료 상태를 수거해야 합니다. 수거하지 않으면 좀비(Zombie) 프로세스가 누적됩니다. - PID 1 종료 시 커널 패닉 — PID 1이 어떤 이유로든 종료되면 커널은 즉시 패닉(Panic)합니다. init은 절대 종료되어서는 안 됩니다.
waitpid()를 호출하지 않으면 고아 프로세스가 좀비(Z) 상태로 남습니다. 좀비 프로세스는 메모리를 거의 사용하지 않지만, PID 번호를 점유합니다. 커널의 PID 공간(/proc/sys/kernel/pid_max, 기본값 32768)이 소진되면 새 프로세스를 생성할 수 없어 시스템이 사실상 마비됩니다.
프로세스 생성/종료 메커니즘의 상세 내용은 프로세스 관리를 참고하세요.
고아 프로세스 reparenting과 subreaper
전통적으로 부모가 종료된 모든 고아 프로세스는 PID 1에 재배정됩니다. 리눅스 3.4에서 도입된 PR_SET_CHILD_SUBREAPER는 이 동작을 변경합니다.
/* prctl()로 subreaper 설정 — 컨테이너 init에서 활용 */
#include <sys/prctl.h>
/* 이 프로세스를 subreaper로 설정하면,
* 이 프로세스의 하위 자식이 고아가 될 때
* PID 1 대신 이 프로세스에 재배정됩니다. */
prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0);
/* tini, dumb-init 등 컨테이너 init은
* 이 기능을 사용하여 좀비 수거를 담당합니다. */
/* kernel/signal.c — PID 1 시그널 보호 */
static bool sig_task_ignored(struct task_struct *t, int sig, bool force)
{
/* init 프로세스는 명시적 핸들러가 없는 시그널을 무시 */
if (is_global_init(t) && !force &&
!sigismember(&t->signal->action[sig-1].sa.sa_mask, sig))
return true;
...
}
/* kernel/exit.c — PID 1 종료 시 커널 패닉 */
void __noreturn do_exit(long code)
{
...
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n", ...);
...
}
커널에서 init으로의 전환
커널 부팅의 마지막 단계에서 kernel_init() 함수가 사용자 공간의 init 바이너리를 실행합니다. 검색 순서는 다음과 같습니다.
- 커널 커맨드라인의
init=파라미터가 지정한 바이너리 /sbin/init/etc/init/bin/init/bin/sh(최후의 수단)
/* init/main.c — kernel_init() 에서 init 프로세스 실행 */
static int __ref kernel_init(void *unused)
{
...
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret) return 0;
}
if (execute_command) { /* init= 커널 파라미터 */
ret = run_init_process(execute_command);
if (!ret) return 0;
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found.");
}
reboot() 시스템 콜
SysVinit의 shutdown/halt/reboot 명령은 최종적으로 reboot() 시스템 콜을 호출하여 커널에 시스템 종료/재부팅을 요청합니다.
| 매직 번호 | 동작 |
|---|---|
RB_AUTOBOOT | 시스템 재부팅 |
RB_HALT_SYSTEM | 시스템 정지 (전원 유지) |
RB_POWER_OFF | 시스템 전원 차단 |
RB_ENABLE_CAD | Ctrl+Alt+Del로 즉시 재부팅 활성화 |
RB_DISABLE_CAD | Ctrl+Alt+Del 시 init에 SIGINT 전달 |
init 관련 커널 파라미터
| 파라미터 | 설명 | 예 |
|---|---|---|
init= | PID 1으로 실행할 바이너리 지정 | init=/bin/bash |
rdinit= | initramfs에서 실행할 init 바이너리 | rdinit=/init |
single / 1 | 단일 사용자 모드로 부팅 | single |
emergency | 비상 모드로 부팅 | emergency |
S | 단일 사용자 모드 | S |
관련 커널 소스 참조
| 소스 경로 | 함수/구조체 | 역할 |
|---|---|---|
init/main.c | kernel_init(), try_to_run_init_process() | 커널에서 PID 1 실행 |
kernel/signal.c | sig_task_ignored(), is_global_init() | PID 1 시그널 보호 |
kernel/exit.c | do_exit(), find_new_reaper() | 고아 프로세스 reparenting, PID 1 패닉 |
kernel/reboot.c | SYSCALL_DEFINE4(reboot, ...) | 시스템 재부팅/종료 |
fs/exec.c | kernel_execve() | init 바이너리 실행 (사용자 공간 전환) |
kernel/fork.c | copy_process() | fork 시 부모/subreaper 설정 |
include/linux/sched.h | struct task_struct | 프로세스 디스크립터 (pid, parent, children) |
커널 부팅 전체 흐름(firmware → bootloader → 커널 → init)의 상세 내용은 부팅 과정을 참고하세요.
트러블슈팅
SysVinit 환경에서 자주 발생하는 문제와 해결 방법을 정리합니다.
부팅 중단 디버깅
| 증상 | 원인 | 해결 |
|---|---|---|
| 부팅 중 특정 서비스에서 멈춤 | 스크립트가 블로킹 대기 | 커널 파라미터에 init=/bin/sh 추가 후 문제 스크립트 수정 |
| "Give root password for maintenance" | 파일시스템 점검(fsck) 실패 | 루트 암호 입력 후 fsck 수동 실행 |
| getty만 나오고 서비스 없음 | sysinit 스크립트 실패 | /etc/init.d/rcS 또는 rc.sysinit 확인 |
| Ctrl+Alt+Del이 동작하지 않음 | inittab에 ctrlaltdel 항목 누락 | inittab에 ca::ctrlaltdel:/sbin/shutdown -r now 추가 |
런레벨 전환 문제
# 현재 런레벨 확인
runlevel
who -r
# 특정 런레벨의 스크립트 목록 확인
ls -la /etc/rc3.d/
# 스크립트 직접 실행하여 오류 확인
/etc/init.d/myservice start
echo $? # 종료 코드 확인
# init에 inittab 재읽기 요청
sudo telinit q
PID 파일 오염(Stale PID)
# 증상: "service already running" 이지만 실제로 프로세스 없음
cat /var/run/myservice.pid
# 12345
# 해당 PID의 프로세스 확인
ps -p 12345
# PID 12345가 다른 프로세스이거나 존재하지 않음
# 해결: stale PID 파일 삭제 후 재시작
sudo rm -f /var/run/myservice.pid
sudo service myservice start
의존성 순환
# insserv로 순환 의존성 감지
insserv --showall 2>&1 | grep -i "loop"
# insserv: warning: current start runlevel(s) (empty) of script 'X'
# insserv: There is a loop between service A and B
# 해결: LSB 헤더의 Required-Start/Should-Start 수정으로 순환 제거
디버깅 플레이북
SysVinit 환경에서 서비스 문제를 체계적으로 진단하는 절차입니다.
- 1단계: 로그 확인 —
/var/log/syslog또는/var/log/messages에서 서비스 관련 메시지를 확인합니다. - 2단계: 스크립트 직접 실행 —
/etc/init.d/서비스 start를 직접 실행하여 에러 메시지를 확인합니다. - 3단계: 종료 코드 확인 —
echo $?로 종료 코드를 확인합니다 (LSB 종료 코드 참조). - 4단계: strace로 추적 — 스크립트나 데몬의 시스템 콜을 추적하여 정확한 실패 지점을 파악합니다.
- 5단계: 의존성 확인 —
insserv --showall로 의존성 순서를 검증합니다.
# strace로 init.d 스크립트 디버깅
sudo strace -f -e trace=process,file /etc/init.d/myservice start 2>&1 | tail -50
# -f: fork된 자식 프로세스도 추적
# -e trace=process,file: 프로세스 생성과 파일 접근만 필터
# 데몬 프로세스 직접 추적
sudo strace -p $(cat /var/run/myservice.pid) -e trace=network,signal
# 네트워크와 시그널 관련 시스템 콜만 추적
# 부팅 시 서비스 실행 시간 측정 (간이 프로파일링)
time /etc/init.d/myservice start
# real 0m2.345s ← 서비스 시작에 걸린 시간
# 커널 파라미터로 init 디버깅
# GRUB에서 다음 파라미터 추가:
# init=/bin/sh ← 셸로 직접 진입 (비상 복구)
# initcall_debug ← 커널 initcall 타이밍 출력
# printk.devkmsg=on ← 커널 메시지 콘솔 출력
일반적인 실수
| 실수 | 증상 | 해결 |
|---|---|---|
| 스크립트 실행 권한 누락 | Permission denied | chmod +x /etc/init.d/myservice |
| 셸 인터프리터 줄 오류 | bad interpreter | #!/bin/sh 확인 (Windows CRLF 제거: dos2unix) |
| LSB 헤더 누락 | update-rc.d 경고, 순서 번호 비정상 | LSB 헤더 추가 |
| PID 파일 경로 불일치 | status/stop 동작 안 함 | 스크립트와 데몬의 PID 파일 경로 일치시키기 |
exit 0 누락 | 부팅 중 오류 표시 | 스크립트 마지막에 exit 0 추가 |
| 환경 변수 미설정 | PATH가 비어 명령 실행 실패 | 스크립트 상단에 PATH=/sbin:/bin:/usr/sbin:/usr/bin |
| 데몬이 포그라운드에서 실행 | 스크립트가 start에서 반환하지 않음 | start-stop-daemon --background 또는 데몬 자체의 -d 플래그 사용 |
start-stop-daemon에 --exec 미지정 | 동일 바이너리의 다른 인스턴스를 잘못 감지 | --exec와 --pidfile을 함께 지정 |
| Required-Start에 순환 의존성 | insserv 오류, 서비스 시작 실패 | insserv --showall로 순환 감지, Should-Start로 완화 |
| tmpfs 마운트 전 PID 파일 접근 | 부팅 중 No such file or directory | start_pre()에서 디렉터리 생성, 또는 Required-Start: $local_fs |
자주 묻는 질문 (FAQ)
Q1: SysVinit은 아직 사용할 가치가 있는가?
특정 환경에서는 여전히 유효합니다. 임베디드 시스템(Buildroot/Yocto), 교육 목적(init 시스템의 기본 원리 이해), systemd 의존성을 피하고자 하는 환경(Devuan, Slackware), 극도로 단순한 시스템에서는 SysVinit의 투명성과 단순성이 장점입니다. 다만 프로덕션 서버나 데스크톱 환경에서는 systemd의 프로세스 관리, 보안 기능, 리소스 제어가 압도적으로 우수합니다.
Q2: init.d 스크립트를 systemd unit으로 변환하려면?
기본적인 변환 규칙은 다음과 같습니다.
| init.d 스크립트 요소 | systemd unit 등가 |
|---|---|
LSB Required-Start: $network | After=network-online.target |
Default-Start: 2 3 4 5 | [Install] WantedBy=multi-user.target |
start-stop-daemon --background | Type=simple (포그라운드 실행 권장) |
| PID 파일 | 불필요 (cgroup 추적, 또는 Type=forking + PIDFile=) |
/etc/default/myservice | EnvironmentFile=/etc/default/myservice |
respawn (inittab) | Restart=always |
start-stop-daemon --retry=TERM/30/KILL/5 | TimeoutStopSec=30 |
자세한 마이그레이션 절차는 systemd 종합 가이드 — 실전 예제를 참고하세요.
마이그레이션 워크플로우
# 마이그레이션 검증 명령
# 1. unit 파일 문법 검증
systemd-analyze verify /etc/systemd/system/myapp.service
# 2. 의존성 트리 확인
systemctl list-dependencies myapp.service
# 3. 기존 init.d 스크립트 비활성화
sudo update-rc.d myapp disable
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
# 4. generator가 더 이상 변환하지 않는지 확인
ls /run/systemd/generator.late/myapp.service 2>/dev/null && echo "경고: init.d 스크립트가 아직 활성" || echo "OK"
Q3: 런레벨 2와 3의 차이는?
배포판에 따라 다릅니다. Debian/Ubuntu는 런레벨 2~5를 모두 동일하게 취급하며 기본값은 2입니다. Red Hat/CentOS는 런레벨 3을 "텍스트 모드 다중 사용자"로, 런레벨 5를 "GUI 모드"로 구분합니다. 실제로 런레벨 간 차이는 /etc/rcN.d/의 심볼릭 링크 구성에 의해서만 결정되므로, 관리자가 자유롭게 정의할 수 있습니다.
Q4: inittab이 없는 시스템에서 런레벨을 확인하려면?
systemd 기반 시스템에서는 /etc/inittab이 존재하지 않습니다. 대신 타깃(Target)이 런레벨을 대체합니다. systemctl get-default로 기본 타깃을 확인하고, runlevel 명령은 systemd 호환 레이어를 통해 여전히 동작합니다.
Q5: BusyBox init과 SysVinit의 차이는?
BusyBox init은 SysVinit의 /etc/inittab 형식을 지원하지만, 런레벨 개념이 없습니다. sysinit, respawn, askfirst, shutdown, ctrlaltdel 등 제한된 액션만 지원합니다. /etc/init.d/와 rcN.d 체계도 BusyBox init 자체에는 포함되지 않으며, 별도의 스크립트로 구현해야 합니다. 상세 내용은 BusyBox 종합 가이드 — init 시스템을 참고하세요.
참고 링크
- SysVinit 공식 프로젝트 — Savannah (GNU)
- SysVinit GitHub 저장소 — 현재 유지보수 포크 (slicer69/sysvinit)
- init(8) man 페이지 — Linux man-pages 프로젝트
- inittab(5) man 페이지 — /etc/inittab 형식 레퍼런스
- telinit(8) man 페이지 — 런레벨 변경 명령
- Linux Standard Base (LSB) — init 스크립트 규격
- Debian Wiki: LSB Init Scripts — Debian의 init 스크립트 가이드
- Debian Wiki: SysVinit — Debian에서의 SysVinit 현황
- Gentoo Wiki: OpenRC — SysVinit 호환 현대적 init 프레임워크
- LWN.net: The end of the SysV init era — SysVinit 시대의 종료 (2014)
- LWN.net: Debating inits — Debian의 init 시스템 논쟁 (2016)
- LWN.net: Revisiting the init system debate — init 시스템 재논의 (2019)
- 커널 소스: init/main.c — kernel_init()에서 /sbin/init 실행 코드 (Bootlin Elixir)
- freedesktop.org: systemd Incompatibilities — SysVinit 호환성 참고 사항
관련 문서
SysVinit과 관련된 다른 리눅스 커널 문서를 참고하여 더 깊이 이해할 수 있습니다.