首页 > 编程语言 > 详细

linux进程-线程-协程上下文环境的切换与实现

时间:2020-07-04 09:39:24      阅读:40      评论:0      收藏:0      [点我收藏+]

进程切换分两步
1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文。

对于linux来说,线程和进程的最大区别就在于地址空间。
对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。所以明显是进程切换代价大

 

线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

四:上线文切换的实质

 

 

 

 

 

 

  1.  
    X86 32 Bists
  2.  
    SS --> 选择子--->段描述表-->(段限,段基址)
  3.  
    CR3 --->页目录,页表
  4.  
    ESP-->
  5.  
    EBP-->
这其实和参数传递有点相似,只需要传递地址就够了。
ESP,EBP 正式 堆栈指针寄存器。
变量通过ESP,EBP 两个指针 加偏移量访问。

五:比较

 

1、进程多与线程比较

线程是指进程内的一个执行单元,也是进程内的可调度实体。线程与进程的区别:

1) 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间

2) 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源

3) 线程是处理器调度的基本单位,但进程不是

4) 二者均可并发执行

5) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

  2、协程多与线程进行比较

1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。

2) 线程进程都是同步机制,而协程则是异步

3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

4) 协程是用户级的任务调度,线程是内核级的任务调度。

5) 线程是被动调度的,协程是主动调度的。 

补充协程上下文环境的切换

 

协程

        协程是一种编程组件,可以在不陷入内核的情况进行上下文切换。如此一来,我们就可以把协程上下文对象关联到fd,让fd就绪后协程恢复执行。

       当然,由于当前地址空间和资源描述符的切换无论如何需要内核完成,因此协程所能调度的,只有在同一进程(线程)中的不同上下文而已。

        我们在内核里实行上下文切换的时候,其实是将当前所有寄存器保存到内存中,然后从另一块内存中载入另一组已经被保存的寄存器。对于图灵机来说,当前状态寄存器意味着机器状态——也就是整个上下文。其余内容,包括栈上内存,堆上对象,都是直接或者间接的通过寄存器来访问的。 但是请仔细想想,寄存器更换这种事情,似乎不需要进入内核态么。事实上我们在用户态切换的时候,就是用了类似方案。也就是说协程是在用户态保存寄存器状态的!

 

作为推论:在单个线程中执行的协程,可以视为单线程应用。这些协程,在未执行到特定位置(基本就是阻塞操作)前,是不会被抢占,也不会和其他CPU上的上下文发生同步问题的。因此,一段协程代码,中间没有可能导致阻塞的调用,执行在单个线程中。那么这段内容可以被视为同步的。

我们经常可以看到某些协程应用,一启动就是数个进程。这并不是跨进程调度协程。一般来说,这是将一大群fd分给多个进程,每个进程自己再做fd-协程对应调度。

 

基于就绪通知的协程框架(epool本身是同步的)
  1. 协程
  2. 首先需要包装read/write,在发生read的时候检查返回。如果是EAGAIN,那么将当前协程标记为阻塞在对应fd上,然后执行调度函数。
  3. 调度函数需要执行epoll(或者从上次的返回结果缓存中取数据,减少内核陷入次数),从中读取一个就绪的fd。如果没有,上下文应当被阻塞到至少有一个fd就绪。
  4. 查找这个fd对应的协程上下文对象,并调度过去。
  5. 当某个协程被调度到时,他多半应当在调度器返回的路上——也就是read/write读不到数据的时候。因此应当再重试读取,失败的话返回1。
  6. 如果读取到数据了,直接返回。

这样,异步的数据读写动作,在我们的想像中就可以变为同步的。而我们知道同步模型会极大降低我们的编程负担。

我们经常可以看到某些协程应用,一启动就是数个进程。这并不是跨进程调度协程。一般来说,这是将一大群fd分给多个进程,每个进程自己再做fd-协程对应调度。

基于就绪通知的协程框架

  1. 首先需要包装read/write,在发生read的时候检查返回。如果是EAGAIN,那么将当前协程标记为阻塞在对应fd上,然后执行调度函数。
  2. 调度函数需要执行epoll(或者从上次的返回结果缓存中取数据,减少内核陷入次数),从中读取一个就绪的fd。如果没有,上下文应当被阻塞到至少有一个fd就绪。
  3. 查找这个fd对应的协程上下文对象,并调度过去。
  4. 当某个协程被调度到时,他多半应当在调度器返回的路上——也就是read/write读不到数据的时候。因此应当再重试读取,失败的话返回1。
  5. 如果读取到数据了,直接返回。

这样,异步的数据读写动作,在我们的想像中就可以变为同步的。而我们知道同步模型会极大降低我们的编程负担。

 

 

 

C/C++怎么实现协程

作为一个C++后台开发,我知道像go, lua之类的语言在语言层面上提供了协程的api,但是我比较关心C++下要怎么实现这一点,下面的讨论都是从C/C++程序员的角度来看协程的问题的。

boost和腾讯都推出了相关的库,语言层面没有提供这个东西。我近期阅读了微信开源的libco协程库,协程核心要解决几个问题:

1. 协程怎么切换? 这个是最核心的问题,有很多trick可以做到这点,libco的做法是利用glibc中ucontext相关调用保存线程上下文,然后用swapcontext来切换协程上下文,libco的实现中对swapcontext的汇编实现做了一些删减和改动,所以在性能上会比C库的swapcontext提升1个数量级。

