本人第一次在CSDN上发表文章哈,请多多支持。
这个大作业其实涉及到大二的计组课程(寄存器相关知识),有助于提前理解。感谢本校黄海老师出的题目(坏耶)。
话不多说,上链接,是放在github上的。
链接有所有的配套资料,以及我所有的 code,单线程+线程都(确保能运行的嗷,而且有注释,多个思路)。
链接:BUPTtask_2020
具体怎么用github,这里也放个链接:github使用方法
可以借鉴,但不要抄袭。
此外,一定要保证看完课题要求、指导PPT、指令集和需处理文件,并适当思考,再看下面的文章,要不然就是天方夜谭了。下文又臭又长,还请耐心看完。
题目概述
首先是课题叫模拟冯诺依曼式计算机CPU简易模拟器,先放张图:
这个就是冯诺依曼式CPU简易构架,课题的目的就是要简易模拟其中的运作方式。
我们要实现读入文件数据后用临时变量处理这些数据,并且模拟内存的运作方式,将处理的数据暂存,并根据特定指令运算和输入输出。
其实无非就是:
打开文件流处理文件;
声明定义变量来分析数据(如指令寄存器的数,立即数等);
用学过的数据类型来模拟我们需要的内存(根据处理的数据特征选择合适的数据类型),并将数据暂存,模拟内存的功能;
用 switch 或者 if/else 写好主函数的指令选择;
写好指令函数(子函数)。
所以思路很明确,写好文件处理,写好指令选择,写好内存模拟就行了。
多线程无非是多一个创建线程;单核是一个源码一个跑,多核是一个源码两个跑。
所以在有基本了解的基础上,我觉得最重要的是把难点讲好。
写不出来?不存在的。无非就是卡了一下,其实不会有太大问题的。相信自己!
本人遇到的所有困难请看上面链接中的 ”实验总结“,下面篇幅我详细讲几个重要的难点。
0. 参数的设置
老规矩嗷,程序员得从 0 开始数 O(∩_∩)O (其实是后面发现自己得把这个写 1 前面)。
文件是 32 个 0、1 数字为一行,不超过32行的dic格式文式。上图,先了解一下每行的数据构成:
- “com(command)”:每行前8个数字,我使用二进制转十进制来存,便于书写(因为前四个数字默认为0,所以单线程范围是0-12,多线程是0-15)。这8个数字就是来控制进行操作种类(加减乘除或者存储打印)。
- “front”:前一个寄存器,我使用二进制转十进制来存,一般可以理解为信息流流入的寄存器,操作终点。
- “behind”:后一个寄存器,我使用二进制转十进制来存,一般可以理解为信息流流出的寄存器,操作起点。
- “im(immediateValue)”:立即数,为补码形式,理解为行可操作数据源。
可知,这些可以直接在主函数里声明定义,生命周期即为 main() 的周期。
再来了解一下寄存器参数。当然,先来看一下实验要求:
第一个是系统寄存器,是指每次行指令执行时程序的数据暂存位点,纪录了程序当前执行的数据。
- 当前文件指针前面的字节数 “ip” (注:默认32位系统,8位1字节,所以1行4字节),也称“程序计数器”。
- 当前行的前16位数字转十进制数 “ir”(因为后16位是立即数,前16位代表了在哪里操作,做什么操作),也称“指令寄存器”。
- 当前的逻辑运算结果 “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 )
"dataSegment"是数据段内存,我认为是最难理解的。
这里要讲一个前提:地址从16384开始,两个字节一段,上图 “5” 所在空间对应地址为16384,“6” 所在空间对应16386,以此类推。
观察指令集可知,是通过地址寄存器 5 6 7 8 中的数(地址寄存器存的是地址,这个是有输入前提的,不会有什么奇怪的输入)来找到对应的你模拟的内存空间(就是二维数组啦,初始值为0,16行16列),并修改相应的值。
比如下面对应的指令:
00000001 0101 0001 0000000000000000
将寄存器1的内容存入寄存器5所指向内存单元。
此时地址寄存器存的数据是16384,所以如上图所示, 第一个数字变成了5。
1. 文件读入
因为是文件流读取数据,那么就必须用到文件流的相关知识(函数),而其中最重要的就是关于文件指针的操作。
但是不仅仅如此,因为牵扯到文件指针的跳转,以及数据的处理,其实还是蛮复杂的。
首先,因为数据处理的大循环是以行为单位,所以面临两个选择:
储存模式的选择:
是一次性读完存储于内存数组里呢,还是直接操作文件呢?
对于储存模式,我觉得文件指针的移动显然不如二维数组改变下标的查找来的直接(例如往前跳转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上说:二选一即可实现线程的交替运行;但是这个是有问题的,后面细说。
-
互斥句柄
说来惭愧,我是在写这篇文章的时候才发现我在 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和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.