首页 > 编程语言 >操作系统实验1 体验 Nachos 下的并发程序设计

操作系统实验1 体验 Nachos 下的并发程序设计

时间:2023-04-02 18:11:37浏览次数:44  
标签:操作系统 void dllist 并发程序 线程 newnode Nachos next ptr

操作系统实验报告

实验:

Lab1 The Trouble with Concurrent Programming

专业

计算机科学与技术

班级

1班

姓名

姚怀聿

学号

22920202204632

2023年3月24日

目 录

一、 实验目的 3

二、 实验要求 3

三、 实验设计及关键代码实现 3

1. 多线程执行可能出现的问题 3

2. 正确编译Nachos 4

3. 代码涉及的参数 5

4. 实现双向链表 5

5. 线程并发以及并发可能引起的问题 11

问题1:内存共享 12

问题2:覆盖 13

问题3:乱序 15

四、 遇到的问题 18

五、 实验总结 18

实验目的

  1. 熟悉Nachos系统。了解Nachos的目录结构,最主要的部分是 Nachos 的code部分。
  2. 初步了解Makefile的构成和相互关系。
  3. 修改Nachos 线程管理部分源代码体会多线程并发会导致的问题。

实验要求

  1. 安装 、编译nachos
  2. 实现双向有序链表

撰写dllist.cc、dllist.h、dllist-driver.cc文件。文件中要包括类的定义和实现,以及有序插入N个随机数函数和删除函数的实现,同时打印出删除的项目。

  1. 体验 nachos 线程系统

需要做的更改有:

  1. 将dllist.cc、dllist.h、dllist-driver.cc、dllist-driver.h等文件拷贝到 nachos-3.4/code/threads/目录下。
  2. 修改 Makefile.common 中的 THREAD_H、THREAD_C、THREAD_O 以保证新的文件可以被正确编译。
  3. 根据实验内容,修改main.cc,threadtest.cc等文件,通过操作双向链表展现多线程并发所引发的问题。

实验设计及关键代码实现

多线程执行可能出现的问题

  1. 线程安全问题
  • 原子性:即一个操作或多个操作,要么全部执行并且执行过程中不会被任何的因素打断,要么就都不执行
  • 原子操作:即不会被线程调度机制打断的操作,没有上下文切换

在并发编程中很多的操作都不是原子操作,如:

操作1: i = 0; 对基本数据类型变量的赋值操作是原子操作

操作2: i++; 包含3个操作,读取i的值,将i加1,将值赋给i

操作3: i = j; 包含2个操作,读取j的值,将j的值赋给i

操作4 : i = i + 1; 包含3个操作,读取i的值,将i加1,将值赋给i

所以非原子操作的每个操作都可能被线程调度机制打断,引发问题

  • 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即得到这个修改的值。每个线程都有自己的工作内存,工作内存和主存间要通过store和load进行交互。假如有两个线程:线程1在自己的工作内存中完成赋值操作,却没有及时将新值刷新到主内存中。而线程2会从主内存中读取i的值,然后加载到自己的工作内存中,但赋值工作还没有完成,就进行了读取。这就是可见性问题。
  1. 活跃性问题:可分为死锁、活锁和饥饿。
  2. 性能问题
  • 在创建或撤销进程时,系统都要为之分配或回收进程控制块PCB及其他资源。操作系统为此所付出的开销,明显大于创建或撤销线程时的开销。
  • 在进程切换时涉及进程上下文的切换,而线程切换时只需保存和设置少量寄存器内容,开销很小。

本次实验主要涉及线程安全问题

正确编译Nachos

Makefile.common文件定义了编译链接生成一个完整的Nachos可执行文件所需要的所有规则。我们需要把要添加的.h和.cc文件放到_H、_C还有_O的列表中。将新编写的dllist.cc、dllist.h、dllist-driver.cc文件加入列表中,使make可以正常编译链接出目标文件。

代码涉及的参数

表1 可传入的参数

参数标记

对应变量名

参数含义

-q

int testnum

测试编号,用于进入不同的测试分支(默认为1)

-t

int threadnum

