第七章  应用实例

上海市环境监视和检测系统是基于一个小型服务器的多任务系统,该系统是应用于环境的检查和监视的。对于所要检查和监视的样本,从随时采样开始,输入样本的采集过程和方式,然后样本在各个部门之间流动,各个部门对其进行详细监测,包括样本的酸度,各种微量化学成份的含量和其它的相关检测,然后在各个部门内部对于该指标做出分析和评估,然后把所有的数据和评估结果送到更高一级的部门,在这一更高一级的部门中,对于来自下面不同的部门的收据和结果做出总结报告。

其中在检测的过程中,数据庞大,包括各个部门不同的监测要求及其相应的数据,而且在各个部门中是同时进行多个样本的检测的,部门多,样本多,所以数据复杂而且庞大。同时对于所有的数据和相关资料都保存在该服务器上。

在检测中进年来由于随着对检测范围的扩大和任务的增加,要求有更多的任务同时进行,而不能等待别的任务完成后才可以进行,对旧版本升级后,开始考虑能是否能有更多的任务能有更多的任务同时进行,也就是是否可以有更多的进程可以使用。该系统是基于 Linux2.2.0内核的,我们可以推广到 Linux 2.2.x(i386体系结构),以下我们对于这一问题的分析和解决。这个问题也可以称为:Linux 2.2.x(i386体系结构)最大进程数限制的突破;

 

7.1 引起进程受限的起因

    通过以上的讨论,我们可以看出Linux 2.2.0最大进程数限制产生的原因:

    Linux 2.2.0中定义的宏NR_TASKS,在编译时静态决定了X86系统运行时的进程数上限。该数在系统初始化时静态决定了gdt表的总长度。但因为i386体系结构的特点,全局描述符表寄存器gdtr长度域为16位,每项描述符为8字节,故可容纳的

    最大描述符数 = 1(16-3)=113 = 8192

    内核在初始化时对gdt表的前12 项已有特殊安排,

    空描述符(第0项),保留描述符(第1,6,7项)

    内核代码、数据段(第2,3项) 及用户代码、数据段(第4,5项)

    APM BIOS 使用段(第8--11项)

    同时由于每个进程需要两项分别存放tssldt描述符,因此理论上 可用于进程的数目为 = (8192-12)/2=4090个。

 

7.2对于这一问题的解决方案

 

    可见,突破最大进程数的限制,必须解决没有足够的gdt表项的问题。Gdt的大小是硬件限制的,所以只能考虑动态地设置进程的tssldt描述符,取消为进程预先分配gdt空间的做法。

    事实上,根据进程数据结构PCB的分析可知,内核中可以动态地寻址到每个进程的tssldt (如果有的话)段,因此在任务切换时通过PCB指针即可引用上述两段的寻址,分别为:

    进程tss段:proc->tss

    进程ldt段:proc->mm->segments

    所以,静态地分配并保留这两个段的描述符是完全没有必要的。我们可以在任何需要的时候建立起它们,例如在切换时将该两段的段描述符动态设置到gdt表中即可。

    这种方法的关键在于在gdt中为每个cpu保留两个描述符,分别存放该cpu上正在运行的进程的tssldt描述符。这两个描述符则是在进程切换时由操作系统根据进程PCB中的内容建立起来的。

 

    7.3 方案具体的实现

 

    根据以上的分析,要突破上述4090进程数的限制,需要动态设置进程的TSS及LDT段描述符。具体体现在两方面:

 

    1系统初始化:

    a)在head.S中将静态设置的GDT表长度置为最大值8192。

    b)在desc.h的GDT表定义中,在进程可用的第一项位置插入NR_CPUS*2项,作为每颗CPU的专用项。用于超出GDT表之外的进程运行使用。仍留下4090-NR_CPUS=4090-32=4058项用于原有算法使用。

    CPU0:SHARED_TSS_ENTRY = 12

    SHARED_LDT_ENTRY = 13

    CPU1:SHARED_TSS_ENTRY+1 = 14

    SHARED_LDT_ENTRY+1 = 15

    ...

    CPUn:SHARED_TSS_ENTRY+n = SHARED_TSS_ENTRY + n

    SHARED_LDT_ENTRY+n = SHARED_LDT_ENTRY + n

    (n<NR_CPUS)

    c)改变宏NR_TASKS为变量 int NR_TASKS,并在初始化函数start_kernel()中合适位置动态分配进程指针数组空间:

    task = kmalloc(sizeof(void*)*NR_TASKS,GFP_ATOMIC);

    这样,符号名task在内核中的使用方法与静态分配时一样。另外变量NR_TASKS可在lilo启动时以参数形式传入内核。

    d)修改 parse_options(),使内核能够识别lilo传来的表示最大进程数的变量nrtasks,以用于task数组空间的分配。

 

    2任务切换

    a)fork时,PCB中的tss.ldttss.tr分别用于保存该进程的ldttss的选择符,以便在切换时装载 tr寄存器和ldtr寄存器.由于现在存在大于4090的进程,按照原有算法计算,这些进程的ldt将会超出16位的范围,所以这里我们使用了tss结构中保留未用的short __ldth ,与short ldt合并使用变为长整数 (int) ldt,因此赋值指令变为:

    *((unsigned long*)&(p->tss.ldt)) = (unsigned long)_LDT(nr);

    if(*((unsigned long*)&(p->tss.ldt))<(unsigned long)(8292<<3))

    set_ldt_desc(nr,ldt,LDT_ENTRIES);// 此句为原有代码;

    else{ //什么也不做,留在切换时作类似设置工作}

    这种方法的一个好处是仅仅通过判断PCB中ldt的值,就可以知道该进程是否大于4090,这大大方便了下面的进程切换的实现。

    b)切换时:我们已允许超出gdt表数目的进程数存在,即超出部分的进程无法在gdt表中预先分配,此时可以使用上述为每颗CPU预留的gdt入口,在gdt表中的寻址为:

    SHARED_TSS_ENTRY+smp_processor_id();

    这样,超出GDT表的进程段描述符的设置使用如下算法,而未超出部分的处理不变:

    ...

    #define set_shared_tss_desc(addr,cpu)\par

    _set_tssldt_desc(gdt_table+SHARED_TSS_ENTRY+2*cpu,(int)addr,235,0x89);

    #define set_shared_ldt_desc(addr,size,cpu)\par

    _set_tssldt_desc(gdt_table+SHARED_LDT_ENTRY+2*cpu,(int)addr,((size<<3)-1),0x82);

    ....

    void __switch_to(task_struct *prev,task_struct *next){

    ...

    if(next->tss.tr <= 0x0000ffff)

    {

    由原来的代码处理...

    }else

    {

    set_shared_tss_desc(&next->tss),smp_processor_id());

    set_shared_ldt_desc(&next->mm->segments,LDT_ENTRIES,smp_processor_id());

    }

    //接下来用适当的值装载ldtrtr 寄存器。

    ...

    }

    综上所述,可以以尽量简单的改动来突破最大进程数的限制,并在lilo设置文件中写上特定参数,启动时自动传入内核,就可以实现在kernel v2.2.x下超过4090个的进程数了。参数的格式为:

    append = "nrtasks=40000"

 

   7.4 方案总结

 

    经过以上修改,使系统理论上的进程数上限数可设置到2的31次方.但实际使用中,每增加一个进程纯内核内存空间分配的代价为:

    PCB及内核2页 + 页目录1页 + 页表1页(至少) = 4页共16k)

    因此,假设机器内存1G,进程内存管理开销以20k计算,操作系统占据约20M,则最大进程数为:

    (1G-20M)/20k=(1073741824-20971520)/20480 = 51404 ~= 50000.

    实际应用进程的开销不止2k,若假设为5k,则

    1G机器实际最大进程数为=50000 *(2/5) = 20000(

需要说明的是,Linux 2.2.x原来的实现方法在进程切换时是比较快的。我们的方案则在进程数小于4090时与原来相近,超过4090个进程后会略微慢一点。 本方案是在小型机器上实现的,在微型计算机,处理的速度和效率都会降低,更多的时间将用于调度这是降低速度的主要原因,所以以前有极高的CPU处理速度和其它相应的硬件配合。

经过我们的改进该机器运行正常。