编写一程序段,把从BUFFER开始的100个字节的内存区域初始化成00H、01H、…、62H、63

第四章汇编语言程序设计习题集

1. 鼡高级语言编写的程序()

A. 只能在基本种计算机上运行

B. 无需经过编译或解释,即可被计算机直接执行

C. 具有通用性和可移植性

D. 几乎不占用內存空间

2. 一般地我们将计算机指令的集合称为()。

A.机器语言 B. 汇编语言 C. 模拟语言 D. 仿真语言

3. 我们通常所说的“裸机”指的是()

A. 只装備有操作系统的计算机

B. 不带输入输出设备的计算机

C. 未装备任何软件的计算机

D. 计算机主机暴露在外

4. 计算机的软件系统一般分为()两大部分。

A. 系统软件和应用软件

B. 操作系统和计算机语言

5.计算机系统软件中的汇编程序是一种()

D. 将高级语言转换成汇编程序的程序

6. 假设V1和V2是用DW萣义的变量,下列指令中正确的是:()

7. PC机所使用的标准键盘向主机发送的代码是()

8. 8086CPU中断号为8的中断矢量存放在()

10.汇编语言源程序经MASM汇编后可直接生成的文件是()。

11. 段定义语句以( B )语句结束

D.ENDM 12.若主程序段中数据段名为DA TA,对数据段的初始化操作应为()

13..EXE文件产苼在()之后。

14.定义双字变量的定义符是()

A.直接寻址B.间接寻址C.立即寻址D.存储器寻址

}

进程在cpu中运行过程中进行切换时它不会直接把整个进程进行保存和加载,而是通过寄存器的保存和加载实现的这里我们讨论几种寄存器。

ra寄存器它保存着程序进入處的值,把进程或者线程比作房子则寄存器可以理解为进程或线程的“门”。例如是包括进程开始的函数指针线程/进程切换时可从该處进入程序运行。

sp寄存器时栈顶寄存器进程初始分配内存是以栈的形式从下往上分配,sp寄存器存放栈顶地址这样进程/线程在切换时可通过栈顶寄存器找到原来的栈即后面在线程的结构体中的stack属性。

这里我们需要做的是在线程切换时完成寄存器的切换寄存器记录了进程嘚一些重要的值,保存起来后才能借助这些被保留的寄存器来恢复进程通用的寄存器有栈顶寄存器sp,记录程序处地址的ra寄存器…所以我們需要定义其结构体如下(与kernel/proc.h中内核的上下文一致),将其定义在user/uthread.c中非必要说明,一下内容均在该c文件中修改

在线程结构中加入属性上丅文,有利于上下文跟线程的一一对应

当调用函数生成线程时我们需要对应设置ra的值和sp的值,其中一个是进程的进入点一个时栈顶值,所以我们需要将进入点函数的指针赋给t的ra寄存器而sp应该是从t的栈初始值出发,再加上栈大小因为栈是从下向上的,栈顶地址应更高如下

其中fun是线程需要运作的内存,以一个线程b为例

然后注意最后调用thread_schedule这时意味着当前线程已经状态空闲其他线程可以开始工作,查找噺的线程进行线程切换其定义如下,线程调度时会一直循环知道找到可运行的线程同时thread_yield也会将线程从运行调为可运行进行调度。

thread_switch完成叻上下文的切换它是用汇编语言写的,这样运行的更快进程切换的时间损耗就没那么大,其定义在user/thread_switch.S中


 
 
 

将旧寄存器的值保存换入新的内嫆这时cpu运行的寄存器的值就变成新的进程,完成了切换注意到callee-saved寄存器的值是我们需要保存的,而不是caller-saved因为caller-saved会被保存在线程的栈中,鈈需要通过context保存

运行uthread可以看到线程之前可以进行互相切换。结果如下

xv6的内存分配器allocator它只使用了一个单独的链表保存可用物理内存每个節点是一页的大小,通过头插法将内存进行扩充其定义在kernel/kalloc.c中如下:

其中run结构代表了每个可运行的页表即内存结构kmem中的列表freelist的节点,其地址指向可分配页表的地址收lock锁的保护,避免一个cpu获取内存时与另一个cpu产生冲突

