满分答卷:北邮大一计导大作业--冯诺依曼式CPU简易模拟器

本文详述了一位学生在CSDN上首次发表的文章,分享了如何实现冯诺依曼式计算机CPU简易模拟器,包括文件读入、数据处理、指令执行和内存模拟。文章介绍了使用结构体管理寄存器,通过二维数组模拟内存,并讲解了线程创建和线程互斥在多核版本中的应用,以及对运行效率的优化。作者还探讨了在处理多线程同步时遇到的问题及解决方案,并提供了代码示例。
摘要由CSDN通过智能技术生成
本人第一次在CSDN上发表文章哈,请多多支持。

这个大作业其实涉及到大二的计组课程(寄存器相关知识),有助于提前理解。感谢本校黄海老师出的题目(坏耶)。
请添加图片描述
话不多说,上链接,是放在github上的。
链接有所有的配套资料,以及我所有的 code,单线程+线程都(确保能运行的嗷,而且有注释,多个思路)。
链接:BUPTtask_2020
下载位置预览
具体怎么用github,这里也放个链接:github使用方法

可以借鉴,但不要抄袭。

此外,一定要保证看完课题要求、指导PPT、指令集和需处理文件,并适当思考,再看下面的文章,要不然就是天方夜谭了。下文又臭又长,还请耐心看完

题目概述

首先是课题叫模拟冯诺依曼式计算机CPU简易模拟器,先放张图:请添加图片描述
这个就是冯诺依曼式CPU简易构架,课题的目的就是要简易模拟其中的运作方式。

我们要实现读入文件数据后用临时变量处理这些数据,并且模拟内存的运作方式,将处理的数据暂存,并根据特定指令运算和输入输出。

其实无非就是:
打开文件流处理文件
声明定义变量来分析数据(如指令寄存器的数,立即数等);
用学过的数据类型来模拟我们需要的内存(根据处理的数据特征选择合适的数据类型),并将数据暂存,模拟内存的功能;
用 switch 或者 if/else 写好主函数的指令选择
写好指令函数(子函数)。

所以思路很明确,写好文件处理,写好指令选择,写好内存模拟就行了。
多线程无非是多一个创建线程;单核是一个源码一个跑,多核是一个源码两个跑。

所以在有基本了解的基础上,我觉得最重要的是把难点讲好。
写不出来?不存在的。无非就是卡了一下,其实不会有太大问题的。相信自己!

本人遇到的所有困难请看上面链接中的 ”实验总结“,下面篇幅我详细讲几个重要的难点。

0. 参数的设置

老规矩嗷,程序员得从 0 开始数 O(∩_∩)O (其实是后面发现自己得把这个写 1 前面)。

文件是 32 个 0、1 数字为一行,不超过32行的dic格式文式。上图,先了解一下每行的数据构成: 二进制行

  1. com(command)”:每行前8个数字,我使用二进制转十进制来存,便于书写(因为前四个数字默认为0,所以单线程范围是0-12,多线程是0-15)。这8个数字就是来控制进行操作种类(加减乘除或者存储打印)。
  2. front”:前一个寄存器,我使用二进制转十进制来存,一般可以理解为信息流流入的寄存器,操作终点。
  3. behind”:后一个寄存器,我使用二进制转十进制来存,一般可以理解为信息流流出的寄存器,操作起点。
  4. im(immediateValue)”:立即数,为补码形式,理解为行可操作数据源。

可知,这些可以直接在主函数里声明定义,生命周期即为 main() 的周期

再来了解一下寄存器参数。当然,先来看一下实验要求:
任务概述
第一个是系统寄存器,是指每次行指令执行时程序的数据暂存位点,纪录了程序当前执行的数据。

  1. 当前文件指针前面的字节数 “ip” (注:默认32位系统,8位1字节,所以1行4字节),也称“程序计数器”。
  2. 当前行的前16位数字转十进制数 “ir”(因为后16位是立即数,前16位代表了在哪里操作,做什么操作),也称“指令寄存器”。
  3. 当前的逻辑运算结果 “flag” ,也称“标志寄存器”。

