The Problem with close()
When you call close() on a socket, both directions of communication are shut down immediately — you can no longer send OR receive. For many protocols this is fine, but sometimes you want to signal to the peer that you are done sending, while still being able to receive the peer’s response. This is called a half-close.
There is also a subtle problem: if the same socket is referenced by multiple file descriptors (after fork() or dup()), close() only decrements the reference count and does not actually shut down the connection until the last descriptor is closed. shutdown() acts on the socket immediately regardless of the reference count.
The shutdown() System Call
#include <sys/socket.h>
int shutdown(int sockfd, int how);
/* Returns 0 on success, -1 on error */
The how argument controls which direction is shut down:
| how value | Effect | Peer sees |
|---|---|---|
| SHUT_RD (0) | Close reading half. Further reads return 0 (EOF). | Nothing — peer can still write |
| SHUT_WR (1) | Close writing half. Further writes fail. Sends FIN to peer. | EOF on their read end |
| SHUT_RDWR (2) | Close both halves. Equivalent to close() on connection. | EOF on their read end |
close() vs shutdown() — Key Differences
This is the most important difference and a common source of bugs in multi-process servers:
/* After fork(), parent and child both hold a copy of the socket fd */
pid_t pid = fork();
if (pid == 0) {
/* Child process handles the connection */
close(listen_fd); /* Good: child does not need listener */
/* ... process request ... */
close(conn_fd);
/*
* close() here only decrements reference count from 2 to 1.
* The connection is NOT terminated! Parent still holds it.
* Peer will NOT receive FIN yet.
*/
exit(0);
}
/* Parent: close its copy of conn_fd */
close(conn_fd);
/*
* Now reference count drops to 0 — ONLY NOW does FIN get sent.
* The connection is finally terminated.
*/
With shutdown(), termination happens immediately regardless of the reference count:
/* In the child process */
shutdown(conn_fd, SHUT_RDWR);
/*
* This shuts down the connection NOW, even if parent still has a copy.
* FIN is sent to peer immediately.
*/
close(conn_fd); /* Cleanup the descriptor */
Practical Use: Implementing pipe() with socketpair()
socketpair() creates two connected sockets (both bidirectional). We can use shutdown() to make them unidirectional, simulating a traditional pipe():
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
/*
* my_pipe - Creates a unidirectional pipe using socketpair() + shutdown().
*
* pipefd[0] — read end
* pipefd[1] — write end
*
* Returns 0 on success, -1 on error.
*/
int my_pipe(int pipefd[2])
{
int sv[2];
/* Create a bidirectional socket pair */
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1)
return -1;
/*
* sv[0] = read end: shut down writing
* sv[1] = write end: shut down reading
*
* Now data can only flow from sv[1] --> sv[0]
* exactly like a pipe(pipefd) where pipefd[0]=read, pipefd[1]=write
*/
if (shutdown(sv[0], SHUT_WR) == -1 ||
shutdown(sv[1], SHUT_RD) == -1) {
close(sv[0]);
close(sv[1]);
return -1;
}
pipefd[0] = sv[0]; /* read end */
pipefd[1] = sv[1]; /* write end */
return 0;
}
int main(void)
{
int pipefd[2];
char buf[64];
ssize_t nr;
if (my_pipe(pipefd) == -1) {
perror("my_pipe");
return 1;
}
/* Write to write-end */
const char *msg = "Hello from socketpair-based pipe!";
write(pipefd[1], msg, strlen(msg));
close(pipefd[1]); /* Signal EOF to reader */
/* Read from read-end */
nr = read(pipefd[0], buf, sizeof(buf) - 1);
buf[nr] = '\0';
printf("Read: %s\n", buf);
close(pipefd[0]);
return 0;
}
This works because shutdown(sv[0], SHUT_WR) closes the write half of sv[0], and shutdown(sv[1], SHUT_RD) closes the read half of sv[1]. Together, data can only flow one way.
Real-World Pattern: Half-Close for Protocol Signalling
Many text protocols (like HTTP/1.0, simple FTP data transfers, or custom protocols) use this pattern: the client sends all its data, then sends a half-close to signal “I am done sending”, while it still wants to read the server’s response.
#include <sys/socket.h>
#include <unistd.h>
void send_request_and_read_response(int sockfd,
const char *request,
size_t req_len)
{
char buf[4096];
ssize_t nr;
/* 1. Send the entire request */
write(sockfd, request, req_len);
/*
* 2. Signal EOF on the write half.
* Server sees EOF and knows request is complete.
* We can still read from sockfd.
*/
shutdown(sockfd, SHUT_WR);
/* 3. Read the server's response until it also closes */
while ((nr = read(sockfd, buf, sizeof(buf))) > 0) {
/* process buf[0..nr-1] */
write(STDOUT_FILENO, buf, nr);
}
/* 4. Close our file descriptor */
close(sockfd);
}
Without shutdown(sockfd, SHUT_WR), the server would block waiting for more data because it would not know the request was complete.
Interview Questions
Answer: close() decrements the file descriptor reference count and only shuts down the connection when the count reaches zero (relevant after fork()). shutdown() shuts down the connection immediately regardless of reference count. Also, shutdown() lets you close only one direction (half-close), whereas close() always closes both directions.
Answer: The peer’s read() returns 0 (EOF). This is how you signal to the peer that you have finished sending data. The peer can still send data back to you — you can still call read() on your end after calling shutdown(fd, SHUT_WR).
Answer: SHUT_RD is useful when you want to stop reading from a socket even though the peer may still be sending. After SHUT_RD, any further read() calls return 0 immediately. This can be used to signal to the application that it should stop processing incoming data, for example when implementing a timeout or abort scenario. The peer does not receive any notification that you stopped reading.
Answer: After fork(), both parent and child have open file descriptors referring to the same socket. The kernel’s internal socket structure has a reference count of 2. When the child calls close(conn_fd), the count drops to 1. The connection only terminates when the count reaches 0, which requires the parent to also close its copy. Using shutdown() in the child bypasses this and terminates the connection immediately.
Answer: Yes. Call socketpair(AF_UNIX, SOCK_STREAM, 0, sv) to get two connected bidirectional sockets. Then call shutdown(sv[0], SHUT_WR) to close the write end of sv[0] and shutdown(sv[1], SHUT_RD) to close the read end of sv[1]. Now sv[0] is read-only and sv[1] is write-only, exactly like a pipe.