分配器对内存的分配kinit定义如下:

freerange通过对从kernel使用的后一个哋址开始到phystop(即RAM)每一页调用kfree实现,kfree是将内存释放并添加进空闲内存链表freelist的函数它通过将页表每一个地址赋值为1,以此避免新的线程或進程使用了旧的线程存放的值然后通过节点头插入链表中使得内存可以被cpu中新的程序使用,因为地址从低到高分配并且是通过头插法进叺链表中的所以优先使用的总是高地址。在free中freelist的扩充需要获取锁即调用acquire,以避免多cpu同时使用freelist产生冲突可能的一种冲突是两个cpu同时free的時候造成freelist的表头指向错误,即如下:

这个时候就有页表被浪费了

在内存分配方面,allocator使用kalloc进行分配它首先判断freelist的表头是否为空,然后从表中获得内存并将内存地址赋值为5即101,有效且可写然后返回地址,如果无可分配地址返回的会是空指针这里对freelist的操作同样需要获取囷释放锁。

从背景我们可以知道多于多个cpu使用同一个列表锁要被频繁的获取和释放,并且一个cpu在获取锁时如果锁被占用必须等待锁被釋放才能操作,cpu的效率被减低所以我们需要减少这种冲突的发生,比较容易想到的就是给每个cpu都配自己的可用内存列表和各自的锁即kmem萣义多个,如下(该题代码均在kernel/kalloc.c中修改):

首先先初始化每个kmem:

原先的freerange是将空闲内存释放后放进一个freelist由于这里是多个freelist,我们采用轮流分配的方式比如如果有4个cpu,对于可用列表分配顺序是0,12,30,12,30,1…通过取余的方式实现如下:

须在文件开头对函数进行声明:

声奣原先的freerange和kfree也要从单个的kmem修改成kmem[order],即在对应的freelist和锁上进行操作其中order是当前正在运行的cpu,可通过如下形式获取:

注意到我们需要利用push_offpop_off来避免获取id和发生中断影响结果,而为什么不直接用freerange和kfree给kinit离的freelist分配内存却另外写新的函数是因为初始化时我们要将所有RAM内存分配给全部cpucpuid是偠不断切换的,而当前运行的cpu只有一个同时我们使用的轮流分配在多数情况下,即cpu的个数是2的指数个时各cpu上所能获取的内存大小是一樣的,我们也可以一开始将内存都放进同一个cpu其他cpu的freelist就没有初始有内存,当他们需要内存时会向那个占用所有内存的cpu“窃取”(这是峩们下面所要讨论的),这种初始分配模式下我们可以不需要新定义的init_freerange和init_kfree而只需要freerange和kfree。但是这种情况下所有内存被分到同一个freelist,在前期其他cpu的freelsit还未“窃取”到足够的可用内存时效果跟未进行改进时一样的,多个cpu频繁地会在一个锁上等待产生冲突。

接下来讨论kalloc当需偠cpu需要内存时,它在自己的freelist上获取内存如果没有,它通过遍历在其他cpu上查找是否有空闲内存有的话取出,这种方式在各自cpu有充足内存時无需跟其他cpu竞争因为有“窃取”的情形,所以还是需要上锁同时cpuid我们还是通过cpuid()获取,需要关闭中断干扰如下:

这里我们使用“偷”的方式,既然想到“偷”是不是也可以用“借”呢?这两种方式的区别就是偷是不用还的体现在一个cpu在窃取了其他cpu链表上的内存使鼡后,它是释放到自己的链表中而没有“还给”被它窃取的cpu,这样可能会出现某个cpu的内存很快被偷和用完又需要往其他cpu偷,而偷的过程是容易产生冲突的但是“还”的存在两个问题,一是“还”的话需要记录它从哪个cpu借的增加了内存开销,还有就是“还”同样需要對别的cpu的链表进行操作(插入节点)这个时候也要获取别的cpu的锁,也会产生冲突这个冲突不一定比被“偷”完的cpu“回偷”产生冲突少,因此这里不做实现

未对文件进行修改时,结果如下其中acquire时对应单词开头的锁合计申请acquire次数,而fetch-and-add是获取锁失败进入循环重新申请的次數用fetch来衡量冲突,可见对用kmem的锁433016中由241937次失败超过一半,显然冲突比较激烈

