Linux内核源码剖析之TCP保活机制(KeepAlive)
写在前面:
版本信息:
Linux内核2.6.24(大部分centos、ubuntu应该都在3.1+。但是2.6的版本比较稳定,后续版本本质变化也不是很大)
ipv4 协议
https://blog.csdn.net/ComplexMaze/article/details/124201088
本文使用案例如上地址,感谢案例的分享,本篇文章核心部分还是在Linux内核源码分析~
为什么写下这篇文章,因为在实际项目中,是无法避免TCP通讯(对于这点,可能大部分Java程序员感受不到底层的网络通讯),正因为无法避免TCP通讯,恰好TCP通讯存在三次握手和四次挥手的过程,如果建立一次连接就三次握手和四次挥手,而我们清楚的知道三次握手和四次挥手是同步的过程,此过程也会带来不少的时间浪费和资源的浪费。所以Linux内核TCP网络协议栈就出现了KeepAlive机制,此机制减少三次握手和四次挥手次数,第一次建立连接后保持长连接,后续通讯就可以只考虑发送数据报文即可。往往出现一个机制解决某个问题,其他问题又出现,如果所有连接都建立长连接保活机制,而连接数又有限制,此时该如何解决呢?如下代码,Linux使用心跳机制去检测连接是否存活~
#define TCP_KEEPALIVE_TIME (120*60*HZ) // 首次,2小时
#define TCP_KEEPALIVE_PROBES 9 // 重试9次
#define TCP_KEEPALIVE_INTVL (75*HZ) // 后续,每75秒一次
- 在Linux内核中默认关闭KeepAlive
- 开启KeepAlive后,默认2小时后往对端发送心跳包,检查是否还活着
- 默认后续每75秒往对端发送心跳包,检查是否还活着
- 默认当对端9次都没有响应报文就发送RST报文,断开TCP连接,释放资源!
- 当然这一切参数都可以配置,通过sys_setsockopt系统调用,当然setsockopt函数库就行啦
回到上述描述的话题,往往出现一个机制解决某个问题,其他问题又出现。解决了频繁握手和挥手的时间,但是连接数量不够的问题又出现了,可能很多连接建立在那里,完全不通讯了,或者对端已经断网,或者宕机等等原因占用连接不释放,而Linux默认一个连接存活检测需要2个小时+ 才去检测对端是否活着,如果说服务器的负荷比较大,2小时才检测一次,会导致正常请求无法进行,所以此参数需要通过setsockopt函数库重新设置参数(当然,如果是Java等等虚拟机语言,本身也有自身的封装函数去操作setsockopt函数库,或者直接调用sys_setsockopt系统调用,这个需要看语言手册~!)话又说回来,如果设置的阈值大小、时间太短的问题也会很明显,一直都在发心跳包检测,甚至性能损耗大于了握手和挥手的时间,所以需要根据业务环境、服务器的硬件从性能损耗和空闲连接数量做折中考虑~
案例:
下面是C语言的服务端的案例源码,此案例是借用的,但是我们重点关心机制~
/*server.c*/
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include <netinet/tcp.h>
#define PORT 4000//端口号
#define BACKLOG 5/*最大监听数*/
#define MAX_DATA 100//接收到的数据最大程度
int main(){int sockfd,new_fd;/*socket句柄和建立连接后的句柄*/struct sockaddr_in my_addr;/*本方地址信息结构体,下面有具体的属性赋值*/struct sockaddr_in their_addr;/*对方地址信息*/int sin_size;char buf[MAX_DATA];//储存接收数据
sockfd=socket(AF_INET,SOCK_STREAM,0);//建立socket if(sockfd==-1){printf("socket failed:%d",errno);return -1;}my_addr.sin_family=AF_INET;/*该属性表示接收本机或其他机器传输*/my_addr.sin_port=htons(PORT);/*端口号*/my_addr.sin_addr.s_addr=htonl(INADDR_ANY);/*IP,括号内容表示本机IP*/bzero(&(my_addr.sin_zero),8);/*将其他属性置0*/if(bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr))<0){//绑定地址结构体和socketprintf("bind error");return -1;}listen(sockfd,BACKLOG);//开启监听 ,第二个参数是最大监听数 while(1){sin_size=sizeof(struct sockaddr_in);new_fd=accept(sockfd,(struct sockaddr*)&their_addr,&sin_size);//在这里阻塞知道接收到消息,参数分别是socket句柄,接收到的地址信息以及大小 // 开启保活,1分钟内探测不到,断开连接int keep_alive = 1;int keep_idle = 3;int keep_interval = 1;int keep_count = 57;if (setsockopt(new_fd, SOL_SOCKET, SO_KEEPALIVE, &keep_alive, sizeof(keep_alive))) {perror("Error setsockopt(SO_KEEPALIVE) failed");exit(1);}if (setsockopt(new_fd, IPPROTO_TCP, TCP_KEEPIDLE, &keep_idle, sizeof(keep_idle))) {perror("Error setsockopt(TCP_KEEPIDLE) failed");exit(1);}if (setsockopt(new_fd, SOL_TCP, TCP_KEEPINTVL, (void *)&keep_interval, sizeof(keep_interval))) {perror("Error setsockopt(TCP_KEEPINTVL) failed");exit(1);}if (setsockopt(new_fd, SOL_TCP, TCP_KEEPCNT, (void *)&keep_count, sizeof(keep_count))) {perror("Error setsockopt(TCP_KEEPCNT) failed");exit(1);}while(new_fd != -1) {recv(new_fd,buf,MAX_DATA,0);//将接收数据打入buf,参数分别是句柄,储存处,最大长度,其他信息(设为0即可)。 printf("%s",buf);}}return 0;
}
此服务端案例非常的简单,当客户端与服务端建立连接后,修改KeepAlive的机制参数,使用setsockopt库函数修改。
SO_KEEPALIVE:开启KeepAlive机制
TCP_KEEPIDLE:首次检测的时长
TCP_KEEPINTVL:下次检测的间隔时长
TCP_KEEPCNT:重试阈值次数
源码分析:
首先看到TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT这三个参数的设置,源码在net/ipv4/tcp.c 文件do_tcp_setsockopt方法,此方法由sys_setsockopt系统调用方法调用。
static int do_tcp_setsockopt(struct sock *sk, int level,int optname, char __user *optval, int optlen)
{struct tcp_sock *tp = tcp_sk(sk);struct inet_connection_sock *icsk = inet_csk(sk);int val;int err = 0;switch (optname) {…………case TCP_KEEPIDLE: // 设置第一次触发的时间if (val < 1 || val > MAX_TCP_KEEPIDLE)err = -EINVAL;else {// 算出设置的时间tp->keepalive_time = val * HZ;// 如果KeepAlive机制已开启,并且当前不是关闭状态和监听状态。if (sock_flag(sk, SOCK_KEEPOPEN) &&!((1 << sk->sk_state) &(TCPF_CLOSE | TCPF_LISTEN))) {// 当前时间 - 上次ACK的时候 = 相对时间__u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;if (tp->keepalive_time > elapsed)// 如果上次ACK同步的时间小于设置的时间,那就把剩余的时间算出来elapsed = tp->keepalive_time - elapsed;else// 如果上次ACK同步的时间大于设置的时间,那就立马检测elapsed = 0;// 设置内核的定时器inet_csk_reset_keepalive_timer(sk, elapsed);}}break;case TCP_KEEPINTVL: // 设置每次的间隔时间if (val < 1 || val > MAX_TCP_KEEPINTVL)err = -EINVAL;elsetp->keepalive_intvl = val * HZ;break;case TCP_KEEPCNT: // 设置阈值次数if (val < 1 || val > MAX_TCP_KEEPCNT)err = -EINVAL;elsetp->keepalive_probes = val;break;release_sock(sk);return err;
}
这里非常的简单,通过switch case的形式把参数添加到结构体中,并且设置了首次触发的时间
接下来,我们看到定时器何时设置的。在net/ipv4/tcp_ipv4.c 文件中tcp_v4_init_sock方法。
static int tcp_v4_init_sock(struct sock *sk)
{…………tcp_init_xmit_timers(sk);…………return 0;
}void tcp_init_xmit_timers(struct sock *sk)
{inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,&tcp_keepalive_timer);
}void inet_csk_init_xmit_timers(struct sock *sk,void (*retransmit_handler)(unsigned long),void (*delack_handler)(unsigned long),void (*keepalive_handler)(unsigned long))
{struct inet_connection_sock *icsk = inet_csk(sk);…………// 初始化sk->sk_timer,也即初始化timer_list// timer_list在内核是一个定时器的结构体init_timer(&sk->sk_timer);// 设置定时器的回调函数sk->sk_timer.function = keepalive_handler;…………
}
把大部分无关的代码省略掉以后,源码看起来非常的简单,这里初始化了定时器,并且把定时器的回调函数设置成tcp_keepalive_timer,所以接下来,我们直接分析tcp_keepalive_timer方法即可。在net/ipv4/tcp_timer.c 文件中 tcp_keepalive_timer方法。
// 当达到keepalive设置的值以后回掉此方法。
static void tcp_keepalive_timer (unsigned long data)
{struct sock *sk = (struct sock *) data;struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);__u32 elapsed;/* Only process if socket is not in use. */bh_lock_sock(sk);if (sock_owned_by_user(sk)) {// 这里很简单,因为锁的原因,所以需要重试。inet_csk_reset_keepalive_timer (sk, HZ/20);goto out;}// 4次挥手阶段,而此时达到了保活的检测,此时发送RST报文给对端,表示我要断开了,然后释放资源即可。if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {if (tp->linger2 >= 0) {const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;if (tmo > 0) {tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);goto out;}}tcp_send_active_reset(sk, GFP_ATOMIC);goto death;}// 如果KeepAlive没有开启,或者当前已经是关闭状态if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)goto out;// 算出下次检测的时间elapsed = keepalive_time_when(tp);// 此时正在发送报文,所以无须检测,直接重置下次检测的时间if (tp->packets_out || tcp_send_head(sk))goto resched;// 算出距离上一次ACK的相对时间elapsed = tcp_time_stamp - tp->rcv_tstamp;// 如果上一次ACK的相对时间 大于等于 设置的时间,那么就代表达到一次阈值if (elapsed >= keepalive_time_when(tp)) {// 查看是否达到次数阈值,达到阈值后直接发送RST报文给对方,然后关闭连接。if ((!tp->keepalive_probes && icsk->icsk_probes_out >= sysctl_tcp_keepalive_probes) ||(tp->keepalive_probes && icsk->icsk_probes_out >= tp->keepalive_probes)) {tcp_send_active_reset(sk, GFP_ATOMIC);tcp_write_err(sk);goto out;}// 没达到阈值的情况// 尝试发送报文给对方,看是否还活着if (tcp_write_wakeup(sk) <= 0) {// 如果回复了,那就把下次检测的时间设置好icsk->icsk_probes_out++;elapsed = keepalive_intvl_when(tp);} else { // 对端没有回复,不知道是因为丢失还是怎么了,所以加快速度,尝试下一次。elapsed = TCP_RESOURCE_PROBE_INTERVAL;}} else {// 没有达到上次ACK的相对时间,所以算出差值,设置到定时器中。elapsed = keepalive_time_when(tp) - elapsed;}TCP_CHECK_TIMER(sk);sk_stream_mem_reclaim(sk);resched:// 把最新值设置到定时器中。inet_csk_reset_keepalive_timer (sk, elapsed);goto out;death:// 关闭连接,释放资源。tcp_done(sk);out:bh_unlock_sock(sk);sock_put(sk);
}
此方法是当定时器结束后回调执行,检测是否达到了我们设置或者默认的阈值,如果没有达到,再设置下一次定时器的时间,如果达到了就发送RST报文,关闭连接,释放资源~!
相关文章:

