-
积极支持一下活动!好活动!
-
个人信息无误,确认可以完成阅读分享计划。
-
这不是发阅读心得吗?怎么在拉取SDK包,调试代码了啊
-
如何查看进程的调度信息?
有时候我们需要查看进程相关的调度信息,如进程的nice信息、优先级、调度策略、vruntime以及量化计算能力等信息。在Linux的proc目录中,为每个进程提供一个独立的目录,该目录包含了与进程相关的信息。下图是进程ID为6839的proc目录。
在进程proc目录里,看到和进程调度相关的节点为sched,可使用cat命令来读取这个节点的信息。
上图中可获取到如下信息:
进程名称为bash,ID是6839,线程有1个;进程的优先级为120;当前进程的虚拟时间(vruntime)是82.644603ms;总运行时间是9658850ms;进程发生过0次迁移,82次进程上下文切换,其中主动调度有74次,被抢占调度有8次;进程的权重se.load.weight和se.runnable_weight相等,都是1048576。注意这两个值均为原本的权重值乘以1024,进程优先级为120,nice值为0,它原本的权重值为1024;当前时刻,进程se.avg.load_sum值和se.avg.runnable_load_sum相等,都是260;进程的se.avg.load_avg和se.avg.runnable_load_avg是相等的,都是5。对于该进程来说,它的量化负载的最大值就等于它的权重值,即1024,这是在100%占用CPU的情况下得到的。进程的量化计算能力(se.avg.util_avg)为5。
Linux内核还提供一个与调度信息相关的数据结构sched_statistics,其中包含了非常多和调度相关的统计信息。要查看这些统计信息,需要打开CONFIG_SCHEDSTATS配置选项,另外还需要打开sched_schedstats节点。
重新查看ID为6839的进程调度信息,我们会发现里面多了很多统计信息,见下图所示:
如何查看CFS的调度信息?
Linux内核在调度方面实现了一些调试接口,为开发者提供窥探调度器内部信息的接口。在proc目录下面有一个sched_debug节点,其信息如下图:
Sched调试信息的版本号:v0.11;Linux内核版本号:4.15.0-45-generic;内核时间(ktime)的值;sched_clock和cpu_clk的值;当前jiffies值;调度相关的sysctl_sched的值:调度周期sysctl_sched_latency为6ms;调度最小粒度为0.75ms;唤醒的最小粒度为1ms;fork调用完成之后禁止子进程先运行。
输出的所有进程的相关信息如下:
如何查看调度域的拓扑关系?
在理解SMP负载均衡机制的过程中,CPU的拓扑关系是一个难点。Linux内核新增一个调节点来帮助开发者理解(详见kernel/sched/topology.c文件)。在编译内核时,不仅需要打开CONFIG_SCHED_DEBUG选项,还需要在内核启动参数里传递sched_debug参数。在内核启动之后,可以通过dmesg命令来得到CPU拓扑关系。由于虚拟机设置了单核,因此拓扑结构此时显示单核。
假设在一个双核处理器的系统中,在Shell界面下运行test程序,CPU0的就绪队列中有4个进程,而CPU1的就绪队列中有1个进程。test程序和这5个进程的nice值都为0。
——请画出test程序在内核空间的运行流程。
——若干时间之后,CPU0和CPU1的就绪队列如何变化?
站在用户空间的角度,在Shell界面运行test程序,Shell程序会调用fork()系统调用函数来创建一个新进程,然后调用exec()系统调用函数来装载test程序,因此新进程便开始运行test程序。站在用户空间的角度看问题,我们只能看到test程序被运行了,但是我们看不到新进程是如何创建的、它会添加到哪个CPU里、它是如何运行的,以及CPU0和CPU1之间如何做负载均衡等。
运行test程序的流程如上图所示,其中的操作步骤如下:
调用系统调用fork()来创建一个新进程。
使用_do_fork()创建新进程
创建新进程的task_struct数据结构。
复制父进程的task_struct数据结构到新进程。
复制父进程相关的页表项到新进程。
设置新进程的内核栈。
父进程调用wake_up_new_task()尝试去唤醒新进程
调用调度类的select_task_rq(),为新进程寻找一个负载最轻的CPU,这里选择CPU1。
调用调度类的enqueue_task()把新进程添加到CPU1的就绪队列里。
CPU1重新选择一个合适的进程来运行。
每次时钟节拍到来时,scheduler_tick()会调用调度类的task_tick()检查是否需要重新调度。Check_preempat_tick()会做检查,当需要重新调度时会设置当前进程的thread_info中的TIF_NEED_RESCHED标志位。假设这时CPU1准备调度新进程,就会设置当前进程的thread_info中TIF_NEED_RESCHED标志位。
在中断返回前会检查当前进程是否需要调度。如果需要调度,调用preempt_schedule_irq()来切换进程运行。
调度器的schedule()函数会调用调度类的pick_next_task()来选择下一个最合适运行的进程。在该场景中,选择新进程。
switch_mm()切换父进程和新进程的页表。
在CPU1上,switch_to()切换新进程来运行。
运行新进程。
新进程第一次运行时会调用ret_from_fork()函数。
返回用户空间运行Shell程序。
Shell程序调用exec()来运行test程序,最终新进程变成了test进程。
实现负载均衡
在每个时钟节拍到来时,检查是否需要触发软中断来实现SMP负载均衡,即调用scheduler_tick()->trigger_load_balance()。下一次实现负载均衡的时间点存放在就绪队列的next_balance成员里。
触发SCHED_SOFTIRQ软中断。
在软中断处理函数run_rebalance_domains()里,从当前CPU开始遍历CPU拓扑关系,从调度域的低层往高层遍历调度域,并寻找有负载不均匀的调度组。本例子中的CPU拓扑关系很简单,只有一层MC层级的调度域。
CPU0对应的调度域是domain_mc_0,对应的调度组是group_mc_0;CPU1对应的调度域是domain_mc_1,对应的调度组是group_mc_1。CPU0的调度域domain_mc_0管辖CPU0和CPU1,其中group_mc_0和group_mc_1这两个调度组会被链接到domian_mc_0的一个链表中。同理,CPU1的调度域domain_mc_1管理着group_mc_1和group_mc_0这两个调度组。
假设当前运行的CPU是CPU1,也就是说,运行run_rebalance_domains()函数的CPU为CPU1,那么在当前MC的调度域(domain_mc_1)里找哪个调度组是最繁忙的。很容易发现CPU0的调度组(group_mc_0)是最繁忙的,然后计算需要迁移多少负载到CPU1上才能保持两个调度组负载平衡。
从CPU0迁移部分进程到CPU1。
-
个人信息无误,确认可以完成评测计划
-
请简述进程优先级、nice值和权重之间的关系。
操作系统中经典的进程调度算法是基于优先级调度的。优先级调度的核心思想是把进程按照优先级进行分类,紧急的进程优先级高,不紧急、不重要的进程优先级低。调度器总是从就绪队列中选择优先级高的进程进行调度,而且优先级高的进程分配的时间片会比优先级低的进程长,这体现一种等级制度。Linux系统最早采用nice值来调整进程的优先级。nice值的思想是要对其他进程友好,降低优先级来支持其他进程消耗更多的处理器时间。它的范围是-20 ~ +19,默认值是0。nice值越大,优先级反而越低;nice值越低,优先级越高。nice值-20表示这个进程是非常重要的,优先级最高;而nice值19则表示允许其他进程比这个线程优先享有宝贵的CPU时间,这也是nice值的由来。内核使用0~139的数值表示进程的优先级,数值越小,优先级越高。优先级0~99给实时进程使用,100~139给普通进程使用。另外,在用户空间中有一个传统的变量nice,它用于映射普通进程的优先级,即100~139。
请简述CFS是如何工作的。
CFS调度器抛弃以前固定时间片和固定调度周期的算法,采用进程权重值的比重来量化和计算实际运行时间。引入虚拟时钟的概念,每个进程的虚拟时间是实际运行时间相对nice值为0的权重的比例值。进程按照各自不同的速率比在物理时钟节拍内前进。nice值小的进程,优先级高且权重大,其虚拟时钟比真实时钟跑得慢,但是可以获得比较多的运行时间;反之,nice值大的进程,优先级低,权重也低,其虚拟时钟比真实时钟跑得快,获得比较少的运行时间。CFS调度器总是选择虚拟时钟跑得慢的进程,类似一个多级变速箱,nice值为0的进程是基准齿轮,其他各个进程在不同变速比下相互追赶,从而达到公正公平。
CFS中vruntime是如何计算的?
在CFS中有一个计算虚拟时间的核心函数calc_delta_fair(),它的计算公式vruntime=(delta_exec*nice_0_weight)/weight。其中,delta_exec为实际运行时间,nice_0_weight为nice为0的权重值,weight表示该进程的权重值。在update_curr()函数中,完成了该值的计算,此时,为了计算高效,将计算方式变成了乘法和移位vruntime= (delta_exec*nice_0_weight*inv_weight)>>shift,其中inv_weight=2^32/weight,是预先计算好存放在prio_to_wmult中的。
-
个人信息无误,已知晓需自己支付邮费,使用E金币支付,谢谢!
-
个人信息确认无误,感谢论坛!
-
本帖最后由 yin_wu_qing 于 2024-1-11 22:44 编辑
进程是什么?
顾名思义,进程是执行中的程序,即一个程序加载到内存后变成了进程,公式表达如下:进程 = 程序 + 执行
进程是一段执行中的程序,是一个有“生命力”的个体。一个进程除了包含可执行的代码(如代码段),还包含进程的一些活动信息和数据,如用来存放函数形参、局部变量以及返回值的用户栈,用于存放进程相关数据的数据段,用于切换内核中进程的内核栈,以及用于动态分配内存的堆等。进程是用于实现多进程并发执行的一个实体,实现对CPU的虚拟化,让每个进程都认为自己独立拥有一个CPU。实现这个CPU虚拟化的核心技术是上下文切换以及进程调度。
操作系统如何描述和抽象一个进程?
进程是操作系统中调度的一个实体,需要对进程所拥有的资源进行抽象,这个抽象形式称为进程控制块(Process Control Block,PCB),本书也称其为进程描述符。进程描述符是用于描述进程运行状况以及控制进程运行所需要的全部信息,是操作系统用来感知进程存在的一个非常重要的数据结构。任何一个操作系统的实现都需要有一个数据结构来描述进程描述符,所以Linux内核采用一个名为task_struct的结构体。task_struct数据结构包含的内容很多,它包含进程所有相关的属性和信息。
进程是否有生命周期?
在进程的生命周期内,进程要和内核的很多模块进行交互,如内存管理模块、进程调度模块以及文件系统模块等。因此,它还包含了内存管理,进程调度、文件管理等方面的信息和状态。Linux内核把所有进程的进程描述符task_struct数据结构链接成一个单链表(task_struct->tasks),task_struct数据结构定义在include/linux/sched.h文件中。
如何标识一个进程?
在创建时会分配唯一的号码来标识进程,这个号码就是进程标识符(Process Identifier,PID)。PID存放在进程描述符的pid字段中,PID是整数类型。为了循环使用PID,内核使用bitmap机制来管理当前已经分配的PID和空闲的PID,bitmap机制可以保证每个进程创建时都能分配到唯一的PID。
除了PID之外,Linux内核还引入了线程组的概念。一个线程组中所有的线程使用和该线程组中主线程相同的PID,即该组中第一个进程的ID,它会被存入task_struct数据结构的tgid成员中。这与POSIX 1003.1c标准里的规定有关系,一个多线程应用程序中所有的线程必须有相同的PID,这样可以把指定信号发送给组里所有的线程。如一个进程创建之后,只有这个进程,它的PID和线程组ID(Thread Group ID,TGID)是一样的。这个进程创建了一个新的线程之后,新线程有属于自己的PID,但是它的TGID还是指父进程的TGID,因为它和父进程同属一个线程组。
进程与进程之间的关系如何?
进程间的关系
成 员
描 述
real_parent
指向创建了进程A的描述符,如果进程A的父进程不存在了,则指向进程1(init进程)的描述符
parent
指向进程的当前父进程,通常和real_parent一致
children
所有的子进程都链接成一个链表,这是链表头
sibling
所有兄弟进程都链接成一个链表,链表头在父进程的sibling成员中
Linux操作系统的第0个进程是什么?
系统中所有进程的task_struct数据结构都通过list_head类型的双向链表链接在一起,因此每个进程的task_struct数据结构包含一个list_head类型的tasks成员。这个进程链表的头是init_task进程,也就是所谓的进程0。init_task进程的tasks.prev字段指向链表中最后插入进程的task_struct数据结构的tasks成员。另外,若这个进程下面的有线程组(即PID==TGID),那么线程会添加到线程组的thread_group链表中。
Linux操作系统的第1个进程是什么?
Linux内核初始化函数start_kernel()在初始化完内核所需要的所有数据结构之后会创建另一个内核线程,这个内核线程就是进程1或init进程。进程1的ID为1,与进程0共享进程所有的数据结构。
进程1会执行kernel_int()函数,它会调用execve()系统调用来装入可执行程序init,最后进程1变成了一个普通进程。这些init程序就是常见的/sbin/init、/bin/init或者/bin/sh等可执行的init以及systemd程序。进程1从内核线程变成普通进程init之后,它的主要作用是根据/etc/inittab文件的内容启动所需要的任务,包括初始化系统配置、启动一个登录对话等。
请简述fork()、vfork()和clone()之间的区别。
在Linux内核中,fork()、vfork()、clone()以及创建内核线程的接口函数都是通过调用_do_fork()函数来完成的,只是调用的参数不一样。
//fork()实现
_do_fork(SIGCHLD, 0,0,NULL,NULL,0);
//vfork()实现
_do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0 , 0, NULL, NULL, 0);
//clone()实现
_do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
//内核线程
_do_fork(flags|CLONE_VM |CLONE_UNTRACED,(unsigned long)fn,(unsigned long)arg,NULL,NULL,0);
fork()函数通过系统调用进入Linux内涵,然后通过_do_fork()函数实现。
fork函数只使用SIGCHLD标志位,在子进程终止后发送SIGCHLD信号通知父进程。fork()是重量级调用,为子进程建立了一个基于父进程的完整副本,然后子进程基于此执行。为了减少工作量,子进程采用写时复制技术,只复制父进程的页表,不会复制页面内容。当子进程需要写入新内容时才触发写时复制机制,并为子进程创建一个副本。
fork()函数也有一些缺点,尽管使用了写时复制机制技术,但是它还需要复制父进程的页表,在某些场景下会比较慢,所以有了后来的vfork()原语和clone()原语。
vfork()函数和fork()函数类似,但是vfork()的父进程会一直阻塞,直到子进程调用exit()或者execve()为止。在fork()实现写时复制之前,UNIX系统的设计着很关心fork()之后马上执行execve()所造成的地址空间浪费和效率低下问题,因此设计了vfork()系统调用。
clone()函数通常用于创建用户线程。在Linux内核中没有专门的线程,而是把线程当成普通进程来看待,在内核中还以task_struct数据结构来描述线程,并没有使用特殊的数据结构或者调度算法来描述线程。
clone()函数功能强大,可以传递众多参数,可以有选择地继承父进程的资源,如可以和vfork()一样,与父进程共享一个进程地址空间,从而创建线程;也可以不和父进程共享进程地址空间,甚至可以创建兄弟关系进程。
-
我申请37号:四色板组合式开发平台 Kinetis MCU 套件
申请理由:该开发套件包含了MCU主板,通用外设板、专业应用板、桥接扩展板,虽然飞思卡尔K64 MCU已停止供应,但其高稳定性能、低功耗特性至今难以忘怀,笔者之前有使用K64+JN5169开发Zigbee网关,跑freeRTOS平台,希望在原来的功能基础上,既能实现彩屏显示功能,又能体现其最优功耗,因此想申请这款Kinetis MCU套件来评估其可行性。
-
Linux内核的内存管理模块都对哪些页面进行了统计?
内存管理模块定义了3个全局的vm_stat计数值,其中vm_zone_stat是内存管理区相关的计数值,vm_numa_stat是与NUMA相关的计数值,vm_node_stat是与内存节点相关的计数值。
vm_zone_stat计数值的统计项
统计项
描述
NR_FREE_PAGES
空闲页面数量
NR_ZONE_LRU_BASE
用于LRU_BASE的统计。LRU链表是从LRU_BASE开始标记的
NR_ZONE_INACTIVE_ANON
不活跃匿名页面数量
NR_ZONE_ACTIVE_ANON
活跃匿名页面数量
NR_ZONE_INACTIVE_FILE
不活跃文件映射页面数量
NR_ZONE_ACTIVE_FILE
活跃文件映射页面数量
NR_ZONE_UNEVICTABLE
不可回收的页面数量
NR_ZONE_WRITE_PENDING
脏页、正在回写以及不稳定的页面数量
NR_MLOCK
使用mlock()锁住的页面数量
NR_PAGETABLE
用于页表的页面数量
NR_KERNEL_STACK_KB
用于内核栈的页面数量
NR_BOUNCE
跳跃页面的数量
NR_ZSPAGES
用于zsmalloc机制的页面数量
NR_FREE_CMA_PAGES
CMA中的空闲页面数量
NR_VM_ZONE_STAT_ITEMS
ZONE中vm_stat计数值的项数
请解释/proc/meminfo节点中每一项的含义。
meminfo节点实现在meminfo_proc_show()函数中,该函数实现在fs/proc/meminfo.c。
meminfo节点显示的内容
统计项
描述(实现)
MemTotal
系统当前可用物理内存总量,通过读取全局变量_totalram_pages来获得
MemFree
系统当前剩余空闲物理内存,通过读取全局变量vm_zone_stat[]数组中的NR_FREE_PAGES来获得
MemAvailable
系统中可使用页面的数量,有si_mem_available()函数来计算。公式为Available=memfree+pagecache+reclaimable-totalreserve_pages。这里包括了空闲页面(memfree)、文件映射页面(pagecache)、可回收的页面(reclaimable),最后减去系统保留的页面
Buffers
用于块层的缓存,有nr_blockdev_pages()函数来计算
Cached
用于页面高速缓存的页面。计算公式为Cached=NR_FILE_PAGES-swap_cache-Buffers
SwapCached
这里统计交换缓存的数量,交换缓存类似与内容缓存,只不过它对应的是交换分区,而内容缓存对应的是文件。这里表示匿名页面曾经被交换出去,现在又被交换回来,但是页面内容还在交换缓存中。
Active
活跃的匿名页面(LRU_ACTIVE_ANON)和活跃的文件映射页面(LRU_ACTIVE_FILE)
Inactive
不活跃的匿名页面(LRU_INACTIVE_ANON)和不活跃的文件映射页面(LRU_INACTIVE_FILE)
Active(anon)
活跃的匿名页面(LRU_ACTIVE_ANON)
Inactive(anon)
不活跃的匿名页面(LRU_INACTIVE_ANON)
Active(file)
活跃的文件映射页面(LRU_ACTIVE_FILE)
Inactive(file)
不活跃的文件映射页面(LRU_INACTIVE_FILE)
Unevictable
不能回收的页面(LRU_UNEVICTABLE)
Mlocked
不会被交换到交换分区的页面,由全局的vm_zone_stat[]中的NR_MLOCK来统计
SwapTotal
交换分区的大小
SwapFree
交换分区的空闲空间大小
Dirty
脏页的数量,由全局的vm_node_stat[]中的NR_FILE_DIRTY来统计
Writeback
正在回写的页面数量,由全局的vm_node_stat[]中的NR_WRITEBACK来统计
AnonPages
统计有反向映射(RMAP)的页面,通常这些页面都是匿名页面并且都映射到了用户空间,但是并不是所有匿名页面都配置了反向映射,如部分的shmen和tmpfs页面就没有设置反向映射。这个计数由全局的vm_node_stat[]中的NR_ANON_MAPPED来统计
Mapped
统计所有映射到用户地址空间的内容缓存页面,由全局的vm_node_stat[]中的NR_FILE_MAPPED来统计
Shmem
共享内存(基于tmpfs实现的shmem、devtmfs等)页面的数量,由全局的vm_node_stat[]中的NR_SHMEM来统计
KReclaimable
内核可回收的内存,包括可回收的slab页面(NR_SLAB_RECLAIMABLE)和其他的可回收的内核页面(NR_KERNEL_MISC_RECLAIMABLE)
Slab
所有slab页面,包括可回收的slab页面(NR_SLAB_RECLAIMABLE)和不可回收的slab页面(NR_SLAB_UNRECLAIMABLE)
SReclaimable
可回收的slab页面(NR_SLAB_RECLAIMABLE)
SUnreclaim
不可回收的slab页面(NR_SLAB_UNRECLAIMABLE)
KernelStack
所有进程内核栈的总大小,由全局的vm_zone_stat[]中的NR_KERNEL_STACK_KB来统计
PageTables
所有用于页表的页面数量,由全局的vm_zone_stat[]中的NR_PAGETABLE来统计
NFS_Unstable
在NFS中,发送到服务器端但是还没有写入磁盘的页面(NR_UNSTABLE_NFS)
WritebackTmp
回写过程中使用的临时缓存(NR_WRITEBACK_TEMP)
VmallocTotal
vmalloc区域的总大小
VmallocUsed
已经使用的vmalloc区域总大小
Percpu
percpu机制使用的页面,由pcpu_nr_pages()函数来统计
AnonHugePages
统计透明巨页的数量
ShmemHugePages
统计在shmem或者tmpfs中使用的透明巨页的数量
ShmemPmdMapped
使用透明巨页并且映射到用户空间的shmem或者tmpfs的页面数量
CmaTotal
CMA机制使用的内存
CmaFree
CMA机制中空闲的内存
HugePages_Total
普通巨页的数量,普通巨页的页面是预分配的
HugePages_Free
空闲的普通巨页的数量
Hugepagesize
普通巨页的大小,通常是2MB或者1GB
Hugetlb
普通巨页的总大小,单位是KB
为什么/proc/meminfo节点中的MemTotal不等于QEMU虚拟机中分配的内存大小?
读者可能会发现MemTotal显示的总内存大小并不等于物理系统中真实的内存大小或者QEMU虚拟中分配的内存大小,如在QEMU虚拟机启动参数中指定内存大小为1GB,但是进入QEMU虚拟机后发现MemTotal为“999784KB”。这是因为内核静态使用的内存(如内核代码等)在启动阶段需要用到,它没有计入MemTotal统计项中,而是统计到reserved中。从计算机内核启动日志信息来看,得出在内核初始化完成之后,init段的内存会被释放,因此被保留的内存大小为53400KB – 4608KB = 48792KB,加上MemTotal正好是1GB内存。
-
本帖最后由 yin_wu_qing 于 2024-1-7 20:21 编辑
1. page数据结构中的_refcount和_mapcount有什么区别?
_refcount和_mapcount是page数据结构中非常重要的两个引用计数,且都是atomic_t类型的变量。
A _refcount表示内核中引用该页面的次数。
当_refcount的值为0时,表示该页面为空闲页面或即将要被释放的页面。
当_refcount的值大于0时,表示该页面已经被分配且内核正在使用,暂时不会被释放。内核中提供加/减_refcount的接口函数,读者应该使用这些接口函数来使用_refcount引用计数。
get_page():_refcount加1;put_page():_refcount减1。若_refcount减1后等于0,那么会释放该页面。
get_page()函数调用page_ref_inc()来增加引用计数,最后使用atomic_inc()函数原子地增加引用计数。
put_page()首先使用put_gage_testzero()函数来使_refcount减1并且判断其是否为0。如果_refcount减1之后等于0,就会调用_put_page()来释放这个页面。
B _mapcount表示这个页面被进程映射的个数,即已经映射了多少个用户PTE。每个用户进程都拥有各自独立的虚拟空间(256TB)和一份独立的页表,所以可能出现多个用户进程地址空间同时映射到一个物理页面的情况,RMAP系统就是利用这个特征来实现的。_mapcount主要用于RMAP系统中。
若_mapcount等于-1,表示没有PTE映射到页面。
若_mapcount等于0,表示只有父进程映射到页面。匿名页面刚分配时,_mapcount初始化为0。例如,当do_anonymous_page()产生的匿名页面通过page_add_new_anon_map()添加到rmap系统中时,会设置_mapcount为0,这表明匿名页面当前只有父进程的PTE映射到页面。
若_mapcount大于0,表示除了父进程外还有其他进程映射到这个页面。同样以创建子进程时共享父进程地址空间为例,设置父进程的PTE内容到子进程中并增加该页面的_mapcount。
2.匿名页面和高速缓存页面有什么区别?
匿名页面的产生:从内核的角度来看,在如下情况会产生匿名页面。
用户空间通过malloc()/mmap()接口函数来分配内存,在内核空间中发生缺页中断时,do_anonymous_page()会产生匿名页面。
发生写时复制。当缺页中断出现写保护错误时,新分配的页面是匿名页面,下面又分两种情况。
a 调用do_wp_page().
分配只读的特殊映射的页面,如映射到零页面的页面。
分配非单身匿名页面(有多个映射的匿名页面,即page->_mapcount>0)。
分配只读的私有映射的内容缓存页面。
分配KSM页面。
b 调用do_cow_page()共享的匿名映射(Shared Anonymous Mapping,SHMM)页面。
上述这些情况在发生写时复制时会新分配匿名页面。
do_swap_page(),从交换分区读回数据时会分配匿名页面。
迁移页面。以do_anonymous_page()分配一个匿名页面为例,匿名页面刚分配时的状态如下。
page->_refcount = 1。
page->_mapcount = 0。
设置PG_swapbacked标志位。
加入LRU_ACTIVE_ANON链表中,并设置PG_lru标志位。
page->mapping指向VMA中的anon_vma数据结构。
匿名页面在缺页中断中分配完成之后,就建立了进程虚拟地址空间和物理页面的映射关系,用户进程访问虚拟地址即访问匿名页面的内容。
高速缓存页面
假设现在系统内存紧张,需要回收一些页面来释放内存,匿名页面刚分配时会加入活跃LRU链表(LRU_ACTIVE_ANON)的头部,在活跃LRU链表移动一段时间后,该匿名页面到达活跃LRU链表的尾部,shrink_active_list()函数把该页面加入不活跃LRU链表(LRU_INACTIVE_ANON)。Linux内核为页面迁移提供了一个系统调用migrate_pages,它可以迁移一个进程的所有页面到指定内存节点上。该系统调用最早是为了在UMA系统中提供一种迁移进程到任意内存节点的能力。现在内核除了为NUMA系统提供页面迁移能力外,其他的一些模块也可以利用页面迁移功能做一些事情。如内存规整和内存热插拔等。页面迁移的设计初衷是在UMA系统中提高内存访问性能,把一些页面从一个内存节点迁移到另外一个内存节点。它还有一个应用场景(内存规整)。这些迁移的页面都是LRU链表上的页面。但是,最近几年Linux内核引入了一些新的特性,如zsmalloc和virtio-balloon页面。以virtio-balloon页面为例,它也有页面迁移的需求,之前的做法是在virtio-balloon驱动中进行迁移操作和相应的逻辑。如果其他的驱动也想做类似的页面迁移,那么它们就不能复用与virtio-balloon驱动相关的代码,必须重新写一套代码,这样会造成很多代码的重复与冗余。为了解决这个问题,内存管理的页面迁移机制提供相应的接口来支持这些非LRU页面的迁移。因此,页面迁移机制支持两大类内存页面。
传统LRU页面,如匿名页面和文件映射页面。
非LRU页面,如zsmalloc或者virtio-balloon页面。
3. page数据结构中有一个锁,我们称为页锁,请问trylock_page()和lock_page()有什么区别?
Page数据结构中的成员flags定义了一个标志PG_locked,内核通常利用PG_locked来设置一个页锁。lock_page()函数用于申请页锁,如果页锁被其他进程占用了,那么它会睡眠等待。从lock_page()函数的声明和实现代码来看,lock_page()函数首先会调用trylock_page()函数,然后调用__lock_page()函数。trylock_page()和lock_page()这两个函数看起来很相似,但有很大的区别。trylock_page()定义在include/linux/pagemap.h文件中,它使用test_and_set_bit_lock()尝试为page的flags设置PG_locked标志位,并且返回原来标志位的值。如果page的PG_locked位已经置位了,那么当前进程调用trylock_page()时返回false,说明有其他进程已经锁住了page。因此,若trylock_page()返回false,表示获取锁失败;若返回true,表示获取锁成功。
trylock_page()尝试给页面加锁。若trylock_page()返回false,表示别的进程已持有了这个页面的锁;否则,表示当前进程已经成功获取锁。
如果尝试获取页面不成功,当前不是强制迁移(force=0)或迁移模式为MIGRATE_ASYNC,则会直接忽略这个页面,因为这种情况下没有必要睡眠等待页面释放锁。
如果当前进程设置了PF_MEMALLOC标志位,表示当前进程可能处于直接内存压缩的内核路径上,通过睡眠等待页锁是不安全的,所以直接忽略该页面。例如,在文件预读中,预读的所有页面都会加锁并被添加到LRU链表中,等到预读完成后,这些页面会标记PG_uptodate并释放锁,这个过程中块设备层会把多个页面合并到一个BIO设备中。如果在分配第2个或者第3个页面时发生内存短缺,内核会运行到直接内存压缩的内核路径上,导致一个页面加锁之后又等待这个锁,产生死锁,因此直接内存压缩的内核路径会标记PF_MEMALLOC。PF_MEMALLOC标志位一般在直接内存压缩、直接内存回收以及kswapd中设置,这些场景下可能会有少量的内存分配行为,因此若设置PF_MEMALLOC标志位,表示允许它们使用系统预留的内存,即不用考虑zone水位问题,可以参见__perform_reclaim()、__alloc_pages_direct_compact()和kswapd()等函数。除了上述情况外,其余情况下只能调用lock_page()函数来等待页锁被释放。
4. 请画出page数据结构中flags成员的布局示意图。
num pageflags {
PG_locked, /* Page is locked. Don't touch. */ //表示页面已经上锁;如果该比特位置位,说明已经被锁,内存管理其他模块不能访问这个页面,防止竞争
PG_error, //表示页面操作过程中发生错误时会设置该位;
PG_referenced, //同PG_active一起,用于控制页面的活跃程度,在kswapd页面回收中使用;
PG_uptodate, //表示页面的数据已经从块设备成功读取到内存页面;
PG_dirty, //表示页面内容发生改变,这个页面为脏的,即页面内容被改写,还没同步到外部存储器
PG_lru, //表示页面加入了LRU链表中,内核使用LRU链表来管理活跃和不活跃页面;
PG_active,
PG_workingset,
PG_waiters, /* Page has waiters, check its waitqueue. Must be bit #7 and in the same byte as "PG_locked" */
PG_slab, //页面用于slab分配器
PG_owner_priv_1, /* Owner use. If pagecache, fs may use*/
PG_arch_1,
PG_reserved,
PG_private, /* If pagecache, has fs-private data */
PG_private_2, /* If pagecache, has fs aux data */
PG_writeback, /* Page is under writeback */ //表示页面的内容正在向块设备进行会写
PG_compound,
PG_swapcache,
PG_mappedtodisk, /* Has blocks allocated on-disk */
PG_reclaim, /* To be reclaimed asap */ //表示这个页面马上要被回收
PG_swapbacked, /* Page is backed by RAM/swap */ //表示页面具有swap缓存功能,通过匿名页面才可以写回swap分区
PG_unevictable, /* Page is "unevictable" */ //表示这个页面不能回收
#ifdef CONFIG_MMU
PG_mlocked, /* Page is vma mlocked */ //表示页面对应的vma处于mlocked状态;
#endif
__NR_PAGEFLAGS,
};
5. 请列举page数据结构中_refcount和_mapcount计数的使用案例。
_refcount通常在内核中用于跟踪页面的使用情况,常见的用法归纳总结如下:
初始状态下,空闲页面的_refcount是0
分配页面时,_refcount会变成1。页面分配接口函数alloc_pages()在成功分配页面后,_refcount应该为0,这里使用VM_BUG_ON_PAGE()来判断,然后设置这些页面的_refcount为1。
加入LRU链表时,页面会被kswapd内核线程使用,因此_refcount会加1。以malloc为用户程序分配内存为例,发生缺页中断后,do_anonymous_page()函数成功分配出来一个页面,在设置硬件PTE之前,调用lru_cache_add()函数把这个匿名页面添加到LRU链表中。在这个过程中,使用page_cache_get()宏来增加_refcount。当页面已经添加到LRU链表后,_refcount会减1,这样做的目的是防止页面在添加到LRU链表过程中被释放。
被映射到其他用户进程的PTE时,_refcount会加1。如在创建子进程时共享父进程的地址空间,设置父进程的PTE内容到子进程中并增加该页面的_refcount,详见do_fork()->copy_process()->copy_mm()->dup_mmap()->copy_pte_range()->copy_one_pte()函数。
在copy_one_pte()函数中,通过vm_normal_page()找到父进程的PTE对应的页面,然后增加这个页面的_refcount。
页面的private成员指向私有数据。
对于PG_swapable的页面,__add_to_swap_cache()函数会增加_refcount。
对于PG_private的页面,主要在块设备的buffer_head中使用,如buffer_migrate_page()函数中会增加_refcount。
内核对页面进行操作等关键路径上也会使_refcount加1,如内核的follow_page()函数和get_user_pages()函数。以follow_page()函数为例,调用者通常需要设置FOLL_GET标志位来使其增加_refcount。如KSM中获取可合并的页面函数get_mergeable_page(),另一个例子是DIRECT_IO,详见write_protect_page()函数。
_mapcount表示这个页面被进程映射的个数,即已经映射了多少个用户PTE。每个用户进程都拥有各自独立的虚拟空间(256TB)和一份独立的页表,所以可能出现多个用户进程地址空间同时映射到一个物理页面的情况,RMAP系统就是利用这个特性来实现的。_mapcount主要用于RMAP系统中。
若_mapcount等于-1,表示没有PTE映射到页面。
若_mapcount等于0,表示只有父进程映射到页面。匿名页面刚分配时,_mapcount初始化为0。例如,当do_anonymous_page()产生的匿名页面通过page_add_new_anon_rmap()添加到rmap系统中时,会设置_mapcount为0,这表明匿名页面当前只有父进程的PTE映射到页面。
若_mapcount大于0,表示除了父进程外还有其他进程映射到这个页面。同样以创建子进程时共享父进程地址空间为例,设置父进程的PTE内容到子进程中并增加该页面的_mapcount,详见do_fork()->copy_process()->copy_mm()->dup_mmap()->copy_pte_range()->copy_one_pte()函数。
-
请简述Linux内核在理想情况下页面分配器(page allocator)是如何分配出连续物理页面的。
内核中分配物理内存页面的常用的接口函数是alloc_pages(),它用于分配一个或者多个连续的物理页面,分配的页面个数只能是2的整数次幂。想比于多次分配离散的物理页面,分配连续的物理页面有利于缓解系统内存的碎片化问题,内存碎片化是一个很让人头疼的问题。
alloc_pages()函数的参数有两个,gfp_mask表示分配掩码,order表示分配级数。分配掩码又分为zone的修饰符和action修饰符,zone修饰符确定可以优先从哪个zone开始扫描,action修饰符确定物理页面的迁移类型。根据得到的zone的序号/action/order三个值,确定了分配的路径。如果在指定的zone/迁移类型/order上分配不到连续的物理页面,则会考虑从不同的zone,不同的迁移类型和不同的order去分配物理页面。
在页面分配器中,如何从分配掩码(gfp_mask)中确定可以从哪些zone中分配内存?
内存管理区修饰符使用gfp_mask的低4位来表示。__GFP_DMA:从ZONE_DMA中分配内存,__GFP_DMA32:从ZONE_DMA32中分配内存,__GFP_HIGHMEM:优先从ZONE_HIGHMEM中分配内存,__GFP_MOVABLE:页面可以被拆迁移或者回收,如用于内存规整机制。一般优先从HIGHMEM -> MOVABLE ->DMA32->DMA这个顺序分配。
页面分配器是按照什么方向来扫描zone的?
ZONE_HIGHMEM是分配器的首选zone,而ZONE_NORMAL是备选zone,在for_each_zone_zonelist_nodemask()函数中,next_zones_zonelist(++z,highidx,nodemask)依然会返回ZONE_NORMAL。因此这里会遍历ZONE_HIGHMEM和ZONE_NORMAL这两个ZONE,但是会先遍历ZONE_HIGHMEM,后遍历ZONE_NORMAL。
为用户进程分配物理内存时,分配掩码应该选用GFP_KERNEL,还是GFP_HIGHUSER_ MOVABLE呢?
GFP_KERNEL主要用于分配内核使用的内存,在分配过程中会引起睡眠,在中断上下文和不能睡眠的内核路径里使用该类型标志需要特别警惕,因为这会引起死锁或者其它系统异常。GFP_HIGHUSER_ MOVABLE优先使用高端内存并且分配的内存具有可移动属性。因此应该选择GFP_HIGHUSER_MOVABLE,因为此宏分配的内存是可迁移的,而GFP_KERNEL,分配的是不可迁移的内存类型。
-
请简述内存架构中UMA和NUMA的区别。
UMA架构:内存有统一的结构并且可以统一寻址。目前大部分嵌入式系统、手机系统以及台式机操作系统等采用UMA架构。如下图,该系统使用UMA架构,有4个CPU,它们都有L1高速缓存,其中CPU0和CPU1组成一个簇(Cluster0),它们共享一个L2高速缓存。另外,CPU2和CPU3组成另外一个簇(Cluster1),它们共享另外一个L2高速缓存。4个CPU都共享同一个L3的高速缓存。最重要的一点,它们可以通过系统总线来访问物理内存DDR。
NUMA架构:系统中有多个内存节点和多个CPU簇,CPU访问本地内存节点的速度最快,访问远端的内存节点的速度要慢一点。如下图,该系统使用NUMA架构,有两个内存节点,其中CPU0和CPU1组成一个节点(Node0),它们可以通过系统总线访问本地DDR物理内存,同理,CPU2和CPU3组成另外一个节点(Node1),它们也可以通过系统总线访问本地的DDR物理内存。如果两个节点通过超路径互连(Ulra Path Interconnect,UPI)总线连接,那么CPU0可以通过这个内部总线访问远端的内存节点的物理内存,但是访问速度要比访问本地物理内存慢很多。
CPU访问各级存储结构的速度是否一样?
一般来说,CPU访问各级内存的速度是不一样的。如下表所示:
CPU访问各级内存设备的延时
访问类型
延 迟
L1高速缓存命中
约4个时钟周期
L2高速缓存命中
约10个时钟周期
L3高速缓存命中(高速缓存行没有共享)
约40个时钟周期
L3高速缓存命中(和其他CPU共享高速缓存行)
约65个时钟周期
L3高速缓存命中(高速缓存行被其他CPU修改过)
约75个时钟周期
访问远端的L3高速缓存
约100~300个时钟周期
访问本地DDR物理内存
约60ns
访问远端内存节点的DDR物理内存
约100ns
请绘制内存管理常用的数据结构的关系图。如mm_struct、VMA、vaddr、page、PFN、PTE、zone、paddr和pg_data等,并思考如下转换关系。
3.1 如何由mm_struct和vaddr找到对应的VMA? ①
3.2 如何由page 和VMA 找到 vaddr? ②
3.3 如何由page找到所有映射的VMA? ③
3.4 如何由VMA和vaddr找出相应的page数据结构? ③
3.5 page和PFN之间如何互换? ④
3.6 PFN和paddr之间如何互换? ⑤
3.7 page和PTE之间如何互换? ⑥
3.8 zone和page之间如何互换? ⑦
3.9 zone和pgdata之间如何互换? ⑧
-
本帖最后由 yin_wu_qing 于 2023-12-9 17:49 编辑
ARM64处理器中有两个页表基地址寄存器TTBR0和TTBR1,处理器如何使用它们?
在AArch64架构中,因为地址总线位宽最多支持48位,所以VA被划分为两个空间,每个空间最多支持256TB。
□低位的虚拟地址空间位于0x00000000_00000000到0x0000FFFF_FFFFFFFF。如果虚拟地址的最高位等于0,就使用这个虚拟地址空间,并且使用TTBR0_ELx来存放页表的基地址。
□高位的虚拟地址空间位于0xFFFF0000_00000000到0xFFFFFFFF_FFFFFFFF。如果虚拟地址的最高位等于1,就使用这个虚拟地址空间,并且使用TTBR1_ELx来存放页表的基地址。
请简述ARM64处理器的4级页表的映射过程,假设页面粒度为4KB,地址宽度为48位。
●处理器根据页表基地址控制寄存器和虚拟地址来判断使用哪个页表基地址寄存器,是TTBR0还是TTBR1。当虚拟地址第63位(简称VA[63])为1时选择TTBR1;当VA[63]为0时选择TTBR0。页表基地址寄存器中存放着1级页表(见下图中的L0页表)的基地址。
●处理器将VA[47:39]作为L0索引,在1级页表(L0页表)中找到页表项,1级页表有512个页表项。
□1级页表的页表项中存放着2级页表(L1页表)的物理基地址。处理器将VA[38:30]作为L1索引,在2级页表中找到相应的页表项,2级页表有512个页表项。
□2级页表的页表项中存放着3级页表(L2页表)的物理基地址。处理器以VA[29:21]作为L2索引,在3级页表(L2页表)中找到相应的项表项,3级页表有512个页表项。
□3级页表的页表项中存放着4级页表(L3页表)的物理基地址。处理器以VA[20:12]作为L3索引,在4级页表(L3页表)中找到相应的项表项,4级页表有512个页表项。
□4级页表的页表项里存放着4KB页面的物理基地址让,然后加上 VA[11:0],就构成了新的物理地址,因此处理器就完成了页表的查询和翻译工作。
在L0~L2页表项描述符中,如何判断一个页表项是块类型还是页表类型?
L0~L2有三种页表项类型:无效类型,块类型,页表类型。
bit[0]=0为无效类型,bit[0]=1为块类型或者页表类型。
bit[1]=0为块类型;
bit[1]=1为页表类型。
在ARM64 Linux内核中,用户空间和内核空间是如何划分的?
Linux内核在大多数架构上把两个地址空间划分为用户空间和内核空间。
□用户空间:0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF
□内核空间: 0xFFFF 0000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF
-
请简述精简指令集RISC和复杂指令集CISC的区别。
RISC与CISC都是时代的产物,RISC在思想上更先进。原来复杂指令集中有20%的简单指令会被编译器生成的代码用到,占程序总指令数的80%,其余80%的复杂指令很少被用到。精简指令集RISC的诞生保留了常用的简单指令,不需要浪费太多的晶体管去完成那些很复杂又很少使用的复杂指令。
请简述数值0x1234 5678在大小端字节序处理器的存储器中的存储方式。
大端模式指数据的高字节保存到内存的低地址中,而数据的低字节保存在内存的高地址中,比如:
大端模式下地址的增长顺序与值的增长顺序相同。
小端模式指数据的高字节保存到内存的高地址中,而数据的低字节保存到内存的低地址中,比如:
小端模式下地址的增长顺序与值的增长顺序相反。
请简述在你所熟悉的处理器(如双核Cortex-A9)中一条存储读写指令的执行全过程。
经典处理器架构的流水线是5级流水线,分别是取指、译码、执行、数据内存访问和写回。在Cortex-A9处理器中,存储指令首先通过主存储器或者L2高速缓存加载到L1指令高速缓存中,通过总线接口单元(BIU)中的主接口连接到主存储器。在指令预取阶段,主要做指令预取和分支预测,然后指令通过指令队列和预测队列送到译码器,进行指令的译码工作。译码器支持两路译码,可以同时译码两条指令。在寄存器重命名阶段会做寄存器重命名,避免指令进行不必要的顺序化操作,提高处理器的指令级并行能力。在指令分发阶段,这里支持4路猜测发射和乱序执行,因此它支持基于推测的乱序的发射功能。然后在执行单位中乱序执行指令,最终的计算结果会在乱序写回阶段写入寄存器中。存储指令会计算有效地址并将其发送到内存系统中的加载存储单元,最终LSU会访问L1数据高速缓存。在RAM中,只有可缓存的内存地址才需要访问高速缓存。
在多处理器环境下,还需要考虑高速缓存的一致性问题。L1和L2高速缓存控制器需要保证高速缓存的一致性,在Cortex-A9中,高速缓存的一致性是由MESI协议来实现的。Cortex-A9处理器内置了一级缓存模块,由窥探控制单元来实现高速缓存的一致性管理。L2高速缓存需要外接芯片,在最糟糕的情况下需要访问主存储器,并将数据重新传递给LSQ,完成一次存储器读写的全过程。
请简述内存屏障(memory barrier)产生的原因。
若程序在执行时的实际内存访问顺序和程序代码编写的访问顺序不一致,会导致内存乱序访问。内存乱序访问的出现是为了提高程序执行时的效率。内存乱序访问主要发生在如下两个阶段。(1)编译时,编译器优化导致内存乱序访问。(2)执行时,多个CPU间交互引起的内存乱序访问。
在一个单核处理器系统中,保证访问内存的正确性比较简单。每次存储器读操作所获得的结果是最近写入的结果,但是在多个处理器并发访问存储器的情况下就很难保证其正确性了。
ARM有几条内存屏障指令?它们之间有什么区别?
ARM64处理器把内存屏障指令细分为数据存储屏障指令、数据同步屏障指令以及指令同步屏障指令。
弱一致性内存模型要求同步访问是顺序一致的,在一个同步访问可以执行之前,之前的所有数据访问必须完成。在一个正常的数据访问可以执行之前,所以之前的同步访问必须完成。这实质上把一致性问题留给了程序员来解决。在ARM处理器中使用内存屏障指令的方式来实现同步访问。内存屏障指令的基本原则如下:
所有在内存屏障指令之前的数据访问必须在内存屏障指令之前完成。所有在内存屏障指令后面的数据访问必须等待内存屏障指令执行完。多条内存屏障指令是按顺序执行的。
请简述高速缓存(cache)的工作方式。
处理器访问主存储器使用地址编码方式。高速缓存也使用类似的地址编码方式,因此处理器使用这些编码地址可以访问各级高速缓存。
处理器在访问存储器时,会把虚拟地址同时传递给TLB和高速缓存。TLB是一个用于存储虚拟地址到物理地址转换的小缓存,处理器先使用有效页帧号在TLB中查找最终的实际页帧号。如果其间发生TLB未命中,将会带来一系列严重的系统惩罚,处理器需要查询页表。假设发生TLB命中,就会很快获得合适的RPN,并得到相应的物理地址。
高速缓存的映射方式有全关联(full-associative)、直接映射(direct-mapping)和组相联(set-associative)3种方式,请简述它们之间的区别。为什么现代的处理器都使用组相联的高速缓存映射方式?
全关联映射是主存块可以映射到cache中的任意一块,说白了就是一对多,‘一’指的是主存中的任意一块,‘多’指的是cache的每一行,即主存块可以映射到cache中的任意一块。
直接映射是当每组只有一个高速缓存行时,高速缓存称为直接映射高速缓存。说白了就是多对一的映射方式。‘多’指的是主存中的不同分区下的块号,‘一’指的是cache中特定的某一个块。
组相联映射是基于直接映射和全相联映射的一个比较折中的方式,即组内是全相联映射,组间是直接映射。由于组相联映射是为了解决直接映射高速缓存中的高速缓存颠簸问题,因此组相联的高速缓存结构在现代处理器中得到广泛应用。
-
个人信息无误,确认可以完成阅读计划和打卡任务
-
已参与,“电磁干扰量化分析比较难”。
-
下载好了
-
已确认,谢谢EEWorld