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

这篇具有很好参考价值的文章主要介绍了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报文,关闭连接,释放资源~!文章来源地址https://www.toymoban.com/news/detail-662764.html

到了这里,关于Linux内核源码剖析之TCP保活机制(KeepAlive)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux内核TCP参数调优全面解读

    TCP 性能的提升不仅考察 TCP 的理论知识,还考察了对于操心系统提供的内核参数的理解与应用。 TCP 协议是由操作系统实现,所以操作系统提供了不少调节 TCP 的参数。 如何正确有效的使用这些参数,来提高 TCP 性能是一个不那么简单事情。我们需要针对 TCP 每个阶段的问题来

    2024年02月11日
    浏览(61)
  • Linux内核源码分析 (B.4) 深度剖析 Linux 伙伴系统的设计与实现

    Linux内核源码分析 (B.4) 深度剖析 Linux 伙伴系统的设计与实现 在上篇文章 《深入理解 Linux 物理内存分配全链路实现》 中,笔者为大家详细介绍了 Linux 内存分配在内核中的整个链路实现: image.png 但是当内核执行到 get_page_from_freelist 函数,准备进入伙伴系统执行具体内存分配

    2024年02月07日
    浏览(46)
  • 【Linux 内核源码分析】RCU机制

    Linux内核的RCU(Read-Copy-Update)机制是一种用于实现高效读取和并发更新数据结构的同步机制。它在保证读操作不被阻塞的同时,也能够保证数据的一致性。 RCU的核心思想是通过延迟资源释放来实现无锁读取,并且避免了传统锁带来的争用和开销。具体而言,RCU维护了一个“回

    2024年01月15日
    浏览(74)
  • Linux0.12内核源码解读(2)-Bootsect.S

    大家好,我是呼噜噜,在上一篇文章聊聊x86计算机启动发生的事?我们了解了x86计算机启动过程,MBR、0x7c00是什么?其中当bios引导结束后,操作系统接过计算机的控制权后,发生了哪些事?本文将揭开迷雾的序章- Bootsect.S 我们先来回顾一下,上古时期计算机按下电源键的启

    2024年04月12日
    浏览(40)
  • 【Linux内核解析-linux-5.14.10-内核源码注释】关于Linux同步机制知识点整理

    在Linux系统中,同步机制是操作系统中非常重要的一部分,以下是一些基本要点: 什么是同步机制?同步机制是一种操作系统提供的机制,用于协调多个进程或线程之间的访问共享资源,防止出现竞态条件和死锁等问题。 Linux中常用的同步机制有哪些?Linux中常用的同步机制

    2024年02月04日
    浏览(50)
  • linux内核TCP/IP源码浅析

    linux内核源码下载:https://cdn.kernel.org/pub/linux/kernel/ 我下载的是:linux-5.11.1.tar.gz linux源码在线看:https://elixir.bootlin.com/linux/v5.11/source 1,一般网卡接收数据是以触发中断来接收的,在网卡driver中,接收到数据时,往kernel的api:netif_rx()丢。 2,接着数据被送到IP层ip_local_deliver_f

    2024年02月13日
    浏览(61)
  • 《ARM Linux内核源码剖析》读书笔记——0号进程(init_task)的创建时机

    最近在读《ARM Linux内核源码剖析》,一直没有看到0号进程(init_task进程)在哪里创建的。直到看到下面这篇文章才发现书中漏掉了set_task_stack_end_magic(init_task)这行代码。 下面这篇文章提到:start_kernel()上来就会运行 set_task_stack_end_magic(init_task)创建初始进程。init_task是静态定义的

    2024年01月17日
    浏览(66)
  • Linux内核源码分析 (6)RCU机制及内存优化屏障

    问题: RCU 英文全称为 Read-Copy-Update ,顾名思义就是 读-拷贝-更新 ,是 Linux 内核中重要的同步机制。 Linux 内核已有原子操作、读写信号量等锁机制,为什么要单独设计一个比较复杂的新机制? RCU的原理 RCU记录所有指向共享数据的指针的使用者,当要修改该共享数据时,首先

    2024年02月10日
    浏览(58)
  • Unity-TCP-网络聊天功能(四): 消息粘包、心跳机制保活(心跳包)、断线重连

    bug1:下线后,如果发送多条消息,在客户端上线时,一瞬间接收到,效果如同粘包,需要拆包。举例,连续发送三条160长度消息,可能实际显示2条消息,原因,第三条消息和第二条消息粘包,第二条消息长度变为320,但是Receive方法没有考虑这个问题,相当于这段代码只运行

    2024年02月11日
    浏览(38)
  • Netty源码剖析之FastThreadLocal机制

    版本信息: JDK1.8 Netty-all:4.1.38.Final 讲netty的FastThreadLocal机制,就不得不提及到JDK自带的ThreadLocal机制,所以下面会用一小段篇幅介绍一下ThreadLocal机制~ ThreadLocal的机制,大致的解释为线程本地变量,独属于线程,所以每个线程有一份,所以互相独立、隔离、线程安全、线程中

    2024年02月10日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包