The shutdown() System Call

 

Part 1: The shutdown() System Call
Chapter 61 · Sockets: Advanced Topics · TLPI

Why Do We Need shutdown()?

When you are done with a socket, you normally call close(). That closes the file descriptor and shuts down both directions — you can no longer read from or write to the socket. That works fine for most programs.

But what if you want to say “I am done sending data, but I still want to receive the reply”? That is where shutdown() comes in. It lets you close just one direction of the connection — either the sending side or the receiving side — while keeping the other direction alive.

This is called a half-close. It is very useful for protocols like TCP echo, file transfer, or any request-response protocol where you want to signal “end of input” to the remote side without hanging up the whole connection.

close() vs shutdown() — What is the Difference?

Feature close() shutdown()
What it closes Both read AND write directions You choose: read, write, or both
File descriptor Closed (freed) Stays open — you must still call close()
Duplicate fds (fork/dup) Only decrements refcount; socket stays up until all copies closed Immediately affects the socket regardless of how many copies exist
Peer sees EOF (eventually, when last fd closed) EOF immediately on the shut-down direction
Use case Normal connection teardown Half-close, or forcing immediate EOF with dup’d fds

The shutdown() API

#include <sys/socket.h>

int shutdown(int sockfd, int how);
/* Returns 0 on success, -1 on error */

The how argument controls which direction to shut down:

SHUT_RD
Close the reading end.

Further reads on this socket return 0 (EOF).
Data in the socket buffer is discarded.
The peer can still write — you just won’t see it.

SHUT_WR
Close the writing end.

You can no longer send data.
The peer receives a FIN (TCP EOF signal).
You can still read replies that come back.

SHUT_RDWR
Close both directions.

Equivalent to SHUT_RD + SHUT_WR.
This is the nuclear option — affects even dup’d fds.
File descriptor itself is NOT closed.

Key Insight — SHUT_RDWR vs close():

Suppose you called dup(sockfd) or fork() — now two file descriptors point to the same socket. Calling close() on one only decrements the reference count. The socket stays alive until the last fd is closed. But shutdown(sockfd, SHUT_RDWR) kills the connection immediately, regardless of how many file descriptors still refer to it.

shutdown() with dup() — A Critical Difference

This is a commonly asked interview topic. Consider this scenario:

/* Case 1: Using dup() — close() is NOT enough to signal EOF */

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* ... connect ... */

int fd2 = dup(sockfd);   /* Now two fds point to the same socket */

close(sockfd);           /* Only decrements refcount. Socket still alive! */
                         /* Peer does NOT see EOF yet. */

close(fd2);              /* NOW the socket closes. Peer sees EOF. */

/* --------------- */

/* Case 2: Using shutdown() — EOF is immediate */

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* ... connect ... */

int fd2 = dup(sockfd);

/* Shut down both directions immediately — affects all fds */
shutdown(sockfd, SHUT_RDWR);
/* Peer sees EOF NOW, even though fd2 is still open */

close(sockfd);  /* clean up fd1 */
close(fd2);     /* clean up fd2 */

shutdown() with fork() — Same Problem

The same issue arises when a socket is shared between parent and child after a fork(). Each process has its own copy of the file descriptor, so calling close() in one process does not close the socket.

pid_t pid = fork();

if (pid == 0) {
    /* Child process — let's shut down the write side */
    shutdown(sockfd, SHUT_RDWR);
    /* Now BOTH parent and child can no longer do I/O on sockfd */
    /* The peer sees EOF immediately */
    exit(EXIT_SUCCESS);
}

/* Parent process — sockfd is also affected by the child's shutdown() */
/* Trying to read or write here will fail */

Practical Example: TCP Echo Client with SHUT_WR

A great real-world example of shutdown() is a TCP echo client. The echo server just sends back whatever the client sends. The client wants to:

1. Send all of stdin to the server.
2. Signal “I am done sending” (so the server knows to stop).
3. Still read the server’s echo back on stdout.

If you called close() after writing, you would lose the ability to read the response. shutdown(sfd, SHUT_WR) is the right tool here.

