嵌入式课程作业记录——ARM复习提纲(下)

Posted Mount256

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了嵌入式课程作业记录——ARM复习提纲(下)相关的知识,希望对你有一定的参考价值。

教材:《ARM 9嵌入式系统设计与开发应用》(熊茂华,杨震伦编著)(清华大学出版社)

考试题型:选择题40分(20题),填空题10分(5题),简答题20分(4题),读程序20分(4题),写程序10分(1题)。

17. S3C2410电源控制模式有正常、慢速、空闲和电源关断4种模式

  • 【正常模式】电源管理模块为CPU和S3C2410中的外围设备提供时钟。在这个模式由于所有外围设备都处于开启状态,因此功耗达到最大。
  • 【慢速模式(无PLL模式)】不使用PLL,而是用外部时钟(XTIPLL/EXTCLK)直接作为FCLK时钟(CPU内核时钟)。在这种模式下,功耗仅取决于外部时频率,与PLL无关。
  • 【空闲模式】电源管理模块只断开FCLK,但仍为所有其他外围设备提供时钟。该模式降低了CPU的功耗,任何中断请求可以从空闲模式唤醒CPU。
  • 【掉电模式】电源管理模块断开内部电源。除了唤醒逻辑外,CPU和内部逻辑都不会产生功耗。激活掉电模式需要两个独立的电源,一个电源为唤醒逻辑供电,另一个电源为包括CPU在内的其他内部逻辑供电,并且可以开关控制。在该模式下,第二个电源将关闭。

18. 汇编语言和C混合编程通常有哪几种方式?

常见的有3种方式:

  • 在C代码中嵌入汇编指令
void string_copy(char *dst,const char *str)

  char ch;
  __asm
  
    loop
      LDRB  ch,[str],#1
      STRB  ch,[dst],#1
      CMP    ch,#0
      BNE loop 
  


int main()

  char *a="hello world!"
  char b[64];
  string_copy(a,b);
  return 0;

  • C调用汇编:

(1)汇编export

(2)C语言定义 extern function

(3)C语言调用汇编

myArm.s
  AREA  myARM,CODE ,READONLY
  export my_strcopy
my_strcopy
      loop
        LDRB R4,[R0],#1
        CMP  R4,#0
        BEQ OVER
        STRB R4,[R1],#1
        B loop
      OVER
           end

myMain.c

extern void my_strcopy(char *dtr,char*str);
int main()

  char *a="hello world!"
  char b[64];
  my_strcopy(a,b);
  return 0;

  • 汇编调用C语言:

(1)C语言实现函数

(2)汇编import函数名

(3)BL 函数名

myArm.s
  AREA  myARM,CODE ,READONLY
  IMPORT c_fun
  ENTRY
start
  mov R0,#1
  mov R1,#2
  mov R2,#3
  BL  c_fun
  end

myMain.c
int c_fun(int a,int b,int c)//从汇编中调到这里,此时a=1,b=2,c=3

  return a+b+c;//从这里再返回去,此时R0=6;

19. ARM汇编语言与C语言混合编程的子程序之间的调用必须遵循一定的调用规则,这些规则统称为ATPCS

ATPCS即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)的简称。
PCS规定了应用程序的函数可以如何分开地写,分开地编译,最后将它们连接在一起,所以它实际上定义了一套有关过程(函数)调用者与被调用者之间的协议。

PCS强制实现如下约定:调用函数如何传递参数(即压栈方法,以何种方式存放参数),被调用函数如何获取参数,以何种方式传递函数返回值。

