日常运维Linux服务器时,你可能会遇到一个头疼的问题:服务器上堆积了大量处于TIME-WAIT状态的TCP连接,导致可用端口被占满,新的连接建立失败,服务响应变慢。常规的系统参数调整(比如修改net.ipv4.tcp_tw_reuse、tcp_tw_recycle)虽然能缓解,但有时我们需要更精准的方式------直接手动终止指定的TIME-WAIT连接。今天我们就聊聊如何通过一个Linux内核模块,实现对TCP连接(尤其是TIME-WAIT套接字)的精准"清理"。
一、先搞懂:TCP TIME-WAIT到底是个啥?
要理解这个模块的价值,得先明白TIME-WAIT是怎么来的。
TCP是个"靠谱"的协议,通信结束时要确保双方都正确收到了断开信号。当一方主动关闭连接(比如客户端),发送FIN包并收到对方的ACK后,不会立刻释放连接资源,而是进入TIME-WAIT状态,默认会停留2MSL(最大报文生存时间,Linux下约1分钟)。
这个设计的目的很简单:
- 防止延迟的报文被后续新建的同名连接接收,导致数据错乱;
- 确保对方能收到最后的ACK包,避免对方重发FIN包时没人回应。
但问题来了:如果服务器短时间内处理大量短连接(比如高频的HTTP短连接),就会堆积大量TIME-WAIT连接。每个连接都会占用一个端口,端口资源是有限的(默认0-65535),堆积多了就会出现"端口耗尽",新连接建不起来。
常规的系统参数调整是"全局生效"的,而我们今天聊的这个内核模块,能实现"精准打击"------只终止你指定的那些TIME-WAIT连接,不影响其他正常连接。
二、核心功能:这个内核模块能做什么?
简单说,这个模块的核心能力就是:
- 精准定位并终止指定的TCP连接(支持IPv4和IPv6);
- 对TIME-WAIT状态的套接字做专门处理,清理效率更高;
- 支持批量终止多个连接,不用一个个手动操作;
- 适配Linux 2.6.32及以上版本,还支持网络命名空间(容器环境也能用);
- 提供简单的交互方式:通过/proc文件系统写入要清理的连接信息,就能触发清理。
比如你查到有几个127.0.0.1:50866 → 127.0.0.1:22的TIME-WAIT连接,只需把这个地址端口对写入指定的/proc文件,模块就会自动找到并终止这个连接。
三、设计思路与实现原理
这个内核模块的实现逻辑并不复杂,核心是"接收用户指令 → 查找对应套接字 → 终止连接",我们一步步拆解:
c
...
static int dts_pton(struct dts_inet *in)
{
char *p, *end;
if (in4_pton(in->p, -1, (void *)in->addr, -1, (const char **)&end)) {
in->ipv6 = 0;
} else if (in6_pton(in->p, -1, (void *)in->addr, -1, (const char **)&end)) {
in->ipv6 = 1;
} else return -EINVAL;
p = (end += 1);
while (*p && isdigit(*p)) p++;
*p = 0; // kstrtoXX requires 0 at the end
return kstrtou16(end, 10, &in->port);
}
static void dts_process(struct dts_pernet *dts, struct dts_data *d)
{
char *p = d->data;
struct dts_inet src, dst;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmisleading-indentation"
while (*p && p < d->data + d->len) {
while (*p && isspace(*p)) p++; if (!*p) return; // skip spaces
src.p = p;
while (*p && !isspace(*p)) p++; if (!*p) return; // skip non-spaces
while (*p && isspace(*p)) p++; if (!*p) return; // skip spaces
dst.p = p;
while (*p && !isspace(*p)) p++; if (!*p) return; // skip non-spaces
if ((dts_pton(&src) || dts_pton(&dst)) || (src.ipv6 != dst.ipv6))
break;
dts_kill(dts->net, &src, &dst), p++;
}
#pragma GCC diagnostic pop
}
static int dts_proc_open(struct inode *inode, struct file *file)
{
struct dts_data *d = kzalloc(PAGE_SIZE, GFP_KERNEL);
if (!d) return -ENOMEM;
d->available = PAGE_SIZE - (sizeof(*d)+1);
file->private_data = d;
return 0;
}
static ssize_t dts_proc_write(struct file *file, const char __user *buf, size_t size, loff_t *pos)
{
struct dts_data *d = file->private_data;
if (d->len + size > d->available) {
size_t new_available = d->available + roundup(size, PAGE_SIZE);
struct dts_data *dnew = krealloc(d, new_available + (sizeof(*d)+1), GFP_KERNEL);
if (!dnew) {
kfree(d), file->private_data = NULL;
return -ENOMEM;
}
(d = dnew)->available = new_available;
file->private_data = d; // update
}
if (copy_from_user(d->data + d->len, buf, size))
return -EFAULT;
d->data[(d->len += size)] = 0;
return size;
}
static int dts_proc_release(struct inode *inode, struct file *file)
{
struct dts_data *d = file->private_data;
if (d) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 17, 0)
dts_process(pde_data(file_inode(file)), d);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3, 10, 0)
dts_process(PDE_DATA(file_inode(file)), d);
#else
dts_process(PDE(file->f_path.dentry->d_inode)->data, d);
#endif
kfree(d), file->private_data = NULL;
} return 0;
}
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
static const struct proc_ops dts_proc_fops = {
.proc_open = dts_proc_open,
.proc_write = dts_proc_write,
.proc_release = dts_proc_release,
};
#else
static const struct file_operations dts_proc_fops = {
.owner = THIS_MODULE,
.open = dts_proc_open,
.write = dts_proc_write,
.release = dts_proc_release,
};
#endif
static int dts_pernet_id = 0;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 33)
static int dts_pernet_init(struct net *net)
{
struct dts_pernet *dts = net_generic(net, dts_pernet_id);
dts->net = net;
dts->pde = proc_create_data(DTS_PDE_NAME, 0600, net->proc_net, &dts_proc_fops, dts);
return !dts->pde;
}
static void dts_pernet_exit(struct net* net)
{
struct dts_pernet *dts = net_generic(net, dts_pernet_id);
BUG_ON(!dts->pde);
remove_proc_entry(DTS_PDE_NAME, net->proc_net);
}
#else
static int dts_pernet_init(struct net *net)
{
struct dts_pernet *dts = NULL;
dts = kzalloc(sizeof(*dts), GFP_KERNEL);
if (!dts) return -ENOMEM;
dts->net = net;
dts->pde = proc_create_data(DTS_PDE_NAME, 0600, net->proc_net, &dts_proc_fops, dts);
if (!dts->pde) {
kfree(dts);
return -ENOMEM;
}
if (net_assign_generic(net, dts_pernet_id, dts)) {
kfree(dts);
return -ENOMEM;
}
return 0;
}
static void dts_pernet_exit(struct net* net)
{
struct dts_pernet *dts = net_generic(net, dts_pernet_id);
net_assign_generic(net, dts_pernet_id, NULL);
BUG_ON(!dts->pde);
remove_proc_entry(DTS_PDE_NAME, net->proc_net);
kfree(dts);
}
#endif
static struct pernet_operations dts_pernet_ops = {
.init = dts_pernet_init,
.exit = dts_pernet_exit,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 33)
.id = &dts_pernet_id,
.size = sizeof(struct dts_pernet),
#endif
};
static inline int dts_register_pernet(void)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 33)
return register_pernet_subsys(&dts_pernet_ops);
#else
return register_pernet_gen_subsys(&dts_pernet_id, &dts_pernet_ops);
#endif
}
static inline void dts_unregister_pernet(void)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 33)
unregister_pernet_subsys(&dts_pernet_ops);
#else
unregister_pernet_gen_subsys(dts_pernet_id, &dts_pernet_ops);
#endif
}
////////////////////////////////////////////////////////////////////////////////
static int __init dts_module_init(void)
{
int res = 0;
if (!tcp_hashinfop()) {
printk("DTS: no tcp_hashinfo found\n");
return -EINVAL;
}
res = dts_register_pernet();
if (res) return res;
return 0;
}
module_init(dts_module_init);
static void __exit dts_module_exit(void)
{
dts_unregister_pernet();
}
module_exit(dts_module_exit);
...
If you need the complete source code, please add the WeChat number (c17865354792)
1. 整体流程(可视化)
先看一张流程图,能快速理解模块的工作全过程:
否
是
TIME-WAIT
其他状态
用户加载内核模块
模块初始化:创建/proc/net下的交互文件
用户向/proc文件写入待清理的连接信息(如127.0.0.1:50866 127.0.0.1:22)
模块接收写入的数据,解析地址和端口(区分IPv4/IPv6)
根据解析出的源/目的地址+端口,在内核中查找对应的TCP套接字(sock结构体)
找到套接字?
结束,无操作
套接字状态?
调用内核接口释放TIME-WAIT套接字资源
调用tcp_done终止连接,释放套接字
输出日志,完成清理
2. 关键步骤拆解
我们用大白话讲每个核心步骤的实现逻辑:
(1)与用户交互:借助proc文件系统
Linux内核模块不能直接和用户态程序通信,所以模块选择了proc文件系统 作为交互桥梁------在/proc/net目录下创建一个专属文件,用户向这个文件写入内容,内核就能接收到。
模块里实现了proc文件的"打开(open)""写入(write)""释放(release)"三个核心接口:
- 打开文件时:内核分配一块内存,用来存放用户后续写入的连接信息;
- 写入文件时:把用户态的内容拷贝到内核内存中(还会自动扩容,支持批量写入);
- 释放文件时(比如echo命令执行完):触发核心的清理逻辑,处理内存里的连接信息。
(2)解析用户输入:识别地址和端口
用户写入的内容是"源地址:端口 目的地址:端口"(比如127.0.0.1:50866 127.0.0.1:22),模块需要把这些字符串解析成内核能识别的格式:
- 先区分是IPv4还是IPv6地址(用
in4_pton/in6_pton函数); - 再把端口号从字符串转成数字(
kstrtou16); - 把地址转换成内核网络栈使用的大端序格式(网络字节序)。
这里要注意一个细节:必须保证源和目的地址都是IPv4或都是IPv6,不能混着来,否则会直接跳过处理。
(3)查找内核中的TCP套接字
解析完地址和端口后,模块要在内核的TCP哈希表中找到对应的套接字(sock结构体)------这是最核心的一步。
Linux内核会把所有TCP套接字存在哈希表中(tcp_hashinfo),模块通过inet_lookup(IPv4)或inet6_lookup(IPv6)函数,传入源/目的地址和端口,就能精准查到对应的套接字。
这里有个兼容技巧:不同Linux内核版本中,tcp_hashinfo的获取方式不一样(6.15以上版本需要通过kallsyms查找符号地址),模块做了版本适配,确保在2.6.32以上版本都能正常工作。
(4)终止连接:分状态处理
找到套接字后,模块会根据套接字的状态做不同处理:
- 如果是TIME-WAIT状态:调用
inet_twsk_deschedule_put(新内核)或inet_twsk_deschedule + inet_twsk_put(旧内核),直接释放TIME-WAIT套接字的资源(这是清理TIME-WAIT的关键,比普通关闭更高效); - 如果是其他状态(比如ESTABLISHED):调用
tcp_done终止连接,再调用sock_put释放套接字资源。
处理完成后,模块还会打印日志,告诉你清理了哪个套接字、源目的地址是什么、原来的状态是多少,方便排查。
3.测试运行完整步骤
步骤1:编译内核模块
进入代码所在目录,执行编译命令:
bash
cd ~/tcp_clean_module
make
- 编译成功:目录下会生成
tcp_clean.ko(核心的内核模块文件)、tcp_clean.o等文件; - 编译失败:大概率是内核版本不匹配或依赖没装全,核对前面的环境准备步骤。
步骤2:加载内核模块
bash
# 加载模块(root权限)
sudo insmod tcp_clean.ko
# 验证模块是否加载成功
lsmod | grep tcp_clean # 能看到tcp_clean模块名,说明加载成功
步骤3:生成测试用的 TIME-WAIT 连接(模拟场景)
我们先手动造一个 TIME-WAIT 连接,方便测试:
bash
# 1. 打开一个终端,启动一个临时的TCP监听端口(比如2222)
nc -l 2222
# 2. 打开第二个终端,连接这个监听端口,然后主动断开(会生成TIME-WAIT)
telnet 127.0.0.1 2222 # 连接后,按 Ctrl + ] 再按 q 退出,或直接关终端
# 3. 查看生成的TIME-WAIT连接
netstat -ant | grep TIME-WAIT | grep 2222
# 会看到类似输出:tcp 0 0 127.0.0.1:2222 127.0.0.1:54321 TIME-WAIT
记下输出里的「源地址:端口」和「目的地址:端口」(比如 127.0.0.1:54321 127.0.0.1:2222)。
步骤4:使用模块清理 TIME-WAIT 连接
模块通过 /proc/net/tcpdropsock 文件接收清理指令,执行清理:
bash
# 替换成你实际查到的源/目的地址+端口(格式:源地址:端口 目的地址:端口)
echo "127.0.0.1:54321 127.0.0.1:2222" | sudo tee /proc/net/tcpdropsock
步骤5:验证清理效果
再次执行查看 TIME-WAIT 连接的命令,确认目标连接已消失:
bash
netstat -ant | grep TIME-WAIT | grep 2222
# 没有输出 → 清理成功;有输出 → 检查地址/端口是否填错,或模块加载是否正常
步骤6:批量清理测试
如果想测试批量清理,先生成多个 TIME-WAIT 连接,再批量写入指令:
bash
# 生成多个测试连接(循环执行多次连接+断开)
for i in {1..3}; do telnet 127.0.0.1 2222 < /dev/null; done
# 批量提取TIME-WAIT连接并清理
netstat -ant | grep TIME-WAIT | grep 2222 | awk '{print $4"\t"$5}' | sudo tee /proc/net/tcpdropsock
# 验证批量清理结果
netstat -ant | grep TIME-WAIT | grep 2222
步骤7:卸载内核模块(测试完成后)
bash
# 卸载模块
sudo rmmod tcp_clean
# 验证卸载
lsmod | grep tcp_clean # 无输出 → 卸载成功
排错小技巧(测试失败时用)
- 模块加载失败:执行
dmesg | tail查看内核日志,会提示失败原因(比如内核版本不兼容、符号查找失败); - 清理连接没效果:
- 核对地址/端口是否写反(源和目的要和
netstat输出一致); - 确认连接确实是 TIME-WAIT 状态(模块对其他状态的连接也能清理,但优先针对 TIME-WAIT);
- 核对地址/端口是否写反(源和目的要和
/proc/net/tcpdropsock文件不存在:模块加载失败,重新编译/加载,或检查内核日志。
四、背后的核心内核知识点
这个模块的实现,用到了几个Linux内核开发的关键知识点,搞懂这些能帮你理解更多内核网络编程的逻辑:
1. proc文件系统的内核编程
proc文件系统是内核和用户态交互的"轻量级通道",不需要创建实际文件,数据都存在内存中。实现自定义proc文件需要:
- 定义文件操作结构体(
proc_ops/file_operations),关联open/write/release等回调; - 用
proc_create_data创建文件,关联到指定的网络命名空间(/proc/net下); - 注意内存管理:用户写入的数据要在内核中分配内存,使用完必须释放,避免内存泄漏。
2. 内核版本兼容性
Linux内核版本迭代快,很多函数和结构体的定义会变(比如tcp_hashinfo的获取、proc文件的操作结构体)。模块通过LINUX_VERSION_CODE宏判断内核版本,适配不同的函数调用方式,这是内核模块开发的必备技巧。
3. 网络命名空间支持
现在容器(Docker、K8s)都用网络命名空间隔离网络环境,模块通过pernet_operations注册每个网络命名空间的初始化/退出回调,确保在不同的网络命名空间中都能创建专属的proc文件,适配容器环境。
4. 套接字(sock)结构体操作
内核中的sock结构体是TCP连接的核心表示,包含连接的状态、地址、端口、哈希表节点等信息。操作sock结构体时要注意:
- 必须用内核提供的接口(比如
inet_lookup、tcp_done),不能直接修改结构体成员; - 释放套接字时要调用
sock_put,避免引用计数泄漏。
五、实际使用价值
这个模块的设计思路和实现,不仅能解决TIME-WAIT堆积的问题,还能给我们带来这些启发:
- 精准运维:相比全局调整内核参数,精准清理指定连接能避免影响正常业务;
- 内核网络编程参考:模块的代码是学习"内核中操作TCP套接字"的绝佳案例,涵盖了地址解析、套接字查找、连接终止等核心操作;
- 兼容性设计:模块对不同内核版本的适配方式,是内核模块开发的通用思路。
需要注意的是:内核模块直接运行在内核态,错误的代码可能导致系统崩溃,使用前一定要在测试环境验证,并且只加载可信的模块。
总结
- TCP TIME-WAIT状态是为了保证TCP连接的可靠性,但大量堆积会导致端口耗尽,需要精准清理;
- 该内核模块通过proc文件系统与用户交互,解析地址端口后查找并终止指定TCP连接,对TIME-WAIT状态做专门优化;
- 模块的实现核心是内核中TCP套接字的查找与释放,同时兼顾了内核版本兼容性和网络命名空间支持。
这个模块的设计思路,本质是"利用内核提供的网络接口,封装成简单的用户交互方式",既体现了Linux内核的灵活性,也给我们解决网络运维问题提供了新的思路。
Welcome to follow WeChat official account【程序猿编码】