# minilinux **Repository Path**: m561247/minilinux ## Basic Information - **Project Name**: minilinux - **Description**: 记录自己学习Linux的过程 based on linux2.6.32 - **Primary Language**: Unknown - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2025-08-04 - **Last Updated**: 2025-08-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 写是最好的学——Linux学习记录 ## 1.前言 ### 1.1 学习Linux的痛点 因为自己平时的工作和linux相关,特别是驱动,所以一直想深入的学习下内核的其他子系统,比如进程管理、内存管理、文件系统等等。为此,也看过许多相关的书籍,文章,但是总感觉不能内化为自己的知识。 后面也想到学习类似“教你从零写操作系统”的课程,但是我发现有几点对于我来说不合适的地方: 1. 99%都是以x86架构来进行讲解,但是目前在嵌入式行业arm依旧是霸主地位 2. 虽然相较Linux简单很多,但是至少从代码的层面都是自己的一套,对于学习Linux帮助不是很直接 ### 1.2 怎么学? 经过不断的尝试,通过这样的一种方式来学习Linux可能会更适合我: 1. 在一定的裸机代码的基础上(这个我下面会说明),从零、渐进地添加Linux代码进去,目标是先不管具体实现简单还是复杂,至少在功能上要和Linux无限接近 2. 使用arm架构的开发板进行实际的调试 基于上述原则,入手了一块正点原子的imx6ull的开发板,选择这个板子的原因也很简单,一是armv7单核处理器,二是该板子配套提供了能让芯片运行起来的裸机代码,这也是上面说的“在一定的裸机代码的基础上”的意思。 同时,我也会把自己的学习过程进行记录,一来能帮助自己对知识进行总结,加深理解,二来也可以供大家参考。 ### 1.3 基础裸机代码 这里介绍下我使用的正点原子提供的裸机代码的目录结构,之后会在此基础上面进行更新: ~~~c minilinux ├── bsp │   ├── beep │   ├── clk │   ├── delay │   ├── epittimer │   ├── exit │   ├── gpio │   ├── int │   ├── key │   ├── keyfilter │   ├── led │   └── uart ├── imx6ul ├── obj ├── project └── stdio ├── include └── lib ~~~ 1. bsp:芯片的时钟、中断初始化以及各种外设的驱动等 2. imx6ul:芯片的寄存器定义头文件 3. obj:存放编译后产生的目标文件 4. project:启动代码,具体如下: ~~~c project ├── main.c └── start.S ~~~ 5. stdio:提供printf等函数的实现代码 补充一句,工程名就叫minilinux吧! ### 1.4 后续更新的代码 后续我会在gitee上面进行更新,地址如下: https://gitee.com/asymptotee/minilinux ## 2.从进程开始 进程是操作系统的核心,也是联系其他子系统的纽带。即使有Linux这个巨人,想要在代码层面从零实现进程管理并不简单。 那就先从最简单的进程切换开始,哦不对,应该是函数的切换。 ### 2.1 在函数function1与function2之间连续切换 现在有如下两个函数: ```c /* * minilinux/project/main.c */ void function1(void) { printf("enter %s\r\n", __func__); while (1) { printf("this is %s\r\n", __func__); delayms(200); } } void function2(void) { printf("enter %s\r\n", __func__); while (1) { printf("this is %s\r\n", __func__); delayms(200); } } ``` 想要实现在两个函数之间连续切换,可以怎么做? 答:在function1中调用function2,在function2中调用function1 嗯....可以,但是似乎有点呆,并且不具备通用性。 还可以使用汇编语言: ~~~c /* * minilinux/project/start.S */ .global __switch_to .type __switch_to function __switch_to: mov pc, r0 ~~~ 这里将__switch_to声明为全局符号以及类型为function,具体的汇编代码只有一句mov pc, r0,这样可以在c代码中这样调用: ~~~c __switch_to(function1); ~~~ 因为第一个参数function1根据c与汇编的调用规则默认会传递给r0寄存器,而mov pc, r0的作用就是将r0的值给到pc寄存器,这样cpu可以直接跳转到pc,也就是function1的地址处,该地址也即是function1函数的起始地址。 修改后的main.c代码如下: ~~~c /* * minilinux/project/main.c */ void function1(void) { printf("enter %s\r\n", __func__); while (1) { printf("this is %s\r\n", __func__); delayms(200); __switch_to(function2); } } void function2(void) { printf("enter %s\r\n", __func__); while (1) { printf("this is %s\r\n", __func__); delayms(200); __switch_to(function1); } } int main(void) { int_init(); /* 初始化中断(一定要最先调用!) */ imx6u_clkinit(); /* 初始化系统时钟 */ delay_init(); /* 初始化延时 */ clk_enable(); /* 使能所有的时钟 */ led_init(); /* 初始化led */ beep_init(); /* 初始化beep */ uart_init(); /* 初始化串口,波特率115200 */ __switch_to(function1); return 0; } ~~~ 这样的话,便可以实现在两个函数之间连续切换,并且具备很好的通用性,因为__switch_to可以跳转到有效范围内任意给定的参数的位置。 执行情况如下: ~~~c enter function1 this is function1 enter function2 this is function2 enter function1 this is function1 enter function2 this is function2 ...... ~~~ 嗯,看起来不错。 ### 2.2 记录上次切换时的位置 仔细想想便可以发现虽然可以切换,但是每次切换后都会从函数的起始处开始执行,并不会在上次切换的位置处继续执行,这从打印的信息中也可以很容易发现。 这就已经可以借鉴Linux的代码了,也就是大名鼎鼎的__switch_to。这里我已经把和此处功能实现无关的代码删除,只留下了最核心的: ~~~c /* * linux/arch/arm/kernel/entry-armv.S */ /* * Register switch for ARMv3 and ARMv4 processors * r0 = previous task_struct, r1 = previous thread_info, r2 = next thread_info * previous and next are guaranteed not to be the same. */ ENTRY(__switch_to) add ip, r1, #TI_CPU_SAVE stmia ip!, {r4 - sl, fp, sp, lr} @ Store most regs on stack add r4, r2, #TI_CPU_SAVE ldmia r4, {r4 - sl, fp, sp, pc} @ Load all regs saved previously ENDPROC(__switch_to) ~~~ 对于arm架构的cpu,想要记住代码执行的位置,需要将切换时一些必要的cpu寄存器记录下来,当需要切换回来的时候再恢复到相应的cpu寄存器即可。 这样的话,针对每一个函数都需要一个结构体来将这些寄存器的值记录下来,在Linux中的arm架构下该结构体为: ~~~c /* * linux/arch/arm/include/asm/thread_info.h */ struct cpu_context_save { __u32 r4; __u32 r5; __u32 r6; __u32 r7; __u32 r8; __u32 r9; __u32 sl; __u32 fp; __u32 sp; __u32 pc; __u32 extra[2]; /* Xscale 'acc' register, etc */ }; ~~~ 这里便可以引入著名的task_struct结构体,众所周知,该结构体用来记录一个进程的所有信息。但是cpu_context_save并不是直接内嵌在task_struct中,还有一个thread_info结构体: ~~~c /* * linux/arch/arm/include/asm/thread_info.h */ struct thread_info { struct cpu_context_save cpu_context; /* cpu context */ }; /* * linux/include/linux/sched.h */ struct task_struct { struct thread_info thread_info; }; ~~~ 这里,关于这两个结构体我都只截取了目前用到的内容,后面会随着内容的增加进行添加。 关于__switch_to还有几点需要注意:__ 1. ENTRY以及ENDPROC的实现见linux/include/linux/linkage.h,目前可以直接写出 2. 从注释可以看到__switch_to有三个参数,这里r0,也就是第一个参数目前用不到 3. TI_CPU_SAVE定义如下: ~~~c /* * linux/arch/arm/kernel/asm-offsets.c */ DEFINE(TI_CPU_SAVE, offsetof(struct thread_info, cpu_context)); ~~~ 也就是cpu_context结构体在thread_info结构体的偏移,由于目前thread_info中只有cpu_context一个元素,所以可以设置为0即可。 综上,修改后的__switch_to如下: ~~~ c /* * linux/arch/arm/kernel/entry-armv.S */ /* * Register switch for ARMv3 and ARMv4 processors * r0 = previous task_struct, r1 = previous thread_info, r2 = next thread_info * previous and next are guaranteed not to be the same. */ .global __switch_to .type __switch_to function __switch_to: add ip, r0, #0 stmia ip!, {r4 - sl, fp, sp, lr} @ Store most regs on stack add r4, r1, #0 ldmia r4, {r4 - sl, fp, sp, pc} @ Load all regs saved previously ~~~ 最后,main.c代码如下: ~~~c /* * minilinux/project/main.c */ struct task_struct task1; struct task_struct task2; void function1(void); void function2(void); void function1(void) { printf("enter %s\r\n", __func__); while (1) { printf("this is %s\r\n", __func__); delayms(200); printf("%s(): before __switch_to\r\n", __func__); __switch_to(&task1, &task2); printf("%s(): after __switch_to\r\n", __func__); } } void function2(void) { printf("enter %s\r\n", __func__); while (1) { printf("this is %s\r\n", __func__); delayms(200); printf("%s(): before __switch_to\r\n", __func__); __switch_to(&task2, &task1); printf("%s(): after __switch_to\r\n", __func__); } } int main(void) { int_init(); /* 初始化中断(一定要最先调用!) */ imx6u_clkinit(); /* 初始化系统时钟 */ delay_init(); /* 初始化延时 */ clk_enable(); /* 使能所有的时钟 */ led_init(); /* 初始化led */ beep_init(); /* 初始化beep */ uart_init(); /* 初始化串口,波特率115200 */ task1.thread_info.cpu_context.sp = 0x90000000; task2.thread_info.cpu_context.sp = 0x91000000; task1.thread_info.cpu_context.pc = function1; task2.thread_info.cpu_context.pc = function2; function1(); return 0; } ~~~ 需要注意的点: 1. 实例化两个task_struct结构体全局变量task1以及task2,并在main中对sp,也就是栈指针分别进行初始化;对pc,也就是cpu的跳转地址分别进行初始化 2. 在function1中调用__switch_to(&task1, &task2),保存当前function1的执行位置到task1中的cpu_context,并恢复task2的cpu_context到cpu的寄存器,完成跳转 3. 在function2中调用__switch_to(&task2, &task1),保存当前function2的执行位置到task2中的cpu_context,并恢复task1的cpu_context到cpu的寄存器,完成跳转 4. 由于修改__switch_to,所以将main中的__switch_to(function1)改为直接调用function1() 修改后执行情况如下: ~~~c enter function1 this is function1 function1(): before __switch_to enter function2 this is function2 function2(): before __switch_to function1(): after __switch_to this is function1 function1(): before __switch_to function2(): after __switch_to this is function2 function2(): before __switch_to function1(): after __switch_to ...... ~~~ 可以看到,已经能记录到上次切换时执行的位置: 1. “enter function1”、“enter function2”分别只打印了一次 2. 以function1的切换过程为例,当function1在切换到function2又切换回来后,打印信息为“function1(): after __switch_to”,也就是上次切换时的下一条指令的位置处 居然能在切换后在原位置继续执行,有点意思,不过这才是千里之行的第一步。但是缺点也很明显,需要显式的调用__switch_to来进行切换动作,可不可以借助一个外力来执行切换,而对于function们是无感的呢? 欲知后事如何,且听下回分解。 ### 2.3 定时器中断来推动切换 没错,可以使用定时器中断来推动切换动作。 但是具体怎么做呢? 大概思路是:在中断中调用__switch_to,来替换在function中的调用。 所以接下来的重点是对中断的改造。 #### 2.3.1 裸机下的中断处理流程 一般的arm架构下的裸机中断入口汇编代码是这样的: ~~~assembly IRQ_Handler: /* 保存现场 */ /* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 */ /* lr-4是异常处理完后的返回地址, 也要保存 */ sub lr, lr, #4 stmdb sp!, {r0-r12, lr} /* 处理irq异常 */ bl system_irqhandler /* 恢复现场 */ ldmia sp!, {r0-r12, pc}^ /* ^会把spsr_irq的值恢复到cpsr里 */ ~~~ > 正点原子的中断入口代码我感觉不是太好,所以此处参考了百问网的代码 1. 由于arm指令流水的原因,需要对lr寄存器进行自减4 2. 如注释所说,在irq异常处理函数中有可能会修改r0-r12,所以需要在调用中断处理函数system_irqhandler之前将这些值保存到irq模式下的栈内存中,然后执行system_irqhandler,最后再恢复即可(**仔细想想,这其实和__switch_to的思想都是相同的:先保护,再恢复**)。 3. 需要注意的是,r0-r12其实如果后面不关注这些值是可以不用保存的,但是lr是必须要保存的,否则就不能返回到中断前的位置处继续执行 #### 2.3.2 如何修改中断处理流程 怎么改造? 将类似下面的代码加入到中断处理函数system_irqhandler中? ~~~c void system_irqhandler(void) { ...... static unsigned char state = 1; state = !state; if (state) { __switch_to(&task2, &task1); } else { __switch_to(&task1, &task2); } ...... } ~~~ 答案是显然不行: 1. 首先要清楚的是,现在有两个完全不同的程序执行流: a. 由task1记录的funtion1函数的执行流 b. 由task2记录的funtion2函数的执行流 假如目前正在funtion1中执行,突然定时器中断到来,进入了IRQ_Handler的中断入口汇编代码,要想保证中断处理完成后还能返回到原位置,必须保存相应的cpu寄存器,这对于在funtion2中被中断打断后的返回要求亦是如此。 所以,需要对每一个不同的执行流都需要保存发生中断的现场,而当前的中断入口汇编代码显然是办不到的,因为此处的代码只保存了某一个执行流的现场。 2. 由于中断的到来对于每个函数执行流是不可预测的(虽然此处是定时器中断,中断是周期性的,但是对于不断变动的代码依旧是不可预测的),所以需要知道当中断到来时的的函数执行流是哪一个,依此来保存切换时的现场以及中断时的现场。 所以总结下来,需要改造的具体有以下两点: 1. **需要针对每一个函数执行流,分别保存发生中断时的现场** 2. **需要知道当前的函数执行流是哪一个** #### 2.3.3 在Linux中是如何做的 我们来看看在Linux中以上问题是怎么解决的。 下面是我画的一个对Linux进程内核栈的描述图: ![image-20221127231233878](readme_pic/image-20221127231233878.png) 所谓进程内核栈,其实是在内存上的一小部分区域,这个内存区域保存着当前进程所需要的必不可少的信息。 具体的,对上图的理解可以依据不同的颜色进行: 1. 紫色部分:保存着当前进程的struct task_struct的内容,目前只有struct thread_info,其他的内容在后续会补充进去。而struct thread_info目前也只有struct cpu_context_save,相同的,其他的内容在后续会补充进去。 2. 蓝色部分:保存着当前进程的函数栈,具体用来保存函数的局部变量 3. 橙色部分:保存着当前进程的中断现场,以struct pt_regs来描述 所以答案便是: 1. 针对问题1,每个进程使用橙色部分所示的struct pt_regs来保存自己的中断现场 2. 针对问题2,Linux使用一个称为current的宏定义来表示当前进程的struct task_struct,具体定义如下: ```c #define THREAD_SIZE 4096 /* * how to get the current stack pointer in C */ register unsigned long current_stack_pointer asm ("sp"); /* * how to get the thread information struct from C */ static struct thread_info *current_thread_info(void) { return (struct thread_info *) (current_stack_pointer & ~(THREAD_SIZE - 1)); } #define get_current() (current_thread_info()->task) #define current get_current() ``` 1. 通过内联汇编**register unsigned long current_stack_pointer asm ("sp")**实现在c语言中获取当前的sp寄存器的值 2. 知道当前的sp寄存器的值后,利用**(current_stack_pointer & ~(THREAD_SIZE - 1))**获取到当前进程内核栈的栈底地址,此地址也即当前进程的struct task_struct的起始地址 在写出修改后的代码之前,先用一张图来理清下整个思路: ![image-20221203214632519](readme_pic/image-20221203214632519.png) 大致执行流程: 1. 首先执行function1函数,在function1中执行了一小段时间后,定时器中断到来,进入中断处理流程 2. 首先将中断现场保存到function1的进程内核栈的struct pt_regs中,然后执行中断处理函数(此处为定时器中断处理函数,之后如果加入其他中断,则会进入相应的处理函数),接着保存function1当前的执行位置到function1的进程内核栈的struct cpu_context_save,最后将function2的进程内核栈的struct cpu_context_save恢复到cpu寄存器中,这样会导致切换到function2函数执行 3. 在function2中执行了一小段时间后,定时器中断再次到来,再次进入中断处理流程 4. 和步骤2相似,首先将中断现场保存到function2的进程内核栈的struct pt_regs中,然后执行中断处理函数(此处为定时器中断处理函数,之后如果加入其他中断,则会进入相应的处理函数),接着保存function2当前的执行位置到function2的进程内核栈的struct cpu_context_save,最后将步骤2中保存的function1的进程内核栈的struct cpu_context_save恢复到cpu寄存器中,这样会导致切换到function1上次执行切换动作的地方 5. 代码继续往下走,也即恢复被中断的function1的中断现场,这样便回到了在步骤1中function1被中断的地方继续执行 6. 在function1中执行了一小段时间后,定时器中断再次到来,再次进入中断处理流程 7. 之后的流程便是不断地重复上述的步骤,不再赘述 #### 2.3.4 最终的修改后的代码 - 中断处理流程代码: ~~~assembly IRQ_Handler: /* * Interrupt dispatcher */ vector_stub irq, IRQ_MODE, 4 .long __irq_usr @ 0 (USR_26 / USR_32) .long __irq_invalid @ 1 (FIQ_26 / FIQ_32) .long __irq_invalid @ 2 (IRQ_26 / IRQ_32) .long __irq_svc @ 3 (SVC_26 / SVC_32) .long __irq_invalid @ 4 .long __irq_invalid @ 5 .long __irq_invalid @ 6 .long __irq_invalid @ 7 .long __irq_invalid @ 8 .long __irq_invalid @ 9 .long __irq_invalid @ a .long __irq_invalid @ b .long __irq_invalid @ c .long __irq_invalid @ d .long __irq_invalid @ e .long __irq_invalid @ f __irq_svc: svc_entry irq_handler /* 使能IRQ中断 */ mrs r0, cpsr /* 读取cpsr寄存器值到r0中 */ bic r0, r0, #0x80 /* 将r0寄存器中bit7清零,也就是CPSR中的I位清零,表示允许IRQ中断 */ msr cpsr, r0 /* 将r0重新写入到cpsr中 */ bl do_work_pending svc_exit r5, irq = 1 @ return from exception __irq_usr: b __irq_usr __irq_invalid: b __irq_invalid /* * macro svc_entry */ .macro svc_entry, stack_hole=0, trace=1, uaccess=1 sub sp, sp, #(SVC_REGS_SIZE + \stack_hole - 4) stmia sp, {r1 - r12} ldmia r0, {r3 - r5} add r7, sp, #S_SP - 4 @ here for interlock avoidance mov r6, #-1 @ "" "" "" "" add r2, sp, #(SVC_REGS_SIZE + \stack_hole - 4) str r3, [sp, #-4]! @ save the "real" r0 copied @ from the exception stack mov r3, lr @ @ We are now ready to fill in the remaining blanks on the stack: @ @ r2 - sp_svc @ r3 - lr_svc @ r4 - lr_, already fixed up for correct return/restart @ r5 - spsr_ @ r6 - orig_r0 (see pt_regs definition in ptrace.h) @ stmia r7, {r2 - r6} .endm /* * macro irq_handler */ /* * Interrupt handling. */ .macro irq_handler bl system_irqhandler .endm /* * macro svc_exit */ .macro svc_exit, rpsr, irq = 0 @ ARM mode SVC restore msr spsr_cxsf, \rpsr ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr .endm /* * macro vector_stub */ .macro vector_stub, name, mode, correction=0 .align 5 vector_\name: .if \correction sub lr, lr, #\correction .endif @ @ Save r0, lr_ (parent PC) and spsr_ @ (parent CPSR) @ stmia sp, {r0, lr} @ save r0, lr mrs lr, spsr str lr, [sp, #8] @ save spsr @ @ Prepare for SVC32 mode. IRQs remain disabled. @ mrs r0, cpsr eor r0, r0, #(IRQ_MODE ^ SVC_MODE) msr spsr_cxsf, r0 @ @ the branch table must immediately follow this code @ and lr, lr, #0x0f mov r0, sp ldr lr, [pc, lr, lsl #2] movs pc, lr @ branch to handler in SVC mode .align 2 @ handler addresses follow this label 1: .endm ~~~ 1. 汇编代码的逻辑,可参考蜗窝的文章:http://www.wowotech.net/irq_subsystem/irq_handler.html,我当时就是参考的这篇,讲的比较详细,强烈推荐。 2. system_irqhandler作为通用的中断处理函数,就专门用来执行具体的中断处理,另外加一个do_work_pending函数,用来执行切换操作,具体代码如下: ~~~c void do_work_pending(void) { static unsigned char state = 1; state = !state; printf("============ this is %s, state = %d ============\r\n", __func__, state); if (state) { __switch_to(current, task1); } else { __switch_to(current, task2); } } ~~~ - function中的__switch_to去掉,具体如下,可以看到已经没有切换相关的代码,均是函数自己的逻辑代码: ~~~c void function1(void) { printf("################### enter %s ###################\r\n", __func__); while (1) { printf("++++++++ this is %s , step 1 ++++++++\r\n", __func__); delayms(200); printf("++++++++ this is %s , step 2 ++++++++\r\n", __func__); delayms(200); printf("++++++++ this is %s , step 3 ++++++++\r\n", __func__); } } void function2(void) { printf("################### enter %s ###################\r\n", __func__); while (1) { printf("++++++++ this is %s , step 1 ++++++++\r\n", __func__); delayms(200); printf("++++++++ this is %s , step 2 ++++++++\r\n", __func__); delayms(200); printf("++++++++ this is %s , step 3 ++++++++\r\n", __func__); } } ~~~ - main函数 ~~~ c struct task_struct *task1 = 0x90000000; struct task_struct *task2 = 0x91000000; int main(void) { struct task_struct *unused_task; int_init(); /* 初始化中断(一定要最先调用!) */ imx6u_clkinit(); /* 初始化系统时钟 */ delay_init(); /* 初始化延时 */ clk_enable(); /* 使能所有的时钟 */ led_init(); /* 初始化led */ beep_init(); /* 初始化beep */ uart_init(); /* 初始化串口,波特率115200 */ epit1_init(0, 66000000 / 2); /* 初始化EPIT1定时器,1分频,计数值为:66000000/2,也就是定时周期为500ms */ task1->thread_info.cpu_context.sp = (unsigned int)task1 + THREAD_SIZE; task2->thread_info.cpu_context.sp = (unsigned int)task2 + THREAD_SIZE; task1->thread_info.cpu_context.pc = function1; task2->thread_info.cpu_context.pc = function2; task1->thread_info.task = task1; task2->thread_info.task = task2; __switch_to(&unused_task, task1); return 0; } ~~~ 1. task1和task2分别赋值一个确定的内存可用地址0x90000000和0x91000000,对应两个结构体实例的起始地址,也即2.3.3中进程内核栈图中的紫色部分高内存地址的起始地址 2. task1和task2的thread_info.cpu_context.sp分别赋值为task1和task2的起始地址+THREAD_SIZE,也即2.3.3中进程内核栈图中的橙色部分的最高内存地址,也是整个进程内核栈的最高内存地址。为什么是THREAD_SIZE呢?其实在合适的基础上,只要和current_thread_info函数中的THREAD_SIZE保持一致即可,这样current就能通过当前的sp地址找到当前运行进程的struct task_struct的起始地址 最后,执行结果如下: ~~~c ################### enter function1 ################### ++++++++ this is function1 , step 1 ++++++++ ++++++++ this is function1 , step 2 ++++++++ ++++++++ this is function1 , step 3 ++++++++ ++++++++ this is function1 , step 1 ++++++++ ============ this is do_work_pending, state = 0 ============ ################### enter function2 ################### ++++++++ this is function2 , step 1 ++++++++ ++++++++ this is function2 , step 2 ++++++++ ++++++++ this is function2 , step 3 ++++++++ ++++++++ this is function2 , step 1 ++++++++ ============ this is do_work_pending, state = 1 ============ ++++++++ this is function1 , step 2 ++++++++ ++++++++ this is function1 , step 3 ++++++++ ++++++++ this is function1 , step 1 ++++++++ ============ this is do_work_pending, state = 0 ============ ++++++++ this is function2 , step 2 ++++++++ ++++++++ this is function2 , step 3 ++++++++ ++++++++ this is function2 , step 1 ++++++++ ============ this is do_work_pending, state = 1 ============ ++++++++ this is function1 , step 2 ++++++++ ++++++++ this is function1 , step 3 ++++++++ ++++++++ this is function1 , step 1 ++++++++ ++++++++ this is function1 , step 2 ++++++++ ============ this is do_work_pending, state = 0 ============ ++++++++ this is function2 , step 2 ++++++++ ++++++++ this is function2 , step 3 ++++++++ ++++++++ this is function2 , step 1 ++++++++ ++++++++ this is function2 , step 2 ++++++++ ...... ~~~ 可以看到,结果完全符合预期,只不过相比之前,切换动作改由定时器中断负责,对于具体的function是完全无感的。 ### 2.4 简易版do_fork() 众所周知,do_fork()是系统调用fork()的内核态底层实现,有了前面的基础,是时候来实现这个函数了。 函数代码如下: ~~~c int do_fork(void (*fn)(void)) { struct task_struct *tsk; /* 第一部分 */ tsk = alloc_task_struct_node(); /* 第二部分 */ tsk->thread_info.cpu_context.pc = (__u32)fn; tsk->thread_info.cpu_context.sp = ((__u32)(tsk) + THREAD_SIZE); tsk->thread_info.task = tsk; /* 第三部分 */ tsk->counter = 15; tsk->priority = 15; tsk->state = TASK_RUNNING; /* 第四部分 */ list_add(&tsk->tasks, &task_head); return 0; } ~~~ 该函数可分为四部分来说明: 1. 为新fork出的进程分配一个struct task_struct地址,具体代码如下: ~~~c static struct task_struct *alloc_task_struct_node(void) { static int task_stack_addr_base = 0x90000000; struct task_struct *tsk = (struct task_struct *)task_stack_addr_base; task_stack_addr_base += THREAD_SIZE; return tsk; } ~~~ 2. 为新fork出的task的部分成员赋初值 3. 新添加的struct task_struct的成员,主要和接下来讲的schedule()有关,所以移到下一节进行说明 4. 将新增加的task加入到一个以task_head为链表头的全局链表中(关于Linux中著名的链表结构struct list_head此处不再进行介绍) 其中,第一、二部分替换掉了之前的main.c中的以下部分: ~~~c struct task_struct *task1 = 0x90000000; struct task_struct *task2 = 0x91000000; int main(void) { ...... task1->thread_info.cpu_context.sp = (unsigned int)task1 + THREAD_SIZE; task2->thread_info.cpu_context.sp = (unsigned int)task2 + THREAD_SIZE; task1->thread_info.cpu_context.pc = function1; task2->thread_info.cpu_context.pc = function2; task1->thread_info.task = task1; task2->thread_info.task = task2; ...... return 0; } ~~~ 可以看到,之前的代码每当新增加一个进程便需要重新赋一遍初值,在do_fork()中对其进行了抽象化。 目前的实现可能起名为简易版do_exec()更合适,因为并没有实现真正的fork部分,而是直接进行赋新值。因为是简易版嘛,真正的fork功能之后会实现的,此处先挂个名。 ### 2.5 简易版schedule() schedule()是Linux中大名鼎鼎的负责执行进程调度的函数,有了上一节中do_fork()的基础,便可以着手实现简易版的schedule()了。 函数代码如下: ~~~c void schedule(void) { struct task_struct *not_used_task; struct task_struct *next_task; next_task = pick_next_task(); switch_to(current, next_task, not_used_task); } ~~~