Linux内核相关知识点记录【内存管理】【进程调度】【异常调试】【锁】【kvm虚拟化】【内核启动】,linuxkvm


1、内存管理 怎么阐述自己的理解?

。硬件原理,分页机制原理

内核内存管理由MMU提供硬件支持,MMU实现虚实地址VA=>PA的转换以及权限检查,虚拟地址和物理地址之间的映射关系是页表机制,每一个页表项都保持物理地址页及其访问权限,页表机制和图书馆借书是一个原理,为什么要用多级页表呢?假设只有一级,类比成数组,用a【i】记录每一项,那么记录所有映射关系i=4GB/4KB=1000*1000项,显然不现实,线性数组查找效率低下,分级的好处就是把线性查找转换才二分法查找,所需时间是log 2^n,极大提高效率,这也就是内存管理使用树结构的原因,类比图书馆借书的例子:首先在电脑上查到书在 4楼-13巷 -5架-6层-100号,那么拿到这个地址去对应的楼层(一级页表),巷号(二级页表),架次(三级页表),层(4级页表),最后找到100这个编号的书(页内偏移)。

 

开启了mmu,那么只有mmu本身可以看到物理地址,CPU发出的都是虚拟地址.mmu的页表寄存器记录了页表本身保存在物理内存的位置。

每个进程都有4G的地址空间,0-3G是用户地址空间,3-4G是共享的内核地址空间,每个进程都有自己独立的页表,进程切换的时候,内核会把进程的页表地址填入MMU实现进程切换.这样就实现了各个进程地址空间隔离.

MMU如何实现内存权限保护?比如定义一个const 常量,那么C编译器就会把这个const常量链接到rodata(只读数据段),载入内存建立页表时被标记为readonly,MMU提供运行时检查,如果有人试图去写它,那么就会被MMU拦截发出segmentfault.实现了内存保护的机制.

TLB保存最频繁使用的页表数据,属于MMU的高速缓存.

 

物理内存划分区域: DMA zone, normal zone,highmem zone,

虚拟地址空间是指每个进程的0-4g虚拟地址空间,属于虚拟地址的概念,而低端、高端内存是属于物理内存的概念.

内核中物理内存的管理是由buudy算法实现,buddy以2的n次方对空闲内存进行管理,最分配粒度是1页,也就是4k,而slub算法是基于buddy算法的二次管理,用于分配更小的内存粒度。

 

。内存动态分配和释放,kmalloc,vmalloc,slub,buddy system的特点和关系.讲述用户空间malloc 1M内存后发生了什么?

从buddy拿到的内存都是以1页为单位粒度,而slub是从buddy拿一大块内存进行二次管理,以更小的分配粒度分配和回收,kmallc就是从slub拿内存,kmallc/kfree和buddy不是一样对应关系,执行kfree不一定还给buddy,具体什么时候还,是slub算法 决定,这样做的目的是为了应对频繁申请释放内存的场景对性能的影响.

 

kmalloc申请的内存是线性映射关系,适用于频繁申请释放且小块的内存,效率高,但是注意kmalloc可以休眠,特别是内存紧张得时候.

vmalloc申请的内存是非线性映射关系,是使用红黑树数据结构管理,用于申请大块的对物理内存没有连续要求的场景,优先从highmem区域拿,通过alloc_page从buddy拿内存.

malloc是libc的库函数,不是系统调用,libc库对申请的内存做二次管理,其最终是通过brk和mmap向内核要内存.malloc/free不是系统调用和内核不是一一对应关系.free后不一定还给内核,具体什么时候还,由libc算法决定,这是为了性能的考量,因为对物理内存频繁的申请释放对性能有影响.

 

malloc 1M内存的时候并没有真正拿到物理内存,只是将分配的连续VMA映射到一片清零的物理地址并标注为readonly,只有当有人去写这块VMA的时候,MMU执行地址转换检查权限发现是只读,会被MMU拦截而触发page fault,但是内核检查到当前的地址权限是R+W,发现是malloc导致的page fault,然后分配物理内存页,改写权限,此刻真正拿到物理内存。而且是边写边拿.这就是所谓内核分配内存的lazzy性,这样做的目的是因为内核无法控制应用程序的行为,尽量防止申请了不用的浪费情况.

内核不信任应用程序只相信内核本身。lazzy只针对应用程序,不针对内核,内核调用kmalloc或者vmalloc就是立即拿到内存.

 

。进程的虚拟内存VMA(virtual memory areas)

VMA区域存在用户空间,可以是独占的也可以是共享的,一个进程拥有多个VMA,零散分别在0-3G的地址空间,每一个进程的代码段,数据段,堆都是一个VMA实例,

查看vma的方法:

/proc/pid/maps 、 /proc/pid/smaps

 

page fault的可能性:

a: 动态内存分配,写时拷贝,属于次要的page fault,因为不需要去硬盘load数据,开销小。

