2012-10-04 15 views
21

Ho un server multi-thread (pool di thread) che gestisce un numero elevato di richieste (fino a 500/sec per un nodo), utilizzando 20 thread. C'è un thread listener che accetta le connessioni in entrata e le accoda per l'elaborazione dei thread del gestore. Una volta che la risposta è pronta, i thread scrivono al client e chiudono il socket. Tutto sembrava andare bene fino a poco tempo fa, un programma di test client iniziato appeso in modo casuale dopo aver letto la risposta. Dopo molte ricerche, sembra che il close() dal server non stia effettivamente disconnettendo il socket. Ho aggiunto alcune stampe di debug al codice con il numero del descrittore di file e ottengo questo tipo di output.close() non chiude correttamente il socket

Processing request for 21 
Writing to 21 
Closing 21 

Il valore di ritorno di close() è 0 o ci sarebbe un'altra istruzione di debug stampata. Dopo questo output con un client che si blocca, lsof sta mostrando una connessione stabilita.

SERVER 8160 radice 21U IPv4 32.754.237 TCP localhost: 9980-> localhost: 47530 (fondata)

CLIENTE 17747 radice 12u IPv4 32.754.228 TCP localhost: 47530-> localhost: 9980 (istituito)

È come se il server non invia mai la sequenza di spegnimento al client e si blocca fino a quando il client non viene ucciso, lasciando il lato server in uno stato di attesa chiuso

SERVER 8160 root 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (CLOSE_WAIT)

Anche se il client ha un timeout specificato, verrà sospeso anziché appeso. Posso anche eseguire manualmente

call close(21) 

nel server da gdb e il client si disconnetterà. Succede forse una volta su 50.000 richieste, ma potrebbe non accadere per periodi prolungati. Versione

Linux: 2.6.21.7-2.fc8xen Centos versione: 5.4 (finale)

azioni di socket sono i seguenti

SERVER:

int client_socket; struct sockaddr_in client_addr; socklen_t client_len = sizeof (client_addr);

while(true) { 
    client_socket = accept(incoming_socket, (struct sockaddr *)&client_addr, &client_len); 
    if (client_socket == -1) 
    continue; 
    /* insert into queue here for threads to process */ 
} 

Quindi il thread preleva lo zoccolo e genera la risposta.

/* get client_socket from queue */ 

/* processing request here */ 

/* now set to blocking for write; was previously set to non-blocking for reading */ 
int flags = fcntl(client_socket, F_GETFL); 
if (flags < 0) 
    abort(); 
if (fcntl(client_socket, F_SETFL, flags|O_NONBLOCK) < 0) 
    abort(); 

server_write(client_socket, response_buf, response_length); 
server_close(client_socket); 

server_write e server_close.

void server_write(int fd, char const *buf, ssize_t len) { 
    printf("Writing to %d\n", fd); 
    while(len > 0) { 
     ssize_t n = write(fd, buf, len); 
     if(n <= 0) 
     return;// I don't really care what error happened, we'll just drop the connection 
     len -= n; 
     buf += n; 
    } 
    } 

void server_close(int fd) { 
    for(uint32_t i=0; i<10; i++) { 
     int n = close(fd); 
     if(!n) {//closed successfully                                 
     return; 
     } 
     usleep(100); 
    } 
    printf("Close failed for %d\n", fd); 
    } 

CLIENTE:

lato client utilizza libcurl v 7.27.0

CURL *curl = curl_easy_init(); 
CURLcode res; 
curl_easy_setopt(curl, CURLOPT_URL, url); 
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); 
curl_easy_setopt(curl, CURLOPT_WRITEDATA, write_tag); 

res = curl_easy_perform(curl); 

Niente di speciale, solo un collegamento ricciolo di base. Il client si blocca in tranfer.c (in libcurl) perché il socket non è percepito come chiuso. Sta aspettando più dati dal server.

Le cose che ho provato finora:

Shutdown prima della chiusura

shutdown(fd, SHUT_WR);                                    
char buf[64];                                      
while(read(fd, buf, 64) > 0);                                   
/* then close */ 

Impostazione SO_LINGER a chiudere forzatamente in 1 secondo

struct linger l; 
l.l_onoff = 1; 
l.l_linger = 1; 
if (setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) 
    abort(); 

Questi hanno fatto alcuna differenza. Qualsiasi idea sarebbe molto apprezzata.

MODIFICA - Questo è risultato essere un problema di sicurezza dei thread all'interno di una libreria di code che ha causato il socket in modo inappropriato da più thread.

+0

Sei positivo al 100% nessun altro thread potrebbe utilizzare il socket quando si chiama 'close' su di esso? Come fai le tue letture non bloccanti? –

