CSE 422S: Studio 19

I/O Event Handling

Multiplexed I/O allows an application to concurrently block on multiple file descriptors and receive notification when any one of them becomes ready to read or write without blocking.

—Robert Love, Linux System Programming, 2nd Edition, Chapter 2, pp. 52.

I/O multiplexing mechanisms such as select and poll allow a program thread to block synchronously on multiple file descriptors at once. These mechanisms are especially useful for networked applications where a server must handle many concurrent connections from clients. This approach has advantages over purely multithreaded/multiprocess approaches in which a thread/process is launched to handle each file descriptor, since each thread/process requires additional memory, context switching overhead, etc. which may not scale well in handling large numbers of concurrent connections. Linux further improves event multiplexing with the epoll I/O multiplexing models and mechanisms.

In this studio, you will:

  1. Use select, poll, and epoll to multiplex I/O events from multiple file descriptors, including sockets
  2. Explore two event notification models offered by epoll

Please complete the required exercises below, as well as any optional enrichment exercises that you wish to complete. We encourage you to please work in groups of 2 or 3 people on each studio (and the groups are allowed to change from studio to studio) though if you would prefer to complete any studio by yourself that is allowed.

As you work through these exercises, please record your answers, and when finished upload them along with the relevant source code to the appropriate spot on Canvas. If you work in a group with other people, only one of you should please upload the answers (and any other files requested) for the studio, and if you need to resubmit (e.g., if a studio were to be marked incomplete when we grade it) the same person who submitted the studio originally should please do that.

Make sure that the name of each person who worked on these exercises is listed in the first answer, and make sure you number each of your responses so it is easy to match your responses with each exercise.

Required Exercises

  1. As the answer to the first exercise, list the names of the people who worked together on this studio.

  2. In this exercise we will use the select() system call with a single file descriptor, for the standard input stream. Even though handling just a single file descriptor isn't what select() is designed for, this helps us get acquainted with this system call. We will write a program that uses select() to watch for data (i.e., input from the keyboard) from the standard input stream (STDIN_FILENO). When the standard input stream becomes ready, your program will read the data and print it out to the screen. Your program should repeatedly invoke select() to watch for new input and only exit when receives the string "quit" from the standard input stream (note that you may need to have your code ignore additional characters after that string, in order to make this work correctly). Page 1335 of the LPI text book (Kerrisk) provides an example of how to use select(), which may be helpful to get started on this exercise.

    Note: The file descriptor sets that you pass to select() may be modified after it returns. Thus, you should keep extra unchanged copies of them and/or (re)initialize them before every invocation of select().

    As the answer to this exercise, please show the output of your program.

  3. In this exercise, we will extend your program by using the select() system call to multiplex multiple file descriptors. Your program will still watch for the readiness of the standard input stream, while it works as a server to handle requests from clients. For the network IPC functionality, you may want to reuse portions of your client and server code from the Linux Pipes, FIFOs, and Sockets studio.

    Your server should perform the following actions:

    1. First, create a socket that will listen for new connections from clients with the socket(), bind(), and listen() system calls, as described in the Linux Pipes, FIFOs, and Sockets studio. Your program should use domain AF_INET so that it can work remotely. We will still use the connection type SOCK_STREAM, and protocol zero. If you want your server to listen at a specific IP address, you can use the ifconfig command from a terminal window, to see the list of network interfaces enabled in the current machine and choose one. Your client should use that same IP address and port to connect to the server.
    2. Second, in a loop, watch both the standard input stream (STDIN_FILENO) and the server's listening socket via the select() system call. For both of them, we will only watch for read events. The calls to select() should block until an event occurs and you should pass NULL as the last argument for select(). If data from the standard input stream is ready, read and print it to the screen as before. If there is a connection request from a client, select() will also return: in this case, your program should accept the client's connection request by calling the accept() system call. This invocation of accept() should return immediately (without blocking). Your server should send a message (for example, it may include the server's hostname, the current time on the server's machine, etc.) to the client, and then close the connection.

    Please also write a client program that connects to the server using the chosen IP address and port, reads the message sent from the server, prints it out to the screen, and exits.

    Build and run your server and client, and as the answer to this exercise, please show the output for each of them.

    Before proceeding, please make separate copies of your client and server code - we will extend the new copies in the next exercise, and then make another copy of the server and update it to use an entirely different mechanism in the last exercise.

  4. One of the disadvantages of the select() system call is that the arguments must be initialized for every invocation. In this exercise, we avoid this by using the poll() system call instead. For this exercise, please make the following changes to the new copies of your server and client (to simplify the data structures in this exercise, you can assume that only a single client at a time is connected, and that there is a maximum length of a (delimited)string that a client can send to the server):

    Because either (1) POLLRDHUP or (2) POLLIN or (3) both or (4) neither of them may be enabled for the client socket fd when poll() returns, it is important that your code handle all four of these cases correctly. One straightforward way to do this is to check first whether either of them is enabled (POLLRDHUP | POLLIN) and then within that branch of the code check for (and handle) each of them separately.

    As the answer to this exercise, please show the output for your server and client.

  5. In the last exercise, we will look at two different notification models for I/O multiplexing, namely level-triggered notification and edge-triggered notification. We will use Linux's epoll_create1(), epoll_ctl(), and epoll_wait() system calls and a specific flag (EPOLLET) to control the notification mode (please see pages 1362-1363 of the LPI text book for an illustration of how to use these features).

    Please make a new copy of your poll server code, to modify in this exercise.

    In that new copy of your server:
    1. Create an epoll instance using the new epoll_create1() system call (not the now deprecated epoll_create() system call).
    2. Add the standard input stream to the list of watched file descriptors of the epoll instance created in the previous step, using the epoll_ctl() system call. Do the same thing for the socket on which to accept connections, and the client socket when it is connected (when a client connection is closed, your server should remove its file descriptor from the list that is watched by the epoll instance).
    3. The event type we would like to watch for is an input event. Thus, your program should use the EPOLLIN flag for all three event sources (the standard input stream, the socket on which to accept connections, and the client socket when it is connected). For the client socket, the server should also use the EPOLLRDHUP flag to watch for that socket connection being closed.
    4. In a loop, repeatedly wait for input to be ready with the epoll_wait() system call, but do NOT consume the available data. Instead, simply print out a message saying that data is available (to end your server program, simply hit Ctrl-C).

    Compile and run the program, including connecting to it from your client from the previous exercise, and save the output from those runs.

    Update step 6.b above by using both EPOLLIN and EPOLLET flags as the event types for the standard input when you call epoll_ctl(). The EPOLLET flag enables edge-triggered notification mode for the standard input. Without this flag, epoll enables the default level-triggered notification mode. Compile and run the program again, including connecting to it from your client.

    As the answer to this exercise, please show the outputs of these two different runs, and explain briefly how (and why) they behave differently.

    Things to Turn In: