现在的位置: 首页 > 综合 > 正文

DOS 下多任务系统的设计与实现 (1995-7)

2014年10月21日 ⁄ 综合 ⁄ 共 13430字 ⁄ 字号 评论关闭

一、引言:
     将DOS扩展为多任务系统,在理论和实践中都有很重要的意义。但由于DOS系统单用户,单任务的设计局限,在实现中有不少难点。 本文提出一种实现方法及其具体设计。
     现有的多任务DOS系统,可以分为两类。 一类是在其它多任务系统(如OS/2、UNIX、WINDOWS NT等)的支持下的DOS多任务。在这类系统中,有着严格的,良好的任务保护和隔离机制,DOS 等于是一个虚拟机器环境或一个客户服务器,不包括在机器内核之中,因此,在内核之上运行多个DOS任务比较容易实现,但是,大量的DOS 上的运行软件要求在运行时完全控制计算机资源,这样的做法势必与操作系统的资源管理机制相冲突。为此,许多系统又设计为在运行DOS程序时可以选择独占系统资源,即便如此,对DOS程序也有诸多限制, 这类任务切换系统的优点是功能强大,可靠性高。
     第二类系统是在DOS系统本身的支持下,通过对DOS内核的改造,使的DOS具有一定的多任务能力,典型的如DOSSHELL等。由于一般用户对DOS的多任务能力要求并不是很高,如后台并发进程,设备驱动排队等并不需要,而仅是希望能够在程序间方便的进行任务切换, 同时不影响被切换的程序,而这类系统对用户的最大方便是用户程序在执行时可以独占机器,同时,这类系统建立在DOS系统之上, 对用户程序兼容性好,因此仍有一定的实用价值。
     相比较之下,第一类占有很大的技术和性能优势,但它对机器硬件要求高,系统设计复杂,并且不能作到对现有程序的全兼容,第二类在近几年内还有一定的市场,特别会流行于低档PC机上, 它的设计较之第一类是很简单的,本文提出的设计方案就基于第二类。
 二、设计思想:
     许多的DOS应用程序在运行时要求全部的内存空间,一般要求达到400K以上。所以在640K内存中运行多个任务是不现实的, 只能将不使用的任务对换出常规内存,暂时存入硬盘或扩展内存(本文将1088K以上空间泛指扩展内存),在任务再次执行时再换入常规内存 。当然,这种方法是以增加任务切换时间为代价的。
     作为任务切换程序必须很好解决DOS重入问题,由于DOS 内核设计上的单任务串行执行性,DOS是不支持多任务的。一些程序通过一些其它方法(如检测IN_DOS信号量)来进行重入,这类方法不但局限性大(许多情况下不能使用),而且只能重入一次, 在多任务环境下无法使用。目前,使DOS内核可以执行多任务代码的最佳方法是SDA 对换重入法,DOS自3.1至6.2均提供稳定支持,但由于许多细节没有澄清, 一些使用此办法的DOS重入程序可靠性不高,程序运行结果似是而非。只有对SDA对换技术作深入研究之后,才能正确使用 。关于这方面的资料,详见资料[1]。
     需要指出的是,使用SDA对换技术之后,DOS可以支持多个任务。由于应用程序可能接管中断向量(如TSR或某些字处理软件),如果系统在这方面缺乏控制,当多个任务在内存中相互作用, 形成复杂的中断链时,几乎肯定要死机,因此从系统的安全性,健壮性的角度出发,应当控制全部的中断向量。
     从单任务转化到多任务,要求对一些系统资源也要进行处理,如屏幕,当前目录等,使它能与系统当前任务相对应。 作为任务切换软件,必须解决以上的问题,本文试给出一种解决方案,具体如下:
   1、内存对换技术
     由于DOS的许多应用程序要求大量内存,因此将活动任务装入内存,而将其它任务暂存于磁盘或扩展内存中, 就能将整个内存分配给单个任务以满足内存需要,在用户请求任务切换时, 将现有任务暂存于磁盘或扩展内存中,并将新的任务装入内存运行,在切换过程中,所有的任务都具有相同的优先级,而由用户决定运行哪一个任务, 这个过程可以表示如下:
