linux操作系统:内核是如何fork出一个进程的,为什么系统的swap变高了,The Linux Kernel

The Linux Kernel

 

fork,意味“分支”,可以创建一个进程。那创建进程这个动作在内核里做了什么事情呢?

fork是一个系统调用,根据系统调用的流程,流程的最后会在sys_call_table中找到相应的系统调用sys_fork。

sys_fork是如何定义的了,如下,根据SYSCALL_DEFINE0这个宏的定义,就定义出了sys_fork:

SYSCALL_DEFINE0(fork)
{
......
	return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}

sys_fork会调用_do_fork:

long _do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr,
	      unsigned long tls)
{
	struct task_struct *p;
	int trace = 0;
	long nr;
 
 
......
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
......
	if (!IS_ERR(p)) {
		struct pid *pid;
		pid = get_task_pid(p, PIDTYPE_PID);
		nr = pid_vnr(pid);
 
 
		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);
 
 
......
		wake_up_new_task(p);
......
		put_pid(pid);
	} 
......

fork的第一件大事:复制结构

_dor_fork里面做的第一件大事就是copy_process。如果所有的数据结构都从头创建一份太麻烦了,所以Ctrl+ C + Ctrl+ V

其复制的是task_struct这个进程数据结构
在这里插入图片描述

static __latent_entropy struct task_struct *copy_process(
					unsigned long clone_flags,
					unsigned long stack_start,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace,
					unsigned long tls,
					int node)
{
	int retval;
	struct task_struct *p;
......
	p = dup_task_struct(current, node);

dup_task_struct 主要做了下面几件事情:

  • 调用alloc_task_struct_node分配一个task_struct结构
  • 调用alloc_thread_stack_node 创建内核栈,这里面调用__vmalloc_node_range 分配一个连续的THREAD_SIZE的内存空间,赋值给task_struct的void *stack成员变量
  • 调度arch_dup_task_struct(struct task_struct *dst, struct task_struct *src),将task_struct进行复制,其实就是调用memcpy
  • 调用setup_thread_stack 设置thread_info

到这里,整个task_struct复制了一份,而且内核栈也创建好了。

接下来就是权限相关了,copy_creds主要做了下面几件事情:

retval = copy_creds(p, clone_flags);
  • 调用prepare_creds,准备一个新的struct cred *new。如果准备呢?其实还是从内存中分配一个新的struct cred结果,然后调用memcpy复制一份父进程的cred
  • 接着p->cred = p->real_cred = get_cred(new),将新进程的“我能操作谁”和“谁能操作我”两个权限都指向新的cred。

接下来,copy_process重新设置进程运行的统计量:

p->utime = p->stime = p->gtime = 0;
p->start_time = ktime_get_ns();
p->real_start_time = ktime_get_boot_ns();

接下来,copy_process 开始设置调度相关的变量:

retval = sched_fork(clone_flags, p);

sched_fork主要做了下面几件事情:

  • 调用__sched_fork,在这里面将on_rq设为0,初始化sched_entity,将里面的exec_start、sum_exec_runtime、prev_exec_runtime、vruntime都设为0。这几个变量涉及进程的实际运行时间和虚拟运行时间。是否到时间应该被调度了,就靠它们几个
  • 设置进程的状态: p->state= TASK_NEW;
  • 初始化优先级: prio、normal_prio、static_prio
  • 设置调度类,如果是普通进程,就设置为p->sched_class = &fair_sched_class;
  • 调用调度类的task_fork函数,对于CFS来讲,就是调用task_fork_fair。在这个函数里,
    • 先调度员update_curr,对当前的进程进行统计量更新
    • 然后把子进程和父进程的vruntime设成一样
    • 最后调用place_entity,初始化sched_entity。 这里有一个变量,sysctl_sched_child_runs_first,可以设置父进程和子进程谁先运行。如果设置了子进程先运行,即便两个进程的vruntime一样,也要把子进程的sched_entity放在前面,然后调用resched_curr,标记当前运行的进程TIF_NEED_RESCHED,也就是说,把父进程设置为应该被调度,这样下次调度的时候,父进程会被子进程抢占。

接下来,copy_process开始初始化与文件和文件系统相关的变量:

retval = copy_files(clone_flags, p);
retval = copy_fs(clone_flags, p);
  • copy_files主要用于复制一个进程打开的文件信息。
    • 这些信息用一个结构files_struct来维护,每个打开的文件都有一个文件描述符。
    • 在copy_files函数里面调用dup_fd,在这里会创建一个新的files_struct,然后将所有的文件描述符数组fdtable拷贝一份
  • copy_fs主要用于复制一个进程的目录信息。
    • 这些信息由一个结构 fs_struct 来维护
    • 一个进程有自己的根目录和根文件系统root,也有当前目录pwd和当前目录的文件系统,都在fs_struct里面维护
    • copy_fs函数里面调用copy_fs_struct,创建一个新的fs_struct,并赋值原来进行的fs_struct。

接下来,copy_process开始初始化与信号相关的变量:

init_sigpending(&p->pending);
retval = copy_sighand(clone_flags, p);
retval = copy_signal(clone_flags, p);
  • copy_sighand会分配一个新的sighand_struct。这里最主要的是维护信号处理函数,在copy_sighand里面会调用memcpy,将信号处理函数sighand->action从父进程赋值到子进程
  • init_sigpending 和 copy_signal 用于初始化,并且复制用于维护发给这个进程的信号的数据结构。copy_signal函数会分配一个新的 signal_struct,并进行初始化

接下来,copy_process开始复制进程内存空间

retval = copy_mm(clone_flags, p);
  • 进程都有自己的内存空间,用mm_struct结构来表示
  • copy_mm函数中调用dup_mm,分配一个新的mm_struct,调用memcpy复制这个函数
  • dup_mmap用于复制内存空间中内存映射的部分(mmap除了可以分配大块的内存,还可以将一个文件映射到内存中,方便可以像读写内存一样读写文件)

接下来,copy_process开始分配pid、设置tid、group_leader,并且建立进程之间的亲缘关系。

	INIT_LIST_HEAD(&p->children);
	INIT_LIST_HEAD(&p->sibling);
......
    p->pid = pid_nr(pid);
	if (clone_flags & CLONE_THREAD) {
		p->exit_signal = -1;
		p->group_leader = current->group_leader;
		p->tgid = current->tgid;
	} else {
		if (clone_flags & CLONE_PARENT)
			p->exit_signal = current->group_leader->exit_signal;
		else
			p->exit_signal = (clone_flags & CSIGNAL);
		p->group_leader = p;
		p->tgid = p->pid;
	}
......
	if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
		p->real_parent = current->real_parent;
		p->parent_exec_id = current->parent_exec_id;
	} else {
		p->real_parent = current;
		p->parent_exec_id = current->self_exec_id;
	}