2. IO阻塞了怎么办?试想在一个多协程的线程里,一个阻塞IO由一个协程发起,那么整个线程都阻塞了,别的协程也拿不到CPU资源,多个协程在一起等着IO的完成。libco中的做法是利用同名函数+dlsym来hook socket族的阻塞IO,比如read/write等,劫持了系统调用之后把这些IO注册到一个epoll的事件循环中,注册完之后把协程yield掉让出cpu资源,在IO完成的时候resume这个协程,这样其实把网络IO的阻塞点放在了epoll上,如果epoll没有就绪fd,那其实在超时时间内epoll还是阻塞的,只是把阻塞的粒度缩小了,本质上其实还是用epoll异步回调来解决网络IO问题的。那么问题来了,对于一些没有fd的一些重IO(比如大规模数据库操作)要怎么处理呢?答案是:libco并没有解决这个问题,而且也很难解决这个问题,首先要明确的一点是我们的目的是让用户只是仅仅调用了一个同步IO而已,不希望用户感知到调用IO的时候其实协程让出了cpu资源,按libco的思路一种可能的方法是,给所有重IO的api都hook掉,然后往某个异步事件库里丢这个IO事件,在异步事件返回的时候再resume协程。这里的难点是可能存在的重IO这么多,很难写成一个通用的库,只能根据业务需求来hook掉需要的调用,然后协程的编写中依然可以以同步的方式调用这些IO。从以上可能的做法来看协程很难去把所有阻塞调用都hook掉,所以libco很聪明的只把socket族的相关调用给hook掉,这样可以libco就成为一个通用的网络层面的协程库,可以很容易移植到现有的代码中进行改造,但是也让libco适用场景局限于如rpc风格的proxy/logic的场景中。在我的理解里,阻塞IO让出cpu是协程要解决的问题,但是不是协程本身的性质,从实现上我们可以看出我们还是在用异步回调的方式在解决这个问题,和协程本身无关。

3. 如果一个协程没有发起IO,但是一直占用CPU资源不让出资源怎么办?无解,所以协程的编写对使用场景很重要,程序员对协程的理解也很重要,协程不适合于处理重cpu密集计算(耗时),只要某个协程即一直占用着线程的资源就是不合理的,因为这样做不到一个合理的并发,多线程同步模型由OS来调度并发,不存在说一个并发点需要让出资源给另一个,而协程在编写的时候cpu资源的让出是由程序员来完成的,所以协程代码的编写需要程序员对协程有比较深刻的理解。最极端的例子是程序员在协程里写个死循环,好,这个线程的所有协程都可以歇歇了。

 

协程有什么好处

说了这么多协程,协程的好处到底是啥?为什么要使用协程?

1. 协程极大的优化了程序员的编程体验,同步编程风格能快速构建模块,并易于复用,而且有异步的性能(这个看具体库的实现),也不用陷入callback hell的深坑。

2. 第二点也是我最近一直在纠结的一点,协程到底有没有性能提升?

1)从多线程同步模型切到协程来看,首先很明确的性能提升点在于同步到异步的切换,libco中把阻塞的点全部放到了epoll线程中,而协程线程并不会发生阻塞。其次是协程的成本比线程小,线程受栈空间限制,而协程的栈空间由用户控制,而且实现协程需要的辅助数据结构很少,占用的内存少,那么就会有更大的容量,比如可以轻松开10w个协程,但是很难说开10w个线程。另外一个问题是很多人拿线程上下文切换比协程上下文切换开销大来推出协程模型比多线程并发模型性能优这点,这个问题我纠结了很久。对于这个问题,我先做一个简单的具体抽象:在不考虑阻塞的情况下,假设8核的cpu,不考虑抢占中断优先级等因素,100个任务并发执行,100个线程并发和10个线程每个线程10个协程并发对比两者都可以把cpu资源利用起来,对OS来说,前者100个线程参与cpu调度,后者10个线程参与cpu调度,后者还有额外的协程切换调度,先考虑线程切换的上下文,根据Linux内核调度器CFS的算法,每个线程拿到的时间片是动态的,进程数在分配的时间片在可变区间的情况下会直接影响到线程时间片的长短,所以100个线程每个线程的时间片在一定条件下会要比10个线程情况下的要短,也就意味着在相同时间里,前者的上下文切换次数是比后者要多的,所以可以得出一个结论:协程并发模型比多线程同步模型在一定条件下会减少线程切换次数(时间片有固定的范围,如果超出这个范围的边界则线程的时间片无差异),增加了协程切换次数,由于协程的切换是由程序员自己调度的,所以很难说协程切换的代价比省去的线程切换代价小,合理的方式应该是通过测试工具在具体的业务场景得出一个最好的平衡点。

2)从异步回调模型切到协程模型来看,从一些已有协程库的实现来看,协程的同步写法却有异步性能其实还是异步回调在支撑这个事情,所以我认为协程模型是在异步模型之上的东西,考虑到本身协程上下文切换的开销(其实很小)和数据结构调用的一些开销,理论上协程是比异步回调的性能要稍微差一点,但是可以处于几乎持平的性能,因为协程实现的代价非常小。

3)从一些异步驱动库的角度来看协程的话,因为异步框架把代码封装到很多个小类里面,然后串起来,这中间会涉及相当多的内存分配,而数据大都在离散的堆内存里面,而协程风格的代码,可以简单理解为一个简洁的连续空间的栈内存池,辅助数据结构也很少,所以协程可能会比厚重的封装性能会更好一些,但是这里的前提是,协程库能实现异步驱动库所需要的功能,并把它封装到同步调用里。

 

 转载

 https://blog.csdn.net/runner668/article/details/80512664

 
 

linux进程-线程-协程上下文环境的切换与实现

原文:https://www.cnblogs.com/xiangshihua/p/13233666.html

(0)
(0)
   
举报
评论 一句话评论(0
分享档案
最新文章
教程昨日排行
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!