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;
    }
}

 

内核版本 4.15.0

 

你可能感兴趣的:(TCPIP协议)