LinuxForum.Fans
Тема

Потоки, usleep(), accept() и режим отладки. Не пойму что происходит.

Автор: Merzley 10 сообщений 3138 просмотров
#321632

Доброго времени суток.
Возникла не понятная мне ситуация. Суть вот в чем:
1. В первом потоке настраивается сокет на прием подключений.
2. Создается новый поток, в котором вызывается функция accept() для этого сокета. Второй поток завершается по выходу из accept().
3. В первом потоке, после создания второго, закрываем сокет и ждем завершения второго потока.
В таком варианте всё работает отлично. Если во втором потоке в нескольких местах вызвать usleep(), то тоже всё работает. Но стоит вызвать usleep() в первом потоке после создания второго, но перед закрытием сокета - второй поток не выходит из функции accept() после закрытия сокета.
Ладно, я мог бы подумать, что в работающем случае сокет закрывается до вызова accept() (да, по большей части так и происходит) и accept() не завершается при закрытии сокета, но есть нюанс.
Если не работающий вариант пошагово под отладкой пройти - всё работает. Более того если прокрутить всё без отладчика до момента, когда уже первый поток ушел в ожидание завершения второго, а второй никак не может вылезти из accept() (т.е. до момента, когда всё повесилось), а потом в любом месте поставить breakpoint - второй поток тут же вываливается из accept() и всё нормально завершается. Вот что происходит?

Да, еще:
1. Под виндой в той же ситуации всё работает абсолютно без проблем.
2. Отлаживал не чистым gdb, а из под Eclipse.

Собственно, сам код:

text

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define Sleep(a) (usleep(a*1000)) //Далее еще куча дефайнов для совместимости с виндой

int InitServerSocket(SOCKET *serverSocket){ //Здесь сокет настраивается для приема подключений };

void *ThreadPro(void *arg){ SOCKADDR_IN clientAddr; SOCKADDR_LEN clientAddrLen; SOCKET serverSocket; SOCKET clientSocket; serverSocket = *(SOCKET *)arg; clientAddrLen = sizeof(clientAddr);

printf("1\n"); while ((clientSocket = accept(serverSocket, (SOCKADDR *)&clientAddr, &clientAddrLen)) != INVALID_SOCKET){ printf("WTF?!\n"); } printf("2\n"); Sleep(5000); return NULL; }

int main(void) { SOCKET serverSocket; pthread_t thread;

InitServerSocket(&serverSocket);

pthread_create(&thread, NULL, ThreadPro, &serverSocket); printf("3\n"); Sleep(5000); closesocket(serverSocket); printf("4\n"); pthread_join(thread, NULL);

return EXIT_SUCCESS; }

#321642

UPD:
Если usleep() в первом потоке заменить на

text

for (i = 0; i < 3000000000; i++){

}

ничего не меняется. Второй поток всё так же не желает вылезать из accept(). Т.е дело не в засыпании потока.
Может я чего-то не понимаю и нельзя пользоваться одним и тем же дескриптором из разных потоков? Но тогда почему второй поток всё-таки вываливается из accept() при установке breakpoint? Чего такого сверхъестественного происходит в этом случае?

#321647

UPD2:
Провел еще один эксперимент.
Если цикл обработки входящих подключений вот так изменить

text

while ((clientSocket = accept(serverSocket, (SOCKADDR *)&clientAddr, &clientAddrLen)) != INVALID_SOCKET){
    Sleep(5000);
    closesocket(cleintSocket);
}

и после этого успеть, скажем телнетом, подключиться к нашему серверу, пока не закрыт слушающий сокет, то всё работает. Т.е. получается такая картина:
1. Второй поток уходит в accept()
2. Первый поток уходит в usleep()
3. Второй поток просыпается от входящего подключения и тут же засыпает в usleep()
4. Первый поток просыпается, закрывает слушающий сокет и засыпает в ожидании завершения второго потока.
5. Второй поток просыпается, пытается залезть в accept(), но сокет уже закрыт и поток завершается.
6. Первый поток завершается.

Проанализировав этот и предыдущий варианты пришел к выводу, что accept() не прерывается при закрытии сокета. Но ведь должен, вроде как. Да и что за история с BREAKPOINTами?

#321648

полный код выложил бы — было бы проще посмотреть, где-что не так... пропущенные куски, конечно, очевидны, но дописывать их лень...

"Merzley⇓" написал:
Если не работающий вариант пошагово под отладкой пройти - всё работает.

ты переключался на второй поток и докручивал его до accept() ?

"Merzley⇓" написал:
Может я чего-то не понимаю и нельзя пользоваться одним и тем же дескриптором из разных потоков?

насчёт «однозначно нельзя» утверждать побоюсь, но это очень плохая идея...
классическое решение: создать внутренний сокет/трубу/очередь сообщений/любое IPC по вкусу и poll'ить его вместе с сокетом... и, соответственно, нормально завершать всё

"Merzley⇓" написал:
Но тогда почему второй поток всё-таки вываливается из accept() при установке breakpoint?

пока не уверен, что он это делает... но даже если и так, то это вполне может быть такой эффект от отладки...

"Merzley⇓" написал:
Проанализировав этот и предыдущий варианты пришел к выводу, что accept() не прерывается при закрытии сокета. Но ведь должен, вроде как.

с чего? не видел этого в доках => «defined by implementation»

ЗЫ: assert(INVALID_SOCKET == -1);

#321654
"Fat-Zer⇓" написал:
ЗЫ: assert(INVALID_SOCKET == -1);

#define INVALID_SOCKET = -1 (см. код ниже)

"Fat-Zer⇓" написал:
ты переключался на второй поток и докручивал его до accept() ?

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

"Fat-Zer⇓" написал:
классическое решение: создать внутренний сокет/трубу/очередь сообщений/любое IPC по вкусу и poll'ить его вместе с сокетом... и, соответственно, нормально завершать всё

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

"Fat-Zer⇓" написал:
полный код выложил бы — было бы проще посмотреть, где-что не так... пропущенные куски, конечно, очевидны, но дописывать их лень...

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

text

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define SOCKET int #define SOCKADDR_IN struct sockaddr_in #define SOCKADDR struct sockaddr #define SOCKADDR_LEN socklen_t

#define INVALID_SOCKET -1 #define SOCKET_ERROR -1

#define ACCEPTOR_INIT_SUCCESS 0x00 #define ACCEPTOR_INIT_E_SOCKET 0x01 #define ACCEPTOR_INIT_E_BIND 0x02 #define ACCEPTOR_INIT_E_LISTEN 0x03

#define closesocket(a) (close(a)) #define Sleep(a) (usleep(a*1000))

#define BACKLOG 50 #define SERVER_PORT 23456

SOCKET serverSocket;

int InitServerSocket(SOCKET *serverSocket){ SOCKADDR_IN serverAddr; int tmpResult;

*serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (*serverSocket == INVALID_SOCKET){ return ACCEPTOR_INIT_E_SOCKET; }

serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVER_PORT); serverAddr.sin_addr.s_addr = 0;

tmpResult = bind(*serverSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)); if (tmpResult == SOCKET_ERROR){ closesocket(*serverSocket); return ACCEPTOR_INIT_E_BIND; }

