In the basic client-server model, the server handles only one client at a time, which is practically a hindrance to build any scalable system.
There are couple of strategies to handle multiple client at a time:
- Multi-threading approach
- I/O multiplexing (select)
Multi-threaded approach:
- This is to spawn a new thread for each client request and handles all its traffic. This way the main thread is not blocked and is free to handle new requests.
- It has few limitations in terms of system overhead because of frequent context switching, difficulty in debugging a thread-based application, and also not scalable for a large number of clients.
I/O Multiplexing:
With I/O multiplexing, a call is made to select() and poll() API and block on one of these system calls, instead of actual I/O system call( recv, recvfrom etc..)

select() API
select() or pselect() API is locked, waiting for the datagram socket to be readable. When select() or pselect() API returns that the socket is readable, then call recvfrom() to copy the datagram into an application buffer.
select() API:
int select(int maxfdp1, fd_set *readfds, fd_set *writefds,fd_set *errorfds ,struct timeval *timeout) struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }
- In this approach the server is made non-blocking and is made to monitor multiple file descriptors at a time.
- It waits until one or more file descriptor becomes ready for some kind of I/O operations or until the specified amount of time has elapsed.
- It(select()) works like an interrupt handler, which gets activated as soon as any file descriptor sends any data.
- The first argument maxfdp1 specifies the number of descriptors to be tested.
- It monitors three independent sets of file descriptors: read, write and error FDs.
- Those listed in readfds will be monitored to see if any characters become available for reading that is more precisely that read will not block; even when file descriptor is also on end-of-file.
- Those listed in writefds will be watched to see if a write will not block and those in errorfds will be watched for errors or exceptions.
There are three possibilities for timeout:
- wait forever: the time specified is a NULL pointer. Return only when any one of the FD is ready.
- wait for a fixed amount of time: Returns when one of the file descriptors is ready but not wait beyond the specified timeout time.
- Do not wait at all: When the timer value is 0. Return immediately after checking the descriptors. This is called polling.
Return value:
- It returns -1 on failure and a total number of file descriptor set that is a total number of bits that are set in readfds, writefds and errorfds.
- Return value is 0 if the select() has timeout.
Changes:
It changes the descriptor lists, when we call the function, we specify the values of descriptors to be tested and on return, it specifies which descriptors are ready. The corresponding bit in the descriptor array is set.

Descriptor sets for select/pselect
Functions associated with data structure fd_set There are four functions associated:
- Clear an fd_set:
FD_ZERO (fd_set * fds); /*clear all bits in fdset/*
Example: FD_ZERO (&writefds)
- Add a descriptor to an fd_set:
FD_SET (int fd, fd_set *fds) /*turn on the bit for fd in fdset */
Example: FD_SET ( sock_fd, &writefds);
- Remove a descriptor from an fd_set
FD_CLR (int fd, fd_set *fds) /* turn off the bit for fd in fdset */
Example: FD_CLR ( sock_fd, &writefds);
- To check if the descriptor is set in given fd_set, if set its a new connection
FD_ISSET (int fd, fd_set *fds); /* is the bit for fd on in fdset ? */
Example: FD_ISSET (sock_fd, &writefds); Returns 1 if it is set else 0.
pselect() API:
int pselect(int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timespec *timeout, sigset_t *sigmask) struct timespec{ long tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }
- pselect() API is useful when the server need to wait for signal(s) as well as file descriptor(s) to become ready.
- select() API is only associated with file descriptors not signals.
- In other aspect pselect() API is similar to select() API except for few major differences listed below:
There is three basic difference between select() and pselect() API:
- select() uses a timeout that is a struct timeval (with seconds and microseconds), while pselect() uses a struct timespec (with seconds and nanoseconds).
- select() may update the timeout argument to indicate how much time was left. pselect() does not change this argument.
- select() has no sigmask argument, and behaves as pselect() called with NULL sigmask.
- select() API just wait for file descriptor to be ready while pselect() waits for signals delivery as well file descriptor(s). select() ignores signal.
Importance of sigmask argument:
- This makes the pselect() API also wait for signals delivery along with file descriptor(s).
- This argument holds a set of signals that the kernel should unblock()( i.e., remove from the signal mask of the calling thread), while the caller is blocked inside the pselect() call.
- Upon call, the current mask(origmask) of the calling thread is overwritten with the new mask(sigmask) and upon return, the original mask is restored.
- If this argument is NULL, it behaves just like select() API.
- There is a race condition between signals and select() API but pselect() makes sure that unmasking the signals that need to be tested and pselect() is atomic in nature.
ready = pselect(maxfdp1, &readfds, &writefds, &errorfds, timeout, &sigmask); /* is equivalent to atomically executing the following calls but practically its not feasible as separate system calls are not executed as an atomic unit */ sigset_t origmask; pthread_sigmask(SIG_SETMASK, &sigmask, &origmask); ready = select(nfds, &readfds, &writefds, &errorfds, timeout); pthread_sigmask(SIG_SETMASK, &origmask, NULL);
- pselect() makes sure that signal is only unblocked once control has passed to the kernel that is atomicity is achieved.
Relevant Posts:
- Sockets Introduction
- Socket APIs
- TCP Socket Program
- UDP Socket Program
- Socket Options
- Host and Network Byte ordering
- Little-endian and big-endian
Reference:
- https://lwn.net/Articles/176911/
- https://stackoverflow.com/questions/9774986/linux-select-vs-ppoll-vs-pselect
- https://notes.shichao.io/unp/ch6/
Categories: Operating system (OS)
Leave a Reply