需要创建的并行线程数量(默认为2)

-n

int oprnum

链表操作的元素个数(默认为2)

-y

bool yield_flag

标志是否能进行线程切换(1表示可以,0表示不可以,默认为0)

testnum

功能

指令

1

测试双向链表

-q 1 -t 1

2

复现进程共享问题

-q 1 -t 2 -n 5 -y 1

3

复现覆盖问题

-q 2 -t 2 -n 5 -y 1

4

复现乱序问题

-q 3 -y 1

表 2 不同testnum的不同功能

实现双向链表

  1. 按照Nachos实验指导的3.1部分将双向链表的定义写入dllist.h文件中,并作适当的添加和修改:

DLLElement类:

class DLLElement {

public:

DLLElement(void *itemPtr, int sortKey); // initialize a list element

DLLElement *next; // next element on list

// NULL if this is the last

DLLElement *prev; // previous element on list

// NULL if this is the first

int key; // priority, for a sorted list

void *item; // pointer to item on the list

};

DLList类:

class DLList {

public:

DLList(); // initialize the list

~DLList(); // de-allocate the list

void Prepend(void *item); // add to head of list (set key = min_key-1)

void Append(void *item); // add to tail of list (set key = max_key+1)

void *Remove(int *keyPtr); // remove from head of list

// set *keyPtr to key of the removed item

// return item (or NULL if list is empty)

bool IsEmpty(); // return true if list has no elements

// routines to put/get items on/off list in order (sorted by key)

void SortedInsert(void *item, int sortKey);

void SortedInsert2(void *item, int sortKey);

void *SortedRemove(int sortKey); // remove first item with key==sortKey

// return NULL if no such item exists

void ShowList(int type);

DLLElement * getFirst() { return first; }

void setFirst(DLLElement *p) { first = p; }

private:

DLLElement *first; // head of the list, NULL if empty

DLLElement *last; // last element of the list, NULL if empty

};

  1. 在dllist.cc文件中根据定义写出具体的函数实现(这里只展示重要代码)

SortedInsert函数:

void DLList::SortedInsert(void *item, int sortKey) {

DLLElement *newnode = new DLLElement(item, sortKey);

DLLElement *ptr;

if(IsEmpty()) { // if is empty, newone is the only one

first = newnode;

last = newnode;

} else if(sortKey < first->key) {

newnode->next = first;

if(yield_flag && (testnum == 2 || testnum == 3)) currentThread->Yield();

first->prev = newnode;

first = newnode;

return ;

} else {

for(ptr = first; ptr->next != NULL; ptr = ptr->next) {

if(sortKey < ptr->next->key) {

newnode->next = ptr->next;

newnode->prev = ptr;

if(yield_flag && (testnum == 2 || testnum == 3)) currentThread->Yield();

ptr->next->prev = newnode;

ptr->next = newnode;

return ;

}

}

// insert to the tail

newnode->prev = last;

if(yield_flag && (testnum == 2 || testnum == 3)) currentThread->Yield();

last->next = newnode;

last = newnode;

}

}

SortedRemove函数:

void *DLList::SortedRemove(int sortKey) { // find the first elem that the key is equal to sortKey and remove it

if(IsEmpty()) return NULL;

DLLElement *ptr;

void *TB_return;

if(first->key == sortKey) { // if the first is equal to the sortKey, then delete it

first = NULL;

last = NULL;

} else {

for(ptr = first->next; ptr->next != NULL; ptr = ptr->next) {

TB_return = ptr->item;

if(ptr->key == sortKey) {

ptr->prev->next = ptr->next;

ptr->next->prev = ptr->prev;

delete ptr;

}

}

if(ptr->key == sortKey) {

ptr->prev->next = NULL;

last = ptr->prev;

TB_return = ptr->item;

delete ptr;

} else return NULL;

}

return TB_return;

}

  1. 在dllist-driver.cc中实现两个函数,其中一个函数需要插入N个元素,另外一个函数需要从双向链表的头部删除N个元素并将它们打印到终端。这两个函数的参数都是一个int整形N和一个指向双向链表的指针。

genItem2List函数:

