写在前面:
版本信息:
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){//绑定地址结构体和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方法。文章来源:https://www.toymoban.com/news/detail-662764.html
// 当达到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模板网!