L10_用户级线程

线程与进程

:::info

  • 进程 = 资源 + 指令执行序列
    • 将资源和指令执行分开
    • 一个资源 + 多个指令执行序列
  • 线程: 保留了并发的优点, 避免了进程切换代价
  • 实质就是映射表不变而PC指针变
  • 切换线程——指令切换(PC指针切换),资源不切换(不更换映射表) :::

    一个例子:网页浏览器

    1. void WebExplorer() {
    2. char URL[] = http://cms.hit.edu.cn”;
    3. char buffer[1000];
    4. pthread_create(..., GetData, URL, buffer); //GetData
    5. pthread_create(..., Show, buffer); //Show
    6. }
    7. void GetData(char *URL, char *p){...};
    8. void Show(char *p){...};

    image.png

    如何切换?

    image.png

  • 跳转时会将当前程序下一个程序的地址压入栈中

  • 程序结束遇到}会将栈中的地址弹出,转到该地址; :::danger 若是两个程序只是用一个栈,会发生混乱; ::: image.png
    Yield切换时要先切换栈,进程1的地址压在TCB1的栈,进程2的地址压在TCB2的栈;

    ThreadCreate

    线程的核心便是TCB、栈、切换的PC;因此在创建线程时,需要创建这三者;
    image.png

    1. void ThreadCreate(A)
    2. {
    3. TCB *tcb=malloc();
    4. *stack=malloc();
    5. *stack = A;//100
    6. tcb.esp=stack;
    7. }

    用户级线程和核心级线程的差异

    用户级线程仍会出现阻塞的现象;

  • 某个线程进入内核并阻塞,CPU并不知道TCB的信息,也不会调用其他的线程,因此会卡住;

  • 与之区别的是核心级线程,由系统调用,会进入内核,知道TCB(但是不用跳转进程)

image.png

L11_内核级线程

有了内核级线程的概念,才能更好地运用多核CPU。

内核级线程与用户级线程的差别

:::info

  • 用户级:两个栈
  • 内核级:两套栈 ::: image.png :::info 用户栈会和内核栈组成一种关联的关系;
    image.png
    *当用户调用INT中断时,会进入内核; :::

    如何切换

  • 用户调用int指令,进入中断

  • 中断处理,启动磁盘读或是时钟中断:schedule()引发切换
  • schedule内使用swith to函数,实现内核切换
  • 切换后执行一段中断函数,最后应该右iret指令,从中断弹出,进入切换后的线程

image.png :::info 直观上讲,有两级切换,先是内核级的切换,从线程S的代码段切到线程T的代码段
然后中断结束回到线程T(因为栈也换了)
image.png :::

image.png

Summar

image.png

L12_内核级线程的实现

从一个实例讲解fork()创建子线(进)程;

用户栈进入内核

  1. main(){
  2. A();
  3. B();
  4. }
  5. A(){
  6. fork();
  7. }
  8. //遇到fork系统调用,通过中断进入内核
  9. mov %eax,__NR_fork
  10. INT 0x80
  11. mov res,%eax

main()进入A()时,用户栈会压入下一条指令的地址:ret=B
image.png

从内核进中断

调用中断进入内核后,内核栈要和用户栈联系起来,栈寄存器指向用户栈,因为要去执行中断函数,因此也要保存内核下一条指令的地址,即mov res,%eax的地址。然后调用中断处理函数system_call

  1. _system_call:
  2. push %ds..%fs
  3. pushl %edx... //首先保存现场,此时保存的这些内容还是当前的用户栈的
  4. call sys_fork //调用fork创建一个子进程
  5. //========================
  6. pushl %eax
  7. movl _current, %eax
  8. cmpl $0,state(%eax)
  9. jne reschedule
  10. cmpl $0, counter(%eax)
  11. je reschedule
  12. //========================
  13. ret_from_sys_call:

:::info 系统判断当前进程是否发生阻塞、需要切换到新的进程是通过上面的两个cmpl来进行的;

  • 第一个比较状态信息是否为非零,若是非0则说明阻塞了,执行schedule切换
  • 第二个判断时间片是否以及没有了,若是没了也是切换 :::

    切换到其他进程

    这里先解释切换五段论中的最后一个,如何从内核回到用户栈。

    1. reschedule:
    2. pushl $ret_from_sys_call
    3. jmp _schedule
  • reschedule首先在当前内核栈中保存返回后的下一条指令,即弹栈ret_from_sys_call

  • 然后才执行切换线程(PCB的切换)

    1. void schedule(void){
    2. next=i;
    3. switch_to(next);
    4. }

    :::tips switch_to切换到下一个就绪的进程,实际上是PCB的切换!也就是说现在内核栈换了!

  • switch_to末尾的ret会执行弹栈,这时因为已经换成新的内核栈了,ret后会跳到新的用户栈。 :::

    1. ret_from_sys_call:
    2. popl %eax //返回值
    3. popl %ebx.h
    4. pop %fs...
    5. iret //返回到哪里呢?INT 0x80后面执行!
    6. //res的值是返回值,实际上是%eax的值

    :::info 若是从schedule出来的话也会执行弹栈,弹栈后会执行ret_from_sys_call
    ret_from_sys_call会执行弹栈操作,因为内核栈已经切换了,所以这时候弹栈实际上是弹的另一个进程的用户栈,执行结束后iret也会进入到新进程的用户栈 :::

    fork创建线程