是否可以要求Linux在套接字读取期间对字节进行黑洞处理?

use*_*066 6 c++ sockets linux

我有一个在Linux Debian 9下运行的c ++程序。我正在从文件描述符中进行简单的read():

int bytes_read = read(fd, buffer, buffer_size);
Run Code Online (Sandbox Code Playgroud)

想象一下,我想从套接字读取更多数据,但是我想先跳过一个已知的字节数,然后再获取我感兴趣的某些内容:

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

int useful_bytes = read(fd, buffer, buffer_size);
Run Code Online (Sandbox Code Playgroud)

在Linux中,是否存在一个系统范围内的“内置”位置,我可以将不需要的字节转储到该位置,而不必维护不需要的数据的缓冲区(unwanted_buffer如上例所示)?

我想我要寻找的东西MSG_PEEK与套接字世界相反(即),也就是说,内核将bytes_to_skip在下一个有用的recv调用之前从其接收缓冲区中清除。

如果我正在从文件中读取内容,lseek那就足够了。但是,如果您正在从套接字读取并且正在使用分散/聚集I / O,并且要删除其中一个字段,则这是不可能的。

我正在考虑这样的事情:

// 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);
Run Code Online (Sandbox Code Playgroud)

我知道我可以b在接收端创建另一个变量,但是如果我不想这样做,我如何读取sizeof(int)文件中占用的字节,然后转储数据并继续进行操作c?我可以创建一个通用缓冲区以转储b到其中,我只想问默认情况下是否存在这样的位置。

[编辑]

遵循@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);
Run Code Online (Sandbox Code Playgroud)

但是,blackhole出来的时候0xffff,我得到了错误号13“权限被拒绝”。我尝试以su身份运行我的代码,但这也不起作用。也许我设置mmap不正确?

ine*_*ght 3

最后有一个 tl;dr 。

在我的评论中,我向您推荐了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;
}
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,您可以看到我创建了一个fork()4KiB(在大多数系统上为单页大小)的私有(写入之后子级将不可见)匿名(不由文件支持)内存映射。然后它被使用两次来为两个 int 提供写入目的地——后一个 int 覆盖前一个 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;
}
Run Code Online (Sandbox Code Playgroud)

所以它看起来甚至在引擎盖下preadv()使用seek(),当然,在套接字上是不允许的。我不确定是否有(还?)一种方法告诉操作系统忽略/删除已建立的流中收到的字节。我怀疑这是因为@geza 是正确的:对于我遇到的大多数情况来说,写入最终(忽略)目的地的成本非常微不足道。而且,在被忽略字节的成本并非微不足道的情况下,您应该认真考虑使用更好的选项、实现或协议。

长话短说:

创建 4KiB 匿名私有内存映射实际上与连续分配容器没有区别(存在细微的差异,对于非常高端性能之外的任何工作负载来说不太重要)。使用标准容器也不太容易出现分配错误:内存泄漏、野指针等。所以我会说 KISS 并且这样做,而不是认可我上面写的任何代码。例如:std::array<char, 4096> ignored;std::vector<char> ignored{4096};并设置iovec.iov_base = ignored.data();并将其设置.iov_len为您需要忽略的任何大小(在容器的长度内)。

  • 需要明确的是,私有匿名内存的“mmap”实际上并没有将其变成黑洞。这就是内存分配器为满足大型分配请求所做的事情(并且根据设计,分配块来满足较小的请求)。写入它实际上与写入堆栈分配的数组或“malloc”/“new”缓冲区相同(实际上比第一次写入时的堆栈慢一点,因为Linux匿名映射是在写入时延迟复制的零页的映射,因此第一次写入必须使真正的零页内存可用)。 (2认同)