部分规则:

  • r0-r3一般用来传递函数的参数,r4-r7则用来放置局部变量。而r12-r15则可以有特别的用途。
  • 小于32位的参数值会被自动扩展为32位。
  • 64位的参数被当成两个32位数。
  • 对于浮点数.如果芯片本身硬件上支持浮点运算,则浮点参数会放在浮点寄存器里传递。如果硬件上不支持浮点数运算,则转化为整型放通用寄存器传递。
  • 其它类型通通转为32位整型数传递。
  • 对于参数可变的程序调用,前四个参数放在r0~r3中传递,如果多于四个参则按相反的顺序进栈保存,所谓相反的顺序是指靠前参数后进栈。
  • 对于固定参数的调用,如果有可以做浮点运算的硬件部件, 各个浮点参数按顺序处理;为每个浮点参数分配FP寄存器;分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FP寄存器。第一个整数参数通过寄存器R0~R3来传递,其他参数通过数据栈传递。

20. ucos内核调度特点(P98)

uC/OS内核调度主要有以下特点:

  • 只支持基于优先级的抢占式调度算法,不支持时间片轮训。
  • 64个优先级,只能创建64个任务,用户只能创建56个任务。有一个优先级最低的空闲任务,在没有用户任务时运行。
  • 每个任务优先级都不相同。0优先级最高,63优先级最低。
  • 不支持优先级逆转。
  • READY队列通过内存映射表实现快速查询,效率非常高。
  • 支持时钟节拍。
  • 支持信号量、消息队列、事件控制块、事件标志组、消息邮箱任务通信机制。
  • 支持中断嵌套,嵌套层数可达255层,中断使用当前任务的堆栈保存上下文。
  • 每个任务都有自己的堆栈,堆栈大小有用户自行决定。
  • 支持动态修改任务优先级。
  • 任务TCB为静态数组,建立任务只是从中获得一个TCB,不用动态分配,释放内存。
  • 任务堆栈为用户静态或动态创建,在任务创建外完成,任务创建本身不进行动态内存分配。
  • 任务的总个数(OS_MAX_TASKS)由用户决定。

21. ucos TCB的内容(P100)

(都是些什么乱七八糟的)

主要参数的功能如下:

  • *OSTCBStkPtr是指向当前任务栈顶的指针。
  • *OSTCBExtPtr是任务扩展模块使用。
  • *OSTCBStkBottom是指向任务堆栈栈底的指针。
  • OSTCBStkSize是存有栈中可容纳的指针元数目。
  • OSTCBOpt把“选择项”传给函数OSTashCreateExt()。只有当用户将OS_CFG.H文件中的OS_TASK_CREATE_EXT设为1时,这个变量才有效。
  • OSTCBId用于存储任务的识别码(ID)。
  • OSTCBNextOSTCBPrev用于任务控制OS_TCBs的双向链表的前后连接,该链表在时钟节拍函数OSTimerTick()
  • OSTCBEventPtr是指向事件控制块的指针。
  • OSTCBMsg是指向传给任务消息的指针。
  • OSTCBDly当需要把任务延时若干个时钟节拍时要用到这个变量,或者需要把任务挂起一段时间以等待某事件的发生。
  • OSTCBStat是任务的状态字。
  • OSTCBPrio是任务优先级,高优先级任务的OSTCBPrio值最小。
  • OSTCBDelReq是一个布尔量,用于表示该任务是否需要删除。
  • OSTCBX OSTCBY OSTCBBitX OSTCBBitY用于加速任务进入就绪态的过程或进入等待事件发声状态的过程。这些值是在任务建立时算好的,或者是在改变任务优先级时算出的。

22. ucos就绪表、写表(登记)、删表算法(P100) 查询最高优先级算法(P101)(填空)

(1)就绪表

任务就绪表记录了系统中所有处于就绪状态的任务,从代码上来看它就是一个类型为INT8U的数组OSRdyTbl[]。。系统中的任务为32个时,OSRdyTbl[]就有4个成员。每个成员占据8位,所以OSRdyTbl[]的每一个数据元素对应8个任务,这8个任务称为一个任务组。在就绪表中,以任务优先二进制位,当该位为1时表示对应的任务处于就绪状态,反之为非就绪状态。

