套接字建立后就会收发数据包。对于发送,数据包是先由应用发给协议栈,由协议栈缓存后发出去的;对于接收,也是先由协议栈将输入数据包缓存,然后才由应用读取走的,这种缓存数据包的行为必然会占用内存。比如应用迟迟不读取数据,那么协议栈就会不停的缓存数据,如果不对套接字占用的内存进行限制,那么很容易会吃光系统内存,内核当然不会让这种事情发生,这篇笔记介绍了内核是对套接字的内存使用进行管理的。
整体策略
无论是接收还是发送,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