Linux TCP拥塞控制接口
Linux TCP拥塞控制接口
本文分析所采用的Linux版本为4.18
Linux系统中的TCP拥塞控制采用面向对象的设计思想,提供拥塞控制接口用于实现不同的拥塞控制策略,主要实现文件在tcp_cong.c中。
基本数据结构
tcp_cong_list为拥塞支持的拥塞控制策略(算法实现)的列表,列表中的每一表项对应一种拥塞控制策略的具体实现。
tcp_cong_list_lock为控制并发访问tcp_cong_list的自旋锁。
tcp_congestion_ops结构为拥塞控制的接口,暴露出可以修改实现的接口函数。
struct tcp_congestion_ops {struct list_head list;u32 key;u32 flags;/* 初始化私有数据(可选) */void (*init)(struct sock *sk);/* 清除私有数据(可选) */void (*release)(struct sock *sk);/* 返回慢启动阈值(必须的) */u32 (*ssthresh)(struct sock *sk);/* 计算新的cwnd值(必须的) */void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);/* 改变ca_state前调用(可选) */void (*set_state)(struct sock *sk, u8 new_state);/* 当cwnd事件发生时调用(可选) */void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);/* ack到达时调用(可选) */void (*in_ack_event)(struct sock *sk, u32 flags);/* 丢包后新的cwnd值(必须的) */void (*undo_cwnd)(struct sock *sk);/* 数据包ack计数的钩子(可选的) */void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);/* 覆盖sysctl_tcp_min_tso_segs */u32 (*min_tso_segs)(struct sock *sk);/* 返回tcp_sndbuf_expand中使用的multiplier(可选的)*/u32 (*sndbuf_expand)(struct sock *sk);/* 在所有ca_state处理之后,当数据包交付以更新cwnd和pacing rate时调用(可选的)*/void (*cong_control)(struct sock *sk, const struct rate_sample *rs);/* 获取inet_diag的信息(可选的)*/size_t (*get_info)(struct sock *sk, u32 ext, int *attr,union tcp_cc_info *info);char name[TCP_CA_NAME_MAX];struct module *owner;
}
当需要定义新的拥塞控制策略时,只需要修改相应的接口函数即可。
以下代码给出tcp_reno控制策略的接口实现。
struct tcp_congestion_ops tcp_reno = {.flags = TCP_CONG_NON_RESTRICTED,.name = "reno",.owner = THIS_MODULE,.ssthresh = tcp_reno_ssthresh,.cong_avoid = tcp_reno_cong_avoid,.undo_cwnd = tcp_reno_undo_cwnd,
};
基本操作
tcp_ca_find用于在列表中查找指定名称的拥塞控制算法。该函数使用简单的线性查找,当表项较多时会导致性能下降。
static struct tcp_congestion_ops *tcp_ca_find(const char *name)
{struct tcp_congestion_ops *e;/* 遍历列表项 */list_for_each_entry_rcu(e, &tcp_cong_list, list) {if(strcmp(e->name, name) == 0)return e;}return NULL;
}
tcp_ca_find_autoload函数用于查找具有指定名称的拥塞控制算法并自动载入,调用时要保持rcu锁定状态。
首先在列表中查找指定的拥塞控制算法,之后存在两种情况:
- 查找到指定的拥塞控制算法,直接返回。
- 为查找到指定的拥塞控制算法,且内核支持模块动态加载,且具有相应的权限,则尝试加载指定的拥塞控制算法。
static struct tcp_congestion_ops *tcp_ca_find_autoload(struct net *net, const char *name)
{struct tcp_congestion_ops *ca = tcp_ca_find(name);
/* 支持内核模块时,加载该模块 */
#ifdef CONFIG_MODULES// 该算法尚未载入并且具有加载权限if(!ca && capable(CAP_NET_ADMIN) {rcu_read_unlock();request_module("tcp_%s", name);rcu_read_lock();ca = tcp_ca_find(name);}
#endifreturn ca;
}
tcp_ca_find_key用于在列表中查找指定键值(key)的拥塞控制算法。该函数使用简单的线性查找,当表项较多时会导致性能下降。
static struct tcp_congestion_ops *tcp_ca_find_key(u32 key)
{struct tcp_congestion_ops *e;/* 遍历列表项 */list_for_each_entry_rcu(e, &tcp_cong_list, list) {if(e->key == key)return e;}return NULL;
}
tcp_register_congestion_control用于注册新的拥塞控制算法,即将新的拥塞控制算法添加到列表中。
int tcp_register_congestion_control(struct tcp_congestion_ops *ca)
{int ret = 0;/* 所有的拥塞控制算法都必须实现这些操作 */if(!ca->ssthresh || !ca->undo_cwnd ||!(ca->cong_avoid || ca->cong_control)) {pr_err("%s does not implement required ops\n", ca->name);return -EINVAL;}ca->key = jhash(ca->name, sizeof(ca->name), strlen(ca->name));spin_lock(&tcp_cong_list_lock);if(ca->key == TCP_CA_UNSPEC || tcp_ca_find_key(ca->key)) {pr_notice("%s already registered or non-unique key\n", ca->name);return -EEXIST;} else {list_add_tail_rcu(&ca->list, &tcp_cong_list);pr_debug("%s registered\n", ca->name);}spin_unlock(&tcp_cong_list_lock);return ret;
}
EXPORT_SYMBOL_GPL(tcp_register_congestion_control);
与tcp_register_congestion_control函数相反,tcp_unregister_congestion_control函数在列表中删除指定的拥塞控制算法。该函数由模块的remove函数调用。模块的引用计数器保证只有在没有套接字使用该算法是才被删除。
void tcp_unregister_congestion_control(struct tcp_congestion_ops *ca)
{spin_lock(&tcp_cong_list_lock);list_del_rcu(&ca->list);spin_unlock(&tcp_cong_list_lock);/* 在模块完全删除前等待所有的读操作完成。*//* 此时,try_module_get将失败,这是因为模块处于"going"状态(没有引用), 并且module_exit()正在被调用。*/synchronize_rcu();
}
EXPORT_SYMBOL_GPL(tcp_unregister_congestion_control);
tcp_ca_get_key_by_name用于通过名称获取指定拥塞控制算法的键值(key)。ecn_ca为返回值。
u32 tcp_ca_get_key_by_name(struct net *net, const char *name, bool *ecn_ca)
{const struct tcp_congestion_ops *ca;u32 key = TCP_CA_UNSPEC;might_sleep();rcu_read_lock();ca = tcp_ca_find_autoload(net, name);if (ca) {key = ca->key;*ecn_ca = ca->flags & TCP_CONG_NEEDS_ECN;}rcu_read_unlock();return key;
}
EXPORT_SYMBOL_GPL(tcp_ca_get_key_by_name);
tcp_ca_get_name_by_key用于通过键值获取指定拥塞控制算法的名称。
char *tcp_ca_get_key_by_name(u32 key, char *buffer)
{const struct tcp_congestion_ops *ca;char *ret = NULL;rcu_read_lock();ca = tcp_ca_find_key(key);if(ca)ret = strncpy(buffer, ca->name, TCP_CA_NAME_MAX);rcu_read_unlock();return ret;
}
EXPORT_SYMBOL_GPL(tcp_ca_get_name_by_key);
tcp_assign_congestion_control函数用于设置指定sock的拥塞控制算法。
首先,获取系统中设置的拥塞控制算法。如果该算法尚未加载,则将拥塞控制算法设置为tcp_reno;否则,使用系统中设置的拥塞控制算法。
void tcp_assign_congestion_control(struct sock *sk)
{struct net *net = sock_net(sk);struct inet_connection_sock *icsk = inet_csk(sk);const struct tcp_congestion_ops *ca;rcu_read_lock();ca = rcu_dereference(net->ipv4.tcp_congestion_control);if(unlikely(!try_module_get(ca->owner()))ca = &tcp_reno;icsk->icsk_ca_ops = ca;rcu_read_unlock();memset(icsk->icsk_ca_priv, 0, sizeof(icsk->icsk_ca_priv));if(ca->flags & TCP_CONG_NEEDS_ECN)INET_ECN_xmit(sk);elseINET_ECN_dontxmit(sk);
}
tcp_init_congestion_control函数用于初始化指定sock的拥塞控制算法。
void tcp_init_congestion_control(struct sock *sk)
{const struct inet_connection_sock *icsk = inet_csk(sk);tcp_sk(sk)->prior_ssthresh = 0;if(icsk->icsk_ca_ops->init)icsk->icsk_ca_ops->init(sk);if(tcp_ca_needs_ecn(sk))INET_ECN_xmit(sk);elseINET_ECN_dontxmit(sk);
}
tcp_reinit_congestion_control函数用于重新初始化指定sock的拥塞控制算法。
static void tcp_reinit_congestion_control(struct sock *sk, const struct tcp_congestion_ops *ca)
{const struct inet_connection_sock *icsk = inet_csk(sk);tcp_cleanup_congestion_control(sk);icsk->icsk_ca_ops = ca;icsk->icsk_ca_setsockopt = 1;memset(icsk->icsk_ca_priv, 0, sizeof(icsk->icsk->icsk_ca_priv));if(sk->sk_state != TCP_CLOSE)tcp_init_congestion_control(sk);
}
tcp_cleanup_congestion_control用于清除指定sock的拥塞控制算法。
void tcp_cleanup_congestion_control(struct sock *sk)
{struct inet_connection_sock *icsk = inet_csk(sk);if(icsk->icsk_ca_ops->release)icsk->icsk_ca_ops->release(sk);module_put(icsk->icsk_ca_ops->owner);
}
tcp_set_default_congestion_control用于设置默认的拥塞控制算法。sysctl调用该函数。
int tcp_set_default_congestion_control(struct net *net, const char *name)
{struct tcp_congestion_ops *ca;const struct tcp_congestion_ops *prev;int ret;rcu_read_lock();ca = tcp_ca_find_autoload(net, name);if(!ca) {ret = -ENOENT;} else if(!try_module_get(ca->owner)) {ret = -EBUSY;} else {prev = xchg(&net->ipv4.tcp_congestion_control, ca);if(prev)module_put(prev->module);ca->flags |= TCP_CONG_NON_RESTRICTED;ret = 0;}rcu_read_unlock();return ret;
}
tcp_congestion_default函数用于系统启动时通过kernel配置信息设置默认值。
static int __init tcp_congestion_default(void)
{return tcp_set_default_congestion_control(&init_net, CONFIG_DEFAULT_TCP_CONG);
}
late_initcall(tcp_congestion_default);
tcp_get_available_congestion_control用于获取可用的拥塞控制算法
void tcp_get_available_congestion_control(char *buf, size_t maxlen)
{struct tcp_congestion_ops *ca;size_t offs = 0;rcu_read_lock();list_for_each_entry_rcu(ca, &tcp_cong_list, list) {offs += snprintf(buf+offs, maxlen - offs, "%s%s", offs == 0 ? "" : " ", ca->name);}rcu_read_unlock();
}
tcp_get_default_congestion_control用于获取当前的默认拥塞控制算法。
void tcp_get_default_congestion_control(struct net *net, char *name)
{const struct tcp_congestion_ops *ca;rcu_read_lock();ca = rcu_dereference(net->ipv4.tcp_congestion_control);strncpy(name, ca->name, TCP_CA_NAME, MAX);rcu_read_unlock();
}
tcp_get_allowed_congestion_control用于获取non-restricted的拥塞控制算法。
void tcp_get_allowed_congestion_control(char *buf, size_t maxlen)
{struct tcp_congestion_ops *ca;size_t offs = 0;*buf = '\0';rcu_read_lock();list_for_each_entry_rcu(ca, &tcp_cong_list, list) {if(!(ca->flags & TCP_CONG_NON_RESTRICTED))continue;offs += snprintf(buf + offs, maxlen - offs, "%s%s",offs == 0 ? "" : " ", ca->name);}rcu_read_unlock();
}
tcp_set_allowed_congestion_control用于改变non-restricted拥塞控制算法列表。val为以空格分隔的拥塞控制算法名称列表。
int tcp_set_allowed_congestion_control(char *val)
{struct tcp_congestion_ops *ca;char *saved_clone, *clone, *name;int ret = 0;saved_clone = clone = kstrdup(val, CFP_USER);if(!clone)return -ENOMEM;spin_lock(&tcp_cong_list_lock);/* pass 1: 检查bad表项 */while ((name = strsep(&clone, " ")) && *name) {ca = tcp_ca_find(name);if(!ca) {ret = -ENOENT;goto out;}}/* pass 2: 清除旧值 */list_for_each_entry_rcu(ca, &tcp_cong_list, list)ca->flags &= ~TCP_CONG_NON_RESTRICTED;/* pass 3: 标记 */while ((name = strsep(&clone, " ")) && *name) {ca = tcp_ca_find(name);WARN_ON(!ca);if(ca)ca->flags |= TCP_CONG_NON_RESTRICTED;}
out:spin_unlock(&tcp_cong_list_lock);kfree(saved_clone);return ret;
}
tcp_set_congestion_control用于设置指定sock的拥塞控制算法。如果load的值为false,调用者负责调用tcp_init_congestion_control或者tcp_reinit_congestion_control(如果当前的拥塞控制算法已经初始化)。
int tcp_set_congestion_control(struct sock *sk, const char *name, bool laod, bool reinit)
{struct inet_connection_sock *icsk = inet_csk(sk);const struct tcp_congestion_ops *ca;int err = 0;if(icsk->icsk_ca_dst_locked)return -EPERM;rcu_read_lock();if(!load)ca = tcp_ca_find(name);elseca = tcp_ca_find_autoload(sock_net(sk), name);/* 如果当前值和设置值相同,不需要修改 */if (ca == icsk->icsk_ca_ops) {icsk->icsk_ca_setsockopt = 1;goto out;}if(!ca) {err = -ENOENT;} else if(!load) {const struct tcp_congestion_ops *old_ca = icsk->icsk_ca_ops;if(try_module_get(ca->owner)) {if(reinit) {tcp_reinit_congestion_control(sk, ca);} else {icsk->icsk_ca_ops = ca;module_put(old_ca->owner);}} else {err = -EBUSY;}} else if(!((ca->flags & TCP_CONG_NON_RESTRICTED) || ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN))) {err = -EPERM;} else if(!try_module_get(ca->owner)) {err = -EBUSY;} else {tcp_reinit_congestion_control(sk, ca);}
out:rcu_read_unlock();return err;
}
当拥塞窗口的大小不大于慢启动阈值时,使用慢启动过程。tcp_slow_start函数基于RFC2581,且适当地处理stretch ACKs。本函数没有实现RFC3456的Appropriate Byte Counting (ABC)。
u32 tcp_slow_start(struct tcp_sock *tp, u32 acked)
{u32 cwnd = min(tp->snd_cwnd + acked, tp->snd->ssthresh);acked -= cwnd - tp->snd_cwnd;tp->snd_cwnd = min(cwnd, tp->snd_cwnd_clamp);return acked;
}
EXPORT_SYMBOL_GPL(tcp_slow_start);
tcp_cong_avoid_ai函数实现拥塞避免阶段的加法增加。理论上,对每个acked的数据包,更新tp->snd_cwnd += 1/tp->snd_cwnd(或w)。
void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w, u32 acked)
{/* 如果在较高的w值累计信用,则温和地应用 */if(tcp->snd_cwnd_cnt >= w) {tp->snd_cwnd_cnt = 0;tp->snd_cwnd++;}tp->snd_cwnd_cnt += acked;if(tp->snd_cwnd_cnt >= w) {u32 delta = tp->snd_cwnd_cnt / w;tp->snd_cwnd_cnt -= delta * w;tp->snd_cwnd += delta;}tp->snd_cwnd = min(tp->snd_cwnd, tp->snd_cwnd_clamp);
}
EXPORT_SYMBOL_GPL(tcp_cong_avoid_ai);
导出的函数列表
- tcp_register_congestion_control
- tcp_unregister_congestion_control
- tcp_ca_get_key_by_name
- tcp_ca_get_name_by_key
- tcp_slow_start
- tcp_cong_avoid_ai
RFC2581
RFC2581描述了4中拥塞控制算法:慢启动、拥塞避免、快速重传和快速恢复。
慢启动和拥塞避免
发送端控制向网络中发送的数据量。为了实现这些算法,每个TCP连接状态增加两个变量:拥塞窗口(cwnd)为发送端在接收到ACK前可以发送到网络中的数据量;发送端通告窗口(rwnd)主机端数据量限制。cwnd和rwnd的较小值决定了数据传输量。
另外,还需要状态变量慢启动阈值(ssthresh),用于确定使用慢启动算法还是拥塞避免算法。
在向未知状态的网络中发送数据时,需要TCP缓慢地探测网络以确定可用的容量。慢启动算法用于这一目的。
ssthresh的初始值可以任意大(例如,一些实现使用通告窗口大小),但在响应拥塞时减少。当cwnd < ssthresh时,使用慢启动算法,当cwnd > ssthresh时使用拥塞避免算法。当cwnd和ssthresh的值相等时,发送端可以使用慢启动或者拥塞避免。
慢启动期间,对每个收到的新数据的ACK,TCP最多将cwnd的值增加SMSS字节。当cwnd值超过ssthresh或观察到拥塞时,慢启动过程终止。
拥塞避免期间,每RTT时间cwnd的值增加1个全尺寸数据段大小。拥塞避免阶段持续到检测到拥塞。,该式是对上述更新1个全尺寸数据段大小的一种可接受逼近,且在每个非重复ACK时更新。另一种可接受的增加cwnd值的方法是计算新数据ACK确认地字节数(缺点是需要维护附加的状态变量)。当确认地字节数量达到cwnd时,cwnd的值增加SMSS字节。
快速重传/快速恢复
当乱序数据包到达时,TCP接收端需要立即发送重复ACK,用于通告发送端接收到乱序数据包和期望的序列号。
发送端使用快速重传算法检测和恢复丢包,基于重复ACK。快速重传算法使用3个重复ACK(4个相同的ACK,且期间没有其他ACK)作为数据包丢失的指示。发送端接收到3个重复ACK后,执行丢失数据包的重传,而不是等到重传计数器超时。
快速重传算法发送丢失的数据包后,进入快速恢复阶段,直到非重复ACK到达。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
