网络协议栈(15)超越进程生命期的TCP套接字

一、套接口超越进程生命期
一般来说,当一个进程退出的时候(不论是主动还是被动),它都会关闭自己的文件描述符,但是对于TCP的套接字来说,它的情况比较特殊,具体怎么特殊呢?我们可以想象一下,TCP是一个有连接的链路,当进程关闭的时候,关闭一端应该告诉对方,也就是发送一个FIN消息到对方,从而让对方有所准备。当对方收到这个FIN报文之后,这个报文对被动断开方最为直接和重要的影响就是:被动段开端的所有读操作不会阻塞,所以读操作可以马上返回,没有数据时返回值为零。反过来说,如果说被动断开方没有收到这个FIN报文,那么它的读操作可能会永远等待下去,而这个进程(至少是一个线程)也相当于已经报废,所以这个断交的FIN报文对被动断开方有极为重要的作用。
既然如此,当主动断开方关闭TCP套接口的时候,它就应该尽量保证这个FIN已经被对方收到,保证的方法就是收到对方对这个FIN的确认报文。现在的问题是,假设此时和被动断开方的链路已经不通(例如对方主机断点、物理链路被城市施工破坏、防火墙等等吧),此时发送方如何保证这个FIN报文会被发送,它将会进行怎样的尝试?
二、关闭套接口
假设说任务退出的时候执行了一个已经建立的TCP连接,此时执行close或者shutdown操作,套接口会应声向对方发送FIN报文并尝试等待对方对这个报文的回应。
void tcp_close(struct sock *sk, long timeout)
if (data_was_unread) {当套接口关闭的时候,己方的套接口中还有一些没有被用户读走的数据,那么直接关闭套接口,向对方发送reset(而不是fin)
        /* Unread data was tossed, zap the connection. */
        NET_INC_STATS_USER(LINUX_MIB_TCPABORTONCLOSE);
        tcp_set_state(sk, TCP_CLOSE);
        tcp_send_active_reset(sk, GFP_KERNEL);
    } else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {未知,暂时不管
        /* Check zero linger _after_ checking for unread data. */
        sk->sk_prot->disconnect(sk, 0);
        NET_INC_STATS_USER(LINUX_MIB_TCPABORTONDATA);
    } else if (tcp_close_state(sk)) {对于一个处于ESTABLISHED状态的套接口,新状态为TCP_FIN_WAIT1,并发送FIN报文,所以会执行条件中的tcp_send_fin函数
        tcp_send_fin(sk);
    }
……
    sock_orphan(sk);这个函数比较重要,它设置了套接口一个关键的状态,那就是SOCK_DEAD,函数内代码为    sock_set_flag(sk, SOCK_DEAD);
……
    /* It is the last release_sock in its life. It will remove backlog. */
    release_sock(sk);释放套接口,并且调用套接口的process_backlog方法,这样,如果之前tcp_send_fin发送的FIN报文确认已经返回,那么套接口将会转换为FIN_WAIT2状态由于此时我们假设链路已经不通,所以套接口还是会停留在FIN_WAIT1状态
……
    if (sk->sk_state != TCP_CLOSE) {如果系统中孤儿套接口数量大于配置值,直接发送reset并关闭套接口
        sk_stream_mem_reclaim(sk);
        if (atomic_read(sk->sk_prot->orphan_count) > sysctl_tcp_max_orphans ||
            (sk->sk_wmem_queued > SOCK_MIN_SNDBUF &&
             atomic_read(&tcp_memory_allocated) > sysctl_tcp_mem[2])) {
            if (net_ratelimit())
                printk(KERN_INFO "TCP: too many of orphaned "
                       "sockets\n");
            tcp_set_state(sk, TCP_CLOSE);
            tcp_send_active_reset(sk, GFP_ATOMIC);
            NET_INC_STATS_BH(LINUX_MIB_TCPABORTONMEMORY);
        }
    }
……
    if (sk->sk_state == TCP_CLOSE)由于套接口处于FIN_WAIT1状态,所以不会释放套接口
        inet_csk_destroy_sock(sk);
    /* Otherwise, socket is reprieved until protocol close. */
