要实现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选项、时间戳和窗口。
应用层使用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;
}
}
函数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;
}
}
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;
}
如果当前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