Linux内核源码剖析之TCP保活机制(KeepAlive)

news/2024/7/20 9:27:47 标签: linux, 运维, 服务器, Linux内核

写在前面:

版本信息:

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秒一次
  1. Linux内核中默认关闭KeepAlive
  2. 开启KeepAlive后,默认2小时后往对端发送心跳包,检查是否还活着
  3. 默认后续每75秒往对端发送心跳包,检查是否还活着
  4. 默认当对端9次都没有响应报文就发送RST报文,断开TCP连接,释放资源!
  5. 当然这一切参数都可以配置,通过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){//绑定地址结构体和socket
        printf("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;
		else
			tp->keepalive_intvl = val * HZ;
		break;
	case TCP_KEEPCNT:			// 设置阈值次数
		if (val < 1 || val > MAX_TCP_KEEPCNT)
			err = -EINVAL;
		else
			tp->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报文,关闭连接,释放资源~!


http://www.niftyadmin.cn/n/4943175.html

相关文章

酒店管理系统哪家好?的修设备售后服务管理系统有什么用?

随着人们的生活水平不断提升&#xff0c;越来越多的人开始对五星级酒店的品质提出更高的要求。这些规模宏大、设施齐全的酒店&#xff0c;需要日常进行复杂的维护工作才能保持正常运转。然而&#xff0c;传统的人工维保早已无法满足信息化管理的需求。为了更好地管理这些设备设…

mybatis-plus批量update数据,且更新条件不是主键id

业务场景 List<StudentEntity> studentEntityList new ArrayList<StudentEntity>();// todo: 依据sno批量修改Data TableName("student") public class StudentEntity implements Serializable {private static final long serialVersionUID 1L;TableI…

Vue项目(购物车)

目录 购物车效果展示&#xff1a; 购物车代码&#xff1a; 购物车效果展示&#xff1a; 此项目添加、修改、删除数据的地方都写了浏览器都会把它存储起来 下次运行项目时会把浏览器数据拿出来并在页面展示 Video_20230816145047 购物车代码&#xff1a; 复制完代码&#xff0…

react-native-webview RN和html双向通信

rn登录后得到的token需要传递给网页&#xff0c;js获取到的浏览器信息需要传递给rn RN Index.js: import React from react import { WebView } from react-native-webview import useList from ./useListexport default function Index(props) {const { uri, jsCode, webVie…

Python爬虫——scrapy_项目结构和基本方法

scrapy项目结构 项目名字项目名字spider文件夹&#xff08;存储的是爬虫文件&#xff09;init自定义的爬虫文件 *核心功能文件inititems 定义数据结构的地方 爬取的数据都包含哪些middlewares 中间件 代理机制pipelines 管道 用来处理下载的数据settin…

华为OD机试-字符串序列判定

题目描述 给定两个字符串 s和 t &#xff0c;判断 s是否为 t 的子序列。 你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长&#xff08;长度n ~ 500,000&#xff09;&#xff0c;而 s 是个短字符串&#xff08;长度 <100&#xff09;。字符串的一个子序列是原…

【C语言】13-数组之二维数组

0. 什么是二维数组 前面学习了一维数组,了解了数组的特点和一维数组的使用方法。但是只有一维数组是远远不够的,现实生活中有很多情况使用一维数组并不能得到很好的解决。例如要统计一个班级的单科平均成绩、班级总平均成绩就需要使用倒二维数组了 二维数组通常称为矩阵(ma…

学习Vue:组件的概念和优势

在现代的前端开发中&#xff0c;组件化开发是一种重要的方法&#xff0c;它可以将复杂的应用程序拆分成多个独立的、可复用的组件。Vue.js 是一个流行的前端框架&#xff0c;它支持组件化开发&#xff0c;让开发者能够更轻松地构建和维护复杂的用户界面。在本文中&#xff0c;我…