- 2025-01-26
-
发表了主题帖:
《Linux内核深度解析》--文件系统
在Linux系统中,一切皆文件,除了通常所说的侠义的文件(文本文件和二进制文件)以外,目录、设备、套接字和管道等都是文件。
文件系统在不同的上下文中有不同的含义。
(1)在存储设备上组织文件的方法,包括数据结构和访问方法。
(2)按照某种文件系统类型格式化的一块存储介质。我们常说在某个目录下挂载或卸载文件系统,这里的文件系统就是这种意思。
(3)内核中负责管理和存储文件的模块,即文件系统模块。
Linux文件系统的架构如下图所示,分为用户空间、内核空间和硬件3个层面。
创建文件的使用方法:
创建不同类型的文件,需要使用不同的命令。
(1)普通文件:touch FILE,这条命令本来用来更新文件的访问时间和修改时间,如果文件不存在,创建文件。
(2)目录:mkdir DIRECTORY 。
(3)符号链接(也称为软链接):In-s TARGET LINK_NAME或In --symbolic TARGET LINK_NAME 。
(4)字符或块设备文件:mknod NAME TYPE(MAJOR MINOR) 。
参数TYPE:b表示带缓冲区的块设备文件,c表示带缓冲区的字符设备文件,u表示不带缓冲区的字符设备文件,p表示命名管道。
(5)命名管道:mkpipe NAME 。
(6)命令“In TARGET LINK_NAME”用来创建硬链接,给已经存在的文件增加新的名称,文件的索引节点有一个硬链接计数,如果文件有n个名称,那么硬链接计数是n。
- 2025-01-22
-
发表了主题帖:
《Linux内核深度解析》--内核互斥技术
在内核中,可能出现多个进程(通过系统调用进入内核模式)访问同一个对象、进程和硬中断访问同一个对象、进程和软中断访问同一个对象、多个处理器访问同一个对象等现象,我们需要使用互斥技术,确保在给定的时刻只有一个主体可以进入临界区访问对象。
如果临界区的执行时间比较长或者可能睡眠,可以使用下面这些互斥技术。
(1)信号量,大多数情况下我们使用互斥信号量。
(2)读写信号量。
(3)互斥锁。
(4)实时互斥锁。
申请这些锁的时候,如果锁被其他进程占有,进程将会睡眠等待,代价很高。
如果临界区的执行时间很短,并且不会睡眠,那么使用上面的锁不太合适,因为进程切换的代价很高,可以使用下面这些互斥技术。
(1)原子变量。
(2)自旋锁。
(3)读写自旋锁,它是对自旋锁的改进,允许多个读者同时进入临界区。
(4)顺序锁,它是对读写自旋锁的改进,读者不会阻塞写者。
申请这些锁的时候,如果锁被其他进程占有,进程自旋等待(也称为忙等待)。
进程还可以使用下面的互斥技术。
(1)禁止内核抢占,防止被当前处理器上的其他进程抢占,实现和当前处理器上的其他进程互斥。
(2)禁止软中断,防止被当前处理器上的软中断抢占,实现和当前处理器上的软中断互斥。
(3)禁止硬中断,防止被当前处理器上的硬中断抢占,实现和当前处理器上的硬中断互斥。
在多处理器系统中,为了提高程序的性能,需要尽量减少处理器之间的互斥,使处理器可以最大限度地并行执行。从互斥信号量到读写信号量的改进,从自旋锁到读写自旋锁的改进,允许读者并行访问临界区,提高了并行性能,但是我们还可以进一步提高并行性能,使用下面这些避免使用锁的互斥技术。
(1)每处理器变量。
(2)每处理器计数器。
(3)内存屏障。
(4)读-复制更新(Read-Copy Update,RCU)。
(5)可睡眠RCU。
使用锁保护临界区,如果使用不当,可能出现死锁问题。内核里面的锁非常多。定位很难,为了方便定位死锁问题,内核提供了死锁检测工具lockdep。
- 2025-01-12
-
发表了主题帖:
内核深度解析--中断
软件通过中断号识别中断,每个中断号唯一对应一个中断源。 中断有以下4种类型。
(1)软件生成的中断(Software Generated Interrupt,SGI):中断号0~15,通常用来实现处理器间中断(Inter-Procesor Interrupt,IPI)。这种中断是由软件写分发器的软件生成中断寄存器(GICD_SGIR)生成的。
(2)私有外设中断(Private Peripheral Interrupt,PPI):中断号16~31。处理器私有的中断源,不同处理器的相同中断源没有关系,比如每个处理器的定时器。
(3)共享外设中断(Shared Peripheral Interrupt,SPI):中断号32~1020。这种中断可以被中断控制器转发到多个处理器。
(4)局部特定外设中断(Locality-specific Peripheral Interrupt,LPI):基于消息的中断。 GIC vI和GIC v2不支持LPI。
中断可以是边沿触发(edge-triggered),也可以是电平触发(level-triggered)。边沿触发是在电压变化的一瞬间触发,电压由高到低变化触发的中断称为下降沿触发,电压由低到高变化触发的中断称为上升沿触发。电平触发是在高电压或低电压保持的时间内触发, 低电压触发的中断称为低电平触发,高电压触发的中断称为高电平触发。
中断有以下4种状态。
(1)Inactive:中断源没有发送中断。
(2)Pending:中断源已经发送中断,等待处理器处理。
(3)Active:处理器已经确认中断,正在处理。
(4)Active and pending:处理器正在处理中断,相同的中断源又发送了一个中断。
中断的状态转换过程如下。
(1)Inactive->Pending:外围设备发送了中断。
(2)Pending->Active:处理器确认了中断。
(3)Active->Inactive:处理器处理完中断。
对于中断控制器的每个中断源,向中断域添加硬件中断号到Linux中断号的映射时, 内核分配一个Linux中断号和一个中断描述符irg_desc,中断描述符有两个层次的中断处理函数。
(1)第一层处理函数是中断描述符的成员handle_irq()。
(2)第二层处理函数是设备驱动程序注册的处理函数。中断描述符有一个中断处理链表(irq_desc.action),每个中断处理描述符(irq_action)保存设备驱动程序注册的处理函数。因为多个设备可以共享同一个硬件中断号,所以中断处理链表可能挂载多个中断处理描述符。
怎么存储Linux中断号到中断描述符的映射关系?有两种实现方式。
(1)如果中断编号是稀疏的(即不连续),那么使用基数树(radix tree)存储。需要开启配置宏CONFIG SPARSE IRQ。
(2)如果中断编号是连续的,那么使用数组存储。
kernel/irq/irqdesc.C
#ifdef CONFIG_SPARSE_IRQ
static RADIX_TREE(irg_desc_tree, GFP_KERNEL);
#else
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0. ...NR_IRQS-1] = {
.handle_irq = handle bad irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};
#endif
ARM64架构默认开启配置宏CONFIG_SPARSE_iRQ,使用基数树存储。
- 2025-01-05
-
发表了主题帖:
《Linux内核深度解析》--内存管理
内存管理子系统的架构如下图所示,分为用户空间、内核空间和硬件3个层面:
1.用户空间
应用程序使用malloc()申请内存,使用free()释放内存。
malloc()和free()是glibc库的内存分配器ptmalloc提供的接口,ptmalloc使用系统调用brk或mmap向内核以页为单位申请内存,然后划分成小内存块分配给应用程序。
用户空间的内存分配器,除了glibc库的ptmalloc,还有谷歌公司的tcmalloc 和FreeBSqD 的jemalloc。
2.内核空间
(1)内核空间的基本功能。
虚拟内存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk用来扩大或收缩堆,sys_mmap用来在内存映射区域分配虚拟页,sys_munmap用来释放虚拟页。
内核使用延迟分配物理内存的策略,进程第一次访问虚拟页的时候,触发页错误异常,页错误异常处理程序从页分配器申请物理页,在进程的页表中把虚拟页映射到物理页。
页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。
内核空间提供了把页划分成小内存块分配的块分配器,提供分配内存的接口kmalloc() 和释放内存的接口kfree(),支持3种块分配器:SLAB分配器、SLUB分配器和SLOB 分配器。
在内核初始化的过程中,页分配器还没准备好,需要使用临时的引导内存分配器分配内存。
(2)内核空间的扩展功能。
不连续页分配器提供了分配内存的接口vmalloc和释放内存的接口vfree,在内存碎片化的时候,申请连续物理页的成功率很低,可以申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续而物理地址不连续。
每处理器内存分配器用来为每处理器变量分配内存。
连续内存分配器(Contiguous Memory Allocator,CMA)用来给驱动程序预留一段连续的内存,当驱动程序不用的时候,可以给进程使用;当驱动程序需要使用的时候,把进程占用的内存通过回收或迁移的方式让出来,给驱动程序使用。
内存控制组用来控制进程占用的内存资源。
当内存碎片化的时候,找不到连续的物理页,内存碎片整理(“memory compaction”的意译,直译为“内存紧缩”)通过迁移的方式得到连续的物理页。
在内存不足的时候,页回收负责回收物理页,对于没有后备存储设备支持的匿名页, 把数据换出到交换区,然后释放物理页;对于有后备存储设备支持的文件页,把数据写回存储设备,然后释放物理页。如果页回收失败,使用最后一招:内存耗尽杀手(OOM killer,Out-of-Memory killer),选择进程杀掉。
3.硬件层面
处理器包含一个称为内存管理单元(Memory Management Unit,MMU)的部件,负责把虚拟地址转换成物理地址。
内存管理单元包含一个称为页表缓存(Translation Lookaside Buffer,TLB)的部件, 保存最近使用过的页表映射,避免每次把虚拟地址转换成物理地址都需要查询内存中的页表。
为了解决处理器的执行速度和内存的访问速度不匹配的问题,在处理器和内存之间增加了缓存。缓存通常分为一级缓存和二级缓存,为了支持并行地取指令和取数据,一级缓存分为数据缓存和指令缓存。
- 2024-12-27
-
发表了主题帖:
《Linux内核深度解析》--进程管理
Linux内核把进程称为任务(task),进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。
进程有两种特殊形式:没有用户虚拟地址空间的进程成为内核线程,共享用户虚拟地址空间的进程称为用户线程,通常在不会引起混淆的情况下把用户线程简称为线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
在Linux内核中,新进程是从一个已经存在的进程复制出来的。内核使用静态数据构造出0号内核线程,0号内核线程分叉生成1号内核线程和2号内核线程(kthreadd线程)。1号内核线程完成初始化以后装载用户程序,变成1号进程,其他进程都是1号进程或者它的子孙进程分叉出来的;其他内核线程是kthreadd线程分叉生成的。
3个系统调用可以用来创建新的进程。
(1)fork(分叉):子进程是父进程的一个副本,采用了写时复制的技术
(2)vfork:用于创建子进程,之后子进程立即调用execve以装载新程序的情况。为了避免复制物理页,父进程会睡眠等待子进程装载新程序。现在fork采用了写时复制的技术,vfork失去了速度优势,已经被废弃。
(3)clone(克隆):可以精确地控制子进程和父进程共享哪些资源。这个系统调用的主要用处是可供pthread库用来创建线程。
clone是功能最齐全的函数,参数多,使用复杂,fork是clone的简化函数。(以下是_do_fork函数)
- 2024-12-22
-
发表了主题帖:
《Linux内核深度解析》--内核引导和初始化
处理器上电以后,首先执行引导程序,引导程序把内核加载到内存,然后执行内核,内核初始化完成以后,启动用户空间的第一个进程。
嵌入式设备通常使用NOR闪存作为只读存储器来存取引导程序。NOR闪存的容量比较小,最小读写单位是字节,程序可以直接在芯片上执行。从物理地址0取指令,也就是到NOR闪存的起始位置取指令。
嵌入式设备通常使用U_Boot作为引导程序。U_Boot(Universal Boot Loader)是德国DENX软件工程中心开发的引导程序,是遵循GPL条款的开源项目。
_start是U_Boot程序入口,直接跳转到标号reset执行,附上reset、_main及run_main_loop代码及注解,大家一起来学习哈:
- 2024-12-14
-
发表了主题帖:
《Linux内核深度解析》之1--开箱
开箱啦!感谢EEWORLD!
废话多不说,直接上图:
接下来的几周时间我会和大家一起分享阅读心得体会,再来一张新书照
- 2024-12-11
-
回复了主题帖:
读书入围名单: 《Linux内核深度解析》
个人信息无误,确认可以完成阅读分享计划