L5 用户级线程

线程有着并发的特点,同时又避免了进程切换时需要切换资源的代价(这里的资源应该是指LDT表)。本章只是简单的概述一下用户级线程设计的思想。理解用户级线程对理解核心级线程很有帮助。

1 生活案例

如果网络不是很好,在浏览网页时,网页通常是先出现一些文字,然后慢慢显示一些图片,最后卡一会再显示一些动画、视频等。对于浏览器显示网页,我们可以简单的看成两个组成部分:获取服务器端的网页内容 和 显示获取到的内容。从上面的例子可以看出,获取服务器端的网页内容 和 显示获取到的内容,两个是同时进行的。运行中的浏览器本身就是一个进程,同时该进程又创建了两个线程,来同时推进 获取服务器端的网页内容 和 显示获取到的内容 两个任务。

2 用户级线程

为了支持用户级线程,需要设计两个函数:ThreadCreate()Yield ()。ThreadCreate()用于创建线程,Yield ()用于切换到下一个线程。函数在调用 Yield 时,只是表明自己主动放弃CPU,但并不知道自己要切到哪个线程去。本节主要分析如何设计这两个函数。由于是用户级线程,因此 Yield () 只能由用户主动调用,而不是放到内核中去执行,这一点与 schedule() 有很大区别由于操作系统感知不到用户级线程的存在,因此在分配CPU资源上面,用户级线程并不占优势

2.1 两个线程之间如何切换

如下图所示,有 T1、T2 两个线程,其中 T1 执行了A(),T2 执行 C(),绿色字体为内存地址,数字为 CPU 执行指令顺序。  前提: 假设线程 T1 中调用了 Yield 函数(在B函数中),该函数会让 CPU 切换到 T2 线程去执行,同样 T2 中也调用了 Yield 函数(在D函数中),该函数会让 CPU 切换到 T1 线程去执行。

本节主要探讨如何设计 ThreadCreate 和 Yield 函数才能让 CPU 按照图中(1)、(2)、(3)的顺序执行,在切换时不混乱。

两个线程

图2.1 两个线程

2.1.1 两个线程共用一个栈

每个程序运行时在内存中都维护一个栈,这个栈让程序在进行各种函数调用、跳转时不发生混乱。”两个线程共用一个栈“也就是说两个线程共用程序运行时维护的那个栈。假设将 Yield () 函数设计为如下形式:

void Yield (){       
	找到下一个要执行线程 Tn;
	找到要跳转到的地址 addr;
	jmp addr;
}

按照这样的设计,两个线程的执行情况如下图所示:

两个线程共用一个栈

图2.2 两个线程共用一个栈

当 CPU 跳转回 204 处后,接着就会执行B函数的 “}”(这里没有问题,可能有人会认为应该是去执行B中Yield 函数的"}“,但其实不是,D中的Yield是直接调用 jmp 指令 跳到了204处,B中Yield 函数的”}"不会被执行),这时会出栈,并将栈顶数据 404 赋给 PC 指针。这时候问题就来了,本来执行完 B() 应该要跳回到 A() 去执行的,结果跳到了 D(),可以看出两个线程共用一个栈是不合格的。

2.2.2 两线程两个栈

“两线程两个栈”,表明这两个线程不再使用程序运行时维护的那个栈了,而是自己开辟一个新的栈,开辟新栈的工作是由 ThreadCreate() 完成。从1.2.1中可以看出 Yield () 需要找到下一个要执行线程,并且还要找到要跳转的位置,这个其实和进程切换的套路差不多:队列 + PCB,线程也可以建立一个TCB:

struct TCB{
	esp;   //线程栈顶地址
    ...;
};

Yield () 函数设计为如下形式,可以看出,此时Yield已经不用像1.2.1中的那样,去找要跳转到的地址 addr,因为此时要跳转的地址就保存在各个线程栈的栈顶。

void Yield (){       
	找到下一个要执行的线程的TCP: TCPn;
    设置当前TCP为: TCPn;
	ESP = TCPn.esp;  //修改栈指针
	jmp [TCPn.esp];  //跳转到 TCPn 栈顶存放的地址处
}

这时两个两线程的执行情况如下:

两个线程两个栈

图2.3 两个线程两个栈

在执行完D()中的Yield ()后,接着就会执行B函数的 “}”,这时会出栈,并将栈顶数据 204 赋给 PC 指针,可以看出还是存在问题,应该要去执行A() 的,结果又转回了B()。稍微修改一下Yield () 函数:

void Yield (){       
	找到下一个要执行的线程的TCP: TCPn;
    设置当前TCP为: TCPn;
	ESP = TCPn.esp;  //修改栈指针
	//jmp [TCPn.esp];  //删除此行
}

修改后分析:在执行D()中Yield函数的 “}”,这时会出栈,并将栈顶数据 204 赋给 PC 指针,然后程序跳转到 204 地址处继续执行,紧接着就会执行B函数的 “}”,这时会出栈,并将栈顶数据 104 赋给 PC 指针,程序跳转到A()。可以看出修改后的Yield ()切换的很合适。

分析完Yield ()后,就可以设计出 ThreadCreate() 了:

//创建一个线程,func为需要执行的函数的函数指针
void ThreadCreate(func){
    TCB *tcb = malloc();   //创建TCB
    *stack = malloc();    //分配线程的栈空间
    *stack = func;        //将线程起始地址放入栈中
    tcb.esp = stack;        //修改栈顶指针
}

2.2 用户级线程如何设计

从2.1小节的分析中可知,一个线程一个栈是合理的,因此在设计用户级线程时应该让每个线程都有一个自己的栈,切换线程时应该先切换TCP,再切换栈。为什么会想到使用一个线程一个栈呢?这里可以从栈的作用入手。不同的线程需要执行不同的指令,也会调用不同的函数,而栈的作用就是为了维护在函数调用时程序执行不发生混乱。当多个线程读写同一个栈时,栈的内容就会发生混乱,程序的执行也就容易出现混乱。
也正是因为每个线程都有自己的栈,线程间才不能共享局部变量,而能共享全局变量(因为局部变量是保存在栈中的,而全局变量不在栈中)。

参考

[1] 操作系统-哈尔滨工业大学-中国大学MOOC
[2] 哈工大操作系统实验手册
[3] Linux内核完全剖析——基于0.12内核

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值