Сокет домена Unix и переполнение буфера записи

Я использую сокеты домена Unix в одном из своих приложений и наблюдаю очень странное поведение при записи в сокет. Краткое описание: есть 2 приложения - клиентское и серверное, где каждое одновременно читает и пишет, т.е. сокет работает в дуплексном режиме. Чтобы предотвратить переполнение, я проверяю буфер записи:

ioctl(socket_handler, TIOCOUTQ, &pending)

Но иногда результат у меня получается очень странный - 768, 1536, 2304 и т.д. С одной стороны, это число не похоже на количество байт, которое я пишу, с другой стороны выглядит немного подозрительно - 0x300, 0x600 и т.д, в Другими словами, это выглядит как флаг или код ошибки. В документации об этом ничего не сказано.

Чтобы быть последовательным и избежать вопросов, я привожу полный пример кода, чтобы воспроизвести проблему:

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/un.h>
#include <linux/sockios.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <pthread.h>
#include <poll.h>

static int sock;
static socklen_t len;
static struct sockaddr_un addr;
static std::string path = "@testsock";
static pthread_t thread;
static bool running;
static pollfd polls[2] = {};
static char buf[200];
static int err;

bool InitSocket()
{
    sock = socket(AF_UNIX, SOCK_STREAM,0);
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, path.c_str(), path.size());
    *addr.sun_path = '\0';
    len = static_cast<socklen_t>(__builtin_offsetof(struct sockaddr_un, sun_path) + path.length());
    int opt_value = 1;
    err = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, static_cast<void *>(&opt_value), sizeof(int));
    int on = 1;
    err = ioctl(sock, FIONBIO, &on);
    return true;
}

bool InitServer()
{
    polls[0].fd = sock;
    polls[0].events = POLLIN;

    err = bind(sock, reinterpret_cast<struct sockaddr *>(&addr), len);
    err = listen(sock, 10);
    return true;
}

bool InitClient()
{
    polls[1].fd = sock;
    polls[1].events = POLLIN;

    err = connect(sock, reinterpret_cast<struct sockaddr *>(&addr), len);
    return true;
}

uint32_t GetRand(uint32_t min, uint32_t max)
{
    return static_cast<uint32_t>(rand()) % (max - min) + min;
}
void *ReadThread(void*)
{
    while(running)
    {
        int status = poll(polls, 2, 1000);
        if(status > 0)
        {
            for(int i = 0;i < 2;i ++)
            {
                if(polls[i].revents == 0)
                    continue;

                if(i == 0)
                {
                    struct sockaddr_un cli_addr;
                    socklen_t clilen = sizeof(cli_addr);
                    int new_socket = accept(sock, reinterpret_cast<struct sockaddr *>(&cli_addr), &clilen);
                    int on = 1;
                    ioctl(new_socket, FIONBIO, &on);
                    polls[1].fd = new_socket;
                    polls[1].events = POLLIN;
                    std::cout << "client connected" << std::endl;
                }
                else
                {
                    ssize_t readBytes = read(polls[1].fd, &buf, 100);
                    if(readBytes > 0)
                    {
                        //std::string str(buf, readBytes);
                        std::cout << "         read " << readBytes << " bytes: " << std::endl;
                        int p = GetRand(10, 100); //
                        usleep(p * 1000);         // removing these lines solves the problem, but ... 
                    }
                    else
                    {
                        std::cout << "read error" << std::endl;
                        running = false;
                    }
                }
            }
        }
    }
    return nullptr;
}



int main(int argc, char *argv[])
{       
    if(argc < 2)
        return 1;

    srand(static_cast<unsigned int>(time(nullptr)));
    InitSocket();
    std::string app_type(argv[1]);
    if(app_type == "-s")
        InitServer();
    else if(app_type == "-c")
        InitClient();
    else
        return 1;

    running = true;
    pthread_create(&thread, nullptr, &ReadThread, nullptr);

    if(app_type == "-s")
    {
        while(polls[1].fd == 0) usleep(100);
    }

    char sendbuf[100];
    for(int i = 0;i < 100;i ++)
    {
        int length = GetRand(10, 100);
        int delay = GetRand(1, 10);
        for(int j = 0;j < length;j ++)
        {
            sendbuf[j] = GetRand('a', 'z');
        }

        unsigned long pending = 0;
        err = ioctl(polls[1].fd, TIOCOUTQ, &pending);
        std::cout << "pending " << pending << std::endl;

        err = send(polls[1].fd, sendbuf, length, MSG_NOSIGNAL);
        std::cout << "write " << length << " bytes" << std::endl;
        usleep(delay * 1000);
    }

    running = false;
    pthread_join(thread, nullptr);

    if(app_type == "-s")
        close(polls[0].fd);
    close(polls[1].fd);

    return 0;
}

запуск кода: сервер:

test_app -s 

