138. Демон и его журнал: setsid, syslog, файл блокировки, backtrace, core
Программа, которая должна работать всегда — сервер, сборщик данных с
приборов, мост между портом и сетью, — не может зависеть от терминала, из
которого её запустили. Такой процесс называют демоном: он живёт в
фоне, без терминала, пишет в журнал, а не на экран, и умеет по сигналу
перечитать настройки и корректно остановиться. Второй половиной раздела
идёт то, без чего демон слеп: собственная библиотека журналирования со
стеком вызовов при ошибке и посмертная отладка по файлу core.
Демон и журнал из этого раздела написаны и прогнаны в stm-sys:1
(_verify/sys138/): ps показывает, что у демона нет терминала, /proc —
что его дескрипторы смотрят в /dev/null, а второй экземпляр отказывается
запускаться.
Как это называется в документации
Оригинал Перевод daemon демон: фоновый процесс без терминала controlling terminal управляющий терминал session, session leader сеанс, лидер сеанса process group группа процессов double fork двойной forkpid file, lock file файл с номером процесса, файл блокировки advisory lock рекомендательная блокировка ( fcntl)syslog, facility, priority системный журнал, источник, важность reload on SIGHUP перечитать настройки по SIGHUPunit file файл службы systemd backtrace стек вызовов core dump, post-mortem debugging слепок памяти, посмертная отладка
138.1 Сеанс, группа, терминал
У каждого процесса три номера родства: pid, группа процессов pgid и
сеанс sid. Оболочка держит все процессы одного конвейера в одной
группе, а все свои группы — в одном сеансе, у которого есть управляющий
терминал. Ctrl-C шлёт SIGINT всей группе переднего плана; закрытие
терминала шлёт SIGHUP лидеру сеанса и дальше всем. Программа, которую
запустили из терминала и просто оставили, умрёт вместе с ним.
Демон должен выйти из этой иерархии: стать лидером собственного сеанса, у которого терминала нет и не будет.
138.2 Семь шагов демонизации
static void daemonize(void)
{
umask(022); /* 1. маска прав — известная и безопасная */
pid_t pid = fork(); /* 2. первый fork: оболочка получает управление */
if (pid < 0) exit(1);
if (pid > 0) exit(0); /* родитель уходит — потомок становится сиротой */
setsid(); /* 3. новый сеанс: без управляющего терминала */
signal(SIGHUP, SIG_IGN);
pid = fork(); /* 4. второй fork: не лидер сеанса — терминал не получить */
if (pid > 0) exit(0);
chdir("/"); /* 5. корень: не держать смонтированный каталог */
struct rlimit rl; getrlimit(RLIMIT_NOFILE, &rl);
if (rl.rlim_max == RLIM_INFINITY) rl.rlim_max = 1024;
for (int i = 0; i < (int)rl.rlim_max; i++) close(i); /* 6. закрыть всё унаследованное */
open("/dev/null", O_RDWR); dup(0); dup(0); /* 7. 0, 1, 2 → /dev/null */
}
Зачем каждый шаг:
umask(022)— маска прав известная и безопасная: иначе файлы, которые создаст демон, получат права, унаследованные неизвестно от кого.umask(0)тоже «известная», но тогдаfopenсоздаёт файлы с правами 0666, и писать в них сможет любой пользователь — а демон часто работает от root. Если посторонним нельзя и читать —umask(027).- Первый
fork— родитель выходит, оболочка думает, что команда завершилась; потомок — не лидер группы, а это условие дляsetsid. setsid— процесс становится лидером нового сеанса и новой группы и теряет управляющий терминал:pid = sid = pgid.- Второй
fork— лидер сеанса может снова получить терминал, открыв терминальное устройство; не-лидер — нет. Библиотечнаяdaemon(3)из glibc этот шаг пропускает, и её демон остаётся лидером сеанса. chdir("/")— процесс в каталоге на съёмном диске не даст его отмонтировать.- Закрыть дескрипторы — унаследованные файлы и сокеты держат ресурсы.
- 0, 1, 2 в
/dev/null— чтобы забытыйprintfне упал с ошибкой и не открыл чужой файл под номером 1.
Результат виден в ps: демон с pid 52 — потомок init (ppid 1), сеанс
51, терминала нет (?), состояние S:
52 1 51 51 ? S daemon
Его дескрипторы:
0 -> /dev/null
1 -> /dev/null
2 -> /dev/null
3 -> /tmp/demo-daemon.pid
cwd: /
nohup и setsid из оболочки делают часть той же работы для чужой
программы; systemd (138.5) делает её всю, и современному демону
демонизировать себя не нужно — нужен ключ, чтобы этого не делать
(здесь -f).
138.3 Один экземпляр: файл блокировки
Два экземпляра демона на одном порту или одном приборе — беда. Классика:
файл /var/run/имя.pid с записанным номером процесса и рекомендательной
блокировкой на нём:
static int already_running(void)
{
int fd = open(LOCKFILE, O_RDWR | O_CREAT, 0644);
if (fd < 0) return -1;
struct flock fl = { .l_type = F_WRLCK, .l_whence = SEEK_SET };
if (fcntl(fd, F_SETLK, &fl) < 0) { /* блокировка снимается ядром при смерти процесса */
if (errno == EACCES || errno == EAGAIN) { close(fd); return 1; }
return -1;
}
ftruncate(fd, 0);
dprintf(fd, "%ld\n", (long)getpid());
return 0; /* дескриптор не закрываем — блокировка живёт с ним */
}
Почему блокировка, а не просто «файл существует»: после падения демона
файл остаётся, и проверка на существование не пустит новый экземпляр
никогда. Блокировку fcntl ядро снимает при смерти процесса само. Второй
экземпляр из прогона:
demo-daemon[60]: уже запущен, файл /tmp/demo-daemon.pid занят
rc=1
Дескриптор файла блокировки не закрывают: close любого дескриптора
этого файла в процессе снимает его блокировки — особенность fcntl,
которой нет у flock.
138.4 Журнал через syslog и перечитывание по SIGHUP
Демон не имеет stderr, и его журнал — syslog:
openlog("demo-daemon", LOG_PID | (foreground ? LOG_PERROR : 0), LOG_DAEMON);
openlog задаёт имя, ключи (LOG_PID — добавлять номер процесса,
LOG_PERROR — дублировать в stderr) и источник (LOG_DAEMON,
LOG_USER, LOG_LOCAL0…LOG_LOCAL7), по которому служба журнала
раскладывает записи по файлам. У каждой записи — важность, от
LOG_EMERG до LOG_DEBUG; %m в формате подставляет strerror(errno):
if (!f) { syslog(LOG_WARNING, "нет файла настроек %s: %m", path); return; }
Записи уходят в сокет /dev/log, где их принимает rsyslog, syslog-ng
или journald и кладёт в /var/log или в journalctl. В контейнере
службы журнала нет — сокета /dev/log не существует, и записи
пропадают; поэтому в прогоне демон запущен с LOG_PERROR.
Настройки демон перечитывает по SIGHUP, а останавливается по SIGTERM.
Обработчик только ставит флаг (раздел 136), главный цикл его проверяет:
while (!stop) {
if (reload) { reload = 0; read_config(cfg); }
syslog(LOG_DEBUG, "тик %d", ++tick);
sleep(period);
}
syslog(LOG_INFO, "остановлен по сигналу после %d тиков", tick);
unlink(LOCKFILE);
Порядок, откуда берутся настройки, у хороших демонов один: аргументы
командной строки важнее переменных окружения, те — важнее файла
пользователя, тот — важнее системного файла, а внизу — разумные
умолчания. Форматы файлов и библиотеки под них: INI (iniparser), JSON
(jansson, cJSON), YAML (libyaml), а для сложных случаев — Lua.
138.5 Запуск при загрузке: systemd
Раньше демонов запускали сценариями /etc/init.d/имя с start-stop-daemon;
сегодня почти везде systemd, и описание службы — файл unit:
[Unit]
Description=Demo daemon
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/demo-daemon -f /etc/demo-daemon.conf
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
[Install]
WantedBy=multi-user.target
Type=simple — программа не демонизируется, systemd сам держит её в
фоне, следит за pid, собирает stdout и stderr в журнал и
перезапускает при падении. Type=forking — для старых демонов с двойным
fork и PIDFile=. Поэтому современный демон обязан уметь работать в
переднем плане: ключ -f в примере и есть режим для systemd. Команды:
systemctl start|stop|reload|status имя, журнал — journalctl -u имя.
138.6 Своя библиотека журналирования
syslog хорош для демона, но приложению часто нужен свой журнал: в файл,
с уровнями, с местом в коде, потокобезопасный и работающий даже тогда,
когда кончилась память. Интерфейс — макросы, которые подставляют файл,
строку и функцию:
#define LOG(lvl, ...) log_write(lvl, __FILE__, __LINE__, __func__, __VA_ARGS__)
#define LOG_D(...) LOG(LOG_DEBUG, __VA_ARGS__)
#define LOG_I(...) LOG(LOG_INFO, __VA_ARGS__)
#define LOG_W(...) LOG(LOG_WARN, __VA_ARGS__)
#define LOG_E(...) LOG(LOG_ERROR, __VA_ARGS__)
Три решения внутри log_write:
- Ничего из кучи. Строка собирается в буфере на стеке
char buf[1024]черезsnprintf/vsnprintfи уходит однимwrite. Журнал должен работать, когдаmallocвернулNULL, — а именно тогда он нужнее всего. - Мьютекс вокруг записи. Одна строка одним
writeв обычный файл не разрежется и без него. Но ошибка — это строка плюс стек из несколькихwrite, и без мьютекса между ними влезут строки других потоков. Четыре потока по сто строк пишут в один файл, и всё идёт по порядку. - Стек вызовов при ошибке.
backtraceсобирает адреса кадров,backtrace_symbols_fdпишет их прямо в дескриптор — тоже безmalloc. С оговоркой: при первом вызовеbacktraceподгружает библиотеку раскрутки стекаlibgcc_s, а загрузка сама зовётmalloc— в опыте пять раз, второй вызов уже ни разу. Поэтомуlog_initделает один холостой вызовbacktrace: в аварийный момент библиотека уже на месте.
pthread_mutex_lock(&log_mx); /* строки разных потоков не перемешиваются */
write(log_fd, buf, n);
if (lvl == LOG_ERROR) { /* при ошибке — стек вызовов */
void *frames[32];
int depth = backtrace(frames, 32);
backtrace_symbols_fd(frames, depth, log_fd); /* пишет прямо в дескриптор, без malloc */
}
pthread_mutex_unlock(&log_mx);
Прогон: 412 строк, из них 401 INFO (одна из main и 400 из четырёх
потоков), одна ошибка со стеком из восьми кадров (ниже первые пять, дальше —
два кадра libc и _start):
2026-09-13 15:41:57 DEBUG log_test.c:21 main(): отладочная строка
2026-09-13 15:41:57 INFO log_test.c:22 main(): старт, аргументов 1
2026-09-13 15:41:57 WARN log_test.c:23 main(): предупреждение
2026-09-13 15:41:57 ERROR log_test.c:14 deep3(): что-то пошло не так: malloc вернул NULL
./log_test(log_write+0x238)[0x5e9e690876da]
./log_test(+0x12b6)[0x5e9e690872b6]
./log_test(+0x12c2)[0x5e9e690872c2]
./log_test(+0x12ce)[0x5e9e690872ce]
./log_test(main+0x12d)[0x5e9e690873fe]
Имена в стеке есть только у функций, видимых компоновщику: log_write и
main. Статические deep1–deep3 показаны смещениями — их имена
восстанавливает addr2line -e log_test 0x12b6 по отладочной информации
(-g). Ключ -rdynamic при сборке кладёт имена внешних функций в таблицу
динамических символов, без него и main был бы смещением. При пороге
LOG_INFO строк DEBUG в файле 0.
138.7 Посмертная отладка: core и gdb
Когда демон падает ночью, стека из журнала мало — нужен слепок памяти.
ulimit -c unlimited разрешает ядру записать core при SIGSEGV,
SIGABRT и других сигналах с действием «core»; куда и под каким именем —
задаёт /proc/sys/kernel/core_pattern. Затем:
gdb -q -batch -ex bt ./log_test core
печатает стек на момент падения, а интерактивный gdb даёт frame N,
print переменная, info locals — всё, как при живой отладке, только
процесс уже мёртв. Для этого исполняемый файл собирают с -g и не
удаляют отладочную информацию: у выпуска её отделяют в файл .debug
(objcopy --only-keep-debug) и хранят рядом с версией.
В контейнере из прогона core_pattern указывает на программу-перехватчик
хоста (|/wsl-capture-crash …), и файл core в текущем каталоге не
появляется — падение видно только по коду 139 (128 + 11). Так же ведёт
себя systemd-coredump на обычных дистрибутивах: слепки лежат в
/var/lib/systemd/coredump, читают их через coredumpctl gdb.
138.8 Грабли
| Симптом | Причина |
|---|---|
| демон умирает при закрытии терминала | нет setsid: остался в сеансе оболочки и получил SIGHUP |
| демон вдруг получил терминал | лидер сеанса открыл терминальное устройство — нужен второй fork |
| диск не отмонтировать | демон сидит в каталоге на нём — chdir("/") |
| после падения демон не запускается: «уже запущен» | проверка по существованию pid-файла; нужна блокировка fcntl |
| блокировка пропала сама | закрыт какой-то дескриптор того же файла — fcntl снимает все |
syslog ничего не пишет |
нет /dev/log (контейнер, минимальная система) — LOG_PERROR или свой файл |
в systemd служба «падает» сразу после старта |
Type=simple, а программа демонизируется сама — либо -f, либо Type=forking |
| в стек ошибки влезли строки других потоков | нет мьютекса вокруг записи строки и стека |
| журнал падает вместе с программой при нехватке памяти | malloc внутри журнала — только стек и write |
| в стеке одни адреса | static-функции и сборка без -rdynamic; addr2line по -g |
core не появляется |
ulimit -c 0 или core_pattern ведёт в перехватчик |
138.9 Итоги
- Демонизация:
umask,fork,setsid,fork,chdir("/"), закрыть дескрипторы, 0/1/2 в/dev/null; результат —ppid1, терминал?. - Один экземпляр — блокировка
fcntlна pid-файле; ядро снимает её при смерти. - Журнал демона —
syslogс источником и важностью;SIGHUP— перечитать,SIGTERM— остановиться. systemd:Type=simple, программа работает в переднем плане.- Своя библиотека: макросы с
__FILE__/__LINE__, буфер на стеке, мьютекс,backtrace_symbols_fdпри ошибке;-rdynamicдля имён. ulimit -c,core_pattern,gdb -ex bt— посмертная отладка.
Полные исходники
daemon.c — демонизация по шагам, файл блокировки, syslog и перечитывание настроек по SIGHUP.
/* Раздел 138: демонизация по шагам, файл блокировки, syslog, перечитывание настроек по SIGHUP.
Запуск: ./daemon <файл настроек>; ./daemon -f — в переднем плане, без демонизации. */
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/resource.h>
#define LOCKFILE "/tmp/demo-daemon.pid"
static volatile sig_atomic_t reload, stop;
static int period = 1;
static void on_sig(int s) { if (s == SIGHUP) reload = 1; else stop = 1; }
static void daemonize(void)
{
umask(022); /* 1. маска прав — известная и безопасная */
pid_t pid = fork(); /* 2. первый fork: оболочка получает управление */
if (pid < 0) exit(1);
if (pid > 0) exit(0); /* родитель уходит — потомок становится сиротой */
setsid(); /* 3. новый сеанс: без управляющего терминала */
signal(SIGHUP, SIG_IGN);
pid = fork(); /* 4. второй fork: не лидер сеанса — терминал не получить */
if (pid > 0) exit(0);
chdir("/"); /* 5. корень: не держать смонтированный каталог */
struct rlimit rl; getrlimit(RLIMIT_NOFILE, &rl);
if (rl.rlim_max == RLIM_INFINITY) rl.rlim_max = 1024;
for (int i = 0; i < (int)rl.rlim_max; i++) close(i); /* 6. закрыть всё унаследованное */
open("/dev/null", O_RDWR); dup(0); dup(0); /* 7. 0, 1, 2 → /dev/null */
}
static int already_running(void)
{
int fd = open(LOCKFILE, O_RDWR | O_CREAT, 0644);
if (fd < 0) return -1;
struct flock fl = { .l_type = F_WRLCK, .l_whence = SEEK_SET };
if (fcntl(fd, F_SETLK, &fl) < 0) { /* блокировка снимается ядром при смерти процесса */
if (errno == EACCES || errno == EAGAIN) { close(fd); return 1; }
return -1;
}
ftruncate(fd, 0);
dprintf(fd, "%ld\n", (long)getpid());
return 0; /* дескриптор не закрываем — блокировка живёт с ним */
}
static void read_config(const char *path)
{
FILE *f = fopen(path, "r");
if (!f) { syslog(LOG_WARNING, "нет файла настроек %s: %m", path); return; }
if (fscanf(f, "period=%d", &period) != 1) period = 1;
fclose(f);
syslog(LOG_INFO, "настройки прочитаны: period=%d", period);
}
int main(int argc, char **argv)
{
int foreground = argc > 1 && strcmp(argv[1], "-f") == 0;
const char *cfg = argc > 2 ? argv[2] : "/tmp/demo-daemon.conf";
if (!foreground) daemonize();
openlog("demo-daemon", LOG_PID | (foreground ? LOG_PERROR : 0), LOG_DAEMON);
if (already_running()) { syslog(LOG_ERR, "уже запущен, файл %s занят", LOCKFILE); return 1; }
struct sigaction sa = { .sa_handler = on_sig };
sigemptyset(&sa.sa_mask);
sigaction(SIGHUP, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGINT, &sa, NULL);
read_config(cfg);
syslog(LOG_INFO, "старт: pid %ld sid %ld pgid %ld", (long)getpid(), (long)getsid(0), (long)getpgid(0));
int tick = 0;
while (!stop) {
if (reload) { reload = 0; read_config(cfg); }
syslog(LOG_DEBUG, "тик %d", ++tick);
sleep(period);
}
syslog(LOG_INFO, "остановлен по сигналу после %d тиков", tick);
unlink(LOCKFILE);
return 0;
}
logger.h — интерфейс библиотеки журналирования.
/* Раздел 138: интерфейс журнала. Макросы подставляют файл, строку и функцию. */
#ifndef LOGGER_H
#define LOGGER_H
#include <fcntl.h>
enum log_level { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR };
int log_init(const char *path, enum log_level min); /* path == NULL — в stderr */
void log_write(enum log_level lvl, const char *file, int line, const char *func, const char *fmt, ...)
__attribute__((format(printf, 5, 6)));
void log_close(void);
#define LOG(lvl, ...) log_write(lvl, __FILE__, __LINE__, __func__, __VA_ARGS__)
#define LOG_D(...) LOG(LOG_DEBUG, __VA_ARGS__)
#define LOG_I(...) LOG(LOG_INFO, __VA_ARGS__)
#define LOG_W(...) LOG(LOG_WARN, __VA_ARGS__)
#define LOG_E(...) LOG(LOG_ERROR, __VA_ARGS__)
#endif
logger.c — библиотека журналирования: уровни, место в коде, стек вызовов, мьютекс.
/* Раздел 138: библиотека журналирования — уровни, место в коде, стек вызовов при ошибке,
потокобезопасность через мьютекс, пригодность при нехватке памяти (без malloc). */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <execinfo.h>
#include "logger.h"
static int log_fd = -1;
static enum log_level log_min = LOG_DEBUG;
static pthread_mutex_t log_mx = PTHREAD_MUTEX_INITIALIZER;
static const char *names[] = { "DEBUG", "INFO", "WARN", "ERROR" };
int log_init(const char *path, enum log_level min)
{
log_fd = path ? open(path, O_WRONLY | O_CREAT | O_APPEND, 0644) : STDERR_FILENO;
log_min = min;
void *warm[1];
backtrace(warm, 1); /* подгрузить libgcc_s сейчас: первый вызов просит malloc */
return log_fd < 0 ? -1 : 0;
}
void log_write(enum log_level lvl, const char *file, int line, const char *func, const char *fmt, ...)
{
if (lvl < log_min || log_fd < 0) return;
char buf[1024]; /* всё на стеке: malloc может быть недоступен */
time_t now = time(NULL);
struct tm tm; localtime_r(&now, &tm);
int n = strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S ", &tm);
n += snprintf(buf + n, sizeof buf - n, "%-5s %s:%d %s(): ", names[lvl], file, line, func);
va_list ap; va_start(ap, fmt);
n += vsnprintf(buf + n, sizeof buf - n, fmt, ap);
va_end(ap);
if (n > (int)sizeof buf - 2) n = sizeof buf - 2;
buf[n++] = '\n';
pthread_mutex_lock(&log_mx); /* строки разных потоков не перемешиваются */
write(log_fd, buf, n);
if (lvl == LOG_ERROR) { /* при ошибке — стек вызовов */
void *frames[32];
int depth = backtrace(frames, 32);
backtrace_symbols_fd(frames, depth, log_fd); /* пишет прямо в дескриптор, без malloc */
}
pthread_mutex_unlock(&log_mx);
}
void log_close(void) { if (log_fd > 2) close(log_fd); log_fd = -1; }
Дальше: раздел 139 — потоки POSIX и синхронизация.