第二个是 数据寄存器 1 2 3 4地址寄存器 5 6 7 8(我用一个int指针来表示了,用malloc申请空间)。
因为单线程要求不能使用全局变量,所以寄存器这个东西该怎么设置呢?总不能所有东西写在main函数里面吧。而且至少要写很多函数来实现功能,传参也不能一个一个传。。。哈哈,相信很多人会和我一样选择结构体

typedef struct mtc
	{
		int ip ;  // 程序计算器
		int flag ;  // 标志寄存器
		int ir ;  // 进制寄存器
		int* ax ;  // 数据寄存器 1 , 2 , 3 , 4 和 地址寄存器 5 , 6 , 7 , 8
	}  MTC ;   // 夹带私货

这会使得传参进入功能函数只需要一个结构体地址,方便的很。

最后是内存
分别是:代码段内存 “codeSegment”数据段内存“dataSegment”
这两个东西都是你需要在内存中模拟的,观察output末尾,很显然,我们想到的是二维数组来存储。

"codeSegment"是代码段内存,指的是你的文件每个行指令32位转十进制的数,存在一个16行,8列,初始值为0的二维数组中。如下图,因为dict.dic只有16行指令,所以存了前2行的codeSegment ( 最后一行全是0所以转十进制也是0 )
codeSegment

"dataSegment"是数据段内存,我认为是最难理解的。
这里要讲一个前提:地址从16384开始,两个字节一段,上图 “5” 所在空间对应地址为16384,“6” 所在空间对应16386,以此类推。
请添加图片描述
观察指令集可知,是通过地址寄存器 5 6 7 8 中的数(地址寄存器存的是地址,这个是有输入前提的,不会有什么奇怪的输入)来找到对应的你模拟的内存空间(就是二维数组啦,初始值为0,16行16列),并修改相应的值。
比如下面对应的指令:
00000001 0101 0001 0000000000000000
将寄存器1的内容存入寄存器5所指向内存单元

此时地址寄存器存的数据是16384,所以如上图所示, 第一个数字变成了5。

1. 文件读入

因为是文件流读取数据,那么就必须用到文件流的相关知识(函数),而其中最重要的就是关于文件指针的操作。
但是不仅仅如此,因为牵扯到文件指针的跳转,以及数据的处理,其实还是蛮复杂的。
dict.dic_二进制文件预览
首先,因为数据处理的大循环是以行为单位,所以面临两个选择:

储存模式的选择
是一次性读完存储于内存数组里呢,还是直接操作文件呢?
对于储存模式,我觉得文件指针的移动显然不如二维数组改变下标的查找来的直接(例如往前跳转28个字节,即7行,参照上面的二进制图片图片)。所以我使用存储在本地内存来实现跳转。

我的 txt存文件数据的二维数组
下面代码中,我在之前的函数中已经算出来了文件的行数line,除非用链表,否则才疏学浅的我们开辟空间 malloc 就必须要知道行数。

int** generateTxtSpace ( FILE* fPtr , int line )
{
	/* 申请  txt 列空间 */
	int** txt = ( int** ) malloc ( sizeof ( int* ) * ( line ) ) ;
	
	/* 申请 txt 行空间 */
	int i , j ;
	char ch ;
	for ( i = 0 ; i < line ; i++ )
	txt[i] = ( int* ) malloc ( sizeof ( int ) * 32 ) ;
	
	/* 存入 txt 数据 */
	for ( i = 0 ; i < line ; i++ ) {
		for ( j = 0 ; j < 32 ; j++ ) {
			ch = fgetc ( fPtr ) ;
			txt[i][j] = ch - 48 ;
		}
	
		/* 判断结束条件 */
		if ( i == line-1 )
			break ;
		else
			while ( ch != 10 )
				ch = fgetc ( fPtr ) ; 
	}
		
	return txt ;
}

这其实是一个投机取巧的方法,因为实验默认了一个前提:文件不会超过32行。当项目大了之后,处理的数据可不是这么点,这要是都存在内存中,估计不到几百万行代码你的系统就要报错了(没内存了)。所以个人建议还是使用文件指针的操作方式,可以锻炼一下自己的文件操作能力。

