UDP之收发内存管理

套接字建立后就要进行收发数据包,对于发送,数据包是先由应用发给协议栈,由协议栈缓存后发出去的;对于接收,也是先由协议栈将输入数据包缓存,然后才由应用读取走的,这种缓存数据包的行为一定会占用内存,比如应用迟迟不读取数据,那么协议栈会不停的缓存数据,如果不对套接字占用的内存进行限制,那么很容易会吃光系统内存,内核当然不会让这种事情发生,这篇笔记就介绍了内核是如何做的。

1. 综述

无论是接收还是发送,内存使用限制都是在两个层面上进行限制:1) 整个传输层层面的限制;2) 具体传输控制块层面的限制。为了实现在两个层面上的内存用量控制,内核定义了一系列的变量,理解这些变量的含义是关键。

2. 接收内存限制

2.1 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,
	.sysctl_rmem	   = &sysctl_udp_rmem_min,
...
};

2.1.1 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]: 使用量已经超过了硬性限制,此时抑制分配,所有数据包会被丢弃

关于这三个门限值的具体细节见下文分析。

2.1.2 sysctl_rmem

UDP层面对单个传输控制块的接收内存使用的限制。当UDP层已分配用量超过了sysctl_mem[0]但是小于sysctl_mem[2]时,如果当前传输控制块的内存用量低于sysctl_rmem,那么也是允许数据包正常接收的。

2.1.3 memory_allocated

在UDP层面记录已经消耗的系统内存大小,该变量以物理页为单位统计。

2.1.4 初始化

上面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;
}

2.2 传输控制块层面的限制

在传输控制块层面,同样定义了一些变量用于控制单个传输控制块的接收内存使用量,如下:

struct sock {
...
    int			sk_rcvbuf;
    atomic_t	sk_rmem_alloc;
    int			sk_forward_alloc;
...
};

2.2.1 sk_rcv_buf

该传输控制块接收队列中的skb占用的内存总大小不能超过该限定值,超限的数据包会被丢弃。传输控制块创建之初被初始化为系统参数sysctl_rmem_default,此外,应用还可以通过setsockopt()接口的设置具体传输控制的该限定值,具体逻辑如下:

void sock_init_data(struct socket *sock, struct sock *sk)
{
...
	sk->sk_rcvbuf		=	sysctl_rmem_default;
...
}

setsockopt()的内核实现如下:

#define SOCK_MIN_RCVBUF 256

int sock_setsockopt(struct socket *sock, int level, int optname,
		    char __user *optval, unsigned int optlen)
{
...
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()来获取真实的值

		//实际设定的值是用户指定的值的两倍,但是最终值又不能超过最小值SOCK_MIN_RCVBUF(256)
		if ((val * 2) < SOCK_MIN_RCVBUF)
			sk->sk_rcvbuf = SOCK_MIN_RCVBUF;
		else
			sk->sk_rcvbuf = val * 2;
		break;
...
}

要特别注意,sk_rcvbuf的取值范围可以是[256, sysctl_rmem_max*2].

2.2.2 sysctl_rmem_default

系统参数,对应于/proc/sys/net/core/rmem_default文件。如上,该参数决定了sk_rcv_buf的默认值。

2.2.3 sysctl_rmem_max

系统参数,对应于/proc/sys/net/core/rmem_max文件。如上,该参数决定了应用所能为sk_rcv_buf设定的最大值。

2.2.4 sk_forward_alloc

每个传输控制块在使用内存前,需要先向传输层申请内存用量,如果申请失败,那么就收发过程由于内存不足而失败,这个过程就是下面要介绍的内存调度。传输层在给传输控制块分配内存用量时是以物理页大小为单位进行分配的,该字段记录了当前已经分配但是还没有被传输控制块使用的内存还剩多少,下次需要使用内存之前,如果余量还够,就不需要再向传输层申请,从该字段中扣除即可。

2.2.5 sk_rmem_alloc

该字段记录了传输控制块当前的内存占用量,以字节为单位。每将一个数据包放入接收队列,该变量都会累计skb->truesize。同样的,应用每从接收队列读取一定的数据量,该变量也会递减相应值。

2.3 代码实现

下面来看看代码中是如何使用上述这些变量来控制接收过程的。

2.3.1 sock_queue_rcv_skb()

该函数用于将数据包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);
...
}

2.3.2 skb_set_owner_r()

skb_set_owner_r()非常简单,该函数实现如下:

static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
...
	//将skb实际占用的内存用量累加到传输控制块的sk_rmem_alloc上面
	atomic_add(skb->truesize, &sk->sk_rmem_alloc);
	sk_mem_charge(sk, skb->truesize);
}

static inline void sk_mem_charge(struct sock *sk, int size)
{
	//如果协议不支持记账,直接返回。TCP和UDP都是支持的
	if (!sk_has_account(sk))
		return;
    //总的已分配内存中扣除指定字节用量
	sk->sk_forward_alloc -= size;
}

static inline int sk_has_account(struct sock *sk)
{
	//实际上就是看具体的协议有没有提供计数变量
	return !!sk->sk_prot->memory_allocated;
}

2.3.3 sk_rmem_schedule()

接收内存调度。

static inline int sk_rmem_schedule(struct sock *sk, int size)
{
	//对于不支持内存记账的协议,直接返回1,这表示这一环节是不限制内存使用量
	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表示是发送调度,还是接收调度
#define SK_MEM_SEND	0
#define SK_MEM_RECV	1

//该函数的作用就是为传输控制块sk分配内存用量
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;
	}
	//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,那么调度成功,否则撤销开始的分配。

3. 发送内存限制

待补充…

猜你喜欢

转载自blog.csdn.net/fanxiaoyu321/article/details/83473769
今日推荐