考虑到查找效率,uCOS-II定义了一个INT8U的变量OSRdyGrp,该变量的每一位都对应OSRdyTbl[]的一个任务组(即数据的一个成员)。若某任务任务所对应的位置置为1,否则为0。

举例:OSRdyGrp=00001011,那么就意味着OSRdyTbl[0]、OSRdyTbl[1]、OSRdyTbl[3]中有就绪的任务。由图可知,uCOS-II最多可以管理8 * 8 = 64个任务。

任务就绪表是以任务的优先级从低到高排序的,那么想要根据任务的优先级来找到该任务所处于就绪表中位置就轻而易举了:

由于系统至多支持64个任务,所以优先级至多也就到63,即二进制的00111111,只占据低6位,每一个OSRdyTbl[]元素只是占据8,所以只需要用3个二进制位即可表示这8位中的哪一位为1,同理,高3位用于表示至多8个OSRdyTbl[]元素的哪一个元素。即:优先级的高3位二进制位(D5、D4、D3)指明,即:优先级的高3位二进制位(D5、D4、D3)指明OSRdyTbl[]的数组下标n,低3位(D2、D1、D0)指明OSRdyTbl[n]的哪一位数据位。另外,确定OSRdyTbl[]的下标n,也说明OSRdyGrp的第几位置位。

举例:某任务的优先级prio=24,问该任务落在就绪表中的哪一位?

24的二进制位为00011000,D5、D4、D3位011,即OSRdyTbl[]的下标为3,D2、D1、D0为0,即优先级prio=24的任务在OSRdyTbl[3]的第0位。OSRdyGrp的第3位置位。

每个任务的就绪态标志都放入到就绪表中,就绪表中有两个变量OSRdyGrpOSRdyTbl[]

OSRdyGrp中,任务按优先级分组,8个任务为一组。OSRdyGrp中的每一位表示8组任务中每一组是否有进入就绪态的任务。

任务进入就绪态时,就绪表OSRdyTbl[]中的相应元素的相应位也置为1。就绪表OSRdyTbl[]数组的大小取决于OS_LOWEST_PRIO

为确定下次该哪个优先级的任务运行了,内核调度器总是将最低优先级的任务在就绪表中相应字节的相应位置1,即OS_LOWEST_PRIO = 1

(2)写表、删表

使任务进入就绪表(通过OSMapTbl[]来在就绪表相应的行和列置1):

OSRdyGrp           |= OSMapTbl[prio >> 3];
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07];

任务优先级的低三位用于确定任务在总就绪表OSRdyTbl[]中的所在位,接下去的三位用于确定是在OSRdyTbl[]数组的第几个元素。OSMapTbl[]用于限制OSRdyTbl[]数组元素下标为0-7。

从就绪表中删除一个任务(通过OSMapTbl[]来在就绪表相应的行和列置0):

if ((OSRdyTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0)
   OSRdyGrp &= ~OSMapTbl[prio >> 3];

将就绪任务表数组OSRdyTbl[]中相应元素的相应位清0。而对于OSRdyGrp,只有当被删除任务所在任务组中全组任务一个都没有进入就绪态时才将相应位清0,即OSRdyTbl[prio >> 3]所有位为0时,OSRdyGrp的相应位才清零。

(3)从就绪表中查找优先级最高的任务

使用的是哈希算法。

所以要找出优先级最高的任务,分两步:

第一步,确定任务组(OSRdyTbl[]的下标)Y:找出OSRedyGrp中为1的最低位Y;

第二步,确定任务组中的位X:找出任务组中OSRdyTbl[x]中为1的最低位X。

综上,找出目标任务的核心算法在于确定某数值为1的最低位,uCOS-II的具体实现是,借助OSUnMapTbl[]数组:

例如0x06(00000110),为1的最低位是Bit[1],那么OSUnMapTbl[0x06]=1;0x20(00100000),为1的最低位是Bit[5],即OSUnMapTbl[0x20]=5。

INT8U   y;
y             = OSUnMapTbl[OSRdyGrp]; //最高优先级所在的任务组     
OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]); //最高优先级任务所在的任务组的位

