Embedded HandbookПриложения
Все разделы › XVII. Системное программирование на C
· уровень средний · 17 мин чтения
Открыть в интерактивном справочнике

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 двойной fork
pid file, lock file файл с номером процесса, файл блокировки
advisory lock рекомендательная блокировка (fcntl)
syslog, facility, priority системный журнал, источник, важность
reload on SIGHUP перечитать настройки по SIGHUP
unit file файл службы systemd
backtrace стек вызовов
core dump, post-mortem debugging слепок памяти, посмертная отладка

138.1 Сеанс, группа, терминал

У каждого процесса три номера родства: pid, группа процессов pgid и сеанс sid. Оболочка держит все процессы одного конвейера в одной группе, а все свои группы — в одном сеансе, у которого есть управляющий терминал. Ctrl-C шлёт SIGINT всей группе переднего плана; закрытие терминала шлёт SIGHUP лидеру сеанса и дальше всем. Программа, которую запустили из терминала и просто оставили, умрёт вместе с ним.

Демон должен выйти из этой иерархии: стать лидером собственного сеанса, у которого терминала нет и не будет.

Семь шагов демонизации: umask, fork, setsid, fork, chdir, закрыть дескрипторы, 0/1/2 в /dev/null


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 */
}

Зачем каждый шаг:

  1. umask(022) — маска прав известная и безопасная: иначе файлы, которые создаст демон, получат права, унаследованные неизвестно от кого. umask(0) тоже «известная», но тогда fopen создаёт файлы с правами 0666, и писать в них сможет любой пользователь — а демон часто работает от root. Если посторонним нельзя и читать — umask(027).
  2. Первый fork — родитель выходит, оболочка думает, что команда завершилась; потомок — не лидер группы, а это условие для setsid.
  3. setsid — процесс становится лидером нового сеанса и новой группы и теряет управляющий терминал: pid = sid = pgid.
  4. Второй fork — лидер сеанса может снова получить терминал, открыв терминальное устройство; не-лидер — нет. Библиотечная daemon(3) из glibc этот шаг пропускает, и её демон остаётся лидером сеанса.
  5. chdir("/") — процесс в каталоге на съёмном диске не даст его отмонтировать.
  6. Закрыть дескрипторы — унаследованные файлы и сокеты держат ресурсы.
  7. 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.

Второй запуск демона до и после падения первого: блокировка fcntl против проверки существования файла


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:

    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 Итоги

Полные исходники

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 и синхронизация.

Открыть в интерактивном справочнике