操作系统真象还原_线程调度原理

重要概念

学习操作系统或是学习计算机编程得明白四个很重要的概念:

  1. CPU中的控制器无处不在,尽管程序中从来没有出现过控制器,但每条指令都是控制器处理的。
  2. 内存是一个大仓库,编译后代码放在一边,由CPU执行代码产生的中间结果和最终结果放在另一边。代码主要是ELF文件包括代码段数据段;中间结果主要是堆栈。
  3. CPU控制器只会到寄存器eip指向的内存地址处取指令。
  4. CPU执行压栈指令时只会压到寄存器esp指向的地址处,执行出栈指令只会从esp指向的地址处取数据。

这一节说明:

  这一节找错改错经历了差不多一个星期,期间复习了线程进程的创建的整个过程,之前的已经忘得差不多了。线程的创建与如何运行搞了三四天才从新搞明白,明白了程序控制块PCB结构,画了结构图搞了很长时间,后来又发现不对,然后在thread.h文件中又从新用文字画了一遍。
  搞明白了intr_stack中ss字段在pcb页的最高地址,PCB_struct中的字段*self_kstack在pcb页的最低地址,结构体intr_stack、thread_stack、PCB_struct在pcb页的中都是倒序排列的。字段*self_kstack是个指针,指向thread_stack中的ebp字段,pcb页ebp字段到stack_magic字段中间是为栈预留的空白的区域。
  然后弄明白了线程创建到调度的过程,线程和进程的创建与调度大体一致又有所不同。当我弄明白线程的创建与
调度之后非常感慨,设计的如此巧妙真想认识一下设计的人。下面是我从schedule()函数复制过来的说明注释。

thread\thread.h

/*********** 中断栈intr_stack ***********/
struct intr_stack
{
uint32_t vec_no; // kernel.S 宏VECTOR中push %1压入的中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy; // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
/* 以下由cpu从低特权级进入高特权级时压入 */
uint32_t err_code; // err_code会被压入在eip之后
void (*eip)(void);
uint32_t cs;
uint32_t eflags;
void *esp;
uint32_t ss;
};
// 进程使用的栈也属于PCB的一部分,不过此栈是进程所使用的0特权级下内核栈(并不是3特权级下的用户栈)。
struct thread_stack
{
uint32_t ebp; // 0-3
uint32_t ebx; // 4-7
uint32_t edi; // 8-11
uint32_t esi; // 12-15
// 线程第一次执行时,eip指向待调用的函数kernel_thread,其它时候eip是指向switch_to的返回地址
void (*eip)(thread_func *func, void *func_arg); // 16-19
/***** 以下仅供第一次被调度上cpu时使用 ****/
/* 参数unused_retaddr只为占位置充数为返回地址 */
void(*unused_retaddr); //充当返回地址,在返回地址所在的栈帧占个位置
thread_func *function; // 由函数Kernel_thread所调用的函数名
void *func_arg; // 由函数Kernel_thread所调用的函数所需的参数
};
/* 定义的PCB,进程或线程的pcb,程序控制块Process Control Block */
struct PCB_struct
{
uint32_t *self_kstack; // 线程的内核栈顶指针,指向thread_stack结构体
pid_t pid;
// 用于记录线程状态,其类型便是前面定义的枚举结构enum task_status。
enum task_status status;
// 用于记录任务(线程或进程)的名字,长度是16,即任务名最长不过16个字符。
char name[TASK_NAME_LEN];
uint8_t priority;
uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数,每次时钟中断都会将当前任务的ticks减1
//用于记录任务在处理器上运行的时钟嘀嗒数,从开始执行,到运行结束所经历的总时钟数。
uint32_t elapsed_ticks;
// general_tag的作用是用于线程在一般的队列中的结点
struct list_elem general_tag;
// all_list_tag的作用是用于线程队列thread_all_list中的结点
struct list_elem all_list_tag;
// 用于存放进程页目录表的虚拟地址,这将在为进程创建页表时为其赋值。如果该任务为线程,pgdir则为NULL
uint32_t *pgdir;
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
// struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
// int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 已打开文件数组
// uint32_t cwd_inode_nr; // 进程所在的工作目录的inode编号
// pid_t parent_pid; // 父进程pid
// int8_t exit_status; // 进程结束时自己调用exit传入的参数
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};
PCB页中的栈内存顺序        

 

