用户进程与内存管理

一. 段选择子、段描述符、GDT、LDT、TSS

  1.段选择符

    段选择符也称为段选择子,16bit,它指向段描述符表中的段选择符,其结构如下:

  2.段描述符

   2.1 段描述符的一般格式如下:

    2.2 代码和数据段描述符类型

      S=1时,该描述符用于代码段和数据段,意义如下表:

    2.3 系统描述符类型

  如果段描述符的S标志是0,那么该描述符是一个系统描述符,处理器能够识别以下类型的系统段描述符:LDT(局部描述符表的段描述符)、TSS(任务段描述符)、调用门描述符、中断门描述符、陷阱门描述符、任务门描述符。

 

  3. GDT、LDT

    描述符表的长度可变,每个描述符的长度为8B,最多可以包含8K个这样的描述符(因为段选择子是16bit的,其中13bit用来做Index),有两种描述符表,GDT:全局描述符表,LDT:局部描述符表。结构如下:

    每个系统必须定义一个GDT,用于系统中的所有任务和程序。可选择性定义若干个LDTGDT本身不是一个段,而是线性地址空间的一个数据结构;GDT的线性基地址和长度必须加载进GDTR之中。因为段描述符长度是8,所以GDT长度位8n-1.同时,因为每个描述符长度是8,所以GDT的基地址最好进行8字节对齐。

    LDT本身是一个段内存,也是一个段,所以也有一个段描述符来描述它,这个描述符保存在GDT中。当要访问LDT时,需要使用LDT对应的段选择子,然后由段选择子找到在GDT中的对应于LDT的段描述符。为了减少访问LDT时的段转换次数,LDT的段选择符,段基址,段限长都存放在LDTR寄存器中。

    TSS是一个特殊的段。在Linux中,CPU从系统态切换到用户态时会用到TSS里面的ss0和esp0。每个CPU只维护一个TSS。

三者的关系参考如下图:

二. 一个用户进程从创建到退出的完整过程分析

  本部分通过一个应用程序(用户进程)的执行过程,来阐释Linux0.11内存管理中的分段、分页机制。

  1.用户进程创建

 

