这是北邮2021计导大作业的第二部分,多核版cpu模拟器。多核版的基本思路与单核版一致,唯一的不同在于要模拟cpu的两个核同时执行指令,这就要使用到多线程的知识。
我之前没有接触过多线程,因此写多核版之前,先去把某鱼发给我的“多线程程序设计”PPT看了好几遍,并且查阅了许多资料。但其实现在对于一些细节还是有些懵。这里先留一个坑,等以后对多线程的知识理解的比较透彻了,再来补一些详细的介绍。
多核版cpu设计以单核版为基础,单核版指令集、分析与代码见我的另一片文章北邮2021计导大作业丨C语言实现冯诺依曼式计算机CPU模拟器(一):单核版
id = 2
ip = 268
flag = 0
ir = 277
ax1 = 10 ax2 = 0 ax3 = 0 ax4 = 0
ax5 = 16384 ax6 = 0 ax7 = 0 ax8 = 0
id = 1 out: 8
与单核版相比,多核版新增了3条指令。
单核版指令集见北邮2021计导大作业丨C语言实现冯诺依曼式计算机CPU模拟器(一):单核版
类别 | 指令 | 说明 |
---|---|---|
多核版指令 | 00001101000000000000000000000000 | 立即数(绿色部分)为内存地址,请求互斥对象,用于锁住立即数所指定的内存。如果互斥对象已占用,则一直等待。 |
多核版指令 | 00001110000000000000000000000000 | 立即数(绿色部分)为内存地址,释放互斥对象,释放掉锁住立即数所指定的内存的互斥对象。与上一条指令对应 |
多核版指令 | 00001111000000000000000000000000 | 休眠立即数(绿色部分)毫秒。 |
所有的指令都是32位二进制数字,具体可拆解为以下四个部分(以下位数均为从左到右):
以指令00000001001001100000000000000000
为例:
00000001
,对应数据传送指令0010
,,对应寄存器2(数据寄存器)0110
,对应寄存器6(地址寄存器)0000000000000000
,转换为十进制即为0
。因为该条指令的两个操作对象都是寄存器,不涉及立即数的运算,因此为0。当指令的1-8位转换为10进制为13-15时,对应多核版指令:
00001101
:十进制的13,锁内存。00001110
:十进制的14,解锁内存。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行,具体含义如下:
- 向ax5赋值16384
- 锁内存(16384)
- 将ax5地址指向的内存中的数赋值给ax1
- 比较ax1中的数与0
- 如果flag为0(即ax1中的数等于0),程序计数器+28
- ax1自减1
- 把ax1中的数赋值给ax5地址所指向的内存
- 解锁内存(16384)
- 输出ax1的值
- 休眠2毫秒
- 无条件跳转,程序计数器-36
- 解锁内存(16384)
- 停机
由于按照课题要求,初始化时已经将要卖的票数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个操作: 互斥对象的创建、互斥对象的释放、互斥对象的请求
调用CreateMutex()
函数创建一个互斥对象:
HANDLE WINAPI CreateMutex (
LPSECURITY_ATTRIBUTES lpMutexAttributes,
WINBOOL bInitialOwner,
LPCSTR lpName);
lpMutexAttributes
:可以给该参数传递 NULL
值,让互斥对象使用默认的安全性binitialOwne
r:BOOL
类型,指定互斥对象初始的拥有者。如果该值为真,则创建这个互斥对象的线程获得该对象的所有权;否则,该线程将不获得所创建的互斥对象的所有权。lpName
:指定互斥对象的名称。如果此参数为 NULL
.则创建一个匿名的互斥对象。如果调用成功,该函数将返回所创建的互斥对象的句柄调用ReleaseMutex()
释放互斥对象:
BOOL ReleaseMutex ( HANDLE hMutex );
ReleaseMutex
函数只有一个HANDLE
类型的参数,即需要释放的互斥对象的句柄。该函数的返回值是BOOL
类型,如果函数调用成功,返回非0值;否则返回0
值。DWORD WaitForSingleObject(
HANDLE hHandle ,
DWORD dwMilliseconds );
Handle
:所请求的互斥对象的句柄。一旦互斥对象处于有信号状态,则该函数就返回。如果该互斥对象始终处于无信号状态,即未通知的状态,则该函数就会一直等待,这样就会暂停线程的执行。dwMilliseconds
:指定等待的时间间隔,以毫秒为单位。如果指定的时间间隔己过,即使所请求的对象仍处于无信号状态,WaitForSingleObject
函数也会返回。如果将此参数设置为0
,那么 WaitForSingleObject
函数将测试该对象的状态并立即返回;如果将此参数设置为INFINITE
, 则该函数会永远等待,直到等待的对象处于有信号状态才会返回。调用WaitForSingleObject
函数后,该函数会一直等待,只有在以下两种情况下才会返回:
线程休眠函数:
void Sleep(DWORD);
Sleep(1000)
Windows下表示1000毫秒,也就是1秒钟;Linux下表示1000秒,Linux下使用毫秒级别的函数可以使用usleep
。Sleep
函数是使调用Sleep
函数的线程休眠,线程主动放弃时间片。当经过指定的时间间隔后,再启动线程,继续执行代码。Sleep
函数并不能起到定时的作用,主要作用是延时。#include
#include
#include
#define Max 16384 //数据段和代码段的长度
#define N 16384 //数据段的下标偏移
#define START_1 0 //核心1的代码起始位置
#define START_2 256 //核心2的代码起始位置
#define TICKETS 100 //需要卖出的票数
/*模拟核心的结构体*/
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;
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并存入数据段
新建一个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;
}
调用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则为停机指令,跳出循环
}
进程识别
由于两个线程使用的是同一个函数,该函数被调用时应该识别当前是哪个线程在进行调用。我们采取的方法是在创建线程时将线程id赋值给了模拟cpu的结构体的对应核心的id成员,在调用process
函数时先使用GetCurrentThreadId
函数获取当前线程id,再与分别与两个核心对应的结构体中的id成员进行比较,从而确定当前线程。
避免输入输出被打断
新增一个互斥对象hMutex_state
用于避免两个线程输出寄存器信息时彼此打断。每个线程每次准备输出寄存器信息时,都先使用WaitForSingleObject
函数申请互斥对象,输出完毕后调用ReleaseMutex
函数释放互斥对象。
由于完整代码较长,且多核版提交还没有截止,这里就不展示完整代码了。感兴趣的朋友可以关注微信公众号“宇梵文书”联系我,我看到消息就会回复。