zoukankan      html  css  js  c++  java
  • linux0.11相关进程数据结构 GDT,LDT,GDTR,LDTR

    http://www.cppblog.com/jake1036/archive/2010/11/13/133530.html


    1 进程结构
        union task_union{
         struct task_struct task ;
         char stack[PAGE_SIZE]  ;
      }
      这实际上是一个内存页,页的底部是进程控制块结构。其余部分是作为进程的内核态堆栈使用。
     
    2 task 数组

    struct task_struct * task[NR_TASKS] = {&(init_task.task), };

    这个数组中存储的是task_struct 结构的指针,但是实际上数组中的每一项都指着一块内存页。


    3 任务段数据 
      

    struct tss_struct {

           long back_link;      /* 16 high bits zero */

           long esp0;

           long ss0;         /* 16 high bits zero */

           long esp1;

           long ss1;         /* 16 high bits zero */

           long esp2;

           long ss2;         /* 16 high bits zero */

           long cr3;

           long eip;

           long eflags;

           long eax,ecx,edx,ebx;

           long esp;

           long ebp;

           long esi;

           long edi;

           long es;          /* 16 high bits zero */

           long cs;          /* 16 high bits zero */

           long ss;           /* 16 high bits zero */

           long ds;          /* 16 high bits zero */

           long fs;           /* 16 high bits zero */

           long gs;          /* 16 high bits zero */

           long ldt;         /* 16 high bits zero */

           long trace_bitmap;  /* bits: trace 0, bitmap 16-31 */

           struct i387_struct i387;        

    };


    4  进程控制块
        

    struct task_struct {

    /*----------------------- these are hardcoded - don't touch -----------------------*/

           long state;       // 进程运行状态(-1不可运行,0可运行,>0以停止)

           long counter;  // 任务运行时间片,递减到0是说明时间片用完

           long priority;  // 任务运行优先数,刚开始是counterpriority

           long signal;     // 任务的信号位图,信号值=偏移+1

           struct sigaction sigaction[32];       //信号执行属性结构,对应信号将要执行的操作和标志信息

           long blocked;  // 信号屏蔽码

    /*----------------------------------- various fields--------------------------------- */

           int exit_code;  // 任务退出码,当任务结束时其父进程会读取

           unsigned long start_code,end_code,end_data,brk,start_stack;

                  // start_code   代码段起始的线性地址

                  // end_code     代码段长度

                  // end_data      代码段长度+数据段长度

                  // brk             代码段长度+数据段长度+bss段长度

                  // start_stack   堆栈段起始线性地址

           long pid,father,pgrp,session,leader;      

                  // pid       进程号

                  // father   父进程号

                  // pgrp     父进程组号

                  // session 会话号

                  // leader 会话首领

           unsigned short uid,euid,suid;

                  // uid       用户标id

                  // euid     有效用户id

                  // suid     保存的用户id

           unsigned short gid,egid,sgid;

                  // gid       id

                  // egid     有效组id

    // sgid     保存组id

           long alarm;     // 报警定时值

           long utime,stime,cutime,cstime,start_time;

                  // utime   用户态运行时间

                  // stime    内核态运行时间

                  // cutime  子进程用户态运行时间

                  // cstime  子进程内核态运行时间

                  // start_time    进程开始运行时刻

           unsigned short used_math;     // 标志,是否使用了387协处理器

    /* ----------------------------------file system info-------------------------------- */

           int tty;            // 进程使用tty的子设备号,-1表示没有使用

           unsigned short umask;    //文件创建属性屏蔽码

           struct m_inode * pwd;   // 当前工作目录的i节点

           struct m_inode * root;    // 根目录的i节点

           struct m_inode * executable;  // 可执行文件的i节点

           unsigned long close_on_exec; // 执行时关闭文件句柄位图标志

           struct file * filp[NR_OPEN]; // 进程使用的文件

    /*------------------ ldt for this task 0 - zero 1 - cs 2 - ds&ss -------------------*/

           struct desc_struct ldt[3];        // 本任务的ldt表,0-空,1-代码段,2-数据和堆栈段

    /* ---------------------------------tss for this task ---------------------------------*/

           struct tss_struct tss;        // 本任务的tss

    };


     5   linux进程结构
      
          (1)  在linux中gdt中的每一项,都有两个表项,一个是ldt描述符,另一个是tss描述符。
          (2)  在task数组中占有一项,每一项是一个物理页面,物理内存页面底端是进程控制块,内存页面的其余部分是内核态堆栈。
          (3)  task数组中的表项和gdt中的表项是一一对应的。 对于一个在task数组中的任务项是nr的任务来说,它的tss描述符在gdt中描述符
                的位置是,gdtr + 3*8 + 16 * nr ,ldt描述符在gdt中的描述符的位置是 gdtr + 3 * 8 + 16 * nr + 8 。
          (4) 对应于表项为nr的进程,它对应的页目录项是16 * nr --------16 * (nr + 1) 。
         



    6 进程0
        进程0是一个特殊的进程,它是所有进程的祖先进程,所有其他的进程都是复制进程0或者其后代进程产生的。 但是进程0不是。
        下面主要讲一下 进程0的创建顺序:
        (1) 进程控制块和页目录页表的手动创建
                       
         以下就是一个任务的初始过程
    #define INIT_TASK 
    /* state etc */    0,15,15,     
    /* signals */    0,{{},},0
    /* ec,brk */    0,0,0,0,0,0
    /* pid etc.. */    0,-1,0,0,0
    /* uid etc */    0,0,0,0,0,0
    /* alarm */    0,0,0,0,0,0
    /* math */    0
    /* fs info */    -1,0022,NULL,NULL,NULL,0
    /* filp */    {NULL,}
        

            
    {0,0},                 // ldt第0项是空
    /* ldt */    {0x9f,0xc0fa00},         //代码段长640K,基地0,G=1,D=1,DPL=3,P=1,TYPE=0x0a
            {0x9f,0xc0f200},         //数据段长640K,基地0,G=1, D=1, DPL=3,P=1, TYPE=0x02
        }

    /*tss*/    {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,
            
    // esp0 = PAGE_SIZE+(long)&init_task    内核态堆栈指针初始化为页面最后
            
    // ss0 = 0x10    内核态堆栈的段选择符,指向系统数据段描述符,进程0的进程控制
            
    //            块和内核态堆栈都在system模块中
            
    // cr3 = (long)&pg_dir 页目录表,其实linux0.11所有进程共享一个页目录表
         0,0,0,0,0,0,0,0
         
    0,0,0x17,0x17,0x17,0x17,0x17,0x17
         _LDT(
    0),0x80000000,     // ldt表选择符指向gdt中的LDT0处
            {} 
        }

    }


      
      进程0的数据段基址为0,段限长为640KB ,代码段基址为0,段限长为640KB。任务0的数据段和代码段 和系统的代码段和数据段是重合的。
      进程0的内核态堆栈和进程控制块都是位于系统模块内。
     
    (2)在main模块中调用了,sched_init()函数加载了 进程0的进程0tss段描述符,ldt段描述符,并且加载TR寄存器,使它指向进程0tss段,这时候
             进程0才完成了启动。
       
        
    /*****************************************************************************/
    /* 功能:    1.    初始化task数组和GDT(包括设置进程1的LDT和TSS)            */
    /*            2.    加载TR和IDTR寄存器                                        */
    /*            3.    设置时钟中断门和系统调用中断门                                */
    /* 参数:    (无)                                                            */
    /* 返回:    (无)                                                            */
    /*****************************************************************************/
    void sched_init(void)
    {
        
    int i;
        
    struct desc_struct * p;

        
    if (sizeof(struct sigaction) != 16)
            panic(
    "Struct sigaction MUST be 16 bytes");
    // 在gdt中设置进程0的tss段描述符
        set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
    // 在gdt中设置进程0的ldt段描述符
        set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
    // 下面的循环把gdt和task中其他的项清空
        p = gdt+2+FIRST_TSS_ENTRY;
        
    for(i=1;i<NR_TASKS;i++{
            task[i] 
    = NULL;
            p
    ->a=p->b=0;
            p
    ++;
            p
    ->a=p->b=0;
            p
    ++;
        }

    /* Clear NT, so that we won't have troubles with that later on */
        __asm__(
    "pushfl ; andl $0xffffbfff,(%esp) ; popfl");
        ltr(
    0);    // 把进程0的tss段加载到TR寄存器
        lldt(0);    // 把进程0的ldt段加载到IDTR寄存器。
                
    // 这是将gdt中进程0的ldt描述符对应的选择符加载到TR中。CPU将
                
    // 选择符加载到可见部分,将tss的基地址和段长等加载到不可见部分。
                
    // TR寄存器只在这里明确加载一次,以后新任务ldt的加载是CPU根据
                
    // TSS段中LDT字段自动加载。
    // 初始化8253定时器
        outb_p(0x36,0x43);        /* binary, mode 3, LSB/MSB, ch 0 */
        outb_p(LATCH 
    & 0xff , 0x40);    /* LSB */
        outb(LATCH 
    >> 8 , 0x40);    /* MSB */
        set_intr_gate(
    0x20,&timer_interrupt);        // 设置时钟中断门
        outb(inb_p(0x21)&~0x01,0x21);
        set_system_gate(
    0x80,&system_call);        // 设置系统调用中断门
    }






        (3)切换回用户态。
        
    // 把进程0从内核态切换到用户态去执行,使用的方法是模拟中断调用返回
    // 利用指令iret完成特权级的转变。
    #define move_to_user_mode() 
    __asm__ (
    "movl %%esp,%%eax "         // 当前堆栈指针保存到eax中
    "pushl $0x17 "     // 当前堆栈段选择符0x17入栈,它指向进程0的数据段描述符// 因为进程0的代码段、数据段、内核代码段、数据段4者重
    // 合,所以它指向的仍然是内核模块区域。
        "pushl %%eax "     // 把当前堆栈指针入栈。这样模拟外层堆栈的SS:ESP。
                            
    // 由于进程0数据段选择符0x17对应的还是内核模块,和
    // 内核数据段选择符0x10的差别仅在与对应描述符的dpl和
    // 本身rpl的不同,所以外层堆栈指针指向的还是原来的堆栈
    // 即user_stack
        "pushfl "             // eflags入栈
        "pushl $0x0f "         // 进程0代码段选择符入栈,模拟返回的CS
        "pushl $1f "         // 下面标号1的偏移地址入栈,模拟返回的EIP
                            
    // 也是由于4段重合,所以这里返回的CS对应的段的基地址与
                            
    // 内核代码段基地址一样,都是0,故将返回的CS:EIP就是下
                            
    // 面标号1处。
        "iret "                 // 中断返回。由于当前CPL=0,将返回的CS的RPL=3,所以
                            
    // 不仅仅要改变CS,EIP,还要发生堆栈切换(但实际上堆栈
    // 还是user_stack),同时CPL变成3。
        "1: movl $0x17,%%eax "     // 把数据段寄存器的值设为进程0的数据段
        "movw %%ax,%%ds " 
        
    "movw %%ax,%%es " 
        
    "movw %%ax,%%fs " 
        
    "movw %%ax,%%gs" 
        :::
    "ax")
     


    6 用fork创建进程
       除了进程0,所有其他的进程都是由fork()系统调用创建的,子进程是通过复制父进程的数据和代码而产生的。
       创建结束之后,子进程与父进程的代码和数据共享,但是子进程有自己的进程控制块,内核堆栈和页表。
     
      一个进程需要以下三中数据结构
       (1) 进程控制块 task__struct 。
       (2) gdt中的tss 和ldt描述符。
       (3)页目录项和页表项。
        所以fork系统调用的任务就是创建进程的上述三个部分。
        sys_fork()函数分两步实现,第一步 首先调用,find_empty_process() 函数,第二步调用 copy_process()函数,复制进程。

      
    _sys_fork:
    // 第一步,调用find_empty_process()函数,找task[]中的空闲项。
    // 找到后数组下标放在eax中。如果没找到直接跳转到ret指令。
        call _find_empty_process
        testl 
    %eax,%eax
        js 1f
        push 
    %gs        // 中断时没有入栈的寄存器入栈,
    // 作为copy_process() 函数的参数
        pushl %esi
        pushl 
    %edi
        pushl 
    %ebp
        pushl 
    %eax
     
    // 第二步,调用copy_process() 函数复制进程。
        call _copy_process    
        addl $
    20,%esp
    1:    ret


      内存复制函数
       
    copy_mem

    /*****************************************************************************/
    /*    功能:设置新进程的LDT项(数据段描述符和代码段描述符)中的基地址部分     */
    /*          并且复制父进程(也就是当前进程)的页目录和页表,                      */
    /*          实现父子进程数据代码共享                                              */
    /*    参数:    nr    新进程任务数组下标                                             */
    /*            p    新进程的进程控制块                                             */
    /*    返回:    0 (成功),    -ENOMEM(出错)                                     */
    /*****************************************************************************/

    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);    // 取当前进程代码段长度
        data_limit=get_limit(0x17);    // 取当前进程数据段长度
        old_code_base = get_base(current->ldt[1]);    // 取当前进程代码段基地址,这是线性地址
        old_data_base = get_base(current->ldt[2]);    // 取当前进程数据段基地址,这是线性地址
        
    // 0.11进程代码段和数据段基地址必须重合
    if (old_data_base != old_code_base)
            panic(
    "We don't support separate I&D");
        
    //0.11中数据段代码段的基地址是重合的,都是nr*64M(nr是task[]数组下标),所以
        
    //数据段的长度肯定大于代码段长度。而且 copy_page_tables()传入的是data_limit,这
        
    // 把代码和数据都包含进去了。
        if (data_limit < code_limit)
            panic(
    "Bad data_limit");
        
    // 新进程的代码段基地址 = 数据段基地址 = 64M*nr
        new_data_base = new_code_base = nr * 0x4000000;
        
    // 设置进程的起始线性地址
        p->start_code = new_code_base;
        
    // 设置新进程的ldt项。在copy_process()中完全复制父进程的ldt,所以
        
    // 只需重新设置ldt的基地址字段,其他字段和父进程一样
        set_base(p->ldt[1],new_code_base);
        set_base(p
    ->ldt[2],new_data_base);
        
    // 把线性地址old_data_base处开始,一共data_limit个字节的内存对应的页目录、
    // 页表复制到线性地址new_data_base。这里仅仅复制相关的页目录和页表,使它们
    // 指向同一个物理页面,实现父子进程数据代码共享。
        if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
            free_page_tables(new_data_base,data_limit);
            
    return -ENOMEM;
        }

        
    return 0;
    }



    复制进程


    /*****************************************************************************/
    /*    功能:复制进程,把当前进程current复制到task[nr]                             */
    /*    参数:当前进程(current)内核堆栈的所有内容                                 */
    /*          当前进程内核堆栈保存了所有寄存器的值,在程序中要把这些寄存器的值     */
    /*          全部复制给子进程,从而给子进程创造和父进程一样的运行环境             */
    /*    返回:子进程pid                                                             */
    /*****************************************************************************/
    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;
    // 在主内存区申请一页新的内存,用来放置子进程的task_struct和内核堆栈
    // get_free_page()返回的是物理地址
        p = (struct task_struct *) get_free_page();
        
    if (!p)
            
    return -EAGAIN;
    // 设置task数组中相关项
        task[nr] = p;
    // 下面的赋值语句仅仅把父基础的task_struct部分全部复制给子进程
    // 注意:仅仅复制task_struct部分,内核堆栈不复制,因此子程序的内核堆栈
    //          是空的,这也是我们希望的
        *= *current;    /* NOTE! this doesn't copy the supervisor stack */
    // 下面的很多赋值语句修改子进程的task_struct中若干字段
    // 这些字段跟父进程是有差别的
        p->state = TASK_UNINTERRUPTIBLE;    //子进程设为不可中断状态
        p->pid = last_pid;            // 设置子进程pid
        p->father = current->pid;    // 把当前进程pid舍为子进程的father
        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;    // 子进程开始时间
        p->tss.back_link = 0;
    // 子进程的内核堆栈指针设置为task_struct所在页面的最高端
        p->tss.esp0 = PAGE_SIZE + (long) p;    
    // 子进程的内核堆栈选择符为0x10,指向GDT中系统数据段。
    // 注意 虽然子进程的内核堆栈位于内核system模块外,在主内存区,但是因为系统数据段
    //        基地址为0,限长为16M,函概了所有物理内存,故子进程内核堆栈也位于系统数
    //        段内。esp0要的是段内偏移,也是因为系统数据段基地址为0,物理地址
    //        PAGE_SIZE + (long) p 也是段内偏移。
    p->tss.ss0 = 0x10;
    // 把父进程系统调用返回地址赋给子进程当前运行的eip。这样当子进程被调度程序选中
    // 后他从fork返回地址处开始执行。
        p->tss.eip = eip;
        p
    ->tss.eflags = eflags;
    // eax是函数返回值存放的地方,把子进程的eax设置为0,这样fork在子进程中返回的是0。
    // 注意 子进程并没有执行fork()函数,子进程的系统堆栈没有进行过操作,当然不会有像
    //        父进程那样的fork函数调用。但是当子进程开始运行时,就好像它从fork中返回。
        p->tss.eax = 0;        
        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;
    // 设置子进程的ldt。从这里可以看到,task下标为nr的进程在GDT中的2项一定是
    // _LDT(nr)和_TSS(nr)。task[]中的项和GDT中的2项一一对应。
        p->tss.ldt = _LDT(nr);
        p
    ->tss.trace_bitmap = 0x80000000;
        
    if (last_task_used_math == current)
            __asm__(
    "clts ; fnsave %0"::"m" (p->tss.i387));
    // 在copy_mem函数中设置子进程的代码段描述符,数据段描述符,并且复制父进程的
    // 页目录、页表。实现和父进程代码数据的共享。
        if (copy_mem(nr,p)) {
            task[nr] 
    = NULL;
            free_page((
    long) p);
            
    return -EAGAIN;
        }

    // 子进程继承父进程打开的文件,所以文件引用数目要加一
        for (i=0; i<NR_OPEN;i++)
            
    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++;
    // GDT中对应位置(和nr对应)放入子进程的TSS描述符、LDT描述符
        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 */
    // 父进程返回子进程的pid
        return last_pid;
    }




      7 进程的结束
     

    进程结束的时候,需要关闭的资源主要有:
      (1)  释放所有的物理页面。(子进程自己清除
      (2)  关闭所有打开的文件。(子进程自己清除
      (3)  清除task[] 中的相应的项。(父进程自己清除

      子进程通过exit()清除前面两个选项,将自身的状态变为TASK_ZOMBIE 。
      父进程通过调用waitpid() 将task[] 数组清空。


       一个进程的经过exit()之后,物理页表被清除 , 页表页目录项也被清除,但是它的进程控制块和内核堆栈还在,,
      此时进程的状态变为TASK_ZOMBIE ,不会再被处理器处理。不被处理但是还占用着task数组中的一个表项,这
     就成为了僵尸进程。

        子进程调用了exit()函数之后,就通知父进程,父进程调用waitpid() 来清除 task数组中的表项。但是很有可能,
        父进程没有执行waitpid()操作,情况如下:
       (1) 父进程早于子进程执行exit()函数。
       (2) 子进程僵死,但是父进程没有调用waitpid()操作。
       (3) 父进程调用了waitpid(),但是因为某种愿意没有释放资源。
       
       解决方法:
        如果父进程无法释放资源,那么就让进程1来释放资源。
        当一个父进程早于子进程exit()的时候,它把所有的子进程过继给父进程。

  • 相关阅读:
    linux会话浅析
    linux memory lock浅析
    浅谈动态库符号的私有化与全局化
    LINUX内核内存屏障
    linux内存屏障浅析
    linux内核mem_cgroup浅析
    记一个linux内核内存提权问题
    linux内核cfs浅析
    linux内核tmpfs/shmem浅析
    linux IPv4报文处理浅析
  • 原文地址:https://www.cnblogs.com/ztguang/p/12648529.html
Copyright © 2011-2022 走看看