launchd

25 мая 2026 · ~14 мин чтения

инструмент macos init-system devops фоновые-задачи

launchd

launchd — это системный менеджер процессов в macOS и iOS. Он первый стартует при включении устройства (PID 1), а потом следит за всеми остальными демонами и фоновыми задачами: запускает по расписанию, перезапускает после падения, поднимает по событию (новый файл в папке, входящий сокет, появление сетевого интерфейса).

История

Что это такое

launchd — это две вещи одновременно: демон-наблюдатель (тот самый процесс с PID 1) и формат конфигурации для задач, которые он должен выполнять. Конфигурация — это XML-файл расширения .plist (property list), описывающий ровно одну задачу: что запустить, когда, от чьего имени, с какими переменными окружения, что делать, если упадёт.

В отличие от cron, launchd никогда не «забывает» задачу. Если ноутбук был выключен в момент, когда задача должна была сработать, launchd запустит её при следующей загрузке (если в plist указан RunAtLoad или пропущенное событие в StartCalendarInterval). В отличие от классического init, launchd следит за PID-ом своих детей: упал процесс — поднимет (если включён KeepAlive). В отличие от inetd, может слушать не только TCP-сокеты, но и события файловой системы, изменения сетевых интерфейсов, IOKit-уведомления, время.

Ключевая идея — декларативность. Ты не пишешь скрипт «запускай каждую минуту, проверяй вот это, делай то». Ты пишешь plist: «вот моя программа, запускай её в 9:00 каждый день, если в системе залогинен мой юзер». Всю логику расписаний, перезапуска и сборки мусора берёт на себя launchd.

Чем отличается от похожего.
- launchd vs systemd: оба — PID 1, оба декларативные. systemd использует свой формат юнит-файлов (.service, .timer, .socket), а launchd — plist XML. systemd живёт в Linux-мире, launchd — только в Apple.
- launchd vs cron: cron — это только про расписание, и расписание точное (cron-выражение). launchd умеет расписание (но грубее, чем cron), а ещё умеет события — файлы, сокеты, очереди.
- launchd vs systemd timer: systemd-таймеры умеют «accuracy» (распределить нагрузку), persistent (сработать после простоя), launchd-таймеры тоже умеют — через StartCalendarInterval и RunAtLoad.
- launchd vs ваш собственный «while true; do … sleep»-скрипт: ну, всегда смешно сравнивать. Свой цикл умрёт после reboot, не будет логировать, не справится с тем, что машина была в сне. launchd всё это закрывает.

Аналогии из жизни

1. Завхоз в гостинице.
Гость не задумывается, как в номере появилась горячая вода, чистые полотенца и почему лифт работает. Все эти процессы запускает и поддерживает завхоз: котельная — в 6 утра, горничные — после выезда, лифт — постоянно с проверками каждый час. launchd — это завхоз для маковских процессов: всё, что должно «просто работать», запускает он.

Где ломается: у завхоза один начальник — хозяин гостиницы. У launchd начальников два: системный (root) и пользовательский (твой логин). Можно посадить горничную с правами завхоза — и она снесёт лобби; точно так же неправильный plist может убить систему. Завхозу нельзя дать собственный sudo — а launchd внутри домена system имеет полный root.

2. Будильник, который сам поднимает с постели и тащит к кофеварке.
Cron — это будильник: прозвенел в 7:00. launchd — будильник, у которого внутри ещё есть программа на день: «если в 7:00 нет реакции — позвенеть в 7:05, если ноутбук был в сне до 9:00 — позвенеть сейчас». Это не просто триггер, а активный наблюдатель.

Где ломается: будильник умеет «снуз» — отложить. launchd этого не умеет в чистом виде. Если задача провалилась — он попытается её перезапустить, но не «давай в следующий раз», а «прямо сейчас, повторно». Поведение «дай мне поспать ещё час» придётся реализовывать руками в скрипте.

3. Регулировщик на перекрёстке.
inetd работал как светофор по расписанию: красный-зелёный по таймеру. launchd — регулировщик-человек: смотрит на поток, открывает движение, когда машины подъехали, останавливает, когда дорога свободна. Демон не висит в памяти впустую — поднимается только когда есть запрос, потом мирно засыпает.

Где ломается: регулировщик человек устаёт. launchd — нет. Зато регулировщик умеет нестандартное (пропустить скорую), а launchd — нет. Если задача сложнее «есть запрос — старт, нет — стоп», логику придётся писать самому.