INT8U  const  OSUnMapTbl[256] = 
    0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x00 to 0x0F  */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x10 to 0x1F */
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x20 to 0x2F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x30 to 0x3F */
    6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x40 to 0x4F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x50 to 0x5F */
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x60 to 0x6F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x70 to 0x7F */
    7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x80 to 0x8F */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0x90 to 0x9F */
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0xA0 to 0xAF */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0xB0 to 0xBF */
    6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0xC0 to 0xCF */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0xD0 to 0xDF */
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,       /* 0xE0 to 0xEF */
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0        /* 0xF0 to 0xFF */
;

任务的优先级prio = (任务组Y) << 3 | 任务组的上哪一位X

y   = OSUnMapTbl[OSRdyGrp];
x   = OSUnMapTbl[OSRdyTbl[y]];

prio = (y << 3) + x;

23. Task任务管理类create(P104),Sem同步类create、pend、post(P105),Mbox通信类create、pend、post使用方法(读程序题、写程序题)(P105)

进程间的通信方式有两种,一种是使用共享内存,这种方式基本不依赖OS,也没有相应的系统开销。另一种则需要OS支持,通过建立链接器实现任务间的通信。

在UCOSii中,多个任务使用同一块内存区域需要提供一种互斥存取的方法。否则该段共享数据很有可能在被访问前就被其他任务重置了。

其中OS_ENTER_CRITICAL()就是关中断函数,而OS_EXIT_CRITICAL()就是开中断函数。
利用关中断宏OS_ENTER_CRITICAL()、OS_EXIT_CRITICAL()以及开调度锁是利用函数 OSSchedLock()、 OSSchekUnlock()可以实现单任务对某一资源的暂时性独享。

用这种方法实现数据共享存在很大的局限性,一个简单的例子,当一个共享资源允许被多个任务同时占用,这种方式就很低效。

RTOS会提供信号量、邮箱和消息队列来支持任务间的通信与同步,即使是非实时性操作系统也同样有这样的接口,这已经类似于一种规范。

信号量的概念最初由Edsger Dijkstra提出。

假定有多个任务需要读写一块板卡上的flash芯片,如果他们之间没有协商,而是各自单独对flash进行读写,就会使flash的读写操作处于不可预料的状态中。此时可以建立一个信号量,当有任务进行读写操作时,便申请该信号量,操作完成后再释放。如果已经有任务占用了该信号量,请求信号量就会失败,任务可以等待该信号量被释放,再进行相关的操作。一个共享资源也可能最多被几个任务占用,这种情况下信号量可以为一个计数器,当有任务占用共享资源时,信号量减一。

很明显,信号量只解决了共享资源的占用的问题。它不能传递信息,假如某下位机有一个专门用来解释上位机控制命令的任务,当上位机没有数据传送过来时,该任务处于挂起状态。但当通讯中断发生后,该任务不仅要知道已经有控制字被传送过来,还需要知道该控制字是什么。所以信号量在这里并不适用,解决的办法是建立一个邮箱。由于要传递的信息很可能超过一个常规变量的大小,所以邮箱的内容是一个指针。将命令解释器的优先级设置为高于其他任务,它始终要等待一个邮箱。将该邮箱里指针的值指向接收缓冲区,该任务就开始处理控制字。

消息队列是一组指针,它可以被看成是一组邮箱的集合。

假设有一个流水线分拣系统,传感器会检测货物的一些物理参数。每来一个产品,系统就建立一个关于该产品的结构体用于描述该产品的属性。如果使用邮箱,那么在一个产品被分拣任务处理前,新到产品就必须延时处理。使用消息队列,可以将一组指针推入队列,每一个指针都描述一个产品。这样分拣任务可以根据先后入队列的顺序,依次分拣每个产品。

