Rambler's Top100 Service Этот текст распечатан с домашней странички Андрея Калинина (www.kalinin.ru).
Оригинал статьи находится по этому адресу: http://www.kalinin.ru/programming/abstract/08_02_03.shtml


Postfix изнутри

08.02.03

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

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

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

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

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

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

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

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

Кстати сказать, пример подобной "эталонной" программы можно найти в книге Э. Таненбаума "Современные операционные системы", где он предлагал в качестве примера свою ОС под названием 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 должен сделать примерно следующее:

Этот список --- только самое необходимое. Хороший 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

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

Значения по-умолчанию (например, 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.

©2000-2001 by Andrey L. Kalinin,
andrey@kalinin.ru