好了,copy_process 要结束了,上面图中的组件也初始化的差不多了

fork的第二件大事:唤醒新进程

_do_fork 做的第二件大事是 wake_up_new_task。新任务刚刚建立,有没有机会抢占别人,获得CPU呢?

void wake_up_new_task(struct task_struct *p)
{
	struct rq_flags rf;
	struct rq *rq;
......
	p->state = TASK_RUNNING;
......
	activate_task(rq, p, ENQUEUE_NOCLOCK);
	p->on_rq = TASK_ON_RQ_QUEUED;
	trace_sched_wakeup_new(p);
	check_preempt_curr(rq, p, WF_FORK);
......
}

首先,将进程的状态设置为TASK_RUNNING

activate_task函数会调用enqueue_task

static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
.....
	p->sched_class->enqueue_task(rq, p, flags);
}

如果是CFS的调度类,则执行相应的enqueue_task_fair

static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &p->se;
......
	cfs_rq = cfs_rq_of(se);
	enqueue_entity(cfs_rq, se, flags);
......
	cfs_rq->h_nr_running++;
......
}
  • 在enqueue_entity中取出的队列就是cfs_rq ,然后调用enqueue_entity
  • 在enqueue_entity函数里面,会调用update_curr更新运行的统计量,然后调用__enqueue_entity,将sched_entity 加入到红黑树里面,然后将se->on_rq = 1设置在队列上
  • 回到enqueue_task_fair 之后,将这个队列上运行的进程数目加一。然后,wake_up_new_task 会调用check_preempt_curr,看是否能够抢占当前进程。
  • 在check_preempt_curr中,会调用相应的调度类q->curr->sched_class->check_preempt_curr(rq, p, flags)。对于CFS调度类来讲,调用的是check_preempt_wakeup
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
	struct task_struct *curr = rq->curr;
	struct sched_entity *se = &curr->se, *pse = &p->se;
	struct cfs_rq *cfs_rq = task_cfs_rq(curr);
