9.内核级线程代码实现

【README】

1.本文内容总结自 B站 《操作系统-哈工大李治军老师》,内容非常棒,墙裂推荐;

【说明】

  • 本文中提到的父线程可以理解为父进程
  • 因为进程包括运行资源和执行指令,又执行指令表示为线程,所以也可以说 进程包括 运行资源和线程;
  • 在执行指令或代码层面,可以理解进程与线程类似(但线程切换成本更低,这是他们的很大区别);

2.进程包括资源与执行序列,其中执行序列用线程来实现,线程分为用户线程和内核线程;进程中的资源管理主要是内存管理;
3.线程在操作系统的代码实现: 通过内核级代码 加上 内存管理代码 ;

【补充】线程切换步骤:

  • 1.    用户栈切换到内核栈;
  • 2.    内核栈找到tcb(线程控制块);
  • 3.    Tcb切换到其他线程的tcb2;
  • 4.    通过tcb2找到内核栈2;
  • 5.    通过内核栈2切换到用户栈2;

【1】进入内核


1) fork:

  • 是一个系统调用,用于创建进程,即创建资源和执行序列,其中创建执行序列就是创建线程;

2 )fork代码的工作:

  • 工作1)线程怎么切换的;即 切换五段论 怎么具体实现的;
  • 工作2)创建一个内核线程需要做哪些事情?

切换五段论参见  8.内核级线程(核心级线程)_PacosonSWJTU的博客-CSDN博客

 3)具体代码
fork系统调用会展开为 

mov %eax, __NR_fork ; // 把系统调用号 __NR_fork 送入 eax寄存器;
INT 0x80 ; // 触发80号中断;
Mov res, %eax;// 中断处理完成后执行的下一条指令; 

例:__NR_write 是系统调用号,用来标识是哪种系统调用;

 4)Int0x80 中断指令发起后,cpu做以下事情

  • 找到当前线程的内核栈;
  • 压入当前线程的物理寄存器ss,sp到内核栈;
  • 压入当前线程的物理寄存器cs,ip到内核栈;

5)Int80的中断处理函数是 system_call 系统调用;

  • 在执行 system_fork(system_read或system_write)过程中, 碰到磁盘读写等情况,需要阻塞以等待,操作系统就会从当前内核线程 切换到 其他内核线程去执行;

【2】切换五段论

【2.1】切换五段论中的中断入口和出口

1)内核栈的内容:存储了用户态所有物理寄存器的值;

 

2)系统调用 _system_call 源码

_system_call:

Push %ds..%fs  // 把用户态的物理寄存器值压入到内核栈;

Pushl %edx  // 把用户态的物理寄存器值压入到内核栈;

Call sys_fork  // 通过系统调用号查询系统调用表,获取系统调用函数是 sys_fork  ;系统调用如 sys_fork sys_writesys_read 因读写磁盘可能导致当前线程阻塞,操作系统就会切换到其他内核线程执行;

Pushl %eax  //  把 eax寄存器值压入内核栈;

Movl _current, %eax // _current 就是 tcb 线程控制块,送入eax寄存器;

Cmpl $0,state(%eax) // 判断pcb.state是否等于0;(不等于0表示当前线程阻塞,0表示就绪)

Jne reschedule  // 不等于0,则重新执行线程调度算法reschedule(切换到其他内核线程与内核栈,或五段论的中间三段)  

Cmpl $0, counter(%eax) // pcb.counter 时间片是否等于0;

je reschedule // 时间片等于0,即时间片用光了,也要切换到其他内核线程

ret_from_sys_call:  // 中断返回(系统调用返回地址)

3)reschedule 线程调度算法(五段论中的三段),切换到其他内核线程

Pushl $ret_from_sys_call // 把中断返回地址,或系统调用返回地址压入到内核栈; 
Jmp _schedule //  调度函数  

 【补充】

  • schedule 执行完成后,一定要执行  ret_from_sys_call  进行中断返回;

【2.2】切换五段论中的 schedule 和 中断出口

1)中断入口是 push;中断出口是 pop;

2)Ret_from_sys_call: 中断返回代码(系统调用返回函数代码);

Popl %eax // 返回值   Popl %ebx  
Pop %fs 
Iret // 内核栈所有寄存器值弹出到物理寄存器,实现线程切换; 

3)void schedule: 线程切换调度

调用 switch_to(next)

void schedule(void)
{
  Next = I; // 找到下一个线程的tcb
  Switch_to(next);  // 切换
}

【2.3】 切换五段论中的 switch_to(切换tss)

