1. Overview
Sockets play a critical role as the endpoints for sending and receiving data across a network. They form the backbone of many Internet applications, enabling communication between clients and servers. Whether it’s a web browser fetching a webpage, an email client retrieving messages, or a streaming service delivering content, sockets are integral to these operations.
However, what happens when an application using a socket is abruptly terminated? This scenario is common in real-world computing environments. For instance, applications may crash, be forcefully killed by users, or be terminated by the system.
In this tutorial, we’ll learn how Linux decides to close a socket when an application on one end is killed. We’ll explore the impact of process termination on sockets and the detailed mechanisms employed by the Linux kernel to handle these situations.
2. Linux Connection Handling
Network connections in Linux are primarily handled by the kernel, which is responsible for overseeing the creation, maintenance, and termination of sockets. The kernel’s networking subsystem, or network stack, handles the complexities of data transmission, ensuring correct routing of packets, retransmission if lost, and reassembly upon arrival.
From the moment of creating a socket to its point of closing, the kernel ensures that all operations adhere to the relevant networking protocols and standards.
The key components that handle this process are:
- Network Stack – facilitates communication between different networking protocols, handling physical transmission, logical addressing, and reliable data transfer.
- Socket Layer – provides APIs for user-space applications to interact with the network stack.
- Transport Layer Protocols (TCP, UDP) – TCP is a connection-oriented protocol ensuring reliable data transfer through a three-way handshake, checksums, and retransmissions. UDP, in contrast, is a connectionless protocol offering faster but less reliable data transfer, suitable for applications like streaming or gaming.
These components collectively ensure the smooth flow of data between applications and network interfaces.
3. Linux Kernel’s Role in Connection Closing
When an application using a network connection is terminated, Linux doesn’t simply abandon the connection. Instead, the kernel steps in to ensure a graceful and controlled closure, following the established TCP protocol. This helps prevent data loss and maintain a healthy network environment.
Let’s consider two scenarios where the Linux kernel takes charge of connection closing.
3.1. Process Termination
When an application using a socket is terminated normally, using signals like SIGTERM or SIGINT, or when it crashes, the kernel attempts to gracefully close the socket connection. It achieves this by sending a FIN (finish) packet to the remote end, notifying it that the application is no longer sending data.
The kernel then waits for an ACK (acknowledgment) packet from the remote end, confirming receipt of the FIN packet. Once acknowledged, the kernel can then safely close the socket and release associated resources.
3.2. Application Termination Without the close() Function Call
Sometimes, an application might inadvertently close its socket using methods other than the standard close() function. This could be due to programming errors or unexpected termination scenarios.
In such cases, the orphaned socket remains open until the kernel detects its abandoned state. After a configurable timeout period, the kernel initiates a graceful closing process similar to the first scenario, sending a FIN packet and waiting for an ACK before finally closing the socket.
This proactive behavior of the Linux kernel ensures that it terminates network connections cleanly, even when applications don’t follow the recommended procedures. However, it’s important to note that the brutal SIGKILL signal bypasses normal cleanup routines. Consequently, we might not always observe a graceful close in such cases.
4. Why Connections Might Remain ESTABLISHED
Even though the Linux kernel manages connection closing, we might occasionally see connections lingering in the ESTABLISHED state with netstat. Here are some reasons why this might happen:
- Missing close() call by the application – The kernel will eventually detect the orphaned socket, but it might take some time. This can lead to a temporary ESTABLISHED state until the kernel initiates the closing process.
- Network issues causing packet loss or delays – For instance, a lost FIN packet might not be retransmitted immediately, causing the connection to remain in the ESTABLISHED state while waiting for a retransmission or timeout.
- Remote server configuration (holding onto connections) – Some remote servers might be configured to hold onto connections for a specific period after receiving a FIN. This allows them to handle potential retransmissions or delayed acknowledgments gracefully.
- TCP keepalive retries and timeouts – TCP keepalive works by sending periodic probes to the remote host. If the remote host is unreachable for an extended period, and the keepalive timeout value is very high (several minutes or hours), the connection might appear stuck in the ESTABLISHED state as retries continue.
To minimize the risk of connections lingering indefinitely, we should consider setting a reasonable timeout for keepalive retries. Additionally, we can incorporate logic to handle unanswered keepalive packets.
5. close() in Application-Level Closing
We’ve seen the role of the Linux kernel in managing connection termination, but the responsibility doesn’t lie solely with the operating system. Applications that use sockets have a significant part to play in ensuring graceful and timely closing.
5.1. Proper Socket Closure
When an application finishes using a socket, it’s essential to call the close() function on the socket descriptor. This function informs the kernel that the application no longer requires the socket and initiates the closing process. By calling close(), the application allows the kernel to send the necessary FIN packet to the remote end, initiating the TCP termination handshake.
5.2. Benefits of Proper Closing
There are several advantages to using close() for application-level closing:
- Graceful termination prevents data loss and potential issues that might arise from an abrupt connection closure.
- Proper release of associated resources prevents resource leaks and ensures optimal performance, especially when dealing with a high volume of connections.
- It avoids situations where the kernel might need to wait for timeouts or detect orphaned sockets, leading to potential delays.
By calling close() when finished with a socket, applications actively participate in a clean and efficient connection termination process. This benefits both the application itself, ensuring predictable behavior and resource management, and the overall system health by preventing resource leaks.
6. Conclusion
In this article, we explored how Linux terminates network connections after killing the application on one end. We’ve seen that the Linux kernel plays an important role in ensuring a graceful and controlled closing process, following the established TCP protocol. This helps prevent data loss and resource leaks and promotes overall network stability.
We discussed scenarios where the kernel intervenes, such as situations where applications forget to call close(), or network issues disrupt the termination handshake. Additionally, we delved into the concept of TCP keepalive and other reasons a connection might remain established. Finally, we emphasized the importance of application-level closing using the close() function.