1

I am trying to send data from javascript to C via a named pipe/FIFO. Everything seems to be working other than every couple messages there will be an additional iteration of the loop reading 0 bytes rather than the expected 256. I realize I could add something like if (bytes_read>0) {... but this seems like a band aid to a bigger problem.

From my testing it looks like the open() call in the reader is unblocked when the writer opens AND closes the file. It looks like sometimes the reader is ready (and blocked on open) for the next message before the writer closes the file from the previous so it will run through its loop twice per message.

Is this just expected behavior or am I misunderstanding something? Any advice would be greatly appriciated!

Writer:

uint32_t writer(buf)
{
  int fd;

  // FIFO file path
  char * myfifo = "/tmp/channel";
  
  // Creating the named file(FIFO)
  // mkfifo(<pathname>, <permission>)
  mkfifo(myfifo, 0666);
  
  // Open FIFO for write only
  fd = open(myfifo, O_WRONLY);

  uint32_t written = write(fd, buf, sizeof(char)*256);
  close(fd);

  return written;
}

Reader:

int main()
{
    int fd;

    // FIFO file path
    char * myfifo = "/tmp/channel";

    // Creating the named file(FIFO)
    mkfifo(myfifo, 0666);

    while (1)
    {
        char buf[256] = "\0";

        // Open FIFO for Read only
        fd = open(myfifo, O_RDONLY);

        // Read from FIFO
        int bytes_read = read(fd, buf, sizeof(char)*256);

        struct access_point *dev = (struct access_point *) &buf;

        printf("Bytes read: %i\n", bytes_read);
        printf("ssid: %s\n", dev->ssid);

        int r = close(fd);
        //sleep(0.01);
    }
    return 0;
}

After 5 calls to writer the output will look like:

# Call 1 (bad)
Bytes read: 256
ssid: kremer
Bytes read: 0
ssid: 

# Call 2
Bytes read: 256
ssid: kremer

# Call 3
Bytes read: 256
ssid: kremer

# Call 4 (bad)
Bytes read: 256
ssid: kremer
Bytes read: 0
ssid: 

# Call 5
Bytes read: 256
ssid: kremer
akremer
  • 123
  • 1
  • 1
  • 5
  • What are you expecting to happen when there is no data available to read from the pipe? – john Jul 08 '22 at 19:53
  • 1
    *I realize I could add something like `if (bytes_read>0) {...` but this seems like a band aid to a bigger problem* Please read the [`read()` documentation](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/read.html) (bolding added): "The *read()* function shall **attempt** to read *nbyte* bytes..." Also, `struct access_point *dev = (struct access_point *) &buf;` is a strict-aliasing violation and therefore undefined behavior. – Andrew Henle Jul 08 '22 at 20:01
  • @AndrewHenle So you are basically saying it is behaving as expected? Also, I am not quite sure what you mean with the aliasing violation. The buffer is created using the same struct from a shared header file on the same machine and is less than 256 bytes. Given this context would it still be considered a strict-aliasing violation? The struct is defined as: struct access_point { uint8_t packettype; uint8_t security; uint8_t strength; bool known; char ssid[33]; char pass[65]; }; – akremer Jul 08 '22 at 20:13
  • 3
    @akremer You're just plain not allowed to declare an array of `char`, take a pointer to it, cast that pointer to any other type (except `unsigned char *`), and then dereference the cast pointer. No exceptions. The correct way to serialize and deserialize a struct to a byte stream is field by field, using functions like the `cpu_to_be32` shown here: https://stackoverflow.com/questions/18853149/how-to-combine-three-variables-to-send-using-boost-asio/18854567#18854567 – zwol Jul 08 '22 at 20:29
  • 1
    @akremer This doesn't have anything to do with the behavior of `read` on a FIFO, though. My suggestion for _that_ is that you should try using an `AF_LOCAL` socket instead. – zwol Jul 08 '22 at 20:30
  • @zwol I understand that it is technically not allowed but if the data is guaranteed to only run on one machine/cpu would this ever be an issue? – akremer Jul 08 '22 at 20:58
  • @akremer yes, https://stackoverflow.com/a/99010/5878272 – Fredrik Jul 09 '22 at 05:07

2 Answers2

3

A reador 0 bytes indicates the pipe was closed on the other end. So this is absolutely expected behavior.

So you do have to catch that 0 and read again so the kernel waits for the next time the pipe is opened.

Note: read can also return -1 with errno set to EINTR. Same deal, just read again.

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
1

FIFOs have very nuanced semantics. Here's a sequence of events that can produce such a result. W refers top the writing thread. R refers to the reading thread.

W: opens the fifo, is blocked until the reader opens the FIFO
R: opens the fifo
W: open succeeds
W: write succeeds
R: read succeeds
W: closes the fifo
W: back at the top of its loop
W: it opens the fifo again, it succeeds because the reader hasn't closed it yet
W: writes to the fifo
R: reader finally closes the fifo, after it receives the first message
R: reader closes the fifo, this effectively causes W's 2nd message to get flushed down the drain
R: back at the top of its loop
R: opens the fifo, the writer still has it open
R: enters read
W: closes the fifo
R: reads 0 bytes

You have no guarantees, whatsoever, of the relative sequence of multiple events that occurs in both threads. As such, you might get unexpected results.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148