《Linux 0.11 Kernel :进程1的创建与执行 一》

 

了解前提: 《汇编: asm函数》

3特权级是普通进程
0特权级是内核级进程

 

// 计算机中有了一个名副其实的3特权级的进程0,第一项工作fork创建进程1

    if (!fork()) {        /* we count on this going ok */ //fork返回值1 ,不执行 init
        init();
    }
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
    for(;;) pause();

// fork函数实际上执行的是 unistd.h中的宏函数 syscall0 中去

//include/unistd.h

#define __NR_setup    0    /* used only by init, to get system going */
#define __NR_exit    1
#define __NR_fork    2
#define __NR_read    3
#define __NR_write    4
#define __NR_open    5
#define __NR_close    6
...

#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    : "=a" (__res) \
    : "0" (__NR_##name)); \
if (__res >= 0) \
    return (type) __res; \
errno = -__res; \
return -1; \
}

// include/linux/sys.h
extern int sys_fork();
extern int sys_read();
extern int sys_write();
extern int sys_open();
extern int sys_close();
...

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close,  ... }
// _syscall0 调用fork 展开后

int fork(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \  // int 0x80 是所有系统调用函数的总入口 fork是其中之一
    : "=a" (__res) \               // 第一个冒号后是输出部分,将_res赋给 eax,返回的 last_pid
    : "0" (__NR_fork)); \          // 第二个冒号后是出入部分,"0" 同上是 eax
if (__res >= 0) \                // __NR_fork 是2, 将2给 eax
    return (int) __res; \          // int 0x80 中断返回后,将执行这一句
errno = -__res; \
return -1; \
}

// __NR_fork 编号赋值给eax,即sys_fork函数在 sys_call_table中的偏移值
// 紧接着执行0x80软中断,CPU从3特权级进程0代码跳到0特权级内核代码中执行
// int 0x80 导致CPU硬件自动将ss,esp,eflags,cs,eip 按顺序压入init_task中进程0内核栈!

// CPU自动压栈完成后,跳转到 system_call.s 中的 _system_call: 处执行,继续
// 将DS,ES,FS,EDX,ECX,EBX 压栈(为了后面调用 copy_process 函数中初始化进程1中的 TSS 准备)

/* 汇编中对应C语言的函数名在前面多加一个下划线 _ , 如 _system_call 其实对应的C语言中的 system_call*/
// 其实也算是约定俗成,这样做的目的是为了防止符号名冲突,因为在一个程序中往往是包含汇编和C文件的,
// 汇编用于启动部分,C文件用于应用程序,最终通过编译器实现编译,对于编译器来说,汇编和C是一视同仁的,
// 那么就会有个问题,如果在汇编和C文件中使用了同一个名字,这是很可能出现的,毕竟汇编相当于机器码也算是稍微高级的语言,
// 在定义子程序或函数时,也是可以用英文拼写的,而C文件中,更会习惯用英文拼写。所以为了防止类似的符号名冲突,
// UNIX下的C语言就规定,C语言的源代码文件中的所有全局变量和函数经过编译后,相应的符号名前面会自动的加上下划线“_”。这样做的好处,
// 就是方便是程序开发人员,不用太小心翼翼的起名,避免了与汇编文件中的符号名的冲突。

// kernel/system_call.s
_system_call:                    # int 0x80 系统调用的总入口
    cmpl $nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds                 # 下面6个push都是为了copy_process()的参数,记住压栈的顺序,
    push %es         # 在此之前还压了5个寄存器的值进栈(进程0)
    push %fs
    pushl %edx
    pushl %ecx        # push %ebx,%ecx,%edx as parameters
    pushl %ebx        # to the system call
    movl $0x10,%edx        # set up ds,es to kernel space
    mov %dx,%ds
    mov %dx,%es
    movl $0x17,%edx        # fs points to local data space
    mov %dx,%fs
    call _sys_call_table(,%eax,4) # eax是2,看成 call (_sys_call_table +2*4)就是 _sys_fork入口
                                  # 转移到调用的子程序 _sys_fork, 4的意思就是每一项有4个字节
    pushl %eax
    movl _current,%eax
    cmpl $0,state(%eax)        # state
    jne reschedule
    cmpl $0,counter(%eax)        # counter
    je reschedule

