Можно ли попросить Linux к байту чёрной дыры во время чтения сокета? - PullRequest
6 голосов
/ 14 мая 2019

У меня есть программа на С ++, работающая под Linux Debian 9. Я делаю простое чтение () из файлового дескриптора:

int bytes_read = read(fd, buffer, buffer_size);

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

int unwanted_bytes_read = read(fd, unwanted_buffer, bytes_to_skip);

int useful_bytes = read(fd, buffer, buffer_size);

В Linux существует ли общесистемное «встроенное» расположение, в которое я могу записать ненужные байты, вместо того, чтобы поддерживать буфер для нежелательных данных (как unwanted_buffer в приведенном выше примере)?

Я предполагаю, что то, что я ищу, будет (вроде) противоположностью MSG_PEEK в мире сокетов, то есть ядро ​​очистит bytes_to_skip от своего буфера приема перед следующим полезным вызовом recv.

Если бы я читал из файла, то lseek было бы достаточно. Но это невозможно, если вы читаете из сокета и используете ввод / вывод scatter / collect и хотите удалить одно из полей.

Я думаю о чем-то вроде этого:

// send side
int a = 1;
int b = 2;
int c = 3;
struct iovec iov[3];
ssize_t nwritten;

iov[0].iov_base = &a;
iov[0].iov_len  = sizeof(int);
iov[1].iov_base = &b;
iov[1].iov_len  = sizeof(int);
iov[2].iov_base = &c;
iov[2].iov_len  = sizeof(int);

nwritten = writev(fd, iov, 3);

// receive side
int a = -1;
int c = -1;
struct iovec iov[3]; // you know that you'll be receiving three fields and what their sizes are, but you don't care about the second.
ssize_t nread;

iov[0].iov_base = &a;
iov[0].iov_len  = sizeof(int);
iov[1].iov_base = ??? <---- what to put here?
iov[1].iov_len  = sizeof(int);
iov[2].iov_base = &c;
iov[2].iov_len  = sizeof(int);

nread = readv(fd, iov, 3);

Я знаю, что я мог бы просто создать другую переменную b на принимающей стороне, но если я не хочу, как я могу прочитать байты sizeof(int), которые она занимает в файле но просто сбросьте данные и перейдите к c? Я мог бы просто создать общий буфер для дампа b, все, что я спрашивал, это если такое расположение по умолчанию.

[EDIT]

Следуя совету @inetknght, я попытался сопоставить память / dev / null и выполнить сбор в сопоставленный адрес:

int nullfd = open("/dev/null", O_WRONLY);
void* blackhole = mmap(NULL, iov[1].iov_len, PROT_WRITE, MAP_SHARED, nullfd, 0);

iov[1].iov_base = blackhole;    

nread = readv(fd, iov, 3);

Однако blackhole отображается как 0xffff, и я получаю сообщение об ошибке 13 «Отказано в доступе». Я попытался запустить свой код как su, и это тоже не работает. Возможно, я неправильно настраиваю mmap?

Ответы [ 2 ]

2 голосов
/ 22 мая 2019

В конце есть тролль.

В моем комментарии я предложил вам mmap() устройство /dev/null. Тем не менее, кажется, что устройство не отображается на моей машине (ошибка 19: No such device). Похоже, что /dev/zero сопоставимо. Другой вопрос / ответ предполагает, что это эквивалентно MAP_ANONYMOUS, что делает аргумент fd и связанный с ним open() ненужным в первую очередь. Посмотрите пример:

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>

extern "C" {
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
}

template <class Type>
struct iovec ignored(void *p)
{
    struct iovec iov_ = {};
    iov_.iov_base = p;
    iov_.iov_len = sizeof(Type);
    return iov_;
}

