linux操作系统:内核是如何fork出一个进程的,为什么系统的swap变高了,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()锁定内存,阻⽌它们的内存换出。