b: 进程访问不可读写的非法地址,会被MMU拦截,内核发出segment fault信号,导致oops.

c: 进程访问VMA区域,但是权限不对,比如代码段权限是R+X,但是你尝试去写,也会和上面一样的结局

d: 进程访问VMA区域,权限检查通过,但是需要从硬盘load数据到内存,这种属于大开销,所谓主要的page fault,比如代码段首次被载入内存的时候,这也就是为什么第一次启动进程比第二次慢的原因.(内存大手机跑得更流程原因之一)

 

VSS- Virtual Set Size 虚拟耗用内存

RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)

PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

USS- Unique Set Size 进程独自占用的物理内存(堆内存,独占)

VSS >= RSS >= PSS >= USS

可以使用smem工具查看进程的VSS,RSS.PSS,USS值,USS值就是堆内存占用,内存泄漏一般只看这个。

安装:$ sudo apt-get install smem

内存泄漏的判定,查看USS,多时间点采样,震荡发散的内存消耗就是存在泄漏.

 

。内存和IO交换

分两类:

a.有文件背景的页(file-backed),比如读写文件,程序代码段属于这类,在硬盘中有实在的文件存在;

b.没有文件背景的页,俗称匿名页,比如栈内存,堆内存等,这类内存的交互通过硬盘上的swap分区来实现匿名页的交换.

 

文件首次载入内存的page cache中,page cache相当于内存中文件的副本,进程通过操作page cache来实现对文件的读写.

$ free

total used free shared buffers cached

Mem: 16340236 14154128 2186108 482520 1835924 3946088

-/+ buffers/cache: 8372116 7968120

Swap: 31875068 384264 31490804

总物理内存大小 = total + shared+ buffers + cached

total = used + free

buffers :直接从裸分区读取的数据到内存的page cache;

cached:从文件背景读取到内存的page cache;

两个是一个东西,背景不一样。

 

页交换使用LRU算法,最近最少使用的被交换出去

内核中使用kswapd进程作为内存回收.可以回收两类内存页。

zram原理:从内存中划分一块区域用于匿名页交换替代硬盘中的swap分区,压缩需要交换的匿名页,zram可以提供内存可用空间,但是会消耗CPU性能,需要平衡.

 

2、进程管理

。数据结构

进程是资源分配的单位,线程是调度的单元,这是经典定义.

内核中每一个进程都有一个数据结构是task_struct,每一个task_struct都包含着mm 资源,file资源,signal资源指针,每个task_struct之间有三种联系的视角:

a: 形成双链表 -- 方便快速遍历所有进程;

b: 形成树 -- 方便父子进程的查询;

c: 形成哈希 -- 方便从pid找进程,比如:kill -9 $pid;

这是典型的以空间换时间的算法,提供不同的视角下的时间最短查询.

。生命周期

六种:ready,run,stop,sleep,disk-sleep(深度睡眠)

sleep(浅度睡眠)可以被中断或者信号唤醒,而深度睡眠只能被资源唤醒,这里资源就是你要等的东西,比如mutex lock,代码段的page fault等。

僵尸进程:子死父没有及时清场的一种临时状态,子进程挂了后,父进程需要调用wait4pid接口才会消失,僵尸进程基本不暂用系统资源了。

。进程和线程的区别

内核中进程和线程都是使用task_struct描述,区别在于内存资源,文件资源,信号资源等是否共享,所以线程也称为轻量级的进程.对于调度器来说,只要是task_struct就可以调度.所以在内核中,进程和线程可以理解为同义词.

。进程0,进程1

开机后第一个进程就是进程0,最后会称为idle进程,idle进程优先级最低,idle进程拿到cpu后会执行WFI指令进入低功耗状态,任何来一个中断就会唤醒调度到新的进程,这样非常有利于电源管理的设计.

进程1就是init进程,用户空间所以的进程都是1号进程直接或者间接fork出来的.

 

。进程调度

吞吐和响应本是一对矛盾行为,需要根据业务需求来选择侧重.

进程调度类型分为内核RT进程和普通用户进程,进程有分IO消耗型(鼠标)和CPU消耗型(编译).

a: RT进程调度策略又可以分为sched_fifo,sched rr类型,

sched_fifo:同优先级进程拿到cpu后,必须等当前进程运行完退出才交出CPU;

sched_rr: 同优先级进程直接轮转获得CPU时间片.

0-99是RT进程优先级,100-139是普通用户进程优先级,数字越大优先级越高,高优先级进程可以抢占低优先级进程.

b: 普通进程的调度是CFS-完全公平调度算法,使用红黑色数据结构,基本原理比较简单,总是调度vrruntime最小的进程,vrruntime = physical runtime/(nice权重系数),所以一个普通进程运行的越久,他的优先级越低,一个进程睡的越久,优先级越高,被调度的概率越高,优先级最高的进程就是睡眠越久而且nice值越高的IO消耗型进程.