tmpResult = listen(*serverSocket, BACKLOG); if (tmpResult == SOCKET_ERROR){ closesocket(*serverSocket); return ACCEPTOR_INIT_E_LISTEN; }

return ACCEPTOR_INIT_SUCCESS; };

void *ThreadPro(void *arg){ if (arg != NULL) return NULL;

SOCKADDR_IN clientAddr; SOCKADDR_LEN clientAddrLen; SOCKET clientSocket;

InitServerSocket(&serverSocket);

clientAddrLen = sizeof(clientAddr);

printf("1\n"); while ((clientSocket = accept(serverSocket, (SOCKADDR *)&clientAddr, &clientAddrLen)) != INVALID_SOCKET){ printf("5\n"); /*Sleep(10000); closesocket(clientSocket);*/ } Sleep(5000); printf("2\n"); return NULL; }

int main(void) { pthread_t thread;

pthread_create(&thread, NULL, ThreadPro, NULL); printf("3\n"); Sleep(5000); closesocket(serverSocket); printf("4\n"); pthread_join(thread, NULL);

return EXIT_SUCCESS; }

#321656
"Merzley⇓" написал:
Это понятно, если сокет активно используется разными потоками. В том что я ваяю подобное реализовано для совместного использования дескриптора лога (в винде нет syslog, а нужна кросплатформенность). Но данный конкретно сокет будет использоваться только в потоке опроса очереди подключений, а в других потоках может только закрываться (как раз это и тестировал, когда наткнулся на эту "непонятность"). Но закрытие сокета синхронизируется спинлоком и флаговой переменной. В данном случае что-то большее ваять не вижу смысла.

не особо понял суть проблему... ну сделай абстрактный интерфейс для логера... под никсом можно в сислог пустить или ещё куда душа ожелает... под оффтопиком - тоже куда угодно...

#321657
"Fat-Zer⇓" написал:
не особо понял суть проблему...

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

Далее, чего еще накопал:
1. shutdown тоже не работает. В принципе оно и ожидалось, но мало ли.
2. Если после закрытия слушающего сокета, когда второй поток висит в accept(), подключиться телнетом к серверу, то подключение проходит и висит до завершения программы, если его, конечно, не убить. Вот это уже как-то ну совсем не понятно. Сокет закрыт, а подключение по нему проходит. Да, accept() после этого не запускается, поток корректно завершается.

#321661
"Fat-Zer⇓" написал:
с чего? не видел этого в доках => «defined by implementation»

Об этом я, конечно, не думал. Считал, что закрытие сокета по определению прерывает и прослушивание. По крайней мере в винде это работает. Сейчас подробнее перечитал ман по accept(). Да, там прямо не сказано, что закрытие сокета прерывает функцию, но там есть возвращаемая ошибка ECONNABORTED. По идее она и должна возвращаться при закрытии сокета. Или помимо закрытия сокета нужны еще какие-то условия?

#321679
"Merzley⇓" написал:
1. shutdown тоже не работает. В принципе оно и ожидалось, но мало ли.

гмм... а у меня заработало...
[console]alexander@goblin /tmp $ wgetpaste thr_soc.c
Your paste can be seen here: http://bpaste.net/show/101748/
alexander@goblin /tmp $ gcc -lpthread thr_soc.c
alexander@goblin /tmp $ ./a.out
new thread started
starting accept() loop
socket is closed
exited loop with error 22: Invalid argument
exiting thread[/console]

#321681
"Fat-Zer⇓" написал:
гмм... а у меня заработало...

Согласен, мой косяк. Я вчера запускал shutdown() с параметром SHUT_WR, приняв его за Write/Read.

Вот только интересно, чего такого делает shutdown(), чего не делает close()?

В любом случае спасибо большое за помощь. Очень признателен.

Войдите или зарегистрируйтесь, чтобы ответить.