_sys_fork:
    call _find_empty_process  # 调用 find_empty_process
    testl %eax,%eax           # 如果返回-EAGAIN(11),说明已经有64个进程在运行
    js 1f
    push %gs                  # 5个push也作为 copy_process 参数初始
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process        # 调用 copy_process
    addl $20,%esp
1:    ret
  ...

// 总结下:
// 1. _syscall0(int,fork)-> 
        _system_call: call _sys_call_table(,%eax,4)-> 
          sys_call_table[2] -> 
            sys_fork: (_sys_fork)
// 2. _sys_fork -> 1. _find_empty_process: (find_empty_process)
                   2. _copy_process:(copy_process)

// sched_init() 函数已经对 task[64] 除0项以外的所有项清空。现在调用
// find_empty_process为进程1获得一个可用的进程号和task[64]中一个位置

// 内核使用全局变量 last_pid 存放系统开机以来累计的进程数,也将此变量用作新建进程的进程号。
// 内核第一次遍历task[64] "&&" 条件成立说明last_pid已被使用。 则++last_pid ,直到获取可用的。
int find_empty_process(void)
{
    int i;

    repeat:
        if ((++last_pid)<0) last_pid=1;  // ++last_pid 如果溢出,则置位1
        for(i=0 ; i<NR_TASKS ; i++)      // 现在,++last_pid后, last_pid 为 1
            if (task[i] && task[i]->pid == last_pid) goto repeat;
    for(i=1 ; i<NR_TASKS ; i++)        // 返回第一个空闲的i
        if (!task[i])
            return i;
    return -EAGAIN;  // 如果task满了,返回 -11
}

 

// 进程0已经是一个可以创建子进程的父进程,内核中有进程0的task_struct和页表项
// 进程0将在copy_process函数中进行子进程的创建工作  
1. 为进程1创建 task_struct,将进程0的task_struct的内容复制给进程1
2. 为进程1的 task_struct ,tss 个性化设计
3. 为进程1创建第一个页表,将进程0的页表内容赋给这个页表
4. 进程1共享进程0的文件
5. 设置进程1的GDT项
6. 最后将进程1设置为就绪态,使其可以参与进程间的轮询。

// copy_process 函数的参数都是前面的代码做了压栈!
// C语言函数如 func(x,y,z), 入栈是从右到左,这样最后入栈的 eax 就是 对应第一个 nr
// C语言栈底是低地址,栈顶是高地址。 正是由于是从右往左入栈,C语言才能实现可变长参数。
 
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;

  // 在16MB内存的最高端获取一页,强制类型转换为 task_struct用
    p = (struct task_struct *) get_free_page();  
    if (!p)
        return -EAGAIN;
    task[nr] = p;   //nr等于1,为什么? call _copy_process之前pushl %eax,eax就是 find_empty_process返回值
    
    // current 指向当前进程的task_struct 的指针,当前进程是进程0,下面将父进程赋给子进程。
    // 这是父子进程创建机制的重要机制,父子进程的task_struct将完全一样
    // 注意类型指针,只复制 task_struct,并未将4KB都复制,即进程0的内核栈并未复制
    // 父进程的进程属性复制给了子进程,子进程继承了父进程的大部分能力,这是父子进程创建机制特点之一。
    
    *p = *current;    /* NOTE! this doesn't copy the supervisor stack */
    p->state = TASK_UNINTERRUPTIBLE; // 只有内核代码中明确表示将该进程设置为就绪状态才能唤醒
                                                                     // 除此之外,没有任何办法将其唤醒
                                                                     
    // 开始子进程个性化设置                                                                 
    p->pid = last_pid;          // 设置进程pid,1进程就是1    
                 
    p->father = current->pid;
    p->counter = p->priority;
    p->signal = 0;
    p->alarm = 0;
    p->leader = 0;        /* process leadership doesn't inherit */
    p->utime = p->stime = 0;
    p->cutime = p->cstime = 0;
    p->start_time = jiffies;
    
    // 开始设置子进程的TSS
    p->tss.back_link = 0;
    
    // 这里需要注意! esp0是内核栈指针,而p是一个分页的首地址4KB,
    // 内核栈指针又是从尾往前增加,所以 PAGE_SIZE + (long) p 到达尾部
    p->tss.esp0 = PAGE_SIZE + (long) p; 
    p->tss.ss0 = 0x10;   // 0x10 就是 10000, 0特权级,GDT,数据段
    p->tss.eip = eip;
    p->tss.eflags = eflags;
    p->tss.eax = 0;      // 重要!决定main()函数中 if(!fork())后面的分支走向
    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;
    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);   // 挂接子进程的 LDT
    p->tss.trace_bitmap = 0x80000000;
    if (last_task_used_math == current)
        __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
    if (copy_mem(nr,p)) {   // 设置子进程的代码段,数据段以及创建复制子进程的第一个页表
        task[nr] = NULL;      // 现在不会出现这种情况
        free_page((long) p);
        return -EAGAIN;
    }
    for (i=0; i<NR_OPEN;i++) // 下面将父进程相关文件属性的引用计数加1,表明父子进程共享文件
        if (f=p->filp[i])
            f->f_count++;
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); // 设置GDT中与子进程相关的项
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); 
    p->state = TASK_RUNNING;    /* do this last, just in case */ // 设置子进程为就绪态
    return last_pid;
}

unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasb\n\t"
    "jne 1f\n\t"                                    // 找不到空闲页,跳转到1
    "movb $1,1(%%edi)\n\t"        // 将1赋给edi+1位置,在mem map[]中
    "sall $12,%%ecx\n\t"          // ecx算数左移12位,页的相对地址
    "addl %2,%%ecx\n\t"           // LOW MEN + ecx ,页的物理地址
    "movl %%ecx,%%edx\n\t"
    "movl $1024,%%ecx\n\t"
    "leal 4092(%%edx),%%edi\n\t"  // 将edx + 4 KB 的有效地址赋给 edi
    "rep ; stosl\n\t"
    "movl %%edx,%%eax\n"
    "1:"
    :"=a" (__res)
    :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
    "D" (mem_map+PAGING_PAGES-1)
    :"di","cx","dx");   // 程序中改变过的量
return __res;
}

// 重点来了!!!
// 介绍进程0的时候,static union task_union init_task = {INIT_TASK,}; // 进程0的task_struct

union task_union {
    struct task_struct task;  // task_struct 与内核栈的共用体
    char stack[PAGE_SIZE];
};

// task_union 的设计是颇具匠心的,前面是task_struct,后面是内核栈,增长的方向正好相反,
// 正好占用一页,顺应分页机制,分配内存非常方便,而且操作系统肯定是经过反复测试,保证了
内核代码所以可能的调用导致压栈长度不会覆盖前面的task_struct.

int copy_mem(int nr,struct task_struct * p)
{
    unsigned long old_data_base,new_data_base,data_limit;
    unsigned long old_code_base,new_code_base,code_limit;

    code_limit=get_limit(0x0f);  // 0x0f即1111(3特权级,LDT,代码段)
    data_limit=get_limit(0x17);  // 0x17即二进制的10111 (3特权级,LDT,数据段)
    old_code_base = get_base(current->ldt[1]);
    old_data_base = get_base(current->ldt[2]);
    if (old_data_base != old_code_base)
        panic("We don't support separate I&D");
    if (data_limit < code_limit)
        panic("Bad data_limit");
    new_data_base = new_code_base = nr * 0x4000000; // nr现在是1,,64MB
    p->start_code = new_code_base;
    set_base(p->ldt[1],new_code_base);  // 设置子进程代码段基址
    set_base(p->ldt[2],new_data_base);  // 设置子进程数据段基址
    
    // 为进程1创建第一个页表,复制进程0的页表,设置进程1的页目录项
    if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
        free_page_tables(new_data_base,data_limit);
        return -ENOMEM;
    }
    return 0;
}

