预分配缓存额度sk_forward_alloc与发送缓存队列统计sk_wmem_queued一同用于计算当前套接口所占用的内存量。sk_forward_alloc属于为套接口预分配,所以缓存并没有实际分配出去。
一、sk_forward_alloc初始化
对于面向连接的套接口类型如TCP,在创建子套接口时,将其sk_forward_alloc初始化为0。
struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority)
{
newsk = sk_prot_alloc(sk->sk_prot, priority, sk->sk_family);
if (newsk != NULL) {
newsk->sk_forward_alloc = 0;
}
二、sk_forward_alloc预分配
套接口内存的页面是按照SK_MEM_QUANTUM的大小为单位,定义为4096(4K),与大多数系统的PAGE_SIZE定义相同。曾经在老一点版本的内核中,SK_MEM_QUANTUM直接使用PAGE_SIZE宏定义,导致一些将页面大小PAGE_SIZE定义为64K字节的系统,预分配不必要的大量内存额度。
#define SK_MEM_QUANTUM 4096
#define SK_MEM_QUANTUM_SHIFT ilog2(SK_MEM_QUANTUM)
static inline int sk_mem_pages(int amt)
{
return (amt + SK_MEM_QUANTUM - 1) >> SK_MEM_QUANTUM_SHIFT;
}
但是,sk_forward_alloc的单位还是以字节表示,只不过其大小为SK_MEM_QUANTUM的整数倍。 预分配额度的基础函数为__sk_mem_schedule如下,函数__sk_mem_raise_allocated判断此次分配是否符合协议的内存限定,如果不符合,需要回退预分配的额度。
int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
int ret, amt = sk_mem_pages(size);
sk->sk_forward_alloc += amt << SK_MEM_QUANTUM_SHIFT;
ret = __sk_mem_raise_allocated(sk, size, amt, kind);
if (!ret)
sk->sk_forward_alloc -= amt << SK_MEM_QUANTUM_SHIFT;
return ret;
}
__sk_mem_raise_allocated函数还将增加协议总内存的占用统计(memory_allocated),其单位为SK_MEM_QUANTUM大小的页面数量(第三个参数amt)。由于内核的网络协议内存限额是以PAGE_SIZE大小页面为单位,如TCP协议,可通过PROC文件/proc/sys/net/ipv4/tcp_mem查看。所以在进行比较时,内核使用函数sk_prot_mem_limits将限定的页面数值转换为以SK_MEM_QUANTUM为单位的页面值。
static inline long sk_prot_mem_limits(const struct sock *sk, int index)
{
long val = sk->sk_prot->sysctl_mem[index];
#if PAGE_SIZE > SK_MEM_QUANTUM
val <<= PAGE_SHIFT - SK_MEM_QUANTUM_SHIFT;
#elif PAGE_SIZE < SK_MEM_QUANTUM
val >>= SK_MEM_QUANTUM_SHIFT - PAGE_SHIFT;
#endif
return val;
}
函数__sk_mem_schedule的封装函数有两个sk_wmem_schedule和sk_rmem_schedule,对应于发送SK_MEM_SEND和接收SK_MEM_RECV两个类别的缓存使用。对于sk_wmem_schedule函数,如果请求的大小在预分配额度内,进行正常分配,否则,由__sk_mem_schedule函数分配新的额度。
static inline bool sk_wmem_schedule(struct sock *sk, int size)
{
if (!sk_has_account(sk))
return true;
return size <= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_SEND);
}
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_forced_mem_schedule,与以上的__sk_mem_schedule函数不同,如果内存额度不够,其强制进行缓存额度的预分配,而不管是否超出网络协议的内存限定。用在比如FIN报文发送等情况下,其不必等待可尽快结束一个连接,否则可能导致FIN报文的延迟或者放弃发送FIN而关闭连接,其结束后又可释放连接的缓存占用。
void sk_forced_mem_schedule(struct sock *sk, int size)
{
if (size <= sk->sk_forward_alloc)
return;
amt = sk_mem_pages(size);
sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
sk_memory_allocated_add(sk, amt);
}
三、sk_forward_alloc预分配额度使用
sk_forward_alloc预分配额度使用有一对sk_mem_charge和sk_mem_uncharge函数组成。在得到套接口预分配额度后,函数sk_mem_charge可由额度中获取一定量的数值使用。
static inline void sk_mem_charge(struct sock *sk, int size)
{
if (!sk_has_account(sk))
return;
sk->sk_forward_alloc -= size;
}
函数sk_mem_uncharge可将一定量回填到预分配额度中。如果sk_forward_alloc预分配额度大于2M字节,立即回收1M字节,预分配额度没有必要太大,立即回收以防止sk_mem_reclain回收不及时导致溢出。
static inline void sk_mem_uncharge(struct sock *sk, int size)
{
if (!sk_has_account(sk))
return;
sk->sk_forward_alloc += size;
/* Avoid a possible overflow.
* TCP send queues can make this happen, if sk_mem_reclaim()
* is not called and more than 2 GBytes are released at once.
*
* If we reach 2 MBytes, reclaim 1 MBytes right now, there is
* no need to hold that much forward allocation anyway.
*/
if (unlikely(sk->sk_forward_alloc >= 1 << 21))
__sk_mem_reclaim(sk, 1 << 20);
}
在清空发送队列函数tcp_write_queue_purge和清空重传队列函数tcp_rtx_queue_purge,以及重传队列元素移除函数tcp_rtx_queue_unlink_and_free中调用sk_wmem_free_skb释放skb,并且uncharge预分配的额度。
static inline void sk_wmem_free_skb(struct sock *sk, struct sk_buff *skb)
{
sock_set_flag(sk, SOCK_QUEUE_SHRUNK);
sk->sk_wmem_queued -= skb->truesize;
sk_mem_uncharge(sk, skb->truesize);
__kfree_skb(skb);
}
四、sk_forward_alloc预分配额度回收
基础的回收函数为__sk_mem_reclaim,以上介绍的sk_mem_uncharge函数也会使用到。除此之外,内核使用两个函数回收预分配内存额度:分别为sk_mem_reclaim和sk_mem_reclaim_partial函数。前者回收之后有可能将额度全部回收或者仅留下小于SK_MEM_QUANTUM大小的额度;而后者不会全部回收额度,其在额度大于SK_MEM_QUANTUM时,执行回收操作,并且保证留下小于SK_MEM_QUANTUM大小的额度。
void __sk_mem_reclaim(struct sock *sk, int amount)
{
amount >>= SK_MEM_QUANTUM_SHIFT;
sk->sk_forward_alloc -= amount << SK_MEM_QUANTUM_SHIFT;
__sk_mem_reduce_allocated(sk, amount);
}
static inline void sk_mem_reclaim(struct sock *sk)
{
if (!sk_has_account(sk))
return;
if (sk->sk_forward_alloc >= SK_MEM_QUANTUM)
__sk_mem_reclaim(sk, sk->sk_forward_alloc);
}
static inline void sk_mem_reclaim_partial(struct sock *sk)
{
if (!sk_has_account(sk))
return;
if (sk->sk_forward_alloc > SK_MEM_QUANTUM)
__sk_mem_reclaim(sk, sk->sk_forward_alloc - 1);
}
五、sk_forward_alloc预分配时机
在TCP重要的skb缓存分配函数sk_stream_alloc_skb中,如果TCP协议总的内存处于承压状态,首先回收部分预分配缓存,因为马上要为skb分配内存,不应进行全部回收。在分配skb之后有两种情况,如果指定了强制分配force_schedule参数,即强制增加分配额度而不进行内存超限判断;否则,使用sk_wmem_schedule进行额度分配。只有在分配额度成功之后返回分配的skb,反之释放skb返回失败。
struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp, bool force_schedule)
{
if (unlikely(tcp_under_memory_pressure(sk)))
sk_mem_reclaim_partial(sk);
skb = alloc_skb_fclone(size + sk->sk_prot->max_header, gfp);
if (likely(skb)) {
if (force_schedule) {
mem_scheduled = true;
sk_forced_mem_schedule(sk, skb->truesize);
} else {
mem_scheduled = sk_wmem_schedule(sk, skb->truesize);
}
if (likely(mem_scheduled)) {
return skb;
}
__kfree_skb(skb);
}
}
对于sk_stream_alloc_skb函数的使用,发生在TCP发送路径上比如tcp_sendmsg_locked和do_tcp_sendpages发送函数,分片函数tcp_fragment和tso_fragment,以及tcp_mtu_probe、tcp_send_syn_data和tcp_connect函数。sk_stream_alloc_skb函数获取到了相应的缓存额度,紧接其后就需要使用此额度。如函数tcp_mtu_probe,其调用sk_mem_charge使用了skb的truesize长度的分配额度。
static int tcp_mtu_probe(struct sock *sk)
{
/* We're allowed to probe. Build it now. */
nskb = sk_stream_alloc_skb(sk, probe_size, GFP_ATOMIC, false);
sk->sk_wmem_queued += nskb->truesize;
sk_mem_charge(sk, nskb->truesize);
}
另外,对于TCP发送函数tcp_sendmsg_locked和do_tcp_sendpages,如果此时套接口发送队列以及重传队列为空,将强制为此数据包分配额度,其额度的使用在函数skb_entail中完成。如果skb已经没有可用的空间,内核需要将数据拷贝到skb的共享页面中,首先sk_wmem_schedule检查额度使用够用,如果额度不足并且不能分配出所需的额度,跳转到等待处理。在数据复制函数skb_copy_to_page_nocache中,使用sk_mem_charge使用预分配的额度。
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
while (msg_data_left(msg)) {
skb = tcp_write_queue_tail(sk);
if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
first_skb = tcp_rtx_and_write_queues_empty(sk);
skb = sk_stream_alloc_skb(sk, select_size(sk, sg, first_skb), sk->sk_allocation, first_skb);
skb_entail(sk, skb);
}
if (skb_availroom(skb) > 0) {
} else if (!uarg || !uarg->zerocopy) {
struct page_frag *pfrag = sk_page_frag(sk);
if (!sk_wmem_schedule(sk, copy))
goto wait_for_memory;
err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb, pfrag->page, pfrag->offset, copy);
}
}
}
六、sk_forward_alloc回收时机
在套接口关闭销毁的时候,回收预分配额度。如函数tcp_close和inet_sock_destruct,或者接收到对端发送的FIN报文,如tcp_fin函数。
void tcp_close(struct sock *sk, long timeout)
{
sk_mem_reclaim(sk);
}
void inet_sock_destruct(struct sock *sk)
{
struct inet_sock *inet = inet_sk(sk);
sk_mem_reclaim(sk);
}
TCP的延时ACK处理函数tcp_delack_timer_handler,首先会调用函数sk_mem_reclaim_partial回收部分预分配额度,在执行最后,如果网络协议内存处于承压状态,还会调用sk_mem_reclaim回收函数。另外,在TCP超时重传函数tcp_write_timer_handler和keepalive超时函数中也有调用sk_mem_reclaim回收函数。
void tcp_delack_timer_handler(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
sk_mem_reclaim_partial(sk);
out:
if (tcp_under_memory_pressure(sk))
sk_mem_reclaim(sk);
}
void tcp_write_timer_handler(struct sock *sk)
{
sk_mem_reclaim(sk);
}
static void tcp_keepalive_timer (struct timer_list *t)
{
sk_mem_reclaim(sk);
}
以上的延时ACK处理函数,或者TCP超时重传处理函数,在TCP套接口的release_sock函数中也都有调用。内核需要保持回收函数的及时调用,保证可用额度。
void release_sock(struct sock *sk)
{
if (sk->sk_prot->release_cb)
sk->sk_prot->release_cb(sk);
}
void tcp_release_cb(struct sock *sk)
{
if (flags & TCPF_WRITE_TIMER_DEFERRED) {
tcp_write_timer_handler(sk);
__sock_put(sk);
}
if (flags & TCPF_DELACK_TIMER_DEFERRED) {
tcp_delack_timer_handler(sk);
__sock_put(sk);
}
}
最后,对发送队里的清除操作,也会伴随预分配额度的回收,如函数sk_stream_kill_queues和tcp_write_queue_purge函数。
void sk_stream_kill_queues(struct sock *sk)
{
/* Account for returned memory. */
sk_mem_reclaim(sk);
}
void tcp_write_queue_purge(struct sock *sk)
{
sk_mem_reclaim(sk);
}
七、sk_forward_alloc超限判断
在函数__sk_mem_schedule预分配额度时,使用函数__sk_mem_raise_allocated判断TCP协议内存是否超过限定值。如下在协议内存承压状态下,如果当前套接口的发送队列缓存、接收缓存已经预分配缓存之和所占用的页面数,乘以当前套接口协议的所有套接口数量,小于系统设定的最大协议内存限值的话(TCP协议:/proc/sys/net/ipv4/tcp_mem),说明还有内存空间可供分配使用。
int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
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;
}
}