#include <stdio.h>
int foo(int n)
{
  char text[2048];
  if(n == 0)
    return 0;
  else 
  {
     int i=0;
     for(i;i<2048;i++)
       text[i] = '\0';
    printf("text_%d= 0x%x",Pid= %d\n",n,text,getpid());
    sleep(5);
    foo(n-1);
  }    
} 

int main(int argc,char **argv)
{
  foo(6);
  return 0;
}
该段代码编译生成的可执行文件str1保存在硬盘上,用户在shell界面上输入一条指令: ./str1

现在来分析接下来程序的响应过程。

    shell进程会解析用户输入的指令,解析后知道是要执行硬盘上的可执行文件str1。于是shell调用fork()函数开始创建用户进程,产生int 0x80软中断进入系统调用(特权级由3切换到0) ,最终映射到sys_fork()函数执行,调用 find_empty_process()为进程申请一个可用的进程号pid,在task[64]中为该进程申请一个位置,调用 copy_process(),在该函数中初始化创建的str1进程的TSS结构,将tss段插入到gdt描述符表中,并设置好TSS的段基址和段长度。

//代码路径:/kernel/system_call.s
_system_call: //int 0x80 系统调用的入口
    cmpl $nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds
    push %es
    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_call_table,调用sys_fork

//代码路径:/kernel/system_call.s
_sys_fork: //sys_fork()函数入口
    call _find_empty_process //调用find_empty_process()函数,获取进程id
    testl %eax,%eax
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process //调用copy_process()函数
    addl $20,%esp  //copy_process()函数执行完后会返回这里,esp+20相当于清除堆栈中用于copy_process调用时所用参数所占内存
1:    ret 

//代码路径:/kernel/fork.c
int find_empty_process(void)
{
    int i;

    repeat:
        if ((++last_pid)<0) last_pid=1; //last_pid是定义的全局变量:未执行str1时(str1是shell启动后执行的第一个进程),last_pid=3(进程0,1,shell进程,update进程)
        for(i=0 ; i<NR_TASKS ; i++)
            if (task[i] && task[i]->pid == last_pid) goto repeat; //str1对应的进程id=5
    for(i=1 ; i<NR_TASKS ; i++) //在task[64]中为str1进程申请一个空闲位置,即task[64]的第5项
        if (!task[i])
            return i;
    return -EAGAIN;
}

//代码路径:/kernel/fork.c
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,  //nr就是task[64]中的项号:对于str1进程
        long ebx,long ecx,long edx,
        long fs,long es,long ds,
        long eip,long cs,long eflags,long esp,long ss)
        //参数是int 0x80、system_call、sys_fork调用时多次累积压栈的结果
{
    struct task_struct *p;
    int i;
    struct file *f;

    p = (struct task_struct *) get_free_page(); //为str1进程申请一个页面,这个页面用来承载进程的task_struct和内核栈:在内核的线性地址空间
    if (!p)
        return -EAGAIN;
    task[nr] = p;         //将str1进程的task_struct挂接到task[64]中,task[64]的项数和线性地址空间的64等分布局正好把每个进程限制在64M线性地址空间
    //将父进程的task_struct内容拷贝到子进程
    *p = *current;     //将shell进程的task_struct结构复制给str1进程
    //设置子进程
    p->state = TASK_UNINTERRUPTIBLE; //设置str1进程为不可中断等待进程
    p->pid = last_pid;               //设置str1进程id
    p->father = current->pid;        //设置shell为str1进程的父进程
    p->counter = p->priority;        //用当前进程的优先级设置str1进程的时间片
    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,所用到的数据都是前面程序压栈形成的参数,TSS是为进程间切换而设计的,每当进程切换,就用TSS来保护现场
    p->tss.back_link = 0; 
    p->tss.esp0 = PAGE_SIZE + (long) p;
    p->tss.ss0 = 0x10;
    p->tss.eip = eip; //EIP指向_syscall0(int,fork)调用中的if(__res>0)
    p->tss.eflags = eflags;
    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;
    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));
      //调用copy_mem()函数为进程分段和分页,即确定进程的线性地址空间
    if (copy_mem(nr,p)) { //设置子进程的LDT表中代码段、数据段描述符基地址及创建、复制子进程的第一个页表
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
       //文件继承处理:str1继承shell打开的文件
    for (i=0; i<NR_OPEN;i++)
        if (f=p->filp[i])
            f->f_count++; //文件引用计数加1
    if (current->pwd)
        current->pwd->i_count++; //当前工作目录i节点引用计数加1
    if (current->root)
        current->root->i_count++; //当前根目录i节点引用计数加1
    if (current->executable)
        current->executable->i_count++;  //可执行文件i节点的引用计数
      //将str1进程的TSS、LDT挂接到GDT的指定位置处
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); //FIRST_TSS_ENTRY = 4对应GDT表中TSS0
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
    p->state = TASK_RUNNING; //设置str1进程为就绪状态,意味着str1进程可以参与轮转了
    return last_pid;
}

//代码路径:kernel/fork.c
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]); //取原数据段基址
    if (old_data_base != old_code_base)        //Linux0.11版不支持代码和数据段分立的情况
        panic("We don't support separate I&D");
    if (data_limit < code_limit)
        panic("Bad data_limit");
    //为str1进程分段,确定进程的线性地址空间
    new_data_base = new_code_base = nr * 0x4000000; //根据在task[64]中确定的项号nr,确定段基址
    p->start_code = new_code_base;
    set_base(p->ldt[1],new_code_base);              //设置代码段描述符中基址域
    set_base(p->ldt[2],new_data_base);              //设置数据段描述符中基址域
    if (copy_page_tables(old_data_base,new_data_base,data_limit)) {  //为strl进程分页
        free_page_tables(new_data_base,data_limit);
        return -ENOMEM;
    }
    return 0;
}


