linux-0.11学习笔记(二)——从main函数到进程1执行

来源:互联网 发布:智慧社区app源码下载 编辑:程序博客网 时间:2024/06/09 23:54

2、从main函数到进程1执行


① 在main到进程执行前,必须先初始化CPU及外设,就如同在单片机编程时开启中断前的所有过程,因为我们可以想象到所需做的工作为:初始化内存、初始化陷阱门、初始化块设备、初始化字符设备、初始化tty、初始化时钟(为后面任务切换做准备)、初始化调度程序(这里将进程0的task_struct初始化,并且将task_struct首地址挂载到task数组中,tss加载到TSS段,ldt加载到LDT中,设置进程0的数据段和代码段,具体的代码起始位置通过cs:eip来寻址)、初始化缓冲区、初始化硬盘、初始化软驱这些,到这里外设初始化完成,最后开启中断,意味着已经可以开始任务调度了,这些都是为进程0的启动做准备。

② 所有的初始化工作完成后,我们发现进程0万事俱备只欠东风,即上面所说的代码执行地址,同时将进程0从内核模式切换到用户模式(使得创建的子进程依旧为用户模式),将进程0的SS、ESP、EFLAGES、CS、EIP(进程0代码起始地址)依次入栈,通过假中断返回将寄存器值弹出,开始执行进程0的代码。

//// 切换到用户模式运行。// 该函数利用iret 指令实现从内核模式切换到用户模式(初始任务0)。#define move_to_user_mode() \__asm__ ( "movl %%esp,%%eax\n\t" \ // 保存堆栈指针esp 到eax 寄存器中。"pushl $0x17\n\t" \ // 首先将堆栈段选择符(SS)入栈。  "pushl %%eax\n\t" \ // 然后将保存的堆栈指针值(esp)入栈。  "pushfl\n\t" \ // 将标志寄存器(eflags)内容入栈。  "pushl $0x0f\n\t" \ // 将内核代码段选择符(cs)入栈。  "pushl $1f\n\t" \ // 将下面标号1 的偏移地址(eip)入栈。  "iret\n" \ // 执行中断返回指令,则会跳转到下面标号1 处。  "1:\tmovl $0x17,%%eax\n\t" \ // 此时开始执行任务0,  "movw %%ax,%%ds\n\t" \ // 初始化段寄存器指向本局部表的数据段。"movw %%ax,%%es\n\t" "movw %%ax,%%fs\n\t" "movw %%ax,%%gs":::"ax")

③ 进程0紧接着开始调用int fork()函数准备创建进程1,这里fork函数是通过嵌入式汇编宏函数定义的static inline _syscall0 (int, fork)(这里_syscall0表示不带参数的系统调用宏函数),在_syscall0函数中指定返回值_res为eax寄存器值,而__NR_##name则是中断调用号(fork函数中断调用号为2),通过eax传入中断服务程序中,然后开启0x80中断,注意这时eip指向if (__res >= 0) (当然其实是if (__res >= 0) 对应的汇编代码),这个将在进程1的执行中扮演重要角色。下面硬件响应中断进入_system_call中执行。

// %0 - eax(__res),%1 - eax(__NR_##name)。其中name 是系统调用的名称,与 __NR_ 组合形成上面// 的系统调用符号常数,从而用来对系统调用表中函数指针寻址。// 返回:如果返回值大于等于0,则返回该值,否则置出错号errno,并返回-1。#define _syscall0(type,name) \type name(void) \{ \long __res; \__asm__ volatile ( "int $0x80" \ // 调用系统中断0x80。:"=a" (__res) \ // 返回值eax(__res)。:"" (__NR_##name)); \ // 输入为系统中断调用号__NR_name。      if (__res >= 0) \ // 如果返回值>=0,则直接返回该值。      return (type) __res; errno = -__res; \ // 否则置出错号,并返回-1。      return -1;} 

4、进入_system_call执行后首先得判断下中断调用号是否在正常范围内,否则就只能退出了,然后依次将DS、ES、FS、EDX、ECX、EBX入栈保存现场(其实也是向copy_process函数传参),这些做完后查询sys_call_table调用sys_fork函数(注意这里硬件自动的将eip压入栈中),在sys_fork中接着将GS、ESI、EDI、EBP、EAX压入栈内(同样是copy_process的参数),然后调用copy_process函数进行父进程向子进程的复制。

   在copy_process函数中首先必须为子进程分配一段内存空间,然后才能将父进程的管理项复制过去,故而这里首先调用了get_free_page函数给子进程分配了一个页的内存空间,估计也是为了简单或者对齐神马的直接将新页的首地址作为了任务管理项的首地址,接着进程1任务管理项首地址登记到进程槽中,标志着进程1正式创建,可以参加进程的调度了,然而此时进程1仅仅只是被创建了而已,还不能体现父进程创建子进程的特点,接着*p = *current;将当前进程任务管理项赋值给进程1任务管理项则体现了父子进程的创建机制。现在进程1和进程0一模一样系统无法分辨,还需对进程1做一些本土化设置,比如进程1的状态设置为不可中断等待状态,pid设置为last_pid(体现了与进程0的区别),设置进程1的tss和ldt,并加载进TSS段和LDT。

