что такое служба демон
Что такое демоны и службы в программировании
Это маленькие программы, которые работают в фоновом режиме
Иногда в разговорах программистов и системных администраторов можно услышать такие фразы: «А ты в курсе, что у тебя демон перестал работать?» или «Слушай, за это вообще другая служба отвечает, надо именно её запускать». Рассказываем, что это значит.
Что такое демоны
Демон (daemon) — это программа в UNIX-системах, которая постоянно работает фоном и выполняет какую-то одну свою задачу. UNIX-системы — это сам Unix, Linux, BSD, Solaris, MacOS и ещё много других. Про Юникс мы расскажем отдельно.
Чаще всего демоны запускаются при старте системы и работают всё время, пока работает компьютер или сервер.
У демонов обычно нет графического интерфейса, а чтобы управлять демонами, используют командную строку, файлы с настройками или специальные программы, которые отправляют демону нужные команды. Более того, демоны проектируются именно так, чтобы сидеть на фоне и не отсвечивать: если вам пришлось настраивать демонов, вы вряд ли будете читать эту статью.
Что такое службы
Службы — это то же самое, что и демоны, только в Windows.
Для управления службами в Windows сделали специальные инструменты — панель управления службами и оснастки. Панель позволяет управлять службами в целом, а оснастки — задавать тонкие настройки процессам в отдельности. Оба этих инструмента помогают смотреть и настраивать службы не через консоль, а с помощью графического интерфейса и окон.
Зачем нужны демоны и службы
Каждая из таких программ отвечает за свой участок работ:
Получается, что демоны — это как работники в гипермаркете: одни следят за выкладкой, вторые работают на кассе, третьи следят за чистотой, четвёртые разгружают и так далее. У каждого своя специализация и фронт работ.
Агенты загрузки и объекты автозапуска
Часто демонов путают с Launch Agents и Startup Items (на MacOS и Windows соответственно). Это общее название программ, которые запускаются при запуске компьютера и входе в систему. Но, в отличие от демонов, у этих агентов может быть графический интерфейс. Например, если при входе в систему автоматически запускается торрент-клиент или приложение для VPN, то это агенты загрузки. Вы можете ими пользоваться.
А демоны невидимы и просто делают так, чтобы компьютер работал — порты пробрасывались, сокеты открывались, данные туда-сюда ходили, жёсткие диски были видны и т. д. Можно сказать, что агенты загрузки — это ваши полезные привычки; а демоны — это ваши инстинкты.
Уровни доступа и разрешения
В UNIX-системах есть довольно строгое разделение, что можно и нельзя делать на разных уровнях допуска. Например, если ты пользователь без администраторских прав, ты не можешь просто так залезть в папку к другому пользователю компьютера и посмотреть, что там у него лежит. Поэтому если вы зашли в систему со своим логином и паролем и запустили веб-сервер, то он сможет веб-серверить только опираясь на ваши собственные файлы.
Демоны в UNIX-системах обычно запускаются на уровне системных и служебных пользователей, то есть живут над обычными пользователями, которые садятся и вводят свой логин и пароль. Вы ещё не залогинились, а демоны уже шуршат портами.
А вот когда вы вошли, то и ваш скрипт автозапуска при логине может запустить других демонов. Они уже, в свою очередь, запускаются на уровне вашего пользователя, и им в системе можно ровно то, что можно вам.
Антивирусы — это демоны?
У антивирусов может быть глубокая системная часть, которая защищает вашу систему на глубочайшем уровне, и это будет демон. Также у антивируса может быть приложение с интерфейсом — это будет просто приложение. Приложение может общаться с демоном, но если вы выйдете из приложения, демон продолжит работать фоном.
Можно ли сделать собственного демона?
Да, можно. Потом нужно будет поковыряться в конфигах системы, чтобы научить её запускать вашего демона с нужными правами доступа. Но в целом ничто не мешает.
Например, если у вас на предприятии используется ультрасекретная система выключения экрана при отводе глаз, то можно написать демона: он будет через камеру следить за глазами и чуть что — выключать монитор. И это будет работать вне зависимости от того, зашёл пользователь в систему или просто мимо проходил.
Почему такое название?
Слово заимствовано из латинского daemon, а оно, в свою очередь, из древнегреческого. И у тех и у других слово означало любого духа: злого, доброго, главное — сверхъестественного. Программы-демоны ровно так и работают: на фоне и незаметно.
В русский язык слово «демон» пришло из греческого именно в значении «злой дух», а в русский айтишный — из английского. Никакой чертовщины в айтишных демонах не предусмотрено.
Что такое демоны (daemons) в Linux?
Обновл. 20 Июл 2021 |
В этой статье мы рассмотрим, что такое демоны (и их примеры) в Linux, а также версии происхождения термина «daemon».
Что такое демоны?
Демоны (англ. «daemons») — это работающие в фоновом режиме служебные программы (или процессы), целью которых является мониторинг определенных подсистем ОС и обеспечение её нормальной работы. Например, демон принтера контролирует возможности печати, демон сети контролирует и поддерживает сетевые коммуникации и т.д.
Демоны являются аналогом служб (services) в Windows: они выполняют определенные действия в заранее определенное время или в ответ на определенные события. Существует множество различных демонов, работающих в Linux, каждый из которых создан специально для наблюдения за своей собственной маленькой частью системы. Из-за того, что демоны выполняют основную часть своей работы в фоновом режиме и не находятся под прямым контролем пользователя, бывает трудно определить предназначение того или иного демона.
Так как демон — это процесс, который выполняется в фоновом режиме и обычно находится вне контроля пользователя, то у него нет управляющего терминала.
Процесс — это запущенная программа. В определенный момент времени процесс может либо выполняться, либо ожидать, либо быть «зомби».
В Linux существует три типа процессов:
Процессы переднего плана (или «интерактивные процессы») — это те процессы, которые запускаются пользователем в терминале.
Фоновые процессы (или «автоматические процессы») — это объединенные в список процессы, не подключенные к терминалу; они не ожидают пользовательского ввода данных.
Демоны (англ. «daemons») — это особый тип фоновых процессов, которые запускаются при старте системы и продолжают работать в виде системных служб; они не умирают.
Процессы переднего плана и фоновые процессы не являются демонами, хотя их можно запускать в фоновом режиме и выполнять некоторую работу по мониторингу системы. Для данных типов процессов необходимо участие пользователя, который бы их запускал. В то время как демонам для их запуска пользователь не требуется.
Когда завершается загрузка системы, процесс инициализации системы начинает создавать демоны с помощью метода fork(), устраняя необходимость в терминале (именно это подразумевается под «отсутствием управляющего терминала»).
Я не буду вдаваться в подробности работы метода fork(), отмечу лишь, что, хотя существуют и другие методы, традиционный способ создания дочернего процесса в Linux заключается в создании копии существующего процесса (посредством своеобразного «ответвления»), после чего выполняется системный вызов exec() для запуска другой программы.
Примечание: Термин «fork» не был взят с потолка. Он получил свое название от метода fork() из Стандартной библиотеки языка программирования Си. В языке Си данный метод предназначен для создания новых процессов.
Примеры демонов в Linux
Команда pstree показывает процессы, запущенные в настоящее время в нашей системе, и отображает их в виде древовидной диаграммы. Откройте терминал и введите следующую команду:
Вывод команды pstree — это довольно хорошая иллюстрация того, что происходит с нашей системой. Перед нами появился список всех запущенных процессов, среди которых можно заметить и несколько демонов: cupsd, dbus-daemon, kdekonnectd, packagekitd и некоторые другие.
Вот несколько «популярных» примеров демонов, которые могут работать в вашей системе:
systemd — это системный демон, который (подобно процессу init) является родителем (прямым или косвенным) всех других процессов, и имеет PID=1.
rsyslogd — используется для регистрации системных сообщений. Это более новая версия syslogd, имеющая несколько дополнительных функций.
udisksd — обрабатывает такие операции, как: запрос, монтирование, размонтирование, форматирование или отсоединение устройств хранения данных (жесткие диски, USB-флеш-накопители и пр.).
logind — крошечный демон, который различными способами управляет входами пользователей в систему.
sshd — демон, отвечающий за управление службой SSH. Используется практически на любом сервере, который принимает SSH-соединения.
ftpd — управляет службой FTP. Протокол FTP (сокр. от англ. «File Transfer Protocol») является широко используемым протоколом для передачи файлов между компьютерами, где один компьютер действует как клиент, другой — как сервер.
crond — демон планировщика заданий, зависящих от времени. С его помощью можно выполнять обновление программного обеспечения, проверку системы и пр.
Версии происхождения термина «daemon»
Есть несколько версий происхождения термина «daemon»:
Научная версия: Использование термина «daemon» в вычислительной технике произошло в 1963 году. Project MAC (сокр. от англ. «Project on Mathematics and Computation») — это проект по математике и вычислениям, созданный в Массачусетском технологическом институте. Именно здесь термин «daemon» вошел в обиход для обозначения любого системного процесса, отслеживающего другие задачи и выполняющего предопределенные действия в зависимости от их поведения. Процессы были названы термином «daemons» в честь демона Максвелла.
Примечание: Демон Максвелла — это результат мысленного эксперимента. В 1867 году Джеймс Клерк Максвелл представил себе разумное и изобретательное существо, способное наблюдать и направлять движение отдельных молекул в заданном направлении. Цель мысленного эксперимента состояла в том, чтобы показать возможность противоречия второму закону термодинамики.
Талисман BSD: В операционных системах BSD есть свой талисман — красный чертёнок (этакая игра слов «daemon/demon»). BSD-демона зовут Beastie (Бисти), и его часто можно увидеть с трезубцем, который символизирует системный вызов fork(), активно используемый программами-демонами.
Примечание: «Бисти» по звучанию напоминает BSD (произносится как «Би-Эс-Ди»). При этом beastie является уменьшительной формой от слова beast (зверь).
Теологическая версия: Сторонники данной версии считают, что первоначальной формой произношения слова «daemon» было «daimon», что обозначает (по одной из версий) ангела-хранителя. В то время как «daemon» — помощник, «demon» — злой персонаж из Библии.
Примечание: Также «daemon» иногда произносится как «day-mon» или как рифма к слову «diamond».
Аббревиатура: Некоторые пользователи утверждают, что термин «daemon» является аббревиатурой от «Disk and Execution Monitor».
Поделиться в социальных сетях:
Android – это Linux? Сравнение Android и Linux
Как работают демоны, процесс Init и как у процессов рождаются потомки — изучаем основы Unix
Авторизуйтесь
Как работают демоны, процесс Init и как у процессов рождаются потомки — изучаем основы Unix
Если вы когда-нибудь работали c Unix-системами, то наверняка слышали термин «демон». В этой статье я хочу объяснить, что это за демоны и как они работают, тем более что их название заставляет думать, что это что-то плохое.
Вообще демон — это фоновый процесс, который не привязан к терминалу, в котором был запущен. Но как они создаются, как они связаны с другими процессами, как они работают? Об этом мы и поговорим, но сперва давайте узнаем, как работает процесс init и как происходит рождение новых процессов.
Как работает процесс Init
Для начала поговорим о процессе init, также известном как PID 1 (поскольку его ID всегда равен 1). Это процесс создаётся сразу при запуске системы, то есть все другие процессы являются его потомками.
Обычно init запускается, когда ядро вызывает конкретный файл, обычно находящийся по адресу /etc/rc или /etc/inittab. Процесс устанавливает путь, проверяет файловую систему, инициализирует серийные порты, задаёт время и т.д. В последнюю очередь он запускает все необходимые фоновые процессы — в виде демонов. Все демоны обычно расположены в папке /etc/init.d/; принято оканчивать имена демонов на букву d (например, httpd, sshd, mysqld и т.п.), поэтому вы можете подумать, что директория названа так по этому же принципу, но на самом деле существует соглашение об именовании папок, содержащих конфигурационные файлы, именем с суффиксом .d. Итак, init запускает демонов, но мы так и не выяснили, как это происходит. Процесс init запускает демонов, создавая свои ответвления для запуска новых процессов.
Как работает разветвление процессов
Единственный способ создать новый процесс в Unix — скопировать существующий. Этот метод, известный как разветвление или форкинг, включает в себя создание копии процесса в виде потомка и системный вызов exec для запуска новой программы. Мы использовали слово «форкинг», поскольку fork — это реальный метод C в стандартной библиотеке Unix, который создаёт новые процессы именно таким образом. Процесс, вызывающий команду fork, считается родительским по отношению к созданному. Процесс-потомок почти полностью совпадает с родительским: отличаются лишь ID, родительские ID и некоторые другие моменты.
В современных дистрибутивах Unix и Linux процессы можно создавать и другим способами (например, при помощи posix_spawn), но большая часть процессов создаётся именно так.
Что такое демоны в Linux
Демоны много работают, для того, чтобы вы могли сосредоточится на своем деле. Представьте, что вы пишите статью или книгу. Вы заинтересованны в том, чтобы писать. Удобно, что вам не нужно вручную запускать принтер и сетевые службы, а потом следить за ними весь день для того чтобы убедится, что всё работает нормально.
За это можно благодарить демонов, они делают эту работу за нас. В сегодняшней статье мы рассмотрим что такое демоны в Linux, а также зачем они нужны.
Что такое демоны в понятии Linux
Многие люди, перешедшие в Linux из Windows знают демонов как службы или сервисы. В MacOS термин «Служба» имеет другое значение. Так как MacOS это тоже Unix, в ней испольуются демоны. А службами называются программы, которые находятся в меню Службы.
Демоны выполняют определённые действия в запланированное время или в зависимости от определённых событий. В системе Linux работает множество демонов, и каждый из них предназначен для того чтобы следить за своей небольшой частью операционной системы. Поскольку они не находятся под непосредственным контролем пользователя, они фактически невидимы, но тем не менее необходимы. Поскольку демоны выполняют большую часть своей работы в фоновом режиме, они могут казаться загадочными.
Какие демоны работают на вашем компьютере
Обычно имена процессов демонов заканчиваются на букву d. В Linux принято называть демоны именно так. Есть много способов увидеть работающих демонов. Они попадаются в списке процессов, выводимом утилитами ps, top или htop. Но больше всего для поиска демонов подходит утилита pstree. Эта утилита показывает все процессы, запущенные в вашей системе в виде дерева. Откройте терминал и выполните такую команду:
Вот демоны Linux, которых вы можете здесь увидеть: udisksd, gvfsd, systemd, logind и много других. Список процессов довольно длинный, поэтому он не поместится в одном окне терминала, но вы можете его листать.
Запуск демонов в Linux
В Linux существует три типа процессов: интерактивные, пакетные и демоны. Интерактивные процессы пользователь запускает из командной строки. Пакетные процессы обычно тоже не связанны с терминалом. Они запускаются обычно во время когда на систему минимальная нагрузка и делают свою работу. Это могут быть, например, скрипты резервного копирования или другие подобные обслуживающие сценарии.
Интерактивные и пакетные процессы нельзя считать демонами, хотя их можно запускать в фоновом режиме и они делают определённую работу. Ключевое отличие в том, что оба вида процессов требуют участия человека. Демонам не нужен человек для того чтобы их запускать.
Когда загрузка системы завершается, система инициализации, например, systemd, начинает создавать демонов. Этот процесс называется forking (разветвление). Программа запускается как обычный интерактивный процесс с привязкой к терминалу, но в определённый момент она делится на два идентичных потока. Первый процесс, привязанный к терминалу может выполнятся дальше или завершится, а второй, уже ни к чему не привязанный продолжает работать в фоновом режиме.
Существуют и другие способы ветвления программ в Linux, но традиционно для создания дочерних процессов создается копия текущего. Термин forking появился не из ниоткуда. Его название походит от функции языка программирования Си. Стандартная библиотека Си содержит методы для управления службами, и один из них называется fork. Используется он для создания новых процессов. После создания процесса, процесс, на основе которого был создан демон считается для него родительским процессом.
Когда система инициализации запускает демонов, она просто разделяется на две части. В таком случае система инициализации будет считаться родительским процессом. Однако в Linux есть ещё один метод запуска демонов. Когда процесс создает дочерний процесс демона, а затем завершается. Тогда демон остается без родителя и его родителем становится система инициализации. Важно не путать такие процессы с зомби. Зомби, это процессы, завершившие свою работу и ожидающие пока родительский процесс примет их код выхода.
Примеры демонов в Linux
Как появился термин демон в Linux
Так откуда же взялся этот термин? На первый взгляд может показаться, что у создателей операционной системы просто было искаженное чувство юмора. Но это не совсем так. Это слово появилось в вычислительной технике ещё до появления Unix. А история самого слова ещё более древняя.
Изначально это слово писалось как daimon и означало ангелов хранителей или духов помощников, которые помогали формировать характеры людей. Сократ утверждал, что у него был демон, который ему помогал. Демон Сократа говорил ему когда следует держать язык за зубами. Он рассказал о своем демоне во время суда в 399 году до нашей эры. Так что вера в демонов существует довольно давно. Иногда слово daimon пишется как daemon. Это одно и то же.
Использовать слово демон (daemon) в вычислительной технике начали в 1963 году. Проект Project MAC (Project on Mathematics and Computation) был разработан в Массачусетском технологическом институте. И именно в этом проекте начали использовать слово демон для обозначения любых программ, которые работают в фоновом режиме, следят за состоянием других процессов и выполняют действия в зависимости от ситуации. Эти программы были названы в честь демона Максвелла.
Однако есть и другие варианты значения этого слова. Например это может быть аббревиатура от Disk And Executive MONitor. Хотя первоначальные пользователи термина демон не использовали его для этих целей, так что вариант с аббревиатурой, скорее всего неверный.
Еще раз об архитектуре сетевых демонов
Во многих статьях, в том числе на хабре, упоминаются и даже описываются разные способы построения архитектуры сетевых сервисов (демонов). При этом мало у кого из авторов есть реальный опыт создания и оптимизации демонов, работающих с десятками тысяч одновременных соединений и/или гигабитным трафиком.
Так как большинство авторов не удосуживается хотя бы залезть в документацию, то обычно в таких статьях вся информация базируется на неких слухах и пересказах слухов. Эти слухи бродят по сети и поражают википедию, хабрахабр и другие уважаемые ресурсы. В результате получаются опусы вроде «Вы наверное шутите, мистер Дал, или почему Node.js» (пунктуация автора сохранена): она, в основном, верная по сути, но изобилует неточностями, содержит ряд фактических ошибок и изображает предмет с какого-то непонятного ракурса.
Мне было сложно пройти мимо статьи, изобилующей фразами вроде «эффективные реализации polling’а на сегодняшний день имеются лишь в *nix-системах» (как будто poll() есть где-то, кроме некоторых *nix). Этот пост начинался как комментарий, разъясняющий уважаемому inikulin ошибки в его статье. В процессе написания оказалось, что проще изложить предмет с самого начала, что я собственно и делаю отдельным постом.
В моем очерке нет срыва покровов или каких-то неизвестных трюков, здесь просто описываются преимущества и недостатки разных подходов человеком, который проверял, как всё это работает на практике в разных операционных системах.
Для желающих просветиться — добро пожаловать под кат.
ТЗ для сетевого демона
Сначала нужно понять, что именно должны делать сетевые сервисы и в чем, вообще, состоит проблема.
Любой демон должен принимать и обрабатывать сетевые соединения. Так как стек протоколов TCP/IP вырос из UNIX, а эта ОС исповедует догму «всё есть файл», то сетевые соединения есть файлы особого типа, которые можно открывать, закрывать, читать и писать стандартными функциями ОС для работы с файлами. Обратите внимание на модальный глагол «можно», в совокупности со словом «теоретически» он очень точно описывает действительность.
Итак, первым делом любой демон вызывает системные функции socket(), затем bind(), затем listen() и в итоге получает файл специального типа «слушающий сокет». Параметры этих функций и дальнейшие действия демона очень сильно зависят от применяемого транспортного протокола (TCP, UDP, ICMP, RPD. ), впрочем, в большинстве ОС вы можете bind-ить только первые два. В этой статье в качестве примера мы рассмотрим наиболее популярный протокол TCP.
Хотя слушающий сокет есть файл, всё, что с ним может происходить — это периодически возникающие события типа «запрос входящего соединения». Демон может принять такое соединение функцией accept(), которая создаст новый файл, на этот раз уже типа «открытый сетевой сокет TCP/IP». Предположительно, демон должен прочитать из этого соединения запрос, обработать его и отправить назад результат.
При этом, сетевой сокет — это уже более-менее нормальный файл: хоть он и был создан не самым стандартным образом, по крайней мере из него можно пытаться читать и писать данные. Но есть и существенные отличия от обычных файлов, расположенных на файловой системе:
* Все события происходят действительно асинхронно и с неизвестной длительностью по времени. Любая операция в худшем случае может занять десятки минут. Вообще любая.
* Соединения, в отличие от файлов, могут закрываться «сами собой» в любой, самый неожиданный момент.
* ОС не всегда сообщает о закрывшемся соединении, «мертвые» сокеты могут висеть по полчаса.
* Соединения на клиенте и на сервере закрываются в разное время. Если клиент попытается создать новое соединение и «доотправить» данные, возможно дублирование данных, а при неправильно написанном клиенте — и их потеря. Также возможно наличие на сервере нескольких открытых соединений от одного клиента.
* Данные расцениваются как поток байтов и могут буквально приходить порциями по 1 байту. Поэтому считать их, например, строками UTF-8 нельзя.
* Никаких буферов кроме тех, которые предоставил сам демон, в сети нет. Поэтому запись в сокет даже 1 байта может заблокировать демон на десятки минут (см. выше). Кроме того, память на сервере «не резиновая», демон должен уметь ограничивать скорость, с которой генерируются результаты.
* Любые ошибки могут случаться в любом месте, демон должен корректно обрабатывать их все.
Если написать цикл по всем открытым соединениям «в лоб», то первое же «подвисшее» соединение заблокирует все остальные. Да-да, на десятки минут. И тут возникают различные варианты организации взаимодействия различных модулей демона. Смотрите рисунок:
Disclaimer: На рисунке приведен не соответствующий реальности код на псевдоязыке. Многие важные системные вызовы и весь код обработки ошибок опущены для ясности.
2. Многопроцессная архитектура
Простейший способ не дать соединениям влиять друг на друга — это запустить для каждого из них отдельный процесс (то есть отдельную копию вашей программы). Недостатки этого метода очевидны — запуск отдельного процесса это очень ресурсоёмкая операция. Но в большинстве статей не объясняется, почему именно такой способ используется в том же Apache.
А всё дело в том, что именно процесс во всех ОС является единицей учета системных ресурсов — памяти, открытых файлов, прав доступа, квот и так далее. Если вы создаете демон удаленного доступа к операционной системе вроде Shell или FTP — вы просто обязаны запускать отдельный процесс от имени каждого залогинившегося пользователя, чтобы правильно учитывать права доступа к файлам. Аналогично, на сервере shared-хостинга одновременно, на одном «физическом» порту крутятся сотни сайтов разных пользователей — и процессы нужны apache для того, чтобы сайты одних пользователей хостинга не могли залезть в данные других пользователей. Использование процессов не очень-то влияет на производительность Апача:
На графике — количество обрабатываемых запросов к статическому файлу в секунду в зависимости от версии ядра Linux. Больше — лучше.
Тестовый стенд: Core i7 970, 3Gb DDR3, Nvidia GTX 460, 64GB OCZ Vertex SSD.
Источник: Phoronix.
Желаю и вашим демонам отдавать по 17к файлов в секудну.
Даже если пользователи вашего демона не зарегистрированы в операционной системе, но к вашему сервису выдвигаются повышенные требования по безопасности, выделение отдельного процесса под каждого пользователя есть очень разумное архитектурное решение. Это не позволит пользователям-«плохишам» получать или блокировать доступ к данным других пользователей, даже если они найдут баг в вашем демоне, позволяющий читать чужие данные или просто рушащий процесс демона.
Наконец, у каждого процесса имеется собственное адресное пространство, и разные процессы не мешают друг другу пользоваться памятью. Почему это преимущество — объясняется в части
3. Многопоточная архитектура
Потоки — это максимально легкие «процессы», имеющие общую память, системные ресурсы и права доступа, но разные стеки. Это означает, что у потоков общие динамические, глобальные и статические переменные, но разные локальные. В многопроцессорных и/или многоядерных системах разные потоки одного процесса могут выполняться физически одновременно.
Многопоточная архитектура похожа на многопроцессную, в которой безопасность и стабильность принесены в жертву производительности и уменьшению расходов памяти.
Главным плюсом многопоточной архитектуры после производительности является последовательность и синхронность алгоритма обработки открытого соединения. Это значит, что алгоритм выглядит и выполняется именно так, как нарисовано на иллюстрации в первой части. Сначала из сокета читаются данные, столько времени, сколько для этого нужно, потом они обрабатываются — опять же, столько времени, сколько эта обработка потребует, потом результаты отправляются клиенту. При этом, если вы начнете отправлять результаты слишком быстро — поток автоматически заблокируется на функции write(). Алгоритм обработки прост и понятен хотя бы на самом верхнем уровне. Это очень, очень большой плюс.
Для относительно небольшого количества одновременных соединений многопоточная архитектура — отличный выбор. Но если соединений действительно много, скажем десять тысяч, переключение между потоками начинает занимать слишком много времени. Но даже это — не главный недостаток многопоточной архитектуры.
А главный состоит в том, что потоки не независимы и могут (и будут) друг друга блокировать. Чтобы понять, как это происходит, рассмотрим пример.
Допустим, нам нужно вычислить значение выражения
a = b + c;
, где a, b и с есть глобальные переменные.
В обычной, однопоточной ситуации компилятор сгенерирует примерно такой машинный код:
a = b; // MOV A, B
a += c; // ADD A, C
В многопоточном варианте использовать этот код нельзя. Другой поток может изменить значение b между первой и второй инструкциями, в результате чего мы получим неверное значение a. Если где-то в другом месте считается, что a всегда равно b+c, возникнет очень трудно воспроизводимая «плавающая» ошибка.
Поэтому, в многопоточном варианте используется код вроде такого:
lock a;
lock b;
lock c;
a = b;
a += c;
unlock c;
unlock b;
unlock a;
, где lock и unlock — это атомарные операции блокировки и разблокировки доступа к переменной. Они устроены таким образом, что если переменная уже заблокирована другим потоком, операция lock() будет ждать её освобождения.
Таким образом, если два потока начнут одновременно выполнять операции a = b + c и b = c + a, они заблокируют друг друга навсегда. Такая ситуация называется клинчем, поиск и разрешение клинчей — отдельная «больная тема» параллельного программирования. Но и без клинчей потоки, если они не снимают блокировки быстро, могут останавливать друг друга на достаточно длительные промежутки времени.
Кроме того, атомарные операции физически реализуются при помощи монопольного захвата шины ОЗУ и самой ОЗУ. Работа напрямую с памятью, а не с кэшем, очень медленна сама по себе, а в данном случае еще и вызывает инвалидацию (сброс) соответствующих кэш-линий всех ядер всех остальных процессоров сервера. То есть даже в лучшем случае, при отсутствии блокировок, каждая атомарная операция выполняется достаточно долго и ухудшает производительность остальных потоков.
Но, казалось бы, поскольку соединения в демонах почти независимы, откуда у них могут взяться общие переменные?
А вот откуда:
* Общая очередь новых соединений;
* Общая очередь доступа к базе данных или подобным ресурсам;
* Общая очередь запросов на выделение памяти (да-да, malloc() и new() могут вызвать блокировку);
* Общий журнал (log-файл) и общие объекты подсчета статистики.
Это только самые очевидные.
В некоторых случаях существуют способы обойтись без общих переменных. Например, от блокировок очереди новых соединений можно отказаться, если наделить один из потоков функцией «диспетчера», который будет неким хитрым образом раздавать задания заранее. Иногда удается применить специальные «неблокирующие» структуры данных. Но в целом проблема взаимных блокировок в многопоточной архитектуре не решена.
4. Неблокирующая архитектура
В идеале количество потоков в приложении должно быть примерно равным количеству процессорных ядер по порядку величины. Одним из механизмов, позволяющим этого добиться, является неблокирующий ввод-вывод.
Неблокирующий ввод-вывод — это просто режим доступа к файлу, который можно установить в большинстве современных ОС. Если в обычном, «блокирующем» режиме функция read читает из файла столько байт, сколько заказал программист, а пока это чтение происходит — «усыпляет» вызвавший её поток, то в неблокирующем режиме та же самая функция read читает не из файла, а из его кэша, столько байт, сколько в этом кэше есть, и после этого сразу возвращается, никаких потоков не усыпляя и не блокируя. Если кэш был пуст, неблокирующий read() прочтет 0 байт, установит код системной ошибки в значение EWOULDBLOCK и сразу вернется. Но всё равно это обычный синхронный вызов обычной синхронной функции.
Некоторая путаница, в частности, в англоязычной википедии, в которой неблокирующий синхронный ввод-вывод назван «асинхронным», вызвана, видимо, не очень любознательными апологетами ОС Linux. В этой операционной системе достаточно долго, вплоть до ядер 2.6.22-2.6.29, просто не было никаких асинхронных функций ввода-вывода вообще (да и сейчас есть не весь необходимый набор, в частности нет асинхронной fnctl), и некоторые программисты, писавшие только под эту ОС, ошибочно называли неблокирующие синхронные функции «асинхронными», что прослеживается в ряде старых мануалов для Linux.
Подробно асинхронный ввод-вывод рассматривается в следующей части, а здесь сосредоточимся на применении неблокирующих функций чтения и записи.
В реальных условиях 95% вызовов неблокирующего read() будут читать по 0 байт. Чтобы избежать этих «холостых» вызовов ядра ОС, существует функция select(), позволяющая попросить операционную систему выбрать из вашего списка соединений те, из которых уже можно читать и/или писать. В некоторых *nix ОС есть вариант этой функции под названием poll().
Подробнее касательно poll(): эта функция появилась как требование очередной версии стандарта POSIX. В случае Linux poll() сначала был реализован как функция стандартной библиотеки языка Си (libc>=5.4.28) в виде обертки поверх обычного select(), и только через некоторое время «переехал» в ядро. В Windows, например, нормальной функции poll() нет до сих пор, но начиная с Vista есть некий паллиатив для упрощения миграции приложений, так же реализованный в виде обертки вокруг select() на Си.
Не могу не поделиться графиком, показывающим, к чему все эти нововведения приводят. На графике — время прокачки 10 Гб данных через интерфейс-петлю в зависимости от версии ядра. Меньше — лучше. Источник тот же, тестовый стенд тот же.
В любом случае, хотя у select() есть определенные ограничения (в частности, на количество файлов в одном запросе), использование этой функции и неблокирующего режима ввод-вывода — это способ в три десятка строк кода переложить всю работу на операционную систему и просто обрабатывать свои данные. В большинстве случаев, отвечающий за неблокирующий ввод-вывод поток будет потреблять всего пару процентов вычислительной мощности одного ядра. Выполняющие все вычисления внутренние потоки ядра операционной системы «съедят» в десятки раз больше.
Вернёмся к уменьшению количества потоков.
Итак, в демоне имеется большое количество объектов класса «соединение», и существует набор операций, которые нужно применить к каждому объекту-соединению, причем в нужном порядке.
В многопоточной архитектуре создается отдельный поток для каждого объекта-соединения, и операции естественным образом выполняются в блокирующем режиме в нужной последовательности.
В архитектуре с неблокирующим вводом-выводом создается поток под каждую операцию, которая последовательно применяется к разным объектам. Это немножко похоже на SIMD-инструкции типа MMX и SSE: одна инструкция применяется сразу к нескольким объектам. Чтобы выдерживать необходимую последовательность операций (то есть сначала вычислить результат, а потом его отправлять), в общей памяти процесса создаются очереди заданий между потоками. Обычно очереди создаются на базе кольцевых буферов, которые в данном случае можно реализовать «неблокирующим» образом.
В реальном сетевом сервисе между чтением запроса и отправкой результата будет стоять достаточно сложный разветвленный алгоритм обработки, возможно включающий вызов сервера приложений, СУБД или другие «тяжелые» операции, а также полный набор ветвлений, циклов, обработку ошибок на каждом шаге и т.п. Разбить всё это по заранее неизвестному количеству выполняющихся одновременно потоков, да еще так, чтобы нагрузка на процессорные ядра была примерно одинаковой — это высший уровень умений разработчика, требующий виртуозного владения всеми аспектами системного программирования. В большинстве случаев делают намного проще: заключают в отдельный поток всё, что находится между read() и write(), и запускают N=количество ядер копий этого потока. А затем изобретают костыли для входящих в клинч, конкурирующих за ресурсы, «убивающих» СУБД и т.д. параллельных потоков.
5. Асинхронный ввод-вывод
Если вы не понимаете отличие асинхронных функций от синхронных, то для простоты можете считать, что асинхронная функция выполняется параллельно и одновременно с вызвавшей её программой, например, на соседнем ядре. С одной стороны, вызвавшей программе не нужно ждать окончания вычислений и она может сделать что-то полезное. С другой стороны, когда результаты асинхронной функции готовы, нужно каким-то образом сообщить об этом программе-«заказчику». То, каким образом происходит это сообщение, реализовано в разных ОС очень по-разному.
Исторически одной из первых ОС с поддержкой асинхронного ввода-вывода стала Windows 2000.
Типичный use case был такой: однопоточное приложение (никаких многоядерных процессоров тогда не было) загружает, например, большой файл в течение десятков секунд. Вместо зависания интерфейса и «часиков», которые наблюдались бы при синхронном вызове read(), в асинхронном варианте основной поток программы не «зависнет», можно будет сделать красивый прогресс-бар, отображающий процесс загрузки, и кнопку «отмена».
Для реализации «прогресс-бара» в асинхронные функции ввода-вывода Windows передается специальная структура OVERLAPPED, в которой ОС отмечает текущее количество переданных байт. Программист может читать содержимое этой структуры в любой удобный ему момент — в основном цикле обработки сообщений, по таймеру и т.д. В этой же структуре в конце операции будет записан её итоговый результат (общее число переданных байт, код ошибки если он есть и т.п.).
Кроме этой структуры, в асинхронные функции ввода-вывода можно передать собственные функции обратного вызова (callback), принимающие указатель на OVERLAPPED, которые будет вызваны операционной системой по окончании операции.
Настоящий, честный асинхронный запуск callback-функции, через прерывание программы в любом месте, где бы она не находилась, не отличим от запуска второго потока выполнения программы на этом же ядре. Соответственно, нужно либо очень аккуратно писать callback-функции, либо применять все «многопоточные» правила, касающиеся блокировок доступа к общим данным, что, согласитесь, очень странно в однопоточном приложении. Чтобы избежать потенциальных ошибок в однопоточных приложениях, Windows ставит необработанные callback-и в очередь, а программист должен явно указать места в своей программе, где она может быть прервана для выполнения этих callback-ов (семейство функций WaitFor*Object).
Описанная выше схема асинхронного ввода-вывода является «родной» для ядра WindowsNT, то есть все остальные операции так или иначе реализуются через неё. Полное название — IOCP (Input/Output Completion Port). Считается, что эта схема позволяет добиться от железа теоретически максимальной производительности. Любые демоны, рассчитанные на серьезную работу под Windows, должны разрабатываться именно на основе IOCP. Подробнее см. введение в IOCP в MSDN.
В Linux вместо нормальной структуры OVERLAPPED есть некое слабое подобие aiocb, позволяющая определить только факт завершения операции, но не её текущий прогресс. Вместо определяемых пользователем callback-ов ядро использует сигналы UNIX (да-да, те, которые kill ). Сигналы приходят полностью асинхронно, со всеми вытекающими, но если вы не чувствуете себя гуру в деле написания реентерабельных функций, вы можете создать файл специального типа (signalfd) и читать из него информацию о пришедших сигналах обычными синхронными функциями ввода-вывода, в том числе неблокирующими. Подробнее см. man aio.h.
Использование асинхронного ввода-вывода не накладывает никаких ограничений на архитектуру демона, теоретически она может быть любой. Но, как правило, используются несколько рабочих потоков (по количеству процессорных ядер), между которыми равномерно распределяются обслуживаемые соединения. Для каждого соединения строится и программируется конечный автомат (Finite State Machine, FSM), появление событий (вызовов callback-функций и/или ошибок) переводит этот автомат из одного состояния в другое.
Резюме
Как мы видим, у каждого способа есть свои преимущества, недостатки и области применения. Если вам нужна безопасность — используйте процессы, если важна скорость работы при большой нагрузке — неблокирующий ввод-вывод, а если важна скорость разработки и понятность кода, то подойдет многопоточная архитектура. Асинхронный ввод-вывод — основной способ в Windows. В любом случае не стоит пытаться писать код для работы с вводом-выводом самостоятельно. В сети есть свободные готовые библиотеки для всех архитектур и операционных систем, вылизанные за десятки лет почти до блеска. Почти — потому что в вашем случае всё равно придется что-то подкручивать, подпиливать и донастраивать под ваши условия. Интернет — сложная штука, здесь не бывает универсальных решений на все случаи жизни.
Как бы то ни было, одним вводом-выводом демоны не обходятся, а во время обработки запросов случаются намного более сложные «затыки». Но это уже тема для другой статьи, если оно кому-либо интересно.