为什么 read 系统调用在缺少 less than 块时停止读取?

Why read system call stops reading when less than block is missing?

介绍和一般objective

我正在尝试将图像从 child 进程(通过从 parent 调用 popen 生成)发送到 parent 进程。

图像是灰度 png 图像。它使用 OpenCV 库打开,并使用同一库的 imencode 函数进行编码。所以得到的编码数据被存储到uchar类型的std::vector结构中,即下面代码中的buf向量。

发送初步图像信息没有错误

首先child发送parent需要的如下图像信息:

这些数据由 child 使用 cout 在标准输出上写入,并由 parent 使用 fgets 系统调用读取。

这条信息是正确发送和接收的所以到现在没问题

正在发送图像数据

child 使用 write 系统调用将编码数据(即向量 buf 中包含的数据)写入标准输出,而 parent 使用file-descriptor由popen返回读取数据。使用 read 系统调用读取数据。

数据写入和读取在 while 循环内以 4096 字节的块为单位执行。行文如下:

written += write(STDOUT_FILENO, buf.data()+written, s);

其中 STDOUT_FILENO 指示在标准输出上写入。 buf.data() returns 指向向量结构内部使用的数组中第一个元素的指针。 written 存储到现在已经写入的字节数,用作索引。 swrite 每次尝试发送的字节数 (4096)。 write returns 实际写入的字节数,用于更新 written.

数据读取非常相似,由以下行执行:

bytes_read = read(fileno(fp), buf+total_bytes, bytes2Copy);

fileno(fp) 告诉从哪里读取数据(fppopen 返回的文件描述符)。 buf 是存储接收数据的数组,total_bytes 是到目前为止读取的字节数,因此用作索引。 bytes2Copy 是预期接收的字节数:它是 BUFLEN(即 4096)或者对于最后一个数据块,剩余数据(例如,如果总字节数是 5000 然后在 1 个 4096 字节块之后,预期另一个 5000-4096 块)。

密码

考虑这个例子。以下是启动一个 child 进程的进程 popen

#include <stdlib.h>
#include <unistd.h>//read
#include "opencv2/opencv.hpp"
#include <iostream>
#define BUFLEN 4096

int main(int argc, char *argv[])
{
    //file descriptor to the child process
    FILE *fp;
    cv::Mat frame;
    char temp[10];
    size_t bytes_read_tihs_loop = 0;
    size_t total_bytes_read = 0;
    //launch the child process with popen
    if ((fp = popen("/path/to/child", "r")) == NULL)
    {
        //error
        return 1;
    }

    //read the number of btyes of encoded image data
    fgets(temp, 10, fp);
    //convert the string to int
    size_t bytesToRead = atoi((char*)temp);

    //allocate memory where to store encoded iamge data that will be received
    u_char *buf = (u_char*)malloc(bytesToRead*sizeof(u_char));

    //some prints
    std::cout<<bytesToRead<<std::endl;

    //initialize the number of bytes read to 0
    bytes_read_tihs_loop=0;
    int bytes2Copy;
    printf ("bytesToRead: %ld\n",bytesToRead);
    bytes2Copy = BUFLEN;
    while(total_bytes_read<bytesToRead &&
        (bytes_read_tihs_loop = read(fileno(fp), buf+total_bytes_read, bytes2Copy))
    )
    {
        //bytes to be read at this iteration: either 4096 or the remaining (bytesToRead-total)
        bytes2Copy = BUFLEN < (bytesToRead-total_bytes_read) ? BUFLEN : (bytesToRead-total_bytes_read);
        printf("%d btytes to copy\n", bytes2Copy);
        //read the bytes
        printf("%ld bytes read\n", bytes_read_tihs_loop);

        //update the number of bytes read
        total_bytes_read += bytes_read_tihs_loop;
        printf("%lu total bytes read\n\n", total_bytes_read);
    }
    printf("%lu bytes received over %lu expected\n", total_bytes_read, bytesToRead);
    printf("%lu final bytes read\n", total_bytes_read);
    pclose(fp);
    cv::namedWindow( "win", cv::WINDOW_AUTOSIZE );
    frame  = cv::imdecode(cv::Mat(1,total_bytes_read,0, buf), 0);
    cv::imshow("win", frame);

    return 0;

}

