Почему Windows select () не всегда уведомляет select () потока B, когда поток A закрывает свой конец пары сокетов?

Ситуация, которая возникла у меня под Windows XP (SP3), сводила меня с ума, и я дошел до конца, так что, возможно, кто-то может вдохновить меня.

У меня есть сетевая программа на C ++ (без графического интерфейса). Эта программа создана для компиляции и запуска под Windows, MacOS / X и Linux, поэтому она использует select () и неблокирующий ввод-вывод в качестве основы для своего цикла событий.

В дополнение к своим сетевым обязанностям этой программе необходимо читать текстовые команды из stdin и корректно завершать работу при закрытии stdin. В Linux и MacOS / X это достаточно просто - я просто включаю STDIN_FILENO в свой read fd_set для select (), а select () возвращается при закрытии stdin. Я проверяю, что FD_ISSET (STDIN_FILENO, & readSet) истинно, пытаюсь прочитать некоторые данные из stdin, recv () возвращает 0 / EOF, и поэтому я выхожу из процесса.

С другой стороны, под Windows вы не можете выбрать STDIN_FILE_HANDLE, потому что это не настоящий сокет. Вы также не можете выполнять неблокирующее чтение на STDIN_FILE_HANDLE. Это означает, что нет возможности читать stdin из основного потока, поскольку ReadFile () может блокироваться на неопределенное время, в результате чего основной поток перестает обслуживать свою сетевую функцию.

Нет проблем, говорю я, я просто создам поток для обработки стандартного ввода за меня. Этот поток будет работать в бесконечном цикле с блокировкой в ​​ReadFile (stdinHandle), и всякий раз, когда ReadFile () возвращает данные, поток stdin будет записывать эти данные в сокет TCP. Другой конец соединения этого сокета будет выбран () d на основном потоке, поэтому основной поток будет видеть данные stdin, поступающие через соединение, и обрабатывать "stdin" так же, как и в любой другой ОС. И если ReadFile () возвращает false, чтобы указать, что stdin закрыт, stdin-thread просто закрывает свой конец пары сокетов, так что основной поток будет уведомлен через select (), как описано выше.