......
	if (test_tsk_need_resched(curr))
		return;
......
	find_matching_se(&se, &pse);
	update_curr(cfs_rq_of(se));
	if (wakeup_preempt_entity(se, pse) == 1) {
		goto preempt;
	}
	return;
preempt:
	resched_curr(rq);
......
}
  • 在check_preempt_wakeup函数中,前面调用task_fork_fair 的时候,设置了sysctl_sched_child_runs_first ,已经将当前父进程的TIF_NEED_RESCHED 设置了,则直接返回
  • 否则,check_preempt_wakeup 还是会调用update_curr 更新一次统计量,然后wakeup_preempt_entity 将父进程和子进程PK一次,看是不是要抢占,如果要则调度resched_curr 标记父进程为TIF_NEED_RESCHED
  • 如果新创建的进程应该抢占父进程,在什么时候抢占了?fork是一个系统调用,从系统调用返回的时候,就是一个抢占的好时机,如果父进程判断自己已经被设置为TIF_NEED_RESCHED,就让子进程先跑,抢占自己

总结

在这里插入图片描述

 

文章目录

基础:为什么系统的swap变高了

引入

问题:当发生了内存泄露,或者运行了大内存的应用程序,导致系统的内存资源紧张时,系统会怎么应对呢?

  • OOM杀死进程
  • 内存回收

问题:什么是OOM

OOM指的是系统杀死占用大量内存的进程,释放这些内存,然后再分配给其他更需要的进程。

问题,什么是内存回收。

内存回收,指的是系统释放掉可以回收的内存,比如缓存和缓冲区,就属于可回收內存。它们在内存管理中,通常被叫做文件页

  • 大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。
  • 而那些被应用程序修改过,并且暂时还没有写入磁盘的数据(也就是脏页),就必须先写入磁盘,然后才能进行内存释放

这些脏页,一般可以通过两种方式写入磁盘:

  • 可以在应用程序中,通过系统调用fsync,把脏页同步到磁盘中
  • 也可以交给系统,由内核线程pdfflush负责这些脏页的刷新

除了缓存和缓冲区,通过内存映射获取的文件映射页,也是一种常见的文件页。它也可以被释放掉,下次再访问的时候,从文件重新读取。

除了文件页外,还有没有其他的内存可以回收呢?

比如,应用程序动态分配的堆内存,也就是我们在内存管理中说到的匿名内存,是不是也可以被回收呢?

  • 答案是这些内存不能被直接释放(回收),因为它们很可能还需要再次被访问。

  • 但是,如果这些内存在分配之后很少被访问,似乎也是一种资源浪费。是不是可以把它们暂时先存在磁盘里,释放内存给其他更需要的进程?

其实,这正是Linux的swa机制,swap把这些不常访问的内存先写到磁盘里,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了

swap原理

swap原理

swap说白了就是把一块磁盘空间或者一个本地文件(下面以磁盘为例),当成内存来使用。它包括换出和换入两个过程:

  • 换出,就是把进程暂时不用的内存年数据存储到磁盘中,并释放这些数据占用的内存
  • 换入,就是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。

即,swap其实是把系统的可用内存变大了。这样,即使服务器的内存不足,也可以运行大内存的应用程序。

下面是一些swap的应用场景:

  • 即使内存不足,有些应用程序也并不想被OOM杀死,而是希望能够缓一段时间,等待人工介入,或者等系统自动释放其他进程的内存,再分配给它
  • 电脑的休眠和快速开机也是基于swap的:休眠时,把系统的内存存入磁盘,这样等到再次开机时,只需要从磁盘中加载内存就可以。这样就省去了很多应用程序的初始化过程,加快了开机速度。

问题:既然swap是为了回收内存,那么Linux到底在什么时候需要回收内存呢?前面一直说内存资源紧张,又该怎么来衡量内存是不是紧张呢?

一个最容易想到的场景就是,有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比起前面提到的缓存),进而尽可能的满足新内存请求。这个过程通常被称为直接内存回收

除了直接内存回收,还有一个专门的内核进程用来定期的回收内存,也就是kswapd0。