而上面打开的进程对应如下:

#include <unistd.h> //STDOUT_FILENO
#include "opencv2/opencv.hpp"
#include <iostream>
using namespace std;
using namespace cv;

#define BUFLEN 4096

int main(int argc, char *argv[])
{
    Mat frame;
    std::vector<uchar> buf;
    //read image as grayscale
    frame = imread("test.png",0);
    //encode image and put data into the vector buf
    imencode(".png",frame, buf);
    //send the total size of vector to parent
    cout<<buf.size()<<endl;
    unsigned int written= 0;

    int i = 0;
    size_t toWrite = 0;
    //send until all bytes have been sent
    while (written<buf.size())
    {
        //send the current block of data
        toWrite = BUFLEN < (buf.size()-written) ? BUFLEN : (buf.size()-written);
        written += write(STDOUT_FILENO, buf.data()+written, toWrite);
        i++;
    }
    return 0;

}

错误

child 读取图像,对其进行编码并首先将尺寸(大小、#rows、#cols)发送到 parent,然后是编码图像数据。

parent 首先读取尺寸(没有问题),然后开始读取数据。每次迭代都会读取 4096 字节的数据。然而,当丢失少于 4096 字节时,它会尝试只读取丢失的字节:在我的例子中,最后一步应该读取 1027 字节(115715%4096),而不是读取所有他们只是读到`15.

我在最后两次迭代中打印的是:

4096 btytes to copy
1034 bytes read
111626 total bytes read

111626 bytes received over 115715 expected
111626 final bytes read
OpenCV(4.0.0-pre) Error: Assertion failed (size.width>0 && size.height>0) in imshow, file /path/window.cpp, line 356
terminate called after throwing an instance of 'cv::Exception'
  what():  OpenCV(4.0.0-pre) /path/window.cpp:356: error: (-215:Assertion failed) size.width>0 && size.height>0 in function 'imshow'

Aborted (core dumped)

为什么 read 没有读取所有丢失的字节?

我正在处理这张图片:

我尝试解码回图像的方式也可能存在错误,因此也将不胜感激。

编辑

在我看来,与某些建议相反,问题与 \n\r[=59=].

的存在无关

事实上,当我使用以下行打印接收到的整数数据时:

for (int ii=0; ii<val; ii++)
{
    std::cout<<(int)buf[ii]<< " ";
}

我在数据中间看到 01013 值(上述字符的 ASCII 值),所以这让我认为这不是问题所在.

您正在将二进制数据写入标准输出,这需要文本。可以添加或删除换行符 (\n) and/or return 字符 (\r),具体取决于文本文件中行尾的系统编码。由于您缺少字符,您的系统似乎正在删除这两个字符之一。

您需要将数据写入以二进制模式打开的文件,并且您应该以二进制方式读入文件。

更新答案

我不是世界上最擅长 C++ 的人,但这行得通并且会给你一个合理的起点。

parent.cpp

#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include "opencv2/opencv.hpp"


int main(int argc, char *argv[])
{
    // File descriptor to the child process
    FILE *fp;

    // Launch the child process with popen
    if ((fp = popen("./child", "r")) == NULL)
    {
        return 1;
    }

    // Read the number of bytes of encoded image data
    std::size_t filesize;
    fread(&filesize, sizeof(filesize), 1, fp);
    std::cout << "Filesize: " << filesize << std::endl;

    // Allocate memory to store encoded image data that will be received
    std::vector<uint8_t> buffer(filesize);

    int bufferoffset   = 0;
    int bytesremaining = filesize;
    while(bytesremaining>0)
    {
        std::cout << "Attempting to read: " << bytesremaining << std::endl;
        int bytesread   = fread(&buffer[bufferoffset],1,bytesremaining,fp);
        bufferoffset   += bytesread;
        bytesremaining -= bytesread;
        std::cout << "Bytesread/remaining: " << bytesread << "/" << bytesremaining << std::endl;
    }
    pclose(fp);

    // Display that image
    cv::Mat frame;
    frame = cv::imdecode(buffer, -CV_LOAD_IMAGE_ANYDEPTH);
    cv::imshow("win", frame);
    cv::waitKey(0);
}

child.cpp

#include <cstdio>
#include <cstdint>
#include <vector>
#include <fstream>
#include <cassert>
#include <iostream>

int main()
{
    std::FILE* fp = std::fopen("image.png", "rb");
    assert(fp);

    // Seek to end to get filesize
    std::fseek(fp, 0, SEEK_END);
    std::size_t filesize = std::ftell(fp);

    // Rewind to beginning, allocate buffer and slurp entire file
    std::fseek(fp, 0, SEEK_SET);
    std::vector<uint8_t> buffer(filesize);
    std::fread(buffer.data(), sizeof(uint8_t), buffer.size(), fp);
    std::fclose(fp);

    // Write filesize to stdout, followed by PNG image
    std::cout.write((const char*)&filesize,sizeof(filesize));
    std::cout.write((const char*)buffer.data(),filesize);
}

原答案

有几个问题:

你的 while 循环写入来自 child 进程的数据不正确:

while (written<buf.size())
{
    //send the current block of data
    written += write(STDOUT_FILENO, buf.data()+written, s);
    i++;
}

假设您的图像是 4097 字节。您将在循环中第一次写入 4096 个字节,然后在缓冲区中只剩下 1 个字节时尝试在第二次通过时写入 4096(即 s)字节。

您应该写入 4096 和缓冲区中剩余字节数中较小的一个。


发送文件的宽度和高度没有意义,它们已经编码在您发送的 PNG 文件中。

没有必要在 child 中调用 imread() 将 PNG 文件从磁盘转换为 cv::Mat,然后调用 imencode() 将其转换回 PNG发送到 parent。只需 open() 并将文件读取为二进制文件并发送 - 它已经是一个 PNG 文件。


我想你需要清楚你发送的是PNG文件还是纯像素数据。 PNG 文件将包含:

  • PNG header,
  • 图片宽度和高度,
  • 创建日期,
  • 颜色类型,bit-depth
  • 压缩、校验和像素数据

一个 pixel-data 唯一的文件将有:

  • RGB,RGB,RGB,RGB
fgets(temp, 10, fp);
...
read(fileno(fp), ...)

这不可能行得通。

stdio 例程是 缓冲的 。缓冲区由实现控制。 fgets(temp, 10, fp); 将从文件中读取未知数量的字节并将其放入缓冲区。这些字节将再也不会被低级文件 IO 看到。

您永远不会将同一个文件与两种 IO 样式一起使用。要么用 stdio 做所有事情,要么用低级 IO 做所有事情。第一个选项是迄今为止最简单的,您只需将 read 替换为 fread

如果出于某些只有邪恶的黑暗势力知道的不敬虔的原因你想保留这两种 IO 样式,你可以在做任何其他事情之前先调用 setvbuf(fp, NULL, _IOLBF, 0) 来尝试。我从来没有这样做过,也不能保证这种方法,但他们说它应该有效。不过,我没有看到使用它的单一理由。

关于一个可能不相关的问题,请注意,您的阅读循环在其终止条件中有一些逻辑不太容易理解并且可能无效。读取文件的正常方式大致如下:

 left = data_size;
 total = 0;
 while (left > 0 &&
        (got=read(file, buf+total, min(chunk_size, left))) > 0) {
    left -= got;
    total += got;
 }

 if (got == 0) ... // reached the end of file
 else if (got < 0) ... // encountered an error

更正确的方法是如果got < 0 && errno == EINTR再试一次,所以修改后的条件可能看起来像

 while (left > 0 &&
        (((got=read(file, buf+total, min(chunk_size, left))) > 0) ||
        (got < 0 && errno == EINTR))) {

但此时可读性开始受到影响,您可能希望将其拆分为单独的语句。