您当前的位置: 首页 > 

风间琉璃•

暂无认证

  • 3浏览

    0关注

    337博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

内核级线程实现

风间琉璃• 发布时间:2021-09-25 13:18:22 ,浏览量:3

文章目录
  • 前言
  • 一、中断入口及出口
    • 1.中断入口
    • 2.中断出口
  • 二、切换
    • 1.schedule
    • 2.Switch_to(内核栈切换)
    • 3.sys_fork
    • 4.fork()典例
  • 总结

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

一、中断入口及出口

从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             
关注
打赏
1665385461
查看更多评论
0.0397s