实验三 消息队列
一、实验目的
理解消息队列的定义及其功能;
掌握消息队列相关数据结构定义及操作规则;
掌握初始化消息队列、建立消息队列、发消息到消息队列、等待消息队列中的消息的流程;
实现OS_QInit( )、OSQCreate( )、OSQPost( )、OSQPend( )。
二、实验内容
假设有任务TaskQSen和TaskQRec, TaskQSen在时间片1创建一个消息队列,然后每秒向消息队列中发邮件,其余时间延时。TaskQRec每2秒从消息队列中取邮件,然后延时。
三、对相关数据结构、函数(函数名、返回值,参数)及关键代码进行详细说明
(一)、主函数
int main(int argc, char **argv) { int p[2],Experiment ; p[0]=0; p[1]=100; VCInit(); printf("0.没有用户任务 "); printf("1.第一个例子,一个用户任务 "); printf("2.第二个例子,两个任务共享CPU交替运行 "); printf("3.第三个例子,任务的挂起和恢复 "); printf("4.第四个例子,信号量管理 "); printf("5.第五个例子,互斥信号量管理 "); printf("6.第六个例子,事件标志组 "); printf("7.第七个例子,消息邮箱 "); printf("8.第八个例子,消息队列 "); printf("9.第九个例子,内存管理 "); printf("请输入序号选择例子: "); scanf("%d",&Experiment); if ((Experiment<0)||(Experiment>10)) { printf("无效的输入!"); return(1); } OSInit(); OSTaskCreate(TaskStart, 0, &TaskStk[1][TASK_STK_SIZE-1], TaskStart_Prio); switch(Experiment) { case 1://一个任务运行 OSTaskCreate(FirstTask, 0, &TaskStk[5][TASK_STK_SIZE-1], 5); break; case 2://两个任务共享CPU OSTaskCreate(E2_task1, 0, &TaskStk[5][TASK_STK_SIZE-1], 5); OSTaskCreate(E2_task2, 0, &TaskStk[6][TASK_STK_SIZE-1], 6); break; case 3://任务的挂起和恢复 OSTaskCreate(E3_task0, 0, &TaskStk[5][TASK_STK_SIZE-1], 5); OSTaskCreate(E3_task1, 0, &TaskStk[6][TASK_STK_SIZE-1], 6); OSTaskCreate(E3_task2, 0, &TaskStk[7][TASK_STK_SIZE-1], 7); break; case 4://信号量管理例程 OSTaskCreate(UserTaskSemA, 0, &TaskStk[5][TASK_STK_SIZE-1], 7); OSTaskCreate(UserTaskSemB, 0, &TaskStk[6][TASK_STK_SIZE-1], 6); OSTaskCreate(UserTaskSemC, 0, &TaskStk[7][TASK_STK_SIZE-1], 5); break; case 5://互斥信号量管理例程 OSTaskCreate(TaskMutex1, 0, &TaskStk[6][TASK_STK_SIZE-1], 6); OSTaskCreate(TaskMutex2, 0, &TaskStk[7][TASK_STK_SIZE-1], 50); OSTaskCreate(TaskPrint, 0, &TaskStk[8][TASK_STK_SIZE-1], 30); break; case 6://时间标志组管理例程 OSTaskCreate(TaskDataProcess, 0, &TaskStk[5][TASK_STK_SIZE-1],5); OSTaskCreate(TaskIO1, 0, &TaskStk[6][TASK_STK_SIZE-1], 6); OSTaskCreate(TaskIO2, 0, &TaskStk[7][TASK_STK_SIZE-1], 7); OSTaskCreate(TaskIO3, 0, &TaskStk[8][TASK_STK_SIZE-1], 8); OSTaskCreate(TaskIO4, 0, &TaskStk[9][TASK_STK_SIZE-1], 9); break; case 7://消息邮箱 OSTaskCreate(TaskMessageSen, 0, &TaskStk[6][TASK_STK_SIZE-1], 6); OSTaskCreate(TaskMessageRec, 0, &TaskStk[7][TASK_STK_SIZE-1], 7); break; case 8://消息队列 OSTaskCreate(TaskQSen, 0, &TaskStk[7][TASK_STK_SIZE-1], 5); OSTaskCreate(TaskQRec, 0, &TaskStk[8][TASK_STK_SIZE-1], 6); OSTaskCreate(TaskQRec, 0, &TaskStk[9][TASK_STK_SIZE-1], 7); break; case 9://内存管理 OSTaskCreate(TaskM, 0, &TaskStk[8][TASK_STK_SIZE-1], 6); break; default: ; } OSStart(); return(0); }
这段代码是一个简化的实时操作系统的例程选择器,它允许用户选择不同的操作系统任务演示。代码的主要部分包括初始化、用户输入接收以及根据输入启动对应的任务。以下是对关键数据结构、函数和代码片段的详细说明:
数据结构int p[2]: 一个整数数组,用来存放相关数据,但在此代码片段中未见其它用途。TaskStk[][]: 一个二维数组,看起来像是为不同任务预设的栈空间。实时操作系统中每个任务通常有自己的栈。TASK_STK_SIZE: 预定义的常量,指定了任务栈的大小。
函数main函数类型: int
参数:int argc: 命令行参数的数量char **argv: 命令行参数的字符串数组指针。
返回值:程序退出码。0表示正常退出,非 0 值表示有错误发生。
VCInit:用于初始化线程的上下文。
OSInit:初始化操作系统环境的函数。它可能会设置一些关键的数据结构、定时器、任务调度器等。
OSTaskCreate:用来创建一个新的任务。
参数可能包括:任务函数指针:指向任务执行体的函数指针。传递给任务的可选参数。任务的栈顶指针:指指向任务栈顶的指针。任务的优先级。
OSStart:用来启动操作系统调度,使得创建的任务开始按照优先级和调度策略执行。
关键代码片段:
1.接收用户输入
scanf("%d", &Experiment); if ((Experiment < 0) || (Experiment > 10)) { printf("无效的输入!"); return (1); }
此段代码要求用户输入一个数字来选择示例任务,并检查输入是否有效。
2.根据输入创建任务
switch(Experiment) { case 1: // 一个任务运行 OSTaskCreate(...); break; // …后续case语句类似地创建不同的任务集合… }
根据用户输入(通过Experiment变量),使用switch结构来选择创建和启动不同的任务例程。
(二)、VCInit函数
void VCInit(void) { HANDLE cp,ct; Context.ContextFlags = CONTEXT_CONTROL; cp = GetCurrentProcess(); //得到当前进程句柄 ct = GetCurrentThread(); //得到当前线程伪句柄 DuplicateHandle(cp, ct, cp, &mainhandle, 0, TRUE, 2); //伪句柄转换,得到线程真句柄 }
这段代码提供VCInit函数的实现,下面对该函数进行详细分析:
HANDLE cp, ct;: HANDLE是一个Windows API中用来指代资源(如进程、线程、文件等)的类型。在这里,`cp` 用来存储当前进程的句柄,而 ct 用来存储当前线程的句柄。
Context.ContextFlags = CONTEXT_CONTROL: CONTEXT_CONTROL通常表示我们只对线程的控制寄存器感兴趣,这可能包括程序计数器和栈指针。Context是一个结构体,可能在这个代码片段之外定义,用来存储线程上下文信息。
GetCurrentProcess();: 这个函数调用返回一个当前进程的句柄。
GetCurrentThread();: 返回一个当前线程的伪句柄,所谓的“伪句柄”是一个特殊值,它只对拥有它的进程有效,并且在该进程的上下文中可以代表它引用的对象(这里是指线程)。
DuplicateHandle(cp, ct, cp, &mainhandle, 0, TRUE, 2);: 该函数用来复制对象的句柄,使得句柄有更多的权限或者在新的进程中有效。参数列表意义如下:cp: 源句柄所在的进程,这里是当前进程。ct: 需要被复制的句柄,这里是当前线程的伪句柄。cp: 目标句柄所在的进程,依然是当前进程。&mainhandle: 一个指向 `HANDLE` 类型的指针,复制成功后这里将保存新句柄。0: 新句柄的所需访问权限,这里设为0表示默认。TRUE: 是否继承标志,在新进程中是否可以继承该句柄。2: 选项标志,`DUPLICATE_SAME_ACCESS` 的值通常定义为2,意味着复制的句柄将和原句柄有相同的访问权限。
(三)、TaskStart函数
void TaskStart(void * pParam) { char err; OS_EVENT *sem1; /*模拟设置定时器中断。开启一个定时器线程,每秒中断100次,中断服务程序OSTickISRuser*/ timeSetEvent(1000/OS_TICKS_PER_SEC, 0, OSTickISRuser, 0, TIME_PERIODIC); OSStatInit(); /*统计任务初始化*/ sem1 = OSSemCreate(0); OSSemPend(sem1, 0, &err); //等待事件发生,被阻塞; }
该函数详细说明如下:
void * pParam: 是一个指向任意数据类型的指针,通常用于传递用户定义数据给任务。在此函数实现中并没有使用这个参数。char err: 用于接收错误代码,它在 OSSemPend函数调用时作为错误状态的输出。
OS_EVENT *sem1: 变量 sem1是指向 OS_EVENT` 类型的指针,OS_EVENT是一种用来处理各种事件的结构体,在这里特指一个信号量。
函数内部的重要操作说明如下:timeSetEvent(1000/OS_TICKS_PER_SEC, 0, OSTickISRuser, 0, TIME_PERIODIC): 这行代码调用了一个函数 timeSetEvent,目的是设置一个定时器,以便周期性地调用中断服务例OSTickISRuser。这个定时器每 1000/OS_TICKS_PER_SEC`毫秒,也就是 OS_TICKS_PER_SEC 次/秒触发一次。这里 TIME_PERIODIC表示定时器是周期性的,也就是说,它会按照指定的间隔无限期地重复触发。OS_TICKS_PER_SEC是一个常量,它说明了操作系统的时钟节拍频率,即每秒钟时钟中断的次数。OSTickISRuser可能是用户定义的中断服务例程,它将在每个时钟节拍被调用。OSStatInit(): 这是一个初始化操作系统统计任务的函数调用。它可能会允许系统收集关于任务执行的统计数据,例如CPU占用和任务切换频率等。
sem1 = OSSemCreate(0): 此调用创建一个新的信号量并将其计数器初始化为0,信号量是用于线程或任务同步的一种手段。OSSemPend(sem1, 0, &err): 这行代码将任务置于等待状态,直到信号量 sem1 变得可用,也就是它的计数器超过0。第二个参数表示任务在获取不到信号量时无限期等待。如果等待过程中出现错误,err将存储相应的错误代码。
四、实验结果与分析
五、问题与解决方法、总结。
通过本次实验,我对消息队列的概念有了更直观的理解。实验中我创建了两个任务,TaskQSen负责定时发送消息至队列,TaskQRec则从队列中接收消息。这个过程深刻展示了消息队列在任务间通信中的异步性和缓冲能力。我体会到了,在嵌入式实时系统中,消息队列是一种重要的数据交换手段,它可以有效地解耦任务的生产者和消费者。对消息队列进行编程操作的过程中,我学习到了如何使用 OSQCreate和 OSQPend等API,不仅理解了函数背后的逻辑,也对应用编程接口的使用有了更加深入的了解。