p->tss.back_link = 0;  p->tss.esp0 = PAGE_SIZE + (long) p; // 堆栈指针(由于是给任务结构p 分配了1 页// 新内存,所以此时esp0 正好指向该页顶端)。  p->tss.ss0 = 0x10; // 堆栈段选择符(内核数据段)  p->tss.eip = eip; // 指令代码指针。也就是压栈传入的值,指向if (__res >= 0)(在 //调用_syscall0(int,fork)时的eip),这里下了扣的,eip指向的正 //是进程1代码起始位置,进程调度后进程1得以重新执行fork //函数if (__res >= 0)后的代码,将__res返回  p->tss.eflags = eflags; // 标志寄存器。  p->tss.eax = 0; //这里下了扣的,在进程1开始执行时eax的值就是__res的值  p->tss.ecx = ecx;  p->tss.edx = edx;  p->tss.ebx = ebx;  p->tss.esp = esp;  p->tss.ebp = ebp;  p->tss.esi = esi;  p->tss.edi = edi;  p->tss.es = es & 0xffff; // 段寄存器仅16 位有效。  p->tss.cs = cs & 0xffff;  p->tss.ss = ss & 0xffff;  p->tss.ds = ds & 0xffff;  p->tss.fs = fs & 0xffff;  p->tss.gs = gs & 0xffff;  p->tss.ldt = _LDT (nr); // 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。  p->tss.trace_bitmap = 0x80000000;

当然除了tss的设置还有复制进程0的页表给进程1,共160个页表项,640K内存空间,所以在进程1设置末了共占据2个页,而且是在物理地址最末端。


5、上面int80软中断执行完返回_syscall0中if (__res >= 0)继续执行,并且返回当前进程号

  if (!fork ())    { /* we count on this going ok */      init ();    }

这里!fork ()为假,进入for (;;) pause ();执行,同样会产生一个int0x80的软中断,经过一些列步骤后进程0调用schedule()函数进行进程调度,在schedule()函数中进行两次遍历,依据进程槽task[64]这个结构,第一次遍历所有进程,然后进行第二次遍历分析时间片和进程的状态来判断需要切换到哪个进程去执行,这时候只有进程0和进程1,且进程0处于可中断等待状态,进程1处于就绪状态,调用 switch_to (next),准备切换到进程1执行。

/** switch_to(n)将切换当前任务到任务nr,即n。首先检测任务n 不是当前任务,* 如果是则什么也不做退出。如果我们切换到的任务最近(上次运行)使用过数学* 协处理器的话,则还需复位控制寄存器cr0 中的TS 标志。*/// 输入:%0 - 新TSS 的偏移地址(*&__tmp.a); %1 - 存放新TSS 的选择符值(*&__tmp.b);// dx - 新任务n 的选择符;ecx - 新任务指针task[n]。// 其中临时数据结构__tmp 中,a 的值是32 位偏移值,b 为新TSS 的选择符。在任务切换时,a 值// 没有用(忽略)。在判断新任务上次执行是否使用过协处理器时,是通过将新任务状态段的地址与// 保存在last_task_used_math 变量中的使用过协处理器的任务状态段的地址进行比较而作出的。#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,造成任务切换。// 在任务切换回来后才会继续执行下面的语句。  "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]));}

在上面的上跳转后,CPU将新任务的TSS中的值弹入CPU寄存器,也就是进程1的相应寄存器值,注意TSS中的EIP值其实是前面调用_syscall0(int,fork)时指向if (__res >= 0)的eip,所以switch_to (next)执行完后,进程1开始从if (__res >= 0)处执行(注意这里其实是在fork函数中),然后返回__res值(即为TSS中eax的值,在copy_process中埋了伏笔的),在每次进程调度后,都会在相同的位置开始新进程的执行。

if (!fork ())

    { /* we count on this going ok */

      init ();

    }

这里!fork为真进程1调用init()执行。


参考书籍:Linux内核设计的艺术 新设计团队著