Вызов ioctl() с FIONREAD приводит к странным побочным эффектам в явном состоянии гонки,

Я пишу симулятор параллельной нейронной сети, и недавно я столкнулся с проблемой в своем коде, которая меня полностью смущает (при условии, что я всего лишь программист на С++ среднего уровня, так что, может быть, я упускаю что-то очевидное?),... Мой код включает в себя «сервер» и множество клиентов (воркеров), которые берут работу и возвращают результаты на сервер. Вот серверная часть:

#include <iostream>
#include <fstream>

#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>

#include <sys/ioctl.h>

void advanceToNextInputValue(std::ifstream &trainingData, char &nextCharacter)
   {

      nextCharacter = trainingData.peek();
      while(nextCharacter != EOF && !isdigit(nextCharacter))
         {
sleep(1);
            trainingData.get();
sleep(1);
            nextCharacter = trainingData.peek();
         }
   }

int main()
   {
      // Create a socket,...
      int listenerSocketNum = socket(AF_INET, SOCK_STREAM, 0);

      // Name the socket,...
      sockaddr_in socketAddress;
      socklen_t socketAddressLength = sizeof(socketAddress);

      inet_pton(AF_INET, "127.0.0.1", &(socketAddress.sin_addr));
      socketAddress.sin_port = htons(9988);
      bind(listenerSocketNum, reinterpret_cast<sockaddr*>(&socketAddress), socketAddressLength);

      // Create a connection queue for worker processes waiting to connect to this server,...
      listen(listenerSocketNum, SOMAXCONN);


      int epollInstance = epoll_create(3); // Expected # of file descriptors to monitor

      // Allocate a buffer to store epoll events returned from the network layer
      epoll_event* networkEvents = new epoll_event[3];

      // Add the server listener socket to the list of file descriptors monitored by epoll,...
      networkEvents[0].data.fd = -1; // A marker returned with the event for easy identification of which worker process event belongs to
      networkEvents[0].events = EPOLLIN | EPOLLET; // epoll-IN- since we only expect incoming data on this socket (ie: connection requests from workers),...
                                                   // epoll-ET- indicates an Edge Triggered watch
      epoll_ctl(epollInstance, EPOLL_CTL_ADD, listenerSocketNum, &networkEvents[0]);


      char nextCharacter = 'A';
      std::ifstream trainingData;

      // General multi-purpose/multi-use variables,...
      long double v;
      signed char w;
      int x = 0;
      int y;

      while(1)
         {
            y = epoll_wait(epollInstance, networkEvents, 3, -1); // the -1 tells epoll_wait to block indefinitely

            while(y > 0)
               {
                  if(networkEvents[y-1].data.fd == -1) // We have a notification on the listener socket indicating a request for a new connection (and we expect only one for this testcase),...
                     {
                        x = accept(listenerSocketNum,reinterpret_cast<sockaddr*>(&socketAddress), &socketAddressLength);

                        networkEvents[y-1].data.fd = x; // Here we are just being lazy and re-using networkEvents[y-1] temporarily,...
                        networkEvents[y-1].events = EPOLLIN | EPOLLET;

                        // Add the socket for the new worker to the list of file descriptors monitored,...
                        epoll_ctl(epollInstance, EPOLL_CTL_ADD, x, &networkEvents[y-1]);

                        trainingData.open("/tmp/trainingData.txt");
                     }
                  else if(networkEvents[y-1].data.fd == x) // Worker is waiting to receive datapoints for calibration,...
                     {
                        std::cout << "nextCharacter before call to ioctl: " << nextCharacter << std::endl;
                        ioctl(networkEvents[y-1].data.fd, FIONREAD, &w);
                        std::cout << "nextCharacter after call to ioctl: " << nextCharacter << std::endl;

                        recv(networkEvents[y-1].data.fd, &v, sizeof(v), MSG_DONTWAIT); // Retrieve and discard the 'tickle' from worker

                        if(nextCharacter != EOF)
                           {
                              trainingData >> v;

                              send(networkEvents[y-1].data.fd, &v, sizeof(v), MSG_DONTWAIT);
                              advanceToNextInputValue(trainingData, nextCharacter);
                           }
                     }

                  y--;
               }
         }

      close(epollInstance);
      return 0;
   }

А вот клиентская часть:

#include <arpa/inet.h>

int main()
   {
      int workerSocket = socket(AF_INET, SOCK_STREAM, 0);

      // Name the socket as agreed with the server:
      sockaddr_in serverSocketAddress;
      serverSocketAddress.sin_family = AF_INET;
      serverSocketAddress.sin_port = htons(9988);
      inet_pton(AF_INET, "127.0.0.1", &(serverSocketAddress.sin_addr));

      // Connect your socket to the server's socket:
      connect(workerSocket, reinterpret_cast<sockaddr*>(&serverSocketAddress), sizeof(serverSocketAddress));

      long double z;
      while(1)
         {
            send(workerSocket, &z, sizeof(z), MSG_DONTWAIT); // Send a dummy result/tickle to server,...
            recv(workerSocket, &z, sizeof(z), MSG_WAITALL);
         }
   }

Раздел кода, с которым у меня возникли проблемы, следующий (с сервера):