事件控制块Ecb用来维护一个事件控制块的所有信息,该结构体不仅包含信号量/邮箱/消息队列的值,还包含等待它的任务列表。

Ecb反映了一种朴素的简化程序逻辑结构的思想。用统一的数据结构来描述对象的属性,再在处理程序里统一处理。对信号量/邮箱/消息队列的创建、维护都只是读写Ecb,在调度程序里,统一处理Ecb。

typedef struct 
void *OSEventPtr; /* 指向消息或者消息队列的指针 */
INT8U OSEventTbl[OS_EVENT_TBL_SIZE]; /* 等待任务列表 */
INT16U OSEventCnt; /* 计数器(当事件是信号量时) */
INT8U OSEventType; /* 事件类型 */
INT8U OSEventGrp; /* 等待任务所在的组 */
 OS_EVENT;

(1)任务类

  • OSTaskCreate():作用是创建一个任务。 有4个参数(任务的入口地址、任务的参数、任务的堆栈的首地址、任务的优先级)。调用该函数后,系统会首先从TCB空闲列表内申请一个空的TCB指针,然后会根据用户给出的参数初始化任务堆栈,并在内部的任务就绪表内标记该任务为就绪状态。返回后即可成功创建一个任务。这个函数应该至少在main函数内调用一次,在OSInit函数调用之后调用。

  • OSTaskSuspend():作用将指定的任务挂起。如果挂起的是当前任务,将会引发系统执行任务先导函数OSShed来进行一次任务切换。 只有一个参数(指定任务的优先级)。注意,任务的ID就是其优先级,所以ucos不允许有相同的优先级。

  • OSTaskResume():作用是将挂起的任务恢复成就绪状态。如果恢复任务的优先级高于当前任务就会引发一次任务切换。 注意,本函数不必跟上面函数成对搭配使用。

(2)消息类

  • OSMBoxCreate():作用是创建消息邮箱。 消息邮箱是操作系统的任务间通信的一种方式,一个任务可以阻塞或不阻塞地等待另外一个任务发送到它的邮箱的邮箱消息,而根据消息内容进行动作。(这说的是人话吗???)

  • OSMBoxPost():用于一个任务向另一个任务的邮箱发邮箱消息,消息的内容在参数中指定。

  • OSMBoxPend():用于一个任务获取本任务邮箱中的消息。 如果邮箱中没有消息,则等待,任务处于阻塞状态。

(3)同步类

  • OSSemCreate():作用是创建信号量。 信号量是操作系统的任务间同步的一种方式,两个或者多个任务可以获取信号量的状态并根据它进行动作从而实现同步。(这写的有点拗口,语文不合格!)

  • OSSemPost():用于一个任务对一个信号量进行设置。 设置的内容在参数中指定。

  • OSSemPend():用于一个任务获取本信号量的状态。 如果信号量不为零,则成功获取信号量并将信号量减去1;如果信号量为零,则等待,任务处于阻塞状态。

(4)时间类(补充)

  • OSTimeDly():作用是先挂其当前任务,然后进行任务切换,在指定时间到来后,将当前任务恢复为就绪状态,但是不一定运行,如果恢复后是优先级最高的就绪任务,那么就运行它。

(5)内存操作类(补充)

  • OSMemCreate():作用是创建一个内存分区。
  • OSMemGet():作用是从一个指定的内存区中分配一个内存块。
  • OSMemPut():作用是释放一个内存块。

24. ucos移植对处理器的要求,移植的4个步骤(P107)

处理器必须满足的要求:

  • 处理器的C编译器能产生可重入代码。
  • 程序中可以打开或关闭中断。
  • 处理器支持中断,并且能产生定时中断。
  • 处理器支持能够容纳一定数据的硬件堆栈。
  • 处理器有将堆栈指针和其他CPU寄存器存储的指令和读出到堆栈(或内存)的指令。