1)tss定义: task struct segment,任务结构段,用任务结构段进行切换;
tss 保存了线程的所有寄存器值;如下图所示。

2)基于tss的切换 变为 基于 kernel stack(内核栈);
tss 用一句长跳转指令 ljmp %0 完成切换,其中长跳转指的是跳转到其他段,但ljmp指令执行慢,所以需要转换到 内核栈切换

  • tss是一个段,GDT中存在段描述符项,指向tss段基址;如GDT的原TSS描述符指向原TSS;

3)TR寄存器

  • TR选择子(TR寄存器值) 从 GDT中找到 TSS描述符,进而找到TSS段;
  • 长跳转指令 ljmp,指的是段与段之间的跳转,实际上是段寄存器值变化,即GDT中新TSS段描述符赋值给 TR寄存器,其中新TSS段描述符由 _TSS(n) 赋值,_TSS(n)中的n是上文中的next指针,是下一个线程的“段寄存器基址CS”;

4)TR寄存器存储的是当前cpu的任务段

  • TR寄存器值指向了 GDT表的一个tss描述符,通过tss描述符可以定位到tss结构体的内存地址,tss结构体存储了所有物理寄存器的值(快照)
  • 只要TR改变,则cpu载入的tss(任务结构段)也会跟着切换,因为cpu中物理寄存器被tss中逻辑寄存器值快照覆写了,即内核线程跟着切换

【补充】ljmp 长跳转指令

  • 步骤1)把cpu所有寄存器的内容送入TSS任务结构段中(拍寄存器快照),TSS任务结构段通过TR寄存器值查询GDT得到TSS描述符,TSS描述符映射到TSS任务结构段地址;
  • 步骤2)把 _TSS(n) 赋值给 TR 寄存器(新的TSS描述符地址);
  • 步骤3)通过TR选择子,寻址GDT得到新TSS描述符,进而映射得到新TSS
  • 步骤4)把 新TSS里的内容 送入对应的cpu寄存器(重写物理寄存器内容);

简单点:TSS是一个结构体,存储在内存,可以存储所有cpu物理寄存器的值;也可以把TSS结构体数据赋值到cpu物理寄存器;


5)cpu物理寄存器举例

EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP, EFLAGS, CR3 等;

其中 ESP被替换为TSS的对应值,完成栈切换;

6)tss 是 pcb的一个子段,pcb的一个部分

  • 或  tss 是 pcb结构体中的一个内嵌结构体元素;  
  • 根据pcb 找到tss,根据tss找到 并切换esp ;

【问题】
因为 ljmp %0 是个复杂的汇编命令,无法利用指令流水提高执行速度,执行速度慢;
所以把基于tss的线程切换模式 转为  基于 内核栈(kernel stack)的线程切换模式

【基于tss的线程切换小结】共3句代码:

  • 1)    int 0x80
  • 2)    switch_to 中的 ljmp // 长跳转
  • 3)    iret

【3】fork创建线程

0)创建线程:sys_fork;

  • 调用 call  _copy_process ;

【3.1】 _copy_process 创建栈

1)call  _copy_process:创建栈

int copy_process(int nr, long ebp, long edi, long esi, long gs, long none, ….) 
// 各个寄存器值

2)在父线程创建子线程前,父线程会把用户态的物理寄存器值压入内核栈;如下。

  • 有了内核栈,就可以创建出与父线程差不多的子线程了;内核栈元素就作为了 copy_process的方法参数。

 补充:内核栈:

SS:SP // 栈底

EFLAGS

ret=??1  // 赋值给 eip ;是父线程触发中断 int 0x80指令的下一句指令;

ds,es,fs  //

edx, ecx, ebx 

??2

gs

esi,edi

ebp

eax

??4

3)copy_process 创建栈细节

p=(struct task_struct *) get_free_page();

// 获得一页内存,是内核态代码;而malloc是用户态代码;这一页内存用来做pcb;

申请内存

p->tss.esp0=PAGE_SIZE+(long)p;

esp0是内核栈;

PAGE_SIZE=4k;

P是这一页内存初始地址,基址;

(示意图如上)

创建内核栈

p->tss.ss0= 0x10;

0x10 表示内核数据段;

ss0是内核堆栈段;

p->tss.ss=ss & 0xffff

父线程用户栈的ss

创建用户栈

P->tss.esp=esp;

父线程用户栈的esp

【补充】

  • 创建的新线程,其用户栈与父线程是同一个,但内核栈不同;
  • 创建的新线程,有自己的 tcb,在内核中是分开的;

