在大多数情况下,理解 TCP Keepalive 不是必要的,但在特定情况下,它可能非常有用。 你需要了解基本的 TCP/IP 网络概念和 C 编程语言才能理解本文档的所有章节。
本 HOWTO 的主要目的是详细描述 TCP Keepalive,并演示各种应用场景。 在一些初步理论之后,讨论重点放在现代 Linux 内核版本(2.4.x、2.6.x)中 TCP Keepalive 例程的 Linux 实现,以及系统管理员如何利用这些例程,并提供具体的配置示例和技巧。
HOWTO 的第二部分涉及 Linux 内核公开的编程接口,以及如何用 C 语言编写启用 TCP Keepalive 的应用程序。 文中介绍了实际示例,并介绍了libkeepalive项目,该项目允许传统应用程序在无需修改代码的情况下从 Keepalive 中受益。
本文档《TCP Keepalive HOWTO》版权归 Fabio Busatto (c) 2007 所有。 允许根据自由软件基金会发布的 GNU 自由文档许可证 1.1 版或任何后续版本的条款复制、分发和/或修改本文档; 无固定章节,无封面文本,无封底文本。 许可证副本可在 https://gnu.ac.cn/copyleft/fdl.html 获取。
本文档中包含的源代码根据自由软件基金会发布的 GNU 通用公共许可证第 2 版或任何后续版本的条款发布。 许可证副本可在 https://gnu.ac.cn/copyleft/gpl.html 获取。
Linux 是 Linus Torvalds 的注册商标。
对于本文档的内容不承担任何责任。 您需要自行承担使用概念、示例和信息的风险。 可能存在错误和不准确之处,可能会对您的系统造成损害。 请谨慎操作,尽管这种情况极不可能发生,但作者不承担任何责任。
所有版权均归其各自所有者所有,除非另有明确说明。 本文档中术语的使用不应被视为影响任何商标或服务标记的有效性。 特定产品或品牌的命名不应被视为认可。
为了理解 TCP Keepalive(我们简称 Keepalive)的作用,您只需要阅读名称:保持 TCP 存活。 这意味着您将能够检查您已连接的套接字(也称为 TCP 套接字),并确定连接是否仍然建立并运行,或者是否已断开。
Keepalive 的概念非常简单:当您建立 TCP 连接时,您会关联一组计时器。 其中一些计时器处理 Keepalive 过程。 当 Keepalive 计时器达到零时,您会向您的对端发送一个 Keepalive 探测数据包,其中不包含数据,并且 ACK 标志已打开。 由于 TCP/IP 规范,您可以这样做,作为一种重复的 ACK,远程端点将没有参数,因为 TCP 是面向流的协议。 另一方面,您将收到来自远程主机的回复(远程主机不需要完全支持 Keepalive,只需支持 TCP/IP),其中不包含数据,并且 ACK 设置为开启。
如果您收到对 Keepalive 探测的回复,您可以断言连接仍然建立并运行,而无需担心用户级实现。 实际上,TCP 允许您处理流,而不是数据包,因此零长度数据包对于用户程序来说并不危险。
此过程很有用,因为如果其他对端失去连接(例如,通过重启),即使您没有流量,您也会注意到连接已断开。 如果您的对端没有回复 Keepalive 探测,您可以断言该连接不能被认为是有效的,然后采取正确的操作。
没有 Keepalive 您也可以愉快地生活,因此如果您正在阅读本文,您可能正在尝试了解 Keepalive 是否是解决您问题的可能方案。 或者你真的没有什么更有趣的事情可做,那也没关系。 :)
Keepalive 是非侵入性的,在大多数情况下,如果您有疑问,您可以打开它而不会冒做错事的风险。 但请记住,它会产生额外的网络流量,这可能会对路由器和防火墙产生影响。
简而言之,请动脑筋并小心谨慎。
在下一节中,我们将区分 Keepalive 的两个目标任务
检查死对端
防止由于网络不活动而断开连接
Keepalive 可用于在您的对端在能够通知您之前死亡时提醒您。 这可能是由于多种原因造成的,例如内核崩溃或处理该对端的进程的粗暴终止。 另一个说明您何时需要 Keepalive 来检测对端死亡的场景是,当对端仍然存活,但它与您之间的网络通道已关闭时。 在这种情况下,如果网络没有再次恢复运行,您就相当于对端死亡。 这是正常 TCP 操作对于检查连接状态无用的情况之一。
考虑对端 A 和对端 B 之间的简单 TCP 连接:存在初始的三次握手,从 A 到 B 的一个 SYN 段,从 B 到 A 的 SYN/ACK,以及从 A 到 B 的最终 ACK。 此时,我们处于稳定状态:连接已建立,现在我们通常会等待某人在通道上发送数据。 问题来了:拔掉 B 的电源,它会立即关闭,而不会通过网络发送任何内容来通知 A 连接即将断开。 A 从其方面来说,已准备好接收数据,并且不知道 B 已崩溃。 现在恢复 B 的电源并等待系统重启。 A 和 B 现在又回来了,但是当 A 知道与 B 的连接仍然处于活动状态时,B 却一无所知。 当 A 尝试通过死连接向 B 发送数据时,情况会自行解决,B 会回复一个 RST 数据包,导致 A 最终关闭连接。
Keepalive 可以告诉您另一个对端何时变得无法访问,而不会有误报的风险。 实际上,如果问题出在两个对端之间的网络中,Keepalive 操作是等待一段时间,然后重试,在将连接标记为断开之前发送 Keepalive 数据包。
_____ _____ | | | | | A | | B | |_____| |_____| ^ ^ |--->--->--->-------------- SYN -------------->--->--->---| |---<---<---<------------ SYN/ACK ------------<---<---<---| |--->--->--->-------------- ACK -------------->--->--->---| | | | system crash ---> X | | system restart ---> ^ | | |--->--->--->-------------- PSH -------------->--->--->---| |---<---<---<-------------- RST --------------<---<---<---| | | |
Keepalive 的另一个有用的目标是防止不活动导致通道断开连接。 当您位于 NAT 代理或防火墙之后时,无缘无故断开连接是一个非常常见的问题。 这种行为是由代理和防火墙中实现的连接跟踪过程引起的,这些过程跟踪通过它们的所有连接。 由于这些机器的物理限制,它们只能在内存中保留有限数量的连接。 最常见和最合理的策略是保留最新的连接,并首先丢弃旧的和不活动的连接。
回到对端 A 和 B,重新连接它们。 一旦通道打开,等待事件发生,然后将其通信给另一个对端。 如果事件在很长一段时间后才验证怎么办? 我们的连接有其范围,但代理不知道。 因此,当我们最终发送数据时,代理无法正确处理它,连接断开。
由于正常的实现会在其中一个数据包到达时将连接放在列表的顶部,并在需要消除条目时选择队列中的最后一个连接,因此定期通过网络发送数据包是始终处于极地位置的好方法,并且删除的风险很小。
_____ _____ _____ | | | | | | | A | | NAT | | B | |_____| |_____| |_____| ^ ^ ^ |--->--->--->---|----------- SYN ------------->--->--->---| |---<---<---<---|--------- SYN/ACK -----------<---<---<---| |--->--->--->---|----------- ACK ------------->--->--->---| | | | | | <--- connection deleted from table | | | | |--->- PSH ->---| <--- invalid connection | | | | |
Linux 内置了对 Keepalive 的支持。 您需要启用 TCP/IP 网络才能使用它。 您还需要procfs支持和sysctl支持才能在运行时配置内核参数。
涉及 Keepalive 的过程使用三个用户驱动的变量
上次发送的数据包(简单的 ACK 不被视为数据)与第一个 Keepalive 探测之间的时间间隔; 在连接被标记为需要 Keepalive 后,此计数器不再使用
连续 Keepalive 探测之间的时间间隔,无论连接在此期间交换了什么
在将连接视为死亡并通知应用程序层之前要发送的未确认探测的数量
请记住,即使在内核中配置了 Keepalive 支持,它也不是 Linux 中的默认行为。 程序必须使用setsockopt接口为其套接字请求 Keepalive 控制。 实现 Keepalive 的程序相对较少,但您可以按照本文档后面解释的说明,轻松地为大多数程序添加 Keepalive 支持。
有两种方法可以通过用户空间命令在内核内部配置 Keepalive 参数
procfs接口
sysctl接口
我们主要讨论如何在 procfs 接口上完成此操作,因为它是最常用、推荐且最容易理解的。 sysctl 接口,特别是关于 sysctl(2) 系统调用,而不是 sysctl(8) 工具,仅在此处用于背景知识。
此接口需要同时sysctl和procfs构建到内核中,并且procfs挂载在文件系统中的某个位置(通常在/proc,如下例所示)。 您可以通过 "cat" 文件来读取实际参数的值/proc/sys/net/ipv4/目录
# cat /proc/sys/net/ipv4/tcp_keepalive_time 7200 # cat /proc/sys/net/ipv4/tcp_keepalive_intvl 75 # cat /proc/sys/net/ipv4/tcp_keepalive_probes 9 |
前两个参数以秒为单位表示,最后一个参数是纯数字。 这意味着 Keepalive 例程在发送第一个 Keepalive 探测之前等待两个小时(7200 秒),然后每 75 秒重新发送一次。 如果连续九次未收到 ACK 响应,则连接被标记为断开。
修改此值非常简单:您需要将新值写入文件。 假设您决定配置主机,以便 Keepalive 在通道不活动十分钟后启动,然后每隔一分钟发送探测。 由于我们的网络主干高度不稳定且间隔值较低,假设您还想将探测次数增加到 20 次。
以下是我们如何更改设置
# echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time # echo 60 > /proc/sys/net/ipv4/tcp_keepalive_intvl # echo 20 > /proc/sys/net/ipv4/tcp_keepalive_probes |
为了确保一切成功,请重新检查文件并确认这些新值显示在旧值的位置。
请记住procfs处理特殊文件,您不能对它们执行任何类型的操作,因为它们只是内核空间中的接口,而不是真实的文件,因此在使用脚本之前先尝试一下,并尝试使用简单的访问方法,如先前示例中所示。
您可以通过 sysctl(8) 工具访问该接口,指定您要读取或写入的内容。
# sysctl \ > net.ipv4.tcp_keepalive_time \ > net.ipv4.tcp_keepalive_intvl \ > net.ipv4.tcp_keepalive_probes net.ipv4.tcp_keepalive_time = 7200 net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_probes = 9 |
请注意sysctl名称非常接近procfs路径。 写入使用 sysctl (8) 的-w开关执行
# sysctl -w \ > net.ipv4.tcp_keepalive_time=600 \ > net.ipv4.tcp_keepalive_intvl=60 \ > net.ipv4.tcp_keepalive_probes=20 net.ipv4.tcp_keepalive_time = 600 net.ipv4.tcp_keepalive_intvl = 60 net.ipv4.tcp_keepalive_probes = 20 |
请注意,sysctl (8) 不使用 sysctl(2) 系统调用,而是直接在procfs子树中读取和写入,因此您将需要procfs在内核中启用并在文件系统中挂载,就像您直接访问procfs接口中的文件一样。 Sysctl(8) 只是执行相同操作的不同方式。
有几种方法可以在每次系统启动时重新配置您的系统。 首先,请记住,每个 Linux 发行版都有一组由 init (8) 调用的 init 脚本。 最常见的配置包括/etc/rc.d/目录,或替代方案/etc/init.d/。 在任何情况下,您都可以在任何启动脚本中设置参数,因为 Keepalive 每次在其过程需要它们时都会重新读取这些值。 因此,如果您更改了tcp_keepalive_intvl的值,当连接仍然建立时,内核将在以后使用新值。
初始化命令应该逻辑上放置在三个位置:第一个是配置网络的位置,第二个是rc.local脚本,通常包含在所有发行版中,它被称为用户配置设置完成的位置。 第三个位置可能已存在于您的系统中。 回到 sysctl (8) 工具,您可以看到-p开关从/etc/sysctl.conf配置文件加载设置。 在许多情况下,您的 init 脚本已经执行了 sysctl-p(您可以 "grep" 它在配置目录中进行确认),因此您只需在/etc/sysctl.conf中添加行即可使其在每次启动时加载。 有关 sysctl.conf(5) 语法的更多信息,请参阅手册页。
本节介绍如果您想创建使用 Keepalive 的应用程序所需的编程代码。 这不是编程手册,它要求您事先了解 C 编程和网络概念。 我认为您熟悉套接字以及与应用程序的一般方面相关的所有内容。
并非所有网络应用程序都需要 Keepalive 支持。 请记住,它是 TCP Keepalive 支持。 因此,正如您可以想象的那样,只有 TCP 套接字才能利用它。
编写应用程序时,您可以做的最美好的事情是使其尽可能可定制,而不是强制执行决策。 如果您想考虑用户的幸福感,您应该实现 Keepalive,并让用户通过使用配置参数或命令行上的开关来决定他们是否想要使用它。
您需要为特定套接字启用 Keepalive 的所有操作是在套接字本身上设置特定的套接字选项。 该函数的原型如下
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen) |
第一个参数是套接字,先前使用 socket(2) 创建; 第二个参数必须是SOL_SOCKET,第三个参数必须是SO_KEEPALIVE。 第四个参数必须是布尔整数值,指示我们想要启用该选项,而最后一个参数是之前传递的值的大小。
根据手册页,成功时返回 0,错误时返回 -1(并且errno已正确设置)。
当您编写应用程序时,您还可以为 Keepalive 设置其他三个套接字选项。 它们都使用SOL_TCP级别而不是SOL_SOCKET,它们仅针对当前套接字覆盖系统范围的变量。 如果您在不先写入的情况下读取,将返回当前的系统范围参数。
TCP_KEEPCNT:覆盖tcp_keepalive_probes
TCP_KEEPIDLE:覆盖tcp_keepalive_time
TCP_KEEPINTVL:覆盖tcp_keepalive_intvl
这是一个创建套接字的小示例,显示 Keepalive 已禁用,然后启用它并检查该选项是否已有效设置。
/* --- begin of keepalive test program --- */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main(void); int main() { int s; int optval; socklen_t optlen = sizeof(optval); /* Create the socket */ if((s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { perror("socket()"); exit(EXIT_FAILURE); } /* Check the status for the keepalive option */ if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) { perror("getsockopt()"); close(s); exit(EXIT_FAILURE); } printf("SO_KEEPALIVE is %s\n", (optval ? "ON" : "OFF")); /* Set the option active */ optval = 1; optlen = sizeof(optval); if(setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) { perror("setsockopt()"); close(s); exit(EXIT_FAILURE); } printf("SO_KEEPALIVE set on socket\n"); /* Check the status again */ if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) { perror("getsockopt()"); close(s); exit(EXIT_FAILURE); } printf("SO_KEEPALIVE is %s\n", (optval ? "ON" : "OFF")); close(s); exit(EXIT_SUCCESS); } /* --- end of keepalive test program --- */ |
并非所有人都是软件开发人员,如果某个软件只缺少一个功能,也并非所有人都会从头开始重写软件。 也许您想为现有应用程序添加 Keepalive 支持,因为尽管作者可能认为它不重要,但您认为它会很有用。
首先,请记住关于您需要 Keepalive 的情况。 现在您需要处理面向连接的 TCP 套接字。
由于 Linux 不提供通过内核本身启用 Keepalive 支持的功能(如 BSD 类操作系统通常所做的那样),因此唯一的方法是在套接字创建后执行 setsockopt (2) 调用。 有两种解决方案
原始程序的源代码修改
使用库预加载技术注入 setsockopt (2)
请记住,Keepalive 与程序无关,而是与套接字相关,因此如果您有多个套接字,您可以分别处理每个套接字的 Keepalive。 第一阶段是了解程序的功能,然后在程序中搜索每个套接字的代码。 这可以使用 grep(1) 完成,如下所示
# grep 'socket *(' *.c |
这将或多或少地向您显示代码中的所有套接字。 下一步是仅选择正确的套接字:您将需要 TCP 套接字,因此请查找PF_INET(或AF_INET), SOCK_STREAM和IPPROTO_TCP(或更常见的,0)在您的套接字列表的参数中,并删除不匹配的套接字。
创建套接字的另一种方法是通过 accept(2)。 在这种情况下,请遵循已识别的 TCP 套接字,并检查其中是否有任何套接字是侦听套接字:如果是,请记住 accept(2) 返回一个套接字描述符,该描述符必须插入到您的套接字列表中。
一旦您确定了套接字,就可以继续进行更改。 最快速和最简单的补丁可以通过在套接字创建块之后简单地添加 setsockopt(2 ) 函数来完成。 可选地,如果您不喜欢系统默认值,您可以包含额外的调用以设置 Keepalive 参数。 在为该函数实现错误检查和处理程序时请小心,可以通过复制周围原始代码的样式来实现。 记住将optval设置为非零值,并在调用该函数之前初始化optlen。
如果您有时间,或者您认为这样做会非常酷,请尝试为您的程序添加完整的 Keepalive 支持,包括命令行上的开关或配置参数,以让用户选择是否使用 Keepalive。
在很多情况下,您无法修改应用程序的源代码,或者您必须为所有程序启用 Keepalive,因此不建议修补和重新编译所有内容。
libkeepalive 项目的诞生是为了帮助为应用程序添加 Keepalive 支持,因为 Linux 内核不提供本地执行相同操作的能力(如 BSD 所做的那样)。 libkeepalive 项目主页是 http://libkeepalive.sourceforge.net/
它由一个共享库组成,该共享库覆盖大多数二进制文件中的套接字系统调用,而无需重新编译或修改它们。 该技术基于 Linux 中包含的 ld.so(8) 加载器的 预加载功能,该功能允许您强制加载优先级高于正常的共享库。 程序通常使用位于glibc共享库中的 socket(2) 函数调用; 使用 libkeepalive,您可以包装它并在套接字创建后立即注入 setsockopt (2),从而返回一个已将 Keepalive 设置为主程序的套接字。 由于用于注入系统调用的机制,当套接字函数静态编译到二进制文件中时,这将不起作用,例如在使用 gcc(1 ) 标志链接的程序中-static.
下载并安装 libkeepalive 后,您将能够为您的程序添加 Keepalive 支持,而无需成为root的先决条件,只需在执行程序之前设置LD_PRELOAD环境变量。 顺便说一句,超级用户也可以使用全局配置强制预加载,然后用户可以通过将KEEPALIVE环境变量设置为off.
来决定关闭它。 环境也用于设置 Keepalive 参数的特定值,因此您可以区分处理每个程序的能力,在启动应用程序之前设置KEEPCNT, KEEPIDLE和KEEPINTVL。
以下是 libkeepalive 用法示例
$ test SO_KEEPALIVE is OFF $ LD_PRELOAD=libkeepalive.so \ > KEEPCNT=20 \ > KEEPIDLE=180 \ > KEEPINTVL=60 \ > test SO_KEEPALIVE is ON TCP_KEEPCNT = 20 TCP_KEEPIDLE = 180 TCP_KEEPINTVL = 60 |
您可以使用 strace (1) 来了解发生了什么
$ strace test execve("test", ["test"], [/* 26 vars */]) = 0 [..] open("/lib/libc.so.6", O_RDONLY) = 3 [..] socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 getsockopt(3, SOL_SOCKET, SO_KEEPALIVE, [0], [4]) = 0 close(3) = 0 [..] _exit(0) = ? $ LD_PRELOAD=libkeepalive.so \ > strace test execve("test", ["test"], [/* 27 vars */]) = 0 [..] open("/usr/local/lib/libkeepalive.so", O_RDONLY) = 3 [..] open("/lib/libc.so.6", O_RDONLY) = 3 [..] open("/lib/libdl.so.2", O_RDONLY) = 3 [..] socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 setsockopt(3, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0 setsockopt(3, SOL_TCP, TCP_KEEPCNT, [20], 4) = 0 setsockopt(3, SOL_TCP, TCP_KEEPIDLE, [180], 4) = 0 setsockopt(3, SOL_TCP, TCP_KEEPINTVL, [60], 4) = 0 [..] getsockopt(3, SOL_SOCKET, SO_KEEPALIVE, [1], [4]) = 0 [..] getsockopt(3, SOL_TCP, TCP_KEEPCNT, [20], [4]) = 0 [..] getsockopt(3, SOL_TCP, TCP_KEEPIDLE, [180], [4]) = 0 [..] getsockopt(3, SOL_TCP, TCP_KEEPINTVL, [60], [4]) = 0 [..] close(3) = 0 [..] _exit(0) = ? |
有关更多信息,请访问 libkeepalive 项目主页: http://libkeepalive.sourceforge.net/