get_limit()函数时利用内嵌汇编取特定段描述符中段限长,其中用到指令lsll

//代码路径:include/linux/sched.h
//取段选择符segment的段限长
//%0:存放段限长(字节数) %1:段选择符segment
#define get_limit(segment) ({ \
unsigned long __limit; \
__asm__("lsll %1,%0\n\tincl %0":"=r" (__limit):"r" (segment)); \
__limit;})
ldt数据段段选择符为0x17 = 0000 0000 0001 0111,高13位表示Index=2,TI=1表示在LDT中,RPL表示特权级3,即数据段描述符在LDT表中的第2项,对照上面图示可知,是一致的;同理可知代码段描述符在LDT表中的第1项(注意第0项为NULL),LDT表的基地址在LDTR中。


get_base(addr)取描述符中指向段的基地址:

//代码路径:include/linux/sched.h
//取局部描述符表ldt中段描述符的基地址
#define get_base(ldt) _get_base( ((char *)&(ldt)) )
#define _get_base(addr) ({\ 
//从地址addr 处描述符中取段基地址
//edx:(__base) %1:[addr+2] %2:[addr+4] %3:[addr+7] 偏移量的单位是字节
unsigned long __base; \
__asm__("movb %3,%%dh\n\t" \  //取段描述符的[64:55]即段基址的[31:24] --> dh
    "movb %2,%%dl\n\t" \    //取段描述符的[39:32]即段基地址的[23:16] --> dl
    "shll $16,%%edx\n\t" \  //将段描述符基地址高16位移到edx中高16位处
    "movw %1,%%dx" \ 
    :"=d" (__base) \
    :"m" (*((addr)+2)), \
     "m" (*((addr)+4)), \
     "m" (*((addr)+7))); \
__base;})


   str1进程分段问题解决后,开始分页,分页是建立在分段的基础上的,具体表现为,分段时用段基址和段限长分别为分页确定了从哪里开始复制页面表项信息,复制到哪里去,复制多少三件事。调用copy_page_tables()函数来完成分页,str1创建后,还没有自己的程序,还要和父进程shell共享程序,这一点在分页时表现为与shell进程共享页面,即为str1进程另起一套也目录项和页表项,使之和shell指向共同的页面。

//代码路径:mm/memory.c
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;

    if ((from&0x3fffff) || (to&0x3fffff))   //检测对齐
       panic("copy_page_tables called with wrong alignment");

    //获取页目录项,每个页目录项占4个字节
    from_dir = (unsigned long *) ((from>>20) & 0xffc);    
    to_dir = (unsigned long *) ((to>>20) & 0xffc);    
    size = ((unsigned) (size+0x3fffff)) >> 22;    
    for( ; size-->0 ; from_dir++,to_dir++) { //体现分段是分页的基础        
        if (1 & *to_dir)            
           panic("copy_page_tables: already exist");        
        if (!(1 & *from_dir))            
           continue;        
        from_page_table = (unsigned long *) (0xfffff000 & *from_dir); //解析页目录项,得到页表项 
        //为创建页表项申请一个页面:这里调用get_free_page()函数申请的页面是将来用来承载进程str1的页表项 
        //这些页表项是用来管理str1进程所占用的页面的,是不让进程使用的,所以这里只申请了页面,并没有映射到Str1进程的线性地址空间 
        if (!(to_page_table = (unsigned long *) get_free_page())) 
           return -1;    /* Out of memory, see freeing */           
        *to_dir = ((unsigned long) to_page_table) | 7;      //页目录项的P位被设置为1                
        nr = (from==0)?0xA0:1024;                
        for ( ; nr-- > 0 ; from_page_table++,to_page_table++) { //复制页表                   
        this_page = *from_page_table;                    
        if (!(1 & this_page))                           
             continue;                    
        this_page &= ~2; //页表项的P位被设置为1:使得共享的页面对于shell进程来讲是只读的                    
        *to_page_table = this_page; //这样设置使得共享的页面对于str1进程来讲是只读的                    
          if (this_page > LOW_MEM) {                            
          *from_page_table = this_page;                            
          this_page -= LOW_MEM;                            
          this_page >>= 12;                           
          mem_map[this_page]++;                    
        }           
    }     
  }      
  invalidate();     
  return 0;
}



