Siksha Sarovar

Siksha Sarovar (sikshasarovar.com) is a free educational web application that helps students in India learn programming and prepare for academic and competitive exams. The platform offers structured coding courses (C, C++, Python, Java, HTML, CSS, PHP, Power BI, AI, Machine Learning, Data Science), complete university curriculum notes for BCA/MCA students with previous year question papers, Class 10 and Class 12 CBSE/HBSE school notes, and dedicated preparation material for SSC, UPSC, Banking, Railway and other government exams. Browsing the site is completely free and requires no account. Users may optionally sign in with Google solely to save their learning progress, quiz scores and personal preferences across devices.

Privacy Policy | Terms of Service | Contact Siksha Sarovar | About Siksha Sarovar

v4.0.9 · PWA
Siksha Sarovar logo
Siksha Sarovar
Your Learning Universe

Siksha Sarovar is a free e-learning platform for coding courses, BCA university notes and competitive exam preparation. Optional Google sign-in saves your learning progress across devices.

Initializing knowledge base…
Compiling modules 0%

Unit 2: I/O Multiplexing — I/O Models, select, shutdown & poll

Lesson 7 of 15 in the free Network Programming notes on Siksha Sarovar, written by Rohit Jangra.

3.1 The five I/O models (the canonical comparison)

ModelWait for dataCopy data to processBlocks where?
Blocking I/Oprocess blocksprocess blocksthe whole call (default)
Non-blocking I/Opoll: EWOULDBLOCK loopblocks during copynowhere (but burns CPU polling)
I/O multiplexingblocks in select/pollblocks in recvfromselect — but on MANY fds at once
Signal-driven I/O (SIGIO)kernel signals readinessblocks in recvfromnowhere while waiting
Asynchronous I/O (aio_)kernel does both, then notifiestruly async (POSIX AIO)

The first four are synchronous (the process itself does the read); only the fifth is asynchronous. The two phases — waiting for data and copying it — are the axis of comparison; draw this 2-phase diagram in exams.

3.2 select — wait on many descriptors at once

int select(int maxfdp1, fd_set *readset, fd_set *writeset,
           fd_set *exceptset, struct timeval *timeout);
/* macros: FD_ZERO(&set) FD_SET(fd,&set) FD_CLR(fd,&set) FD_ISSET(fd,&set) */
  • maxfdp1 = highest descriptor + 1 (the classic off-by-one).
  • timeout: NULL = wait forever; {0,0} = poll; else bounded wait.
  • The sets are value-result — overwritten with the ready subset; rebuild them every loop.
  • A socket is read-ready when: data is queued (≥ SO_RCVLOWAT), peer sent FIN (read returns 0!), a listening socket has a completed connection, or a pending error exists.

Fixing the Unit-2 echo client. The old str_cli blocked in fgets and missed the server's FIN. The select version waits on stdin and the socket simultaneously:

void str_cli(FILE *fp, int sockfd) {
    int stdineof = 0; fd_set rset; char buf[MAXLINE]; int n;
    FD_ZERO(&rset);
    for (;;) {
        if (stdineof == 0) FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        select(max(fileno(fp), sockfd) + 1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(sockfd, &rset)) {            /* socket readable */
            if ((n = read(sockfd, buf, MAXLINE)) == 0) {
                if (stdineof == 1) return;         /* normal end       */
                else err_quit("server terminated prematurely");
            }
            write(fileno(stdout), buf, n);
        }
        if (FD_ISSET(fileno(fp), &rset)) {        /* stdin readable  */
            if ((n = read(fileno(fp), buf, MAXLINE)) == 0) {
                stdineof = 1;                      /* EOF on input    */
                shutdown(sockfd, SHUT_WR);         /* send FIN...     */
                FD_CLR(fileno(fp), &rset);
                continue;                          /* ...keep reading! */
            }
            writen(sockfd, buf, n);
        }
    }
}

3.3 shutdown vs close — and batch input

int shutdown(int sockfd, int howto);  /* SHUT_RD | SHUT_WR | SHUT_RDWR */
close(fd)shutdown(fd, SHUT_WR)
reference countingyes — FIN only when count hits 0ignores it — FIN now
directionbothone direction only (half-close)
can still read after?noyes — that's the point

Why the echo client needs SHUT_WR (batch input): pipe a file into the client (client < file). At EOF, a naive client exits — but the pipeline is still full of requests whose replies haven't arrived. shutdown(SHUT_WR) says "no more requests" (FIN) while the client keeps reading until the server's FIN arrives — draining every reply. Half-close exists precisely for this.

3.4 A select-based concurrent server — no fork at all

One process holds the listening socket and every connected socket in an fd_set; readiness drives the work:

/* core idea */
FD_SET(listenfd, &allset);
for (;;) {
    rset = allset;
    select(maxfd+1, &rset, NULL, NULL, NULL);
    if (FD_ISSET(listenfd, &rset))          /* new client → accept, add to allset */
    for (each client fd in rset)            /* data → read; 0 → close, FD_CLR    */
        ...
}

This is the architecture of classic single-threaded servers — and conceptually of every modern event loop (Node.js, nginx, Redis): one thread, thousands of sockets, readiness notifications. (A caution it inherits: one slow read handler stalls everyone — and a malicious client sending half a line can deny service to the rest; production loops add per-connection buffers and timeouts.)

3.5 poll — select without the bitmask limits