int copy_page_tables(unsigned long from,unsigned long to,long size)
{
    unsigned long * from_page_table;
    unsigned long * to_page_table;
    unsigned long this_page;
    unsigned long * from_dir, * to_dir;
    unsigned long nr;

  /* 
     0x3fffff 是4MB,是一个页表的管辖范围,二进制是22个1,||的两边必须同为0,所以,
     from 和 to 后22位必须都为0,即4MB的整数倍,意思是一个页表对应4MB的连续的线性地址空间
     必须是从 0x00000 开始的4MB的整数倍的线性地址,不能是任意地址开始的4MB,才符合分页要求
  */
    if ((from&0x3fffff) || (to&0x3fffff))
        panic("copy_page_tables called with wrong alignment");
        
    /*
       一个页目录项的管理范围是4MB,一项是4字节,项的地址就是项数*4,也就是项管理的线性地址
       起始地址的M数,比如:0项地址是0,管理范围是0~4MB,1项地址是4,管理范围是4~8MB,
       2项地址是8,管理范围是 8~12MB >>20就是地址的MB数, &0xffc 就是 &1111 1111 1100b
       就是4MB以下部分清零的地址MB数,也就是页目录项的地址
    */    
    from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
    to_dir = (unsigned long *) ((to>>20) & 0xffc);
    size = ((unsigned) (size+0x3fffff)) >> 22;   // >> 22 是 4MB数
    for( ; size-->0 ; from_dir++,to_dir++) {
        if (1 & *to_dir)
            panic("copy_page_tables: already exist");
        if (!(1 & *from_dir))
            continue;
            
        /*
         *from_dir 是页目录项中的地址,0xfffff000 是将低12位清零(0xffc) 
         高20位是页表的地址
        */    
        from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
        if (!(to_page_table = (unsigned long *) get_free_page()))
            return -1;    /* Out of memory, see freeing */
        *to_dir = ((unsigned long) to_page_table) | 7;
        nr = (from==0)?0xA0:1024;   // 0xA0  =  160 ,复制页表的项数
        for ( ; nr-- > 0 ; from_page_table++,to_page_table++) { // 复制父进程页表
            this_page = *from_page_table;
            if (!(1 & this_page))
                continue;
            this_page &= ~2;  // 设置页表项属性,2是010,~2是101,代表用户、只读、存在
            *to_page_table = this_page;
            if (this_page > LOW_MEM) { // 1MB 以内的内核区不参与用户分页管理
                *from_page_table = this_page;
                this_page -= LOW_MEM;
                this_page >>= 12;
                mem_map[this_page]++; // 增加引用计数
            }
        }
    }
    invalidate(); // 重置CR3为0,刷新“页面换高速缓存”
    return 0;
}

// 上面操作之后,此时进程1只是一个空架子,还没有应用程序,仅仅只是从进程0
// 拷贝页表,等有了自己的程序,再解除关系
// copy_process 中最后将进程1设置为就绪态,进程1就可以参与进程调度了。 最后返回进程号1
// 再看 system_call.s
_system_call:                    # int 0x80 系统调用的总入口
    cmpl $nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds                 # 下面6个push都是为了copy_process()的参数,记住压栈的顺序,
    push %es         # 在此之前还压了5个寄存器的值进栈(进程0)
    push %fs
    pushl %edx
    pushl %ecx        # push %ebx,%ecx,%edx as parameters
    pushl %ebx        # to the system call
    movl $0x10,%edx        # set up ds,es to kernel space
    mov %dx,%ds
    mov %dx,%es
    movl $0x17,%edx        # fs points to local data space
    mov %dx,%fs
    call _sys_call_table(,%eax,4) # eax是2,看成 call (_sys_call_table +2*4)就是 _sys_fork入口
                                  # 转移到调用的子程序 _sys_fork, 4的意思就是每一项有4个字节
    pushl %eax                    # sys_fork返回到这里执行,eax就是返回值 last_pid
    movl _current,%eax    # 当前进程是进程0
    cmpl $0,state(%eax)        # state
    jne reschedule        # 如果进程0不是就绪态,则进程调度
    cmpl $0,counter(%eax)    # counter
    je reschedule         # 如果进程0没有时间片,则进程调度

