Rambler's Top100 Service калинин.ru / программирование / отвлеченно /  << 08.02.03 >>

Postfix изнутри

Эта заметка пишется после громадного перерыва и поэтому, наверняка, будет отличаться от всего остального. Что же, год назад я закончил нравоучениями --- значит, логично начать с них же.

На самом деле все не так плохо. Я не собираюсь снова рассказывать о том, что учиться --- хорошо, хотя именно эта идея, почему-то, сильно изумила большинство будущих и состоявшихся программистов, прочитавших предыдущую статью ("Как стать программистом") и почему нельзя ничему научиться не вложив в свою учебу хоть что-то. Просто я хочу попытаться показать на примере, что такое хорошее (с моей точки зрения) программирование.

Лично я практически никогда и нигде не видел эталонных программ для учащихся и это очень странно. Казалось бы, для того чтобы научить программированию, надо хотя бы один раз показать что это такое и как это делать хорошо. Является ли алгоритм сортировки пузырьком показателем "качественного" программирования? Нет, не является, потому что это частное решение некоторой задачи, практически никогда не возникающей перед человеком обособленно. Нет, этот алгоритм используется, конечно, и в его реализациях иногда делают ошибки, но он никогда не будет самоцелью для программиста.

Выполняемые же задания бывают иногда хороши, но только как упражнения для фиксирования некоторого материала: те же сортировки, поиск, нахождение оптимального решения... любой из этих алгоритмов всегда будет средством достижения результата, а не конечным результатом. Даже курсовые работы и те отличаются малым объемом за очень редким исключением. Что остается? Диплом? Слишком поздно для того, чтобы на его основе можно было бы чего-нибудь изучить.

Таким образом, должны существовать некоторые программы заранее известного качества, которые нужно изучить, может быть, внести измения и, что самое главное, понять что было сделано, как это было сделано и почему сделано именно так. Это очень важные вопросы, понимание же последнего из них является залогом накопления опыта и, чего уж там, некоторой профессиональной мудрости.

Так вот, этих эталонных программ не существует. Максимум на что может надеяться студент, так это на написанные мелом на доске, чрезвычайно корявым почерком, надписи, изображающие некоторую программу. Обычно она не работает, а половина студентов не смогла правильно записать весь поток сознания от преподавателя и поэтому ничего не сможет разобрать уже в своих собственных каракулях.

Требования к эталонным программам

Программа, претендующая на звание "эталонной", должна выполнять следующие условия:

  • ее исходный текст должен быть свободно доступен;
  • это должна быть настоящая программа, решающая какую-то реальную задачу;
  • задача не должна требовать много специальных знаний, например, сложного протокола передачи данных или специфических машинных кодов некоторого процессора

  • она должна быть не очень большой, но и не маленькой, примерно 100 тысяч строк;
  • она должна быть популярной и массово используемой;
  • она должна быть поддерживаемой, то есть должен существовать программист или группа программистов, продолжающий работать над ее улучшением;
  • она должна быть хорошо оформлена, хорошо написана, производительна, переносима и т.д. --- в общем, все то, что преподаватель считает "хорошей" программой
  • в ней должны быть недостатки; впрочем, в любой программе есть недостатки.

Кстати сказать, пример подобной "эталонной" программы можно найти в книге Э. Таненбаума "Современные операционные системы", где он предлагал в качестве примера свою ОС под названием Minix. Но она была вытеснена Linux и перестала использоваться, соответсвенно сейчас она выглядит достаточно загадочно. А когда книга только появлялась, Minix, пожалуй, была очень хорошим примером "эталонной" программы, которую можно изучать или критиковать. Опять же, Linux возник именно потому, что Торвальдс изучал книгу Таненбаума, но ему не хватало Minix для своих нужд.

Мне хочется взять на себя смелость и предложить подобную эталонную программу для изучения ее внутренностей. Выбрать исходники FreeBSD? Нет, они слишком большие и, зачастую, специфические. Можно было бы взять Apache, но тут уже я недостаточно хорошо знаком с его исходными текстами.

