From 64fd26d4dcf6e3b2890a6c427aed8280bbf0fdfd Mon Sep 17 00:00:00 2001 From: gatieme Date: Fri, 5 Aug 2016 23:51:02 +0800 Subject: [PATCH] =?UTF-8?q?=E9=8F=87=E5=AD=98=E6=9F=8A=E6=B5=9C=E5=97=97?= =?UTF-8?q?=E5=94=B4=E7=80=9B=E6=A8=BC=EE=85=B8=E9=90=9E=E5=97=95=E7=AE=A3?= =?UTF-8?q?=E9=8D=90=E5=91=AD=E7=93=A8=E9=8E=BB=E5=BF=9A=E5=A0=AA(?= =?UTF-8?q?=E9=8D=A5=3F--=E6=A4=A4=E9=9D=9B=E6=8A=9Astruct=20page=E7=92=87?= =?UTF-8?q?=EF=B9=81=D0=92...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../05-schedule/01-introduction/README.md | 187 ++++- .../05-schedule/03-design/01-design/README.md | 57 -- .../02-memory/01-description/02-node/1.c | 17 + .../01-description/02-node/README.md | 636 ++++++++++++------ study/kernel/02-memory/02-pagetable/README.md | 177 +++++ 5 files changed, 790 insertions(+), 284 deletions(-) create mode 100644 study/kernel/02-memory/01-description/02-node/1.c create mode 100644 study/kernel/02-memory/02-pagetable/README.md diff --git a/study/kernel/01-process/05-schedule/01-introduction/README.md b/study/kernel/01-process/05-schedule/01-introduction/README.md index 00f0f24..f7f128d 100644 --- a/study/kernel/01-process/05-schedule/01-introduction/README.md +++ b/study/kernel/01-process/05-schedule/01-introduction/README.md @@ -148,7 +148,7 @@ linux进程的调度算法其实经过了很多次的演变, 但是其演变主 | O(1)调度器 | linux-2.5 | | CFS调度器 | linux-2.6~至今 | - +关于调度器发展的详细内容, 可以阅读[Linux进程调度策略的发展和演变--Linux进程的管理与调度(十六)](http://blog.csdn.net/gatieme/article/details/51701149) #4 Linux的调度器设计 ------- @@ -287,6 +287,8 @@ linux针对实时进程实现了Roound-Robin, FIFO和Earliest-Deadline-First(EDF ##4.2 进程的调度 ------- + + 首先,我们需要清楚,什么样的进程会进入调度器进行选择,就是处于TASK_RUNNING状态的进程,而其他状态下的进程都不会进入调度器进行调度。 系统发生调度的时机如下 @@ -312,24 +314,193 @@ linux针对实时进程实现了Roound-Robin, FIFO和Earliest-Deadline-First(EDF 系统并不是每时每刻都允许调度的发生,当处于硬中断期间的时候,调度是被系统禁止的,之后硬中断过后才重新允许调度。而对于异常,系统并不会禁止调度,也就是在异常上下文中,系统是有可能发生调度的。 +##4.3 抢占标识TIF_NEED_RESCHED -##周期性调度器 +内核在检查need_resched标识TIF_NEED_RESCHED的值判断是否需要抢占当前进程, 内核在thread_info的flag中设置了一个标识来标志进程是否需要重新调度, 即重新调度need_resched标识TIF_NEED_RESCHED, 内核在即将返回用户空间时会检查标识TIF_NEED_RESCHED标志进程是否需要重新调度 + +系统中每个进程都有一个特定于体系结构的struct thread_info结构, 用户层程序被调度的时候会检查struct thread_info中的need_resched标识TLF_NEED_RESCHED标识来检查自己是否需要被重新调度. + +如果内核检查进程的抢占标识被设置, 则会在一个关键的时刻, 调用调度器来完成调度和抢占的工作 + + + +##4.4 内核抢占和用户抢占 +------- + +而根据进程抢占发生的时机, 抢占可以分为内核抢占和用户抢占, **内核抢占**就是指一个在内核态运行的进程, 可能在执行内核函数期间被另一个进程取 + +一般来说,用户抢占发生几下情况: + +* 从系统调用返回用户空间; + +* 从中断(异常)处理程序返回用户空间 + +内核抢占发生的时机,一般发生在: + +* 当从中断处理程序正在执行,且返回内核空间之前。当一个中断处理例程退出,在返回到内核态时(kernel-space)。这是隐式的调用schedule()函数,当前任务没有主动放弃CPU使用权,而是被剥夺了CPU使用权。 + +* 当内核代码再一次具有可抢占性的时候,如解锁(spin_unlock_bh)及使能软中断(local_bh_enable)等, 此时当kernel code从不可抢占状态变为可抢占状态时(preemptible again)。也就是preempt_count从正整数变为0时。这也是隐式的调用schedule()函数 + +* 如果内核中的任务显式的调用schedule(), 任务主动放弃CPU使用权 + +* 如果内核中的任务阻塞(这同样也会导致调用schedule()), 导致需要调用schedule()函数。任务主动放弃CPU使用权 + + +内核抢占采用同抢占标识的类似方法被实现, linux内核在thread_info结构中添加了一个自旋锁标识preempt_count, 称为**抢占计数器(preemption counter)**. + +```c +struct thread_info +{ + /* ...... */ + int preempt_count; /* 0 => preemptable, <0 => BUG */ + /* ...... */ +} +``` + +| preempt_count值 | 描述 | +| ------- |:-------:| +| >0 | 禁止内核抢占, 其值标记了使用preempt_count的临界区的数目 | +| 0 | 开启内核抢占 | +| <0 | 锁为负值, 内核出现错误 | + +内核自然也提供了一些函数或者宏, 用来开启, 关闭以及检测抢占计数器preempt_coun的值, 这些通用的函数定义在[include/asm-generic/preempt.h](http://lxr.free-electrons.com/source/include/asm-generic/preempt.h?v=4.6#L8), 而某些架构也定义了自己的接口, 比如x86架构[/arch/x86/include/asm/preempt.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/preempt.h?v=4.6) + + +>详细内容请参见 [Linux用户抢占和内核抢占详解(概念, 实现和触发时机)--Linux进程的管理与调度(二十)](http://blog.csdn.net/gatieme/article/details/51872618) + + +##4.5 周期性调度器scheduler_tick ------- -##核心调度器 +**周期调度器** + +周期性调度器scheduler_tick由内核时钟中断周期性的触发, 周期性调度器以固定的频率激活负责当前进程调度类的周期性调度方法, 以保证系统的并发性, 周期性调度器通过调用进程所属调度器类的task_tick操作完成周期性调度的通知和配置工作, 通过resched_curr函数(早期的resched_task函数)设置抢占标识TIF_NEED_RESCHED来通知内核在必要的时间由主调度函数完成真正的调度工作, 此种做法称之为延迟调度策略 + +>关于周期性调度器的详细信息, 参见[Linux核心调度器之周期性调度器scheduler_tick--Linux进程的管理与调度(十八)](http://blog.csdn.net/gatieme/article/details/51872561) + + + + + +##4.6 主调度器schedule +------- + +**主调度器** + +schedule就是主调度器的工作函数, 在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule或者其子函数__schedule. + +**__schedule完成抢占** + +* 完成一些必要的检查, 并设置进程状态, 处理进程所在的就绪队列 + +* 调度全局的pick_next_task选择抢占的进程 + + 如果当前cpu上所有的进程都是cfs调度的普通非实时进程, 则直接用cfs调度, 如果无程序可调度则调度idle进程 + + 否则从优先级最高的调度器类sched_class_highest(目前是stop_sched_class)开始依次遍历所有调度器类的pick_next_task函数, 选择最优的那个进程执行 + +* context_switch完成进程上下文切换 + + 调用switch_mm(), 把虚拟内存从一个进程映射切换到新进程中 + + 调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息 + +>主调度器的详细信息, 可以参考 [Linux进程核心调度器之主调度器--Linux进程的管理与调度(十九)](http://blog.csdn.net/gatieme/article/details/51872594) + + +##4.7 进程上下文切换context_switch ------- -##linux内核抢占与用户抢占 +**context_switch流程** + +context_switch其实是一个分配器, 他会调用所需的特定体系结构的方法 + +* 调用switch_mm(), 把虚拟内存从一个进程映射切换到新进程中 + + switch_mm更换通过task_struct->mm描述的内存管理上下文, 该工作的细节取决于处理器, 主要包括加载页表, 刷出地址转换后备缓冲器(部分或者全部), 向内存管理单元(MMU)提供新的信息 + +* 调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息 + + switch_to切换处理器寄存器的呢内容和内核栈(虚拟地址空间的用户部分已经通过switch_mm变更, 其中也包括了用户状态下的栈, 因此switch_to不需要变更用户栈, 只需变更内核栈), 此段代码严重依赖于体系结构, 且代码通常都是用汇编语言编写. + + +**为什么switch_to需要3个参数** + + + +在新进程被选中执行时, 内核恢复到进程被切换出去的点继续执行, 此时内核只知道谁之前将新进程抢占了, 但是却不知道新进程再次执行是抢占了谁, 因此底层的进程切换机制必须将此前执行的进程(即新进程抢占的那个进程)提供给context_switch. 由于控制流会回到函数的该中间, 因此无法通过普通函数的返回值来完成. 因此使用了一个3个参数, 但是逻辑效果是相同的, 仿佛是switch_to是带有两个参数的函数, 而且返回了一个指向此前运行的进程的指针. + +```c +switch_to(prev, next, last); + +即 + +prev = last = switch_to(prev, next); +``` +其中返回的prev值并不是做参数的prev值, 而是prev被再次调度的时候抢占掉的那个进程last. + +详情请参见, [Linux进程上下文切换过程context_switch详解--Linux进程的管理与调度(二十一)](http://blog.csdn.net/gatieme/article/details/51872659) + + +##4.8 处理进程优先级 +------- +内核使用一些简单的数值范围0~139表示内部优先级, 数值越低, 优先级越高。 + +从0~99的范围专供实时进程使用, nice的值[-20,19]则映射到范围100~139 + +动态优先级 静态优先级 实时优先级 + +其中task_struct采用了三个成员表示进程的优先级:prio和normal_prio表示动态优先级, static_prio表示进程的静态优先级. + + +此外还用了一个字段rt_priority保存了实时进程的优先级 + +|字段 | 描述 | +|:-------:|:-------:| +| static_prio | 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定 | +| prio | 保存进程的动态优先级 | +| normal_prio | 表示基于进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级 | +| rt_priority | 用于保存实时优先级, 实时进程的优先级用实时优先级rt_priority来表示 | + +静态优先级static_prio(普通进程)和实时优先级rt_priority(实时进程)是计算的起点 + +因此他们也是进程创建的时候设定好的, 我们通过nice修改的就是普通进程的静态优先级static_prio + +首先通过静态优先级static_prio计算出普通优先级normal_prio, 该工作可以由nromal_prio来完成, 该函数定义在[kernel/sched/core.c#L861](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L861) + +内核通过ffective_prio设置动态优先级prio, 计算动态优先级的流程如下 + +* 设置进程的普通优先级(实时进程99-rt_priority, 普通进程为static_priority) + +* 计算进程的动态优先级(实时进程则维持动态优先级的prio不变, 普通进程的动态优先级即为其普通优先级) + +最后, 我们综述一下在针对不同类型进程的计算结果 + +| 进程类型 | 实时优先级rt_priority | 静态优先级static_prio | 普通优先级normal_prio | 动态优先级prio | +| ------- |:-------:|:-------:|:-------:| +| EDF调度的实时进程 | rt_priority | 不使用 | MAX_DL_PRIO-1 | 维持原prio不变 | +| RT算法调度的实时进程 | rt_priority | 不使用 | MAX_RT_PRIO-1-rt_priority | 维持原prio不变 | +| 普通进程 | 不使用 | static_prio | static_prio | static_prio | +| 优先级提高的普通进程 | 不使用 | static_prio(改变) | static_prio | 维持原prio不变 | + +>关于进程优先级的详细信息请参见[Linux进程优先级的处理--Linux进程的管理与调度(二十二)](http://blog.csdn.net/gatieme/article/details/51719208) + + + +##4.9 唤醒抢占 ------- -##处理进程优先级 -------- +当在try_to_wake_up/wake_up_process和wake_up_new_task中唤醒进程时, 内核使用全局check_preempt_curr看看是否进程可以抢占当前进程可以抢占当前运行的进程. + +每个调度器类都因应该实现一个check_preempt_curr函数, 在全局check_preempt_curr中会调用进程其所属调度器类check_preempt_curr进行抢占检查, 对于完全公平调度器CFS处理的进程, 则对应由check_preempt_wakeup函数执行该策略. + +新唤醒的进程不必一定由完全公平调度器处理, 如果新进程是一个实时进程, 则会立即请求调度, 因为实时进程优先极高, 实时进程总会抢占CFS进 -##linux进程上下文切换 -------- +参见[Linux唤醒抢占----Linux进程的管理与调度(二十三)](http://blog.csdn.net/gatieme/article/details/51872831) +>内核为了实现完全公平, 对一些交互式进程有补偿机制, 这些交互式进程多数情况下属于睡眠状态, 只有在接收到信号以后被唤醒, 比如vim在接收了键盘录入的信号后被唤醒, 完成工作后又进入睡眠态, 因此我们需要对唤醒的进程做一些补偿, 关于补偿的内容我们会在各个调度器类的设计中讲解. diff --git a/study/kernel/01-process/05-schedule/03-design/01-design/README.md b/study/kernel/01-process/05-schedule/03-design/01-design/README.md index e556fb7..2187fdb 100644 --- a/study/kernel/01-process/05-schedule/03-design/01-design/README.md +++ b/study/kernel/01-process/05-schedule/03-design/01-design/README.md @@ -89,63 +89,6 @@ linux把进程区分为实时进程和非实时进程, 其中非实时进程进 并且每个调度器包括两个内容:**调度框架**(其实质就是两个函数框架)及**调度器类** -**抢占标识TIF_NEED_RESCHED** -内核在检查need_resched标识TIF_NEED_RESCHED的值判断是否需要抢占当前进程, 内核在thread_info的flag中设置了一个标识来标志进程是否需要重新调度, 即重新调度need_resched标识TIF_NEED_RESCHED, 内核在即将返回用户空间时会检查标识TIF_NEED_RESCHED标志进程是否需要重新调度 - -**周期调度器** - -周期性调度器scheduler_tick由内核时钟中断周期性的触发, 周期性调度器以固定的频率激活负责当前进程调度类的周期性调度方法, 以保证系统的并发性, 周期性调度器通过调用进程所属调度器类的task_tick操作完成周期性调度的通知和配置工作, 通过resched_curr函数(早期的resched_task函数)设置抢占标识TIF_NEED_RESCHED来通知内核在必要的时间由主调度函数完成真正的调度工作, 此种做法称之为延迟调度策略 - ->关于周期性调度器的详细信息, 参见[Linux核心调度器之周期性调度器scheduler_tick--Linux进程的管理与调度(十八)](http://blog.csdn.net/gatieme/article/details/51872561) - -**主调度器** - -schedule就是主调度器的工作函数, 在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule或者其子函数__schedule. - -**__schedule完成抢占** - -* 完成一些必要的检查, 并设置进程状态, 处理进程所在的就绪队列 - -* 调度全局的pick_next_task选择抢占的进程 - - 如果当前cpu上所有的进程都是cfs调度的普通非实时进程, 则直接用cfs调度, 如果无程序可调度则调度idle进程 - - 否则从优先级最高的调度器类sched_class_highest(目前是stop_sched_class)开始依次遍历所有调度器类的pick_next_task函数, 选择最优的那个进程执行 - -* context_switch完成进程上下文切换 - - 调用switch_mm(), 把虚拟内存从一个进程映射切换到新进程中 - - 调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息 - ->主调度器的详细信息, 可以参考 [Linux进程核心调度器之主调度器--Linux进程的管理与调度(十九)](http://blog.csdn.net/gatieme/article/details/51872594) - - - - - -**内核抢占和用户抢占** - -而根据进程抢占发生的时机, 抢占可以分为内核抢占和用户抢占, **内核抢占**就是指一个在内核态运行的进程, 可能在执行内核函数期间被另一个进程取 - -一般来说,用户抢占发生几下情况: - -* 从系统调用返回用户空间; - -* 从中断(异常)处理程序返回用户空间 - -内核抢占发生的时机,一般发生在: - -* 当从中断处理程序正在执行,且返回内核空间之前。当一个中断处理例程退出,在返回到内核态时(kernel-space)。这是隐式的调用schedule()函数,当前任务没有主动放弃CPU使用权,而是被剥夺了CPU使用权。 - -* 当内核代码再一次具有可抢占性的时候,如解锁(spin_unlock_bh)及使能软中断(local_bh_enable)等, 此时当kernel code从不可抢占状态变为可抢占状态时(preemptible again)。也就是preempt_count从正整数变为0时。这也是隐式的调用schedule()函数 - -* 如果内核中的任务显式的调用schedule(), 任务主动放弃CPU使用权 - -* 如果内核中的任务阻塞(这同样也会导致调用schedule()), 导致需要调用schedule()函数。任务主动放弃CPU使用权 - - ->详细内容请参见 [Linux用户抢占和内核抢占详解(概念, 实现和触发时机)--Linux进程的管理与调度(二十)](http://blog.csdn.net/gatieme/article/details/51872618) ##2.2 6种调度策略 ------- diff --git a/study/kernel/02-memory/01-description/02-node/1.c b/study/kernel/02-memory/01-description/02-node/1.c new file mode 100644 index 0000000..bd4f045 --- /dev/null +++ b/study/kernel/02-memory/01-description/02-node/1.c @@ -0,0 +1,17 @@ +enum node_states { + N_POSSIBLE, /* The node could become online at some point */ + N_ONLINE, /* The node is online */ + N_NORMAL_MEMORY, /* The node has regular memory */ +#ifdef CONFIG_HIGHMEM + N_HIGH_MEMORY, /* The node has regular or high memory */ +#else + N_HIGH_MEMORY = N_NORMAL_MEMORY, +#endif +#ifdef CONFIG_MOVABLE_NODE + N_MEMORY, /* The node has memory(regular, high, movable) */ +#else + N_MEMORY = N_HIGH_MEMORY, +#endif + N_CPU, /* The node has one or more cpus */ + NR_NODE_STATES +}; \ No newline at end of file diff --git a/study/kernel/02-memory/01-description/02-node/README.md b/study/kernel/02-memory/01-description/02-node/README.md index a1ae7c3..56f5818 100644 --- a/study/kernel/02-memory/01-description/02-node/README.md +++ b/study/kernel/02-memory/01-description/02-node/README.md @@ -1,219 +1,417 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - -#1 前景回顾 -------- - - -前面我们讲到[服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA)](http://blog.csdn.net/gatieme/article/details/52098615) - - -#1.1 UMA和NUMA两种模型 -------- - - -共享存储型多处理机有两种模型 - -* 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 - - -* 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型 - - -**UMA模型** - -物理存储器被所有处理机均匀共享。所有处理机对所有存储字具有相同的存取时间,这就是为什么称它为均匀存储器存取的原因。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。 - -**NUMA模型** - -NUMA模式下,处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。 - - -#1.2 (N)UMA模型中linux内存的机构 -------- - - - -非一致存储器访问(NUMA)模式下 - -* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 - - -* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. 比如,可能把内存的一个簇指派给每个处理器,或则某个簇和设备卡很近,很适合DMA,那么就指派给该设备。因此当前的多数系统会把内存系统分割成2块区域,一块是专门给CPU去访问,一块是给外围设备板卡的DMA去访问 - ->在UMA系统中, 内存就相当于一个只使用一个NUMA节点来管理整个系统的内存. 而内存管理的其他地方则认为他们就是在处理一个(伪)NUMA系统. - - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - - - - - -#2 内存节点node -------- - - -##2.1 为什么要用node来描述内存 -------- - - -这点前面是说的很明白了, NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快 - -Linux适用于各种不同的体系结构, 而不同体系结构在内存管理方面的差别很大. 因此linux内核需要用一种体系结构无关的方式来表示内存. - -因此linux内核把物理内存按照CPU节点划分为不同的node, 每个node作为某个cpu结点的本地内存, 而作为其他CPU节点的远程内存, 而UMA结构下, 则任务系统中只存在一个内存node, 这样对于UMA结构来说, 内核把内存当成只有一个内存node节点的伪NUMA - - -##2.2 内存结点的概念 -------- - ->CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 -> ->系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - -内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - - -内存中的每个节点都是由pg_data_t描述,而pg_data_t由struct pglist_data定义而来, 该数据结构定义在[include/linux/mmzone.h, line 615](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L615) - - -在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到 - - - - -##2.3 pg_data_t描述内存节点 -------- - -表示node的数据结构为[`typedef struct pglist_data pg_data_t`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630), 这个结构定义在[include/linux/mmzone.h, line 615](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L615)中,结构体的内容如下 - -```c -/* - * The pg_data_t structure is used in machines with CONFIG_DISCONTIGMEM - * (mostly NUMA machines?) to denote a higher-level memory zone than the - * zone denotes. - * - * On NUMA machines, each NUMA node would have a pg_data_t to describe - * it's memory layout. - * - * Memory statistics and page replacement data structures are maintained on a - * per-zone basis. - */ -struct bootmem_data; -typedef struct pglist_data { - /* 包含了结点中各内存域的数据结构 , 可能的区域类型用zone_type表示*/ - struct zone node_zones[MAX_NR_ZONES]; - /* 指点了备用结点及其内存域的列表,以便在当前结点没有可用空间时,在备用结点分配内存 */ - struct zonelist node_zonelists[MAX_ZONELISTS]; - int nr_zones; /* 保存结点中不同内存域的数目 */ -#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */ - struct page *node_mem_map; /* 指向page实例数组的指针,用于描述结点的所有物理内存页,它包含了结点中所有内存域的页。 */ -#ifdef CONFIG_PAGE_EXTENSION - struct page_ext *node_page_ext; -#endif -#endif -#ifndef CONFIG_NO_BOOTMEM - /* 在系统启动boot期间,内存管理子系统初始化之前, - 内核页需要使用内存(另外,还需要保留部分内存用于初始化内存管理子系统) - 为解决这个问题,内核使用了自举内存分配器 - 此结构用于这个阶段的内存管理 */ - struct bootmem_data *bdata; -#endif -#ifdef CONFIG_MEMORY_HOTPLUG - /* - * Must be held any time you expect node_start_pfn, node_present_pages - * or node_spanned_pages stay constant. Holding this will also - * guarantee that any pfn_valid() stays that way. - * - * pgdat_resize_lock() and pgdat_resize_unlock() are provided to - * manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG. - * - * Nests above zone->lock and zone->span_seqlock - * 当系统支持内存热插拨时,用于保护本结构中的与节点大小相关的字段。 - * 哪调用node_start_pfn,node_present_pages,node_spanned_pages相关的代码时,需要使用该锁。 - */ - spinlock_t node_size_lock; -#endif - /* /*起始页面帧号,指出该节点在全局mem_map中的偏移 - 系统中所有的页帧是依次编号的,每个页帧的号码都是全局唯一的(不只是结点内唯一) */ - unsigned long node_start_pfn; - unsigned long node_present_pages; /* total number of physical pages 结点中页帧的数目 */ - unsigned long node_spanned_pages; /* total size of physical page range, including holes 该结点以页帧为单位计算的长度,包含内存空洞 */ - int node_id; /* 全局结点ID,系统中的NUMA结点都从0开始编号 */ - wait_queue_head_t kswapd_wait; /* 交换守护进程的等待队列, - 在将页帧换出结点时会用到。后面的文章会详细讨论。 */ - wait_queue_head_t pfmemalloc_wait; - struct task_struct *kswapd; /* Protected by mem_hotplug_begin/end() 指向负责该结点的交换守护进程的task_struct。 */ - int kswapd_max_order; /* 定义需要释放的区域的长度 */ - enum zone_type classzone_idx; - -#ifdef CONFIG_COMPACTION - int kcompactd_max_order; - enum zone_type kcompactd_classzone_idx; - wait_queue_head_t kcompactd_wait; - struct task_struct *kcompactd; -#endif - -#ifdef CONFIG_NUMA_BALANCING - /* Lock serializing the migrate rate limiting window */ - spinlock_t numabalancing_migrate_lock; - - /* Rate limiting time interval */ - unsigned long numabalancing_migrate_next_window; - - /* Number of pages migrated during the rate limiting time interval */ - unsigned long numabalancing_migrate_nr_pages; -#endif - -#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT - /* - * If memory initialisation on large machines is deferred then this - * is the first PFN that needs to be initialised. - */ - unsigned long first_deferred_pfn; -#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */ - -#ifdef CONFIG_TRANSPARENT_HUGEPAGE - spinlock_t split_queue_lock; - struct list_head split_queue; - unsigned long split_queue_len; -#endif -} pg_data_t; -``` - -| 字段| 描述 | -| :------- | ----: | -|node_zones | 每个Node划分为不同的zone,分别为ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM | -|node_zonelists | 这个是备用节点及其内存域的列表,当当前节点的内存不够分配时,会选取访问代价最低的内存进行分配。分配内存操作时的区域顺序,当调用free_area_init_core()时,由mm/page_alloc.c文件中的build_zonelists()函数设置 | -|nr_zones | 当前节点中不同内存域zone的数量,1到3个之间。并不是所有的node都有3个zone的,比如一个CPU簇就可能没有ZONE_DMA区域 | -| node_mem_map | node中的第一个page,它可以指向mem_map中的任何一个page,指向page实例数组的指针,用于描述该节点所拥有的的物理内存页,它包含了该页面所有的内存页,被放置在全局mem_map数组中 | -| bdata | 这个仅用于引导程序boot 的内存分配,内存在启动时,也需要使用内存,在这里内存使用了自举内存分配器,这里bdata是指向内存自举分配器的数据结构的实例 | -| node_start_pfn | pfn是page frame number的缩写。这个成员是用于表示node中的开始那个page在物理内存中的位置的。是当前NUMA节点的第一个页帧的编号,系统中所有的页帧是依次进行编号的,这个字段代表的是当前节点的页帧的起始值,对于UMA系统,只有一个节点,所以该值总是0 | -|node_present_pages | node中的真正可以使用的page数量 | -|node_spanned_pages | 该节点以页帧为单位的总长度,这个不等于前面的node_present_pages,因为这里面包含空洞内存 | -|node_id | node的NODE ID 当前节点在系统中的编号,从0开始 | -| kswapd_wait | node的等待队列,交换守护列队进程的等待列表| -| kswapd_max_order | 需要释放的区域的长度,以页阶为单位 | -| classzone_idx | 这个字段暂时没弄明白,不过其中的zone_type是对ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGH,ZONE_MOVABLE,__MAX_NR_ZONES的枚举 | - - -在新的linux3.x~linux4.x的内核中,Linux定义了一个大小为[MAX_NUMNODES](http://lxr.free-electrons.com/source/include/linux/numa.h#L11)类型为[`pgdat_list`](http://lxr.free-electrons.com/source/arch/ia64/mm/discontig.c#L50)数组,数组的大小根据[CONFIG_NODES_SHIFT](http://lxr.free-electrons.com/source/include/linux/numa.h#L6)的配置决定。对于UMA来说,NODES_SHIFT为0,所以MAX_NUMNODES的值为1。内核提供了[for_each_online_pgdat(pgdat)](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L871)来遍历节点 - ->而在linux-2.4.x之前的内核中所有的节点,都由一个被称为[pgdat_list](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L169)的链表维护。这些节点都放在该链表中,均由函数[init_bootmem_core()](http://lxr.free-electrons.com/source/mm/bootmem.c#L96)初始化结点。内核提供了[宏for_each_pgdat(pgdat)]http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L169)来遍历节点链表。 - -对于单一node的系统,contig_page_data 是系统唯一的node数据结构对象。查看contig_page_data的定义[linux-4.5](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L27),[linux-2.4.37](http://lxr.free-electrons.com/source/mm/numa.c?v=2.4.37#L15) - - +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + +#1 前景回顾 +------- + + +前面我们讲到[服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA)](http://blog.csdn.net/gatieme/article/details/52098615) + + +#1.1 UMA和NUMA两种模型 +------- + + +共享存储型多处理机有两种模型 + +* 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 + + +* 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型 + + +**UMA模型** + +物理存储器被所有处理机均匀共享。所有处理机对所有存储字具有相同的存取时间,这就是为什么称它为均匀存储器存取的原因。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。 + +**NUMA模型** + +NUMA模式下,处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。 + + +#1.2 (N)UMA模型中linux内存的机构 +------- + + + +非一致存储器访问(NUMA)模式下 + +* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 + + +* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. 比如,可能把内存的一个簇指派给每个处理器,或则某个簇和设备卡很近,很适合DMA,那么就指派给该设备。因此当前的多数系统会把内存系统分割成2块区域,一块是专门给CPU去访问,一块是给外围设备板卡的DMA去访问 + +>在UMA系统中, 内存就相当于一个只使用一个NUMA节点来管理整个系统的内存. 而内存管理的其他地方则认为他们就是在处理一个(伪)NUMA系统. + + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + + + + + +#2 内存节点node +------- + + +##2.1 为什么要用node来描述内存 +------- + + +这点前面是说的很明白了, NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快 + +Linux适用于各种不同的体系结构, 而不同体系结构在内存管理方面的差别很大. 因此linux内核需要用一种体系结构无关的方式来表示内存. + +因此linux内核把物理内存按照CPU节点划分为不同的node, 每个node作为某个cpu结点的本地内存, 而作为其他CPU节点的远程内存, 而UMA结构下, 则任务系统中只存在一个内存node, 这样对于UMA结构来说, 内核把内存当成只有一个内存node节点的伪NUMA + + +##2.2 内存结点的概念 +------- + +>CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 +> +>系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + +内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + + +内存中的每个节点都是由pg_data_t描述,而pg_data_t由struct pglist_data定义而来, 该数据结构定义在[include/linux/mmzone.h, line 615](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L615) + + +在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到 + + + + +##2.3 pg_data_t描述内存节点 +------- + +表示node的数据结构为[`typedef struct pglist_data pg_data_t`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630), 这个结构定义在[include/linux/mmzone.h, line 615](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L615)中,结构体的内容如下 + +```c +/* + * The pg_data_t structure is used in machines with CONFIG_DISCONTIGMEM + * (mostly NUMA machines?) to denote a higher-level memory zone than the + * zone denotes. + * + * On NUMA machines, each NUMA node would have a pg_data_t to describe + * it's memory layout. + * + * Memory statistics and page replacement data structures are maintained on a + * per-zone basis. + */ +struct bootmem_data; +typedef struct pglist_data { + /* 包含了结点中各内存域的数据结构 , 可能的区域类型用zone_type表示*/ + struct zone node_zones[MAX_NR_ZONES]; + /* 指点了备用结点及其内存域的列表,以便在当前结点没有可用空间时,在备用结点分配内存 */ + struct zonelist node_zonelists[MAX_ZONELISTS]; + int nr_zones; /* 保存结点中不同内存域的数目 */ +#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */ + struct page *node_mem_map; /* 指向page实例数组的指针,用于描述结点的所有物理内存页,它包含了结点中所有内存域的页。 */ +#ifdef CONFIG_PAGE_EXTENSION + struct page_ext *node_page_ext; +#endif +#endif +#ifndef CONFIG_NO_BOOTMEM + /* 在系统启动boot期间,内存管理子系统初始化之前, + 内核页需要使用内存(另外,还需要保留部分内存用于初始化内存管理子系统) + 为解决这个问题,内核使用了自举内存分配器 + 此结构用于这个阶段的内存管理 */ + struct bootmem_data *bdata; +#endif +#ifdef CONFIG_MEMORY_HOTPLUG + /* + * Must be held any time you expect node_start_pfn, node_present_pages + * or node_spanned_pages stay constant. Holding this will also + * guarantee that any pfn_valid() stays that way. + * + * pgdat_resize_lock() and pgdat_resize_unlock() are provided to + * manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG. + * + * Nests above zone->lock and zone->span_seqlock + * 当系统支持内存热插拨时,用于保护本结构中的与节点大小相关的字段。 + * 哪调用node_start_pfn,node_present_pages,node_spanned_pages相关的代码时,需要使用该锁。 + */ + spinlock_t node_size_lock; +#endif + /* /*起始页面帧号,指出该节点在全局mem_map中的偏移 + 系统中所有的页帧是依次编号的,每个页帧的号码都是全局唯一的(不只是结点内唯一) */ + unsigned long node_start_pfn; + unsigned long node_present_pages; /* total number of physical pages 结点中页帧的数目 */ + unsigned long node_spanned_pages; /* total size of physical page range, including holes 该结点以页帧为单位计算的长度,包含内存空洞 */ + int node_id; /* 全局结点ID,系统中的NUMA结点都从0开始编号 */ + wait_queue_head_t kswapd_wait; /* 交换守护进程的等待队列, + 在将页帧换出结点时会用到。后面的文章会详细讨论。 */ + wait_queue_head_t pfmemalloc_wait; + struct task_struct *kswapd; /* Protected by mem_hotplug_begin/end() 指向负责该结点的交换守护进程的task_struct。 */ + int kswapd_max_order; /* 定义需要释放的区域的长度 */ + enum zone_type classzone_idx; + +#ifdef CONFIG_COMPACTION + int kcompactd_max_order; + enum zone_type kcompactd_classzone_idx; + wait_queue_head_t kcompactd_wait; + struct task_struct *kcompactd; +#endif + +#ifdef CONFIG_NUMA_BALANCING + /* Lock serializing the migrate rate limiting window */ + spinlock_t numabalancing_migrate_lock; + + /* Rate limiting time interval */ + unsigned long numabalancing_migrate_next_window; + + /* Number of pages migrated during the rate limiting time interval */ + unsigned long numabalancing_migrate_nr_pages; +#endif + +#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT + /* + * If memory initialisation on large machines is deferred then this + * is the first PFN that needs to be initialised. + */ + unsigned long first_deferred_pfn; +#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */ + +#ifdef CONFIG_TRANSPARENT_HUGEPAGE + spinlock_t split_queue_lock; + struct list_head split_queue; + unsigned long split_queue_len; +#endif +} pg_data_t; +``` + +| 字段| 描述 | +| :------- | ----: | +|node_zones | 每个Node划分为不同的zone,分别为ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM | +|node_zonelists | 这个是备用节点及其内存域的列表,当当前节点的内存不够分配时,会选取访问代价最低的内存进行分配。分配内存操作时的区域顺序,当调用free_area_init_core()时,由mm/page_alloc.c文件中的build_zonelists()函数设置 | +|nr_zones | 当前节点中不同内存域zone的数量,1到3个之间。并不是所有的node都有3个zone的,比如一个CPU簇就可能没有ZONE_DMA区域 | +| node_mem_map | node中的第一个page,它可以指向mem_map中的任何一个page,指向page实例数组的指针,用于描述该节点所拥有的的物理内存页,它包含了该页面所有的内存页,被放置在全局mem_map数组中 | +| bdata | 这个仅用于引导程序boot 的内存分配,内存在启动时,也需要使用内存,在这里内存使用了自举内存分配器,这里bdata是指向内存自举分配器的数据结构的实例 | +| node_start_pfn | pfn是page frame number的缩写。这个成员是用于表示node中的开始那个page在物理内存中的位置的。是当前NUMA节点的第一个页帧的编号,系统中所有的页帧是依次进行编号的,这个字段代表的是当前节点的页帧的起始值,对于UMA系统,只有一个节点,所以该值总是0 | +|node_present_pages | node中的真正可以使用的page数量 | +|node_spanned_pages | 该节点以页帧为单位的总长度,这个不等于前面的node_present_pages,因为这里面包含空洞内存 | +|node_id | node的NODE ID 当前节点在系统中的编号,从0开始 | +| kswapd_wait | node的等待队列,交换守护列队进程的等待列表| +| kswapd_max_order | 需要释放的区域的长度,以页阶为单位 | +| classzone_idx | 这个字段暂时没弄明白,不过其中的zone_type是对ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGH,ZONE_MOVABLE,__MAX_NR_ZONES的枚举 | + + + + +##2.4 查找内存结点 +------- + +node_id作为全局节点id。 系统中的NUMA结点都是从0开始编号的 + + +在新的linux3.x~linux4.x的内核中,Linux定义了一个大小为[MAX_NUMNODES](http://lxr.free-electrons.com/source/include/linux/numa.h#L11)类型为[`pgdat_list`](http://lxr.free-electrons.com/source/arch/ia64/mm/discontig.c#L50)数组,数组的大小根据[CONFIG_NODES_SHIFT](http://lxr.free-electrons.com/source/include/linux/numa.h#L6)的配置决定。对于UMA来说,NODES_SHIFT为0,所以MAX_NUMNODES的值为1。内核提供了[for_each_online_pgdat(pgdat)](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L871)来遍历节点 + +>而在linux-2.4.x之前的内核中所有的节点,都由一个被称为[pgdat_list](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L169)的链表维护。这些节点都放在该链表中,均由函数[init_bootmem_core()](http://lxr.free-electrons.com/source/mm/bootmem.c#L96)初始化结点。内核提供了[宏for_each_pgdat(pgdat)]http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L169)来遍历节点链表。 + +内核提供了NODE_DATA(node_id)宏函数来按照编号来查找对应的结点 + +在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858), 其中全局唯一的cnode结点ontig_page_data定义在[mm/nobootmem.c?v=4.7, line 27](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L27), [linux-2.4.37](http://lxr.free-electrons.com/source/mm/numa.c?v=2.4.37#L15) + + + +```cpp +#ifndef CONFIG_NEED_MULTIPLE_NODES +extern struct pglist_data contig_page_data; +#define NODE_DATA(nid) (&contig_page_data) +#define NODE_MEM_MAP(nid) mem_map +else +/* ...... */ +#endif +``` + + +而对于NUMA结构的系统中, 通过pg_data_t->pgdat_next链接到下一个结点, 系统中所有结点都通过单链表链接起来, 其末尾是一个NULL指针标记 + +同时所有的node都存储在node_data数组中, +NODE_DATA直接通过node编号索引即可, 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA) + +```cpp +extern struct pglist_data *node_data[]; +#define NODE_DATA(nid) (node_data[(nid)]) +``` + + +##2.5 结点的内存管理域 +------- + +```cpp +typedef struct pglist_data { + /* 包含了结点中各内存域的数据结构 , 可能的区域类型用zone_type表示*/ + struct zone node_zones[MAX_NR_ZONES]; + /* 指点了备用结点及其内存域的列表,以便在当前结点没有可用空间时,在备用结点分配内存 */ + struct zonelist node_zonelists[MAX_ZONELISTS]; + int nr_zones; /* 保存结点中不同内存域的数目 */ + +} pg_data_t; +``` + +node_zones[MAX_NR_ZONES]数组保存了节点中各个内存域的数据结构, + +而node_zonelist则指定了备用节点以及其内存域的列表, 以便在当前结点没有可用空间时, 在备用节点分配内存. + +nr_zones存储了结点中不同内存域的数目 + + +##2.6 结点的内存页面 +------- + + +```cpp +typedef struct pglist_data +{ + struct page *node_mem_map; /* 指向page实例数组的指针,用于描述结点的所有物理内存页,它包含了结点中所有内存域的页。 */ + + /* /*起始页面帧号,指出该节点在全局mem_map中的偏移 + 系统中所有的页帧是依次编号的,每个页帧的号码都是全局唯一的(不只是结点内唯一) */ + unsigned long node_start_pfn; + unsigned long node_present_pages; /* total number of physical pages 结点中页帧的数目 */ + unsigned long node_spanned_pages; /* total size of physical page range, including holes 该结点以页帧为单位计算的长度,包含内存空洞 */ + int node_id; /* 全局结点ID,系统中的NUMA结点都从0开始编号 */ +} pg_data_t; +``` + +其中node_mem_map是指向页面page实例数组的指针, 用于描述结点的所有物理内存页. 它包含了结点中所有内存域的页. + +node_start_pfn是该NUMA结点的第一个页帧的逻辑编号. 系统中所有的节点的页帧是一次编号的, 每个页帧的编号是全局唯一的. node_start_pfn在UMA系统中总是0, 因为系统中只有一个内存结点, 因此其第一个页帧编号总是0. + +node_present_pages指定了结点中页帧的数目, 而node_spanned_pages则给出了该结点以页帧为单位计算的长度. 二者的值不一定相同, 因为结点中可能有一些空洞, 并不对应真正的页帧. + + +##2.7 交换守护进程 +------- + + +```cpp +typedef struct pglist_data +{ + wait_queue_head_t kswapd_wait; /* 交换守护进程的等待队列, + 在将页帧换出结点时会用到。后面的文章会详细讨论。 */ + wait_queue_head_t pfmemalloc_wait; + struct task_struct *kswapd; /* Protected by mem_hotplug_begin/end() 指向负责该结点的交换守护进程的task_struct。 */ +}; +``` + +kswapd指向了负责将该结点的交换守护进程的task_struct. 在将页帧换出结点时会唤醒该进程. + +kswap_wait是交换守护进程(swap daemon)的等待队列 + +而kswapd_max_order用于页交换子系统的实现, 用来定义需要释放的区域的长度. + + +#3 结点状态 + + +##3.1 结点状态标识node_states +------- + +内核用enum node_state变量标记了内存结点所有可能的状态信息, 其定义在[include/linux/nodemask.h?v=4.7, line 381](http://lxr.free-electrons.com/source/include/linux/nodemask.h?v=4.7#L381) + +```cpp +enum node_states { + N_POSSIBLE, /* The node could become online at some point + 结点在某个时候可能变成联机*/ + N_ONLINE, /* The node is online + 节点是联机的*/ + N_NORMAL_MEMORY, /* The node has regular memory + 结点是普通内存域 */ +#ifdef CONFIG_HIGHMEM + N_HIGH_MEMORY, /* The node has regular or high memory + 结点是普通或者高端内存域*/ +#else + N_HIGH_MEMORY = N_NORMAL_MEMORY, +#endif +#ifdef CONFIG_MOVABLE_NODE + N_MEMORY, /* The node has memory(regular, high, movable) */ +#else + N_MEMORY = N_HIGH_MEMORY, +#endif + N_CPU, /* The node has one or more cpus */ + NR_NODE_STATES +}; +``` + +| 状态 | 描述 | +|:-----:|:-----:| +| N_POSSIBLE | 结点在某个时候可能变成联机 | +| N_ONLINE | 节点是联机的 | +| N_NORMAL_MEMORY | 结点是普通内存域 | +| N_HIGH_MEMORY | 结点是普通或者高端内存域 | +| N_MEMORY | 结点是普通,高端内存或者MOVEABLE域 | +| N_CPU | 结点有一个或多个CPU | + +其中N_POSSIBLE, N_ONLINE和N_CPU用于CPU和内存的热插拔. + +对内存管理有必要的标志是N_HIGH_MEMORY和N_NORMAL_MEMORY, 如果结点有普通或高端内存则使用N_HIGH_MEMORY, 仅当结点没有高端内存时才设置N_NORMAL_MEMORY + + +```cpp + N_NORMAL_MEMORY, /* The node has regular memory + 结点是普通内存域 */ +#ifdef CONFIG_HIGHMEM + N_HIGH_MEMORY, /* The node has regular or high memory + 结点是高端内存域*/ +#else + /* 没有高端内存域, 仍设置N_NORMAL_MEMORY */ + N_HIGH_MEMORY = N_NORMAL_MEMORY, +#endif +``` + +同样ZONE_MOVABLE内存域同样用类似的方法设置, 仅当系统中存在ZONE_MOVABLE内存域内存域(配置了CONFIG_MOVABLE_NODE参数)时, N_MEMORY才被设定, 否则则被设定成N_HIGH_MEMORY, 而N_HIGH_MEMORY设定与否同样依赖于参数CONFIG_HIGHMEM的设定 + +```cpp +#ifdef CONFIG_MOVABLE_NODE + N_MEMORY, /* The node has memory(regular, high, movable) */ +#else + N_MEMORY = N_HIGH_MEMORY, +#endif +``` + +##3.2 结点状态设置函数 +------- + +内核提供了辅助函数来设置或者清楚位域活特定结点的一个比特位 + +```cpp +static inline int node_state(int node, enum node_states state) +static inline void node_set_state(int node, enum node_states state) +static inline void node_clear_state(int node, enum node_states state) +static inline int num_node_state(enum node_states state) +``` + +此外宏for_each_node_state(__node, __state)用来迭代处于特定状态的所有结点, +```cpp +#define for_each_node_state(__node, __state) \ + for_each_node_mask((__node), node_states[__state]) +``` + +而for_each_online_node(node)则负责迭代所有的活动结点. + +如果内核编译只支持当个结点(即使用平坦内存模型), 则没有结点位图, 上述操作该位图的函数则变成空操作, 其定义形式如下, 参见[include/linux/nodemask.h?v=4.7, line 406](http://lxr.free-electrons.com/source/include/linux/nodemask.h?v=4.7#L406) + +参见内核 +```cpp +#if MAX_NUMNODES > 1 + /* some real function */ +#else + /* some NULL function */ +#endif +``` \ No newline at end of file diff --git a/study/kernel/02-memory/02-pagetable/README.md b/study/kernel/02-memory/02-pagetable/README.md new file mode 100644 index 0000000..aa207c0 --- /dev/null +++ b/study/kernel/02-memory/02-pagetable/README.md @@ -0,0 +1,177 @@ +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + + +##参照 +------- + +| 链接 | +|:-------:| +| [内存管理(一)内存模型之Node](http://biancheng.dnbcw.info/linux/387391.html) | +| [Linux 内存管理 重要结构体](http://blog.chinaunix.net/uid-26009500-id-3078986.html) | +| [Bootmem机制](http://blog.csdn.net/samssm/article/details/25064897) | +| [Linux-2.6.32 NUMA架构之内存和调度](http://www.cnblogs.com/zhenjing/archive/2012/03/21/linux_numa.html) | +| [Linux 用户空间与内核空间——高端内存详解](http://blog.csdn.net/tommy_wxie/article/details/17122923) | +| [探索 Linux 内存模型](http://www.ibm.com/developerworks/cn/linux/l-memmod/) | +| [Linux内存管理](http://blog.chinaunix.net/uid/21718047/cid-151509-list-2.html) | +| [内存管理-之内核内存管理-基于linux3.10](http://blog.csdn.net/shichaog/article/details/45509917) | +| [内存管理(一)](http://www.cnblogs.com/openix/p/3334026.html) | +| [Linux内存管理原理](http://www.cnblogs.com/zhaoyl/p/3695517.html) | +| [第 15 章 内存映射和 DMA](http://www.embeddedlinux.org.cn/ldd3/ch15.html) | +| [ 内存管理(二)struct page ](http://blog.chinaunix.net/uid-30282771-id-5176971.html) | + + +#1 前景回顾 +------- + +前面我们讲到[服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA)](http://blog.csdn.net/gatieme/article/details/52098615) + +#1.1 UMA和NUMA两种模型 +------- + +共享存储型多处理机有两种模型 + +* 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 + + 将可用内存以连续方式组织起来, +* 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型 + +##1.2 UMA模型 +------- + +传统的多核运算是使用SMP(Symmetric Multi-Processor )模式:将多个处理器与一个集中的存储器和I/O总线相连。所有处理器只能访问同一个物理存储器,因此SMP系统有时也被称为一致存储器访问(UMA)结构体系,一致性意指无论在什么时候,处理器只能为内存的每个数据保持或共享唯一一个数值。 + + + +>物理存储器被所有处理机均匀共享。所有处理机对所有存储字具有相同的存取时间,这就是为什么称它为均匀存储器存取的原因。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。 + +很显然,SMP的缺点是可伸缩性有限,因为在存储器和I/O接口达到饱和的时候,增加处理器并不能获得更高的性能,与之相对应的有AMP架构,不同核之间有主从关系,如一个核控制另外一个核的业务,可以理解为多核系统中控制平面和数据平面。 + +##1.3 NUMA模型 +------- + + +NUMA模式是一种分布式存储器访问方式,处理器可以同时访问不同的存储器地址,大幅度提高并行性。 NUMA总是多处理器计算机,系统的哪个CPU都有本地内存, 可支持快速的访问, 各个处理器之前通过总线链接起来, 以支持堆其他CPU的本地内存的访问, 当然访问要比本地内存慢. + + +NUMA模式下,处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。 + +>其访问时间随存储字的位置不同而变化。其共享存储器物理上是分布在所有处理机的本地存储器上。所有本地存储器的集合组成了全局地址空间,可被所有的处理机访问。处理机访问本地存储器是比较快的,但访问属于另一台处理机的远程存储器则比较慢,因为通过互连网络会产生附加时延。 + +NUMA 的主要优点是伸缩性。NUMA 体系结构在设计上已超越了 SMP 体系结构在伸缩性上的限制。通过 SMP,所有的内存访问都传递到相同的共享内存总线。这种方式非常适用于 CPU 数量相对较少的情况,但不适用于具有几十个甚至几百个 CPU 的情况,因为这些 CPU 会相互竞争对共享内存总线的访问。NUMA 通过限制任何一条内存总线上的 CPU 数量并依靠高速互连来连接各个节点,从而缓解了这些瓶颈状况。 + + + +#2 (N)UMA模型中linux内存的机构 +------- + + +Linux适用于各种不同的体系结构, 而不同体系结构在内存管理方面的差别很大. 因此linux内核需要用一种体系结构无关的方式来表示内存. + +Linux内核通过插入一些兼容层, 使得不同体系结构的差异很好的被隐藏起来, 内核对一致和非一致内存访问使用相同的数据结构 + + +#2.1 (N)UMA模型中linux内存的机构 +------- + + + +非一致存储器访问(NUMA)模式下 + +* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 + + +* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. 比如,可能把内存的一个簇指派给每个处理器,或则某个簇和设备卡很近,很适合DMA,那么就指派给该设备。因此当前的多数系统会把内存系统分割成2块区域,一块是专门给CPU去访问,一块是给外围设备板卡的DMA去访问 + +>在UMA系统中, 内存就相当于一个只使用一个NUMA节点来管理整个系统的内存. 而内存管理的其他地方则认为他们就是在处理一个(伪)NUMA系统. + + + +#2.2 Linux物理内存的组织形式 +------- + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + +为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + +* 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +在一个单独的节点内,任一给定CPU访问页面所需的时间都是相同的。然而,对不同的CPU,这个时间可能就不同。对每个CPU而言,内核都试图把耗时节点的访问次数减到最少这就要小心地选择CPU最常引用的内核数据结构的存放位置. + +对于UMA体系的,系统中只有一个node + +在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。 + + +* 对于NUMA系统来讲, 整个系统的内存由一个[node_data](http://lxr.free-electrons.com/source/arch/s390/numa/numa.c?v=4.7#L23)的pg_data_t指针数组来管理。(因为可能有多个node),系统中的每个节点链接到一个以NULL结尾的[pgdat_list](http://lxr.free-electrons.com/source/arch/ia64/include/asm/numa.h#L27)链表中,而其中的每个节点利用pd_data_tnode_next字段链接到下一个节点。 + + +* 对于PC这样的UMA系统,使用struct pglist_datacontig_page_data ,作为系统唯一的node管理所有的内存区域。(UMA系统中中只有一个node) + + + + +##内存节点node +------- + +>CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 +> +>系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + +* 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + + +内存中的每个节点都是由pg_data_t描述,而pg_data_t由struct pglist_data定义而来, 该数据结构定义在[include/linux/mmzone.h, line 615](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L615) + + +在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到 + + +在内存中,每个簇所对应的node又被分成的称为管理区(zone)的块,它们各自描述在内存中的范围。一个管理区(zone)由[struct zone](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L326)结构体来描述,在linux-2.4.37之前的内核中是用[`typedef struct zone_struct zone_t `](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L47)数据结构来描述) + +管理区的类型有如下几种 +* ZONE_DMA +* ZONE_NORMAL +* ZONE_HIGHMEM这三种类型 + +不同的管理区的用途是不一样的,ZONE_DMA类型的内存区域在物理内存的低端,主要是ISA设备只能用低端的地址做DMA操作。ZONE_NORMAL类型的内存区域直接被内核映射到线性地址空间上面的区域(line address space),ZONE_HIGHMEM将保留给系统使用,是系统中预留的可用内存空间,不能被内核直接映射。 + +对于x86机器,管理区(内存区域)类型如下分布 + +| 类型 | 区域 | +| :------- | ----: | +| ZONE_DMA | 0~16MB | +| ZONE_NORMAL | 16MB~896MB | +| ZONE_HIGHMEM | 896MB~物理内存结束 | + +##内存页page +------- + +大多数内核(kernel)的操作只使用ZONE_NORMAL区域,系统内存由很多固定大小的内存块组成的,这样的内存块称作为“页”(PAGE), + +x86体系结构中,page的大小为4096个字节。 + +每个物理的页由一个`struct page`的数据结构对象来描述。页的数据结构对象都保存在`mem_map`全局数组中,该数组通常被存放在ZONE_NORMAL的首部,或者就在小内存系统中为装入内核映像而预留的区域之后。从载入内核的低地址内存区域的后面内存区域,也就是ZONE_NORMAL开始的地方的内存的页的数据结构对象,都保存在这个全局数组中。 + + + +##高端内存 +------- + +由于能够被Linux内核直接访问的ZONE_NORMAL区域的内存空间也是有限的,所以LINUX提出了高端内存(High memory)的概念,并且允许对高端内存的访问