三个线程的调度

  为了简单说明问题只创建有3个线程M、A、B。其中M是主线程也就是调度模块所在的线程,调度模块还未初始化时就已存在。线程调用顺序是M -> A -> B -> M采用顺寻调度模式,其中A、B线程为第一次调用。

  调度模块共四个函数,调用顺序是:intr32entry -> intr_timer_handler() -> schedule() -> switch_to()。函数intr32entry在数据段.data中,是中断入口函数用于保存当前线程上下文环境。

  1. 当发生时钟中断时,A将第一次执行。cpu把中断号0x20作为下标到中断门描述符数组idt中找到对应的元素(元素中包含地址),获取高16位地址和低16位地址,将地址合成为一个32为地址,然后跳到该地址指向的内存处执行。这个地址就是中断入口函数intr32entry的起始地址。
section .data
global intr_entry_table
intr_entry_table:
section .text
intr32entry:
push 0 ; %2代表nop或push 0,中断若有错误码会压在eip后面
; 以下是保存上下文环境
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
;把0x20,即中断向量号压入栈中作为idt_table数组中某个元素所指向的中断处理程序的参数。
push 0x20 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
call [idt_table + 32 * 4] ; 对该地址通过中括号[]取值,调用idt_table中的C版本中断处理函数
jmp intr_exit
section .data
dd intr32entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
  1. 在函数intr32entry中保存上下文,之后esp便指向了thread_stack中的func_arg字段,为什么寄存器esp指向func_arg字段?因为栈是向下增长的,压入esi、edi、vec_no之后就是func_arg字段了。又根据中断号0x20到中断处理程序数组idt_table中找到一个地址,该地址就是intr_timer_handler()函数的地址。然后调用intr_timer_handler()函数。
  2. 在调用intr_timer_handler()之前,call指令会将其下方的指令jmp intr_exit的地址压入thread_stack中的func_arg字段中,也就是此时寄存器esp指向栈顶,栈顶存放的是指令jmp intr_exit的地址。
  3. 在intr_timer_handler()函数中调用schedule(),在schedule()中调用switch_to(cur, next)时栈还会增长。这三个函数都是属于中断处理程序。注意与上图不同,此时eip字段的值不是kernel_thread()函数的地址,因为函数intr32entry中,执行call [idt_table + %1 * 4]时call指令就将其自身下一条指令jmp intr_exit的地址压入到了字段func_arg处,之后调用intr_timer_handler()函数再调用switch_to()函数还要往栈中压入数据,会将下面的function, unused_retaddr, eip, esi .....等字段全部覆盖掉。

thread\switch.S

switch_to:
push esi
push edi
push ebx
push ebp ; 栈顶指针,此时ebp的值是schedule()函数栈帧的ebp吗?是的
mov eax, [esp + 20]
mov [eax], esp ;将当前栈顶指针esp保存到当前线程cur的PCB中的self_kstack成员中
;------------------ 以上是备份当前线程cur的环境,下面是恢复下一个线程next的环境 ----------------
;获取栈中的next的值,也就是next线程的PCB地址
mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24]
mov esp, [eax] ;将next线程的栈指针恢复到esp中
pop ebp ; 每pop一次esp值加4
pop ebx
pop edi
pop esi
ret ;ret指令将寄存器esp中的地址当中的值送上eip执行

thread\thread.c