_sys_fork:
    call _find_empty_process  # 调用 find_empty_process
    testl %eax,%eax           # 如果返回-EAGAIN(11),说明已经有64个进程在运行
    js 1f
    push %gs                  # 5个push也作为 copy_process 参数初始
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process        # 调用 copy_process
    addl $20,%esp    #esp+=20 就是esp清20字节的栈,也就是前面压的 gs,esi,edi,ebp,eax
1:    ret            #返回_system_call中的 pushl %eax 执行
  ...

void main(void)
{
    if (!fork()) {        /* we count on this going ok */ //fork返回值1 ,不执行 init
        init();
    }

    for(;;) pause();
}

/*
pause 函数调用与fork 调用一样。 
syscall0 -> call _sys_call_table(,%eax,4) -> sys_pause()

*/

int sys_pause(void)
{
  // 将当前进程0设置为可中断等待状态,如果产生中断,或者其他进程给这个进程发送特定信号
  // 才有可能改变这个进程的状态为就绪态
    current->state = TASK_INTERRUPTIBLE;
    schedule();
    return 0;
}

void schedule(void)
{
    int i,next,c;
    struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
        if (*p) {
            if ((*p)->alarm && (*p)->alarm < jiffies) { // 如果设置了定时或定时已过
                    (*p)->signal |= (1<<(SIGALRM-1));       // 设置 SIGALRM
                    (*p)->alarm = 0;                        // alarm清0
                }
            if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
            (*p)->state==TASK_INTERRUPTIBLE)            // 现在还不是这种情况
                (*p)->state=TASK_RUNNING;
        }

/* this is the scheduler proper: */

    while (1) {
        c = -1;
        next = 0;
        i = NR_TASKS;
        p = &task[NR_TASKS];
        while (--i) {
            if (!*--p)
                continue;
                
            //找出就绪态中 counter 最大的进程
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                c = (*p)->counter, next = i;
        }
        if (c) break;
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
            if (*p)
                (*p)->counter = ((*p)->counter >> 1) +
                        (*p)->priority;
    }
    switch_to(next);
}

/*
 * Entry into gdt where to find first TSS. 0-nul, 1-cs, 2-ds, 3-syscall
 * 4-TSS0, 5-LDT0, 6-TSS1 etc ...
 */
#define FIRST_TSS_ENTRY 4
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))

#define switch_to(n) {\
struct {long a,b;} __tmp; \          // 为ljmp的CS、EIP准备的数据结构
__asm__("cmpl %%ecx,_current\n\t" \
    "je 1f\n\t" \                      // 如果进程n是当前进程,没必要切换,退出
    "movw %%dx,%1\n\t" \               // EDX的低字赋给*&__tmp.b,即把CS赋给.b
    "xchgl %%ecx,_current\n\t" \       // task[n]与task[current]交换
    "ljmp %0\n\t" \   // ljmp到 __tmp,__tmp中有偏移、段选择符,
    "cmpl %%ecx,_last_task_used_math\n\t" \ // 比较上次是否使用过协处理器
    "jne 1f\n\t" \
    "clts\n" \       // 清除CR0中的切换任务标志
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \  //.a 对应EIP, .b 对应CS
    "d" (_TSS(n)),"c" ((long) task[n])); \ // EDX是TSS n的索引号 ECX=task[n]
}

总结:
pause()函数通过int 0x80中断从3特权级的进程0代码翻转到0特权级的内核代码执行
_system_call 中的 call _sys_call_table(,%eax,4) 调用 sys_pause()->
schedule()-> switch_to()


接下来,轮到进程1执行。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

HarkerYX

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

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

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

打赏作者

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

抵扣说明:

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

余额充值