В предшествующих примерах ветвления дочерние процессы обычно вызывали одну из функций семейства exec для запуска новой программы в дочернем процессе. Здесь же дочерний процесс просто вызывает функцию в той же программе и завершается с помощью функции os._ exit. Здесь необходимо вызывать os._exit — если этого не сделать, дочерний процесс продолжит существовать после возврата из handleClient и также примет участие в приеме новых запросов от клиентов.
На самом деле без вызова os._exit мы получили бы столько вечных процессов сервера, сколько было обслужено запросов — уберите вызов
os._exit, выполните команду оболочки ps после запуска нескольких клиентов, и вы поймете, что я имею в виду. При наличии вызова этой функции только родительский процесс будет ждать новые запросы. Функция os._exit похожа на sys.exit, но завершает вызвавший его процесс сразу, не выполняя заключительных операций. Обычно он используется только в дочерних процессах, а sys.exit используется во всех остальных случаях.
Удаление зомби: не бойтесь грязной работы
Заметьте, однако, что недостаточно просто убедиться в завершении дочернего процесса. В таких системах, как Linux, но не в Cygwin, родительский процесс должен также выполнить системный вызов wait, чтобы удалить записи, касающиеся завершившихся дочерних процессов, из системной таблицы процессов. Если этого не сделать, то дочерние процессы выполняться не будут, но будут занимать место в системной таблице процессов. Для серверов, выполняющихся длительное время, такие фальшивые записи могут вызвать неприятности.
Такие недействующие, но числящиеся в строю процессы обычно называют зом би: они продолжают использовать системные ресурсы даже после возврата в операционную систему. Для освобождения ресурсов, занимаемых завершившимися дочерними процессами, наш сервер ведет список activeChildren, содержащий идентификаторы всех порожденных им дочерних процессов. При получении нового запроса от клиента сервер вызывает функцию reapChildren, чтобы вызвать wait для всех завершившихся дочерних процессов путем вызова стандартной функции Python os.waitpid(0,os.WNOHANG).
Функция os.waitpid пытается дождаться завершения дочернего процесса и возвращает идентификатор этого процесса и код завершения. При передаче 0 в первом аргументе ожидается завершение любого дочернего процесса. При передаче значения WNOHANG во втором аргументе функция ничего не делает, если к этому моменту никакой дочерний процесс не завершился (то есть вызвавший процесс не блокируется и не приостанавливается). В итоге данный вызов просто запрашивает у операционной системы идентификатор любого завершившегося дочернего процесса. Если такой процесс есть, полученный идентификатор удаляется из системной таблицы процессов и из списка activeChildren этого сценария.
Чтобы понять, для чего нужны такие сложности, закомментируйте в этом сценарии вызов функции reapChildren, запустите его на сервере, где проявляются описанные выше проблемы, а затем запустите несколько клиентов. На моем сервере Linux команда ps —f, которая выводит полный список процессов, показывает, что все завершившиеся дочерние процессы сохраняются в системной таблице процессов (помечены как <defunct>):
[…]$ ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 |
9990 30778 |
0 04:34 pts/0 |
00:00:00 python fork-server.py |
5693094 |
10844 9990 |
0 04:35 pts/0 |
00:00:00 [python] <defunct> |
5693094 |
10869 9990 |
0 04:35 pts/0 |
00:00:00 [python] <defunct> |
5693094 |
11130 9990 |
0 04:36 pts/0 |
00:00:00 [python] <defunct> |
5693094 |
11151 9990 |
0 04:36 pts/0 |
00:00:00 [python] <defunct> |
5693094 |
11482 30778 |
0 04:36 pts/0 |
00:00:00 ps -f |
5693094 |
30778 30772 |
0 04:23 pts/0 |
00:00:00 -bash |
Если снова раскомментировать вызов функции reapChildren, записи о завершившихся дочерних зомби будут удаляться всякий раз, когда сервер будет получать от клиента новый запрос на соединение, путем вызова функции os.waitpid. Если сервер сильно загружен, может накопиться несколько зомби, но они сохранятся только до получения нового запроса на соединение от клиента:
[…]$ python fork-server.py &
[1] 20515
[…]$ ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 20515 30778 0 04:43 pts/0 00:00:00 python fork-server.py
5693094 20777 30778 0 04:43 pts/0 00:00:00 ps -f
5693094 30778 30772 0 04:23 pts/0 00:00:00 -bash
[…]$
Server connected by (‘72.236.109.185’, 58672) at Sun Apr 25 04:43:51 2010
Server connected by (‘72.236.109.185’, 58673) at Sun Apr 25 04:43:54 2010 […]$ ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 20515 30778 0 04:43 pts/0 00:00:00 python fork-server.py
5693094 21339 20515 0 04:43 pts/0 00:00:00 [python] <defunct>
5693094 21398 20515 0 04:43 pts/0 00:00:00 [python] <defunct>
5693094 21573 30778 0 04:44 pts/0 00:00:00 ps -f
5693094 30778 30772 0 04:23 pts/0 00:00:00 -bash
[…]$
Server connected by (‘72.236.109.185’, 58674) at Sun Apr 25 04:44:07 2010 […]$ ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 20515 30778 0 04:43 pts/0 00:00:00 python fork-server.py
5693094 21646 20515 0 04:44 pts/0 00:00:00 [python] <defunct>
5693094 21813 30778 0 04:44 pts/0 00:00:00 ps —f
5693094 30778 30772 0 04:23 pts/0 00:00:00 —bash
Фактически, если вы печатаете очень быстро, можно успеть увидеть, как дочерний процесс превращается из выполняющейся программы в зомби. Здесь, например, дочерний процесс, порожденный для обработки нового запроса, при выходе превращается в <defunct>. Его подключение удаляет оставшиеся зомби, а его собственная запись о процессе будет полностью удалена при получении следующего запроса:
[…]$
Server connected by (‘72.236.109.185’, 58676) at Sun Apr 25 04:48:22 2010 […] ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 |
20515 30778 |
0 04:43 |
pts/0 |
00:00:00 |
python |
fork-server.py |
||
5693094 |
27120 |
20515 |
0 |
04:48 |
pts/0 |
00:00:00 |
python |
fork-server.py |
5693094 |
27174 |
30778 |
0 |
04:48 |
pts/0 |
00:00:00 |
ps -f |
|
5693094 […]$ |
30778 ps -f |
30772 |
0 |
04:23 |
pts/0 |
00:00:00 |
-bash |
|
UID |
PID |
PPID |
C |
STIME |
TTY |
TIME |
CMD |
|
5693094 |
20515 |
30778 |
0 |
04:43 |
pts/0 |
00:00:00 |
python |
fork-server.py |
5693094 |
27120 |
20515 |
0 |
04:48 |
pts/0 |
00:00:00 |
[python] <defunct> |
|
5693094 |
27234 |
30778 |
0 |
04:48 |
pts/0 |
00:00:00 |
ps -f |
|
5693094 |
30778 |
30772 |
0 |
04:23 |
pts/0 |
00:00:00 |
-bash |
|
Предотвращение появления зомби с помощью обработчиков сигналов
В некоторых системах можно также удалять дочерние процессы-зомби путем переустановки обработчика сигнала SIGCHLD, отправляемого операционной системой родительскому процессу по завершении дочернего процесса. Если сценарий Python определит в качестве обработчика сигнала SIGCHLD действие SIG_IGN (игнорировать), зомби будут удаляться автоматически и немедленно по завершении дочерних процессов — родительскому процессу не придется выполнять вызовы wait, чтобы освободить ресурсы, занимаемые ими. Благодаря этому такая схема служит более простой альтернативой ручному удалению зомби на платформах, где она поддерживается.
Если вы прочли главу 5, то знаете, что обработчики сигналов, программ- но-генерируемых событий, можно устанавливать с помощью стандартного модуля Python signal. В качестве демонстрации ниже приводится небольшой пример, который показывает, как это можно использовать для удаления зомби. Сценарий в примере 12.5 устанавливает функцию обработчика сигналов, написанную на языке Python, реагирующую на номер сигнала, вводимый в командной строке.
Пример 12.5. PP4E\Internet\Sockets\signal-demo.py
Демонстрация модуля signal; номер сигнала передается в аргументе командной строки, а отправить сигнал этому процессу можно с помощью команды оболочки "kill —N pid"; на моем компьютере с Linux SIGUSR1=10, SIGUSR2=12, SIGCHLD=17 и обработчик SIGCHLD остается действующим, даже если не восстанавливается в исходное состояние: все остальные обработчики сигналов переустанавливаются интерпретатором Python после получения сигнала, но поведение сигнала SIGCHLD не регламентируется и его реализация оставлена за платформой;
модуль signal можно также использовать в Windows, но в ней доступны лишь несколько типов сигналов; в целом сигналы не очень хорошо переносимы;
import sys, signal, time
return time.asctime()
def onSignal(signum, stackframe): # обработчик сигнала на Python
print(‘Got signal’, signum, ‘at’, now()) # большинство обработчиков if signum == signal.SIGCHLD: # не требуется переустанавливать,
print(‘sigchld caught’) # кроме обработчика sigchld
#signal.signal(signal.SIGCHLD, onSignal)
Чтобы опробовать этот сценарий, просто запустите его в фоновом режиме и посылайте ему сигналы, вводя команду kill -нонер-сигннлн id-про- цеснн в командной строке — это эквивалент функции os.kill в языке Python, доступной только в Unix-подобных системах. Идентификаторы процессов перечислены в колонке PID результатов выполнения команды ps. Ниже показано, как действует этот сценарий, перехватывая сигналы с номерами 10 (зарезервирован для общего использования) и 9 (безусловный сигнал завершения):
[…]$ python signal-demo.py 10 &
[1] 10141
[…]$ ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 10141 30778 0 05:00 pts/0 00:00:00 python signal-demo.py 10
5693094 10228 30778 0 05:00 pts/0 00:00:00 ps -f
5693094 30778 30772 0 04:23 pts/0 00:00:00 -bash
[…]$ kill -10 10141
Got signal 10 at Sun Apr 25 05:00:31 2010
[…]$ kill -10 10141
Got signal 10 at Sun Apr 25 05:00:34 2010
[…]$ kill -9 10141
[1]+ Killed python signal-demo.py 10
А в следующем примере сценарий перехватывает сигнал с номером 17, который на моем сервере с Linux соответствует сигналу SIGCHLD. Номера сигналов зависят от используемой операционной системы, поэтому обычно следует пользоваться именами сигналов, а не номерами. Поведение сигнала SIGCHLD тоже может зависеть от платформы. У меня в установленной оболочке Cygwin, например, сигнал с номером 10 может иметь другое назначение, а сигнал SIGCHLD имеет номер 20. В Cygwin данный сценарий обрабатывает сигнал 10 так же, как в Linux, но при попытке установить обработчик сигнала 17 возбуждает исключение (впрочем, в Cygwin нет необходимости удалять зомби). Дополнительные подробности смотрите в руководстве по библиотеке, в разделе с описанием модуля signal:
[…]$ python signal-demo.py 17 &
[…]$ ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 11592 30778 0 05:00 pts/0 00:00:00 python signal-demo.py 17
5693094 11728 30778 0 05:01 pts/0 00:00:00 ps -f
5693094 30778 30772 0 04:23 pts/0 00:00:00 -bash
[…]$ kill -17 11592
Got signal 17 at Sun Apr 25 05:01:28 2010
sigchld caught
[…]$ kill -17 11592
Got signal 17 at Sun Apr 25 05:01:35 2010
sigchld caught
[…]$ kill -9 11592
[1]+ Killed python signal-demo.py 17
Теперь, чтобы применить все эти знания для удаления зомби, просто установим в качестве обработчика сигнала SIGCHLD действие SIG_IGN — в системах, где поддерживается такое назначение, дочерние процессы будут удаляться сразу же по их завершении. Вариант ветвящегося сервера, представленный в примере 12.6, использует этот прием для управления своими дочерними процессами.
Пример 12.6. PP4E\Internet\Sockets\fork-server-signal.py
То же, что и fork—server.py, но использует модуль signal, чтобы обеспечить автоматическое удаление дочерних процессов-зомби после их завершения вместо явного удаления перед приемом каждого нового соединения; действие SIG_IGN означает игнорирование и может действовать с сигналом SIGCHLD завершения дочерних процессов не на всех платформах; смотрите документацию к операционной системе Linux, где описывается возможность перезапуска вызова socket.accept, прерванного сигналом;
import os, time, sys, signal, signal
from socket import * # получить конструктор сокета и константы myHost = » # компьютер сервера, » означает локальный хост
myPort = 50007 # использовать незарезервированный номер порта
sockobj = socket(AF_INET, SOCK_STREAM) # создать объект сокета TCP
sockobj.bind((myHost, myPort))
sockobj.listen(5)
signal.signal(signal.SIGCHLD, signal.
def now():
return time.ctime(time.time())
def handleClient(connection):
time.sleep(5)
while True:
data = connection.recv(1024)
if not data: break
reply = ‘Echo=>%s at %s’ % (data, now())
connection.send(reply.encode())
connection.close()
os._exit(0)
def dispatcher(): # пока процесс работает
while True: # ждать запроса очередного клиента,
connection, address = sockobj.accept() # передать процессу
print(‘Server connected by’, address, end=’ ‘) # для обслуживания print(‘at’, now()) childPid = os.fork() # копировать этот процесс
if childPid == 0: # в дочернем процессе: обслужить
handleClient(connection) # иначе: ждать следующего запроса
dispatcher()
Там, где возможно его применение, такой прием:
• Гораздо проще — не нужно следить за дочерними процессами и вручную убирать их.
• Более точный — нет зомби, временно присутствующих в промежутке между запросами клиентов.
На самом деле обработкой зомби здесь занимается всего одна строка программного кода: вызов функции signal.signal в начале сценария, устанавливающий обработчик. К сожалению, данная версия еще в меньшей степени переносима, чем первая с использованием os.fork, поскольку действие сигналов может несколько различаться в зависимости от платформы. Например, на некоторых платформах вообще не разрешается использовать SIG_IGN в качестве действия для SIGCHLD. Однако в системах Linux этот более простой сервер с ветвлением действует замечательно:
[…]$ python fork-server-signal.py &
[1] 3837
Server connected by (‘72.236.109.185’, 58817) at Sun Apr 25 08:11:12 2010
[…] ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 3837 30778 0 08:10 pts/0 00:00:00 python fork-server-signal.py
5693094 4378 3837 0 08:11 pts/0 00:00:00 python fork-server-signal.py
5693094 4413 30778 0 08:11 pts/0 00:00:00 ps -f
5693094 30778 30772 0 04:23 pts/0 00:00:00 -bash
[…]$ ps -f
UID PID PPID C STIME TTY TIME CMD
5693094 3837 30778 0 08:10 pts/0 00:00:00 python fork-server-signal.py
5693094 4584 30778 0 08:11 pts/0 00:00:00 ps —f
5693094 30778 30772 0 04:23 pts/0 00:00:00 —bash
Обратите внимание, что в этой версии запись о дочернем процессе исчезает сразу, как только он завершается, даже раньше, чем будет получен новый клиентский запрос. Никаких зомби с пометкой «defunct» не возникает. Еще более знаменательно, что если теперь запустить наш более старый сценарий, порождающий восемь параллельных клиентов (testecho.py), соединяющихся с сервером, то все они появляются на сервере при выполнении и немедленно удаляются после завершения:
[окно клиента]
C:\…\PP4E\Internet\Sockets> testecho.py learning-python.com
[окно сервера] […]$
Server connected by ( Server connected by ( Server connected by ( Server connected by ( Server connected by ( Server connected by ( Server connected by ( Server connected by ( |
‘72.236.109.185’ ‘72.236.109.185’ ‘72.236.109.185’ ‘72.236.109.185’ ‘72.236.109.185’ ‘72.236.109.185’ ‘72.236.109.185’ ‘72.236.109.185’ |
, 58829) at Sun Apr 25 08:16:34 2010 , 58830) at Sun Apr 25 08:16:34 2010 , 58831) at Sun Apr 25 08:16:34 2010 , 58832) at Sun Apr 25 08:16:34 2010 , 58833) at Sun Apr 25 08:16:34 2010 , 58834) at Sun Apr 25 08:16:34 2010 , 58835) at Sun Apr 25 08:16:34 2010 |
|
, 58836) at Sun |
Apr 25 08:16:34 2010 |
||
[…]$ ps -f |
|
|
|
UID PID PPID |
C STIME TTY |
TIME CMD |
|
5693094 3837 30778 |
0 08:10 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9666 3837 |
0 08:16 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9667 3837 |
0 08:16 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9668 3837 |
0 08:16 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9670 3837 |
0 08:16 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9674 3837 |
0 08:16 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9678 3837 |
0 08:16 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9681 3837 |
0 08:16 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9682 3837 |
0 08:16 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 9722 30778 |
0 08:16 pts/0 |
00:00:00 ps -f |
|
5693094 30778 30772 |
0 04:23 pts/0 |
00:00:00 -bash |
|
[…]$ ps -f |
|
|
|
UID PID PPID |
C STIME TTY |
TIME CMD |
|
5693094 3837 30778 |
0 08:10 pts/0 |
00:00:00 python |
fork-server-signal.py |
5693094 10045 30778 |
0 08:16 pts/0 |
00:00:00 ps -f |
|
5693094 30778 30772 |
0 04:23 pts/0 |
00:00:00 -bash |
|
Теперь, когда я показал вам, как использовать обработчики сигналов для автоматического удаления записей о дочерних процессах в Linux, я должен подчеркнуть, что этот прием не является универсальным и поддерживается не всеми версиями Unix. Если переносимость имеет важное значение, предпочтительнее использовать прием удаления дочерних процессов вручную, использовавшийся в примере 12.4.
Почему модуль multiprocessing не обеспечивает переносимость серверов сокетов
В главе 5 мы познакомились с новым модулем multiprocessing. Как мы видели, он обеспечивает более переносимую возможность выполнения функций в новых процессах, чем функция os.fork, использованная в реализации этого сервера, и выполняет их не в потоках, а в отдельных процессах, чтобы обойти ограничения, накладываемые глобальной блокировкой GIL. В частности, модуль multiprocessing можно использовать также в стандартной версии Python для Windows, в отличие от функции os.fork.
Я решил поэкспериментировать с версией сервера, опирающегося на этот модуль, чтобы посмотреть, сможет ли он помочь повысить переносимость серверов сокетов. Полный программный код этого сервера можно найти в файле multi—server.py в дереве примеров, а ниже приводятся несколько наиболее важных отличительных фрагментов:
…остальной программный код не отличается от fork—server.py…
from multiprocessing import Process
def handleClient(connection):
print(‘Child:’, os.getpid()) # дочерний процесс: ответить, выйти
time.sleep(5) # имитировать блокирующие действия
while True: # чтение, запись в сокет клиента
data = connection.recv(1024) # продолжать, пока сокет
# не будет закрыт … остальной программный код не отличается…
def dispatcher(): # пока процесс работает
while True: # ждать запроса очередного клиента
connection, address = sockobj.accept() # передать процессу print(‘Server connected by’, address, end=’ ‘) # для обслуживания print(‘at’, now())
Process(target=handleClient, args=(connection,)).start()
if __name__ == ‘__main__’:
print(‘Parent:’, os.getpid())
sockobj = socket(AF_INET, SOCK_STREAM) # создать объект сокета TCP sockobj.bind((myHost, myPort)) # связать с номером порта сервера sockobj.listen(5) # не более 5 ожидающих запросов
dispatcher()
Эта версия сервера заметно проще. Подобно ветвящемуся серверу, версией которого он является, данный сервер отлично работает на компьютере localhost под управлением Python для Cygwin в Windows. Вероятно, он также будет работать в других Unix-подобных системах, потому что в таких системах модуль multiprocessing использует прием ветвления процессов, при котором дескрипторы файлов и сокетов наследуются дочерними процессами как обычно. Следовательно, дочерний процесс будет использовать тот же самый подключенный сокет, что и родительский процесс. Ниже демонстрируется картина, наблюдаемая в окне оболочки Cygwin в Windows, где запущен сервер, и в двух окнах с клиентами:
[окно сервера]
[C:\…\PP4E\Internet\Sockets]$ python multi-server.py
Parent: 8388
Server connected by (‘127.0.0.1’, 58271) at Sat Apr 24 08:13:27 2010
Child: 8144
Server connected by (‘127.0.0.1’, 58272) at Sat Apr 24 08:13:29 2010
Child: 8036
[два окна клиентов]
C:\…\PP4E\Internet\Sockets> python echo-client.py
Client received: b"Echo=>b’Hello network world’ at Sat Apr 24 08:13:33 2010"
C:\…\PP4E\Internet\Sockets> python echo-client.py localhost Brave Sir Robin
Client received: b"Echo=>b’Brave’ at Sat Apr 24 08:13:35 2010"
Client received: b"Echo=>b’Sir’ at Sat Apr 24 08:13:35 2010"
Client received: b"Echo=>b’Robin’ at Sat Apr 24 08:13:35 2010"
Однако этот сервер не работает под управлением стандартной версии Python для Windows — из-за попытки использовать модуль multiprocessing в этом контексте — потому что открытые сокеты некорректно сериализуются при передаче новому процессу в виде аргументов. Ниже показано, что происходит в окне сервера в Windows 7, где установлена версия Python 3.1:
C:\…\PP4E\Internet\Sockets> python multi-server.py Parent: 9140
Server connected by (‘127.0.0.1’, 58276) at Sat Apr 24 08:17:41 2010
Child: 9628
Process Process-1:
Traceback (most recent call last):
File "C:\Python31\lib\multiprocessing\process.py", line 233, in _bootstrap self.run()
File "C:\Python31\lib\multiprocessing\process.py", line 88, in run self._target(*self._args, **self._kwargs)
File "C:\…\PP4E\Internet\Sockets\multi-server.py", line 38, in
handleClient data = connection.recv(1024) # продолжать, пока сокет… socket.error: [Errno 10038] An operation was attempted on something that is not a socket
(socket.error: [Ошибка 10038] Попытка выполнить операцию с объектом, не являющимся сокетом)
Как рассказывалось в главе 5, в Windows модуль multiprocessing передает контекст новому процессу интерпретатора Python, сериализуя его с помощью модуля pickle, поэтому аргументы конструктора Process при вызове в Windows должны поддерживать возможность сериализации. При попытке сериализовать сокеты в Python 3.1 ошибки не возникает, благодаря тому, что они являются экземплярами классов, но сама сериализация выполняется некорректно:
> >> from pickle import *
> >> from socket import *
> >> s = socket()
>>> x = dumps(s)
>>> s
<socket.socket object, fd=180, family=2, type=1, proto=0>
>>> loads(x)
<socket.socket object, fd=-1, family=0, type=0, proto=0>
>>> x
b’\x80\x03csocket\nsocket\nq\x00)\x81q\x01N}q\x02(X\x08\x00\x00\x00_io_ refsq\x03K\x00X\x07\x00\x00\x00_closedq\x04\x89u\x86q\x05b.’
Как мы видели в главе 5, модуль multiprocessing имеет другие инструменты IPC, такие как его собственные каналы и очереди, которые могут использоваться вместо сокетов для решения этой проблемы. Но тогда их должны были бы использовать и клиенты — получившийся в результате сервер оказался бы не так широко доступен, как сервер на основе сокетов Интернета.
Но даже если бы модуль multiprocessing работал в Windows, необходимость запускать новый процесс интерпретатора Python сделала бы сервер более медленным, чем более традиционные приемы порождения дочерних потоков выполнения для общения с клиентами. Что, по случайному совпадению, является темой следующего раздела.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, II том, 2011