/* 实现任务调度 */
void schedule()
{
ASSERT(intr_get_status() == INTR_OFF);
// 通过running_thread()获取了当前运行线程的PCB,将其存入PCB指针cur中
struct PCB_struct *cur = running_thread();
// 若此线程只是cpu时间片到了,将其加入到就绪队列尾
if (cur->status == TASK_RUNNING)
{
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
cur->status = TASK_READY;
}else{} // 若此线程需要某事件发生后才能继续上cpu运行,不需要将其加入队列,因为当前线程不在就绪队列中。
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag清空
// 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu.
thread_tag = list_pop(&thread_ready_list);
// 要获得线程的信息,必须将其转换成PCB才行,因此用到了宏elem2entry,通过elem2entry获得新线程的PCB地址
struct PCB_struct *next = elem2entry(struct PCB_struct, general_tag, thread_tag);
next->status = TASK_RUNNING;
// 击活任务页表等
process_activate(next);
switch_to(cur, next);
}
  1. 调用switch_to()

    1. 在schedule()调用switch_to(cur, next)进入到switch_to()函数中时esp指向M线程的PCB页中的栈顶。根据ABI规则执行完push esi, push edi, push ebx, push ebp之后执行mov eax, [esp + 20]指令得到栈中的参数cur,[esp + 20]的位置是形参cur所在的位置。该地址指向cur线程的pcb页的地址也就是页的最低地址,也就是字段self_kstack所在的位置。指令mov [eax], esp将当前M线程栈顶指针esp保存到当前线程cur(也就是M线程)的PCB中的self_kstack字段中。[esp + 16]的位置是返回地址,[esp + 24]的位置是switch_to(cur, next)中的next参数的值。

    2. 然后执行mov eax, [esp + 24]和mov esp, [eax]指令将esi的上方8字节的位置也就是参数next的位置的值放入寄存器esp中。A线程的PCB页的地址放入寄存器eax中,因为PCB页的第一个字段是self_kstack,而self_kstack字段是个指针存放的是A线程的PCB页中thread_stack的esp字段的地址,所以执行mov esp, [eax]指令将字段esp的地址存入寄存器esp中。至此寄存器esp指向A线程的PCB页中的栈thread_stack的esp字段。

  2. 然后执行一系列pop之后esp指向eip字段,此时eip字段的值是kernel_thread()函数的地址,然后执行ret指令,ret指令会将esp指向的值弹出到eip中,然后cpu就跳到kernel_thread()函数所在的地址。在kernel_thread()函数中cpu会从esp+4处获取kernel_thread(thread_func *function, void *func_arg)函数的function的值,也就是A线程的PCB页中thread_stack的thread_func *function字段的值。从esp+8处获取func_arg参数的值,也就是A线程的PCB页中thread_stack的void *func_arg字段的值。

  3. 之后调用function()函数。

  4. 当A线程的时间片到了以后,B线程将会第一次执行,过程如上。

  5. 当B线程时间片到了,cpu根据中断号0x20到中断门描述符数组中找到对应的元素(元素中包含地址),获取高16位地址和低16位地址,将地址合成为一个32为地址,然后跳到该地址指向的内存处执行。这个地址就是函数intr32entry的起始地址。

  6. 在函数intr32entry中保存上下文,之后esp便指向thread_stack中的func_arg字段,之后又根据中断号0x20到中断处理
    程序数组idt_table中找到一个地址,该地址就是intr_timer_handler()函数的地址。然后intr_timer_handler()函数。

  7. 在调用intr_timer_handler()之前,call指令会将其下方的指令jmp intr_exit的地址压入thread_stack中的func_arg字
    段中,也就是此时寄存器esp指向栈顶,栈顶存放的是指令jmp intr_exit的地址。

  8. 然后在intr_timer_handler()函数中调用schedule(),在schedule()调用switch_to(cur, next)栈还会增长,也就是esp还会变化。这三个函数都是属于中断处理程序。

  9. 执行到switch_to()中时寄存器esp指向B线程的PCB页中的栈顶,执行push esi, push edi, push ebx, push ebp之后将esp的值存入B线程的PCB页存放esi的上方4字节的位置,也就是参数cur的位置。然后将esi的上方8字节的位置也就是参数next的位置的值也就是M线程的PCB页的地址放入寄存器esp中,因为M线程的PCB页的第一个字段是self_kstack所以存入esp中的地址也是self_kstack字段的地址,而self_kstack字段是个指针存放的是M线程的PCB页中thread_stack的栈顶的地址。接着执行mov esp, [eax]指令,esp就指向A线程的PCB页中thread_stack的栈顶的地址了。

  10. 恢复M线程的执行

    1. 在switch_to中执行一系列pop之后esp指向M线程的PCB页中thread_stack的栈中的一个位置,该位置存放的是M线程将要恢复执行的指令的地址,也就是在schedule()函数中调用switch_to(cur, next)函数的下一条指令,该条指令返回到schedule()函数的上一级函数intr_timer_handler()中,intr_timer_handler()函数返回到函数intr32entry中。
    2. 与此同时esp指向的栈中的值是指令jmp intr_exit,随即cpu控制器将esp指向的地址赋给cs:ip,cpu执行jmp intr_exit指令进入到恢复M线程的intr_exit函数中,将M线程PCB中的中断栈intr_stack的值一一pop到对应的寄存器中,最后执行iretd指令恢复M线程的执行。