为了衡量内存的使⽤情况,kswapd0定义了三个内存阈值(watermark,也称为⽔位),分别是

  • ⻚最⼩阈值(pages_min)
  • ⻚低阈值(pages_low)
  • ⻚⾼阈值(pages_high)

剩余内存,则使⽤ pages_free 表示。

在这里插入图片描述
kswapd0定期扫描内存的使⽤情况,并根据剩余内存落在这三个阈值的空间位置,进⾏内存的回收操作。

  • 剩余内存⼩于⻚最⼩阈值,说明进程可⽤内存都耗尽了,只有内核才可以分配内存。
  • 剩余内存落在⻚最⼩阈值和⻚低阈值中间,说明内存压⼒⽐较⼤,剩余内存不多了。这时kswapd0会执⾏内存回收,直到剩余内存⼤于⾼阈值为⽌。
  • 剩余内存落在⻚低阈值和⻚⾼阈值中间,说明内存有⼀定压⼒,但还可以满⾜新内存请求。
  • 剩余内存⼤于⻚⾼阈值,说明剩余内存⽐较多,没有内存压⼒。

我们可以看到,⼀旦剩余内存⼩于⻚低阈值,就会触发内存的回收。这个⻚低阈值,其实可以通过内核选项/proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了⻚最⼩阈值,⽽其他两个阈值,都是根据⻚最⼩阈值计算⽣成的,计算⽅法如下 :

pages_low = pages_min*5/4
pages_high = pages_min*3/2

NUME和Swap

很多情况下,你明明发现了swap升高,可是在分析系统的内存使用时,却很可能发现,系统剩余内存还多着呢?为什么剩余内存很多的情况下,也会发生swap呢?

是处理器的的 NUMA (Non-Uniform Memory Access)架构导致的。

  • 在NUMA架构下,多个处理器被划分到不同Node上,而且每个Node都有自己的本地内存空间
  • 而同一个Node内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区DMA、普通内存区NORMAL、伪内存区(MOVABLE)等,如下图所示:
    在这里插入图片描述

先不⽤特别关注这些内存域的具体含义,我们只要会查看阈值的配置,以及缓存、匿名⻚的
实际使⽤情况就够了。

既然 NUMA 架构下的每个 Node 都有⾃⼰的本地内存空间,那么,在分析内存的使⽤时,我们也应该针对每个 Node 单独分析。

可以通过 numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使⽤情况。⽐如,下⾯就是⼀个numactl 输出的示例:

$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1
node 0 size: 7977 MB
node 0 free: 4416 MB
...

这个界⾯显示,我的系统中只有⼀个 Node,也就是Node 0 ,⽽且编号为 0 和 1 的两个 CPU, 都位于 Node 0 上。另外,Node 0 的内存⼤⼩为 7977 MB,剩余内存为 4416 MB。

NUNA架构与NUNA内存,跟swap又有什么关系?

实际上,前⾯提到的三个内存阈值(⻚最⼩阈值、⻚低阈值和⻚⾼阈值),都可以通过内存域在 proc ⽂件系统中的接⼝/proc/zoneinfo 来查看。

⽐如,下⾯就是⼀个 /proc/zoneinfo ⽂件的内容示例:

$ cat /proc/zoneinfo
...
Node 0, zone Normal
pages free 227894
min 14896
low 18620
high 22344
...
nr_free_pages 227894
nr_zone_inactive_anon 11082
nr_zone_active_anon 14024
nr_zone_inactive_file 539024
nr_zone_active_file 923986
...

这个输出中有⼤量指标,⽐较重要的如下:

  • pages处的min、low、high,就是上⾯提到的三个内存阈值,⽽free是剩余内存⻚数,它跟后⾯的 nr_free_pages 相同。
  • nr_zone_active_anon和nr_zone_inactive_anon,分别是活跃和⾮活跃的匿名⻚数
  • nr_zone_active_file和nr_zone_inactive_file,分别是活跃和⾮活跃的⽂件⻚数。

从这个输出结果可以发现,剩余内存远⼤于⻚⾼阈值,所以此时的 kswapd0 不会回收内存。

当然,某个 Node 内存不⾜时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,你可以通过 /proc/sys/vm/zone_reclaim_mode 来调整。它⽀持以下⼏个选项:

  • 默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。
  • 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以⽤ Swap ⽅式回收内存

swappiness