void genItem2List(int n, DLList *dllist) { // generate n random keys and the dllist points to the list

int *item, key;

if(!seed) {

srand(unsigned(time(0)));

seed = 1;

}

for(int i = 0; i < n; i++) {

item = new int;

*item = rand();

key = rand() % NUM_RANGE;

printf("Insert %d into the list\n", key);

dllist->SortedInsert((void *)item, key);

printf("Insert %d into the list complete\n", key);

}

dllist->ShowList1();

}

delItemFromList函数:

void delItemFromList(int n, DLList *dllist) { // removes N items starting from the head of the list

for(int i = 0; i < n; i++) {

if(!dllist->IsEmpty()) {

printf("Delete from the head\n");

int keyval;

dllist->Remove(&keyval);

printf("Delete %d from the list\n", keyval);

} else {

printf("The list is empty\n");

return ;

}

}

dllist->ShowList2();

}

  1. 在threadtest.cc中另写一个函数,调用上述两个函数,验证代码的正确性,展示双向链表。

void DLListTest(int which) {

fprintf(stdout, "Insert items in thread %d\n", which);

genItem2List(oprnum, dllist);

if(yield_flag == true) currentThread->Yield();

fprintf(stdout, "Remove items in thread %d\n", which);

delItemFromList(oprnum, dllist);

}

  1. 运行结果

在终端输入./nachos -q 1 -t 1得到结果:

线程并发以及并发可能引起的问题

  • 首先,我们需要先了解线程切换的方法,强制的线程切换使用currentThread->Yield(),这里需要注意的是需要引用 system.h 文件。

currentThread定义在system.cc中,表示当前正在运行的线程:

调用Thread类中的Yield方法可以使得当前线程立即放弃所占用的CPU,使得其他可以运行的线程获得所需要的CPU资源从而执行其他线程下的任务:

  • 修改 threads/main.cc 和 threads/theadtest.cc 文件,实现可以创建 T 个线程(不同步),每个线程先插入 N 个元素,再依次移除并打印。所有线程共用一个链表。
  • 设计可能发生的线程冲突问题,并探究其发生的原因。

问题1:内存共享

并行执行时一个线程可能会修改其他线程正在进行操作的双向链表,比如一个线程还没有完成它全部应该完成的任务时调用了Yield将资源让给其他线程,其他线程就再次对这个链表进行操作,使得链表还没有被删除就再次被插入。

我们可以在代码中加入这样一行代码来模拟该种情况(红色为加入的代码):

void DLListTest(int which) {

fprintf(stdout, "Insert items in thread %d\n", which);

genItem2List(oprnum, dllist);

if(yield_flag == true) currentThread->Yield();

fprintf(stdout, "Remove items in thread %d\n", which);

delItemFromList(oprnum, dllist);

}

其中yield_flag是在threadtest.cc中定义的一个变量,表示是否可以强制切换线程,若值为1则可以,反之则不可以。通过在命令行中加入-y 1的参数可以改yield_flag的值为1。

但这种情况比较“善良”,并不会出现报错信息,只是会删除掉别的线程所生成的随机节点。运行如下指令,可看到结果:

./nachos -q 1 -t 2 -y 1

可以看到,第一个线程先插入然后紧接着第二个线程进行插入,最后是两个线程接连删除链表中的内容,最后链表中的内容会删空。

问题2:覆盖

并发的线程在链表的同一个地方插入不同的元素,导致其中那个先插入的元素被覆盖。

DLListTest2函数:

void DLListTest2() {

fprintf(stdout, "Insert items in thread %d\n", which); genItem2List(oprnum, dllist);

}

与问题1的区别在于,问题1是在整个插入操作执行完毕后进行线程的强制切换,而要复现出问题2的情况就需要在插入的过程中(即在新生成的节点与其他节点建立链接时)进行线程切换。

修改SortInsert函数(红色为起主要作用的代码):