struct thread_stack
{
uint32_t ebp; // 0-3
uint32_t ebx; // 4-7
uint32_t edi; // 8-11
uint32_t esi; // 12-15
void (*eip)(thread_func *func, void *func_arg); // 16-19
void(*unused_retaddr); //充当返回地址,在返回地址所在的栈帧占个位置
thread_func *function; // 由函数Kernel_thread所调用的函数名
void *func_arg; // 由函数Kernel_thread所调用的函数所需的参数
};
// 在PCB中的栈thread_stack中的顺序
|—————————————————————————————————————————————————————————————————————————————————|
| void *func_arg; |\
|—————————————————————————————————————————————————————————————————————————————————| \
| thread_func *function; | \
|—————————————————————————————————————————————————————————————————————————————————| \
| void(*unused_retaddr); | \
|—————————————————————————————————————————————————————————————————————————————————| |
| void (*eip)(thread_func *func, void *func_arg); | thread_stack
|—————————————————————————————————————————————————————————————————————————————————| |
| uint32_t esi; | |
|—————————————————————————————————————————————————————————————————————————————————| /
| uint32_t edi; | /
|—————————————————————————————————————————————————————————————————————————————————| /
| uint32_t ebx; | /
|—————————————————————————————————————————————————————————————————————————————————| /
| uint32_t ebp; |/
|—————————————————————————————————————————————————————————————————————————————————|

总之,线程在第一次调用时就会将PCB中的线程栈thread_stack的字段覆盖掉了,已不是线程第一次运行前的样子了。

  1. 注意因为之前M线程已经执行过了,所以此时eip字段的值不是kernel_thread()函数的地址,在第一次调用时就覆盖了。当A B线程第一次执行时 M -> A,A -> B的过程中在switch_to(cur, next)函数中指令ret是将kernel_thread()函数的地址送上cpu执行,往后每次在switch_to(cur, next)函数中指令ret是将switch_to(cur, next)函数的返回地址送上cpu执行的,也就是在schedule()函数中call switch_to指令的下面一条指令的地址送上cs:ip。该指令也是一条返回指令所以返回到上一级函数intr_timer_handler()中,intr_timer_handler()函数返回到函数intr32entry中。因此AB线程的第一次调用和后面的每次调用是不同的。
section .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境
add esp, 4 ; 跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ; 跳过error_code
iretd
  1. 在函数intr_exit中执行add esp, 4跳过中断号让esp寄存器执行字段edi。将M线程PCB中的栈thread_stack中的内容恢复到各个寄存器中。
|—————————————————————————————————————————————————————————————————————————————————|
| uint32_t esi; |
|—————————————————————————————————————————————————————————————————————————————————|
| uint32_t edi; |
|—————————————————————————————————————————————————————————————————————————————————|
| uint32_t vec_no; |
|—————————————————————————————————————————————————————————————————————————————————|
| void *func_arg; |
|—————————————————————————————————————————————————————————————————————————————————|

至此M线程恢复执行。

问题

  这里有个问题,esp的值得改变并不会把PCB中PCB_struct中字段覆盖掉。因为PCB_struct在PCB页的最低端地址,intr_stack在PCB页的最高端地址,thread_stack在intr_stack下面。thread_stack与PCB_struct中间还有一大段内存区域,因此调度程序产生的栈增加不会覆盖掉PCB_struct所在的内存。除非调度程序很大,调度程序要调用的函数非常多往线程PCB的栈thread_stack中压入太多数据将魔数覆盖掉了。

总结

  1. 在intr32entry函数中要做的事情是把即将要中断的线程也就是M线程的上下文环境保存到M线程PCB的栈intr_stack中,然后调用调度器程序。intr32entry -> intr_timer_handler() -> schedule() -> switch_to()。
  2. 在switch_to()中做的事情是将调度程序的上下文环境保存到M线程PCB的栈thread_stack中。然后恢复A线程PCB的栈thread_stack中的上下文。

kernel.s文件的作用是保存和恢复线程的上下文环境,switch.S文件的作用使用保存和恢复调度程序的的上下文环境。

排错心得记录

一、

  复习了ELF文件的结构,加载应用文件的过程,以及此系统中内存地址是如何分配的。此系统中的虚拟地址是从0x08048000开始分配的,虚拟地址位图的起始地址是0x08048000,位图中的1位映射1页,每分配1页对应的位图中的位置为1。
  悲剧的是之前在测试的时候在sys_getcwd()函数中将一条语句sys_free()注释掉了,导致分配的0x08048000未能释放,于是在file_read()函数中调用sys_malloc()分配内存块所在的页是0x08049000。而应用程序需要复制4792个字节到起始地址0x08048000处,将前2页覆盖掉了,导致第2页0x08049000到0x08049fff释放的时候在sys_free()函数中报错。