到这里,我们就可以理解内存回收的机制了。这些回收的内存既包括了文件页,又包括了匿名页:

  • 对文件页的回收,当然是直接回收缓存,或者把脏页写回磁盘后再回收
  • 对匿名页的回收,其实就是通过swap机制,它他们写入磁盘后再释放内存

既然有两种不同的内存回收机制,那么在实际回收内存时,到底该先回收哪⼀种呢?

Linux提供了⼀个 /proc/sys/vm/swappiness 选项,⽤来调整使⽤Swap的积极程度。

  • swappiness的范围是0-100,数值越⼤,越积极使⽤Swap,也就是更倾向于回收匿名⻚;数值越⼩,越消极使⽤Swap,也就是更倾向于回收⽂件⻚。
  • 虽然 swappiness 的范围是 0-100,不过要注意,这并不是内存的百分⽐,⽽是调整 Swap 积极程度的权重,即使你把它设置成0,当剩余内存+⽂件⻚⼩于⻚⾼阈值时,还是会发⽣Swap。
  • 如果linux内核是3.5及以后的,最好是设置swappiness=10,不要设置swappiness=0

小结

在内存资源紧张的时候,Linux通过直接回收内存和定期扫描的方式,来释放文件页和匿名页,以便把内存分配给更需要的进程使用。

  • 文件页的回收比较容易理解,直接情空,或者把脏数据写回磁盘后再释放
  • 匿名页的回收,需要通过swap换出到磁盘中,下次访问时,再从磁盘换入到内存中

可以设置/proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值(也就是⻚低阈值),还可以设置/proc/sys/vm/swappiness,来调整⽂件⻚和匿名⻚的回收倾向

在NUMA架构下,每个Node都有自己的本地内存空间,而当本地内存不足时,默认既可以从其他Node寻找空闲内存,也可以从本地内存回收。

可以设置 /proc/sys/vm/zone_reclaim_mode ,来调整NUMA本地内存的回收策略。

  • swap应该是针对以前内存⼩的⼀种优化,不过现在内存没那么昂贵之后,所以就没那么⼤的必要开启了
  • numa感觉是对系统资源做的隔离分区,不过⽬前虚拟化和docker这么流⾏。⽽且node与node之间访问更耗时,针对⼤程序不⼀定启到了优化作⽤,针对⼩程序,也没有太⼤必要。所以numa也没必要开启。

案例

当 Swap 使⽤升⾼时,要如何定位和分析呢

准备

  • 机器配置:2 CPU,8GB 内存
  • 需要预先安装 sysstat 等⼯具,如 apt install sysstat

开始

在终端中运⾏free命令,查看Swap的使⽤情况

$ free
       total   used   free  shared  buff/cache available
Mem:  8169348 331668 6715972  696    1121708     7522896
Swap: 0           0     0

从这个free输出你可以看到,Swap的⼤⼩是0,这说明机器没有配置Swap。

为了继续Swap的案例, 就需要先配置、开启Swap。

要开启Swap,我们⾸先要清楚,Linux本身⽀持两种类型的Swap,即Swap分区和Swap⽂件。以Swap⽂件为例,在第⼀个终端中运⾏下⾯的命令开启Swap,我这⾥配置Swap⽂件的⼤⼩为8GB:

# 创建Swap⽂件
$ fallocate -l 8G /mnt/swapfile
# 修改权限只有根⽤户可以访问
$ chmod 600 /mnt/swapfile
# 配置Swap⽂件
$ mkswap /mnt/swapfile
# 开启Swap
$ swapon /mnt/swapfile

然后,再执⾏free命令,确认Swap配置成功:

$ free
      total   used   free   shared buff/cache available
Mem: 8169348 331668 6715972   696    1121708 7522896
Swap: 8388604  0    8388604

现在,free 输出中,Swap 空间以及剩余空间都从 0 变成了8GB,说明Swap已经正常启。

接下来,我们在第⼀个终端中,运⾏下⾯的 dd 命令,模拟⼤⽂件的读取:

# 写⼊空设备,实际上只有磁盘的读请求
$ dd if=/dev/sda1 of=/dev/null bs=1G count=2048

接着,在第⼆个终端中运⾏ sar 命令,查看内存各个指标的变化情况。你可以多观察⼀会⼉,查看这些指标的变化情况。