void DLList::SortedInsert(void *item, int sortKey) {

DLLElement *newnode = new DLLElement(item, sortKey);

DLLElement *ptr;

if(IsEmpty()) { // if is empty, newone is the only one

first = newnode;

last = newnode;

} else if(sortKey < first->key) {

newnode->next = first;

if(yield_flag && (testnum == 2 || testnum == 3)) currentThread->Yield();

first->prev = newnode;

first = newnode;

return ;

} else {

for(ptr = first; ptr->next != NULL; ptr = ptr->next) {

if(sortKey < ptr->next->key) {

newnode->next = ptr->next;

newnode->prev = ptr;

if(yield_flag && (testnum == 2 || testnum == 3)) currentThread->Yield();

ptr->next->prev = newnode;

ptr->next = newnode;

return ;

}

}

// insert to the tail

newnode->prev = last;

if(yield_flag && (testnum == 2 || testnum == 3)) currentThread->Yield();

last->next = newnode;

last = newnode;

}

}

在终端执行./nachos -q 2 -t 2 -y 1 -n 5表示执行DLListTest2()函数并开启两个线程同时往双向链表中插入5个随机数,设置线程可切换,运行结果如下:

可以看到,两个线程共往链表中插入了10个元素,但是最终打印出来的结果只有6个,从而复现了元素覆盖的情况。

问题3:乱序

两个线程在同一位置插入元素,可能导致顺序颠倒。

DllistTest3函数:

void DLListTest3(int which) { // out of order

fprintf(stdout, "In thread %d\n", which);

if(which == 0) {

InsertItem(which, dllist, 1);

InsertItem(which, dllist, 10); // Here to switch thread

InsertItem(which, dllist, 0);

InsertItem(which, dllist, 5);

PrintList(which, dllist);

InsertItem(which, dllist, 3);

InsertItem(which, dllist, 7);

PrintList(which, dllist);

} else { // which == 1

PrintList(which, dllist);

InsertItem(which, dllist, 3);

PrintList(which, dllist);

}

}

InsertItem函数:

void InsertItem(int which, DLList *dllist, int keyv) {

fprintf(stdout, "Thread %d : Insert %d to the dllist\n", which, keyv);

dllist->SortedInsert(NULL, keyv);

}

为了能够保证乱序情况的出现,DLListTest3中的代码使用了固定参数的节点,同样线程切换的位置发生了变化,即新插入的节点的前驱指针和后继指针都已连接完毕,而指向新插入的节点的指针还未连接时进行线程转换。

运行结果:

在终端输入./nachos -q 3 -y 1可以看到

  • 线程1 想将元素3插入双向链表时,线程0还未将元素10完全插入双向链表,只是将元素10 的前驱指针和后继指针都指向了正确的地方,但是还没有指针指向元素10。

  • 线程1插入元素3时发现元素3应该插在元素1的后面,并且和元素10一样,插了一半就换到了线程0。

  • 换回线程0后,线程0继续完成未做完的工作(将该指向元素10的指针修改正确),于是把元素1的后继指向10。

  • 在线程0插入下一个元素插入到一半的时候,又切换回线程1,线程1继续将没插完的元素3插入双向链表,它会将元素10的后继指向元素3。于是造成了乱序的现象

遇到的问题

  • 一开始不太理解实验要求,像无头苍蝇一样不知道该如何进行实验。
  • 双向链表不熟悉,刚开始建立和将双向链表打印出来的函数写得都有问题,于是我上网查阅资料,并简单地学习了gdb地用法,成功找出了BUG,顺利完成了本次实验。
  • 看网上有资料说还有可能出现断链地情况,但是我一直没有办法复现出来,或者说其实已经出现了这种情况,但是我并没有发现它出现了,于是就没有在报告中体现这一部分地内容了。

实验总结

  • 本次实验我学习到了简单使用nachos系统的方法,本次实验只涉及到线程,所以通过修改threads里的文件编写实现目标功能的函数,并运行,观察结果并分析。
  • 简单了解了编写Makefile的语言规则。Makefile可以将编译众多文件的命令汇集到一个文件了,使用make命令执行Makefile,十分方便。
  • 对于线程方面,主要想说的就是currentThread->Yield(),这个函数实际上就是让当前线程暂时放弃资源,去执行另一个线程的任务,当另一个线程完成操作之后,原线程再继续执行后续的操作。
  • 本次实验中的线程并发本质上还是几个线程之间的切换,因此代码的执行顺序就非常重要,对于测试情况的设置就是基于对执行顺序的考虑,测试在不同位置切换线程可能造成的后果。