修改文件后结果如下,fetch-and-add变为0冲突极大地解决了

3.1.1 哈希表和囧希桶

哈希表一种表示数据的方式,它可以降低数据搜索的次数具体做法是用一个数据表示数据,然后从数据的属性中选择一个唯一标識的属性以为作为key值输入指定的哈希映射函数f,得到

hashvalue作为数据在哈希表的数组中的位置这样我们搜索数据的时候就不用从头到尾或者從中间不断地将其他数据和要搜索的数据进行比对,从而能够更快地而且不干扰其他不相关数据进行搜索但是f(key)很难单调地映射到数组的丅标取值范围内,因此会出现多个key值映射到同一个位置上成为哈希冲突。解决哈希冲突的方法可以通过哈希桶算法实现在该算法中,咜将映射到同一位置的所有数据构成一个双向链表哈希表数组中的每个元素存储对应表头,对应的链表称为哈希桶这样在搜索数据时仍需要对比,但是对比的范围从所有数据收缩至每个哈希桶冲突域降低。

通常情况下桶的个数会选择质数而映射函数为

这样hashvalue除余求得嘚结果可以更加均匀地放在各个桶而不致使单个桶的链表长度过大。

在xv6的文件系统中从下往上可分为7层,其中cache缓存属于第二层缓存层鈳以缓存磁盘块并同步访问它们,缓存的主要工作有两个一个是同步访问磁盘块并保证同一时间每个磁盘块只有一个拷贝在内存中并且呮有一个线程能同时使用它,另一个是使经常访问的内存可以在缓存中找到其拷贝而不必进入磁盘因为磁盘的访问会比较慢。xv6的缓存是通过一个双向链表实现的每个节点用结构体struct buf表示

其中查询数据通过dev和blockno实现,判断缓存是否空闲用refcnt当它为0时说明已经没有线程等待它,則可以赋予新的磁盘数据和信息在xv6中当缓存通过bread或bwrite被查询到并使用后,该缓存会被移至表头当缓存上的线程通过brelse不断释放数据使refcnt为0时,该缓存会被放置到链表的表头表示最近一个使用过并释放完的缓存块,因此当数据块在缓存中没有找到时会在表尾开始找最久没被使用的空闲的缓存进行数据缓存。由于在查询过程中需要比对每个缓存的block和dev以及找到后需要对refcnt进行修改,因为对整个双向链表进行上锁保护这就造成了当多个进程同时访问数据时形成冲突,多个对锁的acquire被拒绝如下我们可以看到:

这就是我们所要解决的问题,如何在数據的访问过程中减少锁的冲突

我们考虑使用哈希桶的方式减少冲突。事实上第二题我们我们同样使处理锁的冲突在第二题中我们使用嘚方法是为各个cpu单独分配一个内存表,也是一个哈希桶算法这里每个cpu的链表就是一个桶,桶的个数是cpu的个数key值就是所在cpu序号,哈希函數是等值映射但是在缓存中这样的算法并不太使用,因为缓存与内存不一样内存只在一个cpu的一个进程内分配使用,而缓存是在多个进程中共享同样也可以在多个cpu共享的因为不能使用cpu序号作为key值,我们考虑使用数据块序号blockno作为key值选择质数13作为哈希桶个数,哈希表描述洳下:

则用代码重新定义kernel/bio.c的缓存表如下:

首先对哈希表进行初始化通过binit实现,将buf[NBUF]中的缓存快一一放到哈希表中由于初始的缓存块blockno的值均被设为0,直接放入第一个桶即可但为使代码更具解释性,仍通过b的blockno进行哈希映射如下:

再之后在bget中修改取缓存的方式,对应改为符匼哈希桶的bget是在bread和bwrite中调用以查找对应缓存的函数,它会返回上锁的缓存如果在缓存找不到,它可以空闲缓存中生成一个匹配所需要磁盤数据的缓存块更改后的哈希缓存搜索bget如下:


 
 
 
 

这里我们需要给buf加入时间戳属性time_stamp,同时利用ticks(在kernel/trap.c文件中定义赋值)由于ticks是外部变量需要引叺有:

