内核级线程实现


前言

提示:
这里主要对内核线程switch_to的五段论程序进行分析。

五段论:
在这里插入图片描述

核心级线程的两套栈, 核心是内核栈
核心级线程的切换过程:
在这里插入图片描述


提示:以下是本篇文章正文内容,

一、中断入口及出口

从INT 中断进入内核:
在这里插入图片描述

main()中:

1.首先在A()函数中系统调用fork(),将B()的地址压入用户栈。

2.fork() 引起中断0x80,进入内核。

3.执行int 0x80时,还未进入内核,首先找到内核栈,压入当前栈地址(即用户栈);
压入当前CS:IP(用户态)(ret = CS:IP)

4.进入内核,执行system_call。

1.中断入口

int对应的中断处理函数是 system_call,int执行时,是用户态,执行完,进入内核态,如图,0x80 对应sysstem_call
在这里插入图片描述
在这里插入图片描述

1.刚进入内核,首先在内核态中的各种寄存器压到栈中,即保护现场。

2.执行sys_fork(),继续向下执行,但在执行sys_fork的时候可能引起切换

3.接下来看当前PCB中的state是否等于0,如果不是那么就要进行调度,就是靠
schedule,完成五端论中的中间三步

state(%eax)相当于state + _current,0(就绪或运行态)作比较,非0即阻塞,
_current即PCB,阻塞则调度(reschedule)

4.再看看它的时间片是否等于0,时间片用光了也要进行调度
再次判断counter + _current 判断是否时间片用尽,若是则切换(reschedule

5.执行中断返回的函数ret_from_sys_call,iret也就是从内核栈到用户栈的切换


system_call.s

_system_call:
    push %ds..%fs
    pushl %edx...
    call _sys_call_table(,%eax,4)
    pushl %eax    //把系统调用号入栈。
//刚进入内核,_system_call将用户态信息压栈,通过 sys fork table 调用 sys_fork

reschedule执行的是_schedule().

reschedule:
;将ret_from_sys_call 的地址入栈,,reschedule遇到 } 出栈,弹出ret_from_sys_call
pushl $ret_from_sys_call
jmp _schedule ;调用schedule

2.中断出口

中断入口: 建立 内核栈和用户栈 的关联 ,sys_fork与中间三段有关,然后先看中断出口
中断出口这里完成第二次切换,从内核栈切换到用户栈
在这里插入图片描述
还原现场,并恢复到用户态

# 中断返回,执行中断返回函数,从内核栈,切换到用户栈
ret_from_sys_call:
...
popl %eax # 弹出信号值,出栈,与中断入口的push对应
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret    # 将内核栈的内容出栈,切换到 下一个进程的TCB

二、切换

1.schedule

schedule()是调度函数
next是下一个进程的PCB,核心是switch_to

void schedule (void)
{
	next = i;
    //找到下一个线程的TCB next,切换到下一个线程
    ...
    switch_to (next);       // 切换到任务号为next 的任务,并运行之
}

2.Switch_to(内核栈切换)

linux 0.11 中基于TSS(Task Struct Segement) 切换,但也可以用栈切换,因为tss中的信息可以写到内核栈中

TSS是一个段,即一块内存,这里保存要切换的进程的cpu信息,包括各种寄存器的值、局部描述表ldt的段选择子等,切换时cpu会将这段内容存进各自对应的寄存器,然后就完成了切换。(任务切换或者说CPU状态更新实质上就是改变各个寄存器的值)

//32位TSS段结构
struct TSS32
{
    int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
    int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
    int es, cs, ss, ds, fs, gs;
    int ldtr, iomap;
}

每一个进程都有自已的TSS和LDT,而TSS(任务描述符)和LDT(私有描述符)必须放在GDT中

使用的话首先要在GDT表中设置一个TSS段,就是保存TSS段的位置,然后将TSS段对应的是段选择子存入TR寄存器,告诉cpu这个TSS段在哪里。按照intel最初的设计,每个任务或者进程都应该设置一个TSS段,任务切换时直接将对应的TSS段的内存加载到CPU就行了。