Как это работает

В самом сердце launchd — список plist-файлов, разложенных по нескольким каталогам:

/System/Library/LaunchDaemons/   # системные демоны Apple (не трогать)
/System/Library/LaunchAgents/    # пользовательские агенты Apple (не трогать)
/Library/LaunchDaemons/          # сторонние демоны, права root
/Library/LaunchAgents/           # сторонние агенты для всех юзеров
~/Library/LaunchAgents/          # твои личные агенты (запускаются, когда залогинен)

Разница между Daemon и Agent простая: демон работает от системы (root, без интерфейса, всегда), агент — от пользователя (когда залогинен, есть доступ к GUI). Если ты пишешь блог-генератор, который должен запускаться в 9:00, когда ты сидишь за маком, тебе нужен LaunchAgent в ~/Library/LaunchAgents/.

Минимальный plist выглядит так:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.pavel.dailydigest</string>

    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>-lc</string>
        <string>/Users/pavel/daily-digest/scripts/run.sh</string>
    </array>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key><integer>9</integer>
        <key>Minute</key><integer>0</integer>
    </dict>

    <key>StandardOutPath</key>
    <string>/Users/pavel/daily-digest/logs/launchd.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/pavel/daily-digest/logs/launchd.err</string>
</dict>
</plist>

Ключевые поля.

Цикл жизни задачи.

  1. Bootstrap (регистрация). Ты говоришь системе: «вот мой plist, запомни его». Команда: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.pavel.dailydigest.plist. Старый аналог: launchctl load.
  2. Триггер (срабатывание). launchd ждёт условие: время, изменение файла, входящий сокет, ручной вызов. По срабатыванию — fork() + exec() твоего бинарника.
  3. Run (исполнение). launchd следит: пишет PID в свою таблицу, прокладывает stdout/stderr в указанные файлы, считает попытки.
  4. Exit (завершение). Если код выхода 0 — задача отработала чисто. Если не 0 и есть KeepAlive (например, SuccessfulExit: false) — launchd подождёт ThrottleInterval секунд (по умолчанию 10) и попробует снова. Без KeepAlive — задача просто заканчивается и ждёт следующего триггера.
  5. Bootout (снятие с регистрации). launchctl bootout gui/$(id -u)/com.pavel.dailydigest. Старый аналог: launchctl unload.

Полезные команды.

launchd не загружает твой shell-окружение

Когда ты пишешь ~/.zshrc или ~/.zshenv и кладёшь туда export ANTHROPIC_API_KEY=..., ты ожидаешь, что любой запущенный из этого шелла процесс увидит переменную. launchd её не увидит. Он запускает программу напрямую через fork() + exec(), без участия шелла. PATH будет минимальный (/usr/bin:/bin:/usr/sbin:/sbin), переменных окружения почти не будет.

Лечение: либо в ProgramArguments явно вызывать /bin/bash -lc 'команда' (-l заставит bash прочитать ~/.bash_profile), либо положить переменные прямо в plist через ключ EnvironmentVariables.

Где встречается в обычной жизни

Где встречается в IT и бизнесе

Кто пользуется

Альтернативы и конкуренты

Когда НЕ стоит использовать

Главное контринтуитивное в launchd

Это не «cron на маке». Это полноценный supervisor, который умеет тысячу триггеров, кроме времени. Если ты используешь его только для расписания — ты используешь 5% возможностей. WatchPaths («сработай, когда файл в этой папке изменился»), QueueDirectories («сработай, когда в папку положили новый файл»), Sockets («сработай, когда на порт пришло соединение») — всё это часто полезнее любого cron-выражения и решает задачи, для которых обычно пишут отдельные демоны.

Связанные понятия

Литература и источники

Где встретилось у меня

Вчера я разбирал доработку Telegram-бота-«Тренера» (/Cursor YD/ЛИЧНОЕ/агенты/Тренер/): нужно было превратить односторонний пуш в интерактивного бота с listener'ом и шестью фоновыми задачами по расписанию (утренний протокол, дневные напоминания, недельный rollup). Естественно, всё это — на launchd. Восемь plist-ов, у каждого свой StartCalendarInterval, свой wrapper-скрипт, чтобы подсосать переменные окружения. Сам этот блог тоже на launchd — com.pavel.dailydigest.plist поднял run.sh сегодня в 9:00.

Краткое резюме