2. str1进程的加载准备 

    str1进程创建完成后,接下来就要为用户进程str1的加载做准备了,str1进程的加载准备与为shell程序的加载做准备的方式是大体相同,包括对参数和环境变量等外围环境的检测、对str1进程task_struct的针对性调整以及最终设置EIP、ESP这几部分。shell调用execve()来加载str1进程,最终映射到do_execve()函数,do_execve()函数主要完成以下任务: 

  (1)为管理str1进程参数和环境变量所占用的页面做准备 

  (2)把str1所在的文件i节点读出来,通过i节点信息,检测文件自身有问题 

  (3)通过i节点找到文件头,对文件进行检测,其中包括检测记录的可执行文件代码长度、数据长度等

注意:copy_process()执行完后(str1进程为就绪状态了)是如何到do_execve()中的呢?一直有疑问,书上也没写详细,只是说了str1进程的加载过程与shell程序加载过程大体相同。所以我在这写下我的理解:copy_process()执行完后,进程str1创建完毕,最终会回到fork(),fork()返回的是pid,pid不等于0,于是进入到wait()。

//代码路径:init/main.c
void init()
{
  int pid,i;
  ....
  if(!(pid=fork())) {
    ....
  }
  if(pid>0)
    while(pid != wait(&i))
      /* nothing */;
    ....
}
wait()最终会映射到系统调用sys_waitpid()中去执行,sys_waitpid()函数先要对所有进程遍历,先确定那个进程是shell进程的子进程,由于进程shell刚刚创建了子进程,即进程str1,于是str1进程被选中了,执行如下代码:

//代码路径:kernel/exit.c
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
    int flag, code;
    struct task_struct ** p;

    verify_area(stat_addr,4);
repeat:
    flag=0;
    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
        if (!*p || *p == current) //当前进程是shell进程
            continue;
        if ((*p)->father != current->pid) //筛选出shell的子进程str1,即p指向str1进程 
            continue;
        if (pid>0) {
            if ((*p)->pid != pid)
                continue;
        } else if (!pid) {
            if ((*p)->pgrp != current->pgrp)
                continue;
        } else if (pid != -1) {
            if ((*p)->pgrp != -pid)
                continue;
        }
        switch ((*p)->state) { //判断str1进程的状态
            case TASK_STOPPED: //停止状态
                if (!(options & WUNTRACED))
                    continue;
                put_fs_long(0x7f,stat_addr);
                return (*p)->pid;
            case TASK_ZOMBIE: //僵死状态
                current->cutime += (*p)->utime;
                current->cstime += (*p)->stime;
                flag = (*p)->pid;
                code = (*p)->exit_code;
                release(*p);
                put_fs_long(code,stat_addr);
                return flag;
            default: //进程str1处于就绪状态
                flag=1;
                continue;
        }
    }
    if (flag) {
        if (options & WNOHANG)
            return 0;
        current->state=TASK_INTERRUPTIBLE; //设置进程shell为可中断状态
        schedule(); //切换到进程str1去执行: 切换到进程str1的3特权级,此时fork为0(特意设置的)回到init()中的if(!(pid=fork()))        
        if (!(current->signal &= ~(1<<(SIGCHLD-1))))
            goto repeat;
        else
            return -EINTR;
    }
    return -ECHILD;
}
执行完后,切换到进程str1,之后调用execve()函数,execve()函数最终会映射到系统调用sys_execve()去执行(由特权级3变为特权级0),sys_execve()中调用do_execve()完成str1程序加载。