将所有的任务看作一个大的轮盘,在任务切换时,将现有任务存于轮盘上,并将新的任务从轮盘装入内存运行,在这种情况下,每个任务都认为自己拥有全部的计算机资源,这种多任务方式可以称作轮盘式多任务.考虑多方面的因素,没有必要支持任务在后台执行.

    2.避免中断向量的复杂链接:

     考虑到应用程序不可避免的要挂接中断向量, 在任务对换出去后,若中断向量还指向原处,在新的任务装入后,中断向量便会指向不可知区域,所以,要保存各个任务的中断向量表,使中断向量表局部于任务.在任务切换时,保存任务的中断向量表,在任务交换到磁盘之前将内存中所有指向此任务的中断向量改为此任务运行之前的值,即强行摘除任务挂接的中断向量,当任务再次运行时, 将保存的中断向量恢复,即"安装"它的中断处理程序,这样,任务挂接的中断仅在任务在内存中执行时起作用,当它被对换出内存后,中断向量被复原.这使的在多个任务均挂接中断向量时,不会形成复杂的, 无法解开的中断链机制.

    3.与其它程序的相容性:

     作为任务切换程序,必须和用户程序共驻内存,这就需要任务切换程序对用户应用程序的影响要尽可能的小,但是对有些实在无法避免冲突的情况,则不予考虑, 这虽然在软件工程中被看作是无法容忍的,但却是理论和实践相结合的很好例子. 只要能够相容于绝大多数普通应用软件,及一部份的TSR,就可以了.而与其它任务切换软件(如DOSSHELL)的相容性则不予考虑,理由是:没有必要在内存中存在两个任务切换程序,同时,不考虑与DOS 5.0的任务切换API的接口问题,但将努力使两个任务切换程序能在内存中相容,不发生冲突.
     相容性是一个软件能够存在和发展的重要因素, 从软件设计思想到具体技术实现,都因当首先考虑,把它贯穿于设计过程之中.首先,应在设计思想中重视相容性,不使用相容性差的思想及技术, 并且要考虑在今后的发展中会遇到的不相容的因素.其次, 在设计过程中要与现有软件作对比和测试,尽最大可能避免冲突.

     需要指出的是,相容性是在系统设计时考虑的重点,但在设计完成之后,不再考虑.

    4.软件设计中的其它考虑:

     出于单任务改造为多任务的需要,我们必须使屏幕,驱动器, 当前目录也局部于具体任务,在任务切换时做相应的保存恢复工作.

     虽然汇编语言是任务切换软件设计的直接选择,但在权衡速度,效率等方面之后,决定采用C语言来编写,毕竟,采用C语言编写系统软件已有许多成功的先例,值得一试.

     由于任务切换软件不可避免的会遇到直接使用DOS 数据结构及未公开的DOS功能调用,因此一定要保证在各个 DOS版本下的兼容性,程序何时与DOS交互,何时深入DOS编程,都应作仔细考虑,特别是直接存取DOS数据结构及使用未公开的DOS功能调用对兼容性影响最大,应使用DOS所稳定支持的,并且将来也不大可能改变的功能,在软件中直接使用的DOS数据结构有:

        1. MCB: 内存控制块(Memory Ctrl Block).
        2. SDA: DOS 可对换数据区(DOS Swap Date Area).
使用未公开功能调用有:
        1. INT 21H,5D06H 取DOS 可对换数据区(SDA)信息.
            入口:
                AX=5D06H
                (DPL 参数有争议,这里按大多数书上所写,将它略去)
            出口:
                DS:SI=SDA 地址
                CX= INDOS 对换区大小
                DX= 一直对换区大小
        2. INT 21H,50H 设置当前PSP.
            入口:
                AH=50H
这些数据结构和功能调用都是DOS自3.1至6.2所稳定支持的( 未考虑DOS4.X下的特殊情况),估计在未来的版本中也不大可能发生变化,否则,不但任务切换软件无法运行,众多的DOS应用软件也将不能运行.
     应当指出,DOS在这种既提供多任务支持, 又不建立多任务内核的矛盾情况下 ,它的内核代码也是有许多反映这种矛盾情况的地方.碰到这种情况意谓着编程者要小心"地雷",不要从上面踩过去.
 