但是后来发现这种设计会带来过多的系统开销,每次切换都要将所有的寄存器更新,需要数百个指令周期,因此主流的操作系统均不使用这种方法。linux采取的方法是绕开TSS段进行任务切换,每个CPU仅设置一个TSS段,仅使用esp0和iomap,采用软件方法切换寄存器,节省了开销。

switch_to 通过 TSS(任务结构段) 实现切换,ljmp 是长跳转指令,如图
在这里插入图片描述
黄色的是原TSS,绿的是新TSS,下边 GDT(全局描述符表Global Descriptor Table保存的是TSS的描述符

粉色的是 CPU当前的寄存器段信息,TR是一个选择子,可以根据TR找到当前进程的tss

切换就是 将 CPU的寄存器信息 写入当前线程的TSS中,TR指向新的TSS(n) 的段描述符,再找到新的TSS,将新的TSS段内容 载入 CPU的寄存器ESP中

swith_to内嵌宏定义

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__( "cmpl %%ecx,_current\n\t" \    // 任务n 是当前任务吗?(current ==task[n]?)
  "je 1f\n\t" \         // 是,则什么都不做,退出。
  "movw %%dx,%1\n\t" \      // 将新任务的选择符*&__tmp.b。
  "xchgl %%ecx,_current\n\t" \  // current = task[n];ecx = 被切换出的任务。
  "ljmp %0\n\t" \       // 执行长跳转至*&__tmp,造成任务切换。
  // %0 是 "m"(*&__tmp.a),%1 是 "m"(*&__tmp.b)
// 在任务切换回来后才会继续执行下面的语句。
  "cmpl %%ecx,_last_task_used_math\n\t" \   // 新任务上次使用过协处理器吗?
  "jne 1f\n\t" \        // 没有则跳转,退出。
  "clts\n" \            // 新任务上次使用过协处理器,则清cr0 的TS 标志。
  "1:"::"m" (*&__tmp.a), "m" (*&__tmp.b),
  "d" (_TSS (n)), "c" ((long) task[n]));
}

3.sys_fork

创建一个进程(或内核级线程),就是要做成能切换的样子

system_call.s
在这里插入图片描述

# 根据父进程,创建子进程,copy_press前,将参数压栈,这些参数是父进程在用户态的样子
_sys_fork:
call _find_empty_process # 调用find_empty_process()(kernel/fork.c)。
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax

call _copy_process # 调用C 函数copy_process()(kernel/fork.c)。
addl $20,%esp # 丢弃这里所有压栈内容。
ret

_copy_process 调用copy_process()函数

copy_process 将父进程的栈都作为参数,C语言中参数越靠后越靠近栈顶

作用如图:
在这里插入图片描述

在这里插入图片描述
注:申请内存空间,注意这是在内核中,用get_free_page(),而不是malloc

同时还要设置TTS,是使能够切换
在这里插入图片描述

父进程与子进程 内核栈不同,用户栈相同

copy_process ()函数

/*
* Ok, this is the main fork-routine. It copies the system process
* information (task[nr]) and sets up the necessary registers. It
* also copies the data segment in it's entirety.
*/
/*
* OK,下面是主要的fork 子程序。它复制系统进程信息(task[n])并且设置必要的寄存器。
* 它还整个地复制数据段。
*/
// 复制进程。
int
copy_process (int nr, long ebp, long edi, long esi, long gs, long none,
          long ebx, long ecx, long edx,
          long fs, long es, long ds,
          long eip, long cs, long eflags, long esp, long ss)
{
  struct task_struct *p;
  int i;
  struct file *f;

  p = (struct task_struct *) get_free_page ();  // 获取一页空闲内存作为PCB,一页是4k
  ……
  p->state = TASK_UNINTERRUPTIBLE;  // 将新进程的状态先置为不可中断等待状态。
  p->pid = last_pid;        // 新进程号。由前面调用find_empty_process()得到。
  p->father = current->pid; // 设置父进程号。
  p->counter = p->priority;
  ……
  // 设置TSS
  p->tss.esp0 = PAGE_SIZE + (long) p;   // esp0 正好指向该页顶端,PAGE_SIZE=4k,p是刚申请的内存空间
  p->tss.ss0 = 0x10;        // 堆栈段选择符(内核数据段)[??]。
  p->tss.eip = eip;     // 指令代码指针。
  p->tss.eflags = eflags;   // 标志寄存器。
  p->tss.eax = 0;
  p->tss.ecx = ecx;
  p->tss.cs = cs & 0xffff;
  ……
  p->tss.ldt = _LDT (nr);   // 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。
  ……
// 在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。
// 在任务切换时,任务寄存器tr 由CPU 自动加载。
  set_tss_desc (gdt + (nr << 1) + FIRST_TSS_ENTRY, &(p->tss));
  set_ldt_desc (gdt + (nr << 1) + FIRST_LDT_ENTRY, &(p->ldt));
  p->state = TASK_RUNNING;  /* do this last, just in case */
/* 最后再将新任务设置成可运行状态,以防万一 */
  return last_pid;      // 返回新进程号(与任务号是不同的)。
}