+0

Temo di aver appena effettuato l'accesso qui e di aver ricordato questo problema. Ho scoperto in seguito che c'era un problema di sicurezza del thread in una coda usata per passare le connessioni in giro. Non c'era nessun bug qui. Scusa per la disinformazione. – DavidMFrey

risposta

54

Ecco il codice che ho usato su molti sistemi Unix-like (ad esempio SunOS 4, SGI IRIX, HP-UX 10.20, CentOS 5, Cygwin) per chiudere una presa di corrente:

int getSO_ERROR(int fd) { 
    int err = 1; 
    socklen_t len = sizeof err; 
    if (-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &len)) 
     FatalError("getSO_ERROR"); 
    if (err) 
     errno = err;    // set errno to the socket SO_ERROR 
    return err; 
} 

void closeSocket(int fd) {  // *not* the Windows closesocket() 
    if (fd >= 0) { 
     getSO_ERROR(fd); // first clear any errors, which can cause close to fail 
     if (shutdown(fd, SHUT_RDWR) < 0) // secondly, terminate the 'reliable' delivery 
     if (errno != ENOTCONN && errno != EINVAL) // SGI causes EINVAL 
      Perror("shutdown"); 
     if (close(fd) < 0) // finally call close() 
     Perror("close"); 
    } 
} 

Ma quanto sopra fa non garantisce che vengano inviate scritture bufferizzate.

Graziosa chiusura: mi ci sono voluti circa 10 anni per capire come chiudere una presa. Ma per altri 10 anni ho semplicemente pigramente chiamato usleep(20000) per un leggero ritardo per "assicurarsi" che il buffer di scrittura sia stato svuotato prima della chiusura. Questo ovviamente non è molto intelligente, perché:

  • Il ritardo era troppo lungo il più delle volte.
  • Il ritardo era troppo breve per un po 'di tempo - forse!
  • Un segnale come SIGCHLD potrebbe verificarsi per terminare usleep() (ma di solito ho chiamato lo usleep() due volte per gestire questo caso: un hack).
  • Non c'era alcuna indicazione se questo funziona. Ma questo forse non è importante se a) hard resets sono perfettamente ok, e/o b) hai il controllo su entrambi i lati del link.

Ma fare un flush adeguato è sorprendentemente difficile. Usare SO_LINGER è apparentemente non la strada da percorrere; Si veda ad esempio:

E SIOCOUTQ sembra essere-Linux specifica.

Nota shutdown(fd, SHUT_WR)non scrittura arresto, contrariamente al suo nome, e magari in contrasto con man 2 shutdown.

Questo codice flushSocketBeforeClose() attende fino a una lettura di zero byte o fino alla scadenza del timer. La funzione haveInput() è un semplice wrapper per select (2) ed è impostata su block per un massimo di 1/100 di secondo.

bool haveInput(int fd, double timeout) { 
    int status; 
    fd_set fds; 
    struct timeval tv; 
    FD_ZERO(&fds); 
    FD_SET(fd, &fds); 
    tv.tv_sec = (long)timeout; // cast needed for C++ 
    tv.tv_usec = (long)((timeout - tv.tv_sec) * 1000000); // 'suseconds_t' 

    while (1) { 
     if (!(status = select(fd + 1, &fds, 0, 0, &tv))) 
     return FALSE; 
     else if (status > 0 && FD_ISSET(fd, &fds)) 
     return TRUE; 
     else if (status > 0) 
     FatalError("I am confused"); 
     else if (errno != EINTR) 
     FatalError("select"); // tbd EBADF: man page "an error has occurred" 
    } 
} 

bool flushSocketBeforeClose(int fd, double timeout) { 
    const double start = getWallTimeEpoch(); 
    char discard[99]; 
    ASSERT(SHUT_WR == 1); 
    if (shutdown(fd, 1) != -1) 
     while (getWallTimeEpoch() < start + timeout) 
     while (haveInput(fd, 0.01)) // can block for 0.01 secs 
      if (!read(fd, discard, sizeof discard)) 
       return TRUE; // success! 
    return FALSE; 
} 

Esempio di utilizzo:

if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s 
     printf("Warning: Cannot gracefully close socket\n"); 
    closeSocket(fd); 

In quanto sopra, il mio getWallTimeEpoch() è simile a time(), e Perror() è un wrapper per perror().