int main()
{
    auto * p = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if ( MAP_FAILED == p ) {
        auto err = errno;
        std::cerr << "mmap(MAP_PRIVATE | MAP_ANONYMOUS): " << err << ": " << strerror(err) << std::endl;
        return EXIT_FAILURE;
    }

    int s_[2] = {-1, -1};
    int result = socketpair(AF_UNIX, SOCK_STREAM, 0, s_);
    if ( result < 0 ) {
        auto err = errno;
        std::cerr << "socketpair(): " << err << ": " << strerror(err) << std::endl;
        return EXIT_FAILURE;
    }

    int w_[3] = {1,2,3};
    ssize_t nwritten = 0;
    auto makeiov = [](int & v){
        struct iovec iov_ = {};
        iov_.iov_base = &v;
        iov_.iov_len = sizeof(v);
        return iov_;
    };
    struct iovec wv[3] = {
        makeiov(w_[0]),
        makeiov(w_[1]),
        makeiov(w_[2])
    };

    nwritten = writev(s_[0], wv, 3);
    if ( nwritten < 0 ) {
        auto err = errno;
        std::cerr << "writev(): " << err << ": " << strerror(err) << std::endl;
        return EXIT_FAILURE;
    }

    int r_ = {0};
    ssize_t nread = 0;
    struct iovec rv[3] = {
        ignored<int>(p),
        makeiov(r_),
        ignored<int>(p),
    };

    nread = readv(s_[1], rv, 3);
    if ( nread < 0 ) {
        auto err = errno;
        std::cerr << "readv(): " << err << ": " << strerror(err) << std::endl;
        return EXIT_FAILURE;
    }

    std::cout <<
        w_[0] << '\t' <<
        w_[1] << '\t' <<
        w_[2] << '\n' <<
        r_ << '\t' <<
        *(int*)p << std::endl;

    return EXIT_SUCCESS;
}

В приведенном выше примере вы можете видеть, что я создаю частную (записи не будут видны детям после fork()) анонимного (без поддержки файла) отображения 4 КБ (один размер страницы в большинстве систем) , Затем он используется дважды для указания места назначения записи для двух целых чисел - позднее int перезаписывает предыдущее.

Это не точно не решит ваш вопрос: как игнорировать байты. Поскольку вы используете readv(), я посмотрел на его сестринскую функцию, preadv(), которая, на первый взгляд, делает то, что вы хотите: пропустить байты. Однако, похоже, что это не поддерживается в дескрипторах файлов сокетов. Следующий код дает preadv(): 29: Illegal seek.

rv = makeiov(r_[1]);
nread = preadv(s_[1], &rv, 1, sizeof(int));
if ( nread < 0 ) {
    auto err = errno;
    std::cerr << "preadv(): " << err << ": " << strerror(err) << std::endl;
    return EXIT_FAILURE;
}

Похоже, что даже preadv() использует seek() под капотом, что, конечно, недопустимо для сокета. Я не уверен, есть ли (пока?) Способ сказать ОС игнорировать / отбрасывать байты, полученные в установленном потоке. Я подозреваю, что это потому, что @ geza является правильным : стоимость записи в конечный (игнорируемый) пункт назначения чрезвычайно тривиальна для большинства ситуаций, с которыми я сталкивался. И в ситуациях, когда стоимость игнорируемых байтов составляет , а не тривиально, вам следует серьезно подумать об использовании лучших вариантов, реализаций или протоколов.

ТЛ; др:

Создание анонимного сопоставления частной памяти размером 4 КБ эффективно неотличимо от контейнеров непрерывного выделения (есть тонкие различия, которые вряд ли будут важны для любой рабочей нагрузки за пределами очень высокой производительности). Использование стандартного контейнера также намного менее подвержено ошибкам выделения: утечки памяти, дикие указатели и др. Поэтому я бы сказал «ПОЦЕЛУЙ» и просто сделал бы это вместо одобрения любого кода, который я написал выше. Например: std::array<char, 4096> ignored; или std::vector<char> ignored{4096}; и просто установите iovec.iov_base = ignored.data(); и установите для .iov_len тот размер, который вам нужно игнорировать (в пределах длины контейнера).

1 голос
/ 22 мая 2019

эффективное чтение данных из сокета происходит, когда:

  1. Размер буфера пользовательского пространства такой же или больше (SO_RCVBUF_size + maximum_message_size - 1), чем у ядраприемный буфер сокета.Вы даже можете отображать страницы буферной памяти два раза подряд, чтобы сделать его кольцевым буфером, чтобы избежать memmove неполных сообщений в начале буфера.
  2. Чтение выполняется за один вызов recv.Это сводит к минимуму количество системных вызовов (которые в наши дни стоят дороже из-за смягчения последствий для Spectre, Meltdown и т. Д.).А также предотвращает голодание других сокетов в том же цикле событий, что может произойти, если код повторно вызывает recv в одном и том же сокете с небольшим размером буфера, пока не произойдет сбой с EAGAIN.А также гарантирует, что вы исчерпаете весь буфер приема ядра в одном recv системном вызове.

Если вы выполните вышеизложенное, вам следует интерпретировать / декодировать сообщение из буфера пользовательского пространства, игнорируя всеявляется необходимым.

Использование нескольких вызовов recv или recvmsg с небольшими размерами буфера является неоптимальным с точки зрения задержки и пропускной способности.

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