# 间隔1秒输出⼀组数据
# -r表示显示内存使⽤情况,-S表示显示Swap使⽤情况
$ sar -r -S 1
04:39:56 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
04:39:57 6249676   6839824 1919632    23.50     740512    67316   1691736 10.22 815156 841868 4
04:39:56 kbswpfree kbswpused %swpused kbswpcad %swpcad
04:39:57 8388604     0        0.00      0      0.00

04:39:57 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
04:39:58 6184472    6807064 1984836   24.30    772768     67380   1691736  10.22   847932   874224    20
04:39:57 kbswpfree kbswpused %swpused kbswpcad %swpcad
04:39:58 8388604     0          0.00    0       0.00
…
04:44:06 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
04:44:07 152780    6525716 8016528    98.13    6530440    51316   1691736  10.22    867124   6869332   0
04:44:06 kbswpfree kbswpused %swpused kbswpcad %swpcad
04:44:07 8384508   4096      0.05     52       1.27

我们可以看到,sar的输出结果是两个表格,第⼀个表格表示内存的使⽤情况,第⼆个表格表示Swap的使⽤情况。其中,各个指标名称前⾯的kb前缀,表示这些指标的单位是KB。

去掉前缀后,你会发现,⼤部分指标我们都已经⻅过了,剩下的⼏个新出现的指标,我来简单介绍⼀下

  • kbcommit,表示当前系统负载需要的内存。它实际上是为了保证系统内存不溢出,对需要内存的估计值。%commit,就是这个值相对总内存的百分⽐。
  • kbactive,表示活跃内存,也就是最近使⽤过的内存,⼀般不会被系统回收
  • kbinact,表示⾮活跃内存,也就是不常访问的内存,有可能会被系统回收。

清楚了界⾯指标的含义后,我们再结合具体数值,来分析相关的现象。你可以清楚地看到,总的内存使⽤率(%memused)在不断增⻓,从开始的23%⼀直⻓到了 98%,并且主要内存都被缓冲区(kbbuffers)占⽤。具体来说:

  • 刚开始,剩余内存(kbmemfree)不断减少,⽽缓冲区(kbbuffers)则不断增⼤,由此可知,剩余内存不断分配给了缓冲区。
  • ⼀段时间后,剩余内存已经很⼩,⽽缓冲区占⽤了⼤部分内存。这时候,Swap的使⽤开始逐渐增⼤,缓冲区和剩余内存则只在⼩范围内波动。

你可能困惑了,为什么缓冲区在不停增⼤?这⼜是哪些进程导致的呢?

显然,我们还得看看进程缓存的情况。在第⼆个终端中,按下Ctrl+C停⽌sar命令,然后运⾏下⾯的cachetop命令,观察缓存的使⽤情况:

$ cachetop 5
12:28:28 Buffers MB: 6349 / Cached MB: 87 / Sort: HITS / Order: ascending
PID    UID CMD     HITS  MISSES DIRTIES READ_HIT% WRITE_HIT%
18280 root python   22     0       0     100.0%    0.0%
18279 root dd      41088 41022     0      50.0%    50.0%

通过cachetop的输出,我们看到,dd进程的读写请求只有50%的命中率,并且未命中的缓存⻚数(MISSES)为41022(单位是⻚)。这说明,正是案例开始时运⾏的dd,导致了缓冲区使⽤升⾼。

你可能接着会问,为什么Swap也跟着升⾼了呢?直观来说,缓冲区占了大部分内存,还属于可回收内存,内存不够用时,不应该先回收缓存区吗?

这种情况,我们还得进一步通过/proc/zoneinfo,观察剩余内存、内存阈值以及匿名页和文件页的活跃情况。

可以在第⼆个终端中,按下Ctrl+C,停⽌cachetop命令。然后运⾏下⾯的命令,观察 /proc/zoneinfo 中这⼏个指标的变化情况:

# -d 表示⾼亮变化的字段
# -A 表示仅显示Normal⾏以及之后的15⾏输出
$ watch -d grep -A 15 'Normal' /proc/zoneinfo
Node 0, zone Normal
pages free 21328
min 14896
low 18620
high 22344
spanned 1835008
present 1835008
managed 1796710
protection: (0, 0, 0, 0, 0)
nr_free_pages 21328
nr_zone_inactive_anon 79776
nr_zone_active_anon 206854
nr_zone_inactive_file 918561
nr_zone_active_file 496695
nr_zone_unevictable 2251
nr_zone_write_pending 0

