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 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.
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.
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:
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.
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.
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_SOURCE
before it includes any
header files).
A straightforward way to manage the pollfd
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
socket()
, bind()
, and listen()
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.
POLLRDHUP
event 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 (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.
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:epoll_create1()
system call (not the now deprecated epoll_create()
system call).
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).
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.
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.