输入模式的选择:如果要将文件数据读取到内存(栈)的话,是一行一行读取( fscanf ( fPtr , “%s” , address ) )呢,还是逐个读取( ch = fgetc ( fPtr ) )呢?

我天真的以为逐个操作字符,一遍处理完所有数据是快捷的(似乎是的)。但是我忽略了调用函数的时耗。如果看过源码的话,其实应该知道调用函数是极其消耗时间的。所以我虽然是逐个操作,但还是希望你们能一行一行操作再来从头来用二维数组遍历运算。
而针对直接操作文件指针的,那这个其实就没得选了,只能逐个操作了。因为文件是以的形式传入,你看到的行只不过是 “\n” 在你软件上的体现,实际上它和其他字符没有区别。

2. 数据处理

既然是二进制文件,那么就不得不说琴九韶算法了:琴九韶。计算codeSegment如下:
我是以流一行的形式来存的,直接干128一维数组。因为我觉得这个更符合实际。

int* generateCodeSpace ( int** txt , int line )
{
	
	/* 申请代码空间 */
	int* code = ( int* ) malloc ( sizeof ( int ) * 128 ) ;
	int i , j ;
	for ( i = 0 ; i < 128 ; i++ ) // 初始化
			code[i] = 0 ;
	
	/* 存入 code 数据 */
	for ( i = 0 ; i < line ; i++ )
		for ( j = 0 ; j < 32 ; j++ )
			code[ i ] = 2*code[i] + txt[i][j] ;  // 因式分解提取2套用循环,琴九韶算法。
			
	return code ;
}

3. 跳转指令

我觉得这个就是用二维数组存文件的好处了。直接通过下标查找,简单粗暴。
比如下面的指令(位于 dict.dic 的13行):
00001010 0000 0000 1111111111100100
无条件转移至 -28 个字节后执行

就是向前跳28字节:执行完该行,现在文件指针位于行末尾,比如现在在13行末尾,操作后你要跳到7行首。效果就是13行完了下一步执行第7行指令。

MTC *cur 就是上面说的寄存器的结构体;data 就是 dataSegment 的首地址,用来操作 dataSegment 的,当然在跳转指令这里没用上哈,其他指令用上了;com 就是文件行指令 1-8 位转十进制的数;front 就是文件行指令 9-12 位转十进制的数;behind 就是文件行指令13-16 位转十进制的数;im 就是文件行指令 17-32 位转十进制的数。

void skip ( int com , int front , int behind , int im , MTC* cur , int* data )
{
	/* 4种跳转指令 */
		if ( behind == 0 )
			cur->ip = cur->ip + im ;
		
		else if ( behind == 1 && cur->flag == 0 )
			cur->ip = cur->ip + im ;
		
		else if ( behind == 2 && cur->flag == 1 )
			cur->ip = cur->ip + im ;
		
		else if ( behind == 3 && cur->flag == -1 )
			cur->ip = cur->ip + im ;
	
	/* 不跳转 */
		else
			cur->ip = cur->ip + 4 ;

	return ;
}

而对于文件指针操作的话,因为文件是理想化的,每行32个数字之后没有空格,只有 ‘\n’ 结尾,跳转注意一点就行。

4. 线程创建(多核版)

先来理解一些东西,下面是我截取我的最重要的、必须的代码,附有PPT内容:
线程建立(线程函数):

	/* 线程参数结构体定义 */
	typedef struct Segment
	{
		// ......
	}  segment ;
	
	// ......
	
	/* 线程函数声明 */ 
	unsigned __stdcall Fun1Proc ( void* pArguments ) ;
	unsigned __stdcall Fun2Proc ( void* pArguments ) ;
	
	// ......
	
	int main ( int argc , char *argv[ ] )
	{
		// 创建线程参数结构体指针,名为 solid,以地址形式传入两个线程
		segment* solid = ( segment* ) malloc ( sizeof ( segment ) ) ;
		
		/* 创建线程 1 , 2 */
		HANDLE hThread1
		= (HANDLE) _beginthreadex ( NULL , 0 , Fun1Proc , solid , 0 , NULL ) ;
		HANDLE hThread2
		= (HANDLE) _beginthreadex ( NULL , 0 , Fun2Proc , solid , 0 , NULL ) ;
		
		/* 线程 1 , 2 相互等待结束后关闭 , 并释放内存 */
		WaitForSingleObject ( hThread1 , INFINITE ) ;
		CloseHandle ( hThread1 ) ;
		WaitForSingleObject ( hThread2 , INFINITE ) ;	
		CloseHandle ( hThread2 ) ;
		
		return 0 ;
	}
	
	// ......
	
	/* 线程1定义 */
	unsigned __stdcall Fun1Proc ( void* pArguments )
	{
		// ......
	}
	
	/* 线程2定义 */
	unsigned __stdcall Fun2Proc ( void* pArguments )
	{
		// ......
	}