//代码路径:fs/exec.c
int do_execve(unsigned long * eip,long tmp,char * filename,
    char ** argv, char ** envp)
{
    struct m_inode * inode;
    struct buffer_head * bh;
    struct exec ex;
    unsigned long page[MAX_ARG_PAGES]; //128KB的指针页:位于64M线性空间末尾,存储可执行程序的参数和环境变量
    int i,argc,envc;
    int e_uid, e_gid;
    int retval;
    int sh_bang = 0;
    unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4; //堆栈指针,指向指针表的第一个元素

    if ((0xffff & eip[1]) != 0x000f)
        panic("execve called from supervisor mode");
    for (i=0;i<MAX_ARG_PAGES;i++)    
        page[i]=0;     //将参数和环境变量的页面指针管理表清零
    if (!(inode=namei(filename)))     //获取str1程序所在文件的i节点
        return -ENOENT;
    argc = count(argv);  //统计参数个数
    envc = count(envp);  //统计环境变量个数
    
restart_interp:
    if (!S_ISREG(inode->i_mode)) {    /* must be regular file */
        retval = -EACCES;
        goto exec_error2;
    }
    i = inode->i_mode; //检测i节点的uid gid属性
    e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
    e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
    if (current->euid == inode->i_uid)
        i >>= 6; //修改i节点属性中的权限位
    else if (current->egid == inode->i_gid)
        i >>= 3;
    if (!(i & 1) &&
        !((inode->i_mode & 0111) && suser())) {
        retval = -ENOEXEC;
        goto exec_error2;
    }
    if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) { //通过i节点,确定str1所在设备的设备号及其文件头的块号(i_zone[0]),获取头文件
        retval = -EACCES;
        goto exec_error2;
    }
    ex = *((struct exec *) bh->b_data);    /* read exec-header 从缓存块中得到头文件信息*/
    //检测头文件信息,str1文件并非脚本文件,if中语句不会执行
    if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
        /*
         * This section does the #! interpretation.
         * Sorta complicated, but hopefully it will work.  -TYT
         */

        char buf[1023], *cp, *interp, *i_name, *i_arg;
        unsigned long old_fs;

        strncpy(buf, bh->b_data+2, 1022);
        brelse(bh);
        iput(inode);
        buf[1022] = '\0';
        if (cp = strchr(buf, '\n')) {
            *cp = '\0';
            for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
        }
        if (!cp || *cp == '\0') {
            retval = -ENOEXEC; /* No interpreter name found */
            goto exec_error1;
        }
        interp = i_name = cp;
        i_arg = 0;
        for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
             if (*cp == '/')
                i_name = cp+1;
        }
        if (*cp) {
            *cp++ = '\0';
            i_arg = cp;
        }
        /*
         * OK, we've parsed out the interpreter name and
         * (optional) argument.
         */
        if (sh_bang++ == 0) {
            p = copy_strings(envc, envp, page, p, 0);
            p = copy_strings(--argc, argv+1, page, p, 0);
        }
        /*
         * Splice in (1) the interpreter's name for argv[0]
         *           (2) (optional) argument to interpreter
         *           (3) filename of shell script
         *
         * This is done in reverse order, because of how the
         * user environment and arguments are stored.
         */
        p = copy_strings(1, &filename, page, p, 1);
        argc++;
        if (i_arg) {
            p = copy_strings(1, &i_arg, page, p, 2);
            argc++;
        }
        p = copy_strings(1, &i_name, page, p, 2);
        argc++;
        if (!p) {
            retval = -ENOMEM;
            goto exec_error1;
        }
        /*
         * OK, now restart the process with the interpreter's inode.
         */
        old_fs = get_fs();
        set_fs(get_ds());
        if (!(inode=namei(interp))) { /* get executables inode */
            set_fs(old_fs);
            retval = -ENOENT;
            goto exec_error1;
        }
        set_fs(old_fs);
        goto restart_interp;
    }
    brelse(bh);
    //通过头文件中的信息,检测str1文件的内容是否符合执行规定
    if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
        ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||
        inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {  //str1进程的代码、数据、堆的总长度不能超过48M
        retval = -ENOEXEC;
        goto exec_error2;
    }
    //如果头文件大小不等于1024B,程序也不能执行
    if (N_TXTOFF(ex) != BLOCK_SIZE) {
        printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
        retval = -ENOEXEC;
        goto exec_error2;
    }
    /* 调整str1进程的管理结构:
       (1)解除与其父进程(shell进程)共享的文件、内存页面关系
       (2)根据str1程序自身情况,量身定做LDT,并设置代码段、数据段、栈段等控制变量
    */    
    if (!sh_bang) { //从用户内存空间拷贝参数和环境变量到内核空闲页面内存
        p = copy_strings(envc,envp,page,p,0); //将环境变量复制到进程空间
        p = copy_strings(argc,argv,page,p,0); //将参数复制到进程空间
        if (!p) {
            retval = -ENOMEM;
            goto exec_error2;
        }
    }
