2

I have two different programs in C++ that talk to each other via (linux) pipes.

One process is the master, and the other process is the slave. Either of them could crash or simply be restarted, but never both at the same time (let's assume this).

Slave creates and opens pipe A in write mode in a blocking fashion (irrelevant last feature), then creates and opens pipe B in read mode in a blocking fashion as well (again, irrelevant last feature).

Master creates and opens pipe A in read mode in a non-blocking fashion (again, irrelevant last feature), then it waits some time so that slave can exit blocking create of pipe A and block in create of pipe B, then creates and opens pipe B in write mode in a non-blocking fashion as well (again, irrelevant last feature).

int open_pipe_read_mode(const char *file_path, bool dont_block)
{
    int pipe_fd = -1;
    if ((mkfifo(file_path, 0666) != 0) && (errno != EEXIST)) {
        return pipe_fd;
    }
    
    if (dont_block) {
        pipe_fd = open(file_path, O_RDONLY | O_NONBLOCK); // For write mode: O_WRONLY | O_CREAT | O_NONBLOCK
    } else {
        pipe_fd = open(file_path, O_RDONLY); // For write mode: O_WRONLY | O_CREAT
    }
    return pipe_fd;
}

So far so good as long as both ends are up and running. Note from my code that actually only one process will create the pipe with mkfifo, and the other one will fail with EEXIST.

My question arises when one of the two processes is restarted. If the other process doesn't use the pipes in the downtime, will the restarted process crush/fail upon calling my functions open_pipe_read_mode or open_pipe_write_mode ? Or, since these pipes are created with mkfifo as files, it is possible to reopen one just like that? Note that in my code I consider (ignore) the EEXIST error from mkfifo.

I have been reading in other questions like this one but all I find is "you cannot" or "why would you? just don't". I believe that creating pipes without filenames indeed makes it impossible (i.e. cumbersome and not safe, definitely not recommended) to reopen them as I intend.

Is there any recommendation against this (namely, if it only works surfing the undefined behaviour effect yet it could not work at any random point)?

My actual question is: considering that a pipe has been created with a file name with mkfifo and whose sides have been opened by two different processes, is it possible to open one end of the pipe (be it read or write, blocking or not) after such end has previously been closed and while the other end has been kept open?

My code works as described before restarting either process, but I couldn't manage to test a restart scenario because my project is complex. Before developing a simple couple of programs (mostly because working them once says nothing about determinism let aside good practices) I decided to look in the internet and I was surprised to find no question/answer to this.

What I have tested

Two very basic programs master and slave who open two pipes A and B to exchange information. Pipe A is slave-to-master flow, pipe B is master-to-slave flow. The exchange of information is irrelevant, just here to check SIGPIPE and other errors.

int master_main()
{
    // Open pipe A as read (non-blocking)
    pid_t pid = getpid();
    int read_pipe_fd = open_pipe_read_mode("./test_pipe_A", true);
    if (read_pipe_fd < 0) {
        printf("%d\tError open_pipe_read_mode\n", pid);
        return 1;
    }

    sleep(1);   // Wait for slave program to open pipe B

    // Open pipe B as write (non-blocking)
    int write_pipe_fd = open_pipe_write_mode("./test_pipe_B", true);
    if (write_pipe_fd < 0) {
        printf("%d\tError open_pipe_write_mode\n", pid);
        return 1;
    }

    int data_var = 1234;
    do {
        // Write data
        if (write(write_pipe_fd, (int*)&data_var, sizeof(int)) < 0) {
            printf("%d\tError write\n", pid);
        } else {
            printf("%d\tWrote data_var = %d\n", pid, data_var);
        }

        // Read data
        ssize_t count = read(read_pipe_fd, &data_var, sizeof(int));
        if (count != sizeof(int)) {
            printf("%d\tError read\n", pid);
        } else {
            printf("%d\tRead data_var = %d\n", pid, data_var);
        }
        data_var++;
        sleep(10);
    } while (true);
}

int slave_main()
{
    // Open pipe A as write (blocking)
    pid_t pid = getpid();
    int write_pipe_fd = open_pipe_write_mode("./test_pipe_A", false);
    if (write_pipe_fd < 0) {
        printf("%d\tError open_pipe_write_mode\n", pid);
        return 1;
    }
    
    // Open pipe B as read (blocking)
    int read_pipe_fd = open_pipe_read_mode("./test_pipe_B", false);
    if (read_pipe_fd < 0) {
        printf("%d\tError open_pipe_read_mode\n", pid);
        return 1;
    }

    int data_var = 5678;
    do {
        // Write data
        if (write(write_pipe_fd, (int*)&data_var, sizeof(int)) < 0) {
            printf("%d\tError write\n", pid);
        } else {
            printf("%d\tWrote data_var = %d\n", pid, data_var);
        }

        // Read data
        ssize_t count = read(read_pipe_fd, &data_var, sizeof(int));
        if (count != sizeof(int)) {
            printf("%d\tError read\n", pid);
        } else {
            printf("%d\tRead data_var = %d\n", pid, data_var);
        }
        data_var++;
        sleep(10);
    } while (true);
}

Then I execute:

    slave_main &
    master_main
        967     Wrote data_var = 5678
        967     Read data_var = 1234
        970     Wrote data_var = 1234
        970     Read data_var = 5678
        ^C
    master_main
        971     Wrote data_var = 1234
        971     Error read
        967     Wrote data_var = 1235
        967     Read data_var = 1234
        971     Wrote data_var = 1235
        971     Read data_var = 1235
        ^C
    [1]+  Broken pipe       slave_main

Until ^C, the flow is working.

I restart the master program and it works fine. The error read happens because pipe is opened in non-blocking mode and slave is still sleeping and thus hasn't sent anything yet. In the next loop we see read/write works fine for both ends.

When I kill the master, slave crashes after waking up upon write operation, probably SIGPIPE signal.

If I do the vice versa version (master in bg, killing slave, etc) I get similar result: the reopen works.

My question remains open. My code seems to work on linux but: is it deterministic? safe? (not) recommended? These questions refer to reopening pipes with mkfifo (I acknowledge there are workarounds to what I want to achieve, but my question is rather theoretical for the sake of knowledge, and in a way, to be able to use it with care when it's needed).

20
  • 1
    Writing to the pipe if the other end has closed it will cause a SIGPIPE signal or EPIPE error if you've disabled the signal. Reading from the pipe when the other end has closed it will return EOF. You need to detect either of these conditions and reopen the pipe. Commented May 8, 2023 at 16:01
  • Please edit your question and add more details. What do you want to achieve? What are the two processes supposed to do? What exactly should happen when one process crashes and gets restarted? Are you familiar with the return values and signals that will occur when one process dies? The remaining process could close and re-open the pipe in this case. If you open a file with O_CREAT and it does not already exist as a named pipe your program will create an ordinary file. Commented May 8, 2023 at 16:01
  • 1
    @nsalu It is a consequence of the behavior of pipes in general. For example, when the reader opens a pipe first, it waits for a writer. But when the writer quits, the reader does not wait for another writer (the reader sees EOF instead). Similarly, if a writer opens first, then it waits for the reader; but when the reader quits, the writer does not wait for another reader (it receives SIGPIPE or EPIPE instead). In both cases, the pipes must be closed down completely, and the protocol must be started all over again. Commented May 9, 2023 at 7:16
  • 1
    Your idea of reading from one pipe and on EOF stop writing to the other is not safe. A pipe has a bufffer and you might need a few read operations until you notice EOF. The other process could even crash just between successfully reading from one pipe and trying to write to the other. A full minimal reproducible example would make it easier for us to write an answer about possible problems in your use case. Your question "if the opening is safe" is difficult to answer because your requirements are not clear. Of course the restarted program can re-open the pipe, but the result may depend on what the other one does. Commented May 9, 2023 at 12:11
  • 1
    No, a minimal reproducible example would be code we can compile and run to reproduce your problem or use case. I don't know if you can reliably continue using a pipe after the system has reported that the other end was closed and if a new process opens it again. Your code neither opens both ends of the pipes nor closes and reopens the pipe, so I cannot confirm it's safe. The comments only show possible problems and unclear requirements. If I would be able to provide an answer I would write an answer, not comments. Commented May 10, 2023 at 8:40

1 Answer 1

0

Creating a named pipe with mkfifo, opening it as read-only by one process and as write-only by another process, then closing any of it's ends by terminating one process, and reopening again the closed end by a new process as the code does in this post, works and seems to be safe.

However! Only the (re)open itself, not everything else (namely reads, writes, etc). How to use and handle such pipe seems quite unsafe for many reasons (see caveats mentioned in this question's comment section, thank you people), and thus it's wisely recommended to find a workaround such as opening both ends in each process to avoid read/write operations in pipes where one end is closed, or simply find a way to close pipes safely in both sides and then reopening pipe as usual.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.