/* Simplified TCP echo client demonstrating SHUT_WR */
/* Based on TLPI Listing 61-2 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>

#define BUF_SIZE 100

int main(int argc, char *argv[])
{
    int sfd;
    ssize_t numRead;
    char buf[BUF_SIZE];

    /* Connect to echo server (connection setup omitted for brevity) */
    sfd = connect_to_echo_server(argv[1]);

    switch (fork()) {

    case -1:
        /* fork failed */
        perror("fork");
        exit(EXIT_FAILURE);

    case 0:
        /* ===== CHILD: Read server response, print to stdout ===== */
        for (;;) {
            numRead = read(sfd, buf, BUF_SIZE);
            if (numRead <= 0)   /* EOF or error — server closed */
                break;
            printf("%.*s", (int)numRead, buf);
        }
        exit(EXIT_SUCCESS);

    default:
        /* ===== PARENT: Send stdin to server ===== */
        for (;;) {
            numRead = read(STDIN_FILENO, buf, BUF_SIZE);
            if (numRead <= 0)   /* EOF on stdin — done reading input */
                break;
            if (write(sfd, buf, numRead) != numRead) {
                fprintf(stderr, "write() failed\n");
                exit(EXIT_FAILURE);
            }
        }

        /*
         * CRITICAL: Signal EOF to server, but do NOT close the socket.
         * The child still needs to read the server's reply.
         * shutdown(SHUT_WR) sends a FIN to the server.
         * The server reads EOF and then closes its end.
         * The child then reads EOF and exits.
         */
        if (shutdown(sfd, SHUT_WR) == -1) {
            perror("shutdown");
            exit(EXIT_FAILURE);
        }

        exit(EXIT_SUCCESS);
    }
}

How Data Flows — Half-Close Visualized

CLIENT
(Parent writes stdin → socket)
→ data →
← echo ←
SERVER
(Reads data, echoes it back)
After parent calls shutdown(sfd, SHUT_WR):
→ Server receives EOF (FIN packet) and closes its socket
→ Child (still reading) gets EOF from server and exits

Remember: shutdown() does NOT close the file descriptor. Even if you call shutdown(sfd, SHUT_RDWR), you must still call close(sfd) to release the file descriptor.

Quick Reference

how Shuts Down Peer Sees You Can Still
SHUT_RD Reading Nothing (you just stop reading) Write to socket
SHUT_WR Writing EOF (FIN packet in TCP) Read from socket
SHUT_RDWR Both directions EOF immediately Nothing (must close() fd)

Interview Questions & Answers

Q1. What is the difference between close() and shutdown() on a socket?
A: close() decrements the file descriptor reference count and only actually closes the socket when the count reaches zero (important with dup/fork). shutdown() immediately affects the underlying socket regardless of how many file descriptors reference it. Also, shutdown() can selectively close just one direction (read or write), while close() always closes both. Finally, shutdown() does NOT release the file descriptor — you must still call close() for that.
Q2. What is a “half-close” in TCP and why is it useful?
A: A half-close means one direction of the TCP connection is shut down while the other remains open. For example, after calling shutdown(sfd, SHUT_WR), you can no longer send data but can still receive. This is useful in request-response protocols where a client wants to signal “I sent everything” (peer sees EOF / FIN) while still waiting for the server’s response.
Q3. After calling fork(), a parent and child share a socket fd. If the child calls close(sockfd), does the parent lose the connection?
A: No. After fork(), both parent and child have a file descriptor referring to the same socket. Calling close() in the child only decrements the reference count by 1. The socket remains open for the parent. The connection is only truly closed when all file descriptors referring to it are closed. However, if the child calls shutdown(sockfd, SHUT_RDWR), the socket is immediately affected and the parent will also lose the connection.
Q4. In an echo client that uses fork(), why does the parent call shutdown(SHUT_WR) instead of close() after writing stdin to the socket?
A: The parent uses shutdown(sfd, SHUT_WR) to send a FIN to the echo server, signaling end-of-input. This causes the server to close its socket, which in turn sends a FIN back to the client’s child process. The child can then detect EOF and exit cleanly. If the parent called close(sfd) instead, the file descriptor would still be open in the child process, so the reference count would not reach zero and the server would never see EOF. shutdown() bypasses reference counting and signals the peer immediately.
Q5. Does shutdown() close the file descriptor?
A: No. shutdown() only controls the I/O capability of the socket. Even shutdown(sfd, SHUT_RDWR) leaves the file descriptor open. You must call close(sfd) separately to release the file descriptor.

Free Embedded & Linux Tutorials

EmbeddedPathashala — Learn Linux System Programming, BLE, and Embedded Systems for free.

Visit EmbeddedPathashala

Leave a Reply

Your email address will not be published. Required fields are marked *