理解进程的新建和执行过程

  • 2020 年 3 月 12 日
  • 笔记

本文以linux0.11版本为基础,分析进程的内存布局,现代版本已经发生比较大的变化,都是很多原理都是类似的。 系统维护了一个全局的数据结构叫GDT( Global Descriptor Table),他保存了所有进程的代码段数据段的一些信息。系统有专门的寄存器保存了GDT的地址,叫GDTR。GTDR的格式如下。

在这里插入图片描述

有专门的指令把这个地址加载到GDTR中。叫LGDT。每个进程可以定义一个LDT,用于存储代码段和数据段信息。GDT布局如下。

在这里插入图片描述

GDT每个项(GDT描述符)对应的结构体是

GDT描述符

我们回顾task_struct结构,看到有两个属性desc_struct,和tss_struct。desc_struct是保存进程代码段和数据段信息的,tss_struct是保存进程执行上下文的。这两个结构体的定义如下。

struct desc_struct {      unsigned long a,b;  }    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;  };

我们从fork函数开始,看看这些数据结构的设置和关系。我们先来看一下一些宏定义。他是根据进程号(进程id)计算出在GDT中的索引。可参考上图。

/*   * 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 FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)  // 第一个tss选择子的偏移是4<<3,4乘以8,等于32,即从GDT的偏移为32开始算,第一个进程的n是0,tss是32  #define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))  // 第一个ldt选择子的偏移是5<<3,5乘以8,等于40,即从GDT的偏移为40开始算,第一个进程的n是0,ldt是40  #define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))

下面代码来自fork。

// nr是进程id,计算进程的ldt结构在gdt中的索引,执行该进程的时候,从GDT的第tss->ldt项中取得进程的信息。p即task_struct  p->tss.ldt = _LDT(nr);  // 设置进程的线性地址的首地址,每个进程占64MB,0.11的进程线性地址是每个进程64M,不是4GB  new_data_base = new_code_base = nr * 0x4000000;  p->start_code = new_code_base;  // 设置线性地址到ldt的描述符中  set_base(p->ldt[1],new_code_base);  set_base(p->ldt[2],new_data_base);  // 把父进程的页目录项和页表复制到子进程,old_data_base,new_data_base是线性地址,父子进程共享物理页面,即copy on write  copy_page_tables(old_data_base,new_data_base,data_limit);  /*      挂载tss和ldt地址到gdt,nr << 1即乘以2,这里算出的是第nr个进程距离第一个tss描述符地址的偏移,      单位是8个字节,即选择描述符大小,_LDT是偏移的大小,单位是1,这里是8  */  set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));  set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
在这里插入图片描述

fork执行完之后,新新建的相关数据结构已经建立好了,并且也和系统的管理数据产生了关联。有自己独立的页表,和父进程共享物理地址。那么当这个进程被调度的时候,他会发生什么。 执行进程的时候,根据进程号,算出tss在gdt的索引,然后把索引里指向的tss里的上下文也加载到对应的寄存器,tss信息中的ldt索引首先从gdt找到进程ldt结构体数据的首地址,即desc_struct结构体数组,然后根据当前段的属性,比如代码段,则从cs中取得选择子,系统从ldt表中取得进程线性空间的首地址、限长、权限等信息。用线性地址的首地址加上ip中的偏移,得到线性地址,然后再通过页目录和页表得到物理地址,物理地址还没有分配则进行缺页异常等处理。