C: anonymous file (created with memfd_create(2)) is always being written according to select(2)

62 views Asked by At

I have a set of file descriptors that I am "watching" with select. All of the other file descriptors are sockets; I want to introduce a new file descriptor so that I can "event driven"-ly register a change (without the need of a two way socket, which sounds a bit "wasteful").

After a bit of searching, I found the memfd_create function; which creates "anonymous files". According to the memfd_create man page:

memfd_create() creates an anonymous file and returns a file descriptor that refers to it.

So my idea is: I will write to that file descriptor, as if it were a file, and that will make select "pop". I will then read the contents of the file descriptor and follow the appropriate procedure.

However, select seems to be registering the file as if it had been read from (even when this is not the case). The file descriptor hasn't even been changed.

Here's some example code that shows this behaviour:

#include <sys/mman.h>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>
#include <iostream>

#define MAXMESSAGES 12

int main() {
    unsigned char message_buffer[MAXMESSAGES] = {0};

    int message_buffer_fd = memfd_create( "message_buffer", MFD_CLOEXEC);


    // POINT A

    ftruncate(message_buffer_fd, MAXMESSAGES);

    fcntl(message_buffer_fd , F_ADD_SEALS, F_SEAL_SHRINK);

    // write test data to the the "file"
    write( message_buffer_fd, message_buffer, sizeof(message_buffer) );

    lseek( message_buffer_fd, 0, SEEK_SET );

    // POINT B

    fd_set fdSet;
    fd_set fdSetWrite;

    FD_ZERO(&fdSet);
    FD_SET(message_buffer_fd, &fdSet);

    FD_ZERO(&fdSetWrite);
    FD_SET(message_buffer_fd, &fdSetWrite);

    fd_set ready_sockets;
    fd_set ready_sockets_write;
    while (true) {
        ready_sockets = fdSet;
        ready_sockets_write = fdSetWrite;

        //Wait until a fd is changed
        select(FD_SETSIZE, &ready_sockets, &ready_sockets_write, nullptr, nullptr);

        for (int i = 0; i < FD_SETSIZE; i++) {
             if (FD_ISSET(i, &ready_sockets)) {
                  std::cout << "I have been read from" << std::endl;
             }
             else if (FD_ISSET(i, &ready_sockets_write)) {
                 std::cout << "I have been written to" << std::endl;
             }
       }
    }


}

Result:

I have been read from
I have been read from
I have been read from
I have been read from
I have been read from
I have been read from
I have been read from
I have been read from
I have been read from
I have been read from
I have been read from
(...)

I am doing this in C++, however the code is basically C.

So, how could I make it so that select is only "awaken" when the file descriptor of this "anonymous file" is actually written (following select's default behavior).

PS: I've tried several combinations of including and not including the lines From POINT A to POINT B.

2

There are 2 answers

2
John Bollinger On

anonymous file (created with memfd_create(2)) is always being written according to select(2)

From your wording and code, I think you have a misunderstanding of select(). This function tells you about I/O operations that can be performed on a file without blocking, not about what has been done with a file. I don't generally expect I/O on a memfd file ever to block -- fail perhaps, but not block. So I expect such a file always to be ready for both reading and writing.

For example, you create your file, size it, write some contents to it, and seek to its beginning. At this point select() reports it both readable and writable, which it is. You could read back some of the data written to it or overwrite that data with new. Your program reports the first, and skips testing the second. And of course, even if the fd were associated with a file that might at times not be ready for reading and / or writing, your program does nothing to it inside the select loop, so there is no reason for its readiness to change.

So, how could I make it so that select is only "awaken" when the file descriptor of this "anonymous file" is actually written (following select's default behavior).

You can't as such. Use a pipe instead. This is a relatively common technique for providing a means to trigger a break from select() and similar functions. The read end of the pipe is included in the (read) selection set. Another thread can then write to the write end, making the read end readable until all the data that were written to the pipe are consumed.

0
Peter Petigru On

Thank you to @Shawn for pointing out the existence of eventfd. I ended up using those and they worked pretty much as expected.

According to the man page, you need to set the eventfd's file descriptor to a value larger than 1 for select to recognize it as a readable file descriptor.

What I did was, I created the eventfd file descriptor and added to the select set:

fd_set socketSet;
eventFd = eventfd(0, 0);
FD_SET(eventFd, &socketSet);

When I needed select to pop, I would make it's value 1, like

(...)
eventfd_write(eventFd, 1);  
(...)

This will make select pop and will check the eventFd for reading. When I wanted select to stop recognizing it as "ready to read", I would reset it's value by reading it (this is behavior is described in the man page).

eventfd_t buffer;
eventfd_read(eventFd, &buffer);

SIDE NOTE: Like @John Bollinger mentioned, eventfd is Linux only. Furthermore, eventfd_read and eventfd_write are GNU extensions. So this solution is by no means very portable. But I did solve my problem.

Thank you all!