。SMP负载均衡

a: RT进程:N个优先级最高的RT进程均匀分布到各个核;

b: 普通进程:核心思想是以“劳动为荣”,比如新fork出来一个进程,exec的时候会把它推到最闲的核上去运行

。cgroup - 资源控制.

核心就是分层调度,各个group内CFS

。RT-OS

为什么linux不是一个硬实时的操作系统?

硬实时强调一定时间内必须完成调度,进程总是运行在4类区间:

a: 硬中断 - 外部中断

b: 软中断-系统调用

c: 不可调度的进程上下文,比如陷入内核的spinlock保护区

d: 可调度的进程上下文

只有第4类区间是可以实时抢占调度的,前面三类的时间都无法确定,所以无法满足硬实时OS的要求。

 

RT-patch的原理:

a: 中断线程话 -- 是线程就可以抢

b: 优先级继承协议 -- 临时提高优先级,不受中等优先级进程的干扰

c: spinlock换成mutex,mutex可以睡眠

打上补丁后,基本上可以达到硬实时OS的需求.

 

3,内核启动流程,从head.S开始到init进程创建.

a: 切换CPU到svc模式,使能d-cache和i-cache,使能MMU;

b: 跳转到start_kernel函数执行,初始化特定体系结构的设置;

c: 解析uboot传入的命令行参数,是否使能早期串口打印;

d: 初始化各种核心数据结构,比如内存管理,异常处理,中断向量,dts,cgroup,等等;

e: 创建1号init进程和2号kthread进程,开始驱动初始化.

f: 最后是用户空间初始化,0号进程演变为idle进程.

 

4、内核死机等异常的分析手法,策略

分类型:

a: 调用BUG()出现的异常属于人为主动上报异常,一般可以直接从oops信息中找到问题点;

b: 异常现场和真实原因重合的异常,同上可以根据oops的pc指针位置,相关寄存器信息判断异常类型是未定义指令异常,还是data abort,或者其他类型;

c: 异常现场和真实原因不重合的类型,抓ramdump使用工具离线分析寄存器信息,确认异常的类型,观察异常线程的变量参数是否异常,确认是否内存被踩导致,若是kmalloc导致的内存被踩,可以打开slub debug浮现问题后抓取。

d: 若是死锁类问题,需要抓到所以D状态的进程调用栈,逐一排查找出互锁关系,进一步分析原因.如果有必要,需要打开mutex debug等开关辅助调试.

 

5、KVM,qemu虚拟化的架构认识,实现原理等认识

kvm - kernel-based virtual machine(基于内核的虚拟机)

基于intel,AMD提供的虚拟化平台,不提供硬件虚拟化操作,IO操作借助QEMU完成.

 

guest作为一个普通进程运行于宿主机.

guest的CPU(vCPU)作为进程的线程存在,并受到宿主机内核的调度.

KVM整体架构:

\

\
kvm提供CPU,内存模拟,qemu提供IO模拟.

kvm提供一个/dev/kvm字符设备,qemu通过ioctl和内核kvm驱动交互,执行初始化,内存分配,指令集加载等操作.

guest的所以硬件操作都由qemu接管,qemu负责和宿主机真实硬件交互.

 

KVM简单来说就是由两部分构成:

a: KVM驱动,已经是内核一部分,负责虚拟机创建、内存分配、虚拟寄存器读写,和虚拟cpu运行;

b: qemu模拟虚拟机的用户空间组件,模拟IO操作访问外设的途径.

 

QEMU是完整的纯软件的虚拟化解决方案,性能比较低,KVM提供CPU和内存的模拟+QEMU提供IO模拟相结合实现互补.

 

KVM运行模式:

客户模式:guest os运行模式,其本身又包含用户模式和内核模式

用户模式:linux os的用户模式,qemu运行在这个模式

内核模式:linux kernel运行模式,kvm内核驱动模块运行在这个模式

 

ARM处理器有hypervisor层,虚拟化的硬件支持。

 

6. 内核锁

a. 类型:信号量锁(现在少用了)

b. mutex lock,可以睡眠,会引起上下文切换,效率不及spinlock

c. spinlock,不允许睡眠,会关抢断,中断中使用spin lock/unlock,进程上下文中使用spinlock irq save/restore. -- 如果当前保护的函数不会被中断上下文打断就不用irqsave,否则就需要用spinlock irqsave,因为中断会打断spinlock,然后又进入等待spinlock的情况,导致dead lock.

d. rcu, read-copy-update,一种支持并发的保护机制,很复杂.

e. 原子锁,atomic,现在用的少了。

使用注意:

被保护的代码如果不允许睡眠,追求效率,那么用spinlock,它不会切换上下文,比如中断上下文不可用mutex lock.

相关内容