а затем клиентское приложение (в другом окне консоли):

test_app -c 

Возможный результат:

Сервер:

client connected
         read 83 bytes: 
pending 0
write 66 bytes
pending 0
write 18 bytes
pending 768          <----- ????
write 17 bytes
pending 1536         <----- ????
write 79 bytes
         read 100 bytes: 
pending 2304         <----- ????
write 51 bytes
pending 3072
write 25 bytes
         read 100 bytes: 
pending 2304
write 96 bytes
pending 3072
write 19 bytes

Клиент:

pending 0
write 83 bytes
         read 66 bytes: 
pending 0
write 19 bytes
pending 768
write 94 bytes
pending 1536
write 52 bytes
pending 2304
write 11 bytes
pending 3072
write 39 bytes
pending 3072
write 35 bytes
         read 100 bytes: 
pending 3840
write 20 bytes
pending 2304
write 92 bytes
pending 3072
write 53 bytes
pending 3840
write 88 bytes
pending 4608
write 84 bytes
         read 100 bytes: 

Как я заметил, эта проблема возникает после добавления искусственной задержки в потоке чтения. В моем реальном приложении я обрабатываю сообщение, и это создает небольшую задержку. Но я совершенно не понимаю, откуда берутся эти цифры (ожидающие) и как на это влияет задержка в потоке чтения?

Как я могу этого избежать? Как мне получить настоящий буфер записи, а не эти странные цифры?


person folibis    schedule 16.12.2020    source источник
comment
Я вижу только один IOCTL, задокументированный на справочной странице unix(7) (SIOCINQ). Это говорит о том, что другие IOCTL либо не поддерживаются, либо определяются реализацией, а это означает, что все может случиться.   -  person rveerd    schedule 16.12.2020
comment
По какой причине вам нужно проверить размер буфера записи? Разве операции над сокетом не будут просто блокироваться, если внутренний буфер заполнен?   -  person rveerd    schedule 16.12.2020
comment
@rveerd Я хочу предотвратить переполнение буфера записи   -  person folibis    schedule 16.12.2020
comment
@rveerd, SIOCINQ дает тот же результат. На самом деле этот вызов для входного буфера, мне нужен выходной.   -  person folibis    schedule 16.12.2020
comment
Почему вам нужно предотвращать переполнение буфера? Операция записи не просто блокируется? Или вернуть ошибку, и вы можете попробовать позже?   -  person rveerd    schedule 16.12.2020
comment
Настоящая цель — небольшая коробка ARM Linux, на которой работает сервер, который взаимодействует с десятками клиентов, используя сокет домена Unix. Сокеты Unix надежны, и если сервер пишет клиенту, а клиент не читает его, буфер переполняется. Поэтому, чтобы обеспечить безопасную работу, я хочу предотвратить это на сервере. Из моих тестов я знаю, что буфер может достигать 108 КБ, а затем он падает. Итак, моя идея состоит в том, чтобы прочитать размер буфера перед записью и не писать, если он заполнен   -  person folibis    schedule 16.12.2020
comment
Хорошо я понял. send() не должен падать, а блокироваться или возвращаться с ошибкой, когда внутренний буфер заполнен. Пахнет глючной реализацией. Делает ли что-нибудь установка выходного буфера (SO_SNDBUF) на небольшое значение? Единственная другая альтернатива, о которой я могу думать, - это отправить отчет о прогрессе от клиента на сервер.   -  person rveerd    schedule 16.12.2020
comment
Насколько я могу судить, TIOCOUTQ — это ioctl для терминалов. Есть ли у вас основания полагать, что это применимо и к сокетам домена unix?   -  person John Bollinger    schedule 17.12.2020


Ответы (1)


Я использую сокеты домена Unix [... и] Чтобы предотвратить переполнение, я проверяю буфер записи

Это не ваша ответственность и, насколько я могу судить, не в вашей власти. В частности, не все ioctl применимы ко всем устройствам. TIOCOUTQ — это ioctl для терминальных устройств — начальная буква «T» в имени макроса является мнемонической для этого — и он не указан среди IOCTLS, применимые к сокетам в Linux (поскольку в комментариях вы упомянули, что целью является Linux).

Ядро гарантирует, что буфер не переполнится. При нехватке буферного пространства он либо блокирует запросы на запись до тех пор, пока не станет доступным достаточно места, либо выполняет короткие записи, либо и то, и другое. Или, если сокет находится в неблокирующем режиме, то пишет, что в противном случае вместо блокировки произойдет сбой с EAGAIN.

Как я могу этого избежать?

Избегайте использования ioctl на устройствах, к которым они не применяются.

Как мне получить настоящий буфер записи, а не эти странные цифры?

Вы не можете, насколько мне известно, и маловероятно, что у вас есть реальная необходимость это делать.

person John Bollinger    schedule 17.12.2020