主函数里waitfor是检测线程是否关闭,如果没有关闭,那就一直停在此行,执行不到下面的close,相当于一直等待,等到接力棒(结束信号)来了才能跑。

线程函数使用其实和普通函数的使用是一样的,无非就是函数名前面是**“unsigned __stdcall”**。在
_beginthreadex里创建后该线程函数就开始执行了。

下面就是几张重点截图:
上面所有的函数原型和解释都能在PPT里找到。从声明到定义,都附上了。
怎么说呢,下面是知识点,但只是用来应付作业的话,还是我上面的代码具有参考价值。
请添加图片描述
请添加图片描述
请添加图片描述

5. 线程互斥(多核版)

这是多线程的核心。为了保证线程间不干扰输出,需要一个中间桥梁来传递信息,表示所有权的归属线程,有权的就可以执行,没权的就不能执行。这个桥梁有两:互斥句柄 handle,和同步锁 lock。PPT上说:二选一即可实现线程的交替运行;但是这个是有问题的,后面细说。

  1. 互斥句柄
    说来惭愧,我是在写这篇文章的时候才发现我在 github 上的代码互斥句柄写的有问题,大家不用作为参考,下下面的代码是我认为正确的思路修改后放上去的,如果不对,还请指正。
    先放几张PPT镇一镇:
    请添加图片描述
    请添加图片描述
    请添加图片描述
    因为主函数调用线程线程又要调用句柄,文件信息,代码内存,数据内存,所以需要把这些线程参数整合为一个结构体在 main 函数中用指针传入创建线程的函数(对应上面创建线程地PPT内容)。
    而互斥句柄又类似于线程的控制权,只能归一个线程所有;因为我们要模拟的效果是1、2线程交替运行,所以互相转换控制权就可以。
    可参考代码如下:

     /* 定义寄存器结构体 struct -- segment */	
     typedef struct Segment
     {
     	HANDLE hMutex ;     //  进程互斥对象 
     	int parameter ; //  归属权随机数
     	int** txt1 ;        //  代码 1
     	int line1 ;         //  代码 1 行数 
     	int** txt2 ;        //  代码 2 
     	int line2 ;         //  代码 2 行数 
     	int* codeSegment ;  //  代码内存 
     	int* dataSegment ;  //  数据内存
     }  segment ;
     
     int main ( int argc , char *argv[ ] )
     {
     	// 创建线程参数结构体
     	segment* solid = ( segment* ) malloc ( sizeof ( segment ) ) ;
     	
     	// 随机生成互斥句柄归属权给线程 1 或线程 2
     	srand ( (unsigned) time ( NULL ) ) ;  // 生成时间种子,所需头文件:<time.h>
     	solid->parameter = rand() % 2 + 1 ; // 随机初始化
    
     	/* 创建线程 1 , 2 */
     	HANDLE hThread1
     	= (HANDLE) _beginthreadex ( NULL , 0 , Fun1Proc , solid , 0 , NULL ) ;
     	HANDLE hThread2
     	= (HANDLE) _beginthreadex ( NULL , 0 , Fun2Proc , solid , 0 , NULL ) ;
     	
     	/* 关闭线程 1 , 2  , 并释放内存 */
     	WaitForSingleObject ( hThread1 , INFINITE ) ;
     	CloseHandle ( hThread1 ) ;
     	WaitForSingleObject ( hThread2 , INFINITE ) ;	
     	CloseHandle ( hThread2 ) ;
    
     	return 0 ;
     }
     
     /* 线程1定义 */
     unsigned __stdcall Fun1Proc ( void* pArguments )
     {	
     	//  线程参数结构体强制转换,使地址传参可用segment结构体类型 
     	segment* solid = (segment*) pArguments ;
     	
     	// 如果随即参数为 1 ,将 hMutex 控制权归为自己
     	if ( solid->parameter == 1 )
     		solid->hMutex = CreateMutex ( NULL , FALSE , NULL ) ;
     		
     	// ......
     	
     	// 选择指令(0-15)
     	switch ( num->com ) {
     		//......
     		
     		case 13 :  // 获取句柄
     			lock       ( num , cur , solid ) ;
     			break ;		
     		
     		case 14 :  // 释放句柄
     			release    ( num , cur , solid ) ;
     			break ;
     		
     		//......
     }
     
     /* 线程2定义 */
     unsigned __stdcall Fun2Proc ( void* pArguments )
     {
     	//  线程参数结构体强制转换,使地址传参可用segment结构体类型 
     	segment* solid = (segment*) pArguments ;
     	
     	// 如果随即参数为 2 ,将 hMutex 控制权归为自己
     	if ( solid->parameter == 2 )
     		solid->hMutex = CreateMutex ( NULL , FALSE , NULL ) ;			
    
     	// ......
     	
     	// 选择指令(0-15)
     	switch ( num->com ) {
     		//......
     		
     		case 13 :  // 获取句柄
     			lock       ( num , cur , solid ) ;
     			break ;		
     		
     		case 14 :  // 释放句柄
     			release    ( num , cur , solid ) ;
     			break ;
     		
     		//......
     }
     
     /* 上锁 -- 13 */
     void lock ( number* num , MTC* cur , segment* solid )
     {
     	/* 锁住句柄 */
     	WaitForSingleObject ( solid->hMutex , INFINITE ) ;
     	
     	return ;
     }
     
     /* 释放 -- 14 */
     void release ( number* num , MTC* cur , segment* solid )
     {
     	/* 释放句柄 */
     	ReleaseMutex ( solid->hMutex ) ;
     	
     	return ;
     }
    

显然,句柄这个控制权要在理论上来说要线程共用,所以我在主函数中创建,归属于线程参数 segmet 这个结构体。这样 segment 传进两个线程,句柄就可以共用了。
而指令13、14分别对应创建后句柄的操作:锁住、释放句柄就是修改权限运行状态)信号。具体还可以参考:hMutex
运行过程:首先在主函数中创建线程参数 struct , 然后随机在一个线程中将互斥对象归为己有(使用随机数)。而句柄具体的释放和申请,就是13、14指令在1、2线程的交替运行实现的,我们只需要把功能写出来即可,文件会默认运行。

  1. 同步锁
    同步锁就很好理解了。在线程外部设置一个函数变量,使得两个线程可以同时使用,表示控制权(我设为1和2,1为控制权在线程1,2为控制权在线程2)。如果没有控制权,那就等在 while 里一直循环至给了控制权跳出循环;如果有控制权那就执行到特定步骤还回控制权。可参考代码如下:

     /* 定义寄存器结构体 struct -- segment */
     typedef struct Segment
     {
     	int lock ;          //  同步锁 
     	int end1 ;          //  线程 1 结束信号 
     	int end2 ;          //  线程 2 结束信号 
     	int** txt1 ;        //  代码 1
     	int line1 ;         //  代码 1 行数 
     	int** txt2 ;        //  代码 2 
     	int line2 ;         //  代码 2 行数 
     	int* codeSegment ;  //  代码寄存器 
     	int* dataSegment ;  //  数据寄存器 
     }  segment ;
     
     int main ( int argc , char *argv[ ] )
     {
     	/* 处理 solid : 包括 codeSegment , dataSegment ,lock */
     	segment* solid = ( segment* ) malloc ( sizeof ( segment ) ) ;
     	srand ( (unsigned) time ( NULL ) ) ;
     	solid->lock = rand() % 2 + 1 ; // lock 随机初始化 :1则控制权在1,2则控制权在2
    
     	/* 创建线程 1 , 2 */
     	HANDLE hThread1
     	= (HANDLE) _beginthreadex ( NULL , 0 , Fun1Proc , solid , 0 , NULL ) ;
     	HANDLE hThread2
     	= (HANDLE) _beginthreadex ( NULL , 0 , Fun2Proc , solid , 0 , NULL ) ;
     	
     	/* 关闭线程 1 , 2  , 并释放内存 */
     	WaitForSingleObject ( hThread1 , INFINITE ) ;
     	CloseHandle ( hThread1 ) ;
     	WaitForSingleObject ( hThread2 , INFINITE ) ;	
     	CloseHandle ( hThread2 ) ;
    
     	return 0 ;
     }
     
     /* 线程1定义 */
     unsigned __stdcall Fun1Proc ( void* pArguments )
     {	
     	//  线程参数结构体强制转换,使地址传参可用segment结构体类型 
     	segment* solid = (segment*) pArguments ;
     		
     	// ......
     	
     	// 选择指令(0-15)
     	switch ( num->com ) {
     		//......
     		
     		case 12 :
     			solid->lock = 2 ;  // 将控制权归还给线程 2
     			break ;
     			
     		case 13 :
     			while ( solid->lock == 2 && solid->end2 == 1 ) ;  // 等待拥有控制权
     			break ;
     		//......
     }
     
     /* 线程2定义 */
     unsigned __stdcall Fun2Proc ( void* pArguments )
     {
     	//  线程参数结构体强制转换,使地址传参可用segment结构体类型 
     	segment* solid = (segment*) pArguments ;
    
     	// ......
     	
     	// 选择指令(0-15)
     	switch ( num->com ) {
     		//......
     		
     		case 12 :
     			solid->lock = 1 ;  // 将控制权归还给线程 1
     			break ;
     			
     		case 13 :
     			while ( solid->lock == 1 && solid->end1 == 1 ) ;  // 等待拥有控制权
     			break ;
     		
     		//......
     }
    

不就是锁住改成同步锁锁住,释放改成同步锁释放嘛。
咋一看,没啥问题;但是,互斥句柄是 13、14 指令,同步锁这是 12、13 指令???哪里弄错了吗?
这里就是我在和同学一起讨论的时候最大的难点。如果是 13、14 指令写同步锁,那么会出现卖票不能严格交替的问题。

文件指令功能具体如下:
请添加图片描述
可见,卖票要经过** 3 个步骤**:将票数 -1 ;将剩余票数储存起来;输出剩余票数。
这个课题的功能设置是有问题的:所谓交替买票(就是总共有 100 张,交替操作卖票将其减一,直至卖完),就是卖一张输出一个剩余票数;但是,它所设置的输出和卖票并不是同时的。。导致一个线程跑的比另一个线程快的时候,他就能卖两张甚至更多,导致输出不对。而且这个问题不只是同步锁写法有,互斥句柄写法也有。所以下面的解决方法在互斥句柄写法中也适用。

经过多次修改侦测,我和同学发现,这个问题的解决办法是:**将这三个步骤串起来,从锁住到输出只有一个线程运行。**在一个线程执行到锁住指令时,如果没有控制权,就等它有控制权;有控制权后,就执行,直到输出指令结束才释放控制权(根据文件分析,锁住指令必定配一个输出指令,这是投机取巧的办法)。具体实现方法看上面代码。这就是为什么把释放写在输出指令 12 中。

6. 运行效率优化

代码能跑还仅仅不够,运行效率也是我们学习计算机应该关注的事情。C语言毕竟是中级语言,优势有底层和高效,总不能把这个给丢了,所以我在完成课题后也进行了效率优化。详细的描述过程在我的实验总结里有,我这直接放截图了:
请添加图片描述

后记

心得体会,直接放图,太累了,已经9500字了。
请添加图片描述
至于开头说的和计组寄存器相关知识有关,偶还没有正式学,就算懂点皮毛也不敢乱说,所以就放个链接,供大家参考:寄存器
因为这是下学期的课题,如果你们明年下学期的时候看到还有这段话,那就说明我摸鱼了,记得评论提醒我哈哈,我会尽自己所能给这个课题适当的寄存器知识补充的。

感谢支持!
Contributed by Spike.

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值