通过课上的例子我们可以看到,对于Sheep -> Maybe Sheep这样的操作的序贯组合,使用朴素地匹配Nothing会导致代码冗余且可读性降低。为此我们想到可以将对Nothing的处理放入函数内部处理。但对于所有的函数都进行类似的修改也是极其不方便的,因此我们将这一类操作抽象出来,放在>>=里实现。此时Maybe也由Applicative进化为了Monad。
详细了解Monad需要对范畴论有一定的了解。正如haskell圈里那句经典的”A monad is just a monoid in the category of endofunctors, what’s the problem?”。对于有范畴论背景知识的人来说,从“自函子范畴上的幺半群”来理解确实可能更为自然,但对于小白与初学者来说,补充范畴论的相关知识确实也是一件代价高昂的事,因此我们用haskell的语言来了解这个概念,等到有需要的时候再去更深入的了解。
为了更深入的了解haskell中Monad这个类型类,首先我们使用:i Monad
查看其具体的定义如下
可以看到其依存于Applicative,我们一步步溯源回去见到了Applicative和Functor
我们首先直接解读Monad的定义
也就是说,Monad在Applicative的基础上新增了bind操作。为了理解这个引入的好处,我们需要整体梳理一下这三个类型类的作用。
(注:以下的论述中,我将a -> f a叫做类型的提升,也有不同的叫法与理解如上下文、环境、盒子等等。对于Maybe、List这样的Monad,我个人觉得类型提升的角度比较好理解,而对于Writer Monda,State Monad等,上下文这种理解可能更加直观。然而他们完全是一样的)
Functor有一个好:fmap将a -> b提升到了f a -> f b上,使我们不必为每一种类型地提升都去特定地去迁移已有的函数。
Applicative有一个好:它使得多元函数(而非简单的a -> b)也得以被提升。这是因为
当然,若是没有<*>的实现,使用liftA2的效果是一样的。
通过这个例子我们可以看到,Applicative其实是Functor的一个多元拓展。
而Monad的好我们在开头已经看到了,它通过>>=将序贯操作组合起来,将类型的匹配与变换规则写入>>=的实现中,使得我们不用在序贯操作中频繁的进行类型的匹配与检查。
总而言之,三者分别实现了这样的功能
fmap (+1) (Just 1)
中,+1
这个一元函数从Int->Int
提升至Maybe Int->Maybe Int
,然后接受Maybe Int
并返回计算后的Maybe Int
pure (+) <*> (Just 3) <*> (Just 5)
,+
这个二元函数从Int->Int->Int
提升至Maybe Int->Maybe Int->Maybe Int
,然后接受Maybe Int
和Maybe Int
并返回计算后的Maybe Int
Monad允许参数间的相互影响,这是Applicative所无法做到的。一个典型的例子就是List Comprehension,即
这个特性也算是Monad对于Applicative的一个优越性:计算的值可以依赖于前面的结果。
提升类型有什么好处呢?Maybe或者List这样的提升是很自然也很实用的做法;而另一大想法就是利用其存储额外的信息/状态。而且这使得副作用得以实现(并被很好的隔离开来)。典型的应用如Writer Monad,State Monad, IO Monad。这些在后续的课程中都会有更详细的学习,在这里就先不做过多的介绍了。
简单说,范畴论并不关心具体的函数对象,而关心对象间的态射及其组合。一个范畴由一组对象和一组态射组成。对于任意两个对象$A\rightarrow B$都存在一个态射的集合$C(A,B)$。而这些态射需要满足复合运算、结合律并对任意对象$A$在$C(A,A)$中存在单位态射。
而Functor本质上是范畴间的态射。一个Functor将范畴$C$里的对象和态射射映射到范畴$D$上,并且保留单位态射与态射间的复合关系。借用Mabye来思考这个问题是有助于理解的。
自函子则是范畴$C$到其自己的函子,并且自函子可以自身复合。
让我们再回头看开头的名言:单子是自函子范畴上的幺半群。我们先想象一下范畴上的幺半群:它只有一个对象$A$,并且有无数$A\rightarrow A$的“自环”——也就是态射,而这些态射的复合就是这个幺半群的运算。现在让我们把对象$A$设定为一个范畴$C$,而这些态射就是所谓的自函子。这些态射中由一个单位自函子$I$,一个自函子$T$及其若干阶复合$T\cdot T,T\cdot T\cdot T,\ldots$。这上面存在着两个自然变换(可以理解为函子到函子的态射):一个是$I\rightarrow T$,对应着haskell里的return。另一个是$T\cdot T->T$(在范畴论里这称做join). 在haskell里面我们并没有看到这个操作,但事实上它可以由>>=实现。
|
|
如果使用之前盒子的比喻的话,二者一个进行着套盒子的操作,另一个进行着脱盒子的操作。此时Monad作为自函子的幺半群的性质就显现出来了。
]]>经历了跨时一个月的两次迭代,我们的大学生竞赛(报名)平台的框架已经搭起来,并实现了基本的创建比赛、组队参赛,载入成绩等基本功能,基本达到了中期展示的要求。而同向对比,与之最接近的就是大二小学期里的django作业了。因此本文主要以对比这两次开发为脉络,主要谈谈自己的一些感受,而非一篇介绍相关技术的博客。这可能会在期末结束后做一次相关的总结。
我个人两周一迭代的开发压力其实还是蛮大的。而在开发软工大作业的过程中时不时穿插着其他课程的小作业大作业。再加上中间两周的期中考备考压力,确实有一段时间确实是疲于奔命,基本没有喘息的余地。相对来说小学期的作业相对轻松不少,虽然时间不长但没有其他的压力。不过再熬过这一段小高潮后也相对也轻松一些,但很快又要进入下一次迭代的开发了(中期展示完的周末就是第三次迭代会议的召开之时),整体的安排还是特别紧凑的。
可以看到这次的软工大作业其实是跨时八到十周的,因此才会采取迭代式开发。那么在这样一个长的周期里面,如何合理安排进度也是对我们的一个考验。安排得不合理,将有可能导致期末开发压力极大,甚至无法上线稳定的经过测试的版本就要草草提交。如此的话这个项目就是彻底失败的。
小学期的项目虽然也是组队开发,但三个人的小组和现在五个人的小组还是有些许不同的。小学期项目中我们三个人我负责前端的页面渲染,另外两人负责后端的两个大模块。其项目本身就易于解耦,前后端的数据接口也没有这次的复杂,因此开发起来相对轻松很多。而这次的开发中两前端两后端加一个配置集成开发环境,因此。尽管做了解耦,但仍会遇到这样或那样的bug需要沟通联调。而沟通难度和成本其实随着组的增加也是在上升的。因此这一次的五人组开发其实也是一个挑战。
不过好在我在一个特别nice的组。每当召开迭代会议和集体开发大家都能到齐而不用催,出现bug的时候大家的心态也都比较平和,相互review代码。
当然,刚开始的时候仍然对于使用gitlab做团队管理等不太熟悉,包括issue的划分,时间的估计等等。而我们组也在不断尝试适合我们组的开发方式。因此开发的过程虽然磕磕绊绊,但也逐步形成了我们组内的一套开发流程(比如共同维护一份接口文档等)。
还有一点需要提的,就是我们这次的开发自然也是需要做单元测试、功能测试乃至性能测试的。同时开发的时候也需要部署相关的集成开发模块,这都和以往开发的小项目有着很大的不同。
最开始的时候我们前后端是口头约定了几个模型的字段。但后来觉得不够,就由后端写出一个极其简陋的示意网页告知前端。最后我们决定由前后端共同在hackmd上维护一个接口文档,有任何需求都在文档里体现出来并交由对方实现。
另一个困难就是项目刚开始的时候完全没有成型,许多前后端的typo和bugs,以及接口的细微差别都难以直接检出。因此我们决定在集体开发的环节就留出一定的时间进行前后端的联调和简单的debug,而在私下的时间则是开发具体的功能。尽管前两次在联调时花费的时间比较久,但如今项目已经基本成型,以后在这方面消耗的时间将会大幅度减少。
小学期的项目基本是交由我们决定上线的网站支持哪些功能,并根据具体的情况进行增删。而这次我们则是真正面对着甲方。因此在每两周的定期迭代检查中,我们也与甲方进行深入的交流,了解甲方的需求,及时修正我们的设计。比如在第一次迭代会议后,我们发现甲方对于比赛多阶段的需求是非常强烈的,为此我们也放弃了之前对于比赛的一些设想,重新规划部署了相关模型。而在这样一次次的交流中,我们对于产品的需求和定位也就越来越清晰,这有助于我们今后的开发工作。当然我们也不是一味的听从甲方的安排,对于一些实现起来确实有困难,以及有更好的设计方案时,我们也和甲方积极沟通,争取达成共识。
其实这次的团队开发确实是一次很珍贵的开发经验,至少比起之前的项目都更加地与现实接轨。
]]>这是大三上学期汇编语言课的大作业,参与者有我,CalciferZh,和IrisYGL 。我们的选题是对过气游戏CS1.6 (Esai 3248版)进行外挂的编写,主要涉及到利用反汇编、注入dll等技术进行游戏的修改。
我们知道,每个dll文件里都有一个dllMain
的入口,可以在那里设置当dll被process
/thread
进行attath
/deattach
等操作时执行的操作。那么我们编写我们的外挂代码,然后将其注入进cstrike.exe
中就可以实现许多作弊功能了。
至于具体的注入方式,我们采取了远程创建线程注入的方式。具体的过程和解释如下
LoadLibraryW
的地址,由于所有win32进程都加在了kernel.dll,因此该函数在所有进程的内存位置都是一样的,这为我们提供了方便LoadLibraryW
,参数自然就是之前写入诸如目标内存空间的dll路径名至此,dll已经被成功注入,并执行了相关的操作。在这里我们执行的就是下面介绍的hook操作
我们知道,cs1.6有opengl模式,也就是通过调用glBegin等函数进行人物等模型的绘制。那么在执行相关操作之前执行我们想要的操作就可以实现相关的hack。比如说,如果识别出正在绘制烟雾弹,直接终止绘图操作就能够实现反烟雾。如果正在绘制人物的模型,那么关闭深度检测,或者将人物的深度拉到很近也就能实现透视的效果了。
因此目前的核心在于,如何在游戏调用相关的函数的时候,先行调用我们插入的函数,并不影响原函数的正常运行。
|
|
为了帮助理解这一段代码的功能,我画了一张草图
注意到下列事实会有助于理解
entry
处执行,因此我们需要在entry
处jmp
到我们预先执行的代码target
jmp
函数会覆盖原有的指令,我们需要对其进行备份,并且留有接口能正确的执行原函数new_entry
new_entry
的地址,以备target
里使用entry
处的汇编指令情况,找到len长度的字节,保证len大于等于5且不会打断指令关于len的示意图如下,hook这个函数是len需要设成7(或者更大,但不能打断指令)
同时为了方便理解,放上相关的示例代码
|
|
上述代码就可以实现在执行glVertex2f
时,先跳转到我们的Hooked_glVertex2f
函数执行,而pglVertex2f
就是原函数的功能备份。当然如果判断完不需要绘制(比如烟雾)的话,直接return也就可以了。
在使用IDA和CE修改器等工具后,我们可以发现一个核心的dll——mp.dll。绝大多数的游戏相关的函数都存在里面,这也是我们攻击的重点。
拿到游戏里人物、枪械的数据是我们实现hack非常重要的一步。这里我们使用CE修改器帮助我们完成相关的操作。这里以生命值(HP)为例来说明我们如何定位关键数据的内存地址。
我们首先在游戏中通过各种方式反复改变自己的HP,同时利用CE监控内存变化。如果内存中的某个数据总是和我们的HP保持一致,那么我们就可以认为这个地址很大概率上储存了玩家的生命值。但在实际操作中,HP的值往往在内存中存在多个副本,还需要逐个分析筛选,找到唯一的记录HP的内存地址。
但是即便我们这里得到了一个地址,这个地址也不会是一成不变的。在重每次新运行进程的时候,进程的地址都有可能变化,因此我们需要把获得的绝对地址转化为相对于进程的偏移地址。具体来说,我们一级一级寻找它的指针,直到这个地址位于游戏主进程cstrike.exe里面。一个基本的步骤如下
至此,我们就成功找到了hp上一级的地址。重复上述操作直到该地址不再是绝对的内存地址,而是cstrike.exe + offset的形式时,我们就找到了hp的正确访问方式,而不用担心进程加载以及malloc的时候开辟空间的不同造成的影响。
同样的,我们也对其他容易观测并修改的值进行了定位,并依靠其获得了许多有用的数据如下
地址 | 描述 |
---|---|
[[[cstrike.exe+11069BC]+7c]+4]+160 | 玩家HP |
[[[cstrike.exe+11069BC]+7c]+4]+16C | 玩家是否可以受伤 |
[[[cstrike.exe+11069BC]+7c]+4]+1BC | 玩家护甲 |
[[[cstrike.exe+11069BC]+7c]+4]+8 | 玩家坐标 X |
[[[cstrike.exe+11069BC]+7c]+4]+C | 玩家坐标 Y |
[[[cstrike.exe+11069BC]+7c]+4]+10 | 玩家坐标 Z |
[[cstrike.exe+11069BC]+7c]+1CC | 玩家金钱 |
[[cstrike.exe+11069BC]+7c]+3C4 | 到达特定区域(包点、购枪点、人质点)会更新 |
[[[cstrike.exe+11069BC]+7c]+5EC]+CC | 玩家当前武器子弹 |
其中像X,Y,Z坐标的信息是难以通过搜索内存定位的。但从已经确定的如玩家HP,护甲等信息,我们可以猜到[cstrike.exe+11069BC]+7c]存放着玩家的相关信息,而[[cstrike.exe+11069BC]+7c]+4]里面存放的就是和玩家物体直接相关的信息。那么我们通过对[[cstrike.exe+11069BC]+7c]+4]里所有的offset进行监控,也就能定位出这样一些难以直接定位的数据。
在找到关键数据后,我们需要找到修改这些数据的函数语句。我们首先使用CE监视修改关键数据的语句,这里往往会得到很多条;然后需要逐条查看反汇编,进行筛选,最后找到最核心的语句。这样我们就拿到了语句相对于动态链接库起始位置的地址。由于动态链接库在内存中的加载地址并不固定,之后我们还需要借助Windows提供的一系列API来计算出这条语句在内存中加载的实际地址,才能进行后续的操作。
定位目标函数的一般流程如下:
修改分为两类,一种是简单地写入新语句,覆盖原来的内容。这部分相对比较简单,只需要把机器码直接写入内存指定即可。但是限制较多,一般只是写入0x90(nop),相当于移除了某条指令。
这个方法确实不错,也能达到很多效果比如不掉血不掉甲。但是由于这些函数都是全局共享的,也就是双方都不掉血掉甲,作为一个外挂来说肯定是失败的。
更为通用的办法还是仿照之前的写法进行hook,而在hook的代码里就可以检测调用者的信息,如果是自己就加入类似无敌、攻击翻倍等功能。
我们实现过的功能包括无限子弹,秒杀(用户造成的伤害锁定为255),无敌,随地买枪下包等。有兴趣的读者也可以自行尝试。建议先用CE修改器达到类似功能,然后再考虑如何写入dll文件中。
游戏入口
这次的项目是我们前端课的课程大作业,我们选择做一个魔方,支持自动复原、用户自己染色等操作。具体的分工是我负责前端部分魔方的绘制与操作,CalciferZh负责魔方的建模和求解工作。
绘制方面我们选择了three.js这个库进行绘制。由于图形库基本的用法都是大同小异,因此主要是需要了解基本的API操作。当然由于我本人也没有系统学习过图形学的相关知识,也借此机会进行了学习。要建模魔方,最基本的想法就是绘制出27个正方体,并支持视角的旋转和方块的旋转。根据three.js的绘制惯例,这里分为七步:initHTML(); initThree(); initCamera(); initScene(); initLight(); initObject(); render();
这里面所需要做的仅是添加一些与three.js无关的HTML元素如各式各样的按钮。同时也在这里注册了一系列鼠标与键盘事件以支持用户的交互。
这一步建立了THREE.WebGLRenderer,设置相关的属性后添加入HTML中
这一步建立了THREE.PersepctiveCamera,并调整位置与方向。同时这里我们建立了一个THREE.OrbitControls这样一个控制器来控制相机的运动如缩放、旋转等。当然在后期我们也发现原生的控制器并不能很好地支持我们的需求,因此我们也对其进行了一定的修改。
这一步建立了THREE.Scene,调用drawCubes()
新建了27个正方体并往场景里面添加。
这一步建立了THREE.AmbientLight,设置颜色后直接加入场景即可。
一个空函数,并没有执行任何指令。
清空之前的画布并重新渲染,这里使用了window.requestAnimFrame(render)
进行动画效果的设置,这里的好处是使得整个网页的动画效果有一个统一的刷新机制,且其刷新频率与显示器的保持一致,同时当浏览器没有处于该标签页的时候不会继续刷新渲染。总之使用这样一个函数来管理动画比使用朴素的setTimeout有相当的优势。
至此,整个绘制的大框架已经建立完毕,接下来的工作一是补全drawCubes(),二就是支持相关的交互操作。
每个小正方体自然都是一个THREE.Mesh对象,其中geometry是THREE.BoxGeometry,而纹理则需要分别计算(generateMaterial(i, j, k)),设置位置时根据ijk坐标可以很容易的计算出来。纹理的绘制方面则是通过canvas对象来实现。首先我们绘制了7个基本的canvas贴纸(正如市面上的魔方一样,我们使用了广泛采用的圆角正方形),并由此建立了THREE.texture,并进一步建立了THREE.MeshBasicMaterial对象。而在贴纹理的时候则是默认贴上黑色纹理,再根据ijk坐标确定“暴露”的面而贴上对应的纹理。
(PS. 由于正方体有6个面因此实际上应该画12个三角形的纹理,但three.js对此进行了一个封装,当只提供6个纹理的时候会自动为一个正方形上的两个三角形填充相同的纹理)
首先是三个基本函数rotateX/Y/Z(objs, rad)
,执行的是对所有objs对象旋转指定的rad角度。而rotate(objs, axis, rad, now, start, last)
函数则是负责渲染旋转的动画效果。这段操作直接看代码应该不难理解。now,start,last这三个时间参数就是用来将大的旋转拆分为若干细微的旋转所需要的指标。整个函数也是一个标准的使用window.requestAnimFrame()
渲染的写法。
根据不同的op值(L表示L面,R表示R面,LR则表示整个魔方采取与L/R旋转相同的轴),从27个cubes里面选择对应的cube。这里选择的方式是根据cube中心点的坐标。由于旋转的过程中可能会有细微的误差,经过若干次旋转后中心点很难再对齐在0或者$\pm$unit处,因此使用appro(lhs, rhs)
实现约等于的比较。
该函数会执行ops里面第i个操作。这里的每个OP可以是L2(L转180°),L’(L转-90°)这样的操作,因此需要对其进行判断,解析出对应的操作然后丢给OP()
处理。而stepByStepRotate(ops)
就是设定定时器,一步步执行ops的操作。这是用于自动恢复魔方的。
魔方的复原算法采取的是 Two-Phase Algorithm Details算法,算法的具体原理详见链接不再赘述。在github上也能找到相关的实现,当然我们也给出了js的实现(backend.js)。
根据算法的要求,将所有的方块的种类、位置、朝向等分别传给四个数组cp,co,ep,eo,包裹成一个对象然后送入求解器求解,最后返回对应的复原步数并使用opClosure()
进行复原。同时我们在命令行里打出了复原的公式,用户可以使用开发者工具看到(如果你不知道怎么调出,先试试F12,或直接搜索您所用的浏览器所支持的打开方式)
鼠标事件的处理相对比较复杂,这牵扯到了鼠标点击的位置、滑动的方向、魔方的状态等信息。在这里我们使用了一个名为last的对象来处理鼠标事件。total记录着鼠标滑过的总像素,最后会被换算为角度并且计算出正确的角度实现“磁力定位”;x,y储存着上次的鼠标事件的x,y值;rotatingNineCubes,rotatingAllCubes记录着魔方的旋转类型(单层转还是整个魔方转);objs记录转动的方块;axis记录着转动轴;main指示着目前的旋转是根据鼠标的x位移还是y位移;intersectFace记录着鼠标点击的面的方向(xyz);sgn是用来表示顺时针or逆时针旋转(键盘事件则根据shift,鼠标事件则根据total的符号);interfacePoint则记录着点击的方块的中心点,这被用来计算旋转的九个方块。
可以看到,当点击角块的时候,其实有三个面的点击方式。我们规定,当点击面为黄色的时候,只能进行蓝色面的旋转(对应于鼠标在x方向的拖拽)和红色面的旋转(对应于鼠标在y方向的拖拽),这样就可以比较合理地处理鼠标的拖拽事件并给出自然的响应。
当且仅当canRotate且不为rotating的时候才会执行接下来的操作。先用clientX,clientY值记录鼠标点击的坐标(需要处理鼠标事件和触屏事件)。如果是右键单击或双指滑动,则last.rotatingAllCubes为true,记录last的x,y值,并将相机的enableRotate置为false。否则则还需要获取点击到的方块,记录下last.intersectPoing, last.intersectFace,last.x,last.y,然后同样的将相机的enableRotate置为false,将last.rotatingNineCubes置为true。并将canRotate置为false。
首先自然还是获取xy坐标。如果旋转的方式已经确定,简单地计算x或者y的diff值即可。将diff值加入last.total中,然后根据diff值渲染细微的动画即可。但旋转的方式未定时则需要有一系列的处理与计算。
当刚开始从单击到初步移动的时候,由于移动的像素太少,我们不能准确的得到用户是在x方向拖拽还是y方向拖拽,因此这段时间内last.x和last.y都不会被更新,保存着最开始单击的坐标。一旦在x方向或y方向的绝对值大于阈值(这里设为20)我们才开始进行处理。后面进行的操作就是根据已有的信息确定旋转的方块以及旋转轴,剩下的事就是动画的渲染问题了。
如果是视角的旋转(controller.enableRotate==true),则执行恢复视角的代码:以当前视角和原始视角的叉积为旋转轴,点积的反余弦为角度调用cameraRoteta()
方法。
否则,将controller.enableRotate置为true(释放后说明当前操作结束,允许用户改变视角观察)。如果是单层的旋转,则将魔方定位回最近的$\pi/2$的整数倍的角度,执行动画,并将last里相关的指标置空。旋转所有方块也是类似的操作,只不过每次都必定旋转$\pm\pi/2$,就不需要“磁力定位”的处理。
在我们的视线里,相机的旋转是交由一个controller实现的。因此在背景旋转的时候我们无需进行额外的代码编写,three.js已经帮我们完成了相关的功能。当然,我们不希望旋转魔方的时候还会让controller调整我们的视角,因此上面进行了controller.enableRotate置为false的操作。但我们在其上面也做了一定的修改,一个是在为touch注册listener的时候加入{passive:false},而不是用stopPropagation()。
生成一个随机操作序列然后调用opClosure()
即可。
将方块里所有的颜色都置空,然后根据选择的颜色以及点击的面进行染色、去染色等。我们限制了每个颜色只能染九次,但仍然无法阻止用户生成一个不可能的状态的魔方。所幸,我们的魔方求解算法可以在求解的时候检测出当前的魔方违反了哪些性质并给出警告。
一个有趣的操作是,选择顶层的三个角块并且都逆时针扭转一下,这样的魔方仍然是合法的并可以被求解。强调这一点是为了说明,仅通过三个面的信息是无法唯一确定魔方的状态的,因此我们没法支持仅靠用户上传的现实中魔方的一张图(最多看到三个面)就给出求解的方式,当然,再给一张图是可以的,有兴趣的读者可以fork出去继续开发相关功能。
支持touchEvent,以及根据屏幕的大小渲染合理布局。
由于求解魔方使用了setTimeout,而整体的动画使用window.requestAnimFrame
,因此切换标签页后会出问题。
另外还可以做如下改进:
游戏入口
这是大二夏季学期前端课的一个小作业
先挖坑,更多信息以后补充
- CPU执行访管指令,引起访管中断
- 处理器保存断点程序上下文环境(PSW、PC和其他寄存器),CPU切换到管态
- 中断处理程序调用相应的系统服务
- 中断处理结束后,恢复中断程序的上下文环境,CPU恢复目态继续执行
- CPU检测到的异常 Fault、Trap、Abort。如算术溢出、除0、用户态使用特权指令
- 程序设定的异常 即程序员通过int、int3指令发出的请求中断(软中断),实现系统调用
- 可屏蔽中断:即IO中断,当外部设备或通道操作正常结束或发生错误产生的,如打印机完成或缺纸、读磁盘时驱动器无磁盘等
- 不可屏蔽中断 由掉电、存储器校验错误等硬件故障引起的硬件中断
检测算法
单资源:检测封闭环路
多资源:不断找既不阻塞又非独立的进程节点,消去请求和分配。若所有孤立则无死锁
解除算法
剥夺资源(危害取决于资源性质)
进程回退(定期备份不同时刻进程状态,死锁后回退,不野蛮但存储和计算代价高)
撤销进程(一直撤销进程直到解除)
破坏互斥条件:允许多个进程使用同个资源(假脱机打印),不普适
破坏请求和保持条件:只允许一次请求所有资源:不知道需要多少、效率低。若申请时先释放所有可能饿死
破坏环路等待条件:资源编号,按序申请。可避免死锁,但存在无法获取到全部资源的风险
又把用户区划分若干分区,每个进程一个分区
分区大小、位置、个数不变。多小分区适量中等少量大。输入队列处理多个进程
内存分配表:分区号+地址+长度+状态+进程名字
内存分配:先放入输入队列,然后最先匹配、最佳匹配等。回收简单
内存利用率不高,内碎片多。总数固定不灵活。地址大小有限。
最先匹配法:碰到第一个足够的,一分为二
下次匹配法:记录当前位置(头指针移动)
最佳匹配法:大小最接近的,外碎片难以利用
最坏匹配法:找最大的
内存回收:trivial
分配:检查是否还有N个空闲。若有则申请一个长度为N的页表,并把地址填入PCB。分配N个物理页框,编号填入页表,修改位示图,加N
回收:根据编号计算位示图位置,写0,减N
最优页面置换(OPT)
(最近)最久未使用(LRU)(开销大,缺乏硬件支持)
最不常用(LFU) 设置访问计数器,淘汰最小
先进先出(FIFO) 选择驻留时间最长的,性能较差
时钟页面置换(Clock) 若被访问后置1,缺页后指针旋转,0则淘汰1则置0
LRU=FIFO 123412341234
LRU>FIFO 123124252
FIFO>LRU 123142314
LRU>Clock 123213414
- 任何两个进程不能同时进入临界区
- 不能事先假定CPU个数和运行速度
- 进程运行在临界区外不能妨碍其他进程进入
- 任何一个进程进入的要求在有限时间得到满足
方法3:Peterson方法调用enter和leave判断是否安全。
|
|
繁忙等待浪费CPU,且低优先级在临界区,高优先级也想进入会死锁
- FIFO/FCFS按作业到达次序
- SJF预计执行时间,先完成短的
- RR就绪进程按FCFS排成队列。轮流执行时间片。公平有保证。Q小花销大,q大退化成FCFS。通常20-50ms
- 优先级算法:SJF把需要用时作为优先级。
- 静态优先级:低优先级可能饥饿
- 动态优先级:可根据运行时间和等待时间调整优先级防止饥饿。
- 可以不同优先级别分组,然后级别间优先级算法,同级别内时间片轮转。
- 有锁的话可能出现优先级反转
- 多级反馈队列算法:引入多个就绪队列,根据进程性质、类型分为若干子队列有不同优先级。前台交互式进程RR,后台批处理FCFS等。根据进程的运行反馈信息调整队列。
新进程优先级高。若时间片未用完升级,否则降级。高优先级低时间片。IO繁忙的高优先级,CPU繁忙的低优先级减少调度次数。
机械部分(设备本身)+电子部分(设备控制器(芯片)or适配器(印刷电路卡),完成设备和主机通讯)
中断驱动方式 IO完成后,控制器通过bus向中断控制器发出信号,如果接受,就将设备编号放在地址线并令CPU中断。CPU根据编号访问中断向量表,取出中断程序地址然后执行,最后向中断控制器发出确认信号
- CPU向设备控制器发出命令,启动读操作
- 设备控制器控制IO设备完成读操作,把数据保存在寄存器或缓冲区,中断CPU
- CPU把数据读入内存
直接内存访问方式 DMA访问系统总线,代替CPU指挥IO和内存间数据传送 有寄存器如内存地址寄存器、字节计数器、多个控制寄存器(指出端口地址、数据传送方向、传送单位、字节数)
- CPU对DMA控制器编程,告诉他把什么数据传送到内存什么地方,并向磁盘控制器发出命令,让它去磁盘驱 动器读入所需的数据块,保存到内部缓冲区,并验证数据正确性。
- DMA控制器通过总线向磁盘控制器发出读操作,并把写入的内存地址打在Bus上
- 磁盘控制器取出一个字节,按地址写入内存
- 磁盘控制器向DMA发送确认信号,DMA吧内存+1,计数器-1
- DMA向CPU发送中断,告诉数据已完成
柱面定位时间+旋转延迟时间+数据传送时间
10000rpm,300扇区,512字节,读150kb,柱面平均6.9ms,旋转平均旋转时间一半(3ms),数据传输17微秒
300个连续扇区:6.9+3+6=15.9
随机300个:(6.9+3+0.017)*300
一般柱面定位时间最久
属性:保护信息、创建者、只读、隐藏、系统、时间(创建、访问、修改)、长度
低格为若干个分区,主磁盘的0扇区为MBR,MBR结尾是一个分区表,记录每个分区的起始扇区和大小。
FCB是OS管理文件的结构,存放文件所有管理信息,文件存在的标志
FCB包含文件的类型、长度、所有者、访问权限、创建、访问、修改时间、物理块信息
连续结构 容易实现、访问速度快、有外碎片、难以动态增长)用于早期、DVD
文件组织成索引文件,磁盘块512byte,文件10+1+1+1,每个地址2字节
一个文件最多 10+2^8+2^16+2^24 个文件页
(1)程序是静态的,而进程是动态的;(2)程序是永久的,进程是短暂的;(3)程序的组成是代码,进程由程序、数据和进程控制块组成;(4)一个程序可以对应多个进程,通过调用关系,一个进程也可以包括多个程序;(5)进程可以生成其他进程,而程序不能生成新的程序。(5分)
]]>这是我的第一篇博客,记录的是我在完成《软件工程(3)》中第一个四周单人 project 的感想。
先谈谈自己在这次的 project 里的收获吧。
这是一个依托微信公众号平台的活动抢票应用。活动管理员可以上传活动的相关信息,包括活动时间、订票时间、票量等。而普通用户可以在公众号里查询到活动的信息,已绑定的用户则可以在抢票时间内进行抢票等操作。
Ubuntu 16.04.1 LTS + python 3.5.2 + django 1.11.5 + uwsgi 2.0.15 + nginx 1.10.3-0
首先我们需要对 django+uwsgi+nginx 有个大致的理解。外部的请求发来后首先被 nginx 处理,如果是静态资源则直接返回(当然其也负责负载均衡等功能),动态网页则通过端口或 socket 进一步转发给 uwsgi 进行处理,uwsgi 再调用 django 的服务。
因此成功部署主要需要下列操作
uwsgi --ini uwsgi.ini
拉起服务,同时(sudo) service nginx restart
启动nginx不出意外的话,经过上述过程已经可以正常运行django程序。
/var/log/nginx
下查看日志。若 nginx 这一块没有 bug,再去项目目录的 log 里(写在 uwsgi.ini 里的daemonize)systemctl service XXX
查看失败的原因单元测试用的 django testcase,功能测试采用 selenium + phantomJs 模拟浏览器。
这其实也是我第一次编写自动化测试单元,如果不算之前写 C/C++ 中写的一些很简单的 assert 的话。
由于时间的关系,测试的样例主要是覆盖在用户端,因为相对而言用户端的行为更加不可控。管理员的操作相对来说还是比较安全的。
同时,对于抢退票的相关操作,自然是需要更多更精细的单元测试,毕竟这是这一块的核心而且极其容易出错。
由于没有想到比较好的 mock 出微信端操作的方法,因此在抢退票方面主要是做了单元测试,保证抢退票操作的合理应对以及对数据库的正确更新。而对其他的像用户查看活动详情与票据详情,则在单元测试的基础上也进行了功能测试,保证相关页面不会出错。
接口文档有许多东西交代的不是特别清楚。比如在调整微信菜单的接口中
我就对这个请求参数的正确打开方式思考了很久。当然我也知道编写文档中难免会出现交代不清楚的地方,但经过这次开发让我对文档的重要性有了进一步认识,也提醒自己以后编写文档要更加小心谨慎,避免跳过一些因为自己是开发者所以觉得很trivial的细节。
我始终觉得这种项目形式,应该先介绍测试相关然后才编写代码。在这次project里等到课上介绍TDD的时候,基本上业务代码都已经完成了。在已经编写完业务代码,再让同一个人编写测试用例时其实效果是鞑靼折扣的。虽然也还是确实能发现一些之前没有注意到的逻辑漏洞,但更多的样例还是会有“为了踩坑而踩坑”之嫌。
匆匆忙忙赶在 ddl 前将作业交上了,许多之前想做的优化也最后没有实现,测试单元也并不完全。作为一次作业的话,感觉还是挺失败的…但抛开作业,这次 project 也确实全方位锻炼了我,在开发、测试、部署的那段时间里也确实学习到了很多,包括部署的过程中遇到各种各样奇奇怪怪的坑然后一个一个填,这样的经历还是涨很多码力的。而测试单元的编写则是用更高的抽象去俯视去审视代码,在编写的过程中也会去不断反思自己当初写业务逻辑犯下的错误。总之,在这次的project里切身体会学习了许多新东西,也就对得起我在这上面花费的时间了。
]]>