FastCGI-приложение на Perl. Часть вторая.

    Написанное в предыдущей статье FastCGI-приложение сковано одной цепью с тем терминалом, из которого его запустили. Приложение будет работать ровно до тех пор, пока открыт терминал. Как только вы закроете терминал, приложение будет немедленно убито.

    Причина в том, что всякая программа, запущенная из терминала, становится потомком этого терминала, а терминал, соответственно, родителем этой программы. Существование же потомков, не имеющих родителей, не допускается. Соответственно, при закрытии родителя-терминала немедленно закрываются все зависящие от него потомки и, в частности, наше FastCGI-приложение.

    Для того, чтобы FastCGI-приложение перестало зависеть от родительского терминала, оно должно быть преобразовано из простого скрипта в демон.

    Что это еще за чертовщина?

    Демон — это программа, работающая в фоновом режиме, без непосредственного взаимодействия с пользователем. Родителем любого демона является init — самая первая программа, запускающаяся при загрузке операционной системы и запускающая все остальные программы. init работает от загрузки и до шатдауна, так что демоны (если, конечно, не вмешается kill) тоже работают без сна и отдыха.

    Для того, чтобы стать демоном, приложение должно выполнить ряд действий, основное из которых — любой ценой отделаться от родительской опеки.

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

    #!/usr/bin/perl
    
    # Для пущего порядку
    use strict;
    use warnings;
    
    # Этот модуль реализует протокол FastCGI
    use FCGI;
    
    # Демонизация {
        # Этот модуль для разговора с операционкой по понятиям:)
        use POSIX;
       
        # Форк
        # избавляемся от родителя
        fork_proc() && exit 0;
       
        # Начать новую сессию
        # наш демон будет родоначальником новой сессии
        POSIX::setsid() or die "Can't set sid: $!";
       
        # Перейти в корневую директорию
        # чтобы не мешать отмонтированию файловой системы
        chdir '/' or die "Can't chdir: $!";
       
        # Сменить пользователя на nobody
        # мы же параноики, ага?
        POSIX::setuid(65534) or die "Can't set uid: $!";
    
        # Переоткрыть стандартные дескрипторы на /dev/null
        # больше не разговариваем с пользователем
        reopen_std();
    # }
    
    # Открываем сокет
    # наш демон будет слушать порт 9000
    # длина очереди запросов - 5 штук
    my $socket = FCGI::OpenSocket(":9000", 5);
    
    # Начинаем слушать
    # демон будет перехватывать стандартные дескрипторы
    my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket);
    
    my $count = 1;
    
    # Бесконечный цикл
    # при каждом принятом запросе выполняется один "оборот" цикла.
    while($request->Accept() >= 0) {
        # Внутри цикла происходит выполнение всех требуемых действие
        print "Content-Type: text/plain\r\n\r\n";
        print $count++;
    };
    
    # Форк
    sub fork_proc {
        my $pid;
       
        FORK: {
            if (defined($pid = fork)) {
                return $pid;
            }
            elsif ($! =~ /No more process/) {
                sleep 5;
                redo FORK;
            }
            else {
                die "Can't fork: $!";
            };
        };
    };
    
    # Переоткрыть стандартные дескрипторы на /dev/null
    sub reopen_std {   
        open(STDIN,  "+>/dev/null") or die "Can't open STDIN: $!";
        open(STDOUT, "+>&STDIN") or die "Can't open STDOUT: $!";
        open(STDERR, "+>&STDIN") or die "Can't open STDERR: $!";
    };
    

    У тех, кто в теме, может возникнуть вопрос — почему я сделал демонизацию вручную, вместо того, чтобы использовать готовый модуль Proc::Daemon?

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

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

    Отдельно стоит отметить смену пользователя на nobody.

    Пользователь nobody — это специальный пользователь, не имеющий в системе никаких привилегий. Работа демона от имени пользователя nobody обеспечивает дополнительную защиту на тот случай, если некий злоумышленник сможет получить контроль над демоном. В этом случае работа демона от имени nobody не позволит злоумышленнику получить доступ к остальным ресурсам системы.

    Число 65534 в функции setuid — это uid пользователя nobody. Уточнить uid можно с помощью специальной команды:

    $ id nobody

    Да, обратите внимание — сменить uid может только root, поэтому запускать демона тоже должен root.

    У особо въедливых может возникнуть еще один вопрос — почему я не предусмотрел обработчики сигналов для корректного завершения?

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

    Запускаем демона. Если нет никаких ошибок — демон запустится молча, ничего не выведет и отцепится от консоли. Теперь можно закрыть терминал, демон на это не обратит никакого внимания и продолжит работу.

    Откройте в браузере test.host/fcgi-bin/ — демон отвечает как и раньше. Теперь заткнуть его можно только вооружившись командой kill.

    В следующей части — распараллеливание, учим демона отвечать на несколько запросов одновременно.

    (оригинал статьи)
    Поделиться публикацией

    Комментарии 22

      0
      #!/usr/bin/perl -w!
      ))
        0
        а если по теме — интересная статья, спасибо!
          +1
          Э, не:)
          Параметр -w это устаревший вариант, прагма warnings более кошерна:)
            +1
            ну тогда и -T для полного счастья в купе с Perl::Critic для развития паранои :-))
              0
              тем не менее — хороший стиль программирования. Причем тут паранойя?
                0
                Дык я не против, только «за»!
                Вот те, кто пробовал писать с -T и Critic, те меня поймут. Это уже не просто стиль программирования. ;-)
                  0
                  Между прочим, Critic рулит! Он умудряется находить скользкие места даже в весьма вылизанном коде.

                  Его, правда, надо немного настроить под свой стиль, и это задача не совсем тривиальная.

                  Зато после этого вполне реально релизить код, на который даже Critic не ругается (правда, иногда приходится в паре мест заюзать локально на строчку/функцию ## no critic (Something), но это тоже не так плохо т.к. выполняет роль маркера нестандартного кода).
                0
                Смотрю Perl::Critic. Открылись бездны.
            –1
            С удовольствием почитал бы про то как сделать нормальный (подобный этому) FastCGI на PHP
            • НЛО прилетело и опубликовало эту надпись здесь
              0
              Демона на php написать можно, например habrahabr.ru/blogs/php/40432/
              Только надоли?
                0
                Да не в демоне дело. Нужна реализация while($request->Accept() >= 0) на PHP. Может кто какие сторонние модули использует? Ну это я только для интереса на будущее.
                  0
                  Она зашита в том или ином виде в сам PHP — под fastcgi они подразумевают, что в памяти висит интерпретатор и форкается на каждый следующий запрос. В случае того же fpm в php в одном из заголовков передается script_FILENAME с путем к скрипту, который требуется интерпретировать.
                  Можно реализовать на Си расширение к PHP, реализующее собственно протокол FastCGI (http://fastcgi.com/devkit/doc/fcgi-spec.html) и тогда можно будет демонизировать любой php-скрипт, повесить его на какой-нибудь порт и передавать на вход любые данные, но если для перла хватит просто поддержки FastCGI для работы, то php еще нужен парсинг POST-данных на наличие файлов и, думаю, ряд других приблуд, которые доставят немало головной боли. Оно нам надо?)
                0
                Остался не затронут простой вопрос: если зависнет FastCGI процесс — кто будет его прибивать и рестартовать? Кто будет исполнять роль Process Manager, которую обычно исполняет соответствующие модули в apache иди lighthttp?

                Какое преимущество имеем, работая с сокетами, а не с CGI::Fast? Процитирую мой ответ из оригинального блога:

                Может быть, проще делать традиционно? Смотрим пример из документации:

                use CGI::Fast qw(:standard);
                $COUNTER = 0;
                while (new CGI::Fast) {
                print header;
                print start_html(«Fast CGI Rocks»);
                print
                h1(«Fast CGI Rocks»),
                «Invocation number »,b($COUNTER++),
                " PID ",b($$),".",
                hr;
                print end_html;
                }

                Что мы получаем: а получаем мы совместимость с модулем CGI например, — например, обращение к /fcgi/script.pl выполнит скрипт в fastCGI стиле, обращение как /cgi-bin/script.pl выполнит скрипт в стиле стандартного CGI — что для отладки очень удобно. И это без изменений в самом скрипте, всего лишь добавлением одной строки в конфигурацию виртуального хоста апача.
                  0
                  Не всем нужна совместимость с CGI, так что тут уже дело вкуса.
                    0
                    Ну какое же дело вкуса. Вы вообще как FastCGI приложения тестируете? И как Вы хотите жить без совместимости с CGI, если это стандарт?
                      0
                      Я немного неточно выразился, речь о совместимости с _модулем_ CGI конечно же.

                      У меня FastCGI-приложение — просто обертка, чтобы передать управление фреймворку и соответственно вернуть из него контент. Такие же обертки есть для mod_perl и для обычного CGI, никакой дополнительной логики там нет.
                        0
                        Это да, если простая обёртка то действительно не важно. Но даже обёртку надо как-то тестировать и отлаживать, а это в случае с CGI::Fast гораздо проще.
                    0
                    >Остался не затронут простой вопрос: если зависнет FastCGI процесс — кто будет его прибивать и рестартовать?

                    Это будет в третьей части.
                      0
                      Ждем :)
                    +1
                    Я рад, что Perl стали писать :)
                    На счет FCGI — есть отличный модуль FCGI::Engine, отличная FCGI-обертка над приложением, с поддержкой graceful. Рекомендую.
                      0
                      Что-то мне код до боли напоминает пересказ FCGI::Spawn search.cpan.org/~veresc/FCGI-Spawn-0.13/

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

                      Самое читаемое