【操作系统】4、线程

内容概要:

用户级线程和核心级线程

 

第零部分:线程的概念

百度词条:线程(英语:thread)是操作系统能够进行运算调度的最小单位。

它被包含在进程之中,是进程中的实际运作单位。

一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

 

下面内容转载于:https://www.zhihu.com/people/lin-jia-jun-24-83-77/posts

第一部分:用户级线程

我们都知道进程,进程的切换我们需要,PCB保存当前进程状态,PC指针和映射表的切换。

那么有没有这种情况,我们只需要一份资源,却要用这份资源来做很多事。这种情况由于只需要一份内存资源,所以只需要一个映射表就够了。

这就产生了线程的概念,就是只切换pc指针却共用同一份内存。

接下来我们来谈谈线程间是怎么切换的。

就像进程的切换一样,我们需要保存当前执行到哪一步,然后再切换至下一个进程,进程的记录靠PCB(进程控制块),那么线程的记录靠什么呢?当然是TCB(线程控制块)了。

线程控制块是有栈组成的,这个栈保存着PC。当系统调用Yield(线程切换函数)方法。

100:A(){              300:C(){                                                      
  B();                   D();
  104;                   304 ;
}                       }
200:B(){              400:D(){
    Yield();              Yield();
    204;                 404;
}                       }
我们来看上面这段代码如何执行,当我们执行到100时调用A方法,A方法里面又调用了B方法,这时候我们把方法B的返回指针压栈也就104。然后调用Yield,将Yield的返回地址204压栈。随后跳转至300(下一个线程),如果这时候只有一个栈,那么就会将D函数返回地址304压栈,随后类似的将404压栈,可是在跳回去执行原来的函数时,却发现在弹栈时,弹到了别的线程的地方。这是明显不合理的。
esp:  104
       204 
       304
       404(出问题了,弹栈的时候地址跑到另一个线程去了)

所以,必须得要两个栈,我们在切换线程的时候,切换栈就好了。每个线程都有自己的栈,这样子执行方法就不会跑到别的线程去了。

esp:104    esp:304
     204    esp:404

那么现在事情就简单了,我们将各个线程的栈存放到TCB里,TCB里存放栈的地址,我们切换就在TCB 来调度,选择一个线程的栈切过去,这样TCB 和栈相互配合,就可以进行线程的切换。于是Yield方法也很明显了

void Yield(){//切换栈就好了
    TCB2.esp=esp;
    esp=TCB1.esp;
}

于是两个线程的样子很明显了,两个线程两个栈,切换的pc在栈中。这样子的切换不需要操作系统,不需要进入内核就可以完成切换,用户自己就能搞定。

第二部分:内核级线程

前面我们提到用户级线程的切换,是通过切换线程栈来实现的,由TCB线程控制块来控制,用户自己就能实现,无需通过内核。这次我们来讲讲内核级线程以及它的切换。

内核级线程,顾名思义,就是处于内核态的线程,对于内核级线程,可以很好配合多核CPU,多核指的是MMU(映射)一样,但是有多个核可以并行的处理事件。

 

 

回顾上次所说的用户级线程的切换,是两个栈之间的切换,那核心级线程的切换是什么呢?不是两个栈,而是两套栈。

因为核心级线程必须到内核态,在用户态使用用户栈,在内核态不也得调用函数,也得使用一个栈,既要在用户态又得在内核态跑,一个核心级线程要两个栈组成的一套栈。

对比用户级线程的切换使用TCB进行切换,核心级线程也用TCB切换,不过这个TCB在内核中,而且根据TCB的切换来切换一套栈,内核栈和用户栈都得切换。

那么具体的如何从一个核心级线程切到另一个核心级线程呢?这就是大名鼎鼎的五段论了。

首先我们得从当前的用户栈切到内核栈,只有通过系统中断才可以进入内核,INT就是系统中断,通过在内核栈里面存储SS,SP来记录切换回来的指针,我们就可以切入内核栈了,如果想返回,可以调用IRET的系统调用返回,这就是一套栈了。

100:A(){
          B();
          104;
      }
      200:B(){
        read();
        204;
      }
用户程序300:read(){
|        int 0x80;
|        304; 
|      }
|      system_call;
内核程序call sys_call
     1000; 
     2000:sys_read(){     }

如上图,我们调用read()调用系统中断进入内核态,现在的栈的情况是

用户栈:104
        204


内核栈:SS SP(记录返回指针)
        EFLAGS
        304
        CS
        1000

可以看到这两个栈配合的非常好,这就是一套栈的精髓所在,也完成了切换的第一段。

由于read需要读磁盘,这会使线程进入堵塞阶段,引发CPU调度,switch_to通过TCB的切换(第二段)找到另一个线程B的内核栈的指针,然后切到另一套栈(第三段),也就是线程的切换,这是切换的第二、三段。

但是我们执行的是用户的函数,所以我们还得切到线程B的用户栈(第四段),这里我们执行完内核栈的函数,通过弹栈中断返回到SS SP弹回线程B的用户栈,这就是切换的第五段。

如果是进程的切换的话,我们另外切换地址映射表就可以了。

 

 

 




posted @ 2022-02-24 12:08  鱼儿冒个泡  阅读(86)  评论(2编辑  收藏  举报