Daemon이란 무엇인가 - 터미널 프로세스에서 systemd까지
백그라운드에 상주하며 요청을 기다리는 프로세스인 daemon을 기초부터 정리합니다. 터미널과 세션, SIGHUP과 nohup, 고전적인 daemon화 절차(double fork), systemd와 launchd, 그리고 container에서는 왜 반대로 전경 실행이 관행인지까지 다룹니다.
sshd, dockerd, systemd처럼 이름이 d로 끝나는 프로그램들의 공통 개념인 daemon을 기초부터 정리한 단편입니다. Docker 기초 시리즈의 보조 글이지만, 내용은 Docker와 무관하게 성립하는 OS 일반 개념입니다.
보통 프로그램과 daemon의 차이
터미널에서 python script.py를 실행하면 프로세스가 하나 생깁니다. 이 프로세스는 두 가지에 묶여 있습니다. 실행한 터미널(출력이 화면에 보이고, Ctrl-C가 먹는 이유), 그리고 로그인 세션(로그아웃하면 함께 정리되는 이유)입니다. 일이 끝나면 프로세스도 끝납니다.
daemon은 이 연결을 끊고 백그라운드에 상주하는 프로세스입니다. 붙어 있는 터미널이 없고, 누가 로그아웃하든 계속 돌며, 할 일(요청, 예약된 시각)이 올 때까지 대기합니다. 서버라고 부르는 것들이 대부분 여기 속합니다.
- sshd: SSH 접속 요청을 기다립니다
- postgres: 5432 포트에서 DB 접속을 기다립니다
- cron: 예약된 시각이 되면 작업을 실행합니다
- dockerd: docker CLI의 API 요청을 기다립니다
이름 끝에 d를 붙이는 것이 Unix의 관례이고, daemon이라는 단어 자체는 1960년대 MIT의 시분할 시스템 프로젝트에서 물리학의 Maxwell’s demon 이름을 가져다 쓴 것이 시초입니다.
터미널에 묶여 있다는 것의 실체
“백그라운드로 돌리면 되는 것 아닌가”라는 의문이 나올 수 있습니다. 쉘에는 실제로 그 문법이 있습니다.
1
python server.py & # 백그라운드 실행. 터미널을 바로 돌려받는다
하지만 &는 daemon을 만들지 못합니다. 프로세스가 여전히 그 터미널의 세션에 소속되어 있기 때문입니다. 터미널을 닫으면 커널이 그 세션의 프로세스들에게 SIGHUP(hangup) 신호를 보내고, 기본 동작은 종료입니다. SSH로 서버에 들어가 &로 띄워 놓은 프로그램이 접속을 끊는 순간 죽는 이유가 이것입니다.
그래서 임시방편으로 nohup이 있습니다.
1
nohup python server.py > server.log 2>&1 &
nohup은 SIGHUP을 무시하게 만들고, 터미널이 사라진 뒤에 출력이 갈 곳이 없어지는 문제를 파일 리다이렉트로 해결합니다. daemon들이 화면 대신 로그 파일을 쓰는 이유도 같습니다. 터미널이 없으니 stdout이 향할 곳을 따로 정해줘야 합니다.
고전적인 daemon화 절차
nohup은 밖에서 씌우는 방식이고, 전통적인 Unix daemon은 프로그램이 시작 직후 스스로 터미널과 절연하는 절차를 밟았습니다. 순서가 정형화되어 있습니다.
- fork 하고 부모는 종료합니다. 쉘은 명령이 끝났다고 판단해 프롬프트를 돌려주고, 남은 자식은 고아가 되어 init(PID 1) 밑으로 들어갑니다
- setsid로 새 세션을 만듭니다. 원래 세션, 그리고 제어 터미널과의 연결이 여기서 완전히 끊어집니다
- 한 번 더 fork 합니다. 세션 리더가 아니게 만들어, 이후 어떤 터미널도 다시 붙을 수 없게 차단합니다. 1번과 합쳐 double fork라고 부릅니다
- 작업 디렉토리를 루트(/)로 옮기고 umask를 초기화합니다. 마운트 해제를 막지 않기 위한 정리입니다
- stdin, stdout, stderr를 닫고 /dev/null과 로그 파일로 연결합니다
오래된 서버 프로그램들의 -d(daemonize) 옵션이 바로 이 절차를 실행합니다. 직접 구현할 일은 이제 거의 없지만, “daemon은 터미널과 세션에서 분리된 프로세스”라는 정의가 코드 수준에서 무엇을 뜻하는지 이 절차가 정확히 보여줍니다.
요즘은 init 시스템이 대신한다
현대의 관행은 반대 방향으로 정리됐습니다. 프로그램은 daemon화 절차 없이 평범한 전경 프로세스로 만들고, 백그라운드 상주는 init 시스템에 맡깁니다.
- Linux: systemd. service 파일에 실행 명령을 적어두면 systemd가 백그라운드 구동, 부팅 시 자동 시작, 죽었을 때 재시작, 로그 수집(journalctl)까지 담당합니다
1
2
sudo systemctl status docker # dockerd가 systemd 관리 아래 떠 있는 것을 확인
sudo systemctl restart docker
- macOS: launchd가 같은 역할을 합니다. Docker Desktop 앱을 켜면 앱이 Linux VM과 그 안의 dockerd를 시작하는 것도 이 계열의 관리 방식입니다
프로그램 입장에서는 전경으로 도는 것과 다를 것이 없고, “누가 나를 상주시키고 살려두는가”의 책임만 밖으로 나간 것입니다.
내 컴퓨터에서 daemon 확인하기
프로세스 목록의 TTY 열이 이 글의 내용을 그대로 보여줍니다. TTY는 프로세스에 붙은 터미널을 뜻하는데, daemon은 터미널이 없으므로 ?(macOS는 ??)로 표시됩니다.
1
2
ps aux | head -20 # TTY 열이 ?인 프로세스들이 터미널 없이 상주하는 프로세스
pgrep -fl sshd # 특정 daemon이 떠 있는지 확인
Docker와 daemon: 두 가지 접점
첫째, Docker의 본체가 daemon입니다. docker라고 치는 CLI는 명령 한 번에 끝나는 보통 프로그램이고, 실제로 container를 관리하는 dockerd가 상주합니다. Cannot connect to the Docker daemon 에러는 이 상주 프로세스가 안 떠 있다는 뜻입니다. 이 분리 구조는 Docker 기초 2편에서 다뤘습니다.
둘째, container 안에서는 정반대의 관행이 적용됩니다. container는 메인 프로세스(PID 1)와 수명을 같이합니다. 만약 container 안의 앱이 고전적인 daemon화(fork 후 부모 종료)를 하면, PID 1이 종료되어 버려서 container 전체가 꺼집니다. 그래서 container 세계의 관행은 앱을 daemon으로 만들지 않고 전경으로, stdout에 로그를 찍으며 돌리는 것입니다. nginx 공식 image가 설정에 daemon off;를 넣어 nginx를 일부러 전경으로 실행하는 이유가 이것입니다. 재시작과 상주 관리는 init 시스템 대신 Docker가 밖에서 해줍니다.
정리
| 질문 | 답 |
|---|---|
| daemon이란 | 터미널과 세션에서 분리되어 백그라운드에 상주하는 프로세스 |
| 이름이 왜 d로 끝나나 | Unix 관례 (sshd, dockerd, systemd) |
&와 무엇이 다른가 | &는 같은 세션 소속이라 터미널이 닫히면 SIGHUP으로 죽는다 |
| 고전적 방법 | double fork, setsid, 표준 입출력을 /dev/null과 로그로 |
| 현대적 방법 | 프로그램은 전경으로 만들고 systemd, launchd가 상주를 관리 |
| container에서는 | 반대로 전경 실행이 관행. daemon화하면 PID 1이 죽어 container가 꺼진다 |
관련 글
- Docker 기초 1편: 이 글이 전제하는 프로세스, 표준 출력 기초
- Docker 기초 2편: CLI와 dockerd의 분리 구조