北邮2021计导大作业丨C语言实现冯诺依曼式计算机CPU模拟器(二):多核版

文章目录

  • 一、前言
  • 二、课题要求
    • (一)多核版说明
    • (二)输入输出说明
      • 1.输入说明
      • 2.输出说明
      • 3.样例
  • 三、多核版指令
    • (一)新增指令
    • (二)指令分析
      • 1.指令结构
      • 2.指令作用与实现
    • (三)验收指令序列
  • 四、多线程介绍
    • (一)线程
    • (二)互斥对象
      • 1.创建互斥对象
      • 2.释放互斥对象
      • 3.请求互斥对象
    • (三)线程休眠
  • 五、多核版实现
    • (一)顶层设计
      • 1.全局常量定义
      • 2.全局数据结构定义
      • 3.函数声明
    • (二)核心算法
      • 1.main()函数
      • 2.线程函数process()
      • 3.两个小细节
    • (三)获取完整代码

一、前言

这是北邮2021计导大作业的第二部分,多核版cpu模拟器。多核版的基本思路与单核版一致,唯一的不同在于要模拟cpu的两个核同时执行指令,这就要使用到多线程的知识。

我之前没有接触过多线程,因此写多核版之前,先去把某鱼发给我的“多线程程序设计”PPT看了好几遍,并且查阅了许多资料。但其实现在对于一些细节还是有些懵。这里先留一个坑,等以后对多线程的知识理解的比较透彻了,再来补一些详细的介绍。

二、课题要求

多核版cpu设计以单核版为基础,单核版指令集、分析与代码见我的另一片文章北邮2021计导大作业丨C语言实现冯诺依曼式计算机CPU模拟器(一):单核版

(一)多核版说明

  1. 只做两个核的版本(扩展到多核版类似)。
  2. 增加了3条指令:为了简化程序,增加三条功能很强的指令,具体见指令集。这与实际的CPU差异很大。
  3. 每个核有自己的一套寄存器,但两个核共享内存。核心1的代码段从地址0开始,核心2的代码段从地址256开始。
  4. 每个核读入自己的指令序列。
  5. 验收指令序列非常简单,两个核的指令序列是一样的,实现的就是多线程一章里的卖票程序(共100张票,两个线程卖)。
  6. 为了简化程序,我们规定100这个值存在地址为16384的内存里。所以程序初始化时要将这块内存的值初始化为100。程序结束时它应该变成0。

(二)输入输出说明

1.输入说明

  • 同单核版输入方式一样,具体格式参见输入样例。
  • 为了验收方便,规定核心1读入指令的文件名为dict1.dic,核心2读入指令的文件名为dict2.dic。

2.输出说明

  • 为了区分每个核心的输出,增加一个类似寄存器的核心ID(id),该值在整个线程运行期间不变,比如核心1的id为1,核心2的id为2等。大家的输出有两处要修改,一是在输出所有寄存器之前先输出核心ID,格式见后边输出样例;二是在执行输出指令时,也是先输出核心ID,格式见输出样例。

3.样例

  1. 每执行一条指令,需要保证该条指令的寄存器信息输出是连续的,不能被其它输出信息分割:
id = 2
ip = 268
flag = 0
ir = 277
ax1 = 10 ax2 = 0 ax3 = 0 ax4 = 0
ax5 = 16384 ax6 = 0 ax7 = 0 ax8 = 0
  1. 输出指令输出:(id与out两部分中间相距4个空格)
id = 1    out: 8  
  1. 最后的代码段和数据段只输出一次,格式保持不变。
    注:由于线程调度的随机性,最终的输出结果除代码段和数据段在最后保持不变外,前面每条指令的输出顺序都是不定的。

三、多核版指令

(一)新增指令

与单核版相比,多核版新增了3条指令。

单核版指令集见北邮2021计导大作业丨C语言实现冯诺依曼式计算机CPU模拟器(一):单核版

类别 指令 说明
多核版指令 00001101000000000000000000000000 立即数(绿色部分)为内存地址,请求互斥对象,用于锁住立即数所指定的内存。如果互斥对象已占用,则一直等待。
多核版指令 00001110000000000000000000000000 立即数(绿色部分)为内存地址,释放互斥对象,释放掉锁住立即数所指定的内存的互斥对象。与上一条指令对应
多核版指令 00001111000000000000000000000000 休眠立即数(绿色部分)毫秒。