struct pollfd { int fd; short events; short revents; };
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout /* ms */);
  • Pass an array — no FD_SETSIZE ceiling, no maxfdp1.
  • events = what you ask (POLLIN, POLLOUT); revents = what happened (plus error bits POLLERR, POLLHUP, POLLNVAL that need no request) — not value-result, no per-loop rebuild.
  • timeout in milliseconds: −1 forever, 0 poll.

select vs poll vs beyond: select is the portable classic; poll scales the API; Linux epoll / BSD kqueue make readiness O(1) per event instead of O(n) per call — mention them as the modern endpoints of this lesson's lineage.

3.6 select vs poll vs epoll — the full comparison table

Criterionselectpollepoll / kqueue
fd limitFD_SETSIZE (typically 1024)none (array of pollfd)none
interest set passedevery call (sets are clobbered)every call (but revents separate)once (epoll_ctl); kernel keeps it
cost per callO(n) scan in kernel and userO(n) both sidesO(ready) — only ready fds returned
timeout granularitystruct timeval (µs)millisecondsmilliseconds (epoll_wait)
portabilityeverywhere, even WindowsPOSIXLinux-only / BSD-only
sweet spotsmall fd counts, max portabilitymedium counts, >1024 fdsthousands of mostly-idle connections (the C10K problem)

The architectural sentence to close a comparison answer: select/poll are stateless — you re-describe your interest every call and the kernel re-checks every descriptor; epoll is stateful — interest is registered once and the kernel reports only events, which is why its cost tracks activity rather than connection count.

3.7 When exactly is a socket writable? (the forgotten half of readiness)

Everyone learns the read-ready list; the write-ready conditions are the discriminating exam detail. A socket is write-ready when:

  1. there is room in the send buffer (free space ≥ SO_SNDLOWAT) and the socket is connected — note this says nothing about the network: writability means "the kernel will take your bytes", not "the peer will get them soon";
  2. the write half has been shut down — write would generate SIGPIPE (readiness includes "ready to fail"!);
  3. a socket using nonblocking connect has completed the connect (successfully or not — check SO_ERROR);
  4. a pending error exists (write will return −1 immediately).

Items 2 and 4 generalise to the rule worth memorising: "ready" means "the call will not block" — not "the call will succeed."

3.8 The full select-based echo server (filling in §3.4's sketch)

int maxfd = listenfd, client[FD_SETSIZE], maxi = -1;
fd_set allset, rset;
for (i = 0; i < FD_SETSIZE; i++) client[i] = -1;
FD_ZERO(&allset); FD_SET(listenfd, &allset);

for (;;) {
    rset = allset;                                   /* struct copy each loop */
    nready = select(maxfd + 1, &rset, NULL, NULL, NULL);

    if (FD_ISSET(listenfd, &rset)) {                 /* new client */
        connfd = accept(listenfd, (SA *)&cliaddr, &clilen);
        for (i = 0; i < FD_SETSIZE; i++)
            if (client[i] < 0) { client[i] = connfd; break; }
        FD_SET(connfd, &allset);
        if (connfd > maxfd) maxfd = connfd;
        if (i > maxi) maxi = i;
        if (--nready <= 0) continue;
    }
    for (i = 0; i <= maxi; i++) {                    /* data on a client? */
        if ((sockfd = client[i]) < 0) continue;
        if (FD_ISSET(sockfd, &rset)) {
            if ((n = read(sockfd, buf, MAXLINE)) == 0) {   /* client FIN */
                close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1;
            } else
                writen(sockfd, buf, n);
            if (--nready <= 0) break;
        }
    }
}

The bookkeeping is the lesson: allset holds long-term interest, rset is the per-loop disposable copy, client[] maps slots to descriptors, maxfd/maxi bound the scans, and nready short-circuits once all ready descriptors are handled. Every line answers a "why?" viva question.

3.9 Worked trace: batch input without shutdown — watching the bug

Run tcpclient < commands.txt where the file holds 10 lines. Without half-close: fgets hits EOF after line 10 while (say) replies 1–7 have arrived; a naive client calls close → FIN → the server's replies 8–10 race against the connection teardown and may meet a closed receive window → lost replies, no error shown. With shutdown(sockfd, SHUT_WR): FIN tells the server "no more requests", the client's read loop continues, replies 8–10 arrive, the server's FIN (read returns 0 with stdineof == 1) ends the program cleanly. Narrating this trace — what's in flight when EOF strikes — is the expected answer to "why is shutdown needed for batch input?", and it's also why the pipeline keeps full throughput: requests stream without waiting per-reply.

Exam pointers

  • "Explain the five I/O models with diagrams" — draw the two-phase (wait / copy) timeline per model; state that only AIO is asynchronous. This is the unit's most reliable long question.
  • "Compare select and poll" — §3.5 + the table above; the value-result-sets vs events/revents distinction is the key discriminator.
  • "Distinguish close and shutdown" — the §3.3 table verbatim, then the batch-input story as the why.
  • Likely code question: "modify str_cli to handle server termination while blocked on stdin" — the §3.2 select version is the model answer; be able to write it.

Check yourself

  1. Why must rset be re-copied from allset on every loop iteration in select but not re-built in poll?
  2. A listening socket becomes read-ready. What does "read" mean for it, and which call do you make?
  3. select says a socket is writable. Can write still block? Can it still fail? Reconcile with §3.7's rule.
  4. In the select server above, why track maxi and nready at all — what gets slower without them?
  5. Which I/O model does fcntl(fd, F_SETFL, O_NONBLOCK) + a read loop implement, and what is its CPU cost compared to select?