三.设计技术:
   1.内存对换:
     一般的内存映象如下图:
    ┌──────┐
    │  D O S     │
    ├──────┤
    │ COMMAND │
    ├──────┤
    │  T A S K    │ First swap seg
    ├──────┤ ── ┌─────┐ ┌─────┐
    │            │      │          │ │          │
    │            │      │          │ │          │
    │   USER 1  │      │  USER 2 │  │  USER 3  │
    │            │      │          │ │          │
    │            │      │          │ │          │
    │            │      │          │ │          │
    │            │      │          │ │          │
    └──────┘      └─────┘ └─────┘
                                          
     如果用单板机上的存储体切换技术来看这张图 , 就比较清楚,TASK及它以前的内存属于不切换块, 而用户空间则随着具体任务的变化而选中相应的存储体, 流行的 VROOM (覆盖) 技术也与此类似.
     用户空间随着系统配置而稍有变化,一般在450-550K左右 , 当DOS被装入HMA后,用户内存可达600K左右, 这些内容在切换时均被写入磁盘,因写入量特别大,用户最好配备扩展内存,以加快切换速度.

   2.任务信息:

     为了尽量压缩任务切换程序所占内存空间, 几乎所有的任务信息均被写入磁盘,任务信息主要包括现场,SDA,中断向量表,内存, 屏幕等等.这样,大大压缩了任务切换程序所占内存空间,但也要求更多的磁盘空间.

   3.关于信号量:

     在多任务环境下,代码间的同步,互锁显得非常重要, 有许多的信号量标志(包括与DOS进行通信和交互), 应当正确处理好这些信号量.如当热键激活后,在容许再次按热键之前,用户按下热键不应当被再次激活,以避免切换程序本身的重入,伪代码为:

INVECT IntrHook proc far
        if not pop_up then
           pop_up=true
             // do anything in here
           pop_up=false
       else
        iret

   4.C语言编程:

     采用C语言编程给程序设计带来了一些新的问题,这些问题必须在编程中很好的解决 , 举一个例子 , 在中断服务代码中无法使用fprintf函数,为什么呢?因为fprintf 在函数内部使用了内存分配函数,而在中断服务程序执行时无法预料能否调用内存分配函数( 这里牵涉到是否遇到DOS重入,当前有无堆可供分配等等 ) . 在不能使用fprintf的前提下,用什么函数来代替,和fprintf 相类似的函数在库函数中还有哪些等等.同时,高级语言如何保护现场,如何进行任务切换,如何切换堆栈等问题,这在资料[2],资料[3]中已被论及. 这里给出一个在屏幕上打出时间的中断服务程序,在此可以看出库函数如何使用:
 
void interrupt NewInt1C(void)
{
static int nIsEnter=0;
static char sStr[12],*pStr;
static char far *p;
static unsigned char h,m,s;

         (*OldInt1C)();
        if(nIsEnter++<4)
                return;         /* 每隔200ms打印一次 */
        else
        {
                nIsEnter=0;     /* 互锁信号量 */
                _AH=2;
                geninterrupt(0x1a);     /* get  time  from
 real clock */
                h=_CH;
                m=_CL;
                s=_DH;
#ifndef NDEBUG          /* 调试信息: ERROR_PTR,INDOS_PTR */

                 sprintf(sStr,"%d %d ",*(pDosSwapArea),
                        *(pDosSwapArea+1));
#else                   /* 任务号,时,分,秒 */
                sprintf(sStr,"%02d %02x:%02x:%02x",
                        uTask,h,m,s);
#endif
         /* Can not use standard I/O library function */
        /* 虽然可以用cprintf函数,但不推荐这样做 */
                pStr=sStr;
                p=MK_FP(0xb800,160-22); /* Video Buffer at right_top */
                while(*pStr)
                {
                        *(p++)=*(pStr++);         /*   OME char */
                        *(p++)=RED;             /* Attrib*/
                }
        }
}
     这个程序虽然很简单,但还有一些需要解释的地方,nIsEnter声明为static是因为多次进入NewInt1C时需要知道此信号量的一贯值,而其它的值声明为static是为了防止过多的局部变量引起堆栈溢出,在调用sprintf时,要在编译时保证DS!=SS,否则参数压栈和引用参数无法得到正确结果(请注意,在NewInt1C执行时,堆栈可能是属于当前内存中的任意进程所有,而不光是你的程序),程序不使用cprintf 是因为它有可能与前台任务的文本函数相互干扰,并有可能造成BIOS重入,最重要的是,它的堆栈占用太大(尽管它可以在程序中工作, 但这并不是使用它的理由).
 
