0
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char* argv[]){ if(argc < 2){ puts("err argv"); return -1; } int r_fd = open(argv[1], O_RDWR); int w_fd = open(argv[1], O_RDWR); fd_set r_set; fd_set w_set; char w_buf[4096]; char r_buf[2048]; int read_count = 0,write_count = 0; while(1){ FD_ZERO(&r_set); FD_ZERO(&w_set); FD_SET(r_fd, &r_set); FD_SET(w_fd, &w_set); int ret = select(w_fd + 1, &r_set, &w_set, NULL, NULL); if(FD_ISSET(r_fd, &r_set)){ read(r_fd, r_buf, sizeof(r_buf)); printf("read count:%d\n", read_count++); } if(FD_ISSET(w_fd, &w_set)){ write(w_fd, w_buf, sizeof(w_buf)); printf("write count:%d\n", write_count++); } //sleep(1); } return 0; } 

execute code:mkfifo 1.fifo && gcc main.c -o main && ./main 1.fifo

7
  • Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. Commented Feb 9, 2023 at 8:54
  • You mean it works the way it is here, with char r_buf[2048];, but changing it to char r_buf[48]; or so makes it fail? Or something else? Commented Feb 9, 2023 at 14:37
  • @ilkkachu yes, change 2048 to 48, write call will be blocked Commented Feb 10, 2023 at 1:54
  • @zhenfeiren, right, I forgot to ask, what system did you test this on? It may affect the exact behaviour, e.g. I can't get that to lock on Linux, but can if I increase the write size. It looks to me like it always writes a full page of 4096 bytes, so the write never blocks. Commented Feb 10, 2023 at 6:44
  • @ilkkachu I test this on macOS .The maximum write buffer size is 4096 ,so you increase it will block Commented Feb 10, 2023 at 9:19

2 Answers 2

1
 if(FD_ISSET(w_fd, &w_set)){ write(w_fd, w_buf, sizeof(w_buf)); printf("write count:%d\n", write_count++); } 

If select tells you an fd is writable, that means you can write at least one byte. The threshold may be higher than 1, depending on how the FIFO buffer is managed, but it certainly doesn't mean you can write 4096 bytes without blocking.

This code is statically wrong and must set both fds to non-blocking mode (and be ready to handle EAGAIN/EWOULDBLOCK return codes appropriately) to be reasonably robust.

The reason some read buffer sizes expose the problem is to do with whether you can reach a situation where the FIFO is empty enough that w_fd shows as writeable, but too full for the write to complete (so it blocks).

You could trivially confirm or falsify this by attaching a debugger to the blocked process, or running it under strace. These are both perfectly normal tools you should learn to use.

3
  • "but it certainly doesn't mean you can write 4096 bytes without blocking" -- as far as I tested on Linux, in practice it seemed to mean just that. :) You're right though, if it works it's something of an accident. Commented Feb 10, 2023 at 6:47
  • But I'm not sure how changing the read buffer size matters, even if we always read just one byte less than the corresponding write, the buffers should creep slowly fuller, until the next write doesn't fit in. Unless it's not just about the numbers but also alignment or something. Commented Feb 10, 2023 at 6:50
  • If you can end up with say 3Kb free in the buffer, and the select marks the fd writable, then you will do a blocking write of 4Kb and block forever. So the only question is which read size gets you into that situation. Anyway, why are we speculating when it's trivial for OP to confirm where their code blocked? Commented Feb 10, 2023 at 11:14
0

The problem appears to be in checking both r_set and w_set after a single select -- possibly a race condition where the read or write invalidates the result.

This is a robust version, with extra debug, and some results.

It is important to do any available reads before writes. If you give the writes precedence, it will fill the fifo (probably up to 64KB) before the reads get a look-in. This may be part of the race condition in your original code.

// xSelect.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char* argv[]){ if(argc < 2){ puts("err argv"); return -1; } ssize_t r_res, w_res; int r_fd, w_fd; r_fd = open(argv[1], O_RDWR); w_fd = open(argv[1], O_RDWR); fprintf (stderr, "r_fd %d, w_fd %d\n", r_fd, w_fd); fd_set r_set; fd_set w_set; char w_buf[4096]; char r_buf[1235]; for (int j = 0, w = 0; j < 20; j++) { FD_ZERO(&r_set); FD_ZERO(&w_set); FD_SET(r_fd, &r_set); FD_SET(w_fd, &w_set); int ret = select(w_fd + 1, &r_set, &w_set, NULL, NULL); if(FD_ISSET(r_fd, &r_set)){ r_res = read(r_fd, r_buf, sizeof(r_buf)); fprintf (stderr, "Read %zd\n", r_res); } else if(w++ < 3 && FD_ISSET(w_fd, &w_set)){ w_res = write(w_fd, w_buf, sizeof(w_buf)); fprintf (stderr, "Write %zd\n", w_res); } else { fprintf (stderr, "Tick\n"); } } return 0; } 
$ make xSelect && echo Run && ./xSelect xSelect.fifo cc xSelect.c -o xSelect Run r_fd 3, w_fd 4 Write 4096 Read 1235 Read 1235 Read 1235 Read 391 Write 4096 Read 1235 Read 1235 Read 1235 Read 391 Write 4096 Read 1235 Read 1235 Read 1235 Read 391 Tick Tick Tick Tick Tick $ 

EDIT: I may have over-thought this. I do not think the r_set and w_set are directly affected by a single read and/or write: they are however obsoleted by those actions.

In your original code, w_fd will always be writeable until the FIFO is completely full. So if you do equal numbers of long writes and short reads, the writer will eventually hang. If your short reads are 2048 bytes, then you will need about 32 cycles to fill the fifo: if short reads are 48, you will fill the fifo with 17 cycles.

Obviously, the average rates of writing and reading must be equal in the long-term. Typically, the kernel achieves this by not scheduling the writer (which is in a different process), but your code enforces one read, one-write. So the w_set.w_fd eventually becomes unset.

One problem is that the select() does not know how much data you plan to write. If the fifo has space for one byte, w_fd is writeable. I do not know whether the write is truncated, or blocked, or failed, and whether having both fds using the same fifo makes any difference. It is not even worth testing, because the answer may depend on the OS, and whether we are using a fifo or a socket, or some other factor. Your code should be written to deal with a partial write, an error returning EAGAIN, and blocking.

Your code suffers from completely filling the fifo, and mine from completely emptying the fifo unnecessarily after each write. A proper demo should tune (or randomise) its behaviour to explore the possibilities.

2
  • 1
    I'd also put the fd's in non-blocking mode anyway, just in case. Not that it seems to be a problem the way you do it here, emptying the pipe each time before writing more. Unless you ever try to write more than can go there in one go... Commented Feb 9, 2023 at 14:49
  • @ilkkachu Of course, very true (and I missed it). If the write is allowed to block, the read is never reached, and we are in deadlock. Commented Feb 9, 2023 at 21:57

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.