Подтвердить что ты не робот

Close() не закрывает гнездо правильно

У меня есть многопоточный сервер (пул потоков), который обрабатывает большое количество запросов (до 500/сек для одного node), используя 20 потоков. Там есть поток прослушивателя, который принимает входящие соединения и ставит их в очередь для обрабатываемых потоков обработчиков. После того, как ответ готов, потоки затем выписываются клиенту и закрывают сокет. Кажется, все было хорошо до недавнего времени, тестовая клиентская программа начала свисать случайно после прочтения ответа. После многократного копания кажется, что close() с сервера фактически не отключает сокет. Я добавил некоторые отладочные отпечатки в код с номером дескриптора файла, и я получаю этот тип вывода.

Processing request for 21
Writing to 21
Closing 21

Возвращаемое значение close() равно 0, или будет напечатан другой отладочный оператор. После этого вывода с зависающим клиентом lsof показывает установленное соединение.

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

КЛИЕНТ 17747 root 12u IPv4 32754228 TCP localhost: 47530- > localhost: 9980 (ESTABLISHED)

Как будто сервер никогда не отправляет последовательность завершения клиенту, и это состояние зависает до тех пор, пока клиент не будет убит, оставив серверную сторону в состоянии ожидания

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

Кроме того, если клиент имеет указанный тайм-аут, он будет тайм-аут вместо того, чтобы висит. Я также могу запустить вручную

call close(21)

на сервере из gdb, а затем клиент отключится. Это случается, возможно, когда-то в 50 000 запросов, но может не произойти в течение длительных периодов.

Версия для Linux: 2.6.21.7-2.fc8xen Версия Centos: 5.4 (Final)

действия сокета следующие

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  */
}

Затем поток поднимает сокет и формирует ответ.

/*  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 и 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);
  }

КЛИЕНТ:

Клиентская сторона использует 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);

Ничего необычного, просто базовое соединение. Клиент зависает в tranfer.c(в libcurl), потому что сокет не воспринимается как закрытый. Он ждет больше данных с сервера.

Вещи, которые я пробовал до сих пор:

Отключение до закрытия

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

Настройка SO_LINGER для принудительного закрытия через 1 секунду

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

Это не имело никакого значения. Любые идеи были бы с благодарностью.

EDIT. Это оказалось проблемой безопасности потоков внутри библиотеки очередей, в результате чего сокет обрабатывался ненадлежащим образом несколькими потоками.

4b9b3361

Ответ 1

Вот код, который я использовал во многих Unix-подобных системах (например, SunOS 4, SGI IRIX, HPUX 10.20, CentOS 5, Cygwin), чтобы закрыть сокет:

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");
   }
}

Но вышеописанное не гарантирует отправку буферизованных записей.

Изящное закрытие: мне потребовалось около 10 лет, чтобы выяснить, как закрыть сокет. Но еще 10 лет я лениво звонил usleep(20000) за небольшую задержку, чтобы "обеспечить", чтобы буфер записи был сброшен до закрытия. Это явно не очень умно, потому что:

  • Задержка была слишком длинной большую часть времени.
  • Задержка была слишком коротка в течение некоторого времени - возможно!
  • Сигнал, который может возникнуть у SIGCHLD, может закончиться usleep() (но я обычно называл usleep() дважды для обработки этого случая - взломать).
  • Не было никаких указаний, работает ли это. Но это, возможно, не важно, если: a) жесткие сбрасывания в порядке и/или b) у вас есть контроль над обеими сторонами ссылки.

Но делать правильный флеш удивительно сложно. Использование SO_LINGER, по-видимому, не путь; см., например:

И SIOCOUTQ представляется специфичным для Linux.

Примечание shutdown(fd, SHUT_WR) не прекращает писать, вопреки его имени, и, возможно, противоречит man 2 shutdown.

Этот код flushSocketBeforeClose() ждет, пока не начнется чтение нулевых байтов или пока истечет таймер. Функция haveInput() является простой оболочкой для select (2) и устанавливается на блокировку до 1/100th секунды.

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;
}

Пример использования:

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

В приведенном выше примере my getWallTimeEpoch() похож на time(),, а Perror() является оберткой для perror().

Изменить: Некоторые комментарии:

  • Мое первое признание немного смущает. OP и Nemo оспаривали необходимость очистки внутреннего so_error до закрытия, но теперь я не могу найти ссылку на это. Эта система была HPUX 10.20. После неудачного connect() просто вызов close() не выпустил дескриптор файла, потому что система хотела выдавить мне выдающуюся ошибку. Но я, как и большинство людей, никогда не удосужился проверить возвращаемое значение close. Итак, у меня в конечном итоге закончились файловые дескрипторы (ulimit -n),, которые, наконец, привлекли мое внимание.

  • (очень незначительная точка). Один комментатор возражал против жестко заданных числовых аргументов shutdown(), а не, например, SHUT_WR для 1. Самый простой ответ - Windows использует разные # define/enums, например. SD_SEND. И многие другие авторы (например, Beej) используют константы, как и многие устаревшие системы.

  • Кроме того, я всегда, всегда, устанавливаю FD_CLOEXEC во всех своих сокетах, поскольку в моих приложениях я никогда не хочу, чтобы они передавались ребенку, и, что более важно, я не хочу, чтобы зависавший ребенок воздействовал на меня.

Пример кода для установки 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");
   }

Ответ 2

Отличный ответ от Джозефа Куинси. У меня есть комментарии к функции haveInput. Удивительно, насколько вероятно, что select возвращает fd, который вы не включили в свой набор. Это будет серьезная ошибка ОС IMHO. Это то, что я проверил бы, если бы я написал модульные тесты для функции select, а не в обычном приложении.

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

Мой другой комментарий относится к обработке EINTR. Теоретически вы могли бы застрять в бесконечном цикле, если select продолжал возвращать EINTR, так как эта ошибка позволяет начать цикл. Учитывая очень короткий тайм-аут (0,01), представляется маловероятным. Тем не менее, я думаю, что подходящим способом борьбы с этим было бы вернуть ошибки вызывающему абоненту (flushSocketBeforeClose). Вызывающий может продолжать вызов haveInput, если его время ожидания еще не истекло, и объявить отказ для других ошибок.

ДОБАВЛЕНИЕ № 1

flushSocketBeforeClose не будет быстро завершен, если read вернет ошибку. Он будет продолжать цикл до истечения таймаута. Вы не можете полагаться на select внутри haveInput, чтобы предвидеть все ошибки. read имеет собственные ошибки (ex: EIO).

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

Ответ 3

Это звучит для меня как ошибка в вашем дистрибутиве Linux.

Документация библиотеки GNU C говорит:

Когда вы закончите использовать сокет, вы можете просто закрыть его файл дескриптор с close

Ничего об очистке каких-либо флагов ошибки или ожидании сброса данных или какой-либо такой вещи.

Ваш код в порядке; ваш O/S имеет ошибку.