四.设计实现:
 
   1.程序设计:
     为了实现强制任务切换,程序必须挂接INT 9H,以达到必要的快速响应.同时,应实现以下流程:

  ┌─────┐
  │  任务 1  │   
  ├─────┤
  │  任务 2  │         INT 9H  服务程序
  ├─────┤    ┌─────────┐
  │  任务 3  │    │                  │
  ├─────┤    │    调度程序      │
  │  任务 4  │    │                  │
  ├─────┤    └─────────┘
  │  任务 5  │
  └─────┘
     就绪队列
    
INT 9H程序伪代码为:

      if not our hotkey
     {
          old_int9 proc
          return;
     }
     if also POPUP or DOS_busy or crit_error
          return;

     POPUP=1;
          enter scheldure

     POPUP=0;
     return
 
     scheldure程序伪代码为:

     save oldSDA    保存老的SDA

     save oldVECT      保存老的中断向量

     save oldmemory    保存老的内存

     get newTASK       取下一个任务  

     load newmemory    装入新的内存

     load newVECT      装入新的中断向量 

     load newSDA       装入新的内存 

     return

   2.关键技术的解决办法:

     内存的保存涉及MCB(memory contrl block)

     struct MCB {
          char attr;      'M': normal,'Z':last MCB
          unsigned size;     
          unsigned owner;     owner psp
     }

     MCB 近乎于单链表,为段对齐,下一块MCB 的段地址等于本块的段地址加本块的长度,一直到最后一个MCB,它的attr为'Z'.保存/ 恢复内存时,从用户的MCB开始,一直到 A000H.以下为伪代码:

     char *p;
     for(p=firstaddress;p<MK_FP(0xa000,0);)
     {
          write(file,p,BLOCKSIZE);
          p+=BLOCKSIZE;
          p=(char huge *)p+0;
     }

      当DOS在关键区时,不容许任务切换,因此建立CRIT_ERROR 信号量来与DOS通信(详见资料[1]):

当DOS进入或退出关键区会通知用户:

     INT 2AH
          AH=80:    DOS 通知用户进入关键区
          AH=81:    DOS 通知用户退出关键区
          AH=82:    DOS 通知用户退出关键区

用户挂接INT 2A 后就可知道DOS是否在关键区.

     INT2A proc far
          switch(ax&0xff00)
          {
               case 8000:         
                    CRIT_ERROR++;
                    break;
               case 8001:
               case 8002:
                    if(CRIT_ERROR)
                         CRIT_ERROR--;
                    break;
               default:
                    old_INT2A
          }
          iret
     endp

     屏幕的保存伪代码如下:

     save display mode
     if mode<8 save textbuffer
     else save  graphicsbuffer

     save txetbuffer
    {
     char far *p=MK_FP(0xb800,0);
          write(fp,p,4000);
     }

     save graphicsbuffer /* 图形下必需保存4个页面 */
     {
     char far *p=MK_FP(0xa000,0);
          select page0
               write(fp,p,38400);
          select page1
               write(fp,p,38400);
          select page2
               write(fp,p,38400);
          select page3
               write(fp,p,38400);
     }    
    
由于使用C语言编程,C语言中断程序实际上是:

     void interrupt a(void)
     {
     do any code in here
     }
  
     相应的汇编语言为:

    a proc far
    push all
    mov ds,DATA
    do any code in here
     pop all
     iret
     endp

     而手工汇编为:
     a proc far
     iret
     endp
 
     了解这些有助于我们的程序设计,C语言相对于汇编语言还存在一定的差距,体积和效率都太差。

     对话框是程序设计中用到的一个对象, 全部风格仿照afxwin.h的式样,代码如下:

/* 画一个比窗口略大的框 */
 

#define SINGLELINE       1
#define DOUBLELINE    2

