Discussion:
Безумные сокеты и параллельное чтение с закрытием
(слишком старое сообщение для ответа)
Valentin Nechayev
2013-09-17 15:44:26 UTC
Permalink
Столкнулись со следующим эффектом на Linux и не знаю, как это
объяснить. То, что мы попали в область, не описанную стандартами,
понятно, но его конкретная реализация поведения, мягко говоря,
удивляет.

Серверная сторона принимает TCP соединение и в одной из ниток делает
ему read(). По получению SIGTERM, отдельная нитка делает close() всем
известным сокетам. (Всё это внутри библиотеки.)
С этого момента первая нитка остаётся в read() до получения любых
данных по сокету (включая закрытие от клиента), после чего происходит
это самое закрытие (дескриптор больше не валиден). Самое интересное,
что netstat показывает сокет как ESTABLISHED, но никому не
принадлежащий!

Тестовая программа:

===
#!/usr/bin/env python

import socket
import threading
import time
import os

PORT = 40900

def doClient():
cln = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
cln.connect(('127.0.0.1', 40900))
print 'Client is connected'
time.sleep(60)
print 'Client is closing'
cln.close()
print 'Client is closed'

def doServer(st):
time.sleep(1)
cancel_thread = threading.Thread(target = doCancel, name = 'canceller',
args = (st,))
cancel_thread.daemon = True
cancel_thread.start()
try:
r = st.recv(65536)
print 'doServer: r=%r' % (r,)
except Exception as e:
print 'doServer: exception: %r' % (e,)

def doCancel(st):
print 'doCancel'
time.sleep(1)
os.close(st.fileno())

def doDaemon():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('', 40900))
srv.listen(5)
while True:
s2, addr = srv.accept()
print 'Accepted connection from %r' % (addr,)
st = threading.Thread(target = doServer, name = 'server',
args = (s2,))
st.daemon = True
st.start()

if __name__ == '__main__':
dt = threading.Thread(target = doDaemon, name = 'daemon')
dt.daemon = True
dt.start()
time.sleep(1)
ct = threading.Thread(target = doClient, name = 'client')
ct.daemon = True
ct.start()
while True:
os.system('netstat -antp | fgrep 40900')
time.sleep(3)

===

Образец лога:

===
Script started on Tue Sep 17 18:27:12 2013
Client is connectedAccepted connection from ('127.0.0.1', 34721)

(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:40900 0.0.0.0:* LISTEN 32024/python
tcp 0 0 127.0.0.1:40900 127.0.0.1:34721 ESTABLISHED 32024/python
tcp 0 0 127.0.0.1:34721 127.0.0.1:40900 ESTABLISHED 32024/python
doCancel
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:40900 0.0.0.0:* LISTEN 32024/python
tcp 0 0 127.0.0.1:40900 127.0.0.1:34721 ESTABLISHED -
tcp 0 0 127.0.0.1:34721 127.0.0.1:40900 ESTABLISHED 32024/python
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:40900 0.0.0.0:* LISTEN 32024/python
tcp 0 0 127.0.0.1:40900 127.0.0.1:34721 ESTABLISHED -
tcp 0 0 127.0.0.1:34721 127.0.0.1:40900 ESTABLISHED 32024/python
[... так не меняется до закрытия со стороны клиента ...]
Client is closing
Client is closed
doServer: r=''
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:40900 0.0.0.0:* LISTEN 32024/python
tcp 0 0 127.0.0.1:34721 127.0.0.1:40900 TIME_WAIT -

===

Hаша задача - обеспечить работу завершения серверного процесса с
минимальными деструктивными последствиями для кода:) Вопрос о смене
алгоритма закрытия - за пределами данной темы и будет решаться
отдельно. Hо как можно минимально деструктивно для кода (с учётом
того, что правки надо посылать в апстрим) решить проблему?
Пока что видится один вариант - заменить read() обычный на poll() для
проверки читаемости-или-закрытия с последующим read() при успехе. Hо
Posix обещает POLLHUP при закрытии сокета со стороны, а Linux - нет
(по крайней мере по ману).


--netch--
Valentin Nechayev
2013-09-17 15:58:30 UTC
Permalink
VN> С этого момента первая нитка остаётся в read() до получения любых
VN> данных по сокету (включая закрытие от клиента), после чего происходит
VN> это самое закрытие (дескриптор больше не валиден). Самое интересное,
VN> что netstat показывает сокет как ESTABLISHED, но никому не
VN> принадлежащий!

shutdown(s, SHUT_RD) из закрывающей нитки - оказалось достаточным.
В принципе на сейчас нам этого решения достаточно...


--netch--
Serguei E. Leontiev
2013-09-17 20:57:41 UTC
Permalink
Привет Valentin,

От вт, 17 сен 2013 19:58:30 в fido7.ru.unix.prog ты писал:
VN>> С этого момента первая нитка остаётся в read() до получения любых
VN>> данных по сокету (включая закрытие от клиента), после чего происходит
VN>> это самое закрытие (дескриптор больше не валиден). Самое интересное,
VN>> что netstat показывает сокет как ESTABLISHED, но никому не
VN>> принадлежащий!
VN> shutdown(s, SHUT_RD) из закрывающей нитки - оказалось достаточным.
VN> В принципе на сейчас нам этого решения достаточно...

