3.6. Event Scheme

The event scheme is a critical component in vortexOS for the operation of device drivers, schemes, and other programs that need to receive events from multiple sources. It functions like a "clearing house" for activity across multiple file descriptors. When a daemon or client program performs a read operation on the event scheme, it blocks until an event occurs. Once an event is detected, the program identifies which file descriptor became active and performs a non-blocking read on that specific descriptor. This allows a program to handle multiple sources without blocking on one source while another is active, ensuring it only blocks on the event scheme and becomes unblocked whenever any source becomes active.

The event scheme in vortexOS is conceptually similar to Linux's epoll mechanism.

What is a Blocking Read

In a regular scenario, when a program reads from a file, it calls the read function, providing an input buffer. Once the read operation is complete, the system places the data in the buffer, and the program continues. Behind the scenes, the system suspends the program while it waits for the read to finish. This approach works well when the program has no other tasks, but it can be inefficient if the program needs to handle additional activities, such as user input or updating the display, while waiting for the read to complete.

Non-blocking Read

To avoid getting stuck waiting for a specific source, a program can use the O_NONBLOCK flag when opening a path. If data is available, the system copies it into the input buffer and returns immediately. If no data is available, the read operation returns an EAGAIN error, indicating the program should try again later. This allows the program to scan multiple file descriptors to check if any are ready for reading. If none have data, the program blocks until there is something to do, and this is where the event scheme becomes useful.

Using the Event Scheme

The event scheme enables a daemon or client program to receive a message on the event file, notifying it that a file descriptor is ready for reading. The daemon then reads from the event file to determine which file descriptor is active. If no descriptor is ready, the read operation on the event file blocks, suspending the daemon until the event scheme detects an active descriptor.

Before setting up the event scheme, the program should open all the necessary resources and set them to non-blocking mode. For example, a scheme provider might open its scheme file in non-blocking mode:

let mut scheme_file = OpenOptions::new()
    .create(true)
    .read(true)
    .write(true)
    .custom_flags(syscall::O_NONBLOCK as i32)
    .open(":myscheme")
    .expect("mydaemon: failed to create myscheme: scheme");

The first step in using the event scheme is to open a connection to it. Each program gets a unique connection to the event scheme, so no path name is required—just the scheme name:

let event_file = File::open("/scheme/event");

Next, the program writes messages to the event scheme, one for each file descriptor that needs to be monitored. A message is structured using the syscall::data::Event struct:

use syscall::data::Event;
let _ = event_file.write(&Event { id: scheme_file.as_raw_fd(), ... });

Timers in vortexOS are also managed through schemes, so if a timer is needed, it must be opened and included in the event file monitoring. Once the setup is complete, the program enters the main loop.

  1. Perform a blocking read on the event file descriptor:

    event_file.read(&mut event_buf);
  2. When an event occurs, such as data becoming available, the read on the event file will complete.

  3. Check the event_buf to see which file descriptor is active.

  4. Perform a non-blocking read on the active file descriptor.

  5. Process the data accordingly.

  6. If using a timer, write to the timer file descriptor to schedule the next event.

  7. Repeat the process.

Non-blocking Write

Sometimes write operations take time, especially when sending messages synchronously or writing to devices with limited buffers. The event scheme can also monitor file descriptors for write availability. If a file descriptor is used for both reading and writing, the program should register it twice with the event scheme—once for reading and once for writing.

Implementing Non-blocking Reads in a Scheme

If your scheme supports non-blocking reads for clients, it must integrate with the event scheme to handle client requests effectively.

  1. Wait for an event indicating activity on the scheme:

    event_file.read(&mut event_buf);
  2. Read the request packet from the scheme file descriptor:

    scheme_file.read(&mut packet);

    The packet will contain details about which file descriptor is being read and where the data should be copied.

  3. If the client's read would block, queue the request and return an EAGAIN error. Write the error response to the scheme file descriptor.

  4. When data becomes available, notify the client by sending an event to the scheme, specifying the handle ID that is active:

    scheme_file.write(&Packet { a: syscall::number::SYS_FEVENT, b: handle_id, ... });
  5. The kernel routes this response back to the client, posting the event on the client's event_fd, if one exists.

The scheme provider doesn't need to know whether the client has set up an event_fd; it must send the event regardless. Care should be taken to avoid sending unnecessary events, but race conditions can sometimes make this challenging. In well-written clients, extra events should not cause problems, but it’s important to avoid sending redundant events whenever possible.

Last updated