Edit: Alcuni commenti:

  • La mia prima ammissione è un po 'imbarazzante. L'OP e Nemo hanno sfidato la necessità di svuotare lo spazio interno so_error prima di chiuderlo, ma ora non riesco a trovare alcun riferimento. Il sistema in questione era HPUX 10.20. Dopo un errore connect(), chiamare semplicemente close() non ha rilasciato il descrittore di file, perché il sistema desiderava fornire un errore in sospeso. Ma io, come la maggior parte delle persone, non mi sono mai preso la briga di controllare il valore di ritorno di close. Quindi alla fine ho esaurito i descrittori di file (ulimit -n), che finalmente hanno attirato la mia attenzione.

  • (punto molto minore) Un commentatore ha obiettato sugli argomenti numerici codificati su shutdown(), anziché ad es. SHUT_WR per 1. La risposta più semplice è che Windows usa diversi # define/enums, ad es. SD_SEND. E molti altri scrittori (ad esempio Beej) usano le costanti, come fanno molti sistemi legacy.

  • Inoltre, ho sempre, sempre, impostato FD_CLOEXEC su tutte le mie prese, poiché nelle mie applicazioni non voglio mai che siano passate a un bambino e, cosa più importante, non voglio che un bambino appeso mi impatti.

codice di esempio per impostare CLOEXEC:

static void setFD_CLOEXEC(int fd) { 
     int status = fcntl(fd, F_GETFD, 0); 
     if (status >= 0) 
     status = fcntl(fd, F_SETFD, status | FD_CLOEXEC); 
     if (status < 0) 
     Perror("Error getting/setting socket FD_CLOEXEC flags"); 
    } 
+5

Vorrei poterlo votare due volte. Questo è solo il secondo esempio di una presa correttamente chiusa che ho visto in natura. – grieve

+1

+1 per 'getsockopt()' ing 'SO_ERROR'. – alk

+0

@JosephQuinsey - Hai un riferimento per "errori ... causerà la chiusura di close()"? Preferibilmente dalle specifiche POSIX? – Nemo

0

Questo suona per me come un insetto nella vostra distribuzione Linux.

Il GNU C library documentation dice:

Quando avete finito di usare una presa, si può semplicemente chiudere il suo descrittore di file con close

Niente di eliminare eventuali flag di errore o in attesa per i dati essere arrossato o qualsiasi cosa del genere.

Il tuo codice è a posto; il tuo O/S ha un bug.

+0

Appoggiato a questa risposta. Ci vorrà del lavoro per far testare un altro sistema operativo. Lo rivisiterò una volta che l'avrò testato. Voglio aggiungere questo link da @Nemo in quanto sembra pertinente alla domanda. e la risposta a cui è stato allegato è stata cancellata. https://sites.google.com/site/michaelsafyan/software-engineering/checkforeintrwheninvokingclosethinkagain – DavidMFrey

+0

Accetto questa risposta, poiché la modifica della coda thread-safe per utilizzare semafori anziché condizioni pthread ha inspiegabilmente (per me stesso) risolto il problema problema. – DavidMFrey

+3

'Niente su come cancellare i flag di errore o attendere che i dati vengano svuotati o qualcosa del genere. Probabilmente," in attesa che i dati vengano scaricati "rientra in" quando hai finito di usare un socket ". –

2

Ottima risposta di Joseph Quinsey. Ho commenti sulla funzione haveInput. Ti chiedi quanto sia probabile che la selezione restituisca un file che non hai incluso nel tuo set. Questo sarebbe un errore del sistema operativo principale IMHO. Questo è il tipo di cosa che controllerei se avessi scritto i test unitari per la funzione select, non in un'app normale.

if (!(status = select(fd + 1, &fds, 0, 0, &tv))) 
    return FALSE; 
else if (status > 0 && FD_ISSET(fd, &fds)) 
    return TRUE; 
else if (status > 0) 
    FatalError("I am confused"); // <--- fd unknown to function 

Il mio altro commento riguarda la gestione di EINTR. In teoria, potresti rimanere bloccato in un ciclo infinito se select continua a restituire EINTR, poiché questo errore consente l'avvio del ciclo. Dato il timeout molto breve (0,01), sembra altamente improbabile che accada. Tuttavia, ritengo che il modo appropriato per gestirlo sia restituire errori al chiamante (flushSocketBeforeClose).Il chiamante può continuare a chiamare haveInput a patto che il timeout non sia scaduto e dichiarare l'errore per altri errori.

OLTRE # 1

flushSocketBeforeClose non uscirà rapidamente in caso di read restituire un errore. Continuerà il ciclo fino alla scadenza del timeout. Non puoi fare affidamento sul select all'interno di haveInput per anticipare tutti gli errori. read ha errori propri (es: EIO).

 while (haveInput(fd, 0.01)) 
     if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop 
      return TRUE; 
Problemi correlati