一、sk_forward_alloc预分配额度
接收路径中使用函数sk_rmem_schedule分配缓存额度,使用宏SK_MEM_RECV表示此次是为接收而分配。如果请求的缓存大小在预分配额度之内,可马上进行正常分配,否则,由__sk_mem_schedule函数分配新的额度。如果分配失败,但是此skb是由内存的PFMEMALLOC保留区分配而来,内核忽略之前的失败,返回成功。另外,还可使用函数sk_forced_mem_schedule强制分配额度。
static inline bool sk_rmem_schedule(struct sock *sk, struct sk_buff *skb, int size)
{
if (!sk_has_account(sk))
return true;
return size<= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_RECV) || skb_pfmemalloc(skb);
}
sk_forward_alloc预分配额度使用由一对sk_mem_charge和sk_mem_uncharge函数组成。在得到套接口预分配额度后,函数sk_mem_charge可由额度中获取一定量的数值使用,函数sk_mem_uncharge可释放一定的额度。sk_mem_charge函数假定预分配额度足够使用,两个函数都不会做缓存超限判断。另外,与发送路径上的函数skb_set_owner_w类似,内核使用skb_set_owner_r封装了sk_mem_charge函数,用于接收路径。
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
skb_orphan(skb);
skb->sk = sk;
skb->destructor = sock_rfree;
atomic_add(skb->truesize, &sk->sk_rmem_alloc);
sk_mem_charge(sk, skb->truesize);
}
对于L2TP,PACKET,RAW和PING等协议的类型的套接口,内核使用函数__sock_queue_rcv_skb将数据包接收到套接口的sk_receive_queue队列中。在此之前,使用sk_rmem_schedule函数检查以及分配缓存额度,继而调用skb_set_owner_r函数使用预分配的额度。
int __sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
struct sk_buff_head *list = &sk->sk_receive_queue;
if (!sk_rmem_schedule(sk, skb, skb->truesize)) {
atomic_inc(&sk->sk_drops);
return -ENOBUFS;
}
skb_set_owner_r(skb, sk);
__skb_queue_tail(list, skb);
}
TCP的核心预分配缓存额度函数为tcp_try_rmem_schedule,如果无法分配缓存额度,将首先调用tcp_prune_queue函数尝试合并sk_receive_queue中的数据包skb以减少空间占用,如果空间仍然不足,最后调用tcp_prune_ofo_queue函数清理乱序数据包队列(out_of_order_queue)。
static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb, unsigned int size)
{
if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
!sk_rmem_schedule(sk, skb, size)) {
if (tcp_prune_queue(sk) < 0)
return -1;
while (!sk_rmem_schedule(sk, skb, size)) {
if (!tcp_prune_ofo_queue(sk))
return -1;
}
}
}
函数tcp_prune_queue和tcp_prune_ofo_queue在清理空间之后,都会使用函数sk_mem_reclaim回收空间。
static int tcp_prune_queue(struct sock *sk)
{
tcp_collapse_ofo_queue(sk);
if (!skb_queue_empty(&sk->sk_receive_queue))
tcp_collapse(sk, &sk->sk_receive_queue, NULL, skb_peek(&sk->sk_receive_queue), NULL, tp->copied_seq, tp->rcv_nxt);
sk_mem_reclaim(sk);
}
函数tcp_try_rmem_schedule在TCP的接收路径中调用,比如tcp_data_queue函数和tcp_data_queue_ofo函数,以及用于TCP套接口REPAIR模式的tcp_send_rcvq函数。如下为tcp_data_queue函数,如果接收队列sk_receive_queue为空,使用sk_forced_mem_schedule函数强制分配缓存额度,否则,使用tcp_try_rmem_schedule函数进行正常分配并检查缓存是否超限。最后使用tcp_queue_rcv函数完成接收工作,同时通过调用skb_set_owner_r函数使用分配的缓存额度。函数tcp_send_rcvq的缓存相关操作与此类似。
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
/* Ok. In sequence. In window. */
queue_and_out:
if (skb_queue_len(&sk->sk_receive_queue) == 0)
sk_forced_mem_schedule(sk, skb->truesize);
else if (tcp_try_rmem_schedule(sk, skb, skb->truesize))
goto drop;
eaten = tcp_queue_rcv(sk, skb, 0, &fragstolen);
if (skb->len)
tcp_event_data_recv(sk, skb);
}
函数tcp_data_queue_ofo与以上两者的不同在于,其直接使用skb_set_owner_r函数使用预分配的缓存额度。
static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
{
if (unlikely(tcp_try_rmem_schedule(sk, skb, skb->truesize))) {
tcp_drop(sk, skb);
return;
}
end:
if (skb)
skb_set_owner_r(skb, sk);
}
二、sk_forward_alloc额度的使用与回填
预分配额度使用函数skb_set_owner_r将skb的销毁回调函数destructor设置为sock_rfree函数,其调用sk_mem_uncharge函数将skb占用的缓存回填到预分配额度sk_forward_alloc中。
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
skb->destructor = sock_rfree;
sk_mem_charge(sk, skb->truesize);
}
void sock_rfree(struct sk_buff *skb)
{
unsigned int len = skb->truesize;
atomic_sub(len, &sk->sk_rmem_alloc);
sk_mem_uncharge(sk, len);
}
另外,在TCP接收过程中,内核尝试将新接收到的数据包合并到之前的数据包中(sk_receive_queue接收队列的末尾数据包),参见函数tcp_try_coalesce,如果合并成功,将释放被合并的skb所占用的缓存,所以其skb结构体所占用缓存空间不需要使用缓存额度,仅需要计算其数据部分的缓存额度(delta)。
static bool tcp_try_coalesce(struct sock *sk, struct sk_buff *to, struct sk_buff *from, bool *fragstolen)
{
if (!skb_try_coalesce(to, from, fragstolen, &delta))
return false;
atomic_add(delta, &sk->sk_rmem_alloc);
sk_mem_charge(sk, delta);
}
如果合并失败,内核将skb添加到接收队列sk_receive_queue中,使用skb_set_owner_r函数将整个skb占用的缓存空间计入缓存额度。
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen, bool *fragstolen)
{
struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);
__skb_pull(skb, hdrlen);
eaten = (tail && tcp_try_coalesce(sk, tail, skb, fragstolen)) ? 1 : 0;
if (!eaten) {
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
}
}
三、sk_forward_alloc额度回收
在套接口关闭销毁的时候,回收预分配缓存额度。如函数tcp_close和inet_sock_destruct。或者接收到对端发送的FIN报文,要关闭TCP连接时,如tcp_fin函数。以及在函数tcp_try_rmem_schedule中,调用的tcp_prune_queue和tcp_prune_ofo_queue两个函数。
static bool tcp_prune_ofo_queue(struct sock *sk)
{
node = &tp->ooo_last_skb->rbnode;
do {
prev = rb_prev(node);
rb_erase(node, &tp->out_of_order_queue);
tcp_drop(sk, rb_to_skb(node));
sk_mem_reclaim(sk);
if (atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf && !tcp_under_memory_pressure(sk))
break;
node = prev;
} while (node);
}
另外,在TCP入队列函数tcp_data_queue和tcp_rcv_established的快速路径中,都会调用到数据接收事件函数tcp_event_data_recv,如果此连接的两次接收时间之差大于当前的重传超时时长RTO,回收一次缓存额度sk_mem_reclaim。
static void tcp_event_data_recv(struct sock *sk, struct sk_buff *skb)
{
if (!icsk->icsk_ack.ato) {
} else {
int m = now - icsk->icsk_ack.lrcvtime;
if (m <= TCP_ATO_MIN / 2) {
icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + TCP_ATO_MIN / 2;
} else if (m < icsk->icsk_ack.ato) {
icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + m;
if (icsk->icsk_ack.ato > icsk->icsk_rto)
icsk->icsk_ack.ato = icsk->icsk_rto;
} else if (m > icsk->icsk_rto) {
/* Too long gap. Apparently sender failed to
* restart window, so that we send ACKs quickly.
*/
tcp_incr_quickack(sk);
sk_mem_reclaim(sk);
}
}
}
在tcp_rcv_established函数的快速路径中,如果检测到当前正在接收的数据包skb占用的缓存长度大于套接口预分配的缓存额度,跳转到慢速路径执行。
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
{
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt && !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
if (len <= tcp_header_len) {
} else {
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;
}
}
}
四、sk_forward_alloc超限判断
基础预分配缓存额度函数sk_rmem_schedule调用__sk_mem_raise_allocated函数进行协议缓存超限判断。如果协议总内存(allocated)还没有超出最大值(TCP协议:/proc/sys/net/ipv4/tcp_mem的第三个值),并且,套接口占用的接收缓存sk_rmem_alloc小于设定的接收缓存最小值(/proc/sys/net/ipv4/tcp_rmem第一个值),说明内存充足返回成功。如果协议总内存处于承压状态,但是当前套接口的发送队列缓存、接收缓存以及预分配缓存之和所占用的页面数,乘以当前套接口协议类型的所有套接口数量,小于系统设定的最大协议内存限值的话(TCP协议:/proc/sys/net/ipv4/tcp_mem),说明还有内存空间可供分配使用。
int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
if (allocated > sk_prot_mem_limits(sk, 2))
goto suppress_allocation;
/* guarantee minimum buffer size under pressure */
if (kind == SK_MEM_RECV) {
if (atomic_read(&sk->sk_rmem_alloc) < sk_get_rmem0(sk, prot))
return 1;
}
if (sk_has_memory_pressure(sk)) {
if (!sk_under_memory_pressure(sk))
return 1;
alloc = sk_sockets_allocated_read_positive(sk);
if (sk_prot_mem_limits(sk, 2) >
alloc * sk_mem_pages(sk->sk_wmem_queued + atomic_read(&sk->sk_rmem_alloc) + sk->sk_forward_alloc))
return 1;
}
}