void Box(RECTSTRUCT *p,int type,char *title) {
int x1,y1,x2,y2;
int i;
int a,b,c,d,h,l;
int titlelen;
       if(type==SINGLELINE) {
              a=218,b=191,c=217,d=192,h=179,l=196;
       }
       if(type==DOUBLELINE) {
              a=201,b=187,c=188,d=200,h=186,l=205;
       }
       x1=p->nXBegin;
       y1=p->nYBegin;
       x2=x1+p->nLength;
       y2=y1+p->nHigh;
        x1--,y1--,x2++,y2++;
     window(1,1,80,25);
       SetColor(ColorStruct.BoxColor,ColorStruct.BoxBackColor);
       gotoxy(x1,y1);
       titlelen=strlen(title)+2;
       cprintf("%c",a);
        for(i=x1+1;i<x2;i++)
              cprintf("%c",l);
       cprintf("%c",b);
       gotoxy(x1+(x2-x1+1-titlelen)/2+1,y1);
       cprintf("%s",title);
       gotoxy(x1,y2);
       cprintf("%c",d);
       for(i=x1+1;i<x2;i++)
              cprintf("%c",l);
       cprintf("%c",c);
       for(i=y1+1;i<y2;i++) {
              gotoxy(x1,i);
              cprintf("%c",h);
              gotoxy(x2,i);
              cprintf("%c",h);
       }
}
 

五.设计检验:

     程序设计及调试是在一台 Legend LX E3/40 上进行的,采用的DOS版本为OME版 DOS 3.31,C语言编译器为Turbo C 2.0.源程序长约13K,可执行文件比源文件略小,说明C语言编程是比较成功的.

      主要指标为:
┌────────┬──────┬───────────┐
│项  目          │指  标      │ 最    好    值       │
├────────┼──────┼───────────┤
│任务数          │  6个      │                      │
├────────┼──────┼───────────┤
│切换速度        │  3秒      │ 1秒(使用扩展内存)    │
├────────┼──────┼───────────┤
│任务显示模式支持│  13H以下  │ 320*400 256色        │
├────────┼──────┼───────────┤
│对换空间占用    │  900K以下 │                      │
└────────┴──────┴───────────┘

     在调试完成之后,软件在 DOS 3.31,DOS 5.0,DOS 6.2下进行了运行测试,主要目的是观察它与其它软件的相容性,同时,也与DOS 5.0下的DOSSHELL进行了对比.
 
     DOS 3.31下的结果如下:

     此机上所装大部分软件均运行良好,清单如下: CCED 5.0, TC 2.0,TURBO PASCAL 5.0, PCTOOLS 5.0, FOXBASE 2.1,  DBASE  3 PLUS, BGIDEMO(TC下的图形演示软件).WPS 和 SPT 运行正常,但任务切换无法进行,原因是它们在挂接INT9H 后均未调用原来的中断处理程序.使用CHINA(一个地图漫游软件)时在装入过程中死机.TT( 一个采用320*200 256色显示模式及模拟语言发声的指法练习软件)使用正常.
     SPDOS 5.0 下使用中效果良好,甚至将SPDOS作为普通任务进行切换也没有什么问题.
     UCDOS 3.0 在使用中效果良好,但由于它的图形模式特别,偶而在切换过程中屏幕保存/恢复出现问题,但程序并未死机,适当处理后可继续运行(UC_DOS必须先装入).
     CodeView 2.0在切换中严重死机,但DEBUG和Turbo Debugger 3.0运行正常.
     当任务数超过4个时,有时有死机现象,主要是因为DOS 3.31 下的设置为 FILES=20,任务数增加时打开文件出错造成的.
     综上所述,大部分的应用软件均可良好相容,但游戏软件相容性比较差,这与设计初的设想一致.
     CodeView 2.0的严重死机可能是由于它再加载进程运行调试的同时,它自己也在内存中进行着覆盖对换, 与切换系统产生冲突而引起.
 
     DOS 6.2下的结果如下:

      由于没有DOS 6.2下的DOSSHELL,因此测试项目与DOS 3.31相同,结果也相同,但由于设置为FILES=30,所以很少出现死机.

     DOS 5.0下的结果如下:

     DOS 5.0下的设置为FILES=60,测试项目与DOS 3.31相同, 结果也相同,因此主要的测试转向与DOSSHELL(DOS 5.0下的任务切换软件)的对比,对比结果如下(TASK 为作者所设计的任务切换软件):

   1.指标对比:

