Discovering 'select'

Sid Shanker -

Over the last couple weeks, Elad Bogomolny and I have worked on building an ssh-like program that allows a client to access a remote server and run bash commands on it. We weren’t totally sure where to take this–but it ended being a pretty awesome project for getting better at systems programming.

ssh_demo

One of the unintended consequences of doing this project was learning about the select syscall in Linux. In this post, I’m going to cover the basics of why one might need to use it, and how.

Brief Summary

The ssh-like program we wrote has both a client and server component. The server simply waits for incoming connections, and when a client connects, the server forks. The forked child process becomes a bash processs, while the parent process enters a loop where it waits for bash commands to be sent over the network, and when bash commands come in, passes those to the child bash process. It then waits for the bash process to send back data, and when bash returns data, sends that data back over the network to the client.

Here’s some psuedo-code for the server process:

// server.c

accept() // accept connection from client

int result = fork()
if (result == 0) {
  // child process
  exec("bash");
} else {
  // parent process

  while(1) {
    data = read_from_network(); // blocks on the client
    write_data_to_bash_process(data);
    bash_result = read_from_child_process(); // blocks on bash process
    write_data_to_network(bash_result);
  }

}

And the client:

// client.c

connect_to_server();

while(1) {
  data = read_from_stdin(); // blocks on user input
  write_data_to_network(data);
  bash_result = read_from_network(); // blocks on server response
  print(bash_result);
}

Obviously, theres a lot more actually going on here, like encrypting the data, but this is a roughly how the program works.

A quick word on file descriptors

Before I go further, I want to briefly discuss the concept of “file descriptors”. In Unix-based operating systems, file descriptors are an abstraction that allow developers to interact with I/O in a consistent way, whether that I/O is reading/writing to a file, or reading/writing bytes over the network.

File descriptors are created through a number of system calls (“syscalls”). For instance, opening a file and opening up network connections both create file descriptors. Once you have access to a file descriptor, there are a number of syscalls that can operate on it, regardless of what the file descriptor is a reference to. For instance, the read and write syscalls are used, as you might guess, to read or write data to a particular file descriptor. As a developer, you don’t have to do anything differently based on what you are reading from or writing to–the OS takes care of it.

It’s also worth noting that file descriptors in Unix-based operating systems are each given a unique integer id. So, when you open a file, or make a network connection in C, the syscall for doing those operations will return a number to you that corresponds to the newly opened file descriptor.

Back to SSH!

Alright, so now that we know what file descriptors are, let’s go through an issue in the pseudocode I mention above, notably in this part:

data = read_from_network(); // blocks on the client
write_data_to_bash_process(data);
bash_result = read_from_child_process(); // blocks on bash process
write_data_to_network(bash_result);

What happens if we run a bash command that doesn’t return any output, like cd? The problem with this code is that if this is the case, we’ll block on the read_from_child_process() indefinitely, and no more commands will be able to be run.

Possible solutions?

We could put some kind of timeout on the read_from_child_process function call, and move on if the timeout is reached. This solution is problematic–either we set too short a timeout and this breaks bash commands that take a long time, or we set too long a timeout and commands that finish quickly, like cd, take way longer than they need to. It’s hard to distinguish between cases where the program is taking too long to run or whether it returns no output.

Another bad option would be to after every bash command, run a command that does return output, like echo. We could then chop off the result of echo when we return data to the client. This would work better than the previous solution, since every command will take the right amount of time. However, this breaks as soon as we want to do things like get the history of the shell, or get the return code of the previous command via echo $?.

What are some better options?

An option that doesn’t have the hacky compromises of the previous solutions is to instead of managing all of this in a single while loop, fork the process into two processes, one responsible for simply reading data out of the network socket and writing it to the bash process, and other responsible for reading bash responses and writing those back to the network.

To put this in pseudo-code:

// server.c
... // code to accept data from client

result = fork()
if (result == 0) {
  // child
  while (1) {
    data = read_from_network(); // blocks on the client
    write_data_to_bash_process(data);
  }

} else {
  // parent
  while (1) {
    bash_result = read_from_child_process(); // blocks on bash process
    write_data_to_network(bash_result);
  }
}

This has the drawback of not allowing us to block the client on responses from bash–the client will be able to continue sending data into the bash process before getting a response. This seems like a fine compromise–the actual ssh doesn’t even do this.

Enter: Select

While this is a pretty good solution, it does have the added cost of requiring another process or thread to be running on the server. Can we do better?

It turns out that Linux has a syscall to solve this very problem, select. select allows you to “watch” multiple file descriptors, and returns if any of them is ready to be read from. In our case, we have two fairly independent processes that each block on a file descriptor, for the bash reader, it blocks on bash, and for the network reader, it blocks on a network socket. Each process, when data is available on the file descriptor it is reading from, then executes some logic.

With select, we could instead of using multiple processes, in a single process pass both the file descriptor to the bash process and the file descriptor to the network socket to the call to select. When data is available on either of those file descriptors, select will return. We can then check which descriptor has data on it, and handle it appropriately. If we put this in a while loop, we no longer need to have multiple processes for handling this.

Basic Usage

The basic setup for using select is as follows:

/* select_example.c */

/*  These macros are used for managing a set of file descriptors */

fd_set read_file_descriptors; // object for managing sets of file descriptors

while (1) {
  FD_ZERO(&rfds);
  FD_SET(bash_fd, &read_file_descriptors);
  FD_SET(network_fd, &read_file_descriptors);
  select(highestFileDescriptor, &read_file_descriptors, NULL, NULL, NULL);
  if(FD_ISSET(bash_fd, &read_file_descriptors)) {
    ... // handle case when data is available to read from bash process
  }
  if(FD_ISSET(network_fd, &read_file_descriptors)) {
    ... // handle case when data is available to read from network
  }
}

Linux provides a number of macros that are helpful for setting up the call to select. The short of it is that you call FD_SET to add file descriptors that you care about to the list of read_file_descriptors, and then when you call select, you supply the file descriptor that is the highest number and the set of file descriptors you care to read from.

After select returns, select will have modified the state of the file descriptor in the set, and you can check it to see which ones are available for reading.

It’s worth also noting here that select can also be passed an fd_set of write file descriptors. If you pass these, select will return when those file descriptors are available for writing, rather than reading.

Conclusion

select is a very powerful syscall–the notion of being able to wait on multiple channels before executing some logic is useful in a bunch of other contexts besides the simple ssh client/server implementation I’m using here.

For instance, if you have a chat server, you can use select to manage multiple connections on a single process. Each connection will be represented by a different file descriptors, and through use of select, you can handle messages from each client when data is available to read from each of them, rather than having to have multiple threads that each block on a client.

If you’re interested in systems programming and want to learn more about select and other related syscalls, I’d recommend checking out this blog post, and reading the man page.

Thanks for reading!

Sid Shanker <sid.p.shanker at gmail.com>