/* OK, This is the point of no return */
    if (current->executable)
        iput(current->executable); //解除与shell进程可执行文件i节点关系
    current->executable = inode; //用str1程序文件的i节点设置executable
    for (i=0 ; i<32 ; i++)
        current->sigaction[i].sa_handler = NULL; //把信号句柄清空,准备加载自己的处理程序
    for (i=0 ; i<NR_OPEN ; i++)
        if ((current->close_on_exec>>i)&1)
            sys_close(i);
    current->close_on_exec = 0; //把打开文件屏蔽位清0
    free_page_tables(get_base(current->ldt[1]),get_limit(0x0f)); //释放与shell进程共享的代码段页面
    free_page_tables(get_base(current->ldt[2]),get_limit(0x17)); //释放与shell进程共享的数据段页面
    if (last_task_used_math == current)
        last_task_used_math = NULL;
    current->used_math = 0;
    p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE; //重新设置str1进程的LDT
    p = (unsigned long) create_tables((char *)p,argc,envc);  //在进程的新栈空间中创建进程的输入参数和环境变量指针管理表
    //设置当前进程堆结尾字段brk = a_text + a_data + a_bss
    current->brk = ex.a_bss +
        (current->end_data = ex.a_data +
        (current->end_code = ex.a_text));
    current->start_stack = p & 0xfffff000; //设置进程的用户栈起始地址
    current->euid = e_uid;
    current->egid = e_gid;
    i = ex.a_text+ex.a_data;
    while (i&0xfff)
        put_fs_byte(0,(char *) (i++));
    //对sys_execve软中断压栈的值进行设置,用str1程序的起始地址设置EIP,用str1新的栈顶指针设置ESP
    //这样软中断返回后,程序将从str1程序开始执行
    eip[0] = ex.a_entry;        /* eip, magic happens :-) */
    eip[3] = p;            /* stack pointer */
    return 0;
exec_error2:
    iput(inode);
exec_error1:
    for (i=0 ; i<MAX_ARG_PAGES ; i++)
        free_page(page[i]);
    return(retval);
}
 