mem_map:把内存拆分为单位为4k的存储块,每个存储块叫做一页(mem_map中等于0的元素);

【总结】 copy_process 的细节:创建栈步骤:

  • 创建tcb;
  • 创建内核栈;
  • 创建用户栈;
  • 关联栈和tcb;

【3.2】copy_process 细节-执行前准备

1)具体步骤:

  • 把内核栈的寄存器值,赋值给tss结构体;
  • tss修改后,cpu载入tss覆写了物理寄存器,寄存器值修改,如cs,ip等,则下一条要执行的指令就修改了,达到线程切换的效果;

 【小结】父线程创建子线程后切换到子线程的过程

  • 1)    父线程 执行完 sys_fork 创建完子线程后;继续向下执行;
  • 2)    执行到  cmpl $0, state(%eax); 又父线程状态阻塞,则进入 reschedule
  • 3)    进入 reschedule,就会进入switch_to 切换到子线程switch_to执行具体步骤如下
    1. 父线程把物理寄存器值快照保存到原tss结构体,tss保存所有寄存器值;
    2. 新tss描述符赋值给TR寄存器,从而TR寄存器指向新tss结构体;
    3. Cpu通过TR寄存器寻址到新的tss结构体,并把新tss载入到cpu寄存器,从而切换到子线程;其中eip是指令int 0x80的内存地址;又 eax等于0,执行 mov res, %eax,则res等于0; 即 子线程fork()函数返回0

上述汇编代码执行过程截图如下:

 

 2)补充:

  • 对于父线程而言,eax寄存器值不等于0;
  • 对于子线程而言, eax寄存器值等于0 ;
  • 从而达到把父子线程分开的目的(fork-分叉)

 (截图自操作系统接口)

代码描述:

  • While循环中, fork()返回值等于0,则表示是子线程,执行 exec(…)
  • fork() 返回值不等于0,则表示是父线程,执行 wait();

【小结2】 父线程创建子线程步骤

  • 1)tss做好了,可以完成线程切换;
  • 2)用户栈,子线程与父线程用同一个;
  • 3)内核栈,为子线程新创建一个内核栈,父子线程各自用各自的内核栈
  • 4)关联父线程用户栈与子线程的内核栈;

【4】利用新建子线程运行业务代码

0)问题:

  • 如何利用新建的子线程执行业务代码 ,如指针A 表示的是 业务代码的首地址;

2) main 函数代码:

if(!fork()) {
 exec(cmd);
} else wait(0); 
//  !fork() 表示返回为0,即子线程时,执行exec(cmd); // 业务代码 

 3)exec 系统调用执行细节

  • 3.1) fork后 ,在子线程进入内核态之前,父线程与子线程执行的代码是相同的;
  • 3.2) 但子线程调用 exec,接着调用 sys_execve系统调用后,进入内核态;执行内核代码结束后,子线程从内核态返回,或从0x80中断返回时,执行 ls->entry  入口程序;而 ls->entry入口程序地址是由 iret指令 把子线程内核栈的eip元素值弹出覆写 ip物理寄存器,从而修改pc寄存器值得到的;

3)通过 exec 系统调用 进入内核态 

_system_call:
  Push %ds .. %fs 
  Pushl %eax 
  Call sys_execve 

_sys_execve:
  // 把 %esp + EIP 的值赋值给 eax寄存器,又EIP偏移量28;
  Lea EIP(%esp), %eax; 
  
  // 把 eax寄存器值压栈,即把EIP的地址(即内核栈28号元素的地址)压栈;
  // 作为 do_execve的方法参数;  
  Pushl %eax; 
    
  // 调用 do_execve方法  
  Call _do_execve 

EIP = 0x1C  // 寻址内核栈下标28的元素 (ret=??1)

代码解说:

// 程序入口地址 赋值给 eip;
// 完成了把 ls->entry 程序入口地址赋值给eip寄存器(逻辑上,非物理)的工作;
eip[0] = ex.a_entry; 

// 线程tcb起始地址 赋值给SP堆栈指针寄存器; 
eip[3]=p; 

【总结】线程切换(非常重要*)

1)演示图

2)线程切换步骤:

  • Step1)用户栈通过int0x80展开为系统调用 sys_call,调用系统调用,进入内核栈;
  • Step2)通过内核栈的esp元素找到tcb;
  • Step3) tcb通过 switch_to 切换到其他线程的tcb2,切换到其他线程的内核栈2;
  • Step4)用 iret 把内核栈2的寄存器值弹出,切换到用户栈2;

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值