linux kernel 学习笔记
主要是这书:《linux内核设计与实现》还有几本别的
---------------
自下而上子系统:
----------------------------------------------
系统调用 | IPC
----------------------------------------------
内存管理 | 虚拟文件系统 | 网络协议栈
----------------------------------------------
进程调度 | 中断调度 | 同步/timer
----------------------------------------------
## 指令/汇编 | ## 设备驱动
----------------------------------------------
boot: 0x7C00/保护模式 ----> 分段/分页,物理地址/逻辑地址/虚拟地址
----------------------------------------------
保护模式:
实模式与保护模式的主要区别在于“内存管理”和“系统安全性”
1 实模式20位地址总线,只能访问1M内存。
2 实模式:段地址+段偏移。保护模式:段选择子+段偏移(GDT)。
3 执行虚拟内存与分页机制。
4 支持指令级任务管理和特权级。
切换步骤:
1. 设置GDT。
2. 设置PE位。
3. 代码跳转。
MMU = 分段单元 + 分页单元
权限位在段描述符里,MMU负责权限判定。
二级页表之所以剩内存是因为第二级是按需动态分配的。
***??? 为什么要分段
进程调度:
***** 状态机??*****
运行队列,等待队列。
** 进程调度的时机:**
硬中断处理结束的时候:
在interrupt数组调用完do_IRQ()函数,然后返回到内核空间时调用。
这里有两个条件:
1. 必须是32-256的中断,因为依赖interrupt框架。
2. 如果是自己写idt注册的硬中断就不调用,比如用_set_gate()函数。
3. 使用desc描述符即request_irq()接口的中断,都满足条件1.
见代码: entry_64.S::interrupt()
好几个出口,例:retint_kernel()->preempt_schedule_irq()
do_IRQ()函数并不调用:
preemt_enable函数会调schedule(),但是2.6的代码这里并不调。
比如在irq_exit()中专门调用了preempt_enable_no_resched()。
软中断处理结束的时候: 框架也不调用。
软中断的处理进程softirqd会调。
处理时钟中断的时候:依赖硬中断框架。
时钟中断代码逻辑里会调用 scheduler_tick(),这地方只是设置need_sched标记,并不做调度。
系统调用结束的时候:这里会调用schedule()。 entry_64.S::system_call()
其他系统事件或中断触发的时候:很多地方会调用,比如 网卡收包的软中断处理里。
总结:
1. 32~256号硬中断返回内核空间时。
2. 系统调用返回用户空间时。
3. 其他业务相关的系统事件或中断触发时(非常多),比如:网卡收包的软中断处理里。
如果用户进程没有系统调用,所在CPU也没绑定中断,操作系统如何调度?
该CPU的时钟中断返回时。单CPU上的时钟中断可以关吗??
硬中断: 一共256个中断号。外部中断从0x20(32)开始,除0x80(128)是系统调用外。
数据结构:
x86架构 irq_desc数组在哪定义的: kernel/irq/handle.c:242
有红黑树和数组两个实现,取决于一个宏,数组是更通用的架构无关实现。
idt_table 定义在 arch/x86/kernel/head_64.S
interrupt数组定义在 /arch/x86/kernel/entry_64.S:755,数组长度是32~256,每一个函数都初始化为do_IRQ()
used_vectors 已注册硬中断的bitmap标记。长度256个bit
vector_irq 每CPU的,长度256的 int数组。初值是-1
用于存储中断号与中断向量号之间的映射。
IDT初始化:
cpu_init()->load_idt(&idt_descr) --> idt_table
idt_table是per cpu的。
idt_table的内容值由 _set_gate()函数写入。
执行函数: do_IRQ() 【所有外部中断,默认全进这个函数,然后查irq_desc】
interrupt()-> commo->interrupt()->do_IRQ()->handle_irq()
->__do_IRQ() 执行 irq_desc的action->handler()
或者
-> desc->handle_irq()->handle_level_irq()/handle_edge_irq()
handle_level_irq()/handle_edge_irq()
这两个handler主要用于处理与“中断控制器的交互”
最终还是要调用__do_IRQ()。
初始化函数:trap_init()
init_IRQ() -> native_init_IRQ() -> init_ISA_irqs() 前16个desc数组设置成chip8259A
-> apic_intr_init()
-> 从32开始并且没设回调的赋值interrupt数组给IDT,也就是do_IRQ()。
ACPI
IO_ACPI
2个for循环注册,setup_IO_APIC()->setup_IO_APIC_irqs()->set_irq_chip_and_handler_name( handle_edge_irq )
8259A
set_irq_chip_and_handler_name(irq, &i8259A_chip, handle_level_irq
APIC_init_uniprocessor()
网卡中断是怎么注册到中断控制器里去的???
安装中断处理程序:request_irq()
注册handler函数给action->handler()
动态分配一个irq编号:create_irq_nr()
*中断处理程序可以抢占下半部和进程上下文。
CPU对中断的处理是用轮询的方式,即不断查询是否有中断到来。CPU在每一个指令执行之后都会查一下。
https://www.cnblogs.com/upnote/p/15646121.html
软中断:
两个关键数据结构:
1. 保存irq action的数组
struct softirq_action softirq_vec[NR_SOFTIRQS] // NR_SOFTIRQS = 10
2. 每核变量 irq_stat->__softirq_pending;
用位标记对应的irq是否被触发。raise一个irq就会置这个位。
所有软中断: cat /proc/softirqs 就能看见
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
执行契机:
run_ksoftirqd 线程内。
irq_exit() 中断退出时。
执行函数:__do_softirq()
注册函数:open_softirq() -> 给softirq_vec[]数组(长度10)赋值。
跟INT指令是什么关系? 跟INT指令没关系。
软中断到达是进程上下文,还是中断上下文?
如果是进程上下文,是哪个进程的上下文?
下半部禁用?
local_hb_disable()
*下半部可以抢占进程上下文。
软中断不会抢占另一个软中断。
tasklet:
调度函数:tasklet_schedule, tasklet_hi_schedule
或者叫激活/注册,tasklet会执行一次,然后结束,而不是循环执行。
handler: tasklet_action, tasklet_hi_action (用open_softirq注册成了softirq)
执行一次之后就从list里清除了。
同类tasklet不会同时执行。
workqueue:
kernel/workqueue.c
初始化 init_workqueues()
线程主函数 worker_thread()
激活 schedule_work()
同步:
原子
单指令都是原子的,如int32/64赋值。但是实现锁,一般都需要
”判断+赋值“原子,除了seq锁。
原子操作指令是实现复杂锁的基础,二者要有1.
1. CAS
2. 内存总线锁
自旋锁,读写自旋锁 (不能睡眠)(读写锁对写者不友好)
信号量,读写信号量,互斥锁,完成变量 (可以睡眠)(不能用在中断上下文)
(完成变量可以用初值为0的信号量实现,
初值为1的二值信号量等价于互斥锁。
初值为0的二值信号量等价于完成变量。)
seq锁,设计精巧,(写者友好。大量读的的读写锁会饿死写者)
写者之间的互锁还是依赖一个自旋锁,但是和读写锁的区别是,读者不给这个锁加锁,
所有读者不会把写者锁住。
内存屏障,编译屏障
禁止抢占
中断中使用自旋锁需要同时使用禁止抢占。
也有场景:不使用自旋锁,只想使用禁止抢占。
时钟:
**硬中断**
主要函数:scheduler_tick()
中断注册:setup_default_timer_irq() 将0号(0x20)中断注册成函数timer_interrupt().
global_clock_event->event_handler = tick_handle_periodic();
irq0就是这个timer_interrupt
hpet设备通过hpet_setup_irq函数动态注册了一个request_irq(), 回调函数是hpet_interrupt_handler()
event_handler = tick_handle_periodic();
hpet设备的irq编号是create_irq()函数申请的。
中断处理:timer_interrupt()->tick_handle_periodic()
**延时执行**
timer:
kernel/timer.c
在软中断中执行。
触发函数:run_local_timers():触发TIMER_SOFTIRQ类型的软中断。
执行函数:run_timer_softirq()
时间轮算法:
https://juejin.cn/post/7083795682313633822
多层时间轮在进位的时候会有性能问题。
初始化:init_timers()
hrtimer:
posix_timer:
忙等待:
while(time_after(jiffies, delay)) ;
cond_resched(), volatile
udelay()
schedule_timeout():
先设置状态:set_current_state(TASK_INTERRUPTIBLE) 或 set_current_state(TASK_UNINTERRUPTIBLE)
是用timer实现的。
等待一段时间或者一个事件发生。
可以用来实现sleep
内存: ***???
MMU 是现代操作系统实现“虚拟内存、内存保护和多任务处理”的基础。
page/zone
buddy ****????
alloc_pages() free_pages()
kmalloc()/kfree()【性能好】, vmalloc()/vfree()【物理机页不连续,可能睡眠】
GFP_KERNEL / GFP_ATOMIC
slab 【高速缓存】 ***???
kmalloc在slab之上。
内核栈:
中断栈,alloca()
kmap() / kunmap()
get_cpu() / put_cpu()
alloc_percpu()/free_percpu()/get_cpu_var()/put_cpu_var()
https://s3.shizhz.me/linux-mm/addressing
VFS:
A 超级块对象 linux/include/linux/fs.h:: super_block
索引节点对象 linux/include/linux/fs.h:: inode
目录项对象 linux/include/linux/dcache.h::dentry
dcache: 加速目录查找,icache(inode cache)也一起被缓存。
文件对象 linux/include/linux/fs.h:: file
文件对象(进程A) 文件对象(进程B)
\ /
\ /
v v
目录项对象(唯一)
|
v
索引节点对象(唯一)
aio ?? 忠告锁??
其他
挂载点 linux/mount.h::vfsmount
进程相关 linux/fdtable.h::files_struct 所有进程文件对象的集合 被task_struct引用
linux/fs_struct.h::fs_struct
块设备:
块设备随机读取,字符设备顺序读取。
块大小是2的整数倍,且不能超过一个页的大小。
缓存区 linux/buffer_head.h::buffer_header 用于描述物理磁盘块和内存缓冲区直接的映射关系。(2.6后已废,由bio替代)
bio linux/blk_types.h 被task_struct引用
请求队列 linux/blkdev.h::request_queue
IO调度程序: block/ *-iosched.c
合并,排序,缓存后提交给硬盘。目的是缩短磁盘寻址时间。
电梯调度,CFQ调度,空操作调度,等。
进程地址空间:
进程好像可以访问所有物理内存。甚至远远大于。 平坦flat(与分段相对而言)
使用同一个地址空间的两个进程,就是线程。
有效区域:访问为有效区域就是“段错误”
有效区域内分段:代码段,数据段等。
内核线程:没有进程地址空间,没有用户上下文,不访问用户空间内存。需要页表时借用上一个进程的地址?。
内存描述符: linux/include/linux/mm_types.h::mm_struct 表示进程地址空间 被task_struct引用
内存区域: linux/include/linux/mm_types.h::vm_area_struct 地址空间中的一个连续段,段和段不可重叠
结构体成员:vm_flags
全局共享不可写区域,用来节省物理内存,例如libc.so
页表: include/asm-generic/page.h::pgd_t
用于虚拟内存到物理内存的转换。页表索引是虚拟内存地址,页表表项是物理内存地址。
linux为了节省表项本身占用的物理内存空间,实现3级页表:pgd_t -> pmd_t -> pte_t
TLB是表项的高速缓存,由硬件实现。(MMU??)
页缓存:通过内存访问加快硬盘访问速度
三种策略:nowrite,write-through,write-back
脏页:写入了缓存但是没用写入硬盘的缓存页。
address_space数据结构,是物理内存维度的全局一份。 include/linux/fs.h
读取使用LRU算法,写入需要单独的线程做回写动作。
回写线程:fluser。为防止阻塞每设备一个。5.10的内核叫: kworker/flush和kworker/writeback
设备与模块
几种设备:字符设备(流式访问),块设备(随机寻址访问),网络设备(套接字API访问),杂项设备,伪设备。
模块
设备树:kobject,ktype,kset include/linux/kobject.h
sysfs,kobject与目录项一对一,devcies目录尤其重要。
HAL:https://www.freedesktop.org/wiki/Software/hal/
事件:kobject_uevent
调试
printk,日志限速,
gdb vmlinux /proc/kcore
其他
indent命令可以方便的调整缩进
---------------------
附录:
---------------------
硬中断号的划分:
/*
* Linux IRQ vector layout.
*
* There are 256 IDT entries (per CPU - each entry is 8 bytes) which can
* be defined by Linux. They are used as a jump table by the CPU when a
* given vector is triggered - by a CPU-external, CPU-internal or
* software-triggered event.
*
* Linux sets the kernel code address each entry jumps to early during
* bootup, and never changes them. This is the general layout of the
* IDT entries:
*
* Vectors 0 ... 31 : system traps and exceptions - hardcoded events
* Vectors 32 ... 127 : device interrupts
* Vector 128 : legacy int80 syscall interface
* Vectors 129 ... 237 : device interrupts
* Vectors 238 ... 255 : special interrupts
*
* 64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table.
*
* This file enumerates the exact layout of them:
*/
0到31的定义: arch/x86/kernel/traps.c
set_intr_gate(0, ÷_error);
set_intr_gate_ist(1, &debug, DEBUG_STACK);
set_intr_gate_ist(2, &nmi, NMI_STACK);
/* int3 can be called from all */
set_system_intr_gate_ist(3, &int3, DEBUG_STACK);
/* int4 can be called from all */
set_system_intr_gate(4, &overflow);
set_intr_gate(5, &bounds);
set_intr_gate(6, &invalid_op);
set_intr_gate(7, &device_not_available);
#ifdef CONFIG_X86_32
set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
set_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endif
set_intr_gate(9, &coprocessor_segment_overrun);
set_intr_gate(10, &invalid_TSS);
set_intr_gate(11, &segment_not_present);
set_intr_gate_ist(12, &stack_segment, STACKFAULT_STACK);
set_intr_gate(13, &general_protection);
set_intr_gate(14, &page_fault);
set_intr_gate(15, &spurious_interrupt_bug);
set_intr_gate(16, &coprocessor_error);
set_intr_gate(17, &alignment_check);
#ifdef CONFIG_X86_MCE
set_intr_gate_ist(18, &machine_check, MCE_STACK);
#endif
set_intr_gate(19, &simd_coprocessor_error);
---------------------
定时器实现
----------------------
https://mp.weixin.qq.com/s?__biz=MzIzODIzNzE0NQ==&mid=2654418061&idx=1&sn=e62b95e82d267e37fbab09d1ef835f55&chksm=f2fff03bc588792dba1942420c171b930aaa6fd6dada75acfe55f5245c70a5d54e751539f668&scene=21#wechat_redirect
---------------------
进程状态机
---------------------
https://astrisk.github.io/linuxkernel/2017/05/05/linux-kernel-process-state/
浙公网安备 33010602011771号