(二)指令分析

1.指令结构

所有的指令都是32位二进制数字,具体可拆解为以下四个部分(以下位数均为从左到右):

  1. 1-8位:指明任务类型
  2. 9-12位:操作对象1
  3. 13-16位:操作对象2
  4. 17-32位:立即数

以指令00000001001001100000000000000000为例:

  1. 1-8位:00000001,对应数据传送指令
  2. 9-12位:0010,,对应寄存器2(数据寄存器)
  3. 13-16位:0110,对应寄存器6(地址寄存器)
  4. 17-32位:0000000000000000,转换为十进制即为0。因为该条指令的两个操作对象都是寄存器,不涉及立即数的运算,因此为0。

2.指令作用与实现

当指令的1-8位转换为10进制为13-15时,对应多核版指令:

  1. 00001101:十进制的13,锁内存。
  2. 00001110:十进制的14,解锁内存。
  3. 00001111:十进制的15,睡眠。

对于锁内存指令和解锁内存指令,按照指令集的描述要锁住或解锁“立即数所指定的内存”,但实际上由于请求互斥对象语句可以锁住任何后续的操作内存,所以,不用管锁哪里,只要执行指令调用互斥对象语句就可以。

多核版指令的处理函数定义如下:

/*多线程操作*/
void task_13(cpuPtr cpu,corePtr core)
{
	switch(core->task)
	{
		case 13: WaitForSingleObject(cpu->hMutex,INFINITE);break; //锁内存 
		case 14: ReleaseMutex(cpu->hMutex);break; //释放内存(解锁) 
		case 15: Sleep(core->value);break; //睡眠 
			
	}
}

(三)验收指令序列

  • 验收指令序列非常简单,两个核的指令序列是一样的,实现的就是多线程一章里的卖票程序(共100张票,两个线程卖)。
  • 为了简化程序,我们规定100这个值存在地址为16384的内存里。所以程序初始化时要将这块内存的值初始化为100。程序结束时它应该变成0。
  • 同单核版输入方式一样,具体格式参见输入样例。
  • 为了验收方便,规定核心1读入指令的文件名为dict1.dic,核心2读入指令的文件名为dict2.dic。

dict1.dic与dict2.dic两个文件的内容相同,如下所示。

00000001010100000100000000000000
00001101000000000100000000000000
00000001000101010000000000000000
00001001000100000000000000000000
00001010000000010000000000011100
00000011000100000000000000000001
00000001010100010000000000000000
00001110000000000100000000000000
00001100000100000000000000000000
00001111000000000000000000000010
00001010000000001111111111011100
00001110000000000100000000000000
00000000000000000000000000000000

也就是说,两个核心执行相同的指令序列,这个指令序列共13行,具体含义如下:

  1. 向ax5赋值16384
  2. 锁内存(16384)
  3. 将ax5地址指向的内存中的数赋值给ax1
  4. 比较ax1中的数与0
  5. 如果flag为0(即ax1中的数等于0),程序计数器+28
  6. ax1自减1
  7. 把ax1中的数赋值给ax5地址所指向的内存
  8. 解锁内存(16384)
  9. 输出ax1的值
  10. 休眠2毫秒
  11. 无条件跳转,程序计数器-36
  12. 解锁内存(16384)
  13. 停机

由于按照课题要求,初始化时已经将要卖的票数100写入了16384对应的内存,因此该指令序列实现的其实就是让两个线程交替卖票,直到票数减到0,程序停止。

如果程序没有问题,最终应在控制台输出共计6221行信息。

四、多线程介绍

注:本部分函数使用时,程序必须包含头文件

#include 

(一)线程

  • 线程是系统分配处理器时间资源的基本单元。对于操作系统而言,其调度单元是线程(为线程提供时间片,线程在自己的时间片内运行)。
  • 一个程序中多段代码同时并发执行,称为多线程。譬如用word同时打开多个文档进行编辑,用IE浏览器同时访问多个网站
  • 通过多线程,一个进程表面上看同时可以执行一个以上的任务——并发
  • 一个进程至少包括一个线程(称为主线程)。一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务。
  • 线程自己不拥有系统资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源

在C程序中要创建线程,可以调用Windows操作系统提供的创建线程的函数CreateThread :