Linux内核源码剖析之TCP保活机制(KeepAlive)
写在前面: 版本信息: Linux内核2.6.24(大部分centos、ubuntu应该都在3.1。但是2.6的版本比较稳定,后续版本本质变化也不是很大) ipv4 协议 https://blog.csdn.net/ComplexMaze/article/details/124201088 本文使用案例…...

后端 springboot 给 vue 提供参数
前端 /** 发起新增或修改的请求 */requestAddOrEdit(formData) {debuggerif(formData.id undefined) {formData.id }getAction(/material/getNameModelStandard, {standard: this.model.standard,name: this.model.name,model: this.model.model}).then((res) > {if (res …...

《vue3实战》运用radio单选按钮或Checkbox复选框实现单选多选的试卷制作
文章目录 目录 系列文章目录 1.《Vue3实战》使用axios获取文件数据以及走马灯Element plus的运用 2.《Vue3实战》用路由实现跳转登录、退出登录以及路由全局守护 3.《vue3实战》运用Checkbox复选框实现单选多选的试卷展现(本文) 文章目录 前言 radio是什…...

排序算法-冒泡排序(C语言实现)
简介😀 冒泡排序是一种简单但效率较低的排序算法。它重复地扫描待排序元素列表,比较相邻的两个元素,并将顺序错误的元素交换位置,直到整个列表排序完成。 实现🧐 以下内容为本人原创,经过自己整理得出&am…...

星际争霸之小霸王之小蜜蜂(一)
目录 前言 一、安装pygame库 1、pygame库简介 2、在windows系统安装pygame库 二 、搭建游戏框架 1、创建游戏窗口 2、改变窗口颜色 总结 前言 大家应该都看过或者都听说过python神书“大蟒蛇”,上面有一个案例是《外星人入侵》,游戏介绍让我想起了上…...

图数据库_Neo4j基于docker服务版安装_Neo4j Desktop桌面版安装---Neo4j图数据库工作笔记0004
然后我们来看看如何用docker来安装Neo4j community server 首先去执行docker pull neo4j:3.5.22-community 去拉取镜像 然后执行命令就可以安装了 可以用docker ps查看一下 看看暴露了哪些端口 然后再看一下访问一下这个时候,要用IP地址了注意 然后再来看一下安装Desktop 去下…...

docker-compose部署可道云
文章目录 一. Mac1.1 下载源码1.2 部署1.2.1 修改密码部署(可忽略)1.2.2 直接部署 1.3 卸载1.4 访问 二. Win2.1 下载源码2.2 部署2.2.1 修改密码部署(可忽略)2.2.2 直接部署 2.3 卸载 一. Mac 1.1 下载源码 mkdir -p /Users/wanfei/docker-compose && cd /Users/wan…...

Windows上使用FFmpeg实现本地视频推送模拟海康协议rtsp视频流
场景 Nginx搭建RTMP服务器FFmpeg实现海康威视摄像头预览: Nginx搭建RTMP服务器FFmpeg实现海康威视摄像头预览_nginx rtmp 海康摄像头_霸道流氓气质的博客-CSDN博客 上面记录的是使用FFmpeg拉取海康协议摄像头的rtsp流并推流到流媒体服务器。 如果在其它业务场景…...

单片机之从C语言基础到专家编程 - 4 C语言基础 - 4.8 运算符
1.算术运算符 运算符名称备注加法运算符双目运算,a b-减法运算符双目运算,a - b*乘法运算符双目运算,a * b/除法运算符双目运算,a / b%求余运算符双目运算, a % b自增运算符单目运算, a–自减运算符单目运算, a– 2.关系运算符…...

轮腿机器人的PID控制
1 PID介绍 PID(Proportional Integral Derivative)控制系统。其实质是根据输入的偏差值,按比例、积分、微分的函数关系进行运算,运算结果用以输出进行控制。它是在长期的工程实践中总结出来的一套控制方法,实际运行经…...

ChatGPT爆火,会给教育带来什么样的影响或者冲击?
近来,人工智能聊天机器人ChatGPT连上热搜,火爆全网。ChatGPT拥有强大的信息整合能力、自然语言处理能力,可谓是“上知天文,下知地理”,而且还能根据要求进行聊天、撰写文章等。 ChatGPT一经推出,便迅速在社…...

Servlet+JDBC实战开发书店项目讲解第三篇:商品查询实现
ServletJDBC实战开发书店项目讲解第三篇:商品查询实现 本篇博客将介绍如何在ServletJDBC实战开发书店项目中实现商品查询功能。我们将从设计数据库表结构和实体类开始,一步一步详细讲解代码实现过程,包括前端页面的设计和后端Servlet代码的编…...

爬虫逆向实战(十七)--某某丁简历登录
一、数据接口分析 主页地址:某某丁简历 1、抓包 通过抓包可以发现数据接口是submit 2、判断是否有加密参数 请求参数是否加密? 通过查看“载荷”模块可以发现有一个enPassword加密参数 请求头是否加密? 通过查看请求头可以发现有一个To…...

《安富莱嵌入式周报》第320期:键盘敲击声解码, 军工级boot设计,开源CNC运动控制器,C语言设计笔记,开源GPS车辆跟踪器,一键生成RTOS任务链表
周报汇总地址:嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 视频版: https://www.bilibili.com/video/BV1Cr4y1d7Mp/ 《安富莱嵌入式周报》第320期:键盘敲击…...

DRF 缓存
应用环境 django4.2.3 ,python3.10 由于对于服务而言,有些数据查询起来比较费时,所以,对于有些数据,我们需要将其缓存。 最近做了一个服务,用的时 DRF 的架构,刚好涉及缓存,特此记…...

Collada .dae文件格式简明教程【3D】
当你从互联网下载 3D 模型时,可能会在格式列表中看到 .dae 格式。 它是什么? 推荐:用 NSDT编辑器 快速搭建可编程3D场景。 1、Collada DAE概述 COLLADA是COLLAborative Design Activity(中文:协作设计活动)…...

在K8s上处理nginx
基本说明 创建一个名为ssl的TLS类型的Secret对象,用于存储证书和密钥信息。 kubectl create secret tls ssl --certserver.crt --keyserver.key配置Nginx的events块,设置worker连接数为1024。 events {worker_connections 1024; }配置Nginx的http块&a…...

嵌入式:ARM Day4
一、自己编写代码实现三盏灯点亮 源码: .text .global _start _start: 进行一次初始化bl RCC_INITbl LED1_INITbl LED2_INITbl LED3_INITb looploop: 循环开关灯bl LED1_ONbl delay_1sbl LED1_OFFbl delay_1sbl LED2_ONbl delay_1sbl LED2_OFFbl delay_1sbl…...

SpringBoot案例-员工管理-分页条件查询
根据页面原型,明确需求 页面原型 需求 查看接口文档 接口文档的链接如下: 【腾讯文档】SpringBoot案例所需文档 https://docs.qq.com/doc/DUkRiTWVaUmFVck9N 思路分析 分页条件查询就时将条件查询的结果进行分页展示,由于有的条件可能设…...

python控制obs实现无缝切换场景!obs-websocket-py
前言 最近一直在研究孪生数字人wav2lip。目前成果可直接输入高清嘴型,2070显卡1分钟音频2.6分钟输出。在直播逻辑上可以做到1比1.3这样,所以现在开始研究直播。在逻辑上涉及到了无缝切换,看到csdn上有一篇文章还要vip解锁。。。那自己研究吧…...

Vue3实现图片懒加载及自定义懒加载指令
Vue3实现图片懒加载及自定义懒加载指令 前言1.使用vue3-lazyload插件2.自定义v-lazy懒加载指令2.1 使用VueUse2.2 使用IntersectionObserver 前言 图片懒加载是一种常见性能优化的方式,它只去加载可视区域图片,而不是在网页加载完毕后就立即加载所有图片…...

LeetCode150道面试经典题-- 环形链表(简单)
1.题目 给你一个链表的头节点 head ,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&…...

音视频学习-音视频基础
文章目录 一、 音视频录制原理二、音视频播放原理三、图像基础概念1.像素2.分辨率3.位深4.帧率5.码率6.Stride跨距 四、RGB、YUV1.RGB2.YUV1. 4:4:4格式2. 4:2:2格式3. 4:2:0格式4. 4:2:0数据格式对比 3.RGB和YUV的转换4.YUV Stride对齐问题 五、视频的主要概念1.基本概念2.I P…...

asp.net core webapi如何执行周期性任务
使用Api执行周期性任务 第一种,无图形化界面1.新建类,继承IJob,在实现的方法种书写需要周期性执行的事件。2.编写方法类,定义事件执行方式3.在启动方法中,进行设置,.net 6中在program.cs的Main方法中&#…...

快速搭建图书商城小程序的简易流程与优势
很多人喜欢阅读电子书,又有很多人依旧喜欢实体书,而实体书店拥有一个图书商城小程序便成为了满足用户需求的理想选择。如果您也想进入这一充满潜力的领域,但担心开发难度和复杂流程,别担心!您能做到快速搭建一个专业、…...

C++ template 循环
在元编程循环中,我们不需要用while,for来循环,一般情况下都要用递归,例如: #include <iostream> using namespace std; template <int Head, int...Data> constexpr static int num Head num<Data..…...

时序预测 | MATLAB实现基于CNN-BiGRU卷积双向门控循环单元的时间序列预测-递归预测未来(多指标评价)
时序预测 | MATLAB实现基于CNN-BiGRU卷积双向门控循环单元的时间序列预测-递归预测未来(多指标评价) 目录 时序预测 | MATLAB实现基于CNN-BiGRU卷积双向门控循环单元的时间序列预测-递归预测未来(多指标评价)预测结果基本介绍程序设计参考资料 预测结果 基本介绍 MATLAB实现基于…...

mysql 数据备份和恢复
操作系统:22.04.1-Ubuntu mysql 版本:8.033 binlog 介绍 binlog 是mysql 二进制日志 binary log的简称,可以简单理解为数据的修改记录。 需要开启binlog,才会产生文件,mysql 8.0 默认开启,开启后可以在 /var/lib/mysql ÿ…...

Lucene教程_编程入门自学教程_菜鸟教程-免费教程分享
教程简介 Lucene是apache软件基金会 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引…...

物联网工程应用实训室建设方案
一、物联网工程应用系统概述 1.1物联网工程定义 物联网工程(Internet of Things Engineering)是一种以信息技术(IT)来改善实体世界中人们生活方式的新兴学科,它利用互联网技术为我们的日常生活活动提供服务和增益&am…...