小结:

1.fork有中断,中断会调用 system_call

2.system_call的作用:

(1) 调用sys_fork,调用 copy_process,父进程与子进程 内核栈不同,用户栈相同

(2) 判断cmpl $0,state(%eax),非0表示阻塞,调用 reschedule 进程调度 reschedule 调用 schedule,schedule调用 switch_to(switch_to中ljmp实现长跳转,子进程将 TSS的内容复制到 CPU上,TSS图中粉色的部分)

(3) iret 内核栈出栈
a.子进程回到用户栈,执行的是 中断下边的一句代码:mov res, %eax ,res = %eax = 0

b.父进程回到用户栈,执行的也是 中断下边的一句代码:mov res, %eax,
父进程 eax != 0

程序在用户态执行,切换时找到 自己的内核栈, 找到TCB,通过switch_to 完成TCB的切换,完成内核栈的切换,再完成用户栈的切换

4.fork()典例

基本使用格式:

if(!fork()) 
{
    //子进程执行
} 
else
{
    //父进程执行
}

shell终端的命令的执行

int main(int argc, char * argv[])
{
    while(1)
    {
        scanf("%s", cmd);
        if(!fork())
        {
            exec(smd); // 执行子进程命令
        }
        wait(0); // 执行父进程命令,shell等待用户输入
    }

}

exec 是一个系统调用,会执行 system_call

子进程将进入if块内,调用exec,子进程将被更换新的代码,如下图

ThreadCreate(*A)创建一个进程:
在这里插入图片描述
更换新的代码,我们知道iret指令将把栈弹出,这时CS:EIP将被更改到用户态代码段,那么我们只需要更改栈中储存的CS:IP即可,先偏移量EIP=0x1C,并将EIP+%esp压入栈中,即EIP在栈中位置,执行do_execve。

子进程A执行:
在这里插入图片描述
do_execve,将程序入口地址给eip,更改代码段;eip[3]正好等于SP,更改栈。

_system_call:
    push %ds ... %fs
    pushl %edx...
    call sys_execve
# sys_execve执行前,执行的是 父进程的代码

_sys_execve:
    lea EIP(%esp),%eax  # EIP = 0x1C是十进制的28,将%esp偏移28,eip的地址复制给eax
    pushl %eax
    call _do_execve
# 子进程通过 _sys_execve 退出内核(通过IRET实现中断返回),回到用户态,执行新的子进程的代码

int do_execve(* eip, ...)
{
    p += change_ldt(...;
    eip[0] = ex.a_entry;// ex.a_entry是可执行程序入口地址,产生可执行文件时 写入
    eip[3] = p;
    // eip[0]=esp + 0x1C; 28的位置存的子进程的入口
    // eip[3]=esp+0x1C+0x0C
    ...
}

总结

提示:这里对文章进行总结:

1.理解switch_to对应的栈切换, 将自己变成计算机
2.ThreadCreate的目的就是初始化这样一套栈
如图
在这里插入图片描述

切换过程:
程序在用户态执行,切换时找到 自己的内核栈, 找到TCB,通过switch_to 完成TCB的切换,完成内核栈的切换,再完成用户栈的切换

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Super.Bear

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值