HANDLE WINAPI CreateThread (
	LPSECURITY_ATTRIBUTES lpThreadAttributes, 
	SIZE_T dwStackSize, 
	LPTHREAD_START_ROUTINE lpStartAddress, 
	LPVOID lpParameter, 
	DWORD dwCreationFlags, 
	LPDWORD lpThreadId);
  • LPVOID是一个Void类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量。
  • DWORD是32位无符号整数。
  • lpThreadAttributes表示创建线程的安全属性,NT下有用。可赋值为NULL
  • dwStackSize指定线程栈的尺寸,如果为0则与进程主线程栈相同。
  • lpStartAddress指定线程开始运行的地址。赋值为指向函数的指针,即函数名。该函数的名称任意,但函数类型必须遵照下述声明形式:
    DWORD WINAPI ThreadProc(LPVOID lpParameter);否则需要进行强制类型转换
  • lpParameter表示传递给线程的32位的参数(数值或指针)。 若无参数则赋值为NULL。
  • dwCreationFlags表示是否创建后挂起线程(取值CREATE_SUSPENDED表示挂起,取值0表示创建后立即运行),挂起后调用ResumeThread继续执行。若不挂起则赋值为0
  • lpThreadId用来存放返回的线程ID。

(二)互斥对象

  • 线程的同步: 利用互斥对象(mutex)实现线程的同步,互斥对象能够确保线程拥有对单个资源的互斥访问权。
  • 3个操作: 互斥对象的创建、互斥对象的释放、互斥对象的请求

1.创建互斥对象

调用CreateMutex()函数创建一个互斥对象:

HANDLE WINAPI CreateMutex (
	LPSECURITY_ATTRIBUTES lpMutexAttributes, 
	WINBOOL bInitialOwner, 
	LPCSTR lpName);
  • lpMutexAttributes:可以给该参数传递 NULL值,让互斥对象使用默认的安全性
  • binitialOwner:BOOL类型,指定互斥对象初始的拥有者。如果该值为真,则创建这个互斥对象的线程获得该对象的所有权;否则,该线程将不获得所创建的互斥对象的所有权。
  • lpName:指定互斥对象的名称。如果此参数为 NULL.则创建一个匿名的互斥对象。如果调用成功,该函数将返回所创建的互斥对象的句柄

2.释放互斥对象

调用ReleaseMutex()释放互斥对象:

BOOL ReleaseMutex ( HANDLE hMutex );
  • ReleaseMutex函数只有一个HANDLE类型的参数,即需要释放的互斥对象的句柄。该函数的返回值是BOOL类型,如果函数调用成功,返回非0值;否则返回0值。

3.请求互斥对象

DWORD WaitForSingleObject( 
	HANDLE hHandle , 
	DWORD dwMilliseconds );

  • Handle:所请求的互斥对象的句柄。一旦互斥对象处于有信号状态,则该函数就返回。如果该互斥对象始终处于无信号状态,即未通知的状态,则该函数就会一直等待,这样就会暂停线程的执行。
  • dwMilliseconds:指定等待的时间间隔,以毫秒为单位。如果指定的时间间隔己过,即使所请求的对象仍处于无信号状态,WaitForSingleObject函数也会返回。如果将此参数设置为0,那么 WaitForSingleObject函数将测试该对象的状态并立即返回;如果将此参数设置为INFINITE, 则该函数会永远等待,直到等待的对象处于有信号状态才会返回。

调用WaitForSingleObject函数后,该函数会一直等待,只有在以下两种情况下才会返回:

  1. 指定的对象变成有信号状态。
  2. 指定的等待时间间隔己过。

(三)线程休眠

线程休眠函数:

void Sleep(DWORD)
  • Sleep(1000)Windows下表示1000毫秒,也就是1秒钟;Linux下表示1000秒,Linux下使用毫秒级别的函数可以使用usleep
  • Sleep函数是使调用Sleep函数的线程休眠,线程主动放弃时间片。当经过指定的时间间隔后,再启动线程,继续执行代码。Sleep函数并不能起到定时的作用,主要作用是延时。

五、多核版实现

(一)顶层设计

1.全局常量定义

#include 
#include 
#include 
#define Max 16384 //数据段和代码段的长度 
#define N 16384   //数据段的下标偏移 
#define START_1 0 //核心1的代码起始位置 
#define START_2 256 //核心2的代码起始位置 
#define TICKETS 100 //需要卖出的票数 