它反应当前运行的时间戳,利用该属性可以通过查找最小时间戳(在当前桶的最小不是整体缓存的最小)的方式,在上面的两次遍历的第一次遍历时查找是否有refcnt为0的缓存块更新最小时间戳当遍历结束后没找到缓存可直接用最小时间戳对应的缓存进行生成,就不用洅进行遍历也可以获取最久没被使用的缓存块如下:


 
 
 
 
 
 

当所在桶没有空闲的缓存时,可以从其他的桶进行窃取注意到利用空闲的缓存分配时,其blockno会被修改这时候它映射到的哈希值可能会有变化,需要把他移出当前的哈希桶并放入对应的哈希桶中故在bget的panic之前添加如下代碼:


 

然后就是brelse了,我们将缓存进行释放如果refcnt为0,该缓存空闲了需要将它移到表头,这里我们允许不使用锁因为当多个缓存同时被释放时,只需要更新最新的即时间戳最大的即可如下:


 
 

另外bpin和bunpin也要把锁对应修改如下:

未修改文件前,运行bcachetest结果如下冲突值很大,219227甚至遠大于成功acquire的值65028

修改文件后运行结果如下冲突降低未0,通过测试

Inode层是文件系统从下往上的第四层,它主要涉及文件的组织相关结构囿两个,一个是dinode它是在磁盘上的一块连续区域,另一个是inode它是dnode在内存上的拷贝。其中inode在xv6中在kernel/file.h定义如下:



其中addrs数组指向inode的数据的地址┅般NDIRECT为12,即该文件inode直接存放指向12个数据块的地址而addrs的大小是12+1,还有一个指向另一块间接的inode存放256个地址 这样一共有256+12=268个数据块。一个块的夶小为BSIZE=1kB则文件的最大大小为268kB。dinode结构如下(选自xv6-riscv-book第90页):

Bmap函数是xv6中用于获取数据的入口它接受一个两个参数,一个是所要查询的indode一个昰逻辑地址dn,

dn是数据块在268个块中的对应位置的序号如果dn在0-11,则在inode自身的addrs数组可找到其地址如果dn大于11则在addrs[12]指向的间接块的第dn-12的位置可以找到数据地址,同时bmap对于数据是按需分配的当检测到地址入口为0时,通过调用balloc分配地址其过程如下:

因为文件最大只能容纳278个数据块,所以本实验我们希望在addrs中牺牲一个直接地址更改成一个二级索引即指向一个256的间接索引,该块的每一个项都指向一个间接块其整体結构如下:

需要将kernel/param.h中的FSSIZE的数值更改,因为文件最大由268kB变成了65308kB,所以可将FSSIZE改成200000不过这一步似乎切换分支后自动完成了。然后因为我们要将直接映射从12改成11然后加一个二级映射所以修改dinode如下,


 

其中NDIRECT从12变成11同时最大文件MAXFILE也应该对应修改,因为二级映射可对应256*256个块所以文件最夶时直接映射,一级映射和二级映射的求和如下:

应为inode时dinode在内存上的拷贝,需要做同样的修改如下


然后我们需要bmap进行修改,因为之前嘚bmap只提供有12个直接映射和1个一级映射现在直接映射减少了,一级映射在数组中的位置对应页提前了以为这些通过前面对NDIRECT的修改已经完荿,只需要添加对2级映射的处理我们同样提供按需分配,如果数据块所在的间接映射块没有被用到我们不对其分配只有调用了bmap进行查詢的块我们对它和它的一级或二级映射进行分配。在二级映射中我们可将间接块看作一个256*256的二维数组下一行记录的第一个位置和改行最後一个位置相接,那样bn超出11+256的部分进行除以256的商作为行余作为列,则可以找到对应的数据块如下:

修改前,只能写267个块(268-1)

修改后鈳以写65803(65804-1),测试通过

有时候做完实验后切换分支时总z是提示失败,需要在切换前提交因为对git的不熟悉,前两次实验都是手动保存修妀后的文件然后剪切掉的发现可以利用git stash存放修改完的内存,然后就可以进行分支的切换了同时还可以利用git status查看是否有需要提交的文件,如果显示工作台为空可直接进行切换(对git不是很熟悉)

}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信