Осмелюсь предложить postfix --- это популярный MTA (Mail Transfer Agent), позволяющий эффективно передавать письма адресатам. Азы протокола SMTP, которые надо знать, не особенно сложны для изучения и их можно освоить за время изучения самого postfix. Никто не против?

Mail Transfer Agent

MTA это программа, которая лежит в основе передачи электронной почты. Когда вы посылаете письмо своему знакомому с адресом vasya@pupkin.ru, то вы делаете это посредством своего SMTP-клиента (например, Outlook, The Bat!, mutt), который передает письмо MTA. Он может делать это по протоколу SMTP или еще каким-нибудь способом, в любом случае он не выполняет доставку письма самостоятельно. MTA получает письмо и проверяет по своей конфигурации, что он должен с ним сделать: послать адресату напрямую (то есть, послать письмо MTA, который установлен на хосте, куда указывает MX-запись соответствующего домена).

MTA должен сделать примерно следующее:

  • Обрабатывать входные соединения для приема почты по протоколу SMTP.
  • Сохранять почту на диске в очереди.
  • Отправлять почту адресату из очереди (это может быть локальный почтовый ящик, другой MTA или еще какой-то иной способ доставки почты).
  • Гарантировать доставку почты получателю или нотификацию отправителю о невозможности доставки.

Этот список --- только самое необходимое. Хороший MTA должен, кроме того, быть надежным, высокопроизводительным, гибко настраиваемым, поддерживать встройку фильтров (например, для борьбы со спамом) и т.д.

Долгое время фактически единственным MTA для Unix'ов был sendmail, отличающийся диким форматом конфигурации и, вообще, неудобством использования. Сейчас ситуация иная --- есть еще несколько свободных почтовых систем, таких как QMail и postfix, которые обладают целым рядом преимуществ по сравнению с sendmail. Впрочем, sendmail все равно есть и, наверное, останется самым популярным MTA, так как традиционно входит в состав огромного количества дистрибутивов Unix. Кроме того, опять же, интерфейс с установленным MTA все равно закрепился как вызов программ из установки sendmail и все остальные MTA поддерживают его.

Общая организация postfix: модульность, управляющий процесс master

Первое, на что стоит я хотел бы обратить внимание: postfix не является монолитной программой. Он весь состоит из модулей, которые передают почтовые сообщения или иную информацию между собой.

Интересно, что модули постфикса, или сервисы, устроены в виде отдельных программ, каждая из которых запускается из управляющего сервиса master (порождается от master). Почему выбрана такая модель (то есть, множество однопоточных процессов) а не, к примеру, популярные потоки?

Давайте я сразу оговорюсь --- многопоточные сервисы вполне могут существовать и создаваться, это мы все рассмотрим чуть позднее. Но все сервисы постфикса основаны либо на select, либо на pre-fork моделях. Связано это, как я думаю, с отлаженностью этих механизмов на различных операционных системах. Постфикс компилируется и успешно работает практически на всем зоопарке более-менее популярных Unix'ов, это было бы попросту невозможно в том случае, если бы использовались потоки, которые везде разные. А pre-fork модель или select вполне отлажена в течение десятилетий использования.