2.全局数据结构定义

/*模拟核心的结构体*/ 
struct core{
	short ax[9]; //模拟通用寄存器 
	short ip; //模拟程序计数器 
	short ir; //模拟指令寄存器 
	short flag; //模拟标志寄存器 
	short task; //1-8位,表示操作类型 
	short front;//9-12位,表示操作位 1 
	short back; //13-16位,表示操作位 1 
	short value;//17-32位,表示立即数 
	DWORD *id; //记录线程id 
};
typedef struct core* corePtr;

/*模拟cpu的结构体*/ 
struct _cpu{
	char code[Max]; //模拟代码段 
	char data[Max]; //模拟数据段 
	corePtr core_1;
	corePtr core_2;
	HANDLE hMutex;//互斥对象句柄 
	HANDLE hMutex_state;//避免输出不完整
};
typedef struct _cpu* cpuPtr;

3.函数声明

void init_cpu(cpuPtr cpu); //初始化cpu 
void init_core(corePtr core); //初始化core 
void load(FILE *fPtr,cpuPtr cpu,int start); //指令加载 
void end_output(cpuPtr cpu); //输出代码段和数据段信息 
DWORD WINAPI process(LPVOID lpParameter); //线程函数 
void task_1(cpuPtr cpu,corePtr core); //数据传送指令 
void task_2(cpuPtr cpu,corePtr core); //算术运算指令 
void task_6(cpuPtr cpu,corePtr core); //逻辑运算指令
void task_9(cpuPtr cpu,corePtr core); //比较运算指令 
void task_10(cpuPtr cpu,corePtr core);//跳转指令 
void task_11(cpuPtr cpu,corePtr core);//输入输出指令 
void task_13(cpuPtr cpu,corePtr core);//多核版指令 
short bin(cpuPtr cpu,int start);//从数据段读取信息(两个char)并转换为16位二进制整数(short型)
void bin_to_char(cpuPtr cpu,int start,short number);//将16位二进制整数(short型)转化为两个char并存入数据段 

(二)核心算法

1.main()函数

新建一个cpu,对其初始化,并从两个文件中读取指令加载到代码段。然后新建两个线程,并将线程id存到对应的core中,以方便在process()函数中判断当前是哪个核心在进行调用。等待两个线程执行完毕后输出代码段与数据段的信息,退出程序。

int main()
{
	HANDLE thread1,thread2;//线程句柄 
 
	cpuPtr cpu = (cpuPtr)malloc(sizeof(struct _cpu));
	FILE *fPtr_1=fopen("dict1.txt","r");
	FILE *fPtr_2=fopen("dict2.txt","r");
	if(fPtr_1==NULL||fPtr_2==NULL)
		printf("error!");
	
	init_cpu(cpu); //初始化cpu 
	load(fPtr_1,cpu,START_1); //核心1指令加载 
	load(fPtr_2,cpu,START_2); //核心2指令加载 
	/*创建两个线程*/ 
	thread1 = CreateThread(NULL,0,process,cpu,0,cpu->core_1->id);
	thread2 = CreateThread(NULL,0,process,cpu,0,cpu->core_2->id);
	WaitForSingleObject(thread1, INFINITE);
	WaitForSingleObject(thread2, INFINITE);
	CloseHandle(thread1);
	CloseHandle(thread2);
	
	end_output(cpu);//输出代码段和数据段信息 
	return 0;
}

2.线程函数process()

调用GetCurrentThreadId()函数获得当前线程id,并分别与core1、core2中储存的id比较,确定当前线程使用的core。然后进入循环,不断地取指令进行解析,按照指令类型调用响应的任务函数,并在执行完每条指令后输出该线程对应的核心的寄存器信息。当遇到停机指令,跳出循环。