shutdown(s, SHUT_RD), наверное неплохо, если я правильно понимаю
после него recv() должен всегда выдавать 0 (либо EBADF, при
вызове после close()). Однако, если бывают send(), то наверное
SHUT_RDWR нужно использовать?
--
Успехов, Сергей Леонтьев. E-mail: ***@CryptoPro.ru <http://www.cryptopro.ru>
Valentin Nechayev
2013-09-18 06:15:41 UTC
Permalink
SEL> shutdown(s, SHUT_RD), наверное неплохо, если я правильно понимаю
SEL> после него recv() должен всегда выдавать 0 (либо EBADF, при
SEL> вызове после close()). Однако, если бывают send(), то наверное
SEL> SHUT_RDWR нужно использовать?

Там send() не полагается - чисто симплексный поток. (Да, я знаю, что
это само по себе не хорошо, но переламывать всю библиотеку нам не с
руки.)


--netch--
Serguei E. Leontiev
2013-09-17 20:48:09 UTC
Permalink
Привет Valentin,

От вт, 17 сен 2013 19:44:26 в fido7.ru.unix.prog ты писал:
VN> Столкнулись со следующим эффектом на Linux и не знаю, как это
VN> объяснить. То, что мы попали в область, не описанную стандартами,
VN> понятно, но его конкретная реализация поведения, мягко говоря,
VN> удивляет.
VN> ...
VN> С этого момента первая нитка остаётся в read() до получения любых
VN> данных по сокету (включая закрытие от клиента), после чего происходит
VN> это самое закрытие (дескриптор больше не валиден). Самое интересное,

Как я понимаю, такое поведение допустимо:
"...All operations that are not canceled shall complete as if
the close() blocked until the operations completed. The close()
operation itself need not block awaiting such I/O completion..."

А ты чего бы ожидал? EBADF на recv() и исключение, как в Mac OSX?

А объяснить, так всё просто: вероятно в Linux используется
единый счётчик использования дескриптора, как для потоков, так и
для процессов.

VN> Пока что видится один вариант - заменить read() обычный на poll() для
VN> проверки читаемости-или-закрытия с последующим read() при успехе. Hо
VN> Posix обещает POLLHUP при закрытии сокета со стороны, а Linux - нет
VN> (по крайней мере по ману).

POLLHUP, строго говоря, он о закрытии с той стороны.
--
Успехов, Сергей Леонтьев. E-mail: ***@CryptoPro.ru <http://www.cryptopro.ru>
Valentin Nechayev
2013-09-18 06:27:44 UTC
Permalink
SEL> Как я понимаю, такое поведение допустимо:
SEL> "...All operations that are not canceled shall complete as if
SEL> the close() blocked until the operations completed. The close()
SEL> operation itself need not block awaiting such I/O completion..."

SEL> А ты чего бы ожидал? EBADF на recv() и исключение, как в Mac OSX?

О каком исключении речь, я не понял, но какая-то немедленная ошибка
выглядит тут логичнее.

SEL> А объяснить, так всё просто: вероятно в Linux используется
SEL> единый счётчик использования дескриптора, как для потоков, так и
SEL> для процессов.

Скорее всего, так и есть.


--netch--
Serguei E. Leontiev
2013-09-20 22:24:23 UTC
Permalink
Привет Valentin,

От ср, 18 сен 2013 10:27:44 в fido7.ru.unix.prog ты писал:
SEL>> А ты чего бы ожидал? EBADF на recv() и исключение, как в Mac OSX?
VN> О каком исключении речь, я не понял,

Это я твой пример запустил. Если данных нет, то:

doCancel
doServer: exception: error(9, 'Bad file descriptor')

Если данные были, то:

doCancel
doServer: r='xxx'
doServer1: exception: error(9, 'Bad file descriptor')
tcp4 0 0 127.0.0.1.40900 127.0.0.1.60515 FIN_WAIT_2
tcp4 0 0 127.0.0.1.60515 127.0.0.1.40900 CLOSE_WAIT

Hо сокет, всё равно остаётся в ожидании FIN_WAIT_2 до закрытия на клиенте.

VN> но какая-то немедленная ошибка
VN> выглядит тут логичнее.

После некоторых раздумий, мне показалось, что иделальной
реакцией было бы наоборот, не возвращать ошибку, но возвращать
0, если во входной очереди не осталось данных, и возвращать
все принятые входной очереди в противном случае.
--
Успехов, Сергей Леонтьев. E-mail: ***@CryptoPro.ru <http://www.cryptopro.ru>
Valentin Nechayev
2013-09-21 16:57:13 UTC
Permalink
SEL>>> А ты чего бы ожидал? EBADF на recv() и исключение, как в Mac OSX?
VN>> О каком исключении речь, я не понял,
SEL> Это я твой пример запустил. Если данных нет, то:

SEL> doCancel
SEL> doServer: exception: error(9, 'Bad file descriptor')

Хм, это интересный результат. Потому что у меня для FreeBSD получился
результат идентичный тому, что в Linux - при shutdown() из соседнего
треда произошёл аккуратный выход из read() с нулём.

Я раньше думал, что особенности микроядерности MacOS X не настолько
существенны, и она ведёт себя как остальные BSD - но, оказывается,
всё сложнее.

Hадо бы другие BSD как-нибудь пощупать.

SEL> Hо сокет, всё равно остаётся в ожидании FIN_WAIT_2 до закрытия на клиенте.

Это вполне нормально. Более того, при тесте через localhost возможно,
что таймаут на FIN_WAIT_2 не будет применяться.

VN>> но какая-то немедленная ошибка
VN>> выглядит тут логичнее.
SEL> После некоторых раздумий, мне показалось, что иделальной
SEL> реакцией было бы наоборот, не возвращать ошибку, но возвращать
SEL> 0, если во входной очереди не осталось данных, и возвращать
SEL> все принятые входной очереди в противном случае.

То есть так, как в Linux & FreeBSD.


--netch--

Loading...