首页 TASK

TASK

举报
开通vip

TASKChapter 4 Task Management We saw in the previous chapter that a task is either an infinite loop function or a function that deletes itself when it is done executing. Note that the task code is not actually deleted, µC/OS-II simply doesn’t know about the tas...

TASK
Chapter 4 Task Management We saw in the previous chapter that a task is either an infinite loop function or a function that deletes itself when it is done executing. Note that the task code is not actually deleted, µC/OS-II simply doesn’t know about the task anymore and thus that code will not run. A task look just like any other C function containing a return type and an argument but, it must never return. The return type of a task must always be declared to be void. The functions described in this chapter are found in the file OS_TASK.C. To review, a task must look as shown below. void YourTask (void *pdata) { for (;;) { /* USER CODE */ Call one of uC/OS-II’s services: OSMboxPend(); OSQPend(); OSSemPend(); OSTaskDel(OS_PRIO_SELF); OSTaskSuspend(OS_PRIO_SELF); OSTimeDly(); OSTimeDlyHMSM(); /* USER CODE */ } } or, void YourTask (void *pdata) { /* USER CODE */ OSTaskDel(OS_PRIO_SELF); } This chapter describes the services that allow your application to create a task, delete a task, change a task’s priority, suspend and resume a task and, allow your application to obtain information about a task. µC/OS-II can manage up to 64 tasks although µC/OS-II reserves the four highest priority tasks and the four lowest priority tasks for its own use. This leaves you with up to 56 application tasks. The lower the value of the priority, the higher the priority of the task. In the current version of µC/OS-II, the task priority number also serves as the task identifier. 4.00 Creating a Task, OSTaskCreate() In order for µC/OS-II to manage your task, you must ‘create’ a task. You create a task by passing its address along with other arguments to one of two functions: OSTaskCreate() or OSTaskCreateExt(). OSTaskCreate() is backward compatible with µC/OS while OSTaskCreateExt() is an ‘extended’ version of OSTaskCreate() and provides additional features. A task can either be created using either function. A task can be created prior to the start of multitasking or by another task. You MUST create at least one task before you start multitasking (i.e. before you call OSStart()). A task cannot be created by an ISR. The code for OSTaskCreate() is shown in listing 4.1. As can be seen, OSTaskCreate() requires four arguments. task is a pointer to the task code, pdata is a pointer to an argument that will be passed to your task when it starts executing, ptos is a pointer to the top of the stack that will be assigned to the task (see section 4.02, Task Stacks) and finally, prio is the desired task priority. INT8U OSTaskCreate (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio) { void *psp; INT8U err; if (prio > OS_LOWEST_PRIO) { (1) return (OS_PRIO_INVALID); } OS_ENTER_CRITICAL(); if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { (2) OSTCBPrioTbl[prio] = (OS_TCB *)1; (3) OS_EXIT_CRITICAL(); (4) psp = (void *)OSTaskStkInit(task, pdata, ptos, 0); (5) err = OSTCBInit(prio, psp, (void *)0, 0, 0, (void *)0, 0); (6) if (err == OS_NO_ERR) { (7) OS_ENTER_CRITICAL(); OSTaskCtr++; (8) OSTaskCreateHook(OSTCBPrioTbl[prio]); (9) OS_EXIT_CRITICAL(); if (OSRunning) { (10) OSSched(); (11) } } else { OSTCBPrioTbl[prio] = (OS_TCB *)0; (12) } return (err); } else { OS_EXIT_CRITICAL(); return (OS_PRIO_EXIST); } } Listing 4.1, OSTaskCreate() OSTaskCreate() starts by checking that the task priority is valid L4.1(1). The priority of a task must be a number between 0 and OS_LOWEST_PRIO, inclusively. Next, OSTaskCreate() makes sure that a task has not already been created at the desired priority L4.1(2). With µC/OS-II, all tasks must have a unique priority. If the desired priority is free then µC/OS-II ‘reserves’ the priority by placing a non-NULL pointer in OSTCBPrioTbl[] L4.1(3). This allows OSTaskCreate() to re-enable interrupts L4.1(4) while it sets up the rest of the data structures for the task. OSTaskCreate() then calls OSTaskStkInit() L4.1(5) which is responsible for setting up the task stack. This function is processor specific and is found in OS_CPU_C.C. Refer to Chapter 8, Porting µC/OS-II for details on how to implement OSTaskStkInit(). If you already have a port of µC/OS-II for the processor you are intending to use then, you don’t need to be concerned about implementation details. OSTaskStkInit() returns the new top-of-stack (psp) which will be saved in the task’s OS_TCB. You should note that the fourth argument (i.e. opt) to OSTaskStkInit() is set to 0. This is because, unlike OSTaskCreateExt(), OSTaskCreate() does not support options and thus, there are no options to pass to OSTaskStkInit(). µC/OS-II supports processors that have stacks that grow from either high memory to low memory or from low memory to high memory. When you call OSTaskCreate(), you must know how the stack grows (see OS_CPU.H (OS_STACK_GROWTH) of the processor you are using) because you must pass the task’s top-of-stack to OSTaskCreate() which can either be the lowest memory location of the stack or the highest memory location of the stack. Once OSTaskStkInit() has completed setting up the stack, OSTaskCreate() calls OSTCBInit() L4.1(6) to obtain and initialize an OS_TCB from the pool of free OS_TCBs. The code for OSTCBInit() is shown in listing 4.2 but is found in OS_CORE.C instead of OS_TASK.C. OSTCBInit() first tries to obtain an OS_TCB from the OS_TCB pool L4.2(1). If the pool contained a free OS_TCB L4.2(2) then the OS_TCB is initialized L4.2(3). Note that once an OS_TCB is allocated, we can re-enable interrupts because, at this point, the creator of the task ‘owns’ the OS_TCB and it cannot be corrupted by another concurrent task creation. We can thus proceed to initialize some of the OS_TCB fields with interrupts enabled. INT8U OSTCBInit (INT8U prio, OS_STK *ptos, OS_STK *pbos, INT16U id, INT16U stk_size, void *pext, INT16U opt) { OS_TCB *ptcb; OS_ENTER_CRITICAL(); ptcb = OSTCBFreeList; (1) if (ptcb != (OS_TCB *)0) { (2) OSTCBFreeList = ptcb->OSTCBNext; OS_EXIT_CRITICAL(); ptcb->OSTCBStkPtr = ptos; (3) ptcb->OSTCBPrio = (INT8U)prio; ptcb->OSTCBStat = OS_STAT_RDY; ptcb->OSTCBDly = 0; #if OS_TASK_CREATE_EXT_EN ptcb->OSTCBExtPtr = pext; ptcb->OSTCBStkSize = stk_size; ptcb->OSTCBStkBottom = pbos; ptcb->OSTCBOpt = opt; ptcb->OSTCBId = id; #else pext = pext; stk_size = stk_size; pbos = pbos; opt = opt; id = id; #endif #if OS_TASK_DEL_EN ptcb->OSTCBDelReq = OS_NO_ERR; #endif ptcb->OSTCBY = prio >> 3; ptcb->OSTCBBitY = OSMapTbl[ptcb->OSTCBY]; ptcb->OSTCBX = prio & 0x07; ptcb->OSTCBBitX = OSMapTbl[ptcb->OSTCBX]; #if OS_MBOX_EN || (OS_Q_EN && (OS_MAX_QS >= 2)) || OS_SEM_EN ptcb->OSTCBEventPtr = (OS_EVENT *)0; #endif #if OS_MBOX_EN || (OS_Q_EN && (OS_MAX_QS >= 2)) ptcb->OSTCBMsg = (void *)0; #endif OS_ENTER_CRITICAL(); (4) OSTCBPrioTbl[prio] = ptcb; (5) ptcb->OSTCBNext = OSTCBList; ptcb->OSTCBPrev = (OS_TCB *)0; if (OSTCBList != (OS_TCB *)0) { OSTCBList->OSTCBPrev = ptcb; } OSTCBList = ptcb; OSRdyGrp |= ptcb->OSTCBBitY; (6) OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; OS_EXIT_CRITICAL(); return (OS_NO_ERR); (7) } else { OS_EXIT_CRITICAL(); return (OS_NO_MORE_TCB); } } Listing 4.2, OSTCBInit() We disable interrupts L4.2(4) when we need to insert the OS_TCB into the doubly linked list of tasks that have been created L4.2(5). The list starts at OSTCBList and the OS_TCB of a new task is always inserted at the beginning of the list. Finally, the task is made ready to run L4.2(6) and OSTCBInit() returns to its caller (OSTaskCreate()) with a code indicating that an OS_TCB has been allocated and initialized L4.2(7). We can now continue the discussion of OSTaskCreate() (see listing 4.1) Upon return from OSTCBInit(), OSTaskCreate() checks the return code L4.1(7) and upon success, increments the counter of the number of tasks created, OSTaskCtr L4.1(8). If OSTCBInit() failed, the priority level is relinquished by setting the entry in OSTCBPrioTbl[prio] to 0 L4.1(12). OSTaskCreate() then calls OSTaskCreateHook() L4.1(9) which is a user specified function that allows you to extend the functionality of OSTaskCreate(). For example, you could initialize and store the contents of floating-point registers, MMU registers or anything else that can be associated with a task. You would, however, typically store this additional information in memory that would be allocated by your application. OSTaskCreateHook() can be declared either in OS_CPU_C.C (if OS_CPU_HOOKS_EN is set to 1) or elsewhere. Note that interrupts are disabled when OSTaskCreate() calls OSTaskCreateHook(). Because of this, you should keep the code in this function to a minimum because it can directly impact interrupt latency. When called, OSTaskCreateHook() receives a pointer to the OS_TCB of the task being created. This means that the hook function can access all members of the OS_TCB data structure. Finally, if OSTaskCreate() was called from a task (i.e. OSRunning is set to TRUE L4.1(10)) then the scheduler is called L4.1(11) to determine whether the created task has a higher priority than its creator. Creating a higher priority task will result in a context switch to the new task. If the task was created before multitasking has started (i.e. you did not call OSStart() yet) then the scheduler is not called. 4.01 Creating a Task, OSTaskCreateExt() Creating a task using OSTaskCreateExt() offers you more flexibility but, at the expense of additional overhead. The code for OSTaskCreateExt() is shown in listing 4.3. As can be seen, OSTaskCreateExt() requires nine (9) arguments! The first four arguments (task, pdata, ptos and prio) are exactly the same as with OSTaskCreate() and also, they are located in the same order. I did that to make it easier to migrate your code to use OSTaskCreateExt(). The id establishes a unique identifier for the task being created. This argument has been added for future expansion and is otherwise unused by µC/OS-II. This identifier will allow me to extend µC/OS-II beyond its limit of 64 tasks. For now, simply set the task’s ID to the same value as the task’s priority. pbos is a pointer to the task’s bottom-of-stack and this argument is used to perform stack checking. stk_size specifies the size of the stack in number of elements. This means that if a stack entry is 4 bytes wide then, a stk_size of 1000 means that the stack will have 4000 bytes. Again, this argument is used for stack checking. pext is a pointer to a user supplied data area that can be used to extend the OS_TCB of the task. For example, you can add a name to a task (see Example #3), storage for the contents of floating-point registers during a context switch, port address to trigger an oscilloscope during a context switch and more. Finally, opt specifies options to OSTaskCreateExt() to specify whether stack checking is allowed, whether the stack will be cleared, whether floating-point operations are performed by the task, etc. uCOS_II.H contains a list of available options (OS_TASK_OPT_STK_CHK, OS_TASK_OPT_STK_CLR, and OS_TASK_OPT_SAVE_FP). Each option consists of a bit. The option is selected when the bit is set (you would simply OR the above OS_TASK_OPT_??? constants). INT8U OSTaskCreateExt (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio, INT16U id, OS_STK *pbos, INT32U stk_size, void *pext, INT16U opt) { void *psp; INT8U err; INT16U i; OS_STK *pfill; if (prio > OS_LOWEST_PRIO) { (1) return (OS_PRIO_INVALID); } OS_ENTER_CRITICAL(); if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { (2) OSTCBPrioTbl[prio] = (OS_TCB *)1; (3) OS_EXIT_CRITICAL(); (4) if (opt & OS_TASK_OPT_STK_CHK) { (5) if (opt & OS_TASK_OPT_STK_CLR) { pfill = pbos; for (i = 0; i < stk_size; i++) { #if OS_STK_GROWTH == 1 *pfill++ = (OS_STK)0; #else *pfill-- = (OS_STK)0; #endif } } } psp = (void *)OSTaskStkInit(task, pdata, ptos, opt); (6) err = OSTCBInit(prio, psp, pbos, id, stk_size, pext, opt); (7) if (err == OS_NO_ERR) { (8) OS_ENTER_CRITICAL; OSTaskCtr++; (9) OSTaskCreateHook(OSTCBPrioTbl[prio]); (10) OS_EXIT_CRITICAL(); if (OSRunning) { (11) OSSched(); (12) } } else { OSTCBPrioTbl[prio] = (OS_TCB *)0; (13) } return (err); } else { OS_EXIT_CRITICAL(); return (OS_PRIO_EXIST); } } Listing 4.3, OSTaskCreateExt() OSTaskCreateExt() starts by checking that the task priority is valid L4.3(1). The priority of a task must be a number between 0 and OS_LOWEST_PRIO, inclusively. Next, OSTaskCreateExt() makes sure that a task has not already been created at the desired priority L4.3(2). With µC/OS-II, all tasks must have a unique priority. If the desired priority is free then µC/OS-II ‘reserves’ the priority by placing a non-NULL pointer in OSTCBPrioTbl[] L4.3(3). This allows OSTaskCreateExt() to re-enable interrupts L4.3(4) while it sets up the rest of the data structures for the task. In order to perform stack checking (see section 4.03, Stack Checking) on a task, you must set the OS_TASK_OPT_STK_CHK flag in the opt argument. Also, stack checking requires that the stack contain zeros (i.e. it needs to be cleared) when the task is created. To specify that a task gets cleared when it is created, you would also set OS_TASK_OPT_STK_CLR in the opt argument. When both of these flags are set, OSTaskCreateExt() clears the stack L4.3(5). OSTaskCreateExt() then calls OSTaskStkInit() L4.3(6) which is responsible for setting up the task stack. This function is processor specific and is found in OS_CPU_C.C. Refer to Chapter 8, Porting µC/OS-II for details on how to implement OSTaskStkInit(). If you already have a port of µC/OS-II for the processor you are intending to use then, you don’t need to be concerned about implementation details. OSTaskStkInit() returns the new top-of-stack (psp) which will be saved in the task’s OS_TCB. µC/OS-II supports processors that have stacks that grow from either high memory to low memory or from low memory to high memory (see section 4.02, Task Stacks). When you call OSTaskCreateExt(), you must know how the stack grows (see OS_CPU.H of the processor you are using) because you must pass the task’s top-of-stack to OSTaskCreateExt() which can either be the lowest memory location of the stack (when OS_STK_GROWTH is 0) or the highest memory location of the stack (when OS_STK_GROWTH is 1). Once OSTaskStkInit() has completed setting up the stack, OSTaskCreateExt() calls OSTCBInit() L4.3(7) to obtain and initialize an OS_TCB from the pool of free OS_TCBs. The code for OSTCBInit() is shown and described with OSTaskCreate() (see section 4.00) Upon return from OSTCBInit(), OSTaskCreateExt() checks the return code L4.3(8) and upon success, increments the counter of the number of tasks created, OSTaskCtr L4.3(9). If OSTCBInit() failed, the priority level is relinquished by setting the entry in OSTCBPrioTbl[prio] to 0 L4.3(13). OSTaskCreateExt() then calls OSTaskCreateHook() L4.3(10) which is a user specified function that allows you to extend the functionality of OSTaskCreateExt(). OSTaskCreateHook() can be declared either in OS_CPU_C.C (if OS_CPU_HOOKS_EN is set to 1) or elsewhere (if OS_CPU_HOOKS_EN is set to 0). Note that interrupts are disabled when OSTaskCreateExt() calls OSTaskCreateHook(). Because of this, you should keep the code in this function to a minimum because it can directly impact interrupt latency. When called, OSTaskCreateHook() receives a pointer to the OS_TCB of the task being created. This means that the hook function can access all members of the OS_TCB data structure. Finally, if OSTaskCreateExt() was called from a task (i.e. OSRunning is set to TRUE L4.3(11)) then the scheduler is called L4.3(12) to determine whether the created task has a higher priority than its creator. Creating a higher priority task will result in a context switch to the new task. If the task was created before multitasking has started (i.e. you did not call OSStart() yet) then the scheduler is not called. 4.02 Task Stacks Each task MUST have its own stack space. A stack MUST be declared as being of type OS_STK and MUST consist of contiguous memory locations You can either allocate stack space ‘statically’ (i.e. compile-time) or ‘dynamically’ (run-time). A static stack declaration looks as shown below. Both of these declarations are made outside a function. static OS_STK MyTaskStack[stack_size]; Listing 4.4, Static Stack or, OS_STK MyTaskStack[stack_size]; Listing 4.5, Static Stack You can allocate stack space dynamically by using the C compiler’s malloc() function as shown in listing 4.6. However, you must be careful with fragmentation. Specifically, if you create and delete tasks then, eventually, your memory allocator may not be able to return a stack for your task(s) because the heap gets fragmented. OS_STK *pstk; pstk = (OS_STK *)malloc(stack_size); if (pstk != (OS_STK *)0) { /* Make sure malloc() had enough space */ Create the task; } Listing 4.6, Using ‘malloc()’ to allocate stack space for a task Figure 4-1 illustrates a heap containing 3 Kbytes or available memory that can be allocated with malloc() F4-1(1). For sake of discussion, you create 3 tasks (task A, B and C) each requiring 1K. We will also assume that the first 1 Kbytes is given to task A, the second to task B and the third to C F4-1(2). Your application then deletes task A and task B and, relinquishes the memory back to the heap using free() F4-1(3). Your heap now has 2 Kbytes of memory free but, it’s not contiguous. This means that you could not create another task (i.e. task D) that required 2 Kbytes. Your heap is thus fragmented. If, however, you never delete a task then using malloc() is perfectly acceptable. Figure 4-1, Fragmentation. µC/OS-II supports processors that have stacks that grow from either high memory to low memory or from low memory to high memory. When you call either OSTaskCreate() or OSTaskCreateExt(), you must know how the stack grows because you need to pass the task’s top-of-stack to the task creation function. When OS_STK_GROWTH is set to 0 in OS_CPU.H, you need to pass the lowest memory location of the stack to the task create function as shown in listing 4.7. OS_STK TaskStack[TASK_STACK_SIZE]; OSTaskCreate(task, pdata, &TaskStack[0], prio); Listing 4.7, Stack grows from LOW memory to HIGH memory When OS_STK_GROWTH is set to 1 in OS_CPU.H, you need to pass the highest memory location of the stack to the task create function as shown in listing 4.8. OS_STK TaskStack[TASK_STACK_SIZE]; OSTaskCreate(task, pdata, &TaskStack[TASK_STACK_SIZE-1], prio); Listing 4.8, Stack grows from HIGH memory to LOW memory This requirement affects code portability. If you need to port your code from a processor architecture that supports a downward growing stack to an upward growing stack then you may need to do like I did in OS_CORE.C for OSTaskIdle() and OSTaskStat(). Specifically, with the above example, your code would look as shown in listing 4.9. OS_STK TaskStack[TASK_STACK_SIZE]; #if OS_STK_GROWTH == 0 OSTaskCreate(task, pdata, &TaskStack[0], prio); #else OSTaskCreate(task, pdata, &TaskStack[TASK_STACK_SIZE-1], prio); #endif Listing 4.9, Supporting stacks which grow in either direction. The size of the stack needed by your task is application specific. When sizing the stack, however, you must account for nesting of all the functions called by your task, the number of local variables that will be allocated by all functions called by your task and, the stack requirements for all nested interrupt service routines. In addition, your stack must be able to store all CPU registers. 4.03 Stack Checking, OSTaskStkChk() It is sometimes necessary to determine how much stack space a task actually uses. This allows you to reduce the amount of RAM needed by your application code by not over allocating stack space. µC/OS-II provides a function called OSTaskStkChk() that provides you with this valuable information. Refer to figure 4-2 for the following discussion. Stack checking is performed on demand as opposed to continuously. Note that figure 4-2 assumes that stack grows from high memory to low memory (i.e. OS_STK_GROWTH is set to 1) but, the discussion applies equally well to a stack growing in the opposite direction F4-2(1). µC/OS-II determines stack growth by looking at the contents of the stack itself. To perform stack checking, µC/OS-II requires that the stack get filled with zeros when the task is created F4-2(2). Also, µC/OS-II needs to know the location of the bottom-of-stack (BOS) F4-2(3) and the size of the stack you assigned to the task F4-2(4). These two values are stored in the task’s OS_TCB when the task is created. Figure 4-2, Stack checking In order for you to use µC/OS-II’s stack checking facilities, you MUST do the following: 1. Set OS_TASK_CREATE_EXT to 1 in OS_CFG.H. 2. Create a task using OSTaskCreateExt() and give the task much more space than you think it really needs. 3. Set the opt argument in OSTaskCreateExt() to OS_TASK_OPT_STK_CHK + OS_TASK_OPT_STK_CLR. Note that if your startup code clears all RAM and you never delete tasks once they are created then, you don’t need to set the OS_TASK_OPT_STK_CLR option. This will reduce the execution time of OSTaskCreateExt(). 4. Call OSTaskStkChk() by specifying the priority of the task you desire to check. OSTaskStkChk() computes the amount of free stack space by ‘walking’ from the bottom of the stack and counting the number of zero entries on the stack until a non-zero value is found F4-2(5). Note that stack entries are checked using the data type of the stack (see OS_STK in OS_CPU.H). In other words, if a stack entry is 32-bit wide then the comparison for a zero value is done using 32 bits. The amount of stack space used F4-2(8) is obtained by subtracting the number of zero value entries F4-2(6) from the stack size you specified in OSTaskCreateExt(). OSTaskStkChk() actually places the number of bytes free and the number of bytes used in a data structure of type OS_STK_DATA (see uCOS_II.H). You should note that at any given time, the stack pointer for the task being checked may be pointing somewhere between the initial Top-Of-Stack (TOS) and the deepest stack growth F4-2(7). Also, every time you call OSTaskStkChk(), you may get a different value for the amount of free space on the stack until your task has reached its deepest growth F4-2(5). You will need to run the application long enough and under your worst case conditions to get proper numbers. Once OSTaskStkChk() provides you with the worst case stack requirement, you can go back and set the final size of your stack. You should accommodate for system expansion so make sure you allocate between 10 and 25% more. What you should get from stack checking is a ‘ballpark’ figure; you are not looking for an exact stack usage. The code for OSTaskStkChk() is shown in listing 4.10. The data structure OS_STK_DATA (see uCOS_II.H) is used to hold information about the task stack. I decided to use a data structure for two reasons. First, I consider OSTaskStkChk() to be a ‘query’ type function and I wanted to have all query functions work the same way – return data about the query in a data structure. Second, passing data in a data structure is both efficient and allows me to add additional fields in the future without changing the API (Application Programming Interface) of OSTaskStkChk(). For now, OS_STK_DATA only contains two fields: OSFree and OSUsed. As you can see, you invoke OSTaskStkChk() by specifying the priority of the task you desire to perform stack checking on. If you specify OS_PRIO_SELF L4.10(1) then it is assumed that you desire to know the stack information about the current task. Obviously, the task must exist L4.10(2). To perform stack checking, you must have created the task using OSTaskCreateExt() and you must have passed the option OS_TASK_OPT_STK_CHK L4.10(3). If all the proper conditions are met, OSTaskStkChk() computes the free stack space as described above by ‘walking’ from the bottom of stack until a non-zero stack entry is encountered L4.10(4). Finally, the information to store in OS_STK_DATA is computed L4.10(5). Note that the function computes the actual number of bytes free and the number of bytes used on the stack as opposed to the number of elements. Obviously, the actual stack size (in bytes) can be obtained by adding these two values. INT8U OSTaskStkChk (INT8U prio, OS_STK_DATA *pdata) { OS_TCB *ptcb; OS_STK *pchk; INT32U free; INT32U size; pdata->OSFree = 0; pdata->OSUsed = 0; if (prio > OS_LOWEST_PRIO && prio != OS_PRIO_SELF) { return (OS_PRIO_INVALID); } OS_ENTER_CRITICAL(); if (prio == OS_PRIO_SELF) { (1) prio = OSTCBCur->OSTCBPrio; } ptcb = OSTCBPrioTbl[prio]; if (ptcb == (OS_TCB *)0) { (2) OS_EXIT_CRITICAL(); return (OS_TASK_NOT_EXIST); } if ((ptcb->OSTCBOpt & OS_TASK_OPT_STK_CHK) == 0) { (3) OS_EXIT_CRITICAL(); return (OS_TASK_OPT_ERR); } free = 0; (4) size = ptcb->OSTCBStkSize; pchk = ptcb->OSTCBStkBottom; OS_EXIT_CRITICAL(); #if OS_STK_GROWTH == 1 while (*pchk++ == 0) { free++; } #else while (*pchk-- == 0) { free++; } #endif pdata->OSFree = free * sizeof(OS_STK); (5) pdata->OSUsed = (size - free) * sizeof(OS_STK); return (OS_NO_ERR); } Listing 4.10, Stack checking function 4.04 Deleting a Task, OSTaskDel() It is sometimes necessary to delete a task. Deleting a task means that the task will be returned to the DORMANT state (see Section 3.02, Task States) and does not mean that the code for the task will be deleted. The task code is simply no longer scheduled by µC/OS-II. You delete a task by calling OSTaskDel() and the code for this function is shown in listing 4.11. Here, we start by making sure that you are not attempting to delete the idle task because this is not allowed L4.11(1). You are, however, allowed to delete the statistic task L4.11(2). OSTaskDel() then checks to make sure you are not attempting to delete a task from within an ISR which is again not allowed L4.11(3). The caller can delete itself by specifying OS_PRIO_SELF as the argument L4.11(4). We then verify that the task to delete does in fact exist L4.11(5). This test will obviously pass if you specified OS_PRIO_SELF. I didn’t want to create a separate case for this situation because it would have increased code size and thus execution time. Once all conditions are satisfied, the OS_TCB is removed from all the possible µC/OS-II data structures. OSTaskDel() does this in two parts to reduce interrupt latency. First, if the task is in the ready list, it is removed L4.11(6). We then check to see if the task is in a list waiting for a mailbox, a queue or a semaphore and if so, the task is removed from that list L4.11(7). Next, we force the delay count to zero to make sure that the tick ISR will not ready this task once we re-enable interrupts L4.11(8). Finally, we set the task’s .OSTCBStat flag to OS_STAT_RDY. Note that we are not trying to make the task ready, we are simply preventing another task or an ISR from resuming this task (i.e. in case the other task or ISR called OSTaskResume() L4.11(9)). This situation could occur because we will be re-enabling interrupts L4.11(11) so, an ISR can make a higher priority task ready which could resume the task we are trying to delete. Instead of setting the task’s .OSTCBStat flag to OS_STAT_RDY, I could have simply cleared the OS_STAT_SUSPEND bit (which would have been clearer) but, this takes slightly more processing time. At this point, the task to delete cannot be made ready to run by another task or an ISR because, it’s been removed from the ready list, it’s not waiting for an event to occur, it’s not waiting for time to expire and cannot be resumed. In other words the task is DORMANT for all intents and purposes. Because of this, I must prevent the scheduler L4.11(10) from switching to another task because, if the current task is almost deleted then it would not be able to be rescheduled! At this point, we want to re-enable interrupts in order to reduce interrupt latency L4.11(11). We could thus service an interrupt but because we incremented OSLockNesting the ISR would return to the interrupted task. You should note that we are still not done with the deletion process because we need to unlink the OS_TCB from the TCB chain and return the OS_TCB to the free OS_TCB list. Note also that I call a ‘dummy’ function (OSDummy()) immediately after calling OS_EXIT_CRITICAL() L4.11(12). I do this because I want to make sure that the processor will execute at least one instruction with interrupts enabled. On many processors, executing an interrupt enable instruction forces the CPU to have interrupts disabled until the end of the next instruction! The Intel 80x86 and Zilog Z-80 processors actually work like this. Enabling and immediately disabling interrupts would behave just as if I didn’t enable interrupts. This would of course increase interrupt latency. Calling OSDummy() thus ensures that I will execute a call and a return instruction before re-disabling interrupts. You could certainly replace OSDummy() with a macro that executes a ‘no-operation’ instruction and thus slightly reduce the execution time of OSTaskDel(). I didn’t think it was worth the effort of creating yet another macro that would require porting. We can now continue with the deletion process of the task. After we re-disable interrupts, re-enable scheduling by decrementing the lock nesting counter L4.11(13). We then call the user definable task delete hook, OSTaskDelHook() L4.11(14). This allows user defined TCB extensions to be relinquished. Next, we decrement the task counter to indicate that there is one less task being managed by µC/OS-II. We remove the OS_TCB from the priority table by simply replacing the link to the OS_TCB of the task being deleted with a NULL pointer L4.11(15). We then remove the OS_TCB of the task to delete from the doubly-linked list of OS_TCBs that starts at OSTCBList L4.11(16). You should note that there is no need to check for the case where ptcb->OSTCBNext == 0 because we cannot delete the idle task which happens to always be at the end of the chain. The OS_TCB is returned to the free list of OS_TCBs to allow another task to be created L4.11(17). Last, but not least, the scheduler L4.11(18) is called to see if a higher priority task has been made ready to run by an ISR that would have occurred when we re-enabled interrupts at step L4.11(11). INT8U OSTaskDel (INT8U prio) { OS_TCB *ptcb; OS_EVENT *pevent; if (prio == OS_IDLE_PRIO) { (1) return (OS_TASK_DEL_IDLE); } if (prio >= OS_LOWEST_PRIO && prio != OS_PRIO_SELF) { (2) return (OS_PRIO_INVALID); } OS_ENTER_CRITICAL(); if (OSIntNesting > 0) { (3) OS_EXIT_CRITICAL(); return (OS_TASK_DEL_ISR); } if (prio == OS_PRIO_SELF) { (4) prio = OSTCBCur->OSTCBPrio; } if ((ptcb = OSTCBPrioTbl[prio]) != (OS_TCB *)0) { (5) if ((OSRdyTbl[ptcb->OSTCBY] &= ~ptcb->OSTCBBitX) == 0) { (6) OSRdyGrp &= ~ptcb->OSTCBBitY; } if ((pevent = ptcb->OSTCBEventPtr) != (OS_EVENT *)0) { (7) if ((pevent->OSEventTbl[ptcb->OSTCBY] &= ~ptcb->OSTCBBitX) == 0) { pevent->OSEventGrp &= ~ptcb->OSTCBBitY; } } ptcb->OSTCBDly = 0; (8) ptcb->OSTCBStat = OS_STAT_RDY; (9) OSLockNesting++; (10) OS_EXIT_CRITICAL(); (11) OSDummy(); (12) OS_ENTER_CRITICAL(); OSLockNesting--; (13) OSTaskDelHook(ptcb); (14) OSTaskCtr--; OSTCBPrioTbl[prio] = (OS_TCB *)0; (15) if (ptcb->OSTCBPrev == (OS_TCB *)0) { (16) ptcb->OSTCBNext->OSTCBPrev = (OS_TCB *)0; OSTCBList = ptcb->OSTCBNext; } else { ptcb->OSTCBPrev->OSTCBNext = ptcb->OSTCBNext; ptcb->OSTCBNext->OSTCBPrev = ptcb->OSTCBPrev; } ptcb->OSTCBNext = OSTCBFreeList; (17) OSTCBFreeList = ptcb; OS_EXIT_CRITICAL(); OSSched(); (18) return (OS_NO_ERR); } else { OS_EXIT_CRITICAL(); return (OS_TASK_DEL_ERR); } } Listing 4.11, Task Delete 4.05 Requesting to delete a task, OSTaskDelReq() Sometimes, a task may own resources such as a memory buffers or a semaphore. If another task attempts to delete this task then, the resource would not be freed and thus would be lost. In this type of situation, you would need to somehow signal the task that owns these resources to tell it to delete itself when it’s done with the resources. You can accomplish this with the OSTaskDelReq() function. Both the requestor and the task to be deleted need to call OSTaskDelReq(). The requestor code would look as shown in listing 4.12. The task that makes the request needs to determine what condition(s) will cause a request for the task to be deleted L4.12(1). In other words, your application will determine what condition(s) will lead to this decision. If the task needs to be deleted then you call OSTaskDelReq() by passing the priority of the task to be deleted L4.12(2). If the task to delete does not exist then OSTaskDelReq() returns OS_TASK_NOT_EXIST. You would get this if the task to delete has already been deleted or, it has not been created yet. If the return value is OS_NO_ERR then, the request has been accepted but the task has not been deleted yet. You may want to wait until the task to be deleted does in fact delete itself. You can do this by delaying the requestor for a certain amount of time like I did in L4.12(3). I decided to delay for one tick but you can certainly wait longer if needed. When the requested task eventually deletes itself, the return value in L4.12(2) would be OS_TASK_NOT_EXIST and the loop would exit L4.12(4). void RequestorTask (void *pdata) { INT8U err; pdata = pdata; for (;;) { /* Application code */ if (‘TaskToBeDeleted()’ needs to be deleted) { (1) while (OSTaskDelReq(TASK_TO_DEL_PRIO) != OS_TASK_NOT_EXIST) { (2) OSTimeDly(1); (3) } } /* Application code */ (4) } } Listing 4.12, Requesting a task to delete itself. The code for the task that needs to delete itself is shown in listing 4.13. This task basically needs to ‘poll’ a flag that resides inside the task’s OS_TCB. The value of this flag is obtained by calling OSTaskDelReq(OS_PRIO_SELF). When OSTaskDelReq() returns OS_TASK_DEL_REQ L4.13(1) to its caller, it indicates that another task has requested that this task needs to be deleted. In this case, the task to be deleted releases any resources owned L4.13(2) and calls OSTaskDel(OS_PRIO_SELF) to delete itself L4.13(3). As previously mentioned, the code for the task is not actually deleted. Instead, µC/OS-II will simply not schedule the task for execution. In other word, the task code will no longer run. You can, however, re-create the task by calling either OSTaskCreate() or OSTaskCreateExt(). void TaskToBeDeleted (void *pdata) { INT8U err; pdata = pdata; for (;;) { /* Application code */ if (OSTaskDelReq(OS_PRIO_SELF) == OS_TASK_DEL_REQ) { (1) Release any owned resources; (2) De-allocate any dynamic memory; OSTaskDel(OS_PRIO_SELF); (3) } else { /* Application code */ } } } Listing 4.13, Requesting a task to delete itself. The code for OSTaskDelReq() is shown in listing 4.14. As usual, we need to check for boundary conditions. First, we notify the caller in case he requests to delete the idle task L4.14(1). Next, we must ensure that the caller is not trying to request to delete an invalid priority L4.14(2). If the caller is the task to be deleted, the flag stored in the OS_TCB will be returned L4.14(3). If you specified a task with a priority other than OS_PRIO_SELF then, if the task exist L4.14(4), we set the internal flag for that task L4.14(5). If the task does not exist, we return OS_TASK_NOT_EXIST to indicate that the task must have deleted itself L4.14(6). INT8U OSTaskDelReq (INT8U prio) { BOOLEAN stat; INT8U err; OS_TCB *ptcb; if (prio == OS_IDLE_PRIO) { (1) return (OS_TASK_DEL_IDLE); } if (prio >= OS_LOWEST_PRIO && prio != OS_PRIO_SELF) { (2) return (OS_PRIO_INVALID); } if (prio == OS_PRIO_SELF) { (3) OS_ENTER_CRITICAL(); stat = OSTCBCur->OSTCBDelReq; OS_EXIT_CRITICAL(); return (stat); } else { OS_ENTER_CRITICAL(); if ((ptcb = OSTCBPrioTbl[prio]) != (OS_TCB *)0) { (4) ptcb->OSTCBDelReq = OS_TASK_DEL_REQ; (5) err = OS_NO_ERR; } else { err = OS_TASK_NOT_EXIST; (6) } OS_EXIT_CRITICAL(); return (err); } } Listing 4.14, OSTaskDelReq(). 4.06 Changing a Task’s Priority, OSTaskChangePrio() When you create a task, you assign the task a priority. At run-time, you can change the priority of any task by calling OSTaskChangePrio(). In other words, µC/OS-II allows you to change priorities dynamically. The code for OSTaskChangePrio() is shown in listing 4.15. You cannot change the priority of the idle task L4.15(1). You can either change the priority of the calling task or another task. To change the priority of the calling task you can must either specify the ‘old’ priority of that task or specify OS_PRIO_SELF and OSTaskChangePrio() will determine what the priority of the calling task is for you. You must also specify the ‘new’ (i.e. desired) priority. Because µC/OS-II cannot have multiple tasks running at the same priority, we need to check that the desired priority is available L4.15(2). If the desired priority is available, µC/OS-II reserves the priority by loading ‘something’ in the OSTCBPrioTbl[] thus reserving that entry L4.15(3). This allows us to re-enable interrupts and know that no other task can either create a task at the desired priority or have another task call OSTaskChangePrio() by specifying the same ‘new’ priority. This is done so that we can pre-compute some values that will be stored in the task’s OS_TCB L4.15(4). These values are used to put or remove the task in or from the ready list (see section 3.04, Ready List). We then check to see if the current task is attempting to change its priority L4.15(5). Next, we need to see if the task for which we are trying to change the priority exist L4.15(6). Obviously, if it’s the current task then this test will succeed. However, if we are trying to change the priority of a task that doesn’t exist then, we must relinquish the ‘reserved’ priority back to the priority table, OSTCBPrioTbl[] L4.15(17), and return an error code to the caller. We now remove the pointer to the OS_TCB of the task from the priority table by inserting a NULL pointer L4.15(7). This will make the ‘old’ priority available for reuse. Then, we check to see if the task for which we are changing the priority is ready-to-run L4.15(8). If it is, it must be removed from the ready list at the current priority L4.15(9) and inserted back in the ready list at the new priority L4.15(10). Note here that we use our pre-computed values L4.15(4) to insert the task in the ready list. If the task is ready then, it could be waiting on a semaphore, a mailbox or a queue. We know that the task is waiting for one of these events if the OSTCBEventPtr is non-NULL L4.15(11). If the task is waiting for an event, we must remove the task from the wait list (at the old priority) of the event control block (see section 6.00, Event Control Block) and insert the task back in the wait list but this time at the new priority L4.15(12). The task could be waiting for time to expire (see chapter 5, Time Management) or the task could be suspended (see section 4.07, Suspending a Task, OSTaskSuspend()). In these cases, items L4.15(8) through L4.15(12) would be skipped. Next, we store a pointer to the task’s OS_TCB in the priority table, OSTCBPrioTbl[] L4.15(13). The new priority is saved in the OS_TCB L4.15(14) and the pre-computed values are also saved in the OS_TCB L4.15(15). After we exit the critical section, the scheduler is called in case the new priority is higher than the old priority or the priority of the calling task L4.15(16). INT8U OSTaskChangePrio (INT8U oldprio, INT8U newprio) { OS_TCB *ptcb; OS_EVENT *pevent; INT8U x; INT8U y; INT8U bitx; INT8U bity; if ((oldprio >= OS_LOWEST_PRIO && oldprio != OS_PRIO_SELF) || (1) newprio >= OS_LOWEST_PRIO) { return (OS_PRIO_INVALID); } OS_ENTER_CRITICAL(); if (OSTCBPrioTbl[newprio] != (OS_TCB *)0) { (2) OS_EXIT_CRITICAL(); return (OS_PRIO_EXIST); } else { OSTCBPrioTbl[newprio] = (OS_TCB *)1; (3) OS_EXIT_CRITICAL(); y = newprio >> 3; (4) bity = OSMapTbl[y]; x = newprio & 0x07; bitx = OSMapTbl[x]; OS_ENTER_CRITICAL(); if (oldprio == OS_PRIO_SELF) { (5) oldprio = OSTCBCur->OSTCBPrio; } if ((ptcb = OSTCBPrioTbl[oldprio]) != (OS_TCB *)0) { (6) OSTCBPrioTbl[oldprio] = (OS_TCB *)0; (7) if (OSRdyTbl[ptcb->OSTCBY] & ptcb->OSTCBBitX) { (8) if ((OSRdyTbl[ptcb->OSTCBY] &= ~ptcb->OSTCBBitX) == 0) { (9) OSRdyGrp &= ~ptcb->OSTCBBitY; } OSRdyGrp |= bity; (10) OSRdyTbl[y] |= bitx; } else { if ((pevent = ptcb->OSTCBEventPtr) != (OS_EVENT *)0) { (11) if ((pevent->OSEventTbl[ptcb->OSTCBY] &= ~ptcb->OSTCBBitX) == 0) { pevent->OSEventGrp &= ~ptcb->OSTCBBitY; } pevent->OSEventGrp |= bity; (12) pevent->OSEventTbl[y] |= bitx; } } OSTCBPrioTbl[newprio] = ptcb; (13) ptcb->OSTCBPrio = newprio; (14) ptcb->OSTCBY = y; (15) ptcb->OSTCBX = x; ptcb->OSTCBBitY = bity; ptcb->OSTCBBitX = bitx; OS_EXIT_CRITICAL(); OSSched(); (16) return (OS_NO_ERR); } else { OSTCBPrioTbl[newprio] = (OS_TCB *)0; (17) OS_EXIT_CRITICAL(); return (OS_PRIO_ERR); } } } Listing 4.15, OSTaskChangePrio(). 4.07 Suspending a Task, OSTaskSuspend() It is sometimes useful to explicitly suspend the execution of a task. This is accomplished with the OSTaskSuspend() function call. A suspended task can only be resumed by calling the OSTaskResume() function call. Task suspension is additive. This means that if the task being suspended is also waiting for time to expire then the suspension needs to be removed and the time needs to expire in order for the task to be ready-to-run. A task can either suspend itself or another task. The code for OSTaskSuspend() is shown in listing 4.16. As usual, we check boundary conditions. First, we must ensure that your application is not attempting to suspend the idle task L4.16(1). Next, you must specify a valid priority L4.16(2). Remember than the highest valid priority number (i.e. lowest priority) is OS_LOWEST_PRIO. Note that you can suspend the statistic task. You may have noticed that the first test L4.16(1) is replicated in L4.16(2). I did this to be backward compatible with µC/OS. The first test could be removed to save a little bit of processing time but, this is really insignificant so I decided to leave it. Next, we check to see if you specified to suspend the calling task L4.16(3) by specifying OS_PRIO_SELF. You could also decided to suspend the calling task by specifying its priority L4.16(4). In both of these cases, the scheduler will need to be called. This is why I created the local variable self which will be examined at the appropriate time. If we are not suspending the calling task then we will not need to run the scheduler because the calling task is suspending a lower priority task. We then check to see that the task to suspend exist L4.16(5). If the task to suspend exist, we remove it from the ready list L4.16(6). Note that the task to suspend may not be in the ready list because it’s waiting for an event or for time to expire. In this case, the corresponding bit for the task to suspend in OSRdyTbl[] would already be cleared (i.e. 0). Clearing it again is faster than checking to see if it’s clear and then clearing it if it’s not. We then set the OS_STAT_SUSPEND flag in the task’s OS_TCB to indicate that the task is now suspended L4.16(7). Finally, we call the scheduler only if the task being suspended is the calling task L4.16(8). INT8U OSTaskSuspend (INT8U prio) { BOOLEAN self; OS_TCB *ptcb; if (prio == OS_IDLE_PRIO) { (1) return (OS_TASK_SUSPEND_IDLE); } if (prio >= OS_LOWEST_PRIO && prio != OS_PRIO_SELF) { (2) return (OS_PRIO_INVALID); } OS_ENTER_CRITICAL(); if (prio == OS_PRIO_SELF) { (3) prio = OSTCBCur->OSTCBPrio; self = TRUE; } else if (prio == OSTCBCur->OSTCBPrio) { (4) self = TRUE; } else { self = FALSE; } if ((ptcb = OSTCBPrioTbl[prio]) == (OS_TCB *)0) { (5) OS_EXIT_CRITICAL(); return (OS_TASK_SUSPEND_PRIO); } else { if ((OSRdyTbl[ptcb->OSTCBY] &= ~ptcb->OSTCBBitX) == 0) { (6) OSRdyGrp &= ~ptcb->OSTCBBitY; } ptcb->OSTCBStat |= OS_STAT_SUSPEND; (7) OS_EXIT_CRITICAL(); if (self == TRUE) { (8) OSSched(); } return (OS_NO_ERR); } } Listing 4.16, OSTaskSuspend(). 4.08 Resuming a Task, OSTaskResume() As mentioned in the previous section, a suspended task can only be resumed by calling OSTaskResume(). The code for OSTaskResume() is shown in listing 4.17. Because we cannot suspend the idle task we must verify that your application is not attempting to resume this task L4.17(1). You will note that this test also ensures that you are not trying to resume OS_PRIO_SELF (OS_PRIO_SELF is #defined to 0xFF which is always greater than OS_LOWEST_PRIO) because this wouldn’t make sense. The task to resume must exist because we will be manipulating its OS_TCB L4.17(2) and, it must also have been suspended L4.17(3). We remove the suspension by clearing the OS_STAT_SUSPEND bit in the OSTCBStat field L4.17(4). For the task to be ready-to-run, the OSTCBDly field must be zero L4.17(5) because there are no flags in OSTCBStat to indicate that a task is waiting for time to expire. The task is made ready-to-run only when both conditions are satisfied L4.16(6). Finally, the scheduler is called to see if the resumed task has a higher priority than the calling task L4.17(7). INT8U OSTaskResume (INT8U prio) { OS_TCB *ptcb; if (prio >= OS_LOWEST_PRIO) { (1) return (OS_PRIO_INVALID); } OS_ENTER_CRITICAL(); if ((ptcb = OSTCBPrioTbl[prio]) == (OS_TCB *)0) { (2) OS_EXIT_CRITICAL(); return (OS_TASK_RESUME_PRIO); } else { if (ptcb->OSTCBStat & OS_STAT_SUSPEND) { (3) if (((ptcb->OSTCBStat &= ~OS_STAT_SUSPEND) == OS_STAT_RDY) && (4) (ptcb->OSTCBDly == 0)) { (5) OSRdyGrp |= ptcb->OSTCBBitY; (6) OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; OS_EXIT_CRITICAL(); OSSched(); (7) } else { OS_EXIT_CRITICAL(); } return (OS_NO_ERR); } else { OS_EXIT_CRITICAL(); return (OS_TASK_NOT_SUSPENDED); } } } 4.09 Getting Information about a Task, OSTaskQuery() Your application can obtain information about itself or other application tasks by calling OSTaskQuery(). In fact, OSTaskQuery() obtains a copy of the contents of the desired task’s OS_TCB. The fields that are available to you in the OS_TCB depend on the configuration of your application (see OS_CFG.H). Indeed, because µC/OS-II is scalable, it only includes the features that your application requires. To call OSTaskQuery(), your application must allocate storage for an OS_TCB as shown in listing 4.18. This OS_TCB is in a totally different data space as the OS_TCBs allocated by µC/OS-II. After calling OSTaskQuery(), this OS_TCB will contain a snapshot of the OS_TCB for the desired task. You need to be careful with the links to other OS_TCBs (i.e. OSTCBNext and OSTCBPrev); you don’t want to change what these links are pointing to! In general, you would only use this function to see what a task is doing – a great tool for debugging. OS_TCB MyTaskData; void MyTask (void *pdata) { pdata = pdata; for (;;) { /* User code */ err = OSTaskQuery(10, &MyTaskData); /* Examine error code .. */ /* User code */ } } Listing 4.18, Obtaining information about a task. The code for OSTaskQuery() is shown in listing 4.19. You should note that I now allow you to examine ALL the tasks, including the idle task L4.19(1). You need to be especially careful NOT to change what OSTCBNext and OSTCBPrev are pointing to. As usual, we check to see if you want information about the current task L4.19(2) and also, the task must have been created in order to obtain information about it L4.19(3). All fields are copied using the assignment shown instead of field by field L4.19(4). This is much faster because the compiler will most likely generate memory copy instructions. INT8U OSTaskQuery (INT8U prio, OS_TCB *pdata) { OS_TCB *ptcb; if (prio > OS_LOWEST_PRIO && prio != OS_PRIO_SELF) { (1) return (OS_PRIO_INVALID); } OS_ENTER_CRITICAL(); if (prio == OS_PRIO_SELF) { (2) prio = OSTCBCur->OSTCBPrio; } if ((ptcb = OSTCBPrioTbl[prio]) == (OS_TCB *)0) { (3) OS_EXIT_CRITICAL(); return (OS_PRIO_ERR); } *pdata = *ptcb; (4) OS_EXIT_CRITICAL(); return (OS_NO_ERR); } Listing 4.19, OSTaskQuery()
本文档为【TASK】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_608072
暂无简介~
格式:doc
大小:138KB
软件:Word
页数:21
分类:互联网
上传时间:2018-09-10
浏览量:32