← System Programming DUJINLABS.COM

Process · Linux userspace / kernel ABI

daemon보다 service supervision

double-fork 관례와 systemd 같은 supervisor 아래의 foreground service를 비교하고 signal, readiness, fd, 종료 순서를 설계합니다.

Series
10 / 37
Build
cc -std=c17 -Wall -Wextra -O2 service_loop.c -o service_loop
Run
./service_loop
Kernel
Linux 6.18.37 LTS

현대 Linux 서비스도 반드시 double-fork해야 하는가?

double-fork는 controlling terminal과 session에서 분리되고 orphan을 init에 맡기던 실행 환경의 관례다. service manager가 PID, cgroup, stdout/stderr, restart를 관리하는 환경에서는 self-daemonize가 오히려 main PID 추적과 readiness 판단을 어렵게 한다.

안정적인 service는 foreground에서 실행하며 시작 완료, 종료 요청, child 회수, log fd 정책을 supervisor와 명시적으로 맞춘다. daemon이라는 형태보다 누가 process lifetime을 소유하는지가 중요하다.

구조 그림

그림 1. service manager가 소유하는 process와 fd
Service managercgroup · restart policy · readiness deadline

Main process

  • foreground PID
  • signalfd
  • shutdown owner

Inherited resources

  • listener fd
  • stdout/stderr
  • configuration fd

Workers

  • child pidfd
  • active request count
  • waitid 회수

Stop contract

  • SIGTERM
  • drain deadline
  • exit status

foreground service는 supervisor와 관계를 끊지 않는다. main PID, cgroup, listener, log, readiness가 한 관리 단위에 남는다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
service manager fd/env/cgroup 준비
exec service foreground 실행
initialize resource 확보
ready protocol로 통지
SIGTERM graceful shutdown

시작과 종료를 함수 내부 flag가 아니라 supervisor와 교환하는 관찰 가능한 사건으로 만든다. ready 전에 요청을 받지 않고, TERM 뒤 새 작업을 수락하지 않는다.

그림 3. 커널 내부에서 지나가는 주요 지점
execve service image
signalfd/epoll signal을 event로 수신
waitid worker 회수
close/fsync 출력 정리
exit_group status 전달

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
kernel/signal.c do_send_sig_info(), get_signal() SIGTERM 같은 종료 요청 전달
kernel/exit.c do_exit(), do_wait() service와 worker 종료 및 회수
kernel/cgroup/cgroup.c cgroup_attach_task_all() supervisor가 process tree를 cgroup 단위로 추적

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 service_loop.c -o service_loop
01#define _GNU_SOURCE
02#include <signal.h>
03#include <stdio.h>
04#include <sys/signalfd.h>
05#include <unistd.h>
06
07int main(void)
08{
09    sigset_t mask;
10    sigemptyset(&mask);
11    sigaddset(&mask, SIGTERM);
12    sigaddset(&mask, SIGINT);
13    if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
14        return 1;
15
16    int sfd = signalfd(-1, &mask, SFD_CLOEXEC);
17    if (sfd < 0)
18        return 1;
19    puts("READY");
20    fflush(stdout);
21
22    struct signalfd_siginfo info;
23    if (read(sfd, &info, sizeof(info)) != sizeof(info))
24        return 1;
25    printf("stopping on signal %u\n", info.ssi_signo);
26    close(sfd);
27    return 0;
28}

코드 조각별 설명

실제 코드 11행sigaddset(&mask, SIGTERM)

service manager의 정상 종료 요청을 async handler가 아니라 event fd로 받기 위해 먼저 대상 signal set을 만든다.

실제 코드 13행sigprocmask(SIG_BLOCK

signalfd를 만들기 전에 signal을 block해 생성 사이에 default action으로 종료되는 창을 없앤다. multithread라면 pthread_sigmask를 초기 thread에서 적용한다.

실제 코드 16행SFD_CLOEXEC

worker exec 시 supervisor용 signal fd가 우발적으로 상속되지 않게 한다.

실제 코드 19행puts("READY")

예제에서는 stdout line이 readiness protocol이다. 실제 환경에서는 sd_notify, pipe close, socket activation 등 supervisor가 이해하는 방법을 쓴다.

실제 코드 23행read(sfd, &info

signal을 고정 크기 record로 소비한다. 여러 signal이 pending이면 반복해서 read해야 한다.

세부 동작

01

readiness와 process 존재는 다르다

fork/exec 성공은 config parse, socket bind, database recovery가 끝났다는 뜻이 아니다. supervisor가 단순히 PID 존재만 보고 traffic을 보내면 startup race가 생긴다.

ready notification은 필요한 resource를 확보하고 request 처리 loop가 실제로 동작할 수 있는 지점에서 한 번 보낸다.

02

shutdown에는 순서가 있다

SIGTERM을 받으면 listener에서 새 요청을 막고, 진행 중 작업에 deadline을 주고, child를 회수하고, durable data를 flush한 뒤 종료한다. signal handler에서 이 모든 작업을 직접 수행하지 않는다.

강제 종료 SIGKILL은 cleanup 코드를 실행하지 않으므로 supervisor timeout은 데이터 손실 허용 범위와 맞춘다.

03

fd ownership을 문서화한다

socket activation이나 supervisor pipe를 쓰면 fd는 exec 전부터 열려 있다. 번호를 가정하기보다 LISTEN_FDS 같은 protocol과 CLOEXEC 상태를 확인한다.

log rotation은 pathname을 바꾸는 것만으로 이미 열린 file description을 바꾸지 않는다. SIGHUP 처리나 journald/stdout 소유 모델을 정한다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
main processsupervisor가 exec하고 exit status까지 추적한다PID/cgroup, readiness, watchdog
listener fdservice가 bind하거나 supervisor에서 상속받는다CLOEXEC, accept ownership
shutdown deadlineTERM 수신 때 시작하고 cleanup 완료 또는 강제 종료까지 유지한다monotonic expiry, active work count

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
서비스는 active인데 요청 실패readiness 전에 traffic 전달ready 시점과 listener 상태
stop이 오래 걸림새 작업 계속 수락 또는 worker 미회수accept gate, waitid loop, deadline
재시작 후 port in use이전 child/cgroup이 listener 보유ss -lptn, /proc/PID/fd

직접 확인

  1. 프로그램을 실행하고 다른 terminal에서 SIGTERM을 보내 signalfd read와 정상 exit status를 확인한다.
  2. READY 전 3초 sleep을 넣고 supervisor가 process start와 readiness를 구분하도록 작은 parent를 작성한다.
  3. listener fd를 parent가 만들고 exec한 child에 넘겨 socket activation의 fd lifetime을 확인한다.
실행./service_loop
추적strace -e trace=rt_sigprocmask,signalfd4,poll,read,exit_group ./service_loop

원문