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, &divide_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/

  

posted on 2025-01-27 18:13  toong  阅读(37)  评论(0)    收藏  举报