std::cout << "nextCharacter before call to ioctl: " << nextCharacter << std::endl;
ioctl(networkEvents[y-1].data.fd, FIONREAD, &w);
std::cout << "nextCharacter after call to ioctl: " << nextCharacter << std::endl;

Здесь (по крайней мере, в моей системе) при определенных обстоятельствах вызов ioctl фактически стирает значение «nextCharacter», и я не могу понять, как и почему!

Вот результаты, которые я ожидаю получить:

$ ./server.exe
nextCharacter before call to ioctl: A
nextCharacter after call to ioctl: A
nextCharacter before call to ioctl: 1
nextCharacter after call to ioctl: 1
nextCharacter before call to ioctl: 9
nextCharacter after call to ioctl: 9
nextCharacter before call to ioctl: 2
nextCharacter after call to ioctl: 2
nextCharacter before call to ioctl: 1
nextCharacter after call to ioctl: 1
nextCharacter before call to ioctl: 1
nextCharacter after call to ioctl: 1
nextCharacter before call to ioctl: 1
nextCharacter after call to ioctl: 1
nextCharacter before call to ioctl: 2
nextCharacter after call to ioctl: 2
nextCharacter before call to ioctl: ÿ
nextCharacter after call to ioctl: ÿ

(строчная буква «y» с умляутом является символом конца файла EOF)

И вот результаты, которые я получаю (обратите внимание, что мы получаем бесконечный цикл, потому что условие остановки зависит от значения nextCharacter, которое стирается, поэтому никогда не останавливается):

$ ./server.exe
nextCharacter before call to ioctl: A
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: 1
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: 9
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: 2
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: 1
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: 1
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: 1
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: 2
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: ÿ
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: ÿ
nextCharacter after call to ioctl:
nextCharacter before call to ioctl: ÿ
nextCharacter after call to ioctl:
.
.
.

Если я закомментирую любой из операторов сна в этом разделе (на сервере):

void advanceToNextInputValue(std::ifstream &trainingData, char &nextCharacter)
   {

      nextCharacter = trainingData.peek();
      while(nextCharacter != EOF && !isdigit(nextCharacter))
         {
sleep(1);
            trainingData.get();
sleep(1);
            nextCharacter = trainingData.peek();
         }
   }

Затем я получаю результаты, которые я ожидаю получить,...

Это makefile, который я использую:

$ cat Makefile
all: server client

server: server.cpp
        g++ server.cpp -o server.exe -ansi -fno-elide-constructors -O3 -pedantic-errors -Wall -Wextra -Winit-self -Wold-style-cast -Woverloaded-virtual -Wuninitialized -Winit-self

client: client.cpp
        g++ client.cpp -o client.exe -ansi -fno-elide-constructors -O3 -pedantic-errors -Wall -Wextra -Winit-self -Wold-style-cast -Woverloaded-virtual -Wuninitialized -Winit-self

С файлом trainingData.txt, который выглядит так:

$ cat trainingData.txt
15616.16993666375,15616.16993666375,9.28693983312753E20,24.99528974548316,16.91935342923897,16.91935342923897,1.386594632397968E6,2.567209162871251

Так я обнаружил новую ошибку или я просто глуп? :) Честно говоря, я не понимаю, почему вызов ioctl с FIONREAD, который должен сообщить мне, сколько байтов у меня есть в сокете, ожидающих чтения, должен каким-либо образом влиять на значение переменной 'nextCharacter',.. .

Обратите внимание, что это урезанная версия исходной программы, которая все еще воспроизводит проблему (по крайней мере, в моей системе), поэтому имейте в виду, что некоторые вещи могут не иметь смысла в приведенных выше фрагментах кода :)

Терри


person Terry    schedule 15.12.2014    source источник


Ответы (1)


От 1_:

FIONREAD int *

То есть FIONREAD ожидает указатель на целое число, но вы передаете указатель на signed char.

Решение: измените свой:

signed char w;

to

int w;

иначе вы будете страдать от неопределенного поведения.

Объяснение того, что вы видите, заключается в том, что, вероятно, компилятор помещает переменные w и nextCharacter вместе в память, и переполнение первого перезаписывает значение последнего.

person rodrigo    schedule 15.12.2014
comment
Вообще смысла звонить нет. После этого количество доступных байтов может быстро измениться. ОП следует просто читать до тех пор, пока не произойдет EAGAIN/EWOULDBLOCK. - person user207421; 15.12.2014
comment
@EJP: Да, я заметил, но вопрос конкретно о странном повреждении данных, а не о {не,}смысле кода. - person rodrigo; 15.12.2014
comment
@rodrigo: Вот и все! Большое спасибо! Очевидно, мне еще многому предстоит научиться! :) - person Terry; 16.12.2014
comment
@EJP: Да, в этом случае я не хотел читать, потому что не хотел буферизоваться (возвращаемые числа все равно будут выброшены на этом этапе калибровки). Я знал, что клиент отправляет 16 байтов, и если бы я прочитал что-то меньше 16, мне пришлось бы отслеживать, сколько еще байтов мне нужно было прочитать, прежде чем я мог бы продолжить следующий шаг,... просто казалось, что проще позвонить ioctl несколько раз, пока у меня не будет 16 байтов, а затем прочитаю все сразу в длинный двойной :) - person Terry; 16.12.2014