Я отмечу сразу же --- то что я даю свои ответы на поставленные мною же вопросы, еще не значит что эти ответы верны. Я не автор постфикса и его мотивация могла быть совершенно иной, я же лишь предполагаю или, точнее, меряю на себя возникающие вопросы. Поэтому все причины, которые я пытаюсь разъяснить, следует понимать следующим образом: "Я бы тоже сделал бы так если бы стоял перед этой проблемой по следующей причине". Сделав эту ремарку, я надеюсь что читатель ее запомнит и не будет так уж безоговорочно верить в мои ответы --- может быть я и не прав. Если у читателя появились свои ответы (а в качестве postfix'а мы, напомню, не сомневаемся, эта программа является эталоном), то это значит что своей цели я добился.

Для функционирования всей системы в целом необходимо запустить только процесс master. Он читает конфигурационный файл следующего вида:

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (50)
# ==========================================================================
smtp      inet  n       -       n       -       -       smtpd
#628      inet  n       -       n       -       -       qmqpd
pickup    fifo  n       -       n       60      1       pickup
cleanup   unix  n       -       n       -       0       cleanup
qmgr      fifo  n       -       n       300     1       qmgr
#qmgr     fifo  n       -       n       300     1       nqmgr
rewrite   unix  -       -       n       -       -       trivial-rewrite
bounce    unix  -       -       n       -       0       bounce
defer     unix  -       -       n       -       0       bounce
flush     unix  n       -       n       1000?   0       flush
smtp      unix  -       -       n       -       -       smtp
showq     unix  n       -       n       -       -       showq
error     unix  -       -       n       -       -       error
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       n       -       -       lmtp

Это список всех доступных сервисов. Вкратце, о том что значат все эти надписи, по столбцам:

  • service --- название сервиса в случае использования сокетов unix-domain и прочих средств IPC (interprocess communication), порт и интерфейс в случае использования интернет-сокетов (smtp, понятно, аналог *:25)
  • type --- тип сокета, который слушается сервисом. Возможные значения, по-моему, ясны.
  • private --- признак того, что сервис может доступен снаружи MTA. Все сервисы с типом inet не могут быть private по понятным причинам --- никак нельзя ограничить доступ процессов к интернет-сокету. В зависимости от этого флага отличается каталог и права доступа на файл, который связан с создаваемым сокетом. Доступные снаружи сервисы нужны по разным причинам, например сервис showq нужен для получения содержимого очереди сообщений для команды mailq.
  • unpriv --- признак того, что сервис запускается от пользователя postfix, а не root. Надо сказать, что смена пользователя целиком и полностью на совести сервиса, а не master'а (единственное, чем отличается запуск непривилегированного процесса от запуска привелигированного --- так это наличием ключа -u). Это можно объяснить тем, что сервисы (а это программы в каталоге /usr/libexec/postfix) просто так не появляются и они должны поддерживать такой интерфейс. Этот флаг нужен для того, чтобы обезопасить сервер от сбоев в работе постфикса: допустим, в постфиксе найдена жуткая ошибка в каком-либо из сервисов, позволяющая злоумышленнику выполнить любой код на вашем сервере; если бы сервисы работали от root'а то этот человек получил бы полный доступ к компьютеру, а если они работают от пользователя postfix, который даже не имеет возможности логина, то под ударом только ваша почта. Естественно, что основной процесс, master, работает от root'а, это значит что master должен быть максимально простым для того, чтобы гарантировать отсутствие в нем грубых ошибок.
  • chroot --- аналогично unpriv, флаг заставляет сервисы выполнить вызов chroot на /var/spool/postfix. Тем самым для сервисов меняется положение каталога '/' и они не могут получить доступ к другим файлам, кроме очереди сообщений. Необходимость этого флага обусловлена теми же причинами, что и для unpriv.
  • wakeup --- нотификация сервиса каждые n секунд. Это позволяет заставить сервисы, допустим, перечитать очередь. На самом деле, нотификация об изменениях в очереди может быть доставлена от других сервисов, но это все равно полезно: вдруг где-то что-то сломалось, или постфикс перезапустился, все равно очередь должна быть проверена.
  • maxproc --- максимальное количество процессов сервиса, которые могут быть запущены. Это число очень полезно для настройки особенно тяжеловесных сервисов, запуск которых может привести к большой загрузке сервера.
  • command --- это просто название программы и аргументы, которые должны быть ей переданы. Здесь никаких тонкостей нет.

Значения по-умолчанию (например, maxproc) настраиваются через другой конфигурационный файл --- main.cf.

master: запуск сервисов, интерфейс с уже запущенными сервисами

Теперь рассмотрим, как появляются новые процессы, выполняющие работу сервисов.

При запуске master считывает master.cf, по которому он определяет какие сервисы понадобятся. Он создает все указанные сокеты и готовиться их "слушать", инициализирует внутри себя структуры, описывающие состояние каждого из сервисов (количество запущенных процессов и т.п.), после чего master загоняет все дескрипторы сокетов в select и ждет какого-нибудь из событий на каждый из них.

Теперь, допустим, на наш сервер должна прийти почта. Сейчас интересно не то, как будет обрабатываться почта, а как будут запускаться сервисы.

Что значит --- пришла почта? Это значит, что некий почтовый релей установил соединение на 25-й порт нашего сервера. В этом случае, дескриптор в master'е, который связан с этим портом, будет "готов к чтению" и, тем самым, select завершится.

Сам по себе master не умеет обрабатывать smtp-сессию, зато это умеет делать сервис smtpd, который и будет запущен при помощи fork. Естественно, что перед запуском производится некоторое количество действий, которые пока что не особенно интересны, важно то, что сразу же после запуска smtpd выполняет accept на этом дескрипторе и приступает к обработке smtp-сессии и пересылке письма дальше по сервисам до менеджера очереди.

Перед запуском master увеличивает счетчик процессов сервиса smtpd и запоминает его pid. Этот счетчик будет уменьшен тогда, когда master получит сигнал SIGCHLD, то есть smtpd завершится. Тем самым, master может контролировать количество запущенных процессов.

Теперь самый интересный вопрос --- пока smtp-сессия обрабатывается, master может опять реагировать на изменения состояния дескриптора, связанного с 25-м портом, а что делать когда эта сессия закончится? Глупо завершать smtpd сразу же после обработки одного письма если через секунду, возможно, придет еще одно письмо: тогда будет слишком много затрат связанных с fork. Тем самым, сервисы должны уметь обрабатывать новые соединения и при этом им не должен мешаться master. Кроме того, master все равно должен следить за сервисами и если кто-то из них захотел "умереть", то это не должно сказаться на работоспобности MTA в целом.

Опять же, технология в этом случае совершенно простая, грубо говоря это и есть pre-fork модель построения сетевых серверов, единственное отличие от, скажем так, классической реализации заключается в том, что обычно сервер выполняет только одну операцию и, тем самым, можно сделать так что "главный" сервер (который и выполняет fork) сам по себе тоже способен обрабатывать соединения. В случае с master, который запускает любые сервисы, реализовать такой подход попросту нереально.

Обычно, каждый свободный экземпляр сервера (то есть, отдельный процесс) выполняет accept (или сначала select, а потом accept) на нужный дескриптор. При этом реально accept будет выполнен только у одного экземпляра сервера (заранее неизвестно какого именно), остальные получат ошибку и могут опять запустить accept или select. Сейчас же, master должен уметь отличать ситуации когда свободных экземпляров сервиса нет (тогда он должен слушать сокет сам и выполнить fork когда придет новое соединение) и когда кто-то свободен (и тогда ему не надо запускать select на этот сокет, поскольку этот "кто-то" сам следит за своим сокетом).

Естественно, что это делается опять же через IPC: между потомком (то есть, экземпляром сервиса) и master'ом есть канал, через который потомок уведомляет master о своей занятости или свободности. Когда потомок изменяет свое состояние, он передает master'у два значения: свой pid и флаг "занят" или "свободен".

Реализация многопроцессного однопотокового сервиса в этом случае простая: сервис все время делает accept, по успеху последнего он оповещает master о занятости, затем начинает обрабатывать соединение, после чего сообщает master'у о своей свободности и опять делает accept. Если в какой-то момент он решит закончить свое выполнение, он может это сделать без оповещений --- master получит сигнал от операционной системы и выполнит все необходимые действия.

Реализация многопотокого сервиса еще проще --- сервис никогда не оповещает master о своей занятости, а обрабатывает соединения во внутреннем select или еще каким-нибудь образом.

Теперь небольшая ремарка о необходимости завершения работы процесса. Это общепринятая практика, когда сервер после выполнения определенного числа операций или простоя, прекращает свое выполнение. Такая логика очень полезно, потому что уменьшает зависимость от ошибок с неудалением выделенной ранее памяти (попросту, "разбухания" процесса), поэтому обычно любые сервера имеют какое-то ограничение на количество обработанных запросов и время простоя. В postfix каждый сервис имеет подобные ограничения, кроме, разумеется, master, так как последний некому перезапустить.

Когда некий сервис требует для своей работы другой сервис (например, для того чтобы передать почтовое сообщение от smtpd в cleanup), он всего-навсего обращается по нужному сетевому адресу (в сети интернет или файловой системе), все остальное за него будет сделано master'ом или работающим целевым сервисом.

PS

Я ничего не могу гарантировать, соответственно я не знаю, когда появится следующая заметка и будет ли она на эту же тему. Надеюсь, правда, что тема интересна окружающим.


Версия для печати


  Ссылки по теме:
http://www.postfix.org
   Официальный сайт почтовой системы Postfix, оттуда его можно скачать.
/programming/network/01_12_00.
   Моя статья про неблокирующий connect --- почти то же самое, что и accept.
  Рядом в разделе:
Как стать программистом: часть вторая, книги и интернет-ресурсы (04.03.02)
   Вторую часть статьи я публикую значительно позже, чем рассчитывал: просто не получилось выкроить немного времени на последнюю проверку текста перед публикацией....   >>>>
Как стать программистом: часть первая, нравоучительная (15.02.02)
   Я не знаю почему, но где-то в среднем пару раз в месяц ко мне обращаются с подобным вопросом --- как стать...   >>>>
  Рядом по дате:
Простой, но полезный аллокатор памяти (18.02.03)
   Эта заметка --- продолжение "Postfix изнутри" в том смысле, что в качестве примера опять берется postfix. Но если в прошлый раз...   >>>>
Как стать программистом: часть вторая, книги и интернет-ресурсы (04.03.02)
   Вторую часть статьи я публикую значительно позже, чем рассчитывал: просто не получилось выкроить немного времени на последнюю проверку текста перед публикацией....   >>>>
  Содержание:
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
   C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Просто так
Студенческое
Туризм
  Байки
Фотографии
Комментарии
   Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Благодарности
Форум
Хронология
 
  В этом разделе:
Postfix изнутри (08.02.03)
   Эта заметка пишется после громадного перерыва и поэтому, наверняка, будет отличаться от всего остального. Что же, год назад я закончил нравоучениями...   >>>>
Как стать программистом: часть вторая, книги и интернет-ресурсы (04.03.02)
   Вторую часть статьи я публикую значительно позже, чем рассчитывал: просто не получилось выкроить немного времени на последнюю проверку текста перед публикацией....   >>>>
Как стать программистом: часть первая, нравоучительная (15.02.02)
   Я не знаю почему, но где-то в среднем пару раз в месяц ко мне обращаются с подобным вопросом --- как стать...   >>>>
Ответственность, доверие и качество (27.03.01)
   Существует такая черта человеческого характера под названием "ответственность". Вообще говоря, очень полезная черта: если она есть у человека, то он может...   >>>>
Традиционное управление (06.02.01)
   Йордон пишет о том, что основная проблема создания программных систем заключается не в программировании или проектировании, а в управлении и он...   >>>>
Содержание раздела полностью...
   Примерно в тоже время
Простой, но полезный аллокатор памяти (18.02.03)
   Эта заметка --- продолжение "Postfix изнутри" в том смысле, что в качестве примера опять берется postfix. Но если в прошлый раз...   >>>>
Как стать программистом: часть вторая, книги и интернет-ресурсы (04.03.02)
   Вторую часть статьи я публикую значительно позже, чем рассчитывал: просто не получилось выкроить немного времени на последнюю проверку текста перед публикацией....   >>>>
Хронология полностью...
   Содержание
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
  C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Туризм
  Байки
Фотографии
Комментарии
  Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Студенческое
Просто так
Благодарности
Форум
Хронология
© 2000-2008, Andrey L. Kalinin
mailto:andrey@kalinin.ru
Rambler's Top100