嵌入式操作系统实验:实验三 消息队列

实验三 消息队列

一、实验目的

理解消息队列的定义及其功能;

掌握消息队列相关数据结构定义及操作规则;

掌握初始化消息队列、建立消息队列、发消息到消息队列、等待消息队列中的消息的流程;

实现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,不仅理解了函数背后的逻辑,也对应用编程接口的使用有了更加深入的了解。