12.线程的引出与实现——核心级线程实现实例

1.从进入内核开始——从某个中断开始

main()
{
    // main先执行A,将B的开始地址压栈,即 A函数返回地址 是 B的开始地址
    A();
    B();
}

A()
{
    //进程由资源和执行序列组成,fork会创建资源和执行序列(线程)
    //fork 是 创建系统进程的调用,会引起中断,可能会引起 内核级线程 切换
    fork();
}

fork:
    ...
    INT 0x80
    mov res,%eax
//fork中有中断,执行 INT 0x80时,CPU找到对应的内核栈,将 SS SP PC CS 压栈
// SS SP 指向用户栈,PC = mov res,%eax 即 INT 的下一句
// int对应的中断处理函数是 system_call,int执行时,是用户态,执行完,进入内核态

2.中断入口 和 中断出口

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

五段论 第一步:中断入口(建立 内核栈和用户栈 的关联),sys_fork与中间三段有关,我们先看 中断出口

接着看 _system_call

movl _current,%eax # 取当前任务(进程)数据结构地址%eax。
# 查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序。
# 如果该任务在就绪状态但counter[??]值等于0,则也去执行调度程序。
cmpl $0,state(%eax) # state,state(%eax),是将state + %eax,eax = _current,_current = PCB,其实就是判断PCB是否是 0 ,0 表示就绪或执行,非0表示阻塞
jne reschedule # 如果是 非0,就会发生调度
cmpl $0,counter(%eax) # counter,完成切换,判断counter(%eax)   是否是0,eax = _current,counter是时间片
je reschedule    # 相等,调度
# 中断返回,执行中断返回函数,从内核栈,切换到用户栈
ret_from_sys_call:
...
popl %eax # 弹出信号值,出栈,与中断入口的push对应
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret    # 将内核栈的内容出栈,切换到 下一个进程的TCB
reschedule:
pushl $ret_from_sys_call # 将ret_from_sys_call 的地址入栈,,reschedule遇到 } 出栈,弹出ret_from_sys_call
jmp _schedule   # 调用 schedule

sched.c

/*
 * 'schedule()' is the scheduler function. This is GOOD CODE! There
 * probably won't be any reason to change this, as it should work well
 * in all circumstances (ie gives IO-bound processes good response etc).
 * The one thing you might take a look at is the signal-handler code here.
 *
 * NOTE!! Task 0 is the 'idle' task, which gets called when no other
 * tasks can run. It can not be killed, and it cannot sleep. The 'state'
 * information in task[0] is never used.
 */
/*
 * 'schedule()'是调度函数。这是个很好的代码!没有任何理由对它进行修改,因为它可以在所有的
 * 环境下工作(比如能够对IO-边界处理很好的响应等)。只有一件事值得留意,那就是这里的信号
 * 处理代码。
 * 注意!!任务0 是个闲置('idle')任务,只有当没有其它任务可以运行时才调用它。它不能被杀
 * 死,也不能睡眠。任务0 中的状态信息'state'是从来不用的。
 */
void schedule (void)
{
    //找到下一个线程的TCB next,切换到下一个线程
    ...
    switch_to (next);       // 切换到任务号为next 的任务,并运行之
}
#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]));
}

TSS(Task Struct Segment),任务结构段
switch_to 通过 TSS 实现切换
ljmp 是长跳转指令,原理如下图

TSS

上图 黄色的是 原TSS,绿的是 新TSS,下边 GDT(全局描述符表Global Descriptor Table)保存的是TSS的描述符
粉色的是 CPU当前的寄存器段信息,TR是CPU的任务段

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

核心代码:INT ljmp IRET

2.1 寄存器解释

eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。
比方说:add eax,-2 ; //可以认为是给变量eax加上-2这样的一个值。
这些32位寄存器有多种用途,但每一个都有“专长”,有各自的特别之处。
EAX 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。
EBX 是”基地址”(base)寄存器, 在内存寻址时存放基地址。
ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
EDX 则总是被用来放整数除法产生的余数。
ESI/EDI分别叫做”源/目标索引寄存器”(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
EBP是”基址指针”(BASE POINTER), 它最经常被用作高级语言函数调用的”框架指针”(frame pointer).
ESP 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。
esp:寄存器存放当前线程的栈顶指针
ebp:寄存器存放当前线程的栈底指针
eip:寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。
来源: http://blog.csdn.net/chenlycly/article/details/37912755

3. 五段的中间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

fork.c

copy_process :
- 申请内存空间
- 创建 TCB
- 创建内核栈和用户栈
- 填写两个栈
- 关联栈和TCB
父进程与子进程 内核栈不同,用户栈相同

/*
* 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的作用:
    2.1 调用sys_fork,调用 copy_process,父进程与子进程 内核栈不同,用户栈相同
    2.2 判断cmpl $0,state(%eax),非0表示阻塞,调用 reschedule 进程调度
    - reschedule 调用 schedule,schedule调用 switch_to(switch_to中ljmp实现长跳转,子进程将 TSS的内容复制到 CPU上,TSS图中粉色的部分)
    2.3 iret 内核栈出栈
    • 子进程回到用户栈,执行的是 中断下边的一句代码:mov res, %eax ,res = %eax = 0
    • 父进程回到用户栈,执行的也是 中断下边的一句代码:mov res, %eax,父进程 eax != 0

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

4.fork的使用

调用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

_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
    ...
}

子进程要执行 ls,ls 对应一个可执行文件,可执行文件有 文件头、入口地址
文件头 是编译时 写入的

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值