launchd
launchd
launchd — это системный менеджер процессов в macOS и iOS. Он первый стартует при включении устройства (PID 1), а потом следит за всеми остальными демонами и фоновыми задачами: запускает по расписанию, перезапускает после падения, поднимает по событию (новый файл в папке, входящий сокет, появление сетевого интерфейса).
История
- 2005, США, Apple, Дэйв Зажицкий (Dave Zarzycki). launchd впервые появился в Mac OS X 10.4 Tiger — это была одна из самых заметных архитектурных перемен релиза. До этого macOS использовала классический BSD-стек:
initстартовал систему,cronзапускал задачи по расписанию,inetdслушал сетевые порты и запускал демоны по входящему соединению,mach_initподнимал mach-сервисы,watchdogперезапускал упавшие процессы. Каждый — со своей конфигурацией, своими логами, своей логикой. launchd собрал всё это в одну программу. - Какую проблему решали. Время загрузки. На Mac OS X 10.3 Panther система могла загружаться 40–60 секунд, потому что демоны стартовали последовательно: каждый ждал, пока предыдущий «прогреется». Zarzycki предложил радикальный подход — стартовать демоны лениво, по факту обращения. Если никто не пользуется CUPS (печатью), его не нужно запускать сразу; поднимем при первом обращении к сокету. Это сократило время загрузки в разы и попутно сделало систему опрятнее.
- 2006: launchd становится open source под лицензией Apache 2.0. Apple публикует исходники в репозитории
launchdна macosforge.org — туда даже принимали патчи извне. - 2010, апрель: Леннарт Поттеринг анонсирует systemd для Linux. В своих статьях он прямо ссылается на launchd как на источник вдохновения — особенно идею с активацией по сокету (socket activation). Systemd, правда, пошёл дальше: добавил cgroups, журнал, управление монтированием.
- 2012–2013: Apple перестаёт публиковать новые версии исходников. Последний открытый код — launchd-842 (примерно macOS 10.10 Yosemite). С Catalina (10.15) launchd глубже интегрировался с TCC (security framework), и значительная часть логики ушла в закрытые библиотеки.
- 2014, macOS Yosemite (10.10). Полностью переписан фронтенд
launchctl: появились понятия «доменов» (system,gui/<uid>,user/<uid>), командыbootstrap/bootout— заменили старыеload/unload. Старый синтаксис до сих пор работает, но Apple считает его deprecated. - Сейчас. launchd — единственный PID 1 во всех Apple-системах: macOS, iOS, iPadOS, watchOS, tvOS, visionOS. Без него не загружается ни один Apple-девайс.
Что это такое
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>
Ключевые поля.
Label— уникальный идентификатор. Обычно reverse-domain:com.имя.задача. Это то, чем ты будешь оперировать в командной строке.ProgramArguments— массив: первый элемент — путь к исполняемому файлу, остальные — аргументы. Здесь нельзя писать просто~/script.shилиscript.sh— только абсолютные пути. launchd не интерпретирует shell.StartCalendarInterval— расписание в духе cron, но с пропусками. Указал толькоHour: 9, Minute: 0— значит «каждый день в 9:00». Указал ещёWeekday: 1— «по понедельникам в 9:00». Указал массив словарей вместо одного — несколько расписаний.StandardOutPath/StandardErrorPath— куда писать stdout/stderr. Без них ты никогда не узнаешь, почему задача упала.
Цикл жизни задачи.
- Bootstrap (регистрация). Ты говоришь системе: «вот мой plist, запомни его». Команда:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.pavel.dailydigest.plist. Старый аналог:launchctl load. - Триггер (срабатывание). launchd ждёт условие: время, изменение файла, входящий сокет, ручной вызов. По срабатыванию —
fork()+exec()твоего бинарника. - Run (исполнение). launchd следит: пишет PID в свою таблицу, прокладывает stdout/stderr в указанные файлы, считает попытки.
- Exit (завершение). Если код выхода
0— задача отработала чисто. Если не0и естьKeepAlive(например,SuccessfulExit: false) — launchd подождётThrottleIntervalсекунд (по умолчанию 10) и попробует снова. БезKeepAlive— задача просто заканчивается и ждёт следующего триггера. - Bootout (снятие с регистрации).
launchctl bootout gui/$(id -u)/com.pavel.dailydigest. Старый аналог:launchctl unload.
Полезные команды.
launchctl list | grep com.pavel— какие задачи зарегистрированы.launchctl print gui/$(id -u)/com.pavel.dailydigest— подробный отчёт: текущий PID (или-если не запущена), последний код выхода, путь к plist, заполнение всех ключей.launchctl kickstart -k gui/$(id -u)/com.pavel.dailydigest— пнуть задачу прямо сейчас (с-k— даже если уже запущена).plutil -lint file.plist— проверить, что XML валиден.plutil -convert xml1 file.plist— нормализовать форматирование.
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.
Где встречается в обычной жизни
- Время Mac'а синхронизируется с серверами Apple — это launchd-демон
com.apple.timed.plist, который раз в N часов опрашивает NTP. Сам этот демон поднимает launchd по расписанию. - Spotlight индексирует диск в фоне —
com.apple.metadata.mds.plist. Он не висит постоянно; launchd поднимает его по событию «появился новый файл» (черезWatchPaths). - Time Machine делает резервную копию каждый час —
com.apple.backupd-auto.plist, тоже launchd-расписание. - Когда ноутбук просыпается ото сна — launchd ловит уведомление от IOKit и поднимает целую пачку задач: проверку обновлений, синхронизацию iCloud, восстановление сетевых соединений.
- iCloud Drive подхватывает изменения файлов — комбо:
WatchPathsв launchd + демонbird(это его кодовое имя). Поэтому когда ты сохраняешь файл в iCloud-папке, через секунду он улетает в облако без всякого «нажми синхронизировать».
Где встречается в IT и бизнесе
- Резервные копии разработческих машин. На маках разработчиков почти всегда стоят бэкапы кода в свой git-репозиторий, синхронизация ключей в 1Password, импорт логов в Datadog — всё через launchd.
- CI-агенты на маках. GitHub Actions runner, GitLab Runner, BuildKite agent — все они умеют ставиться как LaunchDaemon, потому что в маковском CI-парке нужно, чтобы агент сам поднимался после reboot.
- iOS/macOS-разработка: при создании демонов для собственного приложения (например, фоновый сервис для VPN или антивируса) Apple требует обернуть его в plist и зарегистрировать через launchd. Других путей запустить background-демон с правами в macOS нет.
- DevOps-автоматизация на личных машинах. Скрипты, которые подтягивают новости, генерируют отчёты, чистят
~/Downloads/старше 30 дней, синхронизируют заметки между Obsidian и git — стандартный паттерн «launchd + bash + cron-выражение». - Системное администрирование маков в компаниях (Jamf, Munki, Mosyle): через них в
/Library/LaunchDaemons/пушатся скрипты политики, проверки соответствия, отчётность.
Кто пользуется
- Apple — все её устройства. Это миллиарды активных устройств на 2026 год: iPhone (~1.4 млрд активных), Mac (~150 млн), iPad, Apple Watch, Apple TV, Vision Pro. У каждого внутри launchd как PID 1.
- Homebrew — самый популярный пакетный менеджер macOS — генерирует plist для сервисов, которые ты ставишь. Команда
brew services start postgresqlпод капотом — этоlaunchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist. - 1Password, Slack, Notion, Docker Desktop, Dropbox — все эти приложения регистрируют LaunchAgent, чтобы стартовать вместе с системой.
- CI/CD-инфраструктура на маках: MacStadium, AWS EC2 Mac instances, GitHub-hosted macOS runners. Все они опираются на launchd для запуска агентов и менеджмента билд-задач.
- Конкретно в этом блоге —
~/Library/LaunchAgents/com.pavel.dailydigest.plistкаждое утро в 9:00 запускаетscripts/run.sh, который собирает дайджест.
Альтернативы и конкуренты
- cron + nohup-демоны (классический Unix). Плюсы: универсально, работает на всём — Linux, BSD, macOS. Знакомый синтаксис cron-выражения, миллион туториалов. Минусы: не следит за процессами, не умеет события, не запустит пропущенную задачу. На современной macOS cron формально жив, но Apple не рекомендует его использовать.
- systemd (Linux). Плюсы: де-факто стандарт в Linux, мощнее launchd по числу фич (cgroups, журнал, мониторинг ресурсов). Минусы: не работает на macOS вообще. Сложнее в освоении из-за разнообразия типов юнитов.
- Apple Automator / Shortcuts. Плюсы: GUI, нулевой код. Минусы: расписание ограниченное, дебажить сложно, для разработческих задач почти не годится. Хороши для бытовых сценариев («каждый понедельник напомнить полить цветы»).
- n8n, Make.com, Zapier и прочие no-code-оркестраторы. Плюсы: не нужен мак онлайн. Минусы: платные, локальный код они выполнить не могут (нужен webhook на свою машину — а это уже снова launchd на маке).
Когда НЕ стоит использовать
- Если задача должна работать без тебя круглые сутки. LaunchAgent (в
~/Library/LaunchAgents/) живёт только когда твой пользователь залогинен. Закрыл крышку ноутбука — задача не сработает. Если нужна 24/7 — это либо LaunchDaemon на сервере, либо твою задачу надо хостить на отдельной машине (VPS, Raspberry Pi). - Если у тебя ферма из 50 маков. Для большого парка нужна централизованная оркестрация (Jamf Pro, Mosyle, Munki) — голый launchd на каждом маке станет неуправляемым.
- Если нужны cron-выражения вроде «каждые 15 минут, кроме выходных, кроме праздников». launchd-расписание грубее cron-а: ты можешь задать только конкретные числа (Hour, Minute, Day, Weekday). Сложные шаблоны типа
*/15 * * * 1-5придётся складывать из нескольких словарей в массив, и это становится грязно. Проще — внутри скрипта первым делом сделатьif [ $(date +%u) -gt 5 ]; then exit 0; fi.
Главное контринтуитивное в launchd
Это не «cron на маке». Это полноценный supervisor, который умеет тысячу триггеров, кроме времени. Если ты используешь его только для расписания — ты используешь 5% возможностей. WatchPaths («сработай, когда файл в этой папке изменился»), QueueDirectories («сработай, когда в папку положили новый файл»), Sockets («сработай, когда на порт пришло соединение») — всё это часто полезнее любого cron-выражения и решает задачи, для которых обычно пишут отдельные демоны.
Связанные понятия
- plist (property list) — формат XML или бинарной сериализации, придуманный в NeXTSTEP (предок macOS) для конфигов и кэшей. launchd использует только XML-вариант.
- PID 1 — самый первый процесс ядра. На Linux это
initилиsystemd, на macOS —launchd. Если PID 1 падает — система перезагружается. - fork/exec — пара POSIX-вызовов:
forkклонирует процесс,execзаменяет код в клоне на твой бинарник. launchd использует именно эту классическую пару для запуска задач. - socket activation — техника «запустить демон только когда на сокет пришло первое соединение». Изобретена для launchd, перенята systemd.
- inetd — древний BSD-демон, делал ровно socket activation для сетевых сервисов. launchd вобрал и заменил его.
- systemd — Linux-аналог launchd, шире по функциональности, политически спорный.
- cron — классический расписатель Unix-задач; на macOS формально жив, но Apple рекомендует launchd.
- TCC (Transparency, Consent, Control) — подсистема безопасности macOS, через которую launchd-задачи получают (или не получают) доступ к камере, микрофону, контактам и т.д. С 2019 года плотно интегрирована с launchd.
Литература и источники
man launchd.plistв терминале — самая полная справка по ключам plist. Apple обновляет её с каждой версией macOS. Это первая остановка для любого вопроса «какой ключ за что отвечает».man launchctl— справка по командам управления.- «Mac OS X Internals: A Systems Approach», Amit Singh, 2006, en. Книга устарела в деталях, но глава про launchd до сих пор лучшее введение в архитектуру: почему так сделано, какие проблемы решали.
- «*OS Internals, Volume I: User Mode», Jonathan Levin, 2017, en. Современный том про внутренности macOS/iOS, включая launchd. Это уже учебник для security-исследователей.
- Wikipedia: launchd —
en.wikipedia.org/wiki/Launchd. Хорошее краткое введение, список ссылок на исторические материалы. - Apple Developer Documentation: «Creating Launch Daemons and Agents» —
developer.apple.com/documentation/xcode/creating-launch-daemons-and-agents. Актуальная документация с примерами. - Lennart Poettering: «Rethinking PID 1» (2010, en) — статья, где автор systemd признаёт launchd как идейного предшественника. Искать в Google по такому запросу.
- Launchd Tutorial by launchd.info —
launchd.info— бесплатный сайт-учебник с примерами для всех типичных задач (расписания, watchpaths, sockets). Третий-четвёртый результат гугла по запросу «launchd tutorial».
Где встретилось у меня
Вчера я разбирал доработку Telegram-бота-«Тренера» (/Cursor YD/ЛИЧНОЕ/агенты/Тренер/): нужно было превратить односторонний пуш в интерактивного бота с listener'ом и шестью фоновыми задачами по расписанию (утренний протокол, дневные напоминания, недельный rollup). Естественно, всё это — на launchd. Восемь plist-ов, у каждого свой StartCalendarInterval, свой wrapper-скрипт, чтобы подсосать переменные окружения. Сам этот блог тоже на launchd — com.pavel.dailydigest.plist поднял run.sh сегодня в 9:00.
Краткое резюме
- launchd — PID 1 на всех Apple-устройствах с 2005 года, замена паре init+cron+inetd+watchdog.
- Конфигурация — XML-файл
.plistс обязательнымиLabelиProgramArguments; расписание задаётсяStartCalendarInterval, перезапуск при падении —KeepAlive. - Главные грабли: launchd не подтягивает
~/.zshenv/~/.bashrc, нужно либо зватьbash -lc, либо класть переменные вEnvironmentVariables. - Возможности шире cron-а: триггер по файлу (
WatchPaths), по очереди файлов (QueueDirectories), по сокету. Это уже не «таймер», а полноценный supervisor. - Альтернатив на macOS нет — Apple намеренно загнала всю фоновую работу в launchd; cron жив только из совместимости.