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:
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 you finish them each and every person who worked on them should please log into Canvas, select this course in this semester, and then upload a file containing them and also upload any other files the assignment asks for, and submit those files for this studio assignment (there should be a separate submission from each person who worked together on them).
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.
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
As the answer to this exercise, please show the output of your program.
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:
listen()system calls, as described in the Linux Pipes, FIFOs, and Sockets studio. Your program should use domain
AF_INETso 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
ifconfigcommand 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.
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
NULLas 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.
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):
poll()as its I/O multiplexer instead of
select()(please see pages 1340-1341 of the LPI text book for an illustration of how to use
poll()). For read events from the standard input stream, the socket on which it accepts connections, or a connected client socket, the server should use
POLLIN, and for disconnection events on the client socket the server should use
POLLRDHUP(note that in order to use
POLLRDHUP, your program will need to say
#define _GNU_SOURCEbefore it includes any header files).
A straightforward way to manage the
structs for these event sources is to put them into an array with the
fd and events for the standard input stream at position 0, the socket
on which the server listens for (and accepts) connection requests at
position 1 (if it is successfully established), and the fd and events
for a connected client socket at position 2. Then, you can use an
integer variable (which you also will pass into the call to
poll()) to indicate how many (and thus, because of their ordering
in the array, which) of these event sources
are active - it will start off with value 1 since the standard
input stream is always available; it will increase to 2 once the
calls have succeeded, and then it will increase to 3 when a client
socket connection is successfully established and decrease to 2
when that connection is closed.
accept()system call to the watched list for input events. Your server should then use
poll()to wait to read data from the client, store the data when it is ready and has been read, and print it to the screen when a specific delimiter is reached (for example, the line break character). Your server could print the delimiter-separated messages one by one as it receives them from the client.
POLLRDHUPevent on the client connection, it will close the socket and then decrement the number of fds on which it will poll (leaving only the fds for the standard input stream and the socket on which it accepts connections active).
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 (
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.
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:
epoll_create1()system call (not the now deprecated
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).
EPOLLINflag 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.
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
EPOLLET flags as the event types for the standard input when you call
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.