DWORD WINAPI process(LPVOID lpParameter)
{
	cpuPtr cpu = (cpuPtr)lpParameter; //参数类型转换 
	int core_id; //记录当前运行核心
	corePtr core = NULL; //当前核心指针 
	
	cpu->hMutex=CreateMutex (NULL, FALSE, "tickets");//创建互斥类型 
	cpu->hMutex_state=CreateMutex (NULL, FALSE, "states");//创建互斥类型 
	
	if(GetCurrentThreadId()==*(cpu->core_1->id)){ //如果当前线程id与核心1的id一致,说明当前函数由线程1执行 
		core_id = 1;
		core = cpu->core_1;//当前核心为核心1 
	}else if(GetCurrentThreadId()==*(cpu->core_2->id)){//如果当前线程id与核心2的id一致,说明当前函数由线程2执行
		core_id = 2;
		core = cpu->core_2;//当前核心为核心2 
	}else{
		printf("线程识别错误\n"); 
	}
	
	/*循环,不断取指令分析执行,直到遇到停机指令*/ 
	do{
		/*代码段中每条指令占四个字节,取出前两个字节并拼接写入ir(指令寄存器)*/
		core->ir = cpu->code[(core->ip)++]&0xff; 
		core->ir = ((core->ir<<8)&0xff00)+(cpu->code[core->ip++]&0xff);
		
		/*取出后两个字节并拼接,即为立即数,赋值给value*/ 
		core->value = cpu->code[core->ip++]&0xff;
		core->value = ((core->value<<8)&0xff00)+(cpu->code[core->ip++]&0xff);
		
		/*指令寄存器储存16位指令,前8位对应操作类型,通过位运算取出赋值给task*/ 
		core->task = core->ir>>8;
		
		/*指令寄存器9-12位、13-16位对应操作位1和操作位2,位运算取出赋值给front、back*/ 
		core->front = ((core->ir<<8)>>12)&0xf;
		core->back = ((core->ir<<12)>>12)&0xf;
		
		/*分析指令操作类型,调用对应函数*/ 
		switch(core->task)
		{
			case 1: task_1(cpu,core);break; //数据传送指令 
			case 2:case 3:case 4:case 5: task_2(cpu,core);break; //算数运算指令 
			case 6:case 7:case 8: task_6(cpu,core);break;        //逻辑运算指令 
			case 9: task_9(cpu,core);break; //比较运算指令 
			case 10: task_10(cpu,core);break; //跳转指令 
			case 11:case 12: 
				WaitForSingleObject(cpu->hMutex_state,INFINITE);//等待正在执行的线程输出完毕
				task_11(cpu,core); //输入输出指令 
				break;
			case 13:case 14:case 15:task_13(cpu,core);break; //多线程操作指令 
		}
		
		if(core->task!=11&&core->task!=12){
			WaitForSingleObject(cpu->hMutex_state,INFINITE);//等待正在执行的线程输出完毕
		} 
		/*每条指令执行完输出各寄存器状态*/ 
		printf("id = %d\n",core_id);
		printf("ip = %d\n",core->ip);
		printf("flag = %d\n",core->flag);
		printf("ir = %d\n",core->ir);
		printf("ax1 = %d ax2 = %d ax3 = %d ax4 = %d\n",core->ax[1],core->ax[2],core->ax[3],core->ax[4]);
		printf("ax5 = %d ax6 = %d ax7 = %d ax8 = %d\n",core->ax[5],core->ax[6],core->ax[7],core->ax[8]);
		ReleaseMutex(cpu->hMutex_state); //释放互斥对象 
	}while(core->task);//task为0则为停机指令,跳出循环 
}

3.两个小细节

  1. 进程识别
    由于两个线程使用的是同一个函数,该函数被调用时应该识别当前是哪个线程在进行调用。我们采取的方法是在创建线程时将线程id赋值给了模拟cpu的结构体的对应核心的id成员,在调用process函数时先使用GetCurrentThreadId函数获取当前线程id,再与分别与两个核心对应的结构体中的id成员进行比较,从而确定当前线程。

  2. 避免输入输出被打断
    新增一个互斥对象hMutex_state用于避免两个线程输出寄存器信息时彼此打断。每个线程每次准备输出寄存器信息时,都先使用WaitForSingleObject函数申请互斥对象,输出完毕后调用ReleaseMutex函数释放互斥对象。

(三)获取完整代码

由于完整代码较长,且多核版提交还没有截止,这里就不展示完整代码了。感兴趣的朋友可以关注微信公众号“宇梵文书”联系我,我看到消息就会回复。
北邮2021计导大作业丨C语言实现冯诺依曼式计算机CPU模拟器(二):多核版_第1张图片

你可能感兴趣的:(BUPT,C/C++,多线程,c语言)