Linux tcp/ip 源码分析 - connection termination
前两篇文章中我们讲到,shutdown和close方法会发送fin消息给对方,开始tcp连接的关闭流程,现在我们从源码角度看下tcp连接关闭的具体过程,以及中间发送的消息和涉及到的各种状态。
假设客户端先调用了shutdown方法,发起了tcp连接关闭请求,由之前的文章我们可以知道,此时客户端会从TCP_ESTABLISHED状态切换到TCP_FIN_WAIT1状态,shutdown方法同时会发送fin消息给服务端。
下面看下服务端如何处理fin消息的。
由之前的文章可知,ip层在收到消息之后,会通过回调tcp_v4_rcv方法将消息转给tcp层。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
...
const struct tcphdr *th;
...
struct sock *sk;
int ret;
...
th = (const struct tcphdr *)skb->data;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
return ret;
...
}
该方法会先根据ip、端口等信息,找到对应的sk,再调用tcp_v4_do_rcv方法,继续处理fin消息。
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
...
tcp_rcv_established(sk, skb, tcp_hdr(skb));
return 0;
}
...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
由于此时服务端的sk->sk_state处于TCP_ESTABLISHED状态,所以skb的处理流程会进入到tcp_rcv_established方法。
// net/ipv4/tcp_input.c
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th)
{
...
tcp_data_queue(sk, skb);
...
tcp_ack_snd_check(sk);
return;
...
}
EXPORT_SYMBOL(tcp_rcv_established);
该方法先调用tcp_data_queue处理fin消息,再调用tcp_ack_snd_check方法
// net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
...
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
...
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
...
return;
}
...
}
由上可见,fin消息的处理流程会进入到tcp_fin方法。
// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
...
sk->sk_shutdown |= RCV_SHUTDOWN;
sock_set_flag(sk, SOCK_DONE);
switch (sk->sk_state) {
...
case TCP_ESTABLISHED:
/* Move to CLOSE_WAIT */
tcp_set_state(sk, TCP_CLOSE_WAIT);
...
break;
...
}
...
}
方法描述
1. 标记sk->sk_shutdown字段包含RCV_SHUTDOWN。
由上两篇文章可以知道,shutdown和close方法在发送fin消息之前,会先标记sk->sk_shutdown字段包含SEND_SHUTDOWN,即发送fin消息后,客户端是SEND_SHUTDOWN,服务端是RCV_SHUTDOWN。
2. 设置sk的flag为SOCK_DONE。
3. 由于此时服务端的sk状态还是TCP_ESTABLISHED,所以该方法会调用tcp_set_state方法,将sk->sk_state状态设置为TCP_CLOSE_WAIT,即等待应用层关闭tcp连接。
tcp_fin方法处理完之后,上面的tcp_ack_snd_check方法会发送ack给客户端,现在我们再从客户端角度看下ack消息的处理流程。
还是先看tcp_v4_rcv方法。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
...
const struct tcphdr *th;
...
struct sock *sk;
...
th = (const struct tcphdr *)skb->data;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
}
该方法会先根据ip、端口等信息找到对应的sk,再调用tcp_v4_do_rcv方法,对这个sk继续进行ack消息的逻辑处理。
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (tcp_rcv_state_process(sk, skb)) {
...
}
return 0;
...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
该方法又调用了tcp_rcv_state_process方法
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
switch (sk->sk_state) {
...
case TCP_FIN_WAIT1: {
...
tcp_set_state(sk, TCP_FIN_WAIT2);
sk->sk_shutdown |= SEND_SHUTDOWN;
...
}
...
}
...
return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);
由于客户端的sk此时处于TCP_FIN_WAIT1状态,当收到ack消息后,该方法会将sk的状态修改为TCP_FIN_WAIT2。
至此,由客户端到服务端的tcp流就已经被完全关闭。
当服务端应用层调用了shutdown或close方法后,根据前两篇文章我们可以知道,shutdown或close方法会先调用tcp_close_state方法,将服务端sk的状态由TCP_CLOSE_WAIT修改为TCP_LAST_ACK,然后再调用tcp_send_fin方法,发送fin消息给客户端。
继续看下客户端fin消息的处理流程,还是从tcp_v4_rcv方法看起。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
...
const struct tcphdr *th;
...
struct sock *sk;
...
th = (const struct tcphdr *)skb->data;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
}
处理流程和之前差不多,最终还是调用tcp_v4_do_rcv方法。
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (tcp_rcv_state_process(sk, skb)) {
...
}
return 0;
...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
该方法又会调用tcp_rcv_state_process方法。
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
switch (sk->sk_state) {
...
case TCP_FIN_WAIT2:
...
/* Fall through */
case TCP_ESTABLISHED:
tcp_data_queue(sk, skb);
...
break;
}
...
return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);
由于此时客户端的sk的状态为TCP_FIN_WAIT2,该方法最终会调用tcp_data_queue继续处理fin消息。
// net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
...
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
...
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
...
return;
}
...
}
该方法又调用了tcp_fin方法。
// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
...
switch (sk->sk_state) {
...
case TCP_FIN_WAIT2:
/* Received a FIN -- send ACK and enter TIME_WAIT. */
tcp_send_ack(sk);
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
break;
...
}
...
}
由于此时客户端的sk是TCP_FIN_WAIT2状态,所以会执行如上代码
1. 调用tcp_send_ack方法,发送ack给服务端。
2. 调用tcp_time_wait方法,先将sk状态设置为TCP_TIME_WAIT,再开启TIME_WAIT定时,超时后销毁这个sk。
当sk处于TCP_TIME_WAIT状态时,会一直占用对应的ip和端口,防止其他连接再次使用,从而出现错误。
由上可见,谁先发起的tcp连接关闭请求,谁最终就会进入到TIME_WAIT状态,在写服务器端代码时,这个是要注意的。
现在剩下最后一步,即服务端ack消息的处理。
还是从tcp_v4_rcv方法看起。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
...
const struct tcphdr *th;
...
struct sock *sk;
...
th = (const struct tcphdr *)skb->data;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
}
和之前一样,该方法最终还是会调用tcp_v4_do_rcv方法。
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (tcp_rcv_state_process(sk, skb)) {
...
}
return 0;
...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
该方法又调用了tcp_rcv_state_process方法。
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
...
switch (sk->sk_state) {
...
case TCP_LAST_ACK:
if (tp->snd_una == tp->write_seq) {
...
tcp_done(sk);
goto discard;
}
break;
}
...
return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);
由于此时服务器的sk状态为TCP_LAST_ACK,所以该方法最终会调用tcp_done方法。
// net/ipv4/tcp.c
void tcp_done(struct sock *sk)
{
...
tcp_set_state(sk, TCP_CLOSE);
...
sk->sk_shutdown = SHUTDOWN_MASK;
...
}
EXPORT_SYMBOL_GPL(tcp_done);
该方法会先将sk状态设置为TCP_CLOSE,之后再设置sk->sk_shutdown字段的值为SHUTDOWN_MASK,即SEND_SHUTDONW & RCV_SHUTDOWN。
至此,服务端的sk就已经完全关闭。
客户端的sk等TIME_WAIT状态的定时超时之后,也会自动关闭。
这样,tcp连接的关闭流程就完整了。
- HDUOJ-----Computer Transformation
- Veeam Backup & Replication(二):添加虚拟化主机和配置备份存储
- HDUOJ 2672---god is a girl 《斐波那契数》
- Golang语言--一些基础用法
- 复杂SQL性能优化的剖析(一)(r11笔记第36天)
- hduoj1073--Online Judge
- Golang语言--- channel
- Golang语言--将byte的int转换
- HDUOJ------Daydream字符查找-并求其始末位置
- HDUOJ------敌兵布阵
- 复杂SQL性能优化的剖析(二)(r11笔记第37天)
- HDUOJ-------Being a Good Boy in Spring Festival
- Golang语言--slice 切片原理
- GoLang语言--的函数运用
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- IAT HOOK
- 形式化分析工具(六):HLPSL Tutorial
- 推荐一款技术人必备的接口测试神器:Apifox
- GO 文档笔记
- 魔改npm私有仓库 | Verdaccio教程
- 【Vulnhub】AI Web 2.0
- Python迭代器和生成器
- Python深层解析json数据之JsonPath
- Linux 下的 pstack 工具安装及简单应用
- range函数小应用
- 今日算法题,请查收
- 如何将根证书预置到 firefox 浏览器发布包中
- Vue + Flask 实战开发系列(九)
- 推荐几款快速管理 Kubernetes 多集群环境的神器
- 1. 初识Jackson -- 世界上最好的JSON库