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; // 找到下一个线程的tcbSwitch_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;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/329495.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python 列表生成表格_【转】Python 列表生成式

原文:https://blog.csdn.net/heartyhu/article/details/509880071. 生成列表要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],我们可以用range(1, 11):>>> range(1, 11)[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]但如果要生成[1x1, 2x2, 3x3, ……

Linux(CentOS 6.7)下配置Mono和Jexus并且部署ASP.NET MVC3、4、5和WebApi(跨平台)

1.开篇说明 a. 首先我在写这篇博客之前,已经在自己本地配置了mono和jexus并且成功部署了asp.net mvc项目,我也是依赖于在网上查找的各种资料来配置环境并且部署项目的,而其在网上也已有了很多这方面的文章,故而我就想我是写还是不…

10.操作系统演进过程

【README】 1.本文内容总结自 B站 《操作系统-哈工大李治军老师》的《操作系统的那棵树》,内容非常棒,墙裂推荐; 2.思维僵化与发散 the mind is not a vessel that needs filing, but wood that needs igniting. 头脑不是需要归档的容器&am…

Oracle入门(十二I)之误删除数据的恢复方法

转载自 oracle误删除数据的恢复方法今天主要以oracle数据库为例,介绍关于表中数据删除的解决办法。(不考虑全库备份和利用归档日志)删除表中数据有三种方法:delete(删除一条记录)drop或truncate删除表格中数…

Oracle入门(七A)之表空间配额(quota)

转载自 oracle表空间配额(quota)一、quota相关视图 1)dba_ts_quotas(查看所有用户的表空间配额) BYTES字段表示用户已经使用的空间;MAX_BYTES如果为-1表示没有限制,其他值表示限制配额 --只有用alter user user_name quota on tab…

python嵌套列表字典_python中嵌套列表转为字典

题目:# 有一组用例数据如下:cases [[case_id, case_title, url, data, excepted],[1, 用例1, www.baudi.com, 001, ok],[4, 用例4, www.baudi.com, 002, ok],[2, 用例2, www.baudi.com, 002, ok],[3, 用例3, www.baudi.com, 002, ok],[5, 用例5, www.ba…

.NET之全平台一体化的体验

一、前言 近来利用空闲时间研究了一下Xamarin的技术,想想既然提供了如此好的支持,就该尝试一切可能,来一个”大小通吃“。 何为全平台:APP包括Android、IOS、WP,WEB可在Window和Linux部署运行(进可攻,退可守…

11.cpu调度策略与schedule调度函数

【README】 1.本文内容总结自 B站 《操作系统-哈工大李治军老师》,内容非常棒,墙裂推荐; 2.cpu调度: 指的是 cpu从就绪队列中选择一个进程来执行;选择哪一个进程是调度算法的执行结果; 3. 相关定义&…

Oracle入门(十二)之SQL的DDL

一、数据类型 Character 数据类型Number 数据类型Date 数据类型Raw 和 Long Raw 数据类型 LOB 数据类型 注:Oracle数据类型详解二、表 (1)创建表 create table emp ( emp_id char (10) primary key, emp_name varchar2 (30), sal number (…

2016微软开发者峰会在京举办 纳德拉要来做演讲

还有不到一个月的时间,2016 微软开发者峰会就要在北京举办了。 在这场开发者的盛会上,微软 CEO、技术牛人,还有来自微软亚洲研究院、亚太研发集团、Xamarin 团队以及微软中国开发体验的专家们将对各平台的开发进行技术探讨。 据了解&#xff…

centos 卸载ffmpeg_Linux下ffmpeg的完整安装

最近在做一个企业项目, 期间需要将用户上传的视频转成flv格式或mp4格式并用flash插件在前端播放, 我决定采用ffmpeg (http://www.ffmpeg.org/ )实现. 当然以前也用过ffmpeg, 但是没有安装额外的库, 只是源代码下简单地 ./configure, 最后发现好多功能都用不了, 比如最流行的x26…

1.概率论-组合分析

【README】 本文总结自《概率论基础教程》 by M.Ross ,墙裂推荐; 【1.3】排列(考虑顺序) 1)例3d: 用6个字母 PEPPER排列,共有多少种不同的排列方式? 2)推理 对于n个元素,如果其中n1个元素相同,其他n2个元素相同,......,nr个元素也相同,一共有 种不同排列方式;…

Oracle入门(十二B)之表创建

一、创建表(1)简单表 Create Table emp (Emp_id char(10) primary key,Emp_name varchar2(30),Sal number(5),Tel varchar2(20) ); (2)带参数的表格创建 create table emp (emp_id char (10) primary key,emp_name varchar2 (30),…

我的创作纪念日:感恩、感谢、感激!

/bin/bash 机缘 感恩、感谢、感激! 第一次进入到csdn,还是当初老师傅叫我们可以借鉴一下这里的文章 所以! 一开始进入到csdn网站,还以为这里也是和某些贴吧一样,一样的灌水呢! 但是正式在这里书写文章之后&#…

python中seaborn画swarm图_Python可视化 | Seaborn5分钟入门(四)——stripplot和swarmplot

微信公众号:「Python读财」如有问题或建议,请公众号留言Seaborn是基于matplotlib的Python可视化库。 它提供了一个高级界面来绘制有吸引力的统计图形。Seaborn其实是在matplotlib的基础上进行了更高级的API封装,从而使得作图更加容易&#xf…

第四篇 Entity Framework Plus 之 Batch Operations

用 Entity Framework 进行 增,删,改。都是基于Model进行的,且Model都是有状态追踪的。这样Entity Framework才能正常增,删,改。 有时候,要根据某个字段,批量更新或者删除数据,用Ent…

2.概率论-概率论公理

【README】 本文总结自《概率论基础教程》 by M.Ross ,墙裂推荐; 【2.2】样本空间和事件 1)样本空间 所有可能的结果构成的集合,称为该实验的样本空间,记为S;2)事件(一个集合,或样本空间的子集) 样本空间的任一子集E称为事件;或样本空间中选取若干个结果构成的集合…

Oracle入门(十二C)之表修改

一、列操作 (1)添加列alter table 表名 add (列名 数据类型 [default 表达式], ..);alter table tableName add temp varchar2(30);(2)修改列A.修改列类型和属性alter table 表名 modify (列名 数据类型 [default 表达式], ..)…

在ThoughtWorks工作12年的技术主管,所总结的12条技术人经验

原文: 12 years, 12 lessons working at ThoughtWorks 作者: Patrick,ThoughtWorks的技术主管兼敏捷顾问 编译: 孙薇 本文作者在ThoughtWorks工作了12年之久, 回顾了往昔工作之后,他得出了12条经验心得&…

3.条件概率与独立性

【README】 本文总结自《概率论基础教程》 by M.Ross ,墙裂推荐; 【3.2】条件概率 1)条件概率定义: 【补充】条件概率计算示例 【3.3】贝叶斯公式 1)通过第2个事件发生与否计算第1个事件的概率(非常重要…