套接字建立后就会收发数据包。对于发送,数据包是先由应用发给协议栈,由协议栈缓存后发出去的;对于接收,也是先由协议栈将输入数据包缓存,然后才由应用读取走的,这种缓存数据包的行为必然会占用内存。比如应用迟迟不读取数据,那么协议栈就会不停的缓存数据,如果不对套接字占用的内存进行限制,那么很容易会吃光系统内存,内核当然不会让这种事情发生,这篇笔记介绍了内核是对套接字的内存使用进行管理的。
整体策略
无论是接收还是发送,linux内核对内存的使用限制都是在两个层面上进行管控:1) 整个传输层层面的限制;2) 具体某个传输控制块层面的限制。为了实现在两个层面上的内存用量控制,内核定义了一系列的变量,理解这些变量的含义是理解内核相关代码实现的关键。
变量说明
UDP层面的控制变量
UDP层面的内存控制变量全部都定义在了UDP协议结构中,相关字段如下:
/* Networking protocol blocks we attach to sockets.
* socket layer -> transport layer interface
* transport -> network interface is defined by struct inet_proto
*/
struct proto {
...
/* Memory pressure */
void (*enter_memory_pressure)(struct sock *sk);
atomic_t *memory_allocated; /* Current allocated memory. */
struct percpu_counter *sockets_allocated; /* Current number of sockets. */
/*
* Pressure flag: try to collapse.
* Technical note: it is used by multiple contexts non atomically.
* All the __sk_mem_schedule() is of this nature: accounting
* is strict, actions are advisory and have some latency.
*/
// 这些变量之所以是指针,是因为协议栈通用代码在某些上下文会更新这些值
int *memory_pressure;
int *sysctl_mem;
int *sysctl_wmem;
int *sysctl_rmem;
...
};
当然,每个传输层协议也并非要提供所有的字段,按照实际需求提供即可,这些字段的使用见下文代码分析。UDP协议对该结构的实例化如下:
atomic_t udp_memory_allocated;
int sysctl_udp_mem[3] __read_mostly;
int sysctl_udp_wmem_min __read_mostly;
int sysctl_udp_rmem_min __read_mostly;
struct proto udp_prot = {
...
.memory_allocated = &udp_memory_allocated,
.sysctl_mem = sysctl_udp_mem,
.sysctl_wmem = &sysctl_udp_wmem_min, // UDP并未使用该机制
.sysctl_rmem = &sysctl_udp_rmem_min,
...
};
sysctl_mem
和文件/proc/sys/net/ipv4/udp_mem对应的系统参数。该系统参数包含三个值,如上,内核中用数组实现。配置时,这三个成员值应依次增大。如此可以根据占用内存的大小将内存使用情况分成4个等级,如下:
- 已分配用量< sysctl_mem[0]: 内存使用量非常低,没有任何压力;
- sysctl_mem[0] < 已分配用量 < sysctl_mem[1]: 内存使用量还行,没有超过压力值sysctl_mem[1],这时可能会抑制接收;
- sysctl_memp[1] < 已分配用量 < sysctl_mem[2]: 使用量已经超过了压力值,需要重点处理下;
- 已分配用量 > sysctl_mem[2]: 使用量已经超过了硬性限制,此时抑制分配,所有数据包都会被丢弃;
关于这三个门限值的使用细节见下文代码分析。
sysctl_rmem/sysctl_wmem
UDP层面为单个TCB指定的最小内存可用量。当UDP层面的内存用量处于[sysctl_mem[0], sysctl_mem[2]范围时,如果TCB的内存用量没有超过该最小值,那么它也是被允许处理数据包的。UDP只是用了接收方向的内存调度,具体见下面的__sk_mem_schedule()。
memory_allocated
在UDP层面记录已经消耗的系统内存大小,该变量以物理页为单位统计,使用方式见下面的__sk_mem_schedule()。
初始化
上面sysctl_udp_mem、sysctl_udp_rmem_min、sysctl_udp_wmem_min几个变量的初始化如下:
#define SK_MEM_QUANTUM ((int)PAGE_SIZE)
void __init udp_init(void)
{
...
/* Set the pressure threshold up by the same strategy of TCP. It is a
* fraction of global memory that is up to 1/2 at 256 MB, decreasing
* toward zero with the amount of memory, with a floor of 128 pages.
*/
// 根据系统可用物理内存页设置三个门限值
nr_pages = totalram_pages - totalhigh_pages;
limit = min(nr_pages, 1UL<<(28-PAGE_SHIFT)) >> (20-PAGE_SHIFT);
limit = (limit * (nr_pages >> (20-PAGE_SHIFT))) >> (PAGE_SHIFT-11);
limit = max(limit, 128UL);
sysctl_udp_mem[0] = limit / 4 * 3;
sysctl_udp_mem[1] = limit;
sysctl_udp_mem[2] = sysctl_udp_mem[0] * 2;
// 这两个变量都设定为一个物理内存页
sysctl_udp_rmem_min = SK_MEM_QUANTUM;
sysctl_udp_wmem_min = SK_MEM_QUANTUM;
}
传输控制块层面的限制
在传输控制块层面,同样定义了一些变量用于控制单个传输控制块的接收内存使用量,如下:
struct sock {
...
int sk_rcvbuf;
atomic_t sk_rmem_alloc;
int sk_forward_alloc;
int sk_sndbuf;
atomic_t sk_wmem_alloc;
...
};
sk_rcv_buf/sk_sndbuf
这两个参数分别代表TCB的收发缓冲区大小,缓冲区中的数据不能超过这两个限定值。在sock_init_data()中这两个会被分别初始化为系统参数sysctl_rmem_default和sysctl_wmem_default,之后,应用程序还可以通过setsockopt(2)的SO_SNDBUF和SO_RCVBUF设置它们。
void sock_init_data(struct socket *sock, struct sock *sk)
{
...
sk->sk_rcvbuf = sysctl_rmem_default;
sk->sk_sndbuf = sysctl_wmem_default;
...
}
setsockopt()的内核实现如下:
#define SOCK_MIN_SNDBUF 2048
#define SOCK_MIN_RCVBUF 256
int sock_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, unsigned int optlen)
{
...
case SO_SNDBUF:
// 能设置的最大发送缓冲区大小受系统参数sysctl_wmem_max控制
if (val > sysctl_wmem_max)
val = sysctl_wmem_max;
set_sndbuf:
// 设置SOCK_SNDBUF_LOCK标记,表示应用程序已经设定过该参数了
sk->sk_userlocks |= SOCK_SNDBUF_LOCK;
if ((val * 2) < SOCK_MIN_SNDBUF)
sk->sk_sndbuf = SOCK_MIN_SNDBUF; // buffer的最小值为2048字节
else
sk->sk_sndbuf = val * 2; // 实际值会是设置值的2倍
// 缓冲区大小发生变化,尝试通知那些阻塞在写空间上的进程
sk->sk_write_space(sk);
break;
case SO_SNDBUFFORCE:
// 该选项可以强制设置缓冲区大小而不必例会是否超过系统参数sysctl_wmem_max
if (!capable(CAP_NET_ADMIN)) {
ret = -EPERM;
break;
}
goto set_sndbuf;
case SO_RCVBUF:
// 能设置的最大接收缓冲区大小受系统参数sysctl_rmem_max控制
if (val > sysctl_rmem_max)
val = sysctl_rmem_max;
set_rcvbuf:
// 设置SOCK_RCVBUF_LOCK标记,表示应用程序已经设定过该参数了
sk->sk_userlocks |= SOCK_RCVBUF_LOCK;
/*
* We double it on the way in to account for
* "struct sk_buff" etc. overhead. Applications
* assume that the SO_RCVBUF setting they make will
* allow that much actual data to be received on that
* socket.
*
* Applications are unaware that "struct sk_buff" and
* other overheads allocate from the receive buffer
* during socket buffer allocation.
*
* And after considering the possible alternatives,
* returning the value we actually used in getsockopt
* is the most desirable behavior.
*/
// 注释的意思是:内核在记录传输控制块占用内存大小时,不光会统计数据本身占用空间,还会把
// sk_buff结构占用空间也算进去,但是应用程序实际上并不关心sk_buff的内存消耗,应用期望
// 的是接收限制大小不能低于指定的值,为了简便,所以才有了下面的val*2的做法,设定后,应
// 用应该通过getsockopt()来获取真实的值
if ((val * 2) < SOCK_MIN_RCVBUF)
sk->sk_rcvbuf = SOCK_MIN_RCVBUF; // 最小256字节
else
sk->sk_rcvbuf = val * 2; // 实际值会是设置值的2倍
break;
case SO_RCVBUFFORCE:
// 该选项可以强制设置缓冲区大小而不必例会是否超过系统参数sysctl_rmem_max
if (!capable(CAP_NET_ADMIN)) {
ret = -EPERM;
break;
}
goto set_rcvbuf;
...
}
- sysctl_rmem_default:系统参数,对应于/proc/sys/net/core/rmem_default文件。如上,该参数决定了sk_rcv_buf的默认值。
- sysctl_rmem_max:系统参数,对应于/proc/sys/net/core/rmem_max文件。如上,该参数决定了应用所能为sk_rcv_buf设定的最大值。
- sysctl_wmem_default:系统参数,对应于/proc/sys/net/core/wmem_default文件。如上,该参数决定了sk_snd_buf的默认值。
- sysctl_wmem_max:系统参数,对应于/proc/sys/net/core/wmem_max文件。如上,该参数决定了应用所能为sk_snd_buf设定的最大值。
sk_forward_alloc
每个传输控制块在使用内存前,需要先向传输层申请内存用量,如果申请失败,那么就收发过程由于内存不足而失败,这个过程就是下面要介绍的内存调度。传输层在给传输控制块分配内存用量时是以物理页大小为单位进行分配的,该字段记录了当前已经分配但是还没有被传输控制块使用的内存还剩多少,下次需要使用内存之前,如果余量还够,就不需要再向传输层申请,从该字段中扣除即可。
sk_rmem_alloc
记录了该TCB当前的接收内存占用量,以字节为单位。每将一个数据包放入接收队列,该变量都会累计skb->truesize。同样的,应用每从接收队列读取一定的数据量,该变量也会递减相应值。
sk_wmem_alloc
记录了该TCB当前的发送内存占用量,以字节为单位。每分配一个skb,该变量都会累计skb->truesize。同样的,每发送完毕一个skb,该变量也会递减相应值。
发送内存限制
发送过程中,UDP对内存使用限制发生在skb分配和释放流程中。
skb分配
UDP协议在封装发送skb过程中,使用了如下三个套接字层接口来分配skb:
- sock_alloc_send_pskb():通用的套接字层skb分配接口,可以同时指定线性区域和非线性区域的内存大小,飞线性区域会被组织到skb的frags数组中;
- sock_alloc_send_skb(): 是sock_alloc_send_pskb()的包裹函数,只在线性区域分配内存;
- sock_wmalloc(): 上述两个函数均会收到内存使用限制管控,该函数可以通过force参数强制分配skb,而不管当前内存使用是否已经超过了限制;
sock_alloc_send_pskb()
static struct sk_buff *sock_alloc_send_pskb(struct sock *sk,
unsigned long header_len, unsigned long data_len, int noblock, int *errcode)
{
struct sk_buff *skb;
gfp_t gfp_mask;
long timeo;
int err;
gfp_mask = sk->sk_allocation;
if (gfp_mask & __GFP_WAIT)
gfp_mask |= __GFP_REPEAT;
// 内存分配可能会休眠,计算休眠的时长
timeo = sock_sndtimeo(sk, noblock);
while (1) {
// 优先处理socket的错误情况
err = sock_error(sk);
if (err != 0)
goto failure;
// socket已经关闭了发送端
err = -EPIPE;
if (sk->sk_shutdown & SEND_SHUTDOWN)
goto failure;
// 该TCB的发送内存占用已经超过了限制
if (atomic_read(&sk->sk_wmem_alloc) < sk->sk_sndbuf) {
skb = alloc_skb(header_len, gfp_mask);
if (skb) {
int npages;
int i;
// 不需要分配非线性内存
if (!data_len)
break;
// 分配非线性内存,将它们组织到skb的frags数组中
npages = (data_len + (PAGE_SIZE - 1)) >> PAGE_SHIFT;
skb->truesize += data_len;
skb_shinfo(skb)->nr_frags = npages;
for (i = 0; i < npages; i++) {
struct page *page;
skb_frag_t *frag;
page = alloc_pages(sk->sk_allocation, 0);
if (!page) {
err = -ENOBUFS;
skb_shinfo(skb)->nr_frags = i;
kfree_skb(skb);
goto failure;
}
frag = &skb_shinfo(skb)->frags[i];
frag->page = page;
frag->page_offset = 0;
frag->size = (data_len >= PAGE_SIZE ? PAGE_SIZE : data_len);
data_len -= PAGE_SIZE;
}
/* Full success... */
break;
}
err = -ENOBUFS;
goto failure;
}
// 内存超过了限制,所以要等待有内存可用
set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
err = -EAGAIN;
if (!timeo)
goto failure;
if (signal_pending(current))
goto interrupted;
timeo = sock_wait_for_wmem(sk, timeo);
}
// 发送方向在skb分配时就可以确认skb属于哪个TCB了
skb_set_owner_w(skb, sk);
return skb;
interrupted:
err = sock_intr_errno(timeo);
failure:
*errcode = err;
return NULL;
}
sock_alloc_send_skb()
struct sk_buff *sock_alloc_send_skb(struct sock *sk, unsigned long size,
int noblock, int *errcode)
{
return sock_alloc_send_pskb(sk, size, 0, noblock, errcode);
}
sock_wmalloc()
struct sk_buff *sock_wmalloc(struct sock *sk, unsigned long size, int force,
gfp_t priority)
{
if (force || atomic_read(&sk->sk_wmem_alloc) < sk->sk_sndbuf) {
struct sk_buff * skb = alloc_skb(size, priority);
if (skb) {
// 发送方向在skb分配时就可以确认skb属于哪个TCB了
skb_set_owner_w(skb, sk);
return skb;
}
}
return NULL;
}
skb_set_owner_w()
上述三个接口仅仅是分配过程,也看到了TCB层面是如何限制内存分配的,发送内存使用量的记录是在skb_set_owner_w()中完成的。
static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)
{
sock_hold(sk); // 每个发送skb会持有其所属TCB的引用
skb->sk = sk; // 记录所属TCB
skb->destructor = sock_wfree; // 替换destructor()回调
atomic_add(skb->truesize, &sk->sk_wmem_alloc); // 更新TCB的发送内存使用量
}
skb销毁: sock_wfree()
如skb_set_owner_w()介绍,当skb被发送完毕释放时,会回调sock_wfree()。
/*
* Write buffer destructor automatically called from kfree_skb.
*/
void sock_wfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
/* In case it might be waiting for more memory. */
atomic_sub(skb->truesize, &sk->sk_wmem_alloc); // 更新TCB的发送内存使用量
if (!sock_flag(sk, SOCK_USE_WRITE_QUEUE)) // sk_write_space()会通知那些等待内存可用的进程
sk->sk_write_space(sk);
sock_put(sk); // 释放对TCB的引用
}
小结
从上面的实现可以看出,UDP在发送流程中,只使用了套接字层面的sk_sndbuf和sk_wmem_alloc两个变量来控制发送内存的使用量。
接收内存限制
接收过程中,UDP对接收内存使用的限制发生在skb放入TCB接收队列和应用程序从队列中取走数据两个流程中。
skb入队列
sock_queue_rcv_skb()
在查找到skb应该由哪个TCB处理后,会调用该函数将skb入队列。
int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
...
// 如果总的占用内存超过了接收门限,则接收失败,这会导致丢包
if (atomic_read(&sk->sk_rmem_alloc) + skb->truesize >=(unsigned)sk->sk_rcvbuf) {
err = -ENOMEM;
goto out;
}
...
// 接收内存调度,所谓调度就是进行更加细致的内存用量检查,调度失败则接收失败
if (!sk_rmem_schedule(sk, skb->truesize)) {
err = -ENOBUFS;
goto out;
}
...
// 设定该skb的owner为当前传输控制块
skb_set_owner_r(skb, sk);
...
// 将该SKB加入到接收队列中
skb_queue_tail(&sk->sk_receive_queue, skb);
...
}
接收内存调度: sk_rmem_schedule()
内存调度的设计思想如下:
- 每个TCB在使用内存前,先向UDP协议层面申请,申请到后再使用。这种约定可以保证整个协议层面的内存消耗量可控;
- 为了不必每个数据包都申请,每次申请都是若干个物理页大小,TCB用sk_forward_alloc变量记录申请下来尚未被使用的内存量(字节为单位);
static inline int sk_has_account(struct sock *sk)
{
return !!sk->sk_prot->memory_allocated;
}
static inline int sk_rmem_schedule(struct sock *sk, int size)
{
// 传输层协议可以通过在协议对象中不提供memory_allocated字段来关闭传输层协议层面的内存限制
if (!sk_has_account(sk))
return 1;
// 如果当前余量还够,则不用再次申请;否则调用__sk_mem_schedule()申请内存用量
return size <= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_RECV);
}
/**
* __sk_mem_schedule - increase sk_forward_alloc and memory_allocated
* @sk: socket
* @size: memory size to allocate
* @kind: allocation type
*
* If kind is SK_MEM_SEND, it means wmem allocation. Otherwise it means
* rmem allocation. This function assumes that protocols which have
* memory_pressure use sk_wmem_queued as write buffer accounting.
*/
// KIND表示是发送调度,还是接收调度, UDP只是用了接收调度
#define SK_MEM_SEND 0
#define SK_MEM_RECV 1
// 该函数是既可以做发送调度,也可以做接收调度,只不过UDP只是用了接收调度
int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
struct proto *prot = sk->sk_prot;
// size字节按照页对齐后的物理页数保存在amt中
int amt = sk_mem_pages(size);
int allocated;
// 预分配需要的内存
sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
// 累加传输层协议的内存用量计数
allocated = atomic_add_return(amt, prot->memory_allocated);
// 传输层协议的内存用量低于门限值sysctl_mem[0],说明消耗不大,分配成功
if (allocated <= prot->sysctl_mem[0]) {
// 如果传输层协议设置了压力标记,则清除该标记
if (prot->memory_pressure && *prot->memory_pressure)
*prot->memory_pressure = 0;
// 返回调度成功
return 1;
}
// 传输层协议的内存用量超过了门限sysctl_mem[1],表示内存使用较为紧张,
// 这种情况还可以接受,不影响调度成功与否
if (allocated > prot->sysctl_mem[1])
// 如果协议提供了回调,则给协议一个机会可以在内存用量使用紧张时提前做一些事情。
if (prot->enter_memory_pressure)
prot->enter_memory_pressure(sk);
// 传输层协议的内存用量超过了门限sysctl_mem[2],此时内存占用已经非常多了,
// 需要抑制内存使用,这种情况调度失败
if (allocated > prot->sysctl_mem[2])
goto suppress_allocation;
// 到这里,说明传输层协议的内存用量在[sysctl_mem[0], sysctl_mem[2]]之间
if (kind == SK_MEM_RECV) {
// 对于接收调度,如果该传输控制块已占用内存没有超过传输层指定的单个传输控制块
// 最小接收门限时调度成功,否则调度失败
if (atomic_read(&sk->sk_rmem_alloc) < prot->sysctl_rmem[0])
return 1;
} else { /* SK_MEM_SEND */
if (sk->sk_type == SOCK_STREAM) {
if (sk->sk_wmem_queued < prot->sysctl_wmem[0])
return 1;
} else if (atomic_read(&sk->sk_wmem_alloc) <
prot->sysctl_wmem[0])
return 1;
}
// 到这里,通过memory_pressure标记来决定是否调度成功。UDP并未提供该字段,忽略
if (prot->memory_pressure) {
int alloc;
if (!*prot->memory_pressure)
return 1;
alloc = percpu_counter_read_positive(prot->sockets_allocated);
if (prot->sysctl_mem[2] > alloc * sk_mem_pages(
sk->sk_wmem_queued + atomic_read(&sk->sk_rmem_alloc) + sk->sk_forward_alloc))
return 1;
}
suppress_allocation:
// STREAM类型的发送过程相关,先忽略
if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) {
sk_stream_moderate_sndbuf(sk);
/* Fail only if socket is _under_ its sndbuf.
* In this case we cannot block, so that we have to fail.
*/
if (sk->sk_wmem_queued + size >= sk->sk_sndbuf)
return 1;
}
// 调度失败,撤销之前累加到这两个变量上面的值
sk->sk_forward_alloc -= amt * SK_MEM_QUANTUM;
atomic_sub(amt, prot->memory_allocated);
return 0;
}
该函数的逻辑是先进行分配,然后再判断这种分配是否超过了系统限制,如果一切ok,那么调度成功,否则撤销开始的分配。
skb_set_owner_r()
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
skb->sk = sk;
skb->destructor = sock_rfree; // 替换skb的destructor()回调
atomic_add(skb->truesize, &sk->sk_rmem_alloc); // 更新TCB的接收内存使用量
sk_mem_charge(sk, skb->truesize); // 记账,就是更新sk->sk_forward_alloc
}
static inline void sk_mem_charge(struct sock *sk, int size)
{
// 如果协议不支持记账,直接返回。TCP和UDP都是支持的
if (!sk_has_account(sk))
return;
// 消耗一定的预分配内存用量
sk->sk_forward_alloc -= size;
}
skb销毁: sock_rfree()
当应用程序将skb从队列中取走后,skb会被释放,如上,释放回调为sock_rfree()。
/*
* Read buffer destructor automatically called from kfree_skb.
*/
void sock_rfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
atomic_sub(skb->truesize, &sk->sk_rmem_alloc); // 更新TCB的接收内存使用量
sk_mem_uncharge(skb->sk, skb->truesize);
}
static inline void sk_mem_uncharge(struct sock *sk, int size)
{
if (!sk_has_account(sk))
return;
// 将使用的内存还回预分配用量中
sk->sk_forward_alloc += size;
}
小结
UDP在接收方向的内存管理上使用了套接字层的内存控制策略,从TCB层面和UDP层面共同对内存使用进行了限制。要点如下:
- 使用内存前要从UDP层面申请,申请到以后才能接收数据,单个TCB的最小内存使用量为协议层面指定的prot->sysctl_wmem;
- TCB层面限制内存使用量不能超过sk->sk_rcvbuf;