也就是说,当链路不通的时候,进程执行了套接口的close操作之后,这个套接口坚强的存活了下来,即使创建这个套接口的进程已经退出,这也就是套接口成为“孤儿”套接口的原因。此时,当我们通过
netstat -p
显示系统套接口的时候,可以发现又处于FIN_WAIT1状态的套接口,但是它没有所属任务。
三、孤儿套接口生存期有多长
当套接口成为孤儿之后,它的行为其实和通常套接口行为完全相同,它的FIN报文同样存在超时重传机制,也就是该套接口依然会尽职尽责的坚持向对方发送这个FIN报文,并且不断的设置超时定时器。TCP的超时定时器是在inet_csk_destroy_sock--->>>sk->sk_prot->destroy(sk)-->>>tcp_v4_destroy_sock--->>>tcp_clear_xmit_timers(sk)--->>inet_csk_clear_xmit_timers
    sk_stop_timer(sk, &icsk->icsk_retransmit_timer);
    sk_stop_timer(sk, &icsk->icsk_delack_timer);
    sk_stop_timer(sk, &sk->sk_timer);
关闭的,所以这个孤儿套接口依然会在定时器的驱动下,按照指数退避策略坚持发送这个FIN报文,那么它重试的次数是多少呢?我们看一下定时器的超时时间判断:
static int tcp_write_timeout(struct sock *sk)

        if (sock_flag(sk, SOCK_DEAD)) {由于之前说过在tcp_close--->>sock_orphan已经设置了SOCK_DEAD标志,所以该条件满足
            const int alive = (icsk->icsk_rto < TCP_RTO_MAX);

            retry_until = tcp_orphan_retries(sk, alive);
        }
    if (icsk->icsk_retransmits >= retry_until) {
        /* Has it gone just too far? */
        tcp_write_err(sk);
        return 1;
    }
当套接口已经成为孤儿时,它的重试时间是会被人歧视的,它的重试机会比正常处于连接态的套接口重试次数少(这是默认情况,当然也可以通过系统参数调节),该值可以通过sysctl_tcp_orphan_retries变量调节,默认值为零,但是tcp_orphan_retries函数做了特殊处理,使其默认值为8。再精确一点说,当重试次数大于TCP_RTO_MAX(120)秒时,如果sysctl_tcp_orphan_retries还为零,会立即放弃重传。如果按照默认值(sysctl_tcp_orphan_retries为零的情况),最多重试时间不会大于120*2=240s=4min
四、孤儿套接口如何消亡
tcp_write_timeout--->>tcp_write_err--->>>tcp_done
    if (!sock_flag(sk, SOCK_DEAD))
        sk->sk_state_change(sk);
    else
        inet_csk_destroy_sock(sk);对于孤儿套接口,满足该分支,并在该分支中释放套接口资源。
五、它对一些现象的解释
对于一个服务器程序,如果物理链路断开并杀死进程,然后马上重启服务器程序,那么绑定端口十有八九会失败,因为这个残余的FIN_WAIT1套接口依然占用者服务器上的端口。
这一点和之前的time_wait状态不同。time_wait状态是主动断开方已经收到了对方对FIN的ACK,并且也ACK了对方的FIN,这个time_wait是一个结束缓冲。
六、TCP写入操作一些错误码解释
1、链路不通。
此时会由于定时器超时而进入tcp_write_err函数,其中代码为
    sk->sk_err = sk->sk_err_soft ? : ETIMEDOUT;
2、对方重置
static void tcp_reset(struct sock *sk)
{
    /* We want the right error as BSD sees it (and indeed as we do). */
    switch (sk->sk_state) {
        case TCP_SYN_SENT:
            sk->sk_err = ECONNREFUSED;连接时重置返回连接拒绝
            break;
        case TCP_CLOSE_WAIT:
            sk->sk_err = EPIPE;对方已关闭时返回管道断裂
            break;
        case TCP_CLOSE:
            return;
        default:
            sk->sk_err = ECONNRESET;默认直接返回连接重置
    }
 
 
 
 
 

posted on 2019-03-06 21:41  tsecer  阅读(596)  评论(0编辑  收藏  举报

导航