移植的4个步骤:

  • 设置os_cpu.h中与处理器和编译器相关的代码。
  • 用C语言编写6个操作系统相关的函数。(堆栈初始化函数、任务创建钩子函数、任务删除钩子函数、任务切换钩子函数、任务状态钩子函数、时钟节拍钩子函数)
  • 用汇编语言编写4个与处理器相关的函数。(运行优先级最高的就绪任务函数、任务级的任务切换函数、中断级的任务切换函数、时钟节拍中断函数)
  • 编写一个简单的多任务程序来测试移植是否成功。

25. 会出一些C语言选择题,数组、指针、结构体的操作

回去给我复习C语言基础!不再赘述!

26. 读程序(20分)

  • 1个汇编和C互相调用(补充语句)
  • 1个ucos(说明运行结果)
  • 1个裸机(考注释)
  • 1个汇编(说明功能和运行结果)

27. 写程序:ucos的MBOX和SEM(通信和同步机制),(比如说任务A、B、C,A运行1次后,B才运行,最后A、B都运行后,C才运行)(10分)

假设task1优先级为3,task2优先级为7,task3优先级为9。运行顺序:task1、task2、task1、task2、task3。(不会做,可直接背以下框架

#define	Task1StkLengh	64              // 定义用户任务1的堆栈长度
#define	Task2StkLengh	64              // 定义用户任务2的堆栈长度
#define	Task3StkLengh	64              // 定义用户任务3的堆栈长度

OS_STK	Task1Stk [Task1StkLengh];       // 定义用户任务1的堆栈
OS_STK	Task2Stk [Task2StkLengh];       // 定义用户任务2的堆栈
OS_STK	Task3Stk [Task3StkLengh];       // 定义用户任务3的堆栈

OS_EVENT *Sem1;         // 信号量1
OS_EVENT *Sem2;         // 信号量2
OS_EVENT *Sem3;         // 信号量3

//OS_EVENT *RandomMBox;

void Task1	(void *pdata);
void Task2	(void *pdata);
void Task3	(void *pdata);

int main (void)

	OSInit ();			
	
	OSTaskCreate (Task1, (void *)0, &Task1Stk[Task1StkLengh - 1], 3);		
	OSTaskCreate (Task2, (void *)0, &Task2Stk[Task2StkLengh - 1], 7);
	OSTaskCreate (Task3, (void *)0, &Task3Stk[Task3StkLengh - 1], 9);
	
	Sem1 = OSSemCreate (0);
	Sem2 = OSSemCreate (1);
	Sem3 = OSSemCreate (0);
	
	//RandomMBox = OSMBoxCreate((void *)0);
	
	OSStart ();
	return 0;															


void Task1	(void *pdata)

    INT8U Reply;

	pdata = pdata;
	for (;;)
	
	    OSSemPend (Sem1, 0, &Reply);
	    OSSemPost (Sem2);
	    
	    //OSMboxPost (RandomMBox, s);
	    
	    OSTimeDlyHMSM (0, 0, 0, 350);
	


void Task2	(void *pdata)

    INT8U err;

    pdata = pdata;
    for (;;)
    
        //msg = OSMBoxPend (RandomMBox, 0, &err);
        OSTimeDlyHMSM (0, 0, 0, 350);
    


void Task3	(void *pdata)

	pdata = pdata;
	for (;;)
	
	    OSTimeDlyHMSM (0, 0, 0, 350);
	

以上是关于嵌入式课程作业记录——ARM复习提纲(下)的主要内容,如果未能解决你的问题,请参考以下文章

嵌入式课程作业记录——ARM复习提纲(上)

嵌入式课程作业记录

计算机组成原理复习提纲

JMU软件大数据技术复习提纲

JMU软件大数据技术复习提纲

动态SQL基础概念复习(Javaweb作业5)