zoukankan      html  css  js  c++  java
  • 网络协议栈(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 ");
                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;默认直接返回连接重置
        }
     
     
     
     
     
  • 相关阅读:
    音频、摄像机操作
    调用系统相机及摄像机
    图片的放大缩小
    haxm intelx86加速模拟器的安装
    mac eclipse 下安装subclipse
    文件多线程下载实现
    windows与linux之间传输文件
    ZeroMQ接口函数之 :zmq_setsockopt –设置ZMQ socket的属性
    使用C语言在windows下一口气打开一批网页
    Net-SNMP是线程安全的吗
  • 原文地址:https://www.cnblogs.com/tsecer/p/10486313.html
Copyright © 2011-2022 走看看