//代码路径:mm/memory
//释放连续块,块必须4M对齐
int free_page_tables(unsigned long from,unsigned long size)
{
    unsigned long *pg_table;
    unsigned long * dir, nr;

    if (from & 0x3fffff)  //4M对齐检测
        panic("free_page_tables called with wrong alignment");
    if (!from)           //0地址是内核驻留地址,不允许释放
        panic("Trying to free up swapper memory space");
    size = (size + 0x3fffff) >> 22; //size按4M取整
    dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
                               //页目录项是线性地址的高10位,可取线性地址的高10位即可获取(即<<22)
                               //但是在页目录表中,每个页目录项占用4个字节,所以(from>>20) & 0xffc
                            
                       
    for ( ; size-->0 ; dir++) {
        if (!(1 & *dir)) //如果页目录项未使用,跳过
            continue;
        pg_table = (unsigned long *) (0xfffff000 & *dir);  //根据页目录项的内容得到页表项
        for (nr=0 ; nr<1024 ; nr++) {                      //每个页表项对应1024个物理页
            if (1 & *pg_table)                          //页表项的最低位(即P)等于1表示物理页有效
                free_page(0xfffff000 & *pg_table);   //释放物理页
            *pg_table = 0;                              //页表项清零(P位也清零)
            pg_table++;                                 //指向下一个页表
        }
        free_page(0xfffff000 & *dir);                     //释放页表项
        *dir = 0;                                         //页目录项内容清零
    }
    invalidate();                                           //刷新高速缓存
    return 0;
}


//代码路径:fs/exec.c
// 根据执行文件重新调整进程代码段限长
static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
{
    unsigned long code_limit,data_limit,code_base,data_base;
    int i;

    code_limit = text_size+PAGE_SIZE -1;
    code_limit &= 0xFFFFF000;   //将代码段长度规整为页的整数倍
    data_limit = 0x4000000;   //将数据段段限长设置为64MB
    // 取出进程的当前局部代码描述符段基址,并重新设置限长和基址,不过一般也就代码段限长会变
    code_base = get_base(current->ldt[1]);
    data_base = code_base;
    set_base(current->ldt[1],code_base);
    set_limit(current->ldt[1],code_limit);
    set_base(current->ldt[2],data_base);
    set_limit(current->ldt[2],data_limit);
/* make sure fs points to the NEW data segment */
    __asm__("pushl $0x17\n\tpop %%fs"::); //将参数环境空间映射到进程数据段线性地址末端
    data_base += data_limit;
    for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {
        data_base -= PAGE_SIZE;
        if (page[i])
            put_page(page[i],data_base);
    }
    return data_limit;
}


3. str1进程的加载和运行
    在do_execve()最后,调整EIP和ESP,使软中断返回后,直接从str1程序开始位置执行,因为之前str1进程与shell进程解除了共享页面关系,控制页面的页表也已经释放,断绝了与str1进程的页目录项的映射关系,因此页目录项内容为0,P位也为0。str1程序一开始执行,MMU解析线性地址值时会发现对应的页目录项P位为0,因此产生缺页中断。
    缺页中断信号产生后,page_fault会响应这个中断,最终映射到do_no_page()中断处理函数。代码分析如下:

//代码路径:mm/memory.c
void do_no_page(unsigned long error_code,unsigned long address)
{
    int nr[4];
    unsigned long tmp;
    unsigned long page;
    int block,i;

    address &= 0xfffff000;
    tmp = address - current->start_code; //address是线性页面地址,current->atart_code是进程在线性地址空间的起始地址,两个相减所以tmp是进程逻辑空间偏移量
    if (!current->executable || tmp >= current->end_data) {  //executable为str1程序所在文件i节点,end_data为程序代码末端
        get_empty_page(address);
        return;
    }
    if (share_page(tmp))    //试图和其他进程共享
        return;
      //从硬盘上把str1程序加载进来
    if (!(page = get_free_page())) //为str1申请页面 
        oom();
//读取address地址处4096字节内容到内存中刚申请的page物理页面中
    block = 1 + tmp/BLOCK_SIZE;   //由tmp/BLOCK_SIZE即得到tmp逻辑地址在可执行文件中的块序号,+1是考虑到文件头占用的开头一个block
      //由这里可以得知,每个可执行文件在生成后,其第一个block都是用来存放文件头的,这个文件头放置文件的属性信息等。然后从第二个block开始才依次存放真正的可执行

      //文件image内容。

      //因为一个page有4k而一个block为1k,要求读取1page数据而读取函数每次读取1block数据,因此要读取刚才计算的block开始的4个连续block的数据

    for (i=0 ; i<4 ; block++,i++)
        nr[i] = bmap(current->executable,block); //得到执行的代码所在的块号
    bread_page(page,current->executable->i_dev,nr); //读取4个逻辑块(1页)的shell程序内容进内存页面
//完成对多于读出来的无用字节清零这主要是针对可执行文件中tmp后面的内容不足一页的情况下,我们读出的一页内容就有很多字节是无用的
//这种情况下就要把这些内容给清零。
    i = tmp + 4096 - current->end_data; //计算必须清零的字节数
    tmp = page + 4096;
    while (i-- > 0) {
        tmp--;
        *(char *)tmp = 0;
    }
    if (put_page(page,address)) //物理地址映射到线性地址
        return;
    free_page(page);
    oom();
}
    由于这个str1程序大于一个页面,所以在执行过程中,如果需要新代码就会不断产生缺页中断,从而不断加载需要执行的程序。下面介绍str1的运行过程:
