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秒一次
  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报文,关闭连接,释放资源~!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员李哈

创作不易,希望能给与支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值