Конечно, в Windows нет красивой функции socketpair (), поэтому мне пришлось свернуть свою собственную, используя listen (), connect () и accept () (как видно из функции CreateConnectedSocketPair () здесь. Но я сделал это, и, похоже, в целом это работает.

Проблема в том, что он не работает на 100%. В частности, если stdin закрывается в течение нескольких сотен миллисекунд после запуска программы, примерно в половине случаев основной поток не получает никакого уведомления о закрытии stdin-end пары сокетов. Я имею в виду, что я вижу (по моей printf () - отладке), что stdin-thread вызвал closesocket () в своем сокете, и я вижу, что основной поток select () - ing на связанном socket (то есть другой конец пары сокетов), но select () никогда не возвращается, как должен ... и если он действительно возвращается из-за того, что какой-то другой сокет выбирает готовый для чего угодно, FD_ISSET (main_thread_socket_for_socket_pair, & readSet) возвращает 0, как если бы соединение не было закрыто.

На данный момент у меня есть единственная гипотеза, что в реализации select () Windows есть ошибка, из-за которой функция select () основного потока не замечает, что другой конец пары сокетов закрыт потоком stdin. Есть другое объяснение? (Обратите внимание, что об этой проблеме также сообщалось в Windows 7, хотя я лично не рассматривал ее на этой платформе)


person Jeremy Friesner    schedule 28.01.2010    source источник


Ответы (6)


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

Приносим извинения всем за то, что потратили время на отвлекающий маневр; если есть стандартный способ закрыть это дело как необоснованное, дайте мне знать, и я это сделаю.

-Джереми

person Jeremy Friesner    schedule 10.02.2010

Возможно ли у вас состояние гонки? Например. Вы гарантируете, что функция CreateConnectedSocketPair() определенно вернулась до того, как stdin-thread сможет попытаться закрыть свой сокет?

person caf    schedule 28.01.2010
comment
Это очень хорошее предположение, но я дважды проверил, и поток stdin не запускается до тех пор, пока CreateConnectedSocketPair () не вернется успешно. Так что я не думаю, что это проблема. - person Jeremy Friesner; 28.01.2010

Я изучаю ваш код. В CreateConnectedSocketPair () socket1 используется для listen (), а newfd используется для отправки / получения данных. Итак, почему "socket1 = newfd"? Как тогда закрыть listenfd?

person Yufei Ren    schedule 28.01.2010
comment
Сокеты подсчитываются по ссылкам (обратите внимание, что их тип - ConstSocketRef, а не int). Таким образом, listenfd будет закрыт, когда его счетчик ссылок упадет до нуля (то есть оператором присваивания в строке, которую вы процитировали) - person Jeremy Friesner; 28.01.2010

Не решение, но в качестве обходного пути, не могли бы вы отправить какое-то волшебное сообщение «stdin has closed» через TCP-сокет и заставить принимающую сторону отключать свой сокет, когда он это видит, и запускать любой обработчик «stdin has closed»?

person bdk    schedule 05.02.2010
comment
Это сработает в большинстве случаев, но если я хочу, чтобы соединение обрабатывало произвольные данные, тогда любая последовательность байтов волшебного сообщения может случайно быть отправлена ​​по совпадению как часть обычных данных, отправляемых через стандартный ввод. Я бы предпочел по возможности избежать такой возможности. - person Jeremy Friesner; 10.02.2010

Честно говоря, ваш код слишком длинный, и у меня сейчас нет времени на него тратить.

Скорее всего, проблема в некоторых случаях заключается в том, что закрытие сокета не приводит к плавному завершению работы (FIN).

Проверка исключений, возвращаемых из вашего выбора, может выявить остальные случаи. Существует также (небольшая) вероятность того, что в сокет не отправляется уведомление о том, что другой конец закрыт. В этом случае нет другого способа, кроме тайм-аутов или сообщений «keep alive» / ping между конечными точками, чтобы узнать, что сокет закрыт.

Если вы хотите точно выяснить, что происходит, отключите wirehark и поищите FIN и RST (и отсутствие чего-либо). Если вы видите правильную последовательность FIN, когда ваш сокет закрыт, значит, проблема в вашем коде. если вы видите RST, он может быть пойман исключениями, а если вы ничего не видите, вам нужно будет разработать способ в своем протоколе «пинговать» каждую сторону соединения, чтобы убедиться, что они все еще живы, или установить достаточно короткий тайм-аут для дополнительных данных.

person SoapBox    schedule 05.02.2010
comment
Честно говоря, ваш код слишком длинный, и у меня сейчас нет времени на него тратить. - Это несправедливо, ИМХО, вводить потенциально полезный ответ. В вопросе нет кода (возможно, проблема сама по себе), но вопрос действительно требует такого уровня детализации. Почему бы просто не сказать, что у вас есть предположение, догадка, гипотеза? - person JXG; 08.02.2010
comment
Он ссылается на файл .cpp, который содержит более 1400 строк. Извините, но об этом не может быть и речи. Я бегло посмотрел на него, и я даже не смог найти рассматриваемую область (единственные варианты выбора предназначены для установки начального соединения), что заставило меня поверить, что это неправильный файл. - person SoapBox; 08.02.2010
comment
Ссылка имела отношение к упомянутой мной функции CreateConnectedSocketPair (), а не к циклу событий (который, как вы говорите, находится в другом файле) - person Jeremy Friesner; 10.02.2010

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

Вы сказали: «Вы также не можете выполнять неблокирующее чтение на STDIN_FILE_HANDLE. Это означает, что нет возможности читать стандартный ввод из основного потока, поскольку ReadFile () может блокироваться бесконечно», но это еще не все. Посмотрите на ReadConsoleInput, WSAEventSelect и WaitForMultipleObjects. Дескриптор stdin будет сигнализирован только тогда, когда есть ввод, и ReadConsoleInput вернется немедленно (почти такая же идея, как select () в Unix).

Или используйте ReadFileEx и WaitForMultipleObjectsEx, чтобы чтение консоли запускалось с APC (что не так уж и асинхронно, оно выполняется в основном потоке и только во время WaitForMultipleObjectsEx или другой явной функции ожидания).

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

Или, может быть, ваша проблема в другом. В документах select говорится: «Для сокетов, ориентированных на соединение, удобочитаемость также может указывать на то, что запрос на закрытие сокета был получен от однорангового узла». Обычно вы отправляете этот «запрос на закрытие сокета», вызывая shutdown (), а не closesocket ().

person Ben Voigt    schedule 05.02.2010
comment
Что ж, я потратил несколько часов, пытаясь заставить ReadFileEx () выполнять неблокирующее чтение на stdin ... без кубиков. ReadFileEx () по-прежнему будет блокироваться на стандартном вводе, даже если я настрою все элементы перекрывающегося ввода-вывода, которые должны обеспечивать неблокирующее поведение. Я не пробовал ReadConsoleInput (), так что, возможно, это сработает. В любом случае цель API - позволить основному потоку использовать один и тот же цикл событий на основе select () и работать одинаково во всех ОС ... поэтому принудительное использование цикла событий для использования WaitForMultipleObjectsEx () в Windows - это не так. т желательно. - person Jeremy Friesner; 10.02.2010
comment
Ну, вы просто не можете открыть CONIN $ или любую консоль для перекрывающегося ввода-вывода, в документации CreateFile говорится, что все флаги игнорируются при доступе к консоли. В любом случае select действительно ужасный API. В unix вы обычно хотите использовать опрос, семантика которого почти идентична WaitForMultipleObjectsEx. Кроме того, я только что закончил писать кроссплатформенный код для этого, и это неплохо (на самом деле версии select и poll работают годами, но их можно прервать только в * nix, а не в Windows, поэтому я только что добавил вариант WaitForMultipleObjectsEx). - person Ben Voigt; 11.02.2010