你可以发现,剩余内存(pages_free)在⼀个⼩范围内不停地波动。当它⼩于⻚低阈值(pages_low) 时,⼜会突然增⼤到⼀个⼤于⻚⾼阈值(pages_high)的值。

再结合刚刚⽤ sar 看到的剩余内存和缓冲区的变化情况,我们可以推导出,剩余内存和缓冲区的波动变化,正是由于内存回收和缓存再次分配的循环往复。

  • 当剩余内存小于页低阈值时,系统会回收一些缓存和匿名内存,使得剩余内存增长。其中,缓存的回收导致sar中的缓存区域小,而匿名内存的回收导致了swap的使用增大。
  • 紧接着,由于dd还在继续,剩余内存⼜会重新分配给缓存,导致剩余内存减少,缓冲区增⼤

其实还有⼀个有趣的现象,如果多次运⾏dd和sar,你可能会发现,在多次的循环重复中,有时候是Swap⽤得⽐较多,有时候Swap很少,反⽽缓冲区的波动更⼤。

换句话说,系统回收内存时,有时候会回收更多的⽂件⻚,有时候⼜回收了更多的匿名⻚。

显然,系统回收不同类型内存的倾向,似乎不那么明显。swappiness,正是调整不同类型内存回收的配置选项。

$ cat /proc/sys/vm/swappiness
60

swappiness显示的是默认值60,这是⼀个相对中和的配置,所以系统会根据实际运⾏情况,选择合适的回收类型,⽐如回收不活跃的匿名⻚,或者不活跃的⽂件⻚。

到这⾥,我们已经找出了Swap发⽣的根源。另⼀个问题就是,刚才的Swap到底影响了哪些应⽤程序呢?换句话说,Swap换出的是哪些进程的内存?

⾥我还是推荐 proc⽂件系统,⽤来查看进程Swap换出的虚拟内存⼤⼩,它保存在 /proc/pid/status中的VmSwap中(推荐你执⾏man proc来查询其他字段的含义)。

# 按VmSwap使⽤量对进程排序,输出进程名称、进程ID以及SWAP⽤量
$ for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head
dockerd 2226 10728 kB
docker-containe 2251 8516 kB
snapd 936 4020 kB
networkd-dispat 911 836 kB
polkitd 1004 44 kB

从这⾥你可以看到,使⽤Swap⽐较多的是dockerd 和 docker-containe 进程,所以,当dockerd再次访问这些换出到磁盘的内存时,也会⽐较慢。

这也说明了一点,虽然缓存属于可回收内存,但在类似大文件拷贝这类场景下,系统还是会用swap机制来回收匿名内存,而不仅仅是回收占用绝大部分内存的文件页。

最后,如果你在⼀开始配置了 Swap,不要忘记在案例结束后关闭。你可以运⾏下⾯的命令,关闭Swap:

$ swapoff -a

实际上,关闭Swap后再重新打开,也是⼀种常⽤的Swap空间清理⽅法,⽐如:

$ swapoff -a && swapon -a

小结

在内存资源紧张时,Linux会通过Swap,把不常访问的匿名页换出到磁盘中,下次访问的时候再从磁盘中换入到内存中来。可以设置/proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值;也可以设置/proc/sys/vm/swappiness,来调整⽂件⻚和匿名⻚的回收倾向。

当Swap变⾼时,可以⽤ sar、/proc/zoneinfo、/proc/pid/status等⽅法,查看系统和进程的内存使⽤情况,进⽽找出Swap升⾼的根源和受影响的进程。

反过来说,通常,降低Swap的使⽤,可以提⾼系统的整体性能。要怎么做呢?这⾥,我也总结了⼏种常⻅的降低⽅法。

  • 禁止swap,现在服务器的内存足够大,所以除非有必要,禁用swap即可。随着云计算的普及,⼤部分云平台中的虚拟机都默认禁⽌Swap。
  • 如果实在需要⽤到Swap,可以尝试降低swappiness的值,减少内存回收时Swap的使⽤倾向。
  • 响应延时敏感的应用,如果它们可能在开启swap的服务器中运行,你可以用mlock() 或者 mlockall()锁定内存,阻⽌它们的内存换出。

 

posted @ 2022-01-26 14:42  CharyGao  阅读(80)  评论(0编辑  收藏  举报