Linux并不使用太多的分段,原因是某些RISC机器对分段的支持不好。为此Linux的分段都存在“全局描述表(GDT)”中,GDT是一个全局desc_struct数组(位于linux-2.6.32.59archx86includeasm),其结构如下:
#define GDT_ENTRIES 16
struct desc_struct gdt[GDT_ENTRIES];
struct desc_struct {
union {
struct {
unsigned int a;
unsigned int b;
};
struct {
u16 limit0; // 段大小
u16 base0; // 段起始位置
unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; // type表示段类型,占4位;dpl指的段运行权限,占2位
unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; //d 表示内存地址位宽,占1位
};
};
} __attribute__((packed));
所以我们可以看出,段描述结构体占8个字节,至于里面的a,b,那是老的方式,后来使用C++ Struts的Bit Fields后更方便了。type类型由以下几种:
enum {
DESC_TSS = 0×9,
DESC_LDT = 0×2,
DESCTYPE_S = 0×10, /* !system */
};
Linux主要使用以下几种段:
内核代码段(Kernel Code Segment):type=10,dpl=0
内核数据段(Kernel Data Segment):type=2,dpl=0
用户代码段(User Code Segment):type=10,dpl=3
用户数据段(User Data Segment):type=2,dpl=3
任务状态段(Task State Segment),每进程一个:type=9,dpl=3
其它类型可以参见linux-2.6.32.59archx86includeasmsegment.h,里面有非常详细的说明。
它们都存储在“全局描述符表(GDT)”。Linux本身并不使用“局部描述符表(LDT)”,当一个进程被创建时,其指向的是一个默认的LDT,不过系统并不阻止进程创建它。也就是说一个进程最多两个段描述符:TSS与LDT。由于Segment Selector为16位(为什么只有16位,这个就是历史原因了,由于X86在Real Mode下段地址只有20位,其中有效的就是16位,详见:x86
memory segmentation,但Linux段内偏移地址高达32位,所以线性地址总共是48位),其中有效的索引位仅有13位,所以GDT的最大长度为213-1=8192,除去系统保留的12个,留给进程的只有8180个入口,那么就意味Linux进程的最大数为8180/2=4090。需要注意的是,进程在创建的时候并不会马上创建自己的LDT,其指向的是GDT一个默认的LDT,里面的SD为null。只有在需要的时候进程才创建自己的LDT并把它放入GDT中。所以不管是LDT也好,TSS也好,它们都存放在GDT里面。而对于UCS与UDS,所有的进程共享一个。这样地址空间不会重复吗?不会,因为线性不是最终的物理地址,每个进程还有自己的页表,所以最终映射到物理地址是不同的。