┌─────────┬─────────┬──────┐
│ 程序表现         │   DOSSHELL     │   TASK    │
│ 项    目         │                  │            │
├─────────┼─────────┼──────┤
│ 编写语言         │   汇  编         │   TC 2.0   │
├─────────┼─────────┼──────┤
│ 内存占用         │   38K            │   22K     │
├─────────┼─────────┼──────┤
│ 界面友好程度     │   好             │   差       │
├─────────┼─────────┼──────┤
│ 任务数           │   多  个         │   多  个   │
├─────────┼─────────┼──────┤
│ 切换速度(一般)   │   2秒以内        │  3秒以内  │
├─────────┼─────────┼──────┤
│ 挂接中断向量     │   约20个         │  三个     │
├─────────┼─────────┼──────┤
│ 其它功能         │   文件,进程管理  │   很少     │
├─────────┼─────────┼──────┤
│ 应用软件相容性   │   见下面的分析   │            │
└─────────┴─────────┴──────┘
 
   2.应用软件相容性(切换效果)对比:

程    序          DOSSHELL                TASK

TC 2.0            好                      好
TURBO PASCAL 5.0  好                      好
PCTOOLS 5.0       好                      好
CCED 5.0          西文好                  好
DEBUG             好                      好
FOXBASE 2.1       西文好                  好
DBASE 3 PLUS      西文好                  好
BGIDEMO           可以运行                好
                  无法切换
SPDOS 5.0         使用冲突                好
UCDOS 3.0         部分软件使用冲突        好,
                                          但须先加载UCDOS
                                          否则不能切换
WPS 2.1           SPDOS不能运行           可以运行
                  所以无法运行            无法切换
SPT 1.2           SPDOS不能运行           可以运行
                  所以无法运行            无法切换
CodeView 2.0      不能切换                不能切换
TURBODEBUGGER 3.0 不能切换                好
TT                无法运行                好

注:
   好:                     运行切换均正常.
   西文好:                 无法装载汉字系统,西文下正常.
   可以运行,无法切换:      运行正常, 但无法切换或切换时程序退出.
   不能切换:               运行正常,切换时引起死机.
   使用冲突:               运行时死机,无法装入内存.

    运行结果可以总结为:
 
    DOSSHELL 在普通任务切换效果上比TASK好,但它不适合于图形切换,并且与汉字系统和TSR程序很不相容,它的另一个优点是优秀的文本界面.
     TASK 在设计上比 DOSSHELL 粗糙,但它的代码短小, 并且相容性比较好,特别适合于图形及汉字下的任务切换.
     TASK 自始至终没有考虑与DOSSHELL的相容性问题,但两者之间有着良好的相容性,两个程序甚至可以在内存中共同进行任务切换而互不影响.

 六.其它问题:

     任务切换软件还有一些使用技巧, 例如可以用它来任意保存屏幕图象,卸下TSR程序等等,下面列出几个:
    1.保存/恢复屏幕:
     TASK在任务切换时保存了用户屏幕,所以当需要得到屏幕图像时,进行任务切换,然后拷贝屏幕影像文件保存,以后可以利用恢复屏幕功能来重新显示图像 .
   2.卸下TSR程序:
     由于TASK在任务切换时会"摘除"用户程序挂接的中断向量, 所以在TASK以后驻留的TSR程序均可在任务切换中被"卸下".

七.小结:

      TASK在近几年内还有一定的使用价值,随着技术的进步,16位程序将向32位全面过渡,出于兼容的考虑,大量的应用程序将能够 以二进制兼容的方式继续使用,但TASK不属于这类程序.

 
参考文献:

资料[1]: <<DOS 重入问题的探讨与实现>>,赵廷哲.1995
资料[2]: <<未公开的DOS核心技术>>,ANDREW SCHULMAN等.清华大学出版社 1992
资料[3]: <<C语言高级实用教程>>,尹彦芝.清华大学出版社 1991

     在设计过程中,作者始终得到了计算机系老师们的大力帮助,王让定副教授给予了热情指导,提出了许多宝贵意见和建议, 建立了本系统的思想框架,并对本文作了详细的审阅; 王小牛老师作为作者的指导老师,贯穿于论文设计之中提出了许多思想,观点,尤其是他介绍给作者的多任务下的多堆栈切换机制使作者深受启发,并使作者最后下决心将简单的前后台任务改为真正的多任务系统,在设计的整个过程中他都给予了具体指导;索国瑞老师则给作者讲述了显示卡上的许多知识,解决了屏幕保存/恢复的难题;  还有许多老师和同学们则在不同方面提出了见解和建议,它们都是论文思想的来源, 在此一并表示感谢.

附:完整程序清单(请在源码分类中下载) 。

【上篇】
【下篇】

抱歉!评论已关闭.