• TCP协议之《套接口热迁移REPAIR模式》


    要实现TCP套接口的热迁移,必须能够实现在迁移之前保存套接口的当前状态,迁移之后还原套接口的状态。Linux内核中为支持TCP套接口热迁移实现了REPAIR模式以及相关的操作。迁移流程如下,首先启用REPAIR模式后,应用层开始保存当前状态;迁移后进行状态还原,最后关闭REPAIR模式,开始正常工作。
    另外,明确一点,处于LISTEN状态的套接口不能做热迁移。

    static inline bool tcp_can_repair_sock(const struct sock *sk)
    {
        return ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN) && (sk->sk_state != TCP_LISTEN);
    }
    REPAIR模式操作的TCP套接口状态有:数据队列(发送/重传和接收)、序列号、TCP选项、时间戳和窗口。


    一、TCP_PREPAIR选项
    应用层使用setsockopt系统调用,TCP选项名称TCP_REPAIR来开启或者关闭repair模式。函数tcp_can_repair_sock保证不符合条件的套接口不进行处理。设置值为1开启,反之为0关闭。宏SK_FORCE_REUSE使能端口重用,以便负责热迁移的套接口可以访问迁移对象套接口的相关数据。

    static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen)
    {
        switch (optname) {
        case TCP_REPAIR:
            if (!tcp_can_repair_sock(sk))
                err = -EPERM;
            else if (val == 1) {
                tp->repair = 1;
                sk->sk_reuse = SK_FORCE_REUSE;
                tp->repair_queue = TCP_NO_QUEUE;
            } else if (val == 0) {
                tp->repair = 0;
                sk->sk_reuse = SK_NO_REUSE;
                tcp_send_window_probe(sk);
            }
            break;
    }

    在关闭repair模式的同时,调用函数tcp_send_window_probe,对于处在TCP_ESTABLISHED状态的套接口,将发送一个ACK报文,其序列号使用上次对端ACK确认的最后一个字节数据的序号,这样在对端设备接收到之后,其发现ACK中的序列号已经确认过,将会使用正确的ACK序号回应一个ACK报文,以便纠正另一端错误的序列号。同时,通过此数据包本端也可接收到对端更新的窗口大小。

    void tcp_send_window_probe(struct sock *sk)
    {
        if (sk->sk_state == TCP_ESTABLISHED) {
            tcp_sk(sk)->snd_wl1 = tcp_sk(sk)->rcv_nxt - 1;
            tcp_mstamp_refresh(tcp_sk(sk));
            tcp_xmit_probe_skb(sk, 0, LINUX_MIB_TCPWINPROBE);
        }
    }

    二、数据备份还原

    内核套接口数据包括缓存区中未发送或未被确认的数据,以及未被应用程序读取的接收缓存中的数据。在热迁移开始后,应用层需要将内核套接口接收缓存中未读取的数据全部读取出来。之后通过setsockopt设置TCP_REPAIR_QUEUE选项的值为TCP_SEND_QUEUE,还是通过recvmsg函数将发送缓存中数据读取出来保存到热迁移应用控制程序中。迁移完之后,再把这些数据还原到对应的socket缓存中。

    static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen)
    {
        switch (optname) {
        case TCP_REPAIR_QUEUE:
            if (!tp->repair)
                err = -EPERM;
            else if (val < TCP_QUEUES_NR)
                tp->repair_queue = val;
            break;
    }
    如函数tcp_recvmsg所示,当设置了要备份发送队列(TCP_SEND_QUEUE)的数据后,调用recvmsg函数,内核将套接口的重传队列tcp_rtx_queue和发送队列sk_write_queue中的数据返回给应用层。

    int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len)
    {
        if (unlikely(tp->repair)) {
            if (tp->repair_queue == TCP_SEND_QUEUE)
                goto recv_sndq;
        }
    recv_sndq:
        err = tcp_peek_sndq(sk, msg, len);
        goto out;
    }
    static int tcp_peek_sndq(struct sock *sk, struct msghdr *msg, int len)
    {
        skb_rbtree_walk(skb, &sk->tcp_rtx_queue) {
            err = skb_copy_datagram_msg(skb, 0, msg, skb->len);
            copied += skb->len;
        }
        skb_queue_walk(&sk->sk_write_queue, skb) {
            err = skb_copy_datagram_msg(skb, 0, msg, skb->len);
            copied += skb->len;
        }
    }

    热迁移完成之后,首先还原接收队列数据,通过do_tcp_setsockopt将repair_queue设置为TCP_RECV_QUEUE,之后使用sendmsg系统调用将之前备份的接收队列数据发送到内核,内核函数tcp_sendmsg_locked通过tcp_send_rcvq函数将数据重新添加到套接口的接收队列。之后关闭repair_queue选项,将备份的发送队列数据下发到内核中。

    int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
    {
        if (unlikely(tp->repair)) {
            if (tp->repair_queue == TCP_RECV_QUEUE) {
                copied = tcp_send_rcvq(sk, msg, size);
                goto out_nopush;
            }
        }   
    }
    在备份发送数据的过程中,REPAIR模式处在开启状态,以保证这些数据不会发送出去。

    int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
    {
        while (msg_data_left(msg)) {
            if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {            
                if (tp->repair)
                    TCP_SKB_CB(skb)->sacked |= TCPCB_REPAIRED;
            }
            if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
                continue;
     
            if (forced_push(tp)) {
                tcp_mark_push(tp, skb);
                __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
            } else if (skb == tcp_send_head(sk))
                tcp_push_one(sk, mss_now);
        }
    }

    三、序列号的备份还原
    通过getsockopt的TCP_QUEUE_SEQ选项实现,保存套接口中目前写入的最大序列号和下一个要接收的序列号值。static int do_tcp_getsockopt(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen)
    {
        struct tcp_sock *tp = tcp_sk(sk);
     
        switch (optname) {
        case TCP_QUEUE_SEQ:
            if (tp->repair_queue == TCP_SEND_QUEUE)
                val = tp->write_seq;
            else if (tp->repair_queue == TCP_RECV_QUEUE)
                val = tp->rcv_nxt;
            break;
        }
    }

    在热迁移完成之后,函数do_tcp_setsockopt负责序列号的还原,要求套接口处于关闭状态TCP_CLOSE。

    static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen)
    {
        switch (optname) {
        case TCP_QUEUE_SEQ:
            if (sk->sk_state != TCP_CLOSE)
                err = -EPERM;
            else if (tp->repair_queue == TCP_SEND_QUEUE)
                tp->write_seq = val;
            else if (tp->repair_queue == TCP_RECV_QUEUE)
                tp->rcv_nxt = val;
            break;
        }
    }

    四、TCP选项的备份还原
    函数setsockopt的选项TCP_REPAIR_OPTIONS负责TCP选项的恢复功能,只有状态为TCP_ESTABLISHED的套接口才可进行此操作,具体由函数tcp_repair_options_est完成。

    static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen)
    {
        switch (optname) {
        case TCP_REPAIR_OPTIONS:
            if (sk->sk_state == TCP_ESTABLISHED)
                err = tcp_repair_options_est(sk, (struct tcp_repair_opt __user *)optval, optlen);
            break;
    }
    还原的TCP选项有:TCPOPT_MSS、TCPOPT_WINDOW、TCPOPT_SACK_PERM和TCPOPT_TIMESTAMP。其中只有前两个选项需要提供设置的数值,后两个下发0即可。

    static int tcp_repair_options_est(struct sock *sk, struct tcp_repair_opt __user *optbuf, unsigned int len)
    {
        while (len >= sizeof(opt)) {
            switch (opt.opt_code) {
            case TCPOPT_MSS:
                tp->rx_opt.mss_clamp = opt.opt_val;
                tcp_mtup_init(sk);
                break;
            case TCPOPT_WINDOW:
                {
                    u16 snd_wscale = opt.opt_val & 0xFFFF;
                    u16 rcv_wscale = opt.opt_val >> 16;
     
                    if (snd_wscale > TCP_MAX_WSCALE || rcv_wscale > TCP_MAX_WSCALE)
                        return -EFBIG;
                    tp->rx_opt.snd_wscale = snd_wscale;
                    tp->rx_opt.rcv_wscale = rcv_wscale;
                    tp->rx_opt.wscale_ok = 1;
                }
                break;
            case TCPOPT_SACK_PERM:
                if (opt.opt_val != 0)
                    return -EINVAL;
                tp->rx_opt.sack_ok |= TCP_SACK_SEEN;
                break;
            case TCPOPT_TIMESTAMP:
                if (opt.opt_val != 0)
                    return -EINVAL;
                tp->rx_opt.tstamp_ok = 1;
                break;
            }
        }
    }

    五、时间戳的备份与还原
    static int do_tcp_getsockopt(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen)
    {
        struct tcp_sock *tp = tcp_sk(sk);
     
        switch (optname) {
        case TCP_TIMESTAMP:
            val = tcp_time_stamp_raw() + tp->tsoffset;
            break;
        }
    }
    static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen)
    {
        switch (optname) {
        case TCP_TIMESTAMP:
            if (!tp->repair)
                err = -EPERM;
            else
                tp->tsoffset = val - tcp_time_stamp_raw();
            break;
        }
    }

    六、TCP窗口的备份还原
    static int do_tcp_getsockopt(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen)
    {
        struct tcp_sock *tp = tcp_sk(sk);
     
        switch (optname) {
        case TCP_REPAIR_WINDOW: {
            struct tcp_repair_window opt;
     
            opt.snd_wl1 = tp->snd_wl1;
            opt.snd_wnd = tp->snd_wnd;
            opt.max_window  = tp->max_window;
            opt.rcv_wnd = tp->rcv_wnd;
            opt.rcv_wup = tp->rcv_wup;
        }
    }
     
    static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen)
    {
        switch (optname) {
        case TCP_REPAIR_WINDOW:
            err = tcp_repair_set_window(tp, optval, optlen);
            break;
        }
    }
    static int tcp_repair_set_window(struct tcp_sock *tp, char __user *optbuf, int len)
    {
        struct tcp_repair_window opt;
     
        if (opt.max_window < opt.snd_wnd)
            return -EINVAL;
     
        if (after(opt.snd_wl1, tp->rcv_nxt + opt.rcv_wnd))
            return -EINVAL;
     
        if (after(opt.rcv_wup, tp->rcv_nxt))
            return -EINVAL;
     
        tp->snd_wl1 = opt.snd_wl1;
        tp->snd_wnd = opt.snd_wnd;
        tp->max_window  = opt.max_window;
        tp->rcv_wnd = opt.rcv_wnd;
        tp->rcv_wup = opt.rcv_wup;
    }

    七、连接connect
    如果当前TCP套接口处在REPAIR模式,connect系统调用直接将状态修改为TCP_ESTABLISHED,不会进行建立连接的三次握手,不会发出SYN数据包。

    int tcp_connect(struct sock *sk)
    {   
        struct tcp_sock *tp = tcp_sk(sk);
            
        if (unlikely(tp->repair)) {
            tcp_finish_connect(sk, NULL);
            return 0;
        }
    }

  • 相关阅读:
    程序内hook键盘
    香港服务器SAS和SATA硬盘比较,有哪些不同?
    VSCode 配置 Lua 开发环境(清晰明了)
    [附源码]java毕业设计流浪动物领养系统
    leetcode 90双周赛
    Javascript正则表达式常用的验证(验证手机号,电话,邮箱,网址等)
    美团基于 Flink 的实时数仓平台建设新进展
    论文笔记:Pointing Novel Objects in Image Captioning
    FPGA-1、verilog书写基本格式
    自定义终结符:EOF
  • 原文地址:https://blog.csdn.net/wuyongmao/article/details/126266413