二、

  问题到此差不多解决了,但运行的时候出现不断fork新线程的的情况。昨天9点多钟感觉太累了,上床睡觉到凌晨1点多起床5点多才睡期间无所事事都在看手机。今天突然想起来之前的compile.sh文件是直接从15_5节复制过来的,没有将该路径于是将这一节的应用程序写到了15_5节的disk.img硬盘中,15_5_4节(上一节)的disk.img硬盘中什么都没有,而15_5_4节main函数中是在disk80.img硬盘创建一个文件并将disk.img硬盘的第300个扇区中的内容复制到disk80.img硬盘的文件系统中。15_5_4节的disk.img中什么都没有,复制了个寂寞,当时找错找了好久。
  由于之前在15_5节中测试的时候,将disk80.img硬盘的文件系统中的prog_no_arg应用程序删掉了,之后又运行了一次。于是在disk80.img硬盘中的文件系统中创建了一个名为prog_no_arg的文件,却将prog_arg应用程序中的内容复制了进去。
  15_5_4节的disk80.img硬盘是从15_5节复制过来的,于是15_5_4节中的disk80.img中的文件系统中就有了一个名为prog_no_arg实为prog_arg的应用程序。
  15_5_4节程序设计的初衷是prog_arg将prog_no_arg作为参数,fork一个新线程作为prog_arg线程,原来的线程变成prog_no_arg线程。但是prog_no_arg线程中的内容实际上是prog_arg的内容。所以出现prog_arg将prog_no_arg作为参数,prog_no_arg又将prog_arg作为参数互相调用,所以出现了不断fork新线程的情况!(╥﹏╥) 真不容易!

/** 程序控制块PCB结构(4K大小)
****************************************************************************************
*
* |—————————————————————————————————————————————————————————————————————————————————|高地址
* | uint32_t ss; |\
* |—————————————————————————————————————————————————————————————————————————————————| \
* | void *esp; | \
* |—————————————————————————————————————————————————————————————————————————————————| \
* | uint32_t eflags; | \
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t cs; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | void (*eip)(void); | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t err_code; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t ds; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t es; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t fs; | |
* |—————————————————————————————————————————————————————————————————————————————————| intr_stack
* | uint32_t gs; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t eax; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t ecx; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t edx; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t ebx; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t esp_dummy; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t ebp; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t esi; | /
* |—————————————————————————————————————————————————————————————————————————————————| /
* | uint32_t edi; | /
* |—————————————————————————————————————————————————————————————————————————————————| /
* | uint32_t vec_no; |/
* |—————————————————————————————————————————————————————————————————————————————————|
* | void *func_arg; |\
* |—————————————————————————————————————————————————————————————————————————————————| \
* | thread_func *function; | \
* |—————————————————————————————————————————————————————————————————————————————————| \
* | void(*unused_retaddr); | \
* |—————————————————————————————————————————————————————————————————————————————————| |
* | void (*eip)(thread_func *func, void *func_arg); | thread_stack
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t esi; | |
* |—————————————————————————————————————————————————————————————————————————————————| /
* | uint32_t edi; | /
* |—————————————————————————————————————————————————————————————————————————————————| /
* | uint32_t ebx; | /
* |—————————————————————————————————————————————————————————————————————————————————|/
* | uint32_t ebp; |<-----.
* |—————————————————————————————————————————————————————————————————————————————————| \
* | : | \
* | : | \
* | : | |
* | : | |
* | : | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t stack_magic; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | int8_t exit_status; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | pid_t parent_pid; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t cwd_inode_nr; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | struct mem_block_desc u_block_desc[DESC_CNT]; | |
* |—————————————————————————————————————————————————————————————————————————————————| 指|
* | struct virtual_addr userprog_vaddr; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t *pgdir; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | struct list_elem all_list_tag; | 向|
* |—————————————————————————————————————————————————————————————————————————————————| |
* | struct list_elem general_tag; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint32_t elapsed_ticks; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint8_t ticks; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | uint8_t priority; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | char name[TASK_NAME_LEN]; | |
* |—————————————————————————————————————————————————————————————————————————————————| |
* | enum task_status status; | /
* |—————————————————————————————————————————————————————————————————————————————————| /
* | pid_t pid; | /
* |—————————————————————————————————————————————————————————————————————————————————| /
* | uint32_t *self_kstack; |-----'
* |—————————————————————————————————————————————————————————————————————————————————|低地址
*****************************************************************************************/

作者:yangsail

出处:https://www.cnblogs.com/yangsail/p/17738679.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   仰望星空_22  阅读(93)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示
more_horiz
keyboard_arrow_up dark_mode palette
选择主题