本实验完整代码均在gitee上,后续实验代码也会更新:

https://gitee.com/i-rong/nachos

标签:操作系统,void,dllist,并发程序,线程,newnode,Nachos,next,ptr
From: https://www.cnblogs.com/i-rong/p/17280921.html

相关文章

  • Golang 需要至少 5 个操作系统线程
    Golang需要至少5个操作系统线程主线程:Golang代码执行的入口点,负责初始化程序,并启动其他Goroutine。垃圾回收器线程:Golang内置了垃圾回收器,使用专门的线程来执行垃圾回收操作,回收不再使用的内存空间。CPU核心数个系统线程:每个核心需要一个系统线程来支持并发任务的执行......
  • 《30天自制操作系统笔记》---第一天
    第一天第一个实验:用二进制写一个显示helloworld的“操作系统”使用工具:1、HxD-二进制编辑器2、qumu模拟器下载了HxD–二进制编辑器编辑好了书上的二进制程序helloos0.img然后按照书上写了bat脚本。Install脚本:用来制作系统启动盘Run脚本,用来启动qumu模拟器运行。不过......
  • linux操作系统实验四-以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34
    一、搭配环境(1)安装编译工具sudoapt-getinstallgcc-aarch64-linux-gnusudoapt-getinstalllibncurses5-dev build-essentialgitbisonflexlibssl-dev(2)制作根文件系统wget https://busybox.net/downloads/busybox-1.33.1.tar.bz2tar-xjfbusybox-1.33.1.tar.bz2......
  • 企业实践 | 如何在阿里云裸金属服务器上使用UEFI模式实践安装国产银河麒麟V10操作系统
    [点击......
  • 如何将Windows操作系统用户名的中文名称修改为英文名称【亲测有效】
    前言最近电脑重新安装nmap,但是图形化界面无法运行,如图所示:是因为用户名称中存在中文字符。接下来就亲自实操一下如何将Windows操作系统用户名的中文名称修改为英文名称。一、控制面板修改电脑名(1):桌面左下角搜索框搜索控制面板(2):打开控制面板,点击用户帐户下的更改账户类型(3......
  • 03操作系统发展历史3.31
    操作系统发展历史手工操作系统:用户独占全机cpu等待人工操作增加了外存,先把要处理的数据存到外存中,减少CPU等待时间,提高工作效率也称脱机操作方式一次只能执行一个程序批处理阶段:单道批处理:自动,顺序排队进入监督程序,然后依次运行内存中只能一个程序运行,一......
  • 结合 操作系统、Java多线程 学习并发编程
    为什么我们需要考虑并发?不考虑的话会出现什么问题?并发的多个程序(进程/线程)会对计算机资源进行争夺,如果不加以控制会出现混乱、严重影响程序运行效率,甚至错误首先是对CPU时间片的争夺对于多线程编程而言,由于创建线程后,线程的执行顺序是由调度程序控制的,也就是说各个线程的执行顺......
  • OpenCloudOS 9.0 发布:首个全自研服务器操作系统
    系统开源社区OpenCloudOS正式发布首个全自研社区9.0版本(以下简称OC9.0)。据了解,该版本由腾讯等十余家企业共同开发并长期维护,其内核及用户态软件均为自主选型、独立演进,在操作系统发行版的全链路均实现自主可控。​操作系统等基础软件是信息技术的根基,也是亟需实现突破,掌握......
  • 企业实践 | 国产操作系统之光? 银河麒麟KylinOS-V10(SP3)高级服务器操作系统基础安装
    [点击......
  • 操作系统01.3.29
    操作系统概述操作系统的基本概念操作系统(OperationSystem),简称OS,是管理硬件和软件资源的计算机程序。操作系统有很多,比如Windows、Linux、macOS、Unix、andriod、i......