str1程序是foo()函数的迭代调用,str1程序第一次调用foo()时程序压栈,ESP向下扩充2048个字节(为超出1个页面4KB),第二次调用foo()时压栈就会产生缺页中断,此时会执行到do_no_page()函数中的:

void do_no_page(unsigned long error_code,unsigned long address)
{
      ....
    tmp = address - current->start_code; //address是线性页面地址,current->atart_code是进程在线性地址空间的起始地址,两个相减所以tmp是进程逻辑空间偏移量
    if (!current->executable || tmp >= current->end_data) {  //executable为str1程序所在文件i节点,end_data为程序代码末端
        get_empty_page(address);
        return;
    }
       ....
}

这样反复压栈,产生缺页中断,申请新的页面,直至str1程序执行完成,程序执行完毕,foo函数到达递归的终止点返回。这时函数返回导致进程清栈,ESP向高位置收缩。
下面说明str1进程的退出,主要完成以下任务:
    (1)释放程序str1所占页面
    (2)解除str1程序与文件有关的内容,并给父进程发信号
    (3)执行str1进程退出后的程序调度
    str1进程调用exit()函数进行退出,最终映射到sys_exit()函数执行,调用do_exit()函数来处理str1进程退出的相关事宜。

//代码路径:kernel/exit.c
int sys_exit(int error_code)
{
    return do_exit((error_code&0xff)<<8);
}

//代码路径:kernel/exit.c
int do_exit(long code)
{
    int i;
    //释放str1进程代码段和数据段所占用的内存页面
    free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
    free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
    for (i=0 ; i<NR_TASKS ; i++) //检测str1进程是否有子进程
        if (task[i] && task[i]->father == current->pid) { 
            task[i]->father = 1;
            if (task[i]->state == TASK_ZOMBIE) //
                /* assumption task[1] is always init */
                (void) send_sig(SIGCHLD, task[1], 1);
        }
    for (i=0 ; i<NR_OPEN ; i++) //以下为解除str1进程与其他进程、文件、终端等的关系
        if (current->filp[i])
            sys_close(i);
    iput(current->pwd);
    current->pwd=NULL;
    iput(current->root);
    current->root=NULL;
    iput(current->executable);
    current->executable=NULL;
    if (current->leader && current->tty >= 0)
        tty_table[current->tty].pgrp = 0;
    if (last_task_used_math == current)
        last_task_used_math = NULL;
    if (current->leader)
        kill_session();
    current->state = TASK_ZOMBIE; //内核将当前进程str1设置为僵死状态
    current->exit_code = code;
    tell_father(current->father); //给父进程(shell进程)发信息,通知它str1进程即将退出
    schedule();  //进程切换
    return (-1);    /* just to suppress warnings */
}
之后shell进程接收到str1发送的信号而被唤醒,即设置为就绪态,之后切换到shell去执行。shell进程执行进入到内核,内核将释放str1进程task_struct所占用的页面,并解除进程与task[64]的关系,至此str1程序就彻底从系统中退出了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值