I previously described that most Linux interfaces are manipulated as some form of file. The benefit of this is that the common APIs allow you to perform the same operation on different types of file with different properties.

Any writable file can be used as the standard output of a program, so you can redirect output to /dev/null to discard it, a pipe(7) to feed the output of one command to another, or a socket(7) to send the output of a command to another computer.

Pipes and sockets may also be read, and there's other special files, such as /dev/zero, /dev/urandom and /dev/random, which read data served by the kernel.

select(2) and friends

Reading and writing is not the only operation to perform on files.

It is possible to wait for any of a group of them to have data available with the select(2), poll(2) or epoll(7) families of system calls.

These system calls are useful, since attempts to read from, or write to certain special files, will suspend process execution until there is data available to read or the data has finished being writen.

This is a useful property for when a process is handling data, since it allows other proceses to do their work, but if a process is supposed to handle connections from multiple different sources, then it will result in one connection starving the others, when the others might be ready with data.

To solve this the select() family of system calls wait for one of a group of file descriptors to be ready before resuming process execution.

event file descriptors

Previously we were interested in waiting for file descriptors to be ready for data to be read from or written to, but other events can be waited for too.

Instead of having a dedicated system call for waiting for the event, it is better to wait for events in a select() loop, as it allows multiple events to be waited for together.

The kind of event to wait for depends on the type of file descriptor.

Instead of specifying a timeout in the timeout parameter of the select() system calls, you can pass a file descriptor created with the timerfd_create(2) system call.

Instead of registering a signal handler, a signalfd(2) can be created, which will let you handle the signal in the context of the process calling select on the file descriptor.

Not every signal can be handled by the signalfd: SIGBUS for example. However there is, at the time of writing, work in progress to handle this with a userfaultfd(2), which allows you to mmap(2) replacement data into a process when it is not available.

This has to be run in a separate thread, since you can't select() on a userfaultfd while also triggering a page fault.

Finally, there's the eventfd(2), which has two main uses:

  1. Providing a way for a thread/process to send event notifications to another thread/process's event loop.
  2. Provide a semaphore-like lock.

Other files that can be waited for

The file descriptor returned by [epoll_create(2)][] can also be polled for events. This is handy, as it allows you to plug one epoll-based event loop into another, and abstract away that an event-driven library handles multiple file descriptors internally and only hand one file descriptor to the calling program so that it can plug that into its event loop.

Most of the physical devices attached to a system are visible in /dev, network devices are unfortunately missing from here. Instead, notifications and control of network devices is possible with special netlink(7) sockets.