Tier 1 Conference
from Security Conference Ranking and Statistic
(Frontend): Frontend related (Cache): Cache related, includes TLB, Page, etc (Timing): Timing related (Other): Other
Tier 1 Conference
from Security Conference Ranking and Statistic
Why start at 2018? Because Meltdownm, Spectre and MDS emerged, open a new era of (micro-)architectural security.
... TO BE ADD More
... TO BE ADD More
... TO BE ADD More
... TO BE ADD More
... TO BE ADD More
... TO BE ADD More
2019 https://spectreattack.com/spectre.pdf
现代处理器使用分支预测(branch prediction)和推测执行(speculative execution)来最大化性能。例如,如果分支的目标取决于正在读取的内存值,CPU将会尝试预测分支目标地址,并提前执行。当内存加载完成时,CPU将放弃或提交推测计算的结果。推测逻辑的执行方式并不安全,可以访问受害者的内存和寄存器,并会执行一些有侧信道副作用的操作。
幽灵(Spectre)攻击涉及诱使受害者会推测执行在正常的程序执行期间不会执行的操作,并且通过侧信道将受害者的机密信息泄露给攻击者。本文描述了实际的攻击,这些攻击结合了来自侧信道攻击、故障攻击和面向返回的编程的方法,可以从受害者的进程中读取任意内存。此外,本文表明,推测执行的实现违反了支撑众多软件安全机制的安全假设基础,包括操作系统进程分离、容器化、即时(JIT)编译,以及针对缓存时间和侧信道攻击的对策。这些攻击对实际系统构成了严重威胁,因为易受攻击的推测执行能力在Intel,AMD和ARM的特定处理器中被验证有效,这些微处理器已用于数十亿台设备。
虽然在某些情况下,临时的针对特定处理器的对策是可行的,但是合理的解决方案需要对处理器设计以及指令集架构(ISAs)进行更新,以便让硬件架构师和软件开发人员对当前实现的CPU计算状态是否允许(或不允许)泄漏达成共识。
物理设备执行的计算通常会在计算的标准输出之外留下可观察到的副作用。侧信道攻击集中于利用这些副作用来提取不可访问的秘密信息。自90年代末被提出[43]后,许多物理效应比如功耗[41,42]、电磁辐射[58]、或噪声[20]已被用来提取加密密钥及其他秘密。
物理侧信道攻击还可用于从计算机和移动终端等复杂设备中提取秘密信息[21,22]。然而由于这些设备经常执行来自潜在未知来源的代码,因此它们面临着基于软件的攻击形式的额外威胁,且其不需要外部测量设备。有些攻击则利用软件漏洞(如缓冲区溢出[5]或双自由错误[12]),有些软件攻击利用硬件漏洞泄露敏感信息。后者的攻击包括利用缓存时间[8,30,48,52,55,69,74],分支预测历史[1,2],分支目标缓冲区[14,44]或开放DRAM行[56]的微体系结构攻击。基于软件的技术也被用来发动故障攻击,从而改变物理内存[39]或CPU内部值[65]。
在过去的几十年里,几种微体系结构设计技术促进了处理器速度的提高。其中一个技术是推测执行,它被广泛用于提高性能,涉及让CPU猜测未来可能的执行方向,并在这些路径上提前执行指令。更具体地,考虑一个示例,其中程序的控制依赖于位于外部物理内存中的未被缓存的值。由于该内存比CPU慢得多,因此通常需要几百个时钟周期才能知道该值。CPU不会通过空闲状态浪费这些周期,而是尝试猜测控制流的方向,保存其寄存器状态的检查点,并在猜测的路径上推测性地执行程序。当值最终从内存中到达时,CPU将检查其最初猜测的正确性。如果猜测是错误的,CPU会通过将寄存器状态恢复到存储的检查点来丢弃错误的推测执行,导致的性能与空闲状态相近。然而如果猜测正确,则提交推测性执行结果,从而在延迟期间完成了有用的工作,产生显著的性能增益。
从安全角度来看,推测执行涉及以可能不正确的方式执行程序。然而由于CPU被设计为通过将错误的推测性执行的结果恢复到其先前的状态来保持功能的正确性,因此这些错误以前被认为是安全的。
在本文中,我们分析了这种不正确的推测执行的安全隐患。我们提出了一类微体系结构攻击,我们称之为幽灵攻击。在高层次上,幽灵攻击诱使处理器推测性地执行在正确的程序执行下不应该执行的指令序列。因为这些指令对标称CPU状态的影响会被恢复,我们称它们为瞬时指令。通过影响哪些瞬时指令推测性地执行,我们能够从受害者的存储器地址空间内泄漏信息。
我们通过实验证明了幽灵攻击的可行性,即利用瞬时指令序列从非特权的本机代码和可移植的JavaScript代码中泄漏跨安全域的信息。
使用本机代码进行攻击。作为概念验证,我们创建了一个简单的受害者程序,在其内存地址空间中包含秘密数据。接下来,我们搜索编译后的受害者二进制文件和操作系统的共享库,寻找可用于泄漏受害者地址空间信息的指令序列。最后,我们编写了一个攻击者程序,该程序利用CPU的推测执行功能,将之前发现的序列作为瞬时指令执行。使用这种技术,我们能够从受害者的地址空间读取内存,包括存储在其中的秘密。
使用JavaScript和eBPF进行攻击。除了使用本机代码突破进程隔离边界之外,幽灵攻击还可用于突破沙箱隔离,例如,通过可移植的JavaScript代码装载它们。为从经验上证明这一点,我们展示了一个JavaScript程序,它成功地从运行它的浏览器进程的地址空间读取数据。此外,我们还演示了在Linux中利用eBPF解释器和JIT的攻击。
在高层次上,幽灵攻击通过微体系结构隐蔽信道将推测执行与数据泄漏相结合,从而违反内存隔离边界。更具体地,为了发起幽灵攻击,攻击者通过在进程地址空间内定位或引入指令序列来开始,该指令序列在执行时充当泄漏受害者的存储器或寄存器内容的隐蔽信道发送器。然后,攻击者欺骗CPU以推测方式错误地执行此指令序列,从而通过隐蔽信道泄漏受害者的信息。最后,攻击者通过隐蔽信道检索受害者的信息。虽然由该错误的推测执行导致的对标称CPU状态的改变最终被恢复,但是先前泄漏的信息或对CPU的其他微体系结构状态(例如缓存内容)的改变可以在标称状态恢复之后继续存在。
上述对幽灵攻击的介绍是一般性的,需要通过一种引发错误推测执行的方法以及一个微体系结构隐蔽信道来具体的实例化。隐蔽信道组件有许多选择,本文中描述的实现使用基于缓存的隐蔽信道[64],即Flush+Reload
[74]和Evict+Reload
[25,45].
我们现在继续介绍我们诱导和影响错误推测执行的技术。
变体1:利用条件分支。在幽灵攻击的这种变体中,攻击者误训练CPU的分支预测器,使其预测错误的分支方向,导致CPU暂时违反程序语义去执行原本不会执行的代码。正如我们所展示的,这种不正确的推测执行允许攻击者读取存储在程序地址空间中的秘密信息。实际上,考虑以下代码示例:
if (x < array1_size)
y = array2[array1[x] * 4096];
在上面的示例中,假设变量x
包含攻击者控制的数据。为了确保对array1
的内存访问的有效性,上面的代码包含一个if语句,其目的是验证x
的值是否在合法范围内。我们将展示攻击者如何绕过此if
语句,从而从进程的地址空间读取潜在的秘密数据。
首先,在最初的误训练阶段,攻击者使用有效输入调用上述代码,从而训练分支预测器以预期if
将为真。接下来,在攻击阶段,攻击者在array1
的边界之外调用值为x
的代码。CPU没有等待分支结果的确定,而是猜测边界检查将为真,并且已经推测性地执行使用恶意x
来计算指令array2[array1[x]*4096]
。请注意,从array2
加载到缓存的地址取决于使用恶意值x
的array1[x]
,通过放大使其访问不同的缓存行并避免硬件预取效应。
当边界检查的结果最终确定时,CPU会发现其错误,并恢复对其标称微体系结构状态所做的任何更改。但是对缓存状态所做的更改不会被还原,因此攻击者可以分析缓存内容,从而找到在受害者内存被越界读取时获取的可能的秘密字节的值。
变体2:利用间接分支。借鉴面向返回的程序设计(ROP)[63],在该变体中,攻击者从受害者的地址空间中选择一个 gadget ,并影响受害者推测性地执行该 gadget 。与ROP不同,攻击者不依赖于受害者代码中的漏洞。相反,攻击者训练分支目标缓冲区(BTB)错误预测从间接分支指令到 gadget 地址的分支,从而导致 gadget 的推测执行。如前所述,虽然不正确的推测执行对CPU标称状态的影响最终会恢复,但它们对缓存的影响不会恢复,从而允许 gadget 通过缓存侧信道泄漏敏感信息。我们从经验上证明了这一点,并展示了如何通过仔细选择 gadget 来允许该方法从受害者那里读取任意内存。
为了误训练BTB,攻击者在受害者的地址空间中找到 gadget 的虚拟地址,然后对该地址执行间接分支。这种训练是从攻击者的地址空间完成的。在攻击者的地址空间中, gadget 地址上驻留的内容并不重要;所有这一切要求攻击者在训练期间的虚拟地址与受害者的虚拟地址匹配(或别名)。事实上只要攻击者处理了异常,即使攻击者的地址空间中没有映射到 gadget 虚拟地址的代码,攻击也能奏效。
其他变体。进一步的攻击可以通过改变实现推测执行的方法和用于泄漏信息的方法来设计。示例包括误训练返回指令、通过时序变化泄漏信息,以及算术单元上的争用。
硬件。我们已经通过实验验证了多款英特尔处理器在幽灵攻击下的脆弱性,包括Ivy Bridge、Haswell、Broadwell、Sky lake和Kaby Lake处理器。我们还验证了该攻击对AMD Ryzen CPUs的适用。最后,我们还成功地对流行手机中的几款基于ARM的三星和高通处理器发起了幽灵攻击。
当前状态。使用负责任的披露实践,本文中不相交的作者团队向部分重叠的CPU供应商组和其他受影响的公司提供了我们的初版结果。在与业界的协调下,作者们还参与了对结果的封锁。幽灵系列攻击被记录在CVE-2017-5753和CVE-2017-5715中。
熔断攻击(Meltdown Attack)[47]是一种相关的微体系结构攻击,它利用乱序执行来泄漏内核内存。熔断攻击在两个主要方面不同于幽灵攻击。首先与幽灵攻击不同,熔断攻击不使用分支预测。相反,它依赖于这样一种观察,即当一条指令导致陷阱时,后续指令在被终止之前会被乱序执行。其次,熔断攻击利用了许多Intel和某些ARM处理器特有的漏洞,允许某些推测执行的指令绕过内存保护。结合这些问题,熔断攻击从用户空间访问内核内存。这种访问会导致陷阱,但在发出陷阱之前,访问之后的指令会通过缓存隐蔽信道泄漏被访问内存的内容。
相比之下,幽灵攻击适用于更广泛的处理器,包括大多数AMD和ARM处理器。此外,KAISER机制[29]已被广泛用作熔断攻击的缓解措施,但它无法抵御幽灵攻击。
在本节中,我们将介绍现代高速处理器的一些微体系结构组成,它们如何提高性能,以及如何从运行的程序中泄漏信息。我们还介绍了面向返回的编程(ROP)和 gadget 。
乱序执行范例通过允许程序的指令流中更下游的指令与前面的指令并行执行,有时在之前执行,从而提高处理器组件的利用率。
现代处理器在内部使用微操作,模拟体系结构的指令集,即将指令解码为微操作[15]。一旦与一条指令对应的所有微操作以及前面的所有指令都完成,这些指令就可以失效,提交对寄存器和其他体系结构状态的更改,并释放重新排序的缓冲空间。因此,指令按程序执行顺序失效。
处理器通常不知道程序的未来指令流。例如,当向外执行到达其方向依赖于其执行尚未完成的在前指令的条件分支指令时,就会发生这种情况。在这种情况下,处理器可以保留其当前寄存器状态,对程序将遵循的路径进行预测,并沿着该路径推测性地执行指令。如果预测被证明是正确的,则提交推测执行的结果(即保存),从而在等待期间产生优于空闲的性能优势。否则,当处理器确定它遵循错误的路径时,它通过恢复其寄存器状态并沿着正确的路径恢复来放弃它推测地执行的工作。
我们将被错误执行(即作为错误预测的结果)但可能留下微体系结构痕迹的指令称为瞬时指令。尽管推测性执行会保存程序的体系结构状态,就像执行遵循正确的路径一样,但微体系结构元素可能处于与瞬时执行之前不同(但有效)的状态。
现代CPU上的推测执行可以提前运行数百条指令。该限制通常由CPU中的重新排序缓冲区的大小决定。例如,在Haswell微体系结构上,重新排序缓冲区有足够的空间容纳192个微操作[15]。由于微操作数量与指令数量之间不存在一一对应的关系,因此限制取决于所使用的指令。
在推测执行期间,处理器对分支指令的可能结果进行预测。更好的预测通过增加可以成功提交的推测执行操作的数量来提高性能。
现代Intel处理器(例如,Haswell Xeon处理器)的分支预测器具有用于直接和间接分支的多个预测机制。间接分支指令可以跳转到运行时计算的任意目标地址。例如,x86指令可以跳转到寄存器、内存位置或堆栈上的地址,例如jmp eax
、jmp [eax]
和ret
。 ARM(例如,MOV pc, r14
)、MIPS(例如,jr $ra
)、RISC-V(例如,jalr x0,x1,0
)和其他处理器也支持间接分支。为了补偿与直接分支相比的额外灵活性,使用至少两种不同的预测机制来优化间接跳转和调用[35]。
英特尔[35]对处理器预测介绍如下:
因此,使用若干处理器组件来预测分支的结果。分支目标缓冲器(BTB)保存从最近执行的分支指令的地址到目的地址的映射[44]。处理器甚至可以在解码分支指令之前使用BTB预测未来的代码地址。Evtyushkin等人[14]分析了Intel Haswell处理器的BTB,并得出结论,只有分支地址中最不重要的31位用于索引BTB。
对于条件分支,记录目标地址对于预测分支的结果不是必需的,因为在运行时确定条件时,目标通常在指令中编码。为了改进预测,处理器维护分支结果的记录,包括最近的直接分支和间接分支。Bhattacharya等人[9]分析了近期英特尔处理器中分支历史预测的结构。
尽管返回指令是一种间接分支,但在现代CPU中,通常使用一种单独的机制来预测目标地址。返回堆栈缓冲区(RSB)维护调用堆栈最近使用部分的副本[15]。如果RSB中没有可用的数据,不同的处理器将暂停执行或将BTB用作备用[15]。
分支预测逻辑,例如BTB和RSB,通常不会在物理核心之间共享[19]。因此,处理器只能从同一内核上执行的先前分支中学习。
为了弥补较快的处理器和较慢的内存之间的速度差距,处理器使用依次较小但较快的高速缓存的层次结构。高速缓存将内存划分为固定大小的块,称为行,典型的行大小为64或128字节。当处理器需要来自内存的数据时,它首先检查位于层次结构顶部的L1缓存是否包含副本。在高速缓存命中的情况下,即在高速缓存中找到数据的情况下,从L1高速缓存中检索数据并使用。否则,在高速缓存未命中的情况下,重复该过程以尝试从下一高速缓存级并最终从外部存储器检索数据。一旦读取完成,数据通常被存储在缓存中(并且先前缓存的值被逐出以腾出空间),以防在不久的将来再次需要它。现代英特尔处理器通常具有三个高速缓存级别,每个内核具有专用的L1和L2高速缓存,并且所有内核共享公共的L3高速缓存,也称为末级高速缓存(LLC)。
处理器必须使用缓存一致性协议(通常基于MESI协议)确保每个内核的L1和L2高速缓存是一致的[35]。特别地,MESI协议或其一些变体的使用意味着一个核上的存储器写入操作将导致其他核的L1和L2高速缓存中,相同数据的副本被标记为无效,这意味着其他核上对该数据的未来访问将不能快速地从L1或L2高速缓存加载数据[53,68].当这种情况在特定的内存位置重复发生时,这被非正式地称为缓存行反弹。由于内存是以线粒度缓存的,因此即使两个内核访问映射到同一缓存行的不同邻近内存位置,也会发生这种情况。这种行为称为虚假共享,是众所周知的性能问题的根源[33]。高速缓存一致性协议的这些特性有时会被滥用,以代替使用clflush
指令或回收模式的高速缓存回收[27]。这种行为先前被探索为促进Rowhammer攻击的潜在机制[16]。
我们上面讨论的所有微体系结构组件都通过预测未来的程序行为来提高处理器性能。为此,它们维护依赖于过去程序行为的状态,并假设将来的行为与过去的行为相似或相关。
当多个程序同时或通过分时在同一硬件上执行时,由一个程序的行为引起的微体系结构状态的变化可能会影响其他程序。反过来,这可能导致从一个程序到另一个程序的意外信息泄漏[19]。
最初的微体系结构侧信道攻击利用了时间可变性[43]和L1数据高速缓存的泄漏,来从密码原语中提取密钥[52,55,69]。多年来,信道已在多个微体系结构组件(包括指令缓存)被演示,包括指令缓存[3]、低级缓存[30,38,48,74]、BTB[14,44]和分支历史[1,2]。攻击目标已经扩大到包括共定位检测[59]、破坏ASLR[14,26,72]、击键监控[25]、网站文件打印[51]和基因组处理[10]。最近的结果包括跨核心和跨CPU攻击[37,75],基于云的攻击[32,76],对可信执行环境的攻击[10,44,61],来自移动代码的攻击[23,46,51],以及其他新攻击技术[11,28,44]。
在这项工作中,我们使用Flush+Reload
技术[30,74],及其变体Evict+Reload
[25]来泄露敏感信息。使用这些技术,攻击者首先从与受害者共享的缓存中清除缓存行。受害者执行一段时间后,攻击者会测量在与被逐出的缓存行对应的地址执行内存读取所需的时间。如果受害者访问了受监控的缓存行,则数据将在缓存中,并且访问将速度要快。否则,如果受害者尚未访问该行,则读取速度将会很慢。因此,通过测量访问时间,攻击者可以了解受害者是否在逐出和探测步骤之间访问了受监控的高速缓存行。
这两种技术之间的主要区别在于用于从缓存中逐出受监控缓存行的机制。在Flush+Reload
技术中,攻击者使用专用机器指令(例如x86的clflush
)来逐出该行。使用Evict+Reload
,通过对存储行的高速缓存组强制争用来实现逐出,例如,通过访问被加载到高速缓存中的其他存储器位置,并且(由于高速缓存的有限大小)使处理器丢弃(逐出)随后被探测的行。
面向返回的编程(ROP)[63]是一种技术,它允许劫持控制流的攻击者通过将易受攻击的受害者的代码中发现的机器代码片段(称为 gadget )链接在一起,使受害者执行复杂的操作。更具体地说,攻击者首先在受害者二进制文件中找到可用的 gadget 。每个 gadget 在执行返回指令之前执行一些计算。可以修改堆栈指针(例如,指向写入外部可写缓冲区的返回地址)或覆盖堆栈内容(例如,使用缓冲区溢出)的攻击者可以使堆栈指针指向一系列恶意选择的 gadget 地址的开头。当执行时,每个返回指令从堆栈跳转到目的地地址。因为攻击者控制了这一系列地址,所以每次返回都会有效地跳转到链中的下一个 gadget 。
幽灵攻击诱使受害者推测性地执行在程序指令被严格序列化有序执行期间不会发生的操作,并且通过隐蔽信道将受害者的秘密信息泄露给攻击者。我们首先介绍利用条件分支误预测的变体(节IV),然后介绍利用间接分支使目标预测失误的变体(节V)。
在大多数情况下,攻击从设置阶段开始,在该阶段,攻击者执行使处理器出错的操作,以便稍后做出可利用的错误推测预测。此外,设置阶段通常包括有助于引发推测执行的步骤,例如操纵高速缓存状态以移除处理器确定实际控制流所需的数据。在设置阶段期间,攻击者还可以准备将用于提取受害者的信息的隐蔽信道,例如,通过执行Flush+Reload
或Evict+Reload
攻击的冲洗或驱逐部分。
在第二阶段期间,处理器推测性地执行将机密信息从受害者上下文传送到微体系结构隐蔽信道中的指令。这可以通过让攻击者请求受害者执行动作来触发,例如通过系统调用、套接字或文件。在其他情况下,攻击者可能会利用投机(错误)执行自己的代码以从同一进程获取敏感信息。例如,由解释器、即时编译器或“安全”语言进行沙箱化的攻击代码可能希望读取其不应访问的内存。推测执行可能会通过各种隐蔽信道暴露敏感数据,给出的示例会导致推测执行首先读取攻击者选择的地址上的内存值,然后执行内存操作,以暴露该值的方式修改缓存状态。
在最后阶段,敏感数据被恢复。对于使用Flush+Reload
或Evict+Reload
的幽灵攻击,恢复过程包括对正在监视的缓存行中的内存地址的访问进行计时。
幽灵攻击仅假设推测执行的指令可以从受害者进程可以正常访问的存储器中读取,例如,不会触发页面错误或异常。因此,幽灵攻击与熔断攻击[47]正交,它利用了某些CPU允许乱序执行用户指令,以读取内核内存的场景。因此,即使处理器阻止用户进程中指令的推测执行访问内核内存,幽灵攻击仍然有效[17]。
在本节中,我们将演示攻击者如何利用条件分支预测失误从另一个上下文(例如,另一个进程)读取任意内存。
考虑以下情况:清单1是从不可信的源接收无符号整数x
的函数(例如,系统调用或库)的一部分。运行代码的进程可以访问大小为array1_size
的无符号字节数组array1
和大小为1MB的第二个字节数组array2
。
if(x < array1_size) y = array2[array1[x] * 4096];
清单1:条件分支示例
代码片段以对x
的边界检查开始,这对安全性至关重要。特别是,此检查可防止处理器读取array1
外部的敏感内存。否则,超出界限的输入x
可能触发异常,或者可能导致处理器通过提供x =
(要读取的秘密字节的地址)+(array1
的基址)来访问敏感存储器。
图1:在边界检查的正确结果已知之前,分支预测器继续最可能的分支目标,如果结果被正确预测,则导致整体执行加速。但是,如果边界检查被错误地预测为真,则攻击者可以在某些情况下泄漏机密信息。
图1结合推测执行说明了边界检查的四种情况。在边界检查的结果已知之前,CPU通过预测比较的最可能结果来推测地执行遵循该条件的代码。边界检查的结果不能立即知道的原因有很多,例如,边界检查之前或期间的高速缓存未命中、边界检查所需的执行单元的拥塞、复杂的算术依赖性或嵌套推测执行。然而,如图所示,在这些情况下,条件的正确预测导致更快的整体执行。
不幸的是,在推测执行期间,边界检查的条件分支可能会遵循不正确的路径。在此示例中,假设攻击者使其代码运行,使得:
x
的值被恶意选择(超出边界),使得array1[x]
解析为受害者存储器中某处的秘密字节k
;array1_size
和array2
未缓存,但缓存了k
;x
值,导致分支预测器假if
可能为真。该缓存配置可以自然地发生,或者可以由攻击者创建,例如,通过触发逐出(eviction)array1_size
和array2
,使内核在合法操作中使用密钥。
当上面编译的代码运行时,处理器开始将恶意值x
与array1_size
进行比较。读取array1_size
会导致缓存未命中,并且处理器将面临相当长的延迟,直到其值可从DRAM中获得。特别是,如果分支条件或分支之前某处的指令等待未缓存的参数,则可能需要一些时间才能确定分支结果。同时,分支预测器假设IF为真。因此,推测执行逻辑将x
加到array1
的基地址,并从存储器子系统请求结果地址处的数据。该读取是高速缓存命中,并且快速返回秘密字节k
的值。然后,推测执行逻辑使用k
来计算array2[k*4096]
的地址。然后,它发送从内存读取此地址的请求(导致缓存未命中)。当来自阵列2的读取已经在进行中时,可以最终确定分支结果。处理器意识到其推测性执行是错误的,并倒回其寄存器状态。然而,来自array2
的推测性读取以地址特定的方式影响高速缓存状态,其中地址取决于k
。
为了完成攻击,攻击者测量array2
中的哪个位置被带入缓存,例如,通过Flush+Reload
或Prime+Probe
。这揭示了k
的值,因为受害者的推测执行缓存了array2[k*4096]
。类似的,攻击者也可以使用Evict+Time
,即立即使用界内值x
再次调用目标函数,并测量该第二次调用花费多长时间。如果array1[x]
等于k
,则在array2
中访问的位置在高速缓存中,并且操作趋于更快。
使用此变体,许多不同的场景都可能导致可利用的泄漏。例如,错误预测的条件分支可以检查先前计算的安全结果或对象类型,而不是执行边界检查。类似地,推测执行的代码可以采取其他形式,例如将比较结果泄漏到固定的存储器位置中,或者可以分布在更大数量的指令上。上述高速缓存状态也比可能需要的更具限制性。例如,在一些场景中,即使array1_size
被高速缓存,如即使比较中涉及的值是已知的,如果在推测执行期间应用分支预测结果,攻击也会起作用。根据处理器的不同,推测执行也可能在各种情况下启动。更多的变体将在第VI节中讨论。
我们在多个x86处理器架构上进行了实验,包括Intel Ivy Bridge(i7-3630QM)、Intel Haswell(i7-4650U)、Intel Broadwell(i7-5650U)、Intel Sky lake(GoogleCloud上 未指 定的 Xeon, I5-6200U, i7-6600U, i76700K)、英特尔Kaby Lake(i7.7660U)和AMD Ryzen。在所有这些CPU上都发现了幽灵漏洞。在32位和64位模式以及Linux和Windows上都观察到了类似的结果。一些基于ARM架构的处理器也支持推测执行[7],我们在高通Snap dragon 835SoC(配备高通Kyro280CPU)和三星Exynos 7420O ctaSoC(配备Cortex-A57和Cortex-A53CPU)上进行的初步测试证实,这些ARM处理器受到了影响。我们还观察到,推测执行可以远远领先于指令指针。在Haswell i7-4650U上,代码附录C(参见第IV-B节)在 “if”语句和访问array1/array2
的行之间的源代码中插入了多达188条简单指令,这些指令刚好位于该处理器的重新排序缓冲区中的192个微操作之下(参见第II-B节)。
附录C包括一个用于x86处理器1的概念验证代码,该代码与第IV节中的描述密切相关。未优化的实现可以在i7-4650U上以10KB/s读取的数据,错误率较低(<0.01%)。
我们在JavaScript中开发了一个概念验证,并在Google Chrome版本62.0.3202中对其进行了测试,该版本允许网站从其运行的进程中读取私有内存。代码如清单2所示。
在分支预测器偏移通过时,将索引设置(通过位操作)为范围内的值。在最后一次迭代中,将索引设置为SimpleBytearray
中的越界地址。我们使用变量localJunk
来确保操作不会被优化。根据ECMAScript 5.1
第11.10节[13],“|0”操作将值转换为32位整数,作为JavaScript解释器的优化提示。与其他优化的JavaScript引擎一样,V8执行即时编译(just-in-time),将JavaScript
转换为机器语言。虚拟操作被放置在代码周围清单2将SimpleBytearray.length
存储在本地内存中,以便在攻击期间可以将其从缓存中删除。看到清单3用于从D8得到的反汇编输出。
由于无法从JavaScript访问clflush
指令,因此我们改为使用缓存逐出[27,51],即我们以这样的方式访问其他存储器位置,使得目标存储器位置随后被逐出。泄漏的结果被传达通过probeTable[n * 4096]
的缓存状态$n \in 0..255$,因此攻击者必须逐出这256条缓存行。 长度参数(JavaScript代码中的simpleByteArray.length
和反汇编中的[ebp-0xe0])也需要逐出。JavaScript不提供对rdtscp
指令的访问,并且Chrome故意降低其高分辨率计时器的准确性,以阻止使用performance.now()
[62]的计时攻击。然而,HTML5的Web Workers特性使得创建一个单独的线程变得简单,该线程可以反复递减一个在共享内存位置中的值[24,60]。该方法能产生一个提供足够精确的高精度定时器。
作为利用条件分支的第三个示例,我们开发了一个可靠的概念验证,它通过滥用eBPF(扩展BPF)接口,在没有针对幽灵的补丁的情况下,从未经修改的Linux内核中泄漏内核内存。eBPF是基于Berkeley数据包过滤器(BPF)的Linux内核接口[49]且可用于各种目的,包括根据数据包的内容对其进行过滤。eBPF允许非特权用户在内核上下文中触发用户提供的、内核验证的eBPF字节码的解释或JIT编译以及后续执行。攻击的基本概念类似于针对JavaScript的攻击的概念。
在此攻击中,我们仅对推测执行的代码使用eBPF代码。我们使用用户空间中的本地代码来获取隐信道信息。这与上面的JavaScript示例不同,在上面的示例中,两个函数都是用脚本语言实现的。为了推测性地访问用户空间内存中依赖于秘密的位置,我们执行对内核内存中的数组的推测性越界内存访问,其索引足够大,可以访问用户空间内存。概念验证假设目标处理器不支持管理员模式访问保护(SMAP)。然而,没有这种假设的攻击也是可能的。它在Intel Xeon Haswell E5-1650v3上进行了测试,它可以在eBPF的默认解释模式和非默认JIT编译模式下工作。在高度优化的实施中,我们能够在此设置中泄漏高达2000B/s。它还在AMD Pro A8-9600 R7处理器上进行了测试,在该处理器上,它只能在非默认的JIT编译模式下工作。我们将对这一原因的调查留待今后的工作。
eBPF子系统管理存储在内核存储器中的数据结构。用户可以请求创建这些数据结构,然后可以从eBPF字节码访问这些数据结构。为了加强这些操作的内存安全性,内核存储与每个这样的数据结构相关联的一些元数据,并对这些元数据执行检查。特别地,元数据包括数据结构的大小(当数据结构被创建并用于防止越界访问时被设置一次)和来自被加载到内核中的eBPF程序的引用的数量。引用计数跟踪有多少引用该数据结构的eBPF程序正在运行,从而确保在加载的eBPF程序引用该数据结构时不会释放属于该数据结构的内存。
我们通过滥用假共享来增加针对eBPF管理的数组的长度的边界检查的延迟。内核将数组长度和引用计数存储在同一缓存行中,允许攻击者将包含数组长度的缓存行移动到另一个处于修改状态的物理CPU内核上(参见[16,53]).这 是通过加载和丢弃引用另一物理核心上的eBPF阵列的eBPF程序来完成的,这使得内核递增和递减另一物理核心上的阵列的引用计数器。该攻击在Haswell CPU上实现了大约5000B/s的泄漏率。
幽灵攻击可以高精度地揭示数据,但由于多种原因可能会出现错误。发现内存位置是否被缓存的测试通常使用计时测量,其准确性可能受到限制(例如在JavaScript或许多ARM平台中)。因此,可能需要多次攻击迭代才能做出可靠的判断。如果array2
元素意外地被高速缓存,例如,由于硬件损坏、操作系统活动或访问存储器的其他进程(例如,如果array2
对应于其他进程正在使用的共享库中的存储器),则也可能发生错误。攻击者可以重做导致array2中没有元素或2+个元素被缓存的攻击传递。在英特尔Sky lake和Kaby Lake处理器上,使用这种简单的重复标准(但没有其他错误纠正)和精确的基于RDTSCP的时序的测试产生了大约0.005%的错误率。
1 if (index < simpleBytearray.length){ 2 index = simpleBytearray[index| 0]; 3 index = (((index * 4096)|0) & (32 * 1024 * 1024 - 1))|0; 4 localJunk ˆ= probeTable[index|0]|0; 5 }
清单2:通过JavaScript实现推测执行的利用。
1 cmpl r15,[rbp-0xe0] ; Compare index (r15) against simpleBytearray.length 2 jnc 0x24dd099bb870 ; If index >= length, branch to instruction after movq below 3 rex.w leaq rsi,[r12+rdx*1] ; Set rsi = r12 + rdx = addr of first byte in simpleBytearray 4 movzxbl rsi,[rsi+r15*1] ; Read byte from address rsi+r15 (= base address + index) 5 shll rsi,12 ; Multiply rsi by 4096 by shifting left 12 bits 6 andl rsi,0x1ffffff ; AND reassures JIT that next operation is in-bounds 7 movzxbl rsi,[rsi+r8*1] ; Read from probeTable 8 xorl rsi,rdi ; XOR the read result onto localJunk 9 rex.w movq rdi,rsi ; Copy localJunk into rdi
清单3:清单2中JavaScript示例的反汇编。
图2:分支预测器在攻击者中(错误)训练受控上下文A。在上下文B中,分支预测器基于来自上下文A的训练数据进行预测,导致在攻击者选择的地址处进行推测执行,该地址对应于受害者地址空间中的幽灵 gadget 的位置。
在本节中,我们将演示攻击者如何毒害间接分支,以及如何利用间接分支的错误预测从另一个上下文(例如,另一个进程)读取任意内存。间接分支通常用于所有体系结构的程序中(参见第II-C节)。如果间接分支的目的地址的确定被延迟,例如由于高速缓存未命中,则推测执行通常将在从先前代码执行预测的位置继续。
在幽灵变体2中,敌手用恶意目的地扰乱分支预测器,使得推测执行在敌手选择的位置继续。中对此进行了说明参见图2,其中分支预测器在一个上下文中被(误)训练,并且在不同的上下文中应用该预测。更具体地说,攻击者可以将推测执行错误地指向在合法程序执行期间永远不会发生的位置。由于推测执行留下了可测量的副作用,这是一个极端的攻击者的强大手段,例如,即使在没有可利用的条件分支错误预测的情况下,也会暴露受害者内存(参见部分IV)。
对于一个简单的攻击示例,我们考虑攻击者试图读取受害者的内存,当发生间接分支时,受害者可以控制两个寄存器。这通常发生在现实世界的二进制文件中,因为当寄存器包含攻击者控制的值时,操作外部接收的数据的函数通常会进行函数调用。经常是这些被调用的函数会忽略这些值,而只是在函数序言中将它们压入堆栈,并在函数尾声中恢复它们。
攻击者还需要找到一个“幽灵 gadget ”,即一个代码片段,其推测执行将把受害者的敏感信息转移到隐蔽信道中。对于该示例,简单而有效的 gadget 将由两个指令(不一定需要相邻)形成,其中第一个指令是加法(或异或、减法等)。由攻击者控制的寄存器R1寻址到攻击者控制的寄存器R2上的内存位置,后面是访问R2中该地址的内存的任何指令。在这种情况下, gadget 为攻击者提供了(通过R1)对泄漏地址的控制,以及(通过R2)对泄漏内存如何映射到由第二条指令读取的地址的控制。在我们测试的CPU上, gadget 必须驻留在内存中,由受害者执行,以便CPU执行推测执行。然而,由于有几兆字节的共享库映射到大多数进程中,[25],攻击者有足够的空间来搜索 gadget ,甚 至不必搜索受害者自己的代码。
许多其他攻击是可能的,这取决于攻击者已知或控制的状态、攻击者所寻求的信息驻留的位置(例如,寄存器、堆栈、存储器等)、攻击者控制推测执行的能力、可用于形成 gadget 的指令序列以及可从哪些信道泄漏信息投机活动。例如,如果攻击者能够简单地在一条指令上诱导推测执行,从而将寄存器中指定地址的内存带入缓存,那么在寄存器中返回秘密值的加密函数可能会被利用。同样,尽管上面的示例假设攻击者控制两个寄存器,但对于某些 gadget 来说,攻击者对单个寄存器、堆栈上的值或内存值的控制是足够的。
在许多方面,利用漏洞与面向返回的编程(ROP)类似,只是正确编写的软件易受攻击, gadget 的持续时间有限,但不需要完全终止(因为CPU最终会识别推测错误), gadget 必须通过侧信道而不是显式地过滤数据。不过,推测执行可以执行复杂的指令序列,包括从堆栈读取、执行算术、分支(包括多次)和读取内存。
在x86处理器上错误训练分支预测器。攻击者根据自己的上下文对分支预测器进行错误训练,以诱使处理器在运行受害者代码时推测性地执行 gadget 。我们的攻击过程模仿受害者的分支模式,导致分支被误导。
请注意,不同CPU的历史误训练要求各不相同。例如,在Haswell i7-4650U上,使用了大约29个先前目标地址的低20位,尽管观察到了对这些地址的进一步散列。在AMD Ryzen上,只使用大约前9个分支的低12位。附录A中提供了用于更新英特尔至强Haswell E5-1650v3上分支历史缓冲区的反向工程伪代码。
此外,我们在攻击者和受害者进程中的同一虚拟地址处设置了误训练跳转。请注意,这可能不是必需的,例如,如果CPU仅根据跳转地址的低位对预测进行索引。当错误训练分支预测器时,我们只需要模拟虚拟地址;物理地址、时间和进程ID似乎无关紧要。由于分支预测不受其他内核上操作的影响(参见第II-C节),因此必须在同一CPU内核上进行任何误训练。
我们还观察到分支预测器从跳转到非法目的地的过程中学习。尽管在攻击者的过程中触发了异常,但这很容易被捕获,例如,在Linux上使用信号处理程序或在Windows上使用结构化异常处理。与前一种情况一样,分支预测器随后将做出预测,将其他进程发送到同一目标地址,但在受害者的虚拟地址空间(即 gadget 所在的地址空间)中。
与我们关于条件分支预测失误(参见第IV-A节)的结果类似,我们观察到多个x86处理器架构上的间接分支毒化,包括英特尔常春藤桥(i7-3630QM)、英特尔哈斯韦尔(i7-4650U)、英特尔布罗德韦尔(i7-5650U)、英特尔天湖(谷歌云上未指定的至强、i5-6200U、i7-6600U、i7-6700K),英特尔 Kaby Lake(i7-7660U)、AMD Ryzen以及一些ARM处理器。我们能够在32位和64位模式以及不同的操作系统和虚拟机监控程序上观察到类似的结果。
为了衡量分支毒化的有效性,我们实施了一个测试受害者程序,该程序重复执行32个间接跳转的固定模式,使用clflush
刷新最终跳转的目标地址,并在探测内存位置上使用Flush+Reload。受害者程序还包括一个测试 gadget ,可以读取探针位置,并且永远不会被合法执行。我们还实施了一个攻击程序,该程序重复执行31次间接跳转,其目的地与受害者序列中的前31次跳转相匹配,然后间接跳转到受害者 gadget 的虚拟地址(但在攻击过程中,该地址的指令会将控制流返回到第一次跳转)。
在Haswell(i7-4650U)处理器上,受害者进程每秒执行270万次迭代,攻击成功毒害了99.7%的最终跳跃。在Kaby Lake(i7-7660U)处理器上,受害者每秒执行310万次迭代,毒化率为98.6%。当攻击进程停止或在另一个内核上执行时,在探测位置未观察到虚假缓存命中。因此,我们得出结论,间接分支毒化非常有效,包括速度远远高于攻击者试图毒化的典型受害者程序执行给定间接跳转的速度。
作为概念验证,我们构建了一个简单的目标应用程序,该应用程序提供计算密钥和输入消息的SHA1散列的服务。该实现由一个程序组成,该程序连续运行一个循环,该循环调用Sleep(0),从文件加载输入,调用Windows加密函数来计算散列,并在输入发生变化时打印散列。我们发现Sleep()调用是使用寄存器ebx、edi和攻击者已知的edx值中输入文件的数据完成的,即两个寄存器的内容由攻击者控制。这是本节开头介绍的幽灵gadget类型的输入标准。
搜索受害者进程的可执行内存区域,我们在ntdll中识别出一个字节序列。dll(在Windows8和Windows10上),它形成以下(可能未对齐)指令序列,用作幽灵攻击 gadget :
adc edi,dword ptr [ebx+edx+13BE13BDh]
adc dl,byte ptr [edi]
利用攻击者控制的ebx
和edi
推测性地执行此 gadget ,可以让攻击者读取受害者的内存。攻击者将edi设置为探测器阵列的基址,例如共享库中的内存区域,并将ebx设置为m−0x13BE13BD−edx。因此,第一条指令从地址m读取32位值,并将其添加到edi中。然后,第二条指令将探测数组中的索引m提取到缓存中。类似的 gadget 也可以在第一条指令的字节读取中找到。
对于间接分支毒化,我们的目标是Sleep()函数的第一条指令,因为ASLR,每次重新启动时,跳转目标的位置和目标本身都会发生变化。为了让受害者推测性地执行 gadget ,包含跳转的内存位置从缓存中溢出,分支预测器错误地将推测执行发送到幽灵 gadget 中。由于包含跳转目的地的内存页被映射为写时复制,我们可以通过修改Sleep()函数的攻击者副本、将跳转目的地更改为gadget地址并在那里放置ret指令来错误训练分支预测器。然后通过多次从多个线程跳转到gadget地址来完成误训练。
Win32上的代码ASLR只会更改几个地址位,因此只需尝试几个组合即可找到对受害者有效的训练序列。使用一个由指令sbbeax[esp+ebx]组成的指令 gadget 来定位堆栈。
在攻击过程中,使用了一个单独的线程来错误训练分支预测器。该线程与受害者运行在同一个内核上(例如,通过超线程),因此共享分支预测器状态。因为分支预测器使用之前的跳转历史进行预测,所以每次误训练迭代都会模仿受害者在跳转之前的分支历史来重定向。尽管误训练可能与受害者的确切虚拟地址和指令类型完全匹配,但这并不是必需的。相反,每个误训练迭代使用一系列ret指令,其目标地址与受害者跳转历史的低20位相匹配(映射到一个1MB(220字节)可执行数组中的地址,该数组中填充了ret指令)。在模拟历史之后,误训练线程执行跳转到重定向(被修改为跳转到 gadget )。
然后,攻击者可以通过选择ebx(调整要读取的内存地址)和edi(调整读取结果映射到探测器阵列的方式)的值来泄漏内存。然后,攻击者使用Flush+Reload从受害者进程中推断出值。在清单1中,读取值分布在缓存行上,因此可以很容易地推断出来。然而,在上面的示例中,该值的最不重要的6位不会分布在缓存行上,因此,落入同一缓存行的值无法通过基本的Flush+Reload
攻击进行区分。为了区分这些值,探测器阵列的基址可以按字节移位,以确定访问值落入连续缓存行的阈值。通过重复攻击,攻击者可以从受害者进程中读取任意内存。在Intel Haswell(i7-4650U)上的一个未优化概念验证实现,攻击者使用文件影响放置在RAM驱动器上的受害者寄存器,读取速度为41B/s,包括回溯和纠正错误的开销(约占尝试次数的2%)。
现在,我们将介绍对Intel Haswell branch predictor内部进行反向工程的基本方法,为针对KVM的攻击做准备。这种逆向工程有助于优化分支预测器误训练或描述处理器的漏洞,尽管在实践中,在不完全了解分支预测器的情况下,往往可以实现误训练。
第V-D节介绍了对KVM的攻击。
对于逆向工程,我们从公共来源获得的信息开始。英特尔的公开文档包含一些关于其处理器中分支预测实现的基本但权威的信息[35]。AgnerFog[15]介绍了英特尔Haswell处理器分支预测背后的基本思想。最后,我们使用了之前研究中的信息,这些信息对英特尔处理器上预测直接跳跃的方式进行了反向工程[14]。
分支历史缓冲区(BHB)的结构是[15]中模式历史的逻辑扩展。BHB有助于根据指令历史进行预测,同时保持简单性和提供滚动哈希的特性。这自然会产生一个包含重叠数据、XOR组合(混合两段数据的最简单方式)的历史缓冲区,并且在历史缓冲区内没有额外的向前或向后传播(以简单的方式保留滚动哈希属性)。
为了确定分支预测器使用的精确函数,利用了预测器冲突。我们设置了两个超线程,它们运行相同的代码,导致具有不同目标的highlatency间接分支。超读A中的进程配置为执行跳转到目标地址1,而超读B中的进程配置为执行跳转到目标地址2。此外,代码被放在目标地址2的超线程中,该地址加载缓存行以进行Flush+Reload
。然后,我们测量了缓存行在超线程中加载的频率;这是预测失误率。高预测失误率表示处理器无法区分这两个分支,而低预测失误率表示处理器可以区分它们。在其中一个线程中应用了各种更改,例如在地址中一次跳转一个或两个位。然后,预测失误率就像一个二进制甲骨文,显示给定的位是否会影响分支预测(单位预测),或者两位是否异或在一起(两位预测在单独预测时会导致高低预测失误率,但在同时预测时会导致低预测失误率)。
结合这些知识可以得到图3所示的概述。
我们实施了一种攻击(使用Intel Xeon Haswell E5-1650v3,运行Linux内核包Linux-image-4.9.03-amd64,版本为4.9.30-2+deb9u2),该攻击会从来宾虚拟机内部泄漏主机内存,前提是攻击者可以访问来宾环0(即完全控制虚拟机内部运行的操作系统)。
图3:多种机制影响直接、间接和条件分支的预测。
攻击的第一阶段确定有关环境的信息。它通过分析分支历史缓冲区和分支目标缓冲区泄漏来确定虚拟机监控程序ASLR的位置[14,72]。它还使用通过分支目标注入执行的幽灵gadget查找三级缓存集关联信息[48],以及物理内存映射位置信息。此初始化步骤需要10到30分钟,具体取决于处理器。然后,它通过使用间接分支毒化(又称分支目标注入)在虚拟机监控程序内存中作为幽灵 gadget 执行eBPF解释器,以间接分支的主要预测机制为目标,从Attackercosen地址泄漏虚拟机监控程序内存。我们能够泄漏1809B/s,其中1.7%的字节错误/不可读。
在推测执行期间发生的缓存状态。未来处理器(或具有不同微码的现有处理器)的行为可能会有所不同,例如,如果采取措施防止推测执行的代码修改缓存状态。在本节中,我们将研究攻击的潜在变体,包括推测执行如何影响其他微体系结构组件的状态。一般来说,幽灵攻击可以与其他微体系结构攻击相结合。在本节中,我们将探讨潜在的组合,并得出结论,几乎任何可观察到的投机性执行代码的影响都可能导致敏感信息泄漏。尽管测试的处理器不需要以下技术(而且尚未实施),但在设计或评估缓解措施时,了解潜在的变化是至关重要的。
幽灵变种4。幽灵变种4使用存储中的推测来加载转发逻辑[31]。处理器推测负载不依赖于之前的存储[73]。开采机制类似于我们在本文中详细讨论的变体1和变体2。
驱逐+时间。逐出+时间(Evict+Time)攻击[52]的工作原理是根据缓存的状态测量操作的时间。
如下所示该技术同样可适用于幽灵攻击。考虑以下代码:
if(false but mis predicts as true)
read array1 [R1]
read [R2]
假设寄存器R1包含一个秘密值。如果array1
[R1]的推测执行的内存读取是缓存命中,那么内存总线上不会发生任何事情,从[R2]的读取将快速启动。如果对array1
[R1]的读取是缓存未命中,则第二次读取可能需要更长时间,从而导致受害线程的计时不同。此外,系统中可以访问内存的其他组件(例如其他处理器)可能能够感知内存总线上存在的活动或内存读取的其他影响,例如,更改DRAM行地址选择[56]。我们注意到,与我们已经实现的攻击不同,即使推测执行不会修改缓存的内容,这种攻击也会起作用。所需的只是缓存的状态会影响推测执行的代码或某些其他属性的时间,这些属性最终会对攻击者可见。
指令定时。幽灵漏洞不一定需要涉及缓存。定时取决于操作数值的指令可能会泄漏操作数的信息[6]。在下面的示例中,乘法器被乘法R1、R2的推测执行占用。乘法器可用于乘法R3、R4的时间(用于乱序执行或识别预测失误后)可能会受到第一次乘法的时间的影响,从而显示有关R1和R2的信息。
if(false but mis predicts ast rue)
multiply R1,R2
multiply R3,R4
寄存器文件上的争用。假设CPU有一个寄存器文件,其中有一定数量的寄存器可用于存储检查点,以便推测执行。在下面的示例中,如果第二个“if”中R1上的条件为真,则将创建一个比R1上的条件为假更多的推测执行检查点。如果攻击者可以检测到该检查点,例如,如果超线程中的代码推测执行由于存储不足而减少,则会显示有关R1的信息。
if(false but mis predicts as true)
if(condition on R1)
if(condition)
投机处决的变体。即使是不包含条件分支的代码也可能存在风险。例如,考虑攻击者希望确定R1是否包含攻击者选择的值X或某个其他值的情况。做出这种判断的能力足以破坏某些加密实现。攻击者错误地训练了分支预测器,因此在中断发生后,中断会返回对读取内存[R1]的指令的预测失误。然后,攻击者选择X对应于适合Flush+Reload
的内存地址,显示R1是否=X。当iret指令在英特尔CPU上序列化时,其他处理器可能会应用分支预测。
利用任意可观察的效果。实际上,推测执行的代码的任何可观察效果都可以用来创建泄漏敏感信息的隐蔽信道。例如,考虑清单1中的示例运行在一个处理器上,其中推测读取不能修改缓存。在这种情况下,array2中的推测查找仍会发生,其时间将受到进入推测执行的缓存状态的影响。这一时机反过来会影响后续投机操作的深度和时机。因此,通过在投机性执行之前操纵缓存的状态,攻击者可以潜在地利用投机性执行的几乎任何可观察到的效果。
if(x < array1_size){
y = array2[array1[x]* 4096];
// do something detectable when
// speculatively executed
}
最终可观测操作可能涉及几乎任何侧信道或隐蔽信道,包括对资源(总线、算术单元等)的争夺和常规侧信道发射(如电磁辐射或功耗)。
更普遍的形式是:
if(x < array1_size){
y = array1[x];
// do some thing using y that is
// observable when speculatively
// executed
}
针对幽灵攻击提出了几种对策。每种方法都解决了攻击所依赖的一个或多个功能。我们现在讨论这些对策及其适用性、有效性和成本。
幽灵攻击需要推测执行。确保指令只有在确定了通向指令的控制流时才能执行,这将防止推测执行,并由此防止幽灵攻击。虽然是一种有效的对策,但防止推测执行会导致处理器性能显著下降。
虽然目前的处理器似乎没有能允许软件层面禁用推测执行的方法,但这种模式可以在未来的处理器中添加,或者在某些情况下可能通过微代码更改引入。或者,一些硬件产品(如嵌入式系统)可以切换到不实现推测执行的替代处理器型号。不过,这种解决方案不太可能立即解决这个问题。
或者,可以对软件进行修改,以使用序列化或推测阻止指令,确保后续指令不会被推测执行。英特尔和AMD建议使用lfence
指令[4,36]。保护条件分支最安全(但最慢)的方法是在每个条件分支的两个结果上添加这样的指令。然而,这相当于禁用分支预测,我们的测试表明,这将显著降低性能[36]。一种改进的方法是使用静态分析[36]来减少所需的推测阻塞指令的数量,因为许多代码路径不具有读取和溢出内存的可能性。相比之下,微软的C编译器MSVC[54]采取了一种默认无保护代码的方法,除非静态分析器检测到已知的错误代码模式,但因此错过了许多易受攻击的代码模式[40]。
插入序列化指令也有助于减轻间接分支毒化。在间接分支之前插入lfence指令可以确保分支之前的管道被清除,并且分支被快速解析[4]。这反过来又减少了分支毒化时推测执行的指令数量。
该方法要求对所有可能存在漏洞的软件进行检测。因此,为了防护性,需要更新软件的二进制文件和库。这可能是遗留软件的一个问题。
其他对策可以防止推测执行的代码访问机密数据。谷歌Chrome浏览器使用的一种方法是在一个单独的过程中执行每个网站[67]。由于幽灵攻击仅利用受害者的权限,因此我们使用JavaScript(参见第IV-C节)执行的攻击将无法访问分配给其他网站的进程中的数据。
WebKit采用两种策略,通过推测执行的代码来限制对机密数据的访问[57]。第一种策略用索引屏蔽代替数组边界检查。WebKit没有检查数组索引是否在数组的范围内,而是对索引应用位掩码,确保它不会比数组大小大太多。虽然屏蔽可能会导致访问超出阵列边界,但这会限制边界冲突的距离,从而防止攻击者访问任意内存。
第二种策略通过使用伪随机毒药值对指针进行异或来保护对指针的访问。毒药以两种不同的方式保护指针。首先,不知道毒药值的攻击者不能使用毒化指针(尽管各种缓存攻击可能会泄漏毒药值)。更重要的是,poison值确保用于类型检查的分支指令上的预测失误将导致与类型相关的指针用于另一个类型。
这些方法对于即时(JIT)编译器、解释器和其他基于语言的保护非常有用,因为运行时环境可以控制执行的代码,并希望限制程序可以访问的数据。
未来的处理器可能会跟踪数据是否是由于推测操作而获取的,如果是这样的话,就可以防止该数据被用于可能会泄露数据的后续操作中。然而,目前的处理器通常不具备这种能力。
为了从瞬时指令中过滤信息,幽灵攻击使用隐蔽的通信信道。已经提出了多种缓解此类信道的方法(参见[19])。作为对我们基于JavaScript的攻击的尝试缓解,主要浏览器提供商进一步降低了JavaScript计时器的分辨率,可能会增加抖动[50、57、66、71]。这些补丁还禁用了SharedarrayBuffers,它可以用来创建计时源[60]。
虽然这种对策需要对第IV-C节中的攻击进行额外的平均,但它提供的保护级别尚不清楚,因为错误源只会降低攻击者过滤数据的速度。此外,正如[18]所示,当前的处理器缺乏完全消除隐蔽信道所需的机制。因此,虽然这种方法可能会降低攻击性能,但不能保证攻击不可能发生。
为了防止间接分支毒化,英特尔和AMD通过控制间接分支[4,34]的机制扩展了 ISA 。该机制由三个控件组成。第一种是间接分支限制投机(IBRS),它可以防止特权代码中的间接分支受到非特权代码中分支的影响。处理器进入一个特殊的IBRS模式,该模式不受IBRS模式之外的任何计算的影响。第二种是单线程间接分支预测(STIBP),它限制在同一内核的超线程上执行的软件之间共享分支预测。最后,间接分支预测器屏障(IBPB)防止在设置屏障之前运行的软件通过在屏障之后运行的软件影响分支预测,即通过刷新BTB状态。这些控件是在微码补丁后启用的,需要操作系统或BIOS支持才能使用。性能影响从几%到4倍或更多不等,这取决于采用的对策、它们的应用程度(例如,内核中的有限使用与所有进程的完全保护),以及硬件和微码实现的效率。
谷歌提出了一种预防间接分支毒化的替代机制,称为retpoline
[70]。retpoline是用返回指令替换间接分支的代码序列。该构造还包含确保返回指令通过返回堆栈缓冲区预测为良性无止境循环的代码,同时通过将其推到堆栈上并返回到它(即使用ret
指令)来达到实际目标目的地。当返回指令可以通过其他方式预测时,该方法可能不切实际。英特尔发布了一些处理器的微码更新,这些处理器会返回BTB进行预测,以禁用这种返回机制[36]。
支持软件安全技术的一个基本假设是,处理器将忠实地执行程序指令,包括其安全检查。本文介绍了幽灵攻击,它利用了推测执行这一违反假设的事实。我们展示的技术是实用的,不需要任何软件漏洞,允许攻击者读取私有内存并注册来自其他进程和安全上下文的内容。
软件安全从根本上取决于硬件和软件开发人员对CPU实现允许(或不允许)从计算中暴露的信息有一个明确的共识。因此,虽然上一节中介绍的对策可能有助于在短期内限制对其的实际利用,但只是权宜之计,因为通常需要有正式的体系结构保证,以确定任何特定代码构造在当今处理器上是否安全 —— 更不用说未来的设计了。因此我们认为,长期解决方案将需要从根本上改变指令集体系结构。
更广泛地说,安全性和性能之间存在权衡。本文中的漏洞以及其他许多漏洞源于技术行业长期以来对性能最大化的关注。因此,处理器、编译器、设备驱动程序、操作系统和许多其他关键组件已经进化出复杂的优化层次,带来了安全风险。随着不安全成本的上升,这些设计选择需要重新审视。在许多情况下,需要针对安全性进行优化的替代实现。
本文的几位作者独立发现了幽灵攻击,并最终促成了这项合作。我们感谢谷歌零号项目的Mark Brand提供的创意。我们感谢英特尔专业地处理了这个问题,通过明确的时间表把控以及联系了所有相关研究人员。我们感谢ARM就这个问题的各个方面进行技术讨论。我们感谢高通公司和其他供应商在披露该问题时的快速反应。最后,我们要感谢审稿人的宝贵意见。
Daniel Gruss、Moritz Lipp、Stefan Mangard和Michael Schwarz得到了欧洲研究理事会(ERC)在欧盟地平线2020研究和创新计划(第681402号赠款协议)下的支持。
Daniel Genkin获得了美国商务部、国家标准与技术研究所、2017-2018罗斯柴尔德博士后奖学金和国防高级研究计划局(DARPA)根据FA8650-16-C-7622合同授予的NSF奖 #1514261 和 #1652259 、金融援助奖 70NANB15H328 的支持。
详见原文
详见原文
]]>从前端和cpp
中了解到的匿名函数与lambda
表达式,作为一名React
的爱好与使用者,对于Hook
已经是非常熟悉的了。
在React
目前热门且主流的Hook
声明式写法中,存在一个非常有名的公式如下:
$$ UI = f(state) $$
这个公式,可以说是典型的函数式编程,它的意思是,UI
是一个函数,它的参数是state
,返回值是UI
。在使用Hook
多个月后,笔者对这个清晰简单的写法产生了很大的兴趣,在使用其时,在框架上的心智负担非常大小,不需要详细记住很多的生命周期函数/副作用函数,只需要建立好Hook
的抽象思维即可,关于Hook
与声明式编程,有兴趣的同学可以去直接查阅React
文档以及Flutter
文档,其中有很清晰的介绍。
于是笔者去了解了函数式编程,看到了王垠大佬对相关编程语言的赞誉,于是笔者找到了Lisp
这门古老的语言,当年看黑客与画家
时 又名“Lisp是世界上最好的语言(” ,就已经有所好奇 可惜并没有坚持看下去 ,感觉现在是时候认真的深入学习下了。
最开始直接去看Lisp
文档时,几乎是一脸懵,括号语言?什么鬼?习惯了过程式、命令式以及面向对象编程的程序员几乎很难一下子适应,所以笔者先去了解了下函数式编程的一个核心思想,Lambda Calculus
,即Lambda演算。
Lambda
是一个纯净的函数,只由函数的input
和ouput
来表示,Lambda
符号$\lambda$只是一个标识特征,.
是一个分隔符,前面是input
,后面是output
,函数主要的逻辑与运算都在ouput
中。所以也可以表示如$\lambda , input , . , ouput$。
一个典型的Lambda表达式
如下,就如$E = mc^2$一样优雅且简洁,并且其演算形式已经被证明是图灵完备的,即能能用纯函数实现一个while
循环,存在停机问题,这个后文会加以说明。
$$ \lambda x . (x+1) $$
学会了Lambda
的表示与思想,就能瞬间学会很多语言的匿名函数或者说Lambda
特性。不过不同语言对于Lambda
的实现特性存在差异。比如Python
的Lambda
是一个严格的实现,其ouput
体内仅仅允许单一的表达式,像javascript
与cpp
的实现则可以包含多条计算。
$\lambda$ 到普通函数的形式化变换如下:
$$ \lambda x . (x+1) \quad \hArr \quad f(x)=x+1 \quad \hArr \quad x+1=f(x) $$
上面的例子同样可以用c
语言定义如下,不过Lambda
函数计算中全是匿名的,即无法用 变量/标识符 进行 引用/表示 :
int lambda(int x){
return x+1;
}
看了上文的同学可能会疑惑,这不就是一个几乎所有高级语言都有的基本特性,定义一个函数么?这怎么实现一个while
循环,乃至实现一个图灵机,证明图灵完备?不要着急,下面开始进行解释。
首先,有三个法则需要我们作为前置知识进行了解,分别是:$\alpha - \text{变换}$、$\beta - \text{规约}$、$\eta - \text{规约}$,简单来分别是变量等效替换、代入推导,函数等效替换。
他们的公式表达如下:
$\alpha-$变换
$$ \lambda x.\lambda y.2 * x+y \quad \hArr \quad \lambda y.\lambda x. 2 * y+x $$
$\beta-$规约
函数中的自由变量(即ouput
中不由当前对应的input
声明的变量),可以被其上文中的变量的值代入替换,即化简规约。公式样例如下,其中$\lambda y.2*x+y$中的自由变量为$x$,可以被值为3
的$x$规约:
$$ \lambda x.\lambda y. 2 * x + y \quad 3 \quad 2\quad \rArr \quad \lambda y.2 * 3+y \quad 2 \quad \hArr \quad \lambda x.2 * 3 +x \quad 2 \ \rArr 2*3+2 \rArr 8 $$
上文公式先进行关于 $x$ 的 $\beta$ 规约,再进行 $\alpha$ 变换,最后进行关于 $y$ 或者说 $x$ 的 $\beta$ 变换。
再解释下,对于$\lambda x . (x+1)$,需要输入一个$x$作为实际参数才能规约,即实际能够规约的表示为$\lambda , x . (,x + 1)\quad 3$,规约最终结果为$3+1$即$4$。
$\eta-$规约
如果定义域区间上,函数g
和函数f
的等效,那么g
就可以被规约为f
,这个规约的公式如下:
$$ if \quad g(x) \equiv f(x) \quad \forall x \ then \quad \lambda x.g(x) \quad \hArr \quad \lambda x.f(x) $$
假设如果有读者坚持看完上文还没睡着且理解了的话,那么就可以开始进行后续最精彩 无聊且难 的部分了,Y算子的实现。
对于上面如何用函数实现一个while
循环,关键就是靠Y算子。Y算子的定义如下:
$$ \lambda f.(\lambda x. f(x , x)) , (\lambda x. f(x , x)) $$
Y算子可以接收一个待接收一个函数的的函数H,即表达式$Y H$,并根据情况来规约成$H , \text{target function}$还是$H$加上$Y H$自身即$H (Y H)$,以此来产生递归,即循环,具体推导可以见下方参考文献。
接下来我们开始用Lambda
思想编写javascript
,其中包括了Y算子的JavaScript实现。要读懂下面的代码,首先建议了解下currying,即柯里化。首先是完成一个递归计算阶乘的函数的完全体Overview(概览)。
// factorial function
const fac = n => (n == 0 ? 1 : n * fac(n - 1))
// fac = H fac .1
const H = f => n => (n == 0 ? 1 : n * f(n - 1))
// fac = Y H .2
// Y H = H fac .3=1+2
// Y H = H (Y H) .4=3+2
const Y = f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v)))
console.log({ fac_result: fac(10) })
console.log({ fac_result: H(fac)(10) })
console.log({ result: Y(H)(10) })
console.log({
result:
(f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v))))
(f => n => (n == 0 ? 1 : n * f(n - 1)))
(10)
})
// 上述代码放到浏览器/nodejs环境中执行,结果如下x3:
// { result: 3628800 }
// 是不是很神奇?特别是对于第三个log的结果
任何的递归均有一个等价的循环实现,也就是一个while
的实现,进而可以实现一个图灵机,证明图灵完备。
当然这里可能还会有个疑惑,用while
实现的递归,是不是更加的慢?而且由于嵌套的函数调用栈的存在,内存占用等也会极高,比如一个100的循环,那么就会有100个保存的调用栈?那栈复杂度不是会约等于时间复杂度???
哈哈哈,函数式当然有自己的解决方案,那就是大名鼎鼎的尾调用(Tail Calls),就是把下一个函数的调用放当前函数的最后,直接弹出当前栈,弹入调用栈,这样就可以避免函数调用栈的增加,这样就可以达到更好的性能。可惜ECMAScript
对于Syntactic Tail Calls (STC)的提案2016
年pendding至今2022-02-26
,看issue讨论好像是不方便开发调试时的现场定位。
继续以上面的javascript
代码为例子做展开,比如上面提到的$\lambda x . (x+1)$,就可以编写如下:
x => x + 1
// or
(x) => { return x + 1; }
Lambda
的计算过程,其实就是规约的过程,需要有实际的参数输入,那么我们来表示 $\lambda x . (x+1) \quad 3 $ 如下:
(x => x + 1)(3)
// or
((x) => { return x + 1; })(3)
上面的代码都是可以在浏览器/nodejs环境下运行的哦,当然其他语言也有类似的实现。
后续更多的代码细节推荐查阅这篇博客:ES6函数与Lambda演算 | 小蘿蔔丁
那么到现在,已经具备理论基础了,可以去看Common Lisp
文档及其相关的库啦,当然这里笔者还参考了他人的最佳实践。
下面这是一个用Common Lisp
编写的Web记录应用,可以创建、检索、删除记录,并且可以持久化数据。
具体代码及其介绍与参考,见Github: https://github.com/Kingfish404/web-notes-lisp
好了,Lisp函数式之旅到这里暂以告落了,计算机科学的世界非常有趣,旅途愉快,下次见~
毛星云,网络ID「浅墨」,90后,热爱游戏开发、游戏引擎、计算机图形、实时渲染等技术,就职于腾讯互娱。
https://www.zhihu.com/people/mao-xing-yun
https://blog.csdn.net/poem_qianmo
我有一个梦想,将来的某一天,大家都能玩到拥有自己本土文化的优质游戏。
我有一个梦想,有一天,西游记能出ACT,让老外去体会中国文化西游记中”斗战胜佛”的打击快感,那一定比西方的动作巅峰之作《战神》、《鬼泣》更加深邃。
我有一个梦想,有一天,上海滩能出沙盒游戏,而不是玩《GTA》感受美国梦,亦或是玩着《热血无赖》体验国外公司强行塞给我们的”中国文化“。
我有一个梦想,有一天,不少3A大作不需要汉化,因为是我们自己的游戏,配音是中国的,文化也是中国的。
我有一个梦想,将来的某一天,国产游戏能像中国的其他产业一样,以一个领跑者的姿态,面对全世界,面对全宇宙,器宇轩昂,扬眉吐气。
这会是由我们一起去完成的梦想。
等着我们的好消息。
致谢·不是一个人在战斗--浅墨_毛星云
有幸曾经在知乎上看到过他的回答,后来也了解过他所翻译与著的书籍。
作为游戏爱好者与开发者,感到非常难过,希望他的梦想在未来能够得以实现。
]]>于是,使用ih5这个开发工具,完成了大广赛交互组的作品制作,产品是哇哈哈,主题为唤醒这座城市。
低代码开发的经历,开发体验就是非常快,不需要处理复杂的代码,而且非常多的功能和动效,可以通过组件拖拽和面板选项设置的方式,直观的完成,在简单的学习了一下开发模式后,就能很快的上手开发。
开发的时候还不需要去查文档,因为基本上都是所见即所得,所有功能列表列出,拖拽出来就可以看到并使用。
抽象开发中的各个实体,也和通过编码构建dom树非常类似,ih5也是为每个场景可以选择去构建一/多个节点树,并将不同页面通过场景分开,可以设置过渡和切换效果。
在了解低代码开发的时候,我还注意到了,这样一个仓库的存在
taowen/awesome-lowcode: 国内低代码平台从业者交流 - GitHub
对其阅读和递归了解后,感觉收益很大。
下面引用一下其中的原文:
no code / low code / pro code 一切的改进都是源自于人类的缺陷
no code:自己编程给自己用,给用户的感觉是一个更强大的办公/实用软件。主要的手段是用图形化操作等方式降低学习曲线。no code 一定要面向非常固定的领域才能做到好用。 low code:编程给其他人用,为此创造了一个 citizen developer 的概念。主要的手段是平台预制好常见的需求,减少需要从头写的代码。low code 也要面向指定的领域才能让平台提前预测需求,但相比 no code 可以不把使用场景限定得那么死。 pro code:low code 的平台自己不会选择 low code 来创建这个平台本身,因为 low code 并没有降低从头构建一个系统的成本。但是 pro code 的平台自己会选择 pro code 来创建这个平台本身,比如 react 开发者会选择用 react 来创建自己的开发工具,因为 pro code 的工具和平台都是以从根本上降低从头构建一个系统的复杂度为目标的。
活动页面,简单的投票统计页面等。听说阿里的活动页面70%+都是低代码开发工具完成的。
反正各种通用化的简单页面都能做。
来给我的部门打个广告:这是一个阿里中台部门,而且非常的期待有更多新同学的加入,如果大家感兴趣,可以去投递呀(今年投递链接见REF,不仅仅是实习生,也欢迎校招和社招的同学)。
附上一段官方介绍:
最贴近阿里大中台战略的前端团队。主要业务包括阿里集团电商基础业务、设计中台建设、前端中台建设、海外新兴市场电商业务、CFO大财务战略中台,基础业务覆盖交易、商品、会员、评价、店铺等多个阿里电商体系大闭环中的核心业务平台。我们同时拥有 Fusion、BizCharts、ARMS 前端监控、Beidou 同构渲染框架等多个技术人自研开源项目和产品。
在企业中实习,最大的收获可能是接触商业大型项目以及对于业务领域的理解。
满足客户的需求是团队最重要的事情,再厉害的技术,也需要找到它能够产生价值的点,这样才能被继续进行推进与发展。
阿里主要是是弹性工作制,并且没有打卡,实习生的考勤也是自己进行填报。对于我所在的部门而言,其实并没有传闻中的996
现象,大家也都是在自己的岗位做好自己的事情,平衡好自己的生活和工作。
996等过多长时间的工作,会摧残人的创造性,长期来看对项目不利。
对于技术和具体业务的内容,感觉可能会涉及到保密的要求,所以就不谈啦。
不过我在实习期间,将前端的技术栈和中间件,在项目开发使用之余,还进行了一次review
,这个感觉对于自己的提升非常的大,特别是对于多个中间件,react生态以及开发技能的磨练,对于语言特性的理解,是自己一个人折腾所很难理解到的。
实习是对自己技能的一次很好的检验,同时也能够很好的提高自己的技术,认识到自己的不足,结实到许多业界前沿的人士,在和他们交流中获取到许多独到的见解。
最后,期待阿里会有更好的未来。
聪明、乐观、皮实且自省的人聚在一起一定可以创造出更多有意思且有意义的产品。
这里笔者默认读者都了解过React和Vue中都有的虚拟Dom(vnode)概念,同时也对这类框架的更新原理,diff算法有一定的认知。
简单来说,本文前置知识要求:
听说React团队在实现Fiber过程中考虑使用协程来实现,但是由于不适合/工程量太大而没有最终使用。
React在引入Fiber之前,由于其Diff算法需要递归去判断来对视图层做更新,就算实现了O(n)复杂度的Diff算法,但是由于其需要完成一次Diff才能更新视图层,所以导致当有递归层级非常深的组建树需要进行更新时,会产生用户感知非常明显的卡顿,这个问题就是Fiber解决的核心问题。
前端各种技术最根本的目的都是为了提高用户体验
React通过Fiber架构,让Reconcilation过程变成可被中断。'适时'地让出CPU执行权,通过这种方式,能够让浏览器及时地响应用户的交互,用户拥有了更好的体验,编译器也有机会进行编译优化(JIT)及进行热代码优化。
实现Fiber的数据结构是链表,可以说,React升级到Fiber,就是一个数据结构课可能很常见做过的,递归转循环的过程。因为递归变成了循环,所以能够非常方便的回到顶层环境,找到上下文,对真实的dom节点进行更新。
在绝大部分语言中,通过将程序的循环结构变成递归,减少了很多次的函数调用,可以极大的提高程序的效率。
更多Fiber的概念,笔者可能讲的不如下方REF中讲的好,有兴趣可以直接前往查看。
React的Fiber据笔者了解,并不是一个新的东西,而是有点类似参考了操作系统中时间分片,任务调度的思想进行设计的一个优化解决方案。当然前端当前许多的发展方向都是基于以往的系统,编程语言及后端所用到的的思想。
Fiber也称协程。笔者第一次接触这个概念是在学习go以及python的时候,当然很多语言都有这样的一个机制。
React Fiber的思想和协程的概念是契合的: React渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
如果希望更深入的理解Fiber,那就最好对协程有所了解。
协程是在线程基础上进一步的抽象细分,是单线程基础上的一种拓展。
正好整理一下目前已有的运行程式:
简单来说:一个进程可以有多个线程,一个线程占用一个cpu,一个线程可能有多个协程。
关于协程,笔者感觉下面这个Python的例子更加直观:
def consumer():
r = ''
while True:
# 普通函数执行的过程中无法被中断和恢复,协程通过引入 async/await 或者 yield 等语法实现中断机制
n = yield r # 函数中断,等待收到信号r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None) # 发送一个信号给 r
n = 0
while n < 3:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n) # 发送一个信号给 r
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
# output:
# [PRODUCER] Producing 1...
# [CONSUMER] Consuming 1...
# [PRODUCER] Consumer return: 200 OK
# [PRODUCER] Producing 2...
# [CONSUMER] Consuming 2...
# [PRODUCER] Consumer return: 200 OK
# [PRODUCER] Producing 3...
# [CONSUMER] Consuming 3...
# [PRODUCER] Consumer return: 200 OK
看完协程,笔者其实想到了闭包这一概念,因为两者都保留了一个子函数的运行环境,都是单线程运行,可以被其他函数调用进入此环境。但是两者其实差别也很大,比如前者是能够进入函数运行的中间阶段,而后者是在一个函数的开头进入。
站在巨人的肩膀上
计算机领域有很多社区,其中有很多优秀的项目和资料,笔者受益颇多,也希望能够继续从其中汲取,并也参与进行贡献。
本文只是简单的讲Fiber的思路和优点,同时对其设计思想的部分来源协程进行科普和对比,想深入了解请阅读下面不容错过的文章。
最后更新于: 2021-09-05
我们最终真的从0开始,48小时极限开发了一个2D横版的闯关游戏。
周六下午正式开始的比赛,下午讨论好了基本的策划,当晚完成基本框架,第二天进行细节、剧本和贴图的添加,到第三天早上7点,我们最终完成并且构建发布。
下面是游戏的展示。
我感觉这次比赛还是很有趣的,不够这次比赛之后,在打包发布时,注意到了cocos creator 3在不同平台的打包下的表现会有些不同,下次一定用unity或者其他更成熟的框架来做快速开发。
最后附上我们的小队伍合影,左到右分别是Riven94、Kingfish404、bigfish144
]]>核心的点
opencv mask四状态
GCD_BGD
:0 GCD_FGD
:1GCD_PR_BGD
:2GCD_PR_FGD
:3用户通过直接框选目标来得到一个初始的GCD_PR_FGD
为$t_f$,即方框外的像素全部作为GCD_BGD
为$t_b$
对每一像素n,初始化像素n的标签
通过像素标签来估计目标和背景的GMM,通过k-mean算法分别把属于目标和背景的像素聚类为K类,即GMM中的K个高斯模型
对每个像素分配GMM中的高斯分量
对于给定的图像数据Z,学习优化GMM的参数
分割估计(通过1中的Gibbs能量项,建立图,并求出权值t-link和n-link,通过max flow/min cut算法来进行分割):
重复步骤1-3,直到收敛
对分割的边界进行border matting
平滑处理
如果GMM颜色模型换成颜色直方图https://mmcheng.net/zh/salobj/,可能对视觉显著性区域有更好的切割效果
框选并提取出 可能的前景GCD_PR_FGD
,未被框则选为 背景GCD_BGD
,按n
获得初次分割结果
左键点击选取GCD_FGD
,右键点击选取GCD_BGD
,按n
继续迭代更新
https://github.com/Kingfish404/grabcut-apply-cpp
用ES6
语法实现的vue-like
,可能在部分浏览器上有兼容性问题,不过这个用Babel
简单翻译一下就好,问题不大。
写的可能有点小问题,暂时还没有直接的去读过Vue
的源码,只是了解其单向/双向数据原理,虚拟dom,Diff
算法及更新优化算法等,我接手的前端工程化项目用的也主要是是用react
来写,所以我就直接用react
中组件的思想来设计这个vue-like
了(所以是不是应该叫vueact?)。
这个module
的运行流程就是
import Vue from 'base_url/vue-like.js'
Vue
.createApp({ data ,method ,template })
.mount('选择器')
loop
:检测是否有method
触发了set
,有的话则对对应App
重新render
Vue
是在module
中实例化的对象,其createApp
方法接收一个创建参数对象,并将其保存为自身属性
mount
方法根据输入的css
选择器字符串(如#root
表示id
为root
的html
标签),选择对应的根对象生成并加入构造的App
对象。
Vue
的所有方法均支持carry
化调用,也可以分行调用。
单向数据绑定的实现,ES6语法的代理Proxy
与反射Reflect
,对data
中每个对象的get
和set
方法进行代理,有点像切面注入了。
这里的代码其实很简单,ES6为我们自带提供了Proxy
对象,Reflect
作为当前绑定的对象对镜像执行原有预期操作。
this.data = new Proxy(this.data, {
get: function (target, propKey, receiver) {
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
// console.log(arguments);
if (target[propKey] == value) { // 非引用对象值未修时的简单优化
return true;
}
if (Reflect.set(target, propKey, value, receiver)) {
that.render(); // 重新render
return true;
}
return false;
}
})
简易dom生成我就暂时没有写复杂的虚拟dom
的Diff
算法了,而是set
对象如果发现data
中的元素值被修改的话,直接触发当前APP
的render
进行整体重新渲染。
实际的实现中,是用一个类成员函数来解析templete data
,生成一个真实的dom
对象。
templete data
如下
Vue.h('h1', {
onclick: 'decrease' // attribute,props 的name和对应methods中的函数/data中的值名
}, "App1: {{ counter }} click to increase")
实际部署后预览如下,放在GitHub Page上,国内可能出现加载不出来的情况:
上面例子中的App1,调用方式如下,App2的各位也可以自行右键>检查或者f12查看源码。我加入了一些方便自己开发的跳转,总的来说和vue3的非模板调用方式还是差不多的。
<div class="app1"></div>
import Vue from './vue-like.js';
const appData1 = {
data: {
counter: 0
},
methods: {
increase() {
this.counter++;
},
decrease() {
this.counter--;
},
},
render: Vue.h('h1', {
onclick: 'decrease'
// 这里由于jekyll的字符冲突,我被迫加了转义字符'\'
}, "App1: \{\{ counter \}\} click to increase")
}
Vue.createApp(appData1).mount('.app1');
源码我放在了我的GitHub仓库上,https://github.com/Kingfish404/lib-practice/blob/master/vue3-like/vue-like.js
]]>音乐总能给人带来想不到的能量
补一个
]]>jekyll
的博客模板pure-blog文档。
就我目前了解过的,主要搭建博客的方式有以下几种(并且我自己尝试过)。
我最喜欢的博客框架就是jekyll
,本博客也是基于此搭建的。
如果希望在本地跑起自己的博客,就需要在本地搭建起jekyll开发环境。
最简单快速的本地安装并启动jekyll
,创建新博客,可以去官网查阅https://jekyllrb.com,安装指引页面https://jekyllrb.com/docs/installation
对于windows用户,我个人的建议还是使用wsl
,然后按照ubuntu/(Other Linux)方案来安装配置。
下面是官网首页推荐的自己搭建方式
gem install bundler jekyll
jekyll new my-awesome-site
cd my-awesome-site
bundle exec jekyll serve
# => Now browse to http://localhost:4000
bundle exec jekyll build
# => Build site to _site folder (default)
jekyll的常见用命令
bundle exec jekyll s -l # 动态监听启动服务,默认端口4000
bundle exec jekyll b # 构建发布到_site文件夹
jekyll
官方的定义就是:Jekyll 是一个静态站点生成器。将用markdown
编写的文本,使用Liquid
模版语言创建静态网站。可以简单的调整站点的外观、网址、页面上显示的数据等等。
jekyll
其实很像一个编译器,或者说小型虚拟机
在启动时,它首先会去读取项目下的_config.yml
配置文件,初始化全局变量、插件等
然后开始从根目录开始扫描,读取md
、html
、yml
文件
读取完毕后,就开始对html
进行解析,使用md
和yml
的数据,构造出真正的静态网页,根据参数,会将静态页面用内置的http server运转或者生成到_site
文件夹。
下面开始简单方便的构建自己的博客,我的模板是https://github.com/Kingfish404/pure-blog,其他模板也差不多。
首先需要将模板从Template
仓库拉取到自己的仓库。
这里可以简单的fork
或者clone
到自己的本地。如果是后者,那么需要自己创建gitee
或者github
账号,创建仓库来进行托管,并且需要开启gitpage服务。
目前github
的gitpage服务已经成为Setting里面的独立栏目了。
在你的博客的GitHub代码库页面或者本地的代码仓库里,选择文件列表里选择_config.yml
,打开并进行修改,如果要修改页面导航路由,那么就修改_data/nav.yml
文件。
_config.yml
中的相关配置
lang: zh-CN # 站点语言
title: Site Title # 站点标题
author: Author # 站点作者,这些信息会在meta里,有助于seo
email: your-email@example.com # 站长邮箱
description: >- # 站点描述,this means to ignore newlines until "baseurl:"
Write an awesome description for your new site here. You can edit this
line in _config.yml.
baseurl: "/pure-blog" # the subpath of your site, e.g. /pure-blog 站点的子路径,一般设为仓库名
url: "" # the base hostname & protocol for your site, e.g. http://example.com 站点的自定义域名,如果默认github,那么就是"https://<username>.github.io"
twitter_username: jekyllrb # 可以留空
github_username: jekyll # github用户名
descs: # 网站首页的随机标语
- Write an awesome description for your new site here.
- You can edit this in _config.yml.
- hello world!
# 首页显示的最大文章数
max_posts: 10 # the max posts num display in Home page
# gitalk setting,enter clientID to enable
# doc: https://github.com/gitalk/gitalk
gitalk:
clientID:
clientSecret:
repo:
owner:
excerpt_separator: <!-- more --> # set excerpt separator here
# Build settings
permalink: /posts/:year/:month/:day/:title.html
在博客的代码库页面里,点开_posts
文件夹,这里面就是你的博客文章。
这些文章使用的格式是Markdown
,文件后缀名是md,这是一种非常简单易用的有格式文本标记语言,在pure-blog
自带的示例性博文中有一篇Markdown语法介绍,Markdown cheatseet。
在发布博文前,你需要在文章的头部添加这样的内容,包括文章标题,作者名,和tag等。
---
layout: post
title: 我的文章标题
author: 作者
tags: [sample, document]
desc: 文章简介
comments: true # 是否可以评论,如果配置了gitalk
---
完成后,保存为.md
文件,文件名是date-标题,例如 2021-05-01-document.md (注意这里的标题会成为这篇博文的链接,所以请使用字母而非中文,它不影响页面上显示的标题),然后上传到_posts
文件夹,提交修改,很快就可以在博客上看到新文章了。
少量图片可以上传到images文件夹,然后在博文中添加。
但是GitHub用来当做图床有滥用之嫌,如果你的博客以图片为主,建议选择外链图床,例如sm.ms就是和很好的选择。
要寻找更适合自己的图床,敬请搜索一下。
在博文中添加图片的Markdown语法是:![图片名](URL)
Gitalk 是一个基于 GitHub Issue 和 Preact 开发的评论插件。
中文配置使用说明见gitalk-readme-cn
安装说明走完后就会看到你所需要的两个值,clientID和clientSecret,把它们复制到你的_config.yml文件中相应的字段:
gitalk:
clientID: <你的clientID>
clientSecret: <你的clientSecret>
repo: <你的repository名称>
owner: <你的GitHub用户名>
其实还有挺多的配置说明,大家可以去阅读源代码去理解和尝试,需要一定的前端基础。
_includes
里存放的是页面组件_layouts
里存放的是页面种类及布局_sass
里面存放的是css
样式pages
里面是导航栏对应的单页面看了GAMES101-现代计算机图形学入门,做下笔记
计算机图形学中有个有趣的表示,那就是用向量和他的冗余值来表示对象,举例子来说,[2,1,1]
表示点(2,1)
,[2,1,0]
表示向量(2,1)
,两者的不同在于后者的冗余表示为0
.
冗余值对于线性代数中的计算非常有帮助。
比如下面就是一个先逆时针旋转45度,再移动(1,0)
的例子。
由于冗余1
的存在,在线代计算中,变换矩阵的最后一列就有了意义。
用了Eigen作为线性代数运算库,官方文档为http://eigen.tuxfamily.org。
下面我们要把一个点(2,1)
,先逆时针旋转45度,再平移(1,2)
,那么数学上的运算就是3个矩阵相乘。三维上会复杂点,但基本原理差不多。
constexpr double PI = 3.1415926535;
double angle = 45;
double radian = angle * PI / 180.0f;
Eigen::Vector3f p(2.0f, 1.0f, 1.0f); // 点(2,1)
// 旋转
Eigen::Matrix3f r3;
r3 << std::cos(radian), -std::sin(radian), 0,
std::sin(radian), std::cos(radian), 0,
0, 0, 1;
// 位移
Eigen::Matrix3f m3;
m3 << 1, 0, 1,
0, 1, 2,
0, 0, 1;
p = r3 * p; // 先逆时针旋转45度
p = m3 * p; // 再位移(1,2)
下面绕z轴逆时针旋转45度的计算矩阵,这里注意,绕那个轴,哪个轴对应的计算矩阵的行列值为单位矩阵值。
[cos(45),-sin(45),0,0,
sin(45),cos(45),0,0
0, 0, 1, 0,
0, 0, 0, 1]
未完待续:2021-05-02
春招中有幸拿到了阿里和腾讯的开发岗offer,很可惜最后发现只能二选一。
尽管离前去实习还有一段时间,最近有很多计算机相关的比赛也大量占用了我的精力。想稍停下来复盘,写一下和想一下了。
在这些面试中收获了很多,同时也遇到了很多很nice的面试官,他们给了我很多的建议,在这里我也将他们分享出来,希望能对今后看到此文的人有所帮助。
我面试的主要是前端开发岗,先来总结下我了解到的前端开发所必需具备的知识点,可能有所不全,见谅。
上面的内容很多,很难全部都熟练掌握,但是作为前端开发者,至少应该都要有所了解。如果上面出现了你没有了解过的名词,那就要尽快去补习了。
在阿里的面试中印象很深刻的一句话,是来自于阿里四面时前端leader所说的(记得有点不太清了,就简述下大概意思吧)
在学生时代时学习的很多是知识,但是作为今后步入社会后成为的社会人,需要的更多是技术。技术和知识是不一样的,技术需要通过不断地写代码来进行磨练提高的,人的精力是有限的,要想好自己的发展方向并不断提高这方面的技术
还有其他的面试官给的建议,有些在学习的时候没发现,通过他们的建议发现了自己的缺点并改正了许多
计算机基础知识很重要
前端开发在学习到一定程度后,都需要学习其他领域的知识,才能取得突破,比如我们的评级,初级前端,中级前端,高级前端,前端专家,研究员,科学家(具体可能记不清了),在技术实力发展到一定水平后,title就不会再有限定领域了,当然你自己的领域永远是你的基本盘
使用一个框架或者库,一定要去了解其底层原理与实现,最好有时间的话,可以去读一下源码
初次读一个项目的源代码时要带着目标去读,明确自己想要了解什么,不然容易迷失
一个约定,牢记于心
你的一面二面表现都很不错,也可以看出你对计算机是很有兴趣的,不过你的基础还是不够扎实,你可以回去后在复习一个月,还是XXX,可以帮你再投到我们这里
初次接触编程还是初中时,当时就对电脑及上面的事物很好奇,同时作为一个gamer,有着想要制作自己的游戏的小梦想。当时就接触了rpgmaker,flash之类的制作软件,了解到了游戏脚本的存在。
至今还记得,当时用window xp系统,还用dos命令写了个bat病毒脚本,可以设置自己开机启动并,自我复制和有限传播。
其实我刚刚上大学时并不是计算机专业的学生,而是一个很多人可能都没有听过的专业船舶与海洋工程,其实也并不是没有兴趣,作为一个军迷,还是很喜欢海军,军事方面的东西,不过在深入了解这个行业后,被行业的就业情况和行内人士劝退了。
所幸学校对于学生专业选择的态度非常开放,我有机会在大一下转专业到计算机,并且在专业分流时选择了软件工程专业,从此正式进入计算机科班专业学习。
很多科班的课程的学习和训练就不细说啦
我印象比较深的课,都是需要写很多代码的课:
像数据结构与算法,计组,操作系统,编译原理,计算机网络这几门超级重要,感觉可以终身学习。
2019年中初次接触前端,了解了GitHub这个神奇的地方,还开始在w3c上学习,写了很多静态项目,了解到了bootstrap等,后来继续学习中接触到了jquery,初次实践时写了个2048,当是感觉学到了很多,但是后来才发现这其实这是前端初级的一小部分。
lfx大佬建议:MDN文档好好看
后来确实收获甚多。
中间折腾过很多东西,Ubuntu,deepin,wine,深度学习,openCV,etc。
2020中旬开始接触后端,2020疫情在家时,当时为了写一个计算机设计大赛的比赛,去了解了后端相关的知识,当时我对于前后端的概念还没建立起来,至今上手了django,这是我当时第一次独立自己设计并完成一个完整的软件产品,GitHub仓库LearnPython,完成后感觉收获还是挺多的。
2020下半年,了解到了前端工程化,js的高级知识,被xgp醍醐灌顶的说了一通,才发现自己前端学习的还永远不够,基础不牢,地动山摇,还需要继续学习,于是寒假期间开始恶补基础知识,跟着c4项目手写c子集编译器,并且有幸加入了字节跳动的寒假前端训练营,系统性的过了一遍前端的知识体系,在这个训练营,还有机会写一个nodejs后端项目,写了很多基础curd,了解了koa和eggjs等,最终还拿到了优秀营员,甚是幸运。
通过2021春季的面试,在不断查漏补缺中,学到了很多,通过面试发现自己的不足并加以学习改正,是个很好的方法。
Token团队,应该是学校里技术最好的团队了,也是最像互联网公司/社团的团队,从产品一路学习,并兼任了技术开发;加入了ACM集训队,和一群很厉害的人一起训练,刷题;和老师联系,接触到了一些比赛和的项目。
我可能很贪心,不仅仅是前端,我还想更多的学习算法,计算机图形学,人工智能...,并且能达到被业界认可等水平。
路漫漫其修远兮,吾将上下而求索
如果读完一本书,一个文档,抑或学会一项技术后,就觉得自己已经完全掌握,不需要再次学习了,这是最可悲的,写在这里,希望警醒今后的自己。
]]>typescript
实现八大排序算法,分别是:
插入排序,希尔排序,选择排序,堆排序,冒泡排序,快速排序,归并排序,基数排序。
先说一下各个排序的复杂度叭,方便之后速查。
排序类型 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(nlogn) | O(n²) | O(1) | 不稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
堆排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | 不稳定 |
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
快速排序 | O(nlog₂n) | O(nlog₂n) | O(n²) | O(nlog₂n) | 不稳定 |
归并排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) | 稳定 |
基数排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+kd)) | O(n+kd) | 稳定 |
下面是我用来测试的例子,其中要注意的是,由于我没有对基数排序进行特殊处理,所以目前被排序的值要求均为非负数。
var test_arr: Array<number> = [2, 1, 0, 4, 7, 6, 3, 9, 10, 4, 1, 5, 3, 5, 7];
这个是JS的Array对象默认的排序,原地算法。
默认的比较方式是字符串比较,类似于(a,b)=>{return String(a)>String(b)}
,所以如果对于纯数字数组,可能会出现预期外的结果。
// JS默认的排序
function defaultSort(arr: Array<number>): Array<number> {
let final = new Array(...arr);
return final.sort(function (a: any, b: any) { return a - b; });
}
插入排序是维护一个长度为i的有序序列,每次i自增1,为下标为i的数向前找插入位置。
// 插入排序 insertSort
function insertSort(arr: Array<number>): Array<number> {
let final: Array<number> = new Array(...arr);
for (let i = 1; i < final.length; i++) {
let mark: number = final[i];
let j: number;
// 寻找插入位置
for (j = i - 1; j >= 0; j--) {
if (final[j] < mark) {
// 插入位置找到
break;
} else {
final[j + 1] = final[j];
}
}
// 进行插入操作
final[j + 1] = mark;
}
return final;
}
分步长的插入排序,第一个能到nlog(n)
复杂度的排序算法。
// 希尔排序 shellSort
function shellSort(arr: Array<number>): Array<number> {
let final: Array<number> = new Array(...arr);
for (let div = final.length >>> 1; div > 0; div--) {
// 分步长
for (let i = div; i < final.length; i++) {
// 对每个步长对应对子数组进行插入排序
let temp = final[i];
let j;
for (j = i - div; j >= 0 && final[j] > final[j + div]; j -= div) {
final[j + div] = final[j];
}
final[j + div] = temp;
}
}
return final;
}
和插入排序类似,维护长度为i的有序序列,不过是向后寻找i位置合适的值。时间复杂度固定为n^2
。
// 选择排序 selectionSort
function selectionSort(arr: Array<number>): Array<number> {
let final: Array<number> = new Array(...arr);
for (let i = 0; i < final.length; i++) {
let min = i, temp = final[i];
// 寻找当前值后面的最小值
for (let j = i + 1; j < final.length; j++) {
min = final[min] < final[j] ? min : j;
}
// 最小值和当前的交换
final[i] = final[min];
final[min] = temp;
}
return final;
}
维护一个树结构的最小堆或者最大堆,每次取堆顶即可。
// 堆排序 heapSort
function heapSort(arr: Array<number>): Array<number> {
let heap: Array<number> = new Array(arr.length + 1);
heap.fill(0)
let final: Array<number> = new Array(arr.length);
// 每次放入一个元素到最小堆内
for (let i = 0; i < arr.length; i++) {
pushMaxHeap(heap, arr[i]);
}
for (let i = 0; i < arr.length; i++) {
final[i] = popMaxHeap(heap);
}
// 将最小的元素移动到堆顶
function pushMaxHeap(arr: Array<number>, value: number) {
arr[0]++;
arr[arr[0]] = value;
for (let parent = Math.floor(arr[0] / 2), poc = arr[0]; parent >= 1; parent = Math.floor(parent / 2)) {
if (arr[parent] > arr[poc]) {
let d = arr[parent];
arr[parent] = arr[poc];
arr[poc] = d;
poc = parent;
} else {
break;
}
}
}
// 取出堆顶元素并保持堆序
function popMaxHeap(arr: Array<number>): number {
let value = arr[1];
arr[1] = arr[arr[0]];
arr[0]--;
for (let poc = 1; ;) {
let max_idx = poc, left = poc * 2, right = poc * 2 + 1;
if (left > arr[0]) {
break;
} else if (right > arr[0]) {
right = left;
}
max_idx = (arr[left] <= arr[right]) ? left : right;
if (arr[max_idx] < arr[poc]) {
let d = arr[poc];
arr[poc] = arr[max_idx];
arr[max_idx] = d;
poc = max_idx;
} else {
break;
}
}
return value;
}
return final;
}
典型的算法。
// 冒泡排序 bubbleSort
function bubbleSort(arr: Array<number>): Array<number> {
let final: Array<number> = new Array(...arr);
for (let i = 0; i < final.length; i++) {
let no_swap: boolean = true;
for (let j = 0; j < final.length - i - 1; j++) {
// 如果前一个更大,则和后面的交换
if (final[j] > final[j + 1]) {
let temp = final[j + 1];
final[j + 1] = final[j];
final[j] = temp;
no_swap = false;
}
}
// 如果未冒泡,则已经排序完毕
if (no_swap) {
break;
}
}
return final;
}
分而治之的排序,每次用一个值分出两个集合,再对子集合进行分类,直到最小。
// 快速排序,quickSort
function quickSort(arr: Array<number>): Array<number> {
let final: Array<number> = new Array(...arr);
function tureQuickSort(arr: Array<number>, left: number, right: number) {
if (left >= right) {
return;
}
let i: number = left, j: number = right;
let mark: number = arr[i];
// 用mark作为中间值,将数组左右对半分
while (i < j) {
// 寻找右边比mark更小的
while (i < j && arr[j] >= mark) {
j--;
}
arr[i] = arr[j];
// 寻找左边比mark更大的
while (i < j && arr[i] <= mark) {
i++;
}
arr[j] = arr[i];
}
arr[i] = mark;
// 递归分左右两个子数组
tureQuickSort(arr, left, i - 1);
tureQuickSort(arr, i + 1, right);
}
tureQuickSort(final, 0, final.length - 1);
return final;
}
分治算法,每次都得到两个子有序序列,再合并。
// 归并排序 merginSort
function merginSort(arr: Array<number>): Array<number> {
let final: Array<number> = new Array(...arr);
function trueMerginSort(arr: Array<number>, left: number, right: number): Array<number> {
if (right - left == 0) {
return [arr[right]];
}
// 拿到左归并子序列
let list_a: Array<number> = trueMerginSort(arr, left, Math.floor((left + right) / 2));
// 拿到右归并子序列
let list_b: Array<number> = trueMerginSort(arr, Math.floor((left + right) / 2) + 1, right);
let list_final: Array<number> = [];
let i: number = 0, j: number = 0;
// 两个子序列合并成一个有序序列
while (i < list_a.length && j < list_b.length) {
if (i < list_a.length && j < list_b.length && list_a[i] <= list_b[j]) {
list_final.push(list_a[i]);
i++;
}
if (i < list_a.length && j < list_b.length && list_b[j] <= list_a[i]) {
list_final.push(list_b[j]);
j++;
}
}
while (i < list_a.length) {
list_final.push(list_a[i]);
i++;
}
while (j < list_b.length) {
list_final.push(list_b[j]);
j++;
}
return list_final;
}
return trueMerginSort(final, 0, final.length - 1);
}
非常类似桶排序,每对根据同一个位对上的值进行排序,最后合并。
// 基数排序 radixSort
function radixSort(arr: Array<number>, radix: number = 10): Array<number> {
let final: Array<number> = new Array(...arr);
let base_pool = new Array(radix);
// 功能函数:获取数字对应低位的值,如:(1234,0)->4
function getNumAtLoc(n: number, loc: number): number {
while (n > 0 && loc > 0) {
n = Math.floor(n / radix);
loc--;
}
return n % radix;
}
for (let i = 0; i < 64; i++) {
// 初始化基数池
for (let i = 0; i < base_pool.length; i++) {
base_pool[i] = [];
}
// 按照第i位分别入池
for (let j = 0; j < final.length; j++) {
base_pool[getNumAtLoc(final[j], i)].push(final[j]);
}
// 从池中按顺序取出
let count = 0;
for (let j = 0; j < base_pool.length; j++) {
for (let i of base_pool[j]) {
final[count] = i;
count++;
}
}
// 如果所有数都在0,说明已经超过最高位,可以结束了
if (base_pool[0].length == final.length) {
break;
}
}
return final;
}
var test_arr: Array<number> = [2, 1, 0, 4, 7, 6, 3, 9, 10, 4, 1, 5, 3, 5, 7];
console.log('test arr:', test_arr, test_arr.length);
console.log('defaultSort', defaultSort(test_arr));
console.log('insertSort', insertSort(test_arr));
console.log('selectionSort', selectionSort(test_arr));
console.log('heapSort', heapSort(test_arr));
console.log('shellSort', shellSort(test_arr));
console.log('bubbleSort', bubbleSort(test_arr));
console.log('quickSort', quickSort(test_arr));
console.log('merginSort', merginSort(test_arr));
console.log('radixSort', radixSort(test_arr));
最完整的可以跑的代码我贴在ubuntu pastebin:
https://pastebin.ubuntu.com/p/z8sxgpfjgk/
这里先放上一个之前挺火的一个游戏的改版合成武汉理工{:target="_blank"},用cocos creator开发,很久之前入门前端时,还纯Web原生写过一个2048小游戏{:target="_blank"},这个2048也是典型的MVC模型开发。
MVC即Model、View、Controller即模型、视图、控制器,视图根据模型现实,控制器修改模型。
View <=> Model <=> Controller
游戏开发感觉合传统的应用开发挺不一样的,用不同的一套思路去开发。
简单来说来说可以分成2D游戏和3D游戏,二者主要在视图和物理逻辑层面不太一样,后者需要考虑立体空间坐标系。
本文主要说的是基于cocos的游戏开发笔记,据说cocos和unity很像。最新的cocos creator 3已经全面使用typescript
来进行脚本的编写。
创建场景-节点-物体 > 创建脚本 > 将脚本绑定到节点 > 编写逻辑 > 调试等
这里指的是开发中的设计。
一般是一个gameManager
类或者脚本来控制游戏的主核心逻辑,然后对于角色,再分配playerControl
类来控制角色的行为。对于一般的游戏引擎,绑定在物体上的脚本都会有个类似update
的函数,会随着游戏进行周期性的运行,并且间隔非常短,通过update
函数,我们就可以实现角色状态的实时更新。
对于可能会有事件或者自定义行为的物理,往往是绑定上一个脚本来进行定制。
除了瞬移或者直接添加速度的方式,游戏如果启用了物理引擎,那么一般使用两种方式来运动,一个是力,一个是冲量。
如果要给物体启用物理属性,就需要给物体添加上刚体组件(RigidBody)和碰撞器组件(Collider)。这里又分为2d组件和3d组件。这两个不同的组件会有不同的行为。
实现物体的移动的话,可以参考cocos的文档,通过改变刚体的受力或者冲量,亦或者直接设置一个速度来实现。
就在这里放一下我实现的2d有重力情况下,玩家控制角色移动的方法把。
总结一些大概的流程:
SystemEvent.EventType.KEY_DOWN
onKeyDown
onKeyDown
中为角色对象更新受力的值,如_xforce=10
update
周期性函数执行时,根据受力值,如_xforce
,对物体的刚体组件施加对应的冲量/力首先将事件处理函数绑定到绑定键盘输入事件。
setInputActive(active: boolean) {
if (active) {
systemEvent.on(SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
systemEvent.on(SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
} else {
systemEvent.off(SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
systemEvent.off(SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
}
}
键盘输入的处理函数,可以看到,这里我是给角色节点施加了一个力来实现状态的改变。键盘按下时添加对应方向的力,松开时解除力。
onKeyDown(event: EventKeyboard) {
this._isMoving = true;
switch (event.keyCode) {
case macro.KEY.a:
case macro.KEY.left:
this._xForce = -this._force / 2;
this._yForce = 0;
break;
case macro.KEY.d:
case macro.KEY.right:
this._xForce = this._force / 2;
this._yForce = 0;
break;
case macro.KEY.w:
case macro.KEY.up:
if (!this.isUping) {
this.isUping = true;
this._yForce = this._force;
let that = this;
setTimeout(() => {
that._yForce = 0;
}, 200)
}
break;
case macro.KEY.s:
case macro.KEY.down:
this._yForce = 0;
break;
}
}
onKeyUp(event: EventKeyboard) {
this._isMoving = false;
this._keydown = String(event.keyCode);
switch (event.keyCode) {
case macro.KEY.a:
case macro.KEY.left:
case macro.KEY.d:
case macro.KEY.right:
this._xForce = 0;
break;
case macro.KEY.w:
case macro.KEY.s:
// this._yForce = 0;
break;
}
}
实时更新函数,update函数会在游戏运行时,被游戏主线程周期性的调用。通过update
函数,就可以实现将角色受到的力转换成具体的行为。
update(deltaTime: number) {
if (this.player) {
const player = this.player;
const rigidbody2d: RigidBody2D | null = player.getComponent(RigidBody2D);
if (rigidbody2d) {
// 使用刚体运动
// https://docs.cocos.com/creator/3.0/manual/zh/physics/physics-collider.html
rigidbody2d?.applyForceToCenter(new Vec2(this._xForce, this._yForce), true);
const velocity: Vec2 = rigidbody2d?.linearVelocity;
if (velocity) {
// 限制刚体速度
if (Math.abs(velocity.x) > this.maxspeed) {
velocity.x = velocity.x > 0 ? this.maxspeed : -this.maxspeed;
}
if (this._xForce == 0) {
velocity.x /= 1.1;
}
rigidbody2d.linearVelocity = velocity;
}
}
}
}
最后编辑于:2021-03-07
之前也写过挺长时间Python的,Python函数中的传值也有点不自信,虽然在构建深度学习的BP网络时,总是很自然的直接认为参数可以影响外面的值,即传引用调用,而在用一般类型时,认为是传值调用,但是,还是对原理不够清楚,所以现在也一起整理下,系统性的总节。
调用方式主要有两种,分别是传值调用(Call-by-value)和传引用调用(Call-by-reference),实际的编译器实现中,还有很多其他的种类。
在传值调用中,传递给函数参数是函数被调用时所传实参的拷贝。在传值调用中实际参数被求值,其值被绑定到函数中对应的变量上(通常是把值复制到新内存区域)。cpp中默认的函数传递方式就是如此int main(int argc)
,这里的argc
就是传值调用。
在传引用调用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。通常函数能够修改这些参数(比如赋值),而且改变对于调用者是可见的。cpp中void fun(int &num)
,这里的num
就是传引用调用。
cpp🐂,手术刀级别的操控还是舒服,当然也需要编写者自身水平比较高。
引用类型调用(Nicholas)表示如下:
除了上述两种外,实际上,还有一种调用方式,传共享调用(Call by sharing)。
传共享调用和传引用调用的不同之处是,该求值策略传递给函数的参数是对象的引用的拷贝,即对象变量指针的拷贝。也有说法,说这也是传值调用,只不过对于非基本类型,传的是对象的引用,对于基本类型,直接传值。
也就是说如下方代码 changeStuff
函数内的 a
b
c
都分别与 num
obj1
obj2
指向同一块内存,但不是其拷贝。函数内对 a
b
c
所做的任何修改,都将反映到 num
obj1
obj2
上 。不过对于JavaScript,如果实参类型为默认的基本类型,那么就是值拷贝。
function changeStuff(a, b, c) {
a = a * 10; // 对 a 赋值,修改 a 的指向,新的值是 a * 10
b.item = "changed"; // 因为 b 与 obj1 指向同一个对象,所以这里会修改原始对象 obj1.item 的内容
c = {item: "changed"}; // 对 c 重新赋值,修改 c 的指向,其指向的对象内容是 {item: "changed"}
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
console.log(num);
console.log(obj1.item);
console.log(obj2.item);
// 10
// changed
// unchanged
JS 引用类型变量的值是一个指针,指向堆内存中的实际对象。
这点让我想到了Python,感觉这俩这地方挺像的,Python也是共享传参,所以我又去review了下Python基础。
Python中所有的变量实际上都是指向变量的内存地址,并且这个地址可以通过id
函数来查找,而且Python在变量内存分配时,还会有许多有意思的地方,类似下方:
a = 1
id(a) # 4498704688
b = 1
id(b) # 4498704688
b = 2
id(b) # 4498704720
c = 1
id(c) # 4498704688 和a的地址相同
从上面我们就可以发现,Python里同样对象使用的是相同的内存空间,且新生成对象时,会优先去已经分配的空间查找是否已经有存在的,如果不存在,再新建空间存放新对象,否则就是直接建一个新引用。
当然上述表述只对Python的不可变对象有效。
可变对象与不可变对象的区别在于对象本身是否可变。
python内置的一些类型中
Python函数参数都只能是引用传递,所有的变量都是引用,指向对象的地址,并且只有以下两种类型的对象。
不可变对象,修改变量值实际上都是新分配了空间,然后改变引用的地址,所以函数内引用的地址发生改变,表象就是变量值变了,但是函数外任然还是引用原值的地址。
可变对象,修改变量值是直接操作地址上的值,并没有新分配内存空间,所以函数内修改变量,函数外也会受到影响。
自定义类通常是可变的。要模拟类中的不变性,生成不可变对象,应该覆盖属性设置和删除以引发异常。
跑题了,跑题了,这里主题应该是JavaScript,现在回来
还是上面那段代码
function changeStuff(a, b, c) {
a = a * 10; // 对 a 赋值,修改 a 的指向,新的值是 a * 10
b.item = "changed"; // 因为 b 与 obj1 指向同一个对象,所以这里会修改原始对象 obj1.item 的内容
c = {item: "changed"}; // 对 c 重新赋值,修改 c 的指向,其指向的对象内容是 {item: "changed"}
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
console.log(num);
console.log(obj1.item);
console.log(obj2.item);
// 10
// changed
// unchanged
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
可以看到,变量 a
的值就是 num
值的拷贝,变量 b
c
分别是 obj1
obj2
的指针的拷贝。
函数的参数其实就是函数作用域内部的变量,函数执行完之后就会销毁。
a = a * 10;
b.item = "changed";
c = {item: "changed"};
如图所示,变量 a
的值的改变,并不会影响变量 num
。
而 b
因为和 obj1
是指向同一个对象,所以使用 b.item = "changed";
修改对象的值,会造成 obj1
的值也随之改变。
由于是对 c
重新赋值了,所以修改 c
的对象的值,并不会影响到 obj2
。
从上面的例子可以看出,对于 JS 来说:
传值调用本质上传递的是变量的值的拷贝。
传共享调用本质上是传递对象的指针的拷贝,其指针也是变量的值。所以传共享调用也可以说是传值调用。
题目描述
在N*N的方格棋盘放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45角的斜线上。
你的任务是,对于给定的N,求出有多少种合法的放置方法。
题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=2553,具体的IO格式可以在这里看。
典型的DFS题目,比较难得地方就是设计数据结构模拟棋盘,记录不可以走的位置,然后就是要注意通过减枝提高效率。
这里一般不容易想到的是斜对角如何标记不可以访问。
如果直接通过二维数组来进行标记,非常的低效,所以用一种数据结构来模拟标记斜线很重要。这里记录的方式如下:
0 1 2 3
0 | 1 0 2 0 // 用一维数组来标记斜线
1 | 0 1 0 2 // 可以发现,这里面都为1都一个斜线,任意一个点位置[i,j],i-j为0
2 | 3 0 1 0 // 同理,对于3的斜线,坐标[i,j],有i-j为2,同样也适用于2的斜线
3 | 0 3 0 1 // 所以,下方代码里吗,我用mark1来标记正斜对角线
对于反斜对角线,也和正斜对角线有相同的性质,不过这里是换成i+j
来计算,这样就可以用两个数组来标记整个棋盘的斜对角是否访问,同理,列也可以一样。
上方数据结构的设计思想应该是属于hash
查找的思想,速度非常的快,感觉很多的类似的查找都可以通过建立hash
联系的方式来实现O(1)
的复杂度。比如统计最长不重复字串时,就可以通过一个26位长度数组标记字母是否访问过。
最后,AC代码如下,简单来说,就是提前DFS打了个表,然后根据输入输出对应结果。
#include <cstdio>
#include <cstring>
using namespace std;
class Solution
{
// cols 标记已经访问过的列,mark1表示正斜线,mark2为负斜线
int cols[40], mark1[40], mark2[40];
void dfs(int i, int n, int &num)
{
for (int j = 0; j < n; j++)
{
if (mark1[i - j + n] || mark2[i + j] || cols[j])
{
continue;
}
if (i == n - 1)
{
num++;
continue;
}
mark1[i - j + n] = 1, mark2[i + j] = 1, cols[j] = 1;
dfs(i + 1, n, num);
mark1[i - j + n] = 0, mark2[i + j] = 0, cols[j] = 0;
}
}
public:
void solve()
{
int n, num;
// 初始化描述数组
memset(cols, 0, sizeof(cols));
memset(mark1, 0, sizeof(mark1));
memset(mark2, 0, sizeof(mark2));
int res[10];
// 提前打表算出各个值
for (int i = 0; i < 10; i++)
{
num = 0;
dfs(0, i + 1, num);
res[i] = num;
}
while (~scanf("%d", &n) && n)
{
printf("%d\n", res[n - 1]);
}
}
};
int main()
{
Solution s;
s.solve();
return 0;
}
]]>// 类构造
class String
{
public:
String(const char *str = NULL); // 构造函数
String(const String &str); // 拷贝构造函数
~String(); // 析构函数
String operator+(const String &str) const; // 重载+
String& operator=(const String &str); // 重载=
String& operator+=(const String &str); // 重载+=
bool operator==(const String &str) const; // 重载==
char& operator[](int n) const; // 重载[]
int getLength() const; //获取长度
friend istream& operator>>(istream &is, String &str); // 输入
friend ostream& operator<<(ostream &os, String &str); // 输出
private:
char *data; //字符串
};
string类实际上还拥有vector类的大部分方法(好像是全部?),在表现上
理解string类为vector<char>也可以,但是string有对+等运算符的重载,同时内寸管理方式也不同。
#include <iostream>
#include <cstring>
using namespace std;
// 实现 String 类
class String
{
public:
// 普通构造函数
String(const char *str = NULL) {
if (str == NULL) {
m_data = new char[1];
m_data = '\0';
} else {
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
// 拷贝构造函数,深复制
String(const String &other) {
if (!other.m_data) {
m_data = NULL;
}
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
}
// 析构函数
~String() {
if (m_data) {
delete[] m_data;
m_data = NULL;
}
}
// 重载 + 字符串连接, 不能返回引用
String operator+(const String &other) const {
String newString;
delete[] newString.m_data;
if (!other.m_data) {
newString = *this;
} else if (!m_data) {
newString = other;
} else {
newString.m_data = new char[strlen(m_data) + strlen(other.m_data) + 1];
strcpy(newString.m_data, m_data);
strcat(newString.m_data, other.m_data);
}
return newString;
}
// 重载 = 赋值
String& operator=(const String &other) {
if (this != &other) {
delete[] m_data;
if (!other.m_data) {
m_data = NULL;
} else {
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
}
}
return *this;
}
// 重载 ==
bool operator==(const String &other) const {
if (strlen(m_data) != strlen(other.m_data)) {
return false;
} else {
return strcmp(m_data, other.m_data) ? false : true;
}
}
// 重载 +=
String& operator+=(const String &other) {
char *newData = new char[strlen(m_data) + strlen(other.m_data) + 1];
strcpy(newData, m_data);
strcat(newData, other.m_data);
delete[] m_data;
m_data = newData;
return *this;
}
// 重载 []
char& operator[](int n) const {
if (n >= strlen(m_data)) {
return m_data[strlen(m_data) - 1];
}
return m_data[n];
}
// 获取长度
int getLength() const {
return strlen(m_data);
}
// 输入, 重载输入操作,需要先申请一块内存,用于存放输入字符串
friend istream& operator>>(istream &is, String &str) {
char strTemp[100];
memset(strTemp, 0, sizeof(strTemp));
is >> strTemp;
str.m_data = new char[strlen(strTemp) + 1];
strcpy(str.m_data, strTemp);
return is;
}
// 输出
friend ostream& operator<<(ostream &os, String &str) {
os << str.m_data;
return os;
}
private:
char *m_data;
};
// 测试
void test(){
String s;
cin >> s;
cout << s << " : " << s.getLength() << endl;
String s1(s);
cout << s1 << " : " << s1.getLength() << endl;
const char *str = "Hello";
String s2(str);
cout << s2 << " : " << s2.getLength() << endl;
char str1[] = "world!";
String s3(str1);
cout << s3 << " : " << s3.getLength() << endl;
String s4 = s3;
if (s4 == s3) {
cout << "s3 == s4" << endl;
} else {
cout << "s3 != s4" << endl;
}
String s5 = s3 + s4;
cout << s5 << " : " << s5.getLength() << endl;
s3 += s4;
if (s5 == s3) {
cout << "s5 == s3" << endl;
} else {
cout << "s5 != s3" << endl;
}
cout << s5[5] << endl;
}
int main()
{
test();
return 0;
}
这次给博客上代码压缩,实际上遇到了一些坑,不过这些坑其实是属于调用的compress库http://jch.penibelst.de的问题,但是我感觉在日后的开发中为了兼容性也可以注意。
编写代码时遵守建议的代码规范很重要!很重要!很重要!对于防止出现预期之外的结果有很大帮助,可以节约排错时间。(算了,就是自己菜)
这里遇到的问题有,JS代码无法执行,后来发现是JS代码部分没有加;
作为行的结尾,平时浏览器会自动为我们加上,不会报错,但是如果进行了代码压缩,代码压缩库没有注意到这一点,就会导致部分代码出现预期之外的结果。
代码注释也尽量要采用/* content */
的形式,而不是// content
这种形式,在不严谨的压缩中也会出问题。
例子如下:
/* before compress */
var i=0,j=1
console.log(i)
/* 代码压缩前,由于分行的存在,上述代码不会出问题,但是如果压缩后,压缩库存在问题,就会出现以下问题 */
/* after compress */
var i=0,j=1 console.log(i)
/* 显然上述代码无法正常运行,会报`Uncaught SyntaxError: Unexpected identifier`错误
性能指标主要关注的几个点列下来如下:
我的当前博客部署在Github,很多内容也没用上cdn,所以实际的加载效果并不是特别好,LCP time挺长的,不过除此之外也还是有可以优化的地方(懒了,暂时不搞)。
Lighthouse,一个开源自动化工具,开源分析页面性能,并且给出修改建议,目前微软的新Edge内置了这个工具,用起来也是非常的方便。
下图是我给我自己的博客用Lighthouse进行的分析,看起来性能优化的还是不错的。
用Chrome dev tool中的性能分析工具,可以非常精细的定位到问题的位置,具体可以自行f12看看叭(应该没有搞前端的不用chromium系的浏览器吧,哈哈哈),当然Firefox的调试工具也用起来很方便(性感可爱的小狐狸)。
具体参考文档:https://developers.google.com/web/tools/chrome-devtools/evaluate-performance
许多博客通常会写通过监听onScroll事件来做懒加载,但是现在浏览器也有新的通用api来做这个事情,这个api就是Intersection Observer API
。
Intersection Observer API提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。在MDN上对这个api进行了非常详尽的解释,我这里简单写下吧。
Intersection Observer API的实现方式就是观察者模式,当root元素与设备视窗或者其他指定元素发生交集时执行自行设置的回调函数,当然第一次监听目标元素时也会调用回调函数。
// 简单的调用例子
const observer = new IntersectionObserver(
// 相交时出发回调
(entries) =>{
entries.forEach((entry) => {
console.log(entry.target);
});
},
{
// 目标元素的父级元素,用于检查目标可见性,默认为浏览器视窗
root: document.querySelector('#scrollArea'),
// root元素的外边距
rootMargin: '0px',
// 观察者和root相重叠多少算相交
threshold: 1
}
);
observer.observe(div); // 设置观察者的目标
observer.observe(image); // 同上
节流和防抖是闭包的典型应用,通常用于对请求的频率进行控制。
防抖 debounce
节流 throttle
// 防抖 - 实现方法1
// 需要实例化debounce
function debounce(fn,delay)
{
let timer = null;
return function () {
let that = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(that, args);
},delay);
}
}
let debounce_fn = debounce(target_fn,1000);
// 防抖 - 实现方法2
// 可以直接使用debounce
function debounce(fn,delay)
{
clearTimeout(fn.timer);
fn.timer = setTImeout(() => {
fn();
},delay);
}
// 节流 - 需要实例化
// 立即执行版本-时间段开始时执行
function throttle(fn, time) {
let isRuning = false;
return function () {
if (isRuning) {
return;
}
isRuning = true;
fn.apply(this, arguments);
setTimeout(() => {
isRuning = false;
}, time);
}
}
// 延迟执行版本-时间段结束时执行
function throttle(fn, time) {
let isRuning = false;
return function () {
if (isRuning) {
return;
}
isRuning = true;
setTimeout(() => {
fn.apply(this, arguments);
isRuning = false;
}, time);
}
}
var thr = throttle(target_fn,1000);
将代码分割成各种捆绑包,按需要加载或者并行加载。可以可以用于实现较小的包和控制资源负载优先级,如果使用正确,可以对负载时间产生重大影响。
这里就有一种非常常用但是我暂时并没有自己配置过的工具,webpack。日后自己研究下。
预加载是一种非常高效的性能优化方法,主要基于link标签来进行实现,link标签不仅仅可以加载css,还能通过设置不同的rel
来做预加载,下方是一个通过pre提前建立tcp连接的实例,剩余的具体类别介绍也如下:
<link rel="preconnect" href="https//www.example.com/favicon.ico" />
SSR(Servier Side Render),或者又称服务端渲染。在服务器就提前吧页面内容拼接好再发给用户,降低LCP时间。
感觉Jekyll搭建的博客本身也属于服务端渲染的一种,或者更加进一步的说,是SSG(Static Site Generator)。
在做SSG时,就可以提前构建静态页面,压缩页面内容,非常适用于内容不经常变化的网站。像我博客这种经常咕咕咕的页面,用SSG就非常合适了。
PWA(Progressive Web App),可以让Web应用获得更加接近原生应用的体验,使用React开发应用时,public下自动生成的manifest.json文件就是PWA应用的配置文件之一。
这里暂时没有仔细去研究,日后有需求了再看叭。
对着JavaScript红宝书和ES6的文档一起看与学习。
主要有三个变量声明关键词:var,const和let。
var 可以省略进行定义,但不建议;var有个很关键的特性是声明提升,会讲关键字声明的变量自动提升到函数作用域顶部。
let 和var差不多,但是let声明到范围是块作用域,不会被提升,而var声明到范围是函数作用域。
一个经典的例子就是for循环中的var和let声明
for (var i=0;i<5;i++)
{
setTimeout(()=>{ console.log(i) },0)
}
// 上述循环运行的结果为 5,5,5,5,5
for (let i=0;i<5;i++)
{
setTimeout(()=>{ console.log(i) },0)
}
// 上述循环运行的结果为 0,1,2,3,4
const的行为和let基本相同,不过const声明的变量必须声明时初始化,且之后无法再进行修改。
JavaScript严格来说有7种数据类型,如下:
通过typeof
操作符可以很简单的获取参数的类型。
JS的String对象无法像cpp一样通过代数运算如'b'-'a'
直接计算获得某个值的hash index,不过可以通过函数获得
在用JS实现某些算法时String对象的charCodeAt方法很适合用来获取对应的ASCII值,代码如下:
'abc'.charCodeAt(0) // 97
'abc'.charAt(0) // "a"
感觉和cpp std中的map很像,我用JS完成了一下LeetCode的567. 字符串的排列这题,当时没去查如何获取String的ASCII值,就用Map来做了标记,实现了滑动窗口
代码如下:
/**
* @param {string} s1
* @param {string} s2
* @return {boolean}
*/
var checkInclusion = function (s1, s2) {
let mark = new Map();
for (i of s1) {
if (mark.has(i)) {
mark.set(i, mark.get(i) + 1);
} else {
mark.set(i, 1);
}
}
let count = 0;
for (let i = 0; i < s2.length; i++) {
if (mark.get(s2[i]) > 0) {
mark.set(s2[i], mark.get(s2[i]) - 1);
count++;
if (count == s1.length) {
return true;
}
} else {
if (count > 0) {
for (let j = i - count; j < i && (mark.get(s2[i]) == 0 || mark.get(s2[i]) == undefined); j++) {
mark.set(s2[j], mark.get(s2[j]) + 1);
count--;
}
if (mark.get(s2[i]) > 0) {
mark.set(s2[i], mark.get(s2[i]) - 1);
count++;
}
}
}
}
return false;
};
待补充:2020-02-10
]]>本文主要写std中容器库,打了很久ACM,之前一直没有写写常用的cpp的笔记,现在就来补一下。
用cpp编写算法时,使用stl和不使用stl完全是两种体验,stl中有许多可以提高效率的工具函数和类函数。
我学习和补充自己cpp知识的主要地方为cppreference.com和Concurrency-with-Modern-Cpp,分别对应cpp基础和cpp多线程编程,当然cpp博大精深,需要学习的地方还有许多。
STL的实现有很多的版本,GNU C++标准中使用的是SGI STL,MacOS中的Clion使用的是LLVM,其他的版本还有Clang等,各自的性能和特点都有所不同。
本文分析源码中使用的是SGI STL。
顺序容器(sequence container) 可以实现数据的顺序访问,类似访问数组的形式。
在内存中,vector实例化的对象一般是在连续的内存空间上动态进行分配的(这点和内置的静态空间的array很不同),所以可以通过如a[i]
的方式进行访问。
vector的常见操作的复杂度:
vector是属于按需要动态增加空间的容器,在实例化的时候会预分配存储空间,也可以手动进行存储空间的预分配,如果空间不够了就会在堆中新开辟当前空间的两倍的空间,并将当前空间中的值拷贝到新空间,因此如果对vector的操作引起了空间重新配置,那么就会导致指向原来vector的迭代器失效,同时需要一定的运算时间来进行数据迁移。
动态增加步骤:
// 简化版vector类定义
template <class T, class Alloc = alloc>
class vector
{
public:
typedef T value_type;
typedef value_type* iterator; // vector 的迭代器是普通指针
protected:
iterator start; // 表示目前使用空间的头
iterator finish; // 表示目前使用空间的尾
iterator end_of_storage; // 表示目前可用空间的尾
};
通过上述简化版的vector定义,可以看到vecotr对象的内存空间通过三个迭代器(行为类似指针,重载了部分运算符)进行区分,分成了2个空间,分别是已用空间和未使用空间。b表示起来如下:
|-- -- -- used-- -- --|-- -- unused -- --|
^start ^finish ^end_of_storage
deque为双端队列,同时具有栈和队列的属性,是一种双向开口的连续线性空间,头和尾均可以进行元素的插入和删除操作。
list是一个双向链表。
cpp基础类,静态分配存储空间的连续数组。
还有用的不多的,就简单列下,详情看文档。
关联容器(associative container)是可以实现快速查找($O(log_n n)$)的数据结构
set 唯一键的集合,按照键排序。
map 键值对的集合,按照键排序,键是唯一的。
还有许多其他的容器,日后有机会再补充,这里先列着。
正好今天整完了,就写篇博客记录下过程。
最终使用的是:https://github.com/lakinduakash/linux-wifi-hotspot这个仓库的项目来实现的热点。
简单的进行搜索,我看到了一个GitHub上的Wi-Fi Access Point库:https://github.com/oblique/create_ap,不过这个库已经停止维护了,README.md上有这么一段介绍:
NOT MAINTAINED
This project is no longer maintained.
If you are still interested in this project, checkout the following fork that also provides GUI: lakinduakash/linux-wifi-hotspot
所以我前去接任维护的仓库:https://github.com/lakinduakash/linux-wifi-hotspot,去了解下。
我的树莓派没有安装图形化界面,所以我用的是命令行方式部署,更详细的内容可以参考文档:https://github.com/lakinduakash/linux-wifi-hotspot/blob/master/src/scripts/README.md
先很常规的clone仓库到本地,然后make安装。
git clone https://github.com/lakinduakash/linux-wifi-hotspot
cd linux-wifi-hotspot/src/scripts
make install
如果使用默认的如下方式开启热点,启用的热点是WPA加密,似乎不太安全,所以这里我用的是WPA2通道,更加安全。
# sudo create_ap 无线网卡名 有线网卡名 热点名 密码
sudo create_ap wlan0 eth0 MyAccessPoint MyPassPhrase
更安装的pipe部署代码如下:
# sudo echo -e "热点名\n密码" | sudo create_ap 有线网卡名 无线网卡名
sudo echo -e "MyAccessPoint\nMyPassPhrase" | sudo create_ap wlan0 eth0
上述命令执行过后,预期就会输出如下结果(部分私人信息我用******
打了码),就意味着热点启动成功,可以愉快的连接啦:
jinyu@ubuntu:~$ sudo echo -e "******\n******" |sudo create_ap wlan0 eth0
Config dir: /tmp/create_ap.wlan0.conf.HCBP7vbt
PID: 5661
Network Manager found, set wlan0 as unmanaged device... DONE
Sharing Internet using method: nat
hostapd command-line interface: hostapd_cli -p /tmp/create_ap.wlan0.conf.HCBP7vbt/hostapd_ctrl
Configuration file: /tmp/create_ap.wlan0.conf.HCBP7vbt/hostapd.conf
wlan0: Could not connect to kernel driver
Using interface wlan0 with hwaddr ****** and ssid "******"
wlan0: interface state UNINITIALIZED->ENABLED
wlan0: AP-ENABLED
wlan0: STA ****** IEEE 802.11: associated
wlan0: AP-STA-CONNECTED ******
wlan0: STA ****** RADIUS: starting accounting session ******
wlan0: STA ****** WPA: pairwise key handshake completed (RSN)
当然如果要持久化开启热点,可以使用Systemd service
来托管服务,也可以像我,直接使用screen
创建了一个窗口来托管create_ap
。
ERROR: Failed to initialize lock
出现这个问题的主要原因是,热点关闭进程出了问题。在GitHub上也有相关Issue:https://github.com/oblique/create_ap/issues/384
解决方法就是进入\tmp
目录,删除目录下文件名形如create_ap.*.lock
的文件。类似代码如下:
sudo rm /tmp/create_ap.all.lock
主要的创建摘要算法有
摘要算法的主要目的在于创建信息的不可逆摘要,在网上下载程序安装包或者大型文件时,往往会提供一个MD5代码用于验证我们下载的文件是否出现错误,在摘要算法中,输入为我们指定的的数据,输出为一个定长的,由输入的数据确定的唯一确定的一个数据序列,一旦输入数据的任意一个字节数据发生改变,那么生成的数据序列也会发一定会发生改变。
DSA算法
代补充
DES是典型的对称加密,通信的双方都要保存一个相同的密钥。
DES算法工作方式:
如Mode为加密,则用Key 去把数据Data进行加密, 生成Data的密码形式(64位)作为DES的输出结果;
如Mode为解密,则用Key去把密码形式的数据Data解密,还原为Data的明码形式(64位)作为DES的输出结果。
在通信网络的两端,双方约定一致的Key,在通信的源点用Key对核心数据进行DES加密,然后以密码形式在公共通信网(如电话网)中传输到通信网络的终点,数据到达目的地后,用同样的Key对密码数据进行解密,便再现了明码形式的核心数据。
这样,便保证了核心数据(如PIN、MAC等)在公共通信网中传输的安全性和可靠性。
RAS是一种非对称的加密算法。广泛的用于现代的加密通信中。
计算方法:
第一步,随机选择两个不相等的质数p和q。
如61和53。(实际应用中,这两个质数越大,就越难破解。)
第二步,计算p和q的乘积n。
61和53相乘得到3233。
第三步,计算n的欧拉函数φ(n)。
φ(n) = (p-1)(q-1)
这里计算得到60x52=3210
第四步,随机选择一个整数e,条件是1< e < φ(n),且e与φ(n) 互质。
在1到3120之间,随机选择了17。(实际应用中,常常选择65537。)
第五步,计算e对于φ(n)的模反元素d。
所谓"模反元素"就是指有一个整数d,可以使得ed被φ(n)除的余数为1。
ed = 1 (mod φ(n))
这个式子等价于
ed - 1 = kφ(n)
于是,找到模反元素d,实质上就是对下面这个二元一次方程求解。
ex + φ(n)y = 1
已知 e=17, φ(n)=3120,
17x + 3120y = 1
这个方程可以用"扩展欧几里得算法"求解,此处省略具体过程。总之,算出一组整数解为 (x,y)=(2753,-15),即 d=2753。
至此所有计算完成。
第六步,将n和e封装成公钥,n和d封装成私钥。
在上面的例子中,n=3233,e=17,d=2753,所以公钥就是 (3233,17),私钥就是(3233, 2753)。
代补充
最后更新于: 2020-02-10
]]>不知道这是多少次了,在寒风中,我独自前行。
我文笔不好,所以每次哪怕很忧伤,很难过时,也无法像诗人,小说家那般写出来。
难过的时候,就很不喜欢思考。
亲密关系不用太多,有那么一两个就好了,像双子星那样,旋转,螺旋,共同飞行,但是,慢慢的向前走的时候,慢慢的,散了。
不经意间,总是伤害亲近的人,难以保持长期的友好关系,期待着一个人来救赎。
在人群中时,我却感觉到了孤独,大家笑的越开心,我却越觉得像是在雾中。
一直对身边的人友好以待,期待着他们也能给能同样的对自己,一直在坚持做的事情。
音乐给人以救赎。
有幸认识了很多很厉害的人,很好的人,看到他们以及她们身上散发的闪耀的光芒,有的很刺眼,也有点很暖和,总是很羡慕,想要去追赶,同行进,但是跑着跑着,我发现我追不上了。
我想要的是什么?
看到这里,你会何感想呢,会远离我么?会害怕我吗?亦或相反?
最后更新于: 2020-11-29
]]>这次的编译原理实验,我先选的就是把正则表达式转化成NFA的一个实现,一共有四个实验,四个实验和在一起,就是一个简单的词法分析器,即编译器前端的重要组成部分。
代码我已经放到GitHub上了,链接在此:Github:Kingfish404/Compiles
先占个位置,之后慢慢写
最后修改:2020-11-23
]]>补充于2021-06-21: 现在发现,自己要学习的还很多,学习路上需要保持谦虚和好奇,下文就不修改啦,日后自己看
同学评论我挺有极客精神的,我也确实挺喜欢折腾
我吃树莓派,指的是我之前买了个树莓派,放在一个24小时不断电的地方当服务器用
最近开发效率挺低的,可能遇到瓶颈期了吧
技术学多了后,熟练了,就很难有新的惊讶了,相对而言,学习的热情也会下降。不过还好,计算机技术发展非常迅速,生态也很好。
我喜欢用休克这个词,当然这里的休克和医学上的并不是同一个意思,我语文不好,可能也用错了这个词。我这里用的休克并不是死亡,只不过是重大挫折后的暂时性离开,等恢复好了,养精蓄锐,东山再起。
喜欢知乎上的一段回答:
就一份工作(包括编程)来说,是不可能存在某种持续且不衰减的快乐,因为:
“所有的脑力劳动,在熟练之后,都会转变成体力劳动。”
虽然编程相对于其他工作,在某些时刻,确实存在特有的快乐,包括:
1.纯粹的逻辑之美。
2.新技术带来的成长感。
3.相对较高的收入。
4.…但是,相对于职业生涯的漫长,上面提到的只是少有的高光时刻。而大部分时候,编程和其他工作一样,都处于枯燥和无奈之中。
参透一门新技术的那一刻是快乐的,但参悟的过程却是百转千回,甚至痛苦不堪。
加薪的那一刻会令人意气风发,但不出两天,人就会回复平常,觉得心安理得。
应用一门新技术是令人兴奋的,但之后,人就会迅速掉在业务的泥潭中挣扎,整日与“增删改查”为伴。
因此,以编程为职业,靠这些高光时刻,是无法常享快乐的,当然也无法长久的激励自己走下去的。
而最正确的做法,就是把编程作为一个平常的职业对待,首先是一个谋生的方式而已。
和任何其他的工作一样,编程这个职业的第一属性是赋予我们安全、自尊和价值。而这些又集中体现在对家人的照顾。
之后,我们再来观察编程这个职业,就会深深的体会到它的好。
风刮不着,雨晒不着。
相对较高的收入,对出身普通家庭的人来说,这是少有的通过个人努力,就能过上体面生活的途径之一。
常年的逻辑训练让我们拥有严密的思维。而在现代社会,这是一个人安身立命的根本,也是安全感的终极所在。
当明白了这些,从此,我们就能够心安理得的“做一天和尚撞一天钟”,放弃追求那些可能强烈,但却无法持久的快乐。
正如莱蒙托夫的诗所写:
“一只船孤独的航行在海上,它既不寻求幸福,也不逃避幸福,它只是向前航行,底下是沉静碧蓝的大海,而头顶是金色的太阳。”
而这才是生活的常态和真谛所在。
作者:沈世钧
链接:https://www.zhihu.com/question/339068307/answer/850364079
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我曾经和战友们千里追凶,把仇敌吊死在迪拜的楼顶上。
我曾经扶保南宋江山,挽社稷于天倾。我依旧记得在大散关面对几倍于己的西夏人,挥舞着铁鞭浴血奋战直到衣甲都看不出原来的颜色。
我曾经在日本列岛上复兴足利家,一统日本。也曾经为崇祯皇帝拼命修补着分崩离析的国家。
我见证了纽约的繁华,也见证了他在战争,疫病,外星人面前的脆弱。我刚刚和战友们拯救过她,但又转头毫不留情的在纽约释放瘟疫。
我曾在万里之外的荒蛮与海盗厮杀,也曾在美国的小镇上和邪教奋战。
我无数次的保卫了沙漠里的军火库,也无数次的炸毁过他。
我曾在哥谭的夜空中翱翔,在纽约的阳光下荡起蛛丝。
我在三国一骑当千,见证了无数英雄的崛起和陨落。
我和神明厮杀,也屠杀过恶魔。
我已经离辉煌远去,我放下了枪和剑,长矛和武士刀。我的战马和家臣在思念着我,沙漠里的军火库也早已被尘封。地狱里的迪亚波罗无所事事,修道院里的墨菲斯托也依旧在百无聊赖的等待着我。已经被生产出来的天启坦克定格在了那个存档,谭雅看着已经就绪但却永远不会发射了的核弹一片茫然。
因为他们的英雄已经离去。
作者:东门吹牛
链接:https://www.zhihu.com/question/265793577/answer/1580752264
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
至于为什么说是休克,因为心里还有着这个愿想,某个闲暇的午后,打开电脑,点开尘封已久的某个游戏,再次去体验一番别样的人生,感受一段故事,重温感动与激情。
有这样一类男生,他们对待爱情是非常谨慎的。他们的谨慎,是因为他们会为了认定的人,投入他们所能投入的全部爱力。我不知道世界,上有没有爱力这个词,总之我没有听说过,这是一个我自创的词,它代表着在爱情中所应该做的所有事情的能力。
请千万不要把投入全部爱力和付出所有一切混为一谈。这样的男生并不会傻傻地视爱情为唯一,视女友为一切,更不会因此而无条件的付出。但这样的男生会全心全意爱着对方,关心对方,接纳对方,帮助对方。他最渴望的事情,就是彼此共同成长,互相成就完美的自己并和对方组成一条莫比乌斯环。
这样的男生多少是有些理想主义的,但说实在的,又有多少理想主义者完全不明白现实呢?因而他们也是现实的,知道一生只爱一人,一生只若初见这样的故事就像神话一般。但同时他们愿意为了个神话做最大限度的努力,即便最终无法实现,他们也能坦然接受。
无怪乎这样的男生对待爱情是非常谨慎的,因为他做好了追逐神话的准备,但他需要确定对方也是那个愿意和他一起追逐的人。否则,这段旅程将是单方面的无谓的奔跑。他的一生中,这样的奔跑是跑不了几次的了,非为不想,实为不能。
喜欢上述知乎上看到的回答,刺到了我心里的痛点,正好这几年情感不顺,我想我能做的,就是收拾好自己,为迎接更好的做准备吧。还是遇到过许多闪耀着光芒的孩子的。
之后想到什么再补充吧
最后修改:2020-11-26
]]>NOTE:UNIX Time Sharing System一文的作者为Dennis M. Ritchie 和 Ken Thompson,最初发表于ACM的Communications于1974年
本人在这里只是参考网上的资料,对原文进行了下翻译。本文翻译的原文来自于Sasank的博文:The UNIX Time-Sharing System - Sasank's Blog,对于本人有疑惑的翻译,我在一旁放了原文。
UNIX是一个通用的,多用户,交互式操作系统,为Digital Equipment公司的PDP-11/40和11/45计算机所设计。它提供了许多即使在大型操作系统中也很少见的功能,包括:
本文讨论文件系统和用户命令界面的实质和实现。
UNIX有三个版本。最早的版本(大约在1969–1970年)运行在Digital Equipment公司的PDP-7和PDP-9计算机上。第二个版本运行在不受保护的PDP-11 / 20计算机上。本文仅描述了PDP-11 / 40和PDP-45系统[1],因为它更现代,比起较早的UNIX,针对对一些重新设计时发现的不足或缺少的功能进行了修改。
PDP-11 UNIX 在1971年2月后可用,最初大约安装了40套;它们都比我们这里描述的系统要小。其中大部分用于处理非常具体的事务,例如处理和格式化专利申请,或其他的文字材料,对大量基于Bell系统的交换机进行故障数据的收集和处理,记录和检查电话服务命令。我们自己安装的则主要用于操作系统、编程语言、计算机网络及其他计算机科学方面的研究,当然也用来处理文档。
或许UNIX所达成的一个最重要的目的,是证明一个强大的交互式操作系统不需要那么昂贵的设备或人力维护。UNIX能够在不到40,000美元的硬件设备上运行,并且系统软件上的投入不到2人年。UNIX包含了许多在大型系统中少见的特性。可以预料的是,用户将会发现,这些重要的特性在UNIX系统中的实现是简单的、优雅的、易用的。
除了系统本身,UNIX上可主要的可用程序包括:汇编器,基于QED[2]的文本编辑器,链接加载器,符号调试器,如BCPL[3]一样具有类型和结构(C)的语言的编译器,BASIC方言的解释器,文本格式化程序,Fortran编译器,Snobol语言解释器,自顶向下的编译程序编译器(TMG)[4],自底向上的编译程序的编译器(YACC),表单字母生成器,宏处理器(M6),还有一个索引排序程序。
还有许多可维护的,实用的,娱乐性的和全新的程序。这些程序全都是在本地编写的。同时值得注意的是这个系统是完全自给自足的。所有的UNIX程序都在UNIX下维护,同样的,UNIX的相关文档也是通过UNIX编辑器编写和文字格式化程序格式化的。
用于安装我们的UNIX系统的PDP-11/45是一个16位字长(8-bit byte),具有144K字节(byte)核心内存的计算机;UNIX占用了其中的42Kb(K bytes)。然而这个系统包括了大量的设备驱动并且为I/O缓冲区和系统表分配了足够的空间;一个能够运行上述软件的最小系统,最小总共只需要50K字节的内核。
PDP-11有一个1Mb(M bytes)的固定磁头磁盘,用于文件系统的存储和交换,4个可移动磁头磁盘驱动器,均可在移动磁盘盒中提供2.5Mb存储,还有一个单独的,具有可移动磁头,可以使用可移除的40Mb磁盘的驱动器。还有一个高速打孔纸带阅读器,9磁道磁带,以及D-tape(各种磁带设施,可以处理和重写单个记录)。除了控制台键盘输入,还有用于假脱机输出到公用行打印机的14个可变速通讯接口,附加在100系列数据集和201数据集接口。还有一些设备如可视电话接口,音频反馈单元,音频同步器、照相排字机、数字交换网络、和在一个随PDP-11/20产生向量、曲线、字符并在Texktronix 611展现的存储显像管。
大部分的UNIX软件是使用上面提到的C语言[6]编写的的。早期版本的操作系统是使用汇编语言编写的。但是在1973年的夏天,用C语言重写了一遍。新系统的体积比老版本大3倍。由于新的系统不仅更易于理解和修改,还包括了许多函数的改进,如多道程序(multiprogramming/多任务处理)和多用户程序之间共享可重入代码的功能,所以我们认为由此带来的体积增长是完全可接受的。
UNIX中最重要的的工作是提供了一个文件系统。从用户的角度看,一共有三种类型的文件:普通磁盘文件,目录和特殊文件。
一个文件可以包含用户放置的任何类型的信息,例如符号或者二进制(对象)或程序。不需为系统指定特定的数据结构。文本文件包含简单的、由换行符分隔段落的字符串。二进制文件是当程序开始执行时,将会在出现在核心内存中的,按顺序存放的字的序列。少数用户程序以更多的结构操作文件:汇编生成器和加载器需要特定格式的对象文件。即便这样,文件的结构也是由使用它们的程序控制的,而不是系统。
目录提供了文件名和文件本身之间的映射,从而在整个文件系统上引入了一种结构。每个用户都有一个自己文件的目录;他还可以方便地创建包含文件群组的子目录。目录的行为与普通文件完全相同,不同之处在于目录无法由非特权程序写入,只有系统可以控制目录的内容。但是,具有合适权限的任何人都可以像读取其他任何文件一样读取目录。
系统维护几个目录供自己使用。其中之一是root目录。通过跟踪目录链中的路径直到找到所需的文件,可以找到系统中的所有文件,此类搜索的起点通常是根目录。另一个系统目录包含所有通常需要使用的程序,即所有的命令。但是就如将看到的,程序不必驻留在此目录中即可执行。
文件以14个或更少字符的序列命名。将文件名指定给系统后,它可以采用路径名的形式,该路径名是一系列目录名称,由斜杠/分隔,并以文件名结尾。如果序列以斜杠开头,则搜索将从根目录开始。名称/alpha/beta/gamma
使系统在根目录中搜索目录alpha
,然后在alpha中搜索beta
,最后在beta
中找到gamma
。 gamma
可以是普通文件,目录或特殊文件。作为限制,名称/
表示根目录本身。
不以/
开头的路径名会使系统开始在用户当前目录中进行搜索。因此,名称alpha/beta
在当前目录的子目录alpha
中指定了名为beta
的文件。最简单的名称(例如alpha
)是指本身在当前目录中找到的文件。作为另一种限制情况,如果文件名为空,则是指当前目录。
同一非目录的文件可能会以不同的名称出现在多个目录中,此功能称为链接,文件的目录条目有时称为链接。 UNIX与其他允许链接的系统不同,它与文件的所有链接都具有相同的状态。也就是说,文件在特定目录中不存在。文件的目录条目仅由其名称和指向实际描述文件的信息的指针组成,因此,文件实际上独立于任何目录条目而存在,尽管实际上会使该文件与最后一个链接一起消失。
每个目录始终至少有两个条目(.
和..
),每个目录的名称均指对应的目录。因此,程序在不知道完整路径名时可以读取当前目录为.
。而名称..
按照惯例是指向当前目录的父目录,即当前目录被创建时所在的目录。
目录结构被限定为为有根树的形式。除特殊条目 .
和 ..
外,每个目录必须作为另外一个目录(即其父目录)的条目出现,这是为了简化访问目录结构子树的程序的编写,更重要的是避免层次结构各部分的分离。如果允许链接到任意目录,则很难检测到从根到目录的最后一次连接何时断开。
特殊文件构成UNIX文件系统最不寻常的功能。 UNIX支持的每个I / O设备都与至少一个这样的文件相关联。特殊文件的读取和写入与普通磁盘文件一样,但是请求读取或写入会导致关联设备的激活。每个特殊文件的条目都位于目录/dev中,尽管可以像普通文件一样链接到其中一个文件。因此,例如要打孔纸带,可以在文件/dev/ppt上写。每个通信线路,每个磁盘,每个磁带驱动器以及物理核心内存都存在特殊文件。当然,活动磁盘和核心专用文件受到保护,不会受到任意访问。
以这种方式处理I / O设备具有三重优势:文件和设备I / O尽可能相似;文件名和设备名具有相同的语法和含义,因此可以将以文件名作为参数的程序传递给设备名。最后,特殊文件受与常规文件相同的保护机制。
尽管文件系统的根目录始终存储在同一设备上,但是不必将整个文件系统层次结构都驻留在该设备上。有一个挂载系统请求,其中包含两个参数:现有普通文件的名称,以及直接访问的特殊文件的名称,该文件的关联存储卷(例如磁盘包)应具有包含其自身的独立文件系统的结构目录层次结构。 “ mount”的作用是使对以前普通文件的引用改为引用可移动卷上文件系统的根目录。实际上,mount用一个全新的子树(存储在可移动卷上的层次结构)替换层次结构树(普通文件)的叶子。挂载之后,可移动卷上的文件与永久文件系统上的文件之间几乎没有区别。例如,在我们的安装中,根目录位于固定头磁盘上,包含用户文件的大磁盘驱动器由系统初始化程序安装,四个较小的磁盘驱动器可供用户安装磁盘包。通过在其对应的特殊文件上写入来生成可安装文件系统。可以使用实用程序来创建一个空文件系统,或者可以仅复制一个现有文件系统。
对不同设备上的文件进行相同处理的规则只有一个例外:一个文件系统层次结构与另一个文件系统层次结构之间可能不存在链接。实施该限制是为了避免繁琐的簿记工作,否则将需要这些簿记工作,以确保在最终卸下可移动的卷时要卸下链接。特别是,在所有文件系统的根目录(可移动或不可移动的根目录)中,名称..均指目录本身而不是其父目录。
尽管UNIX中的访问控制方案非常简单,但是它具有一些不寻常的功能。系统的每个用户都分配有一个唯一的用户标识号。当创建文件后,将会使用文件所有者的用户ID对其进行标记,还会为新文件提供一个七比特的保护位,其中六个指定文件所有者和所有其他用户的独立读取,写入和执行权限。
如果第七位打开,则每当文件作为程序执行时,系统都会暂时将当前用户的用户标识更改为文件创建者的标识。用户ID的这种更改仅在调用它的程序执行期间有效。设置用户ID功能提供特权程序,这些特权程序可能使用其他用户无法访问的文件。例如,一个程序可能会保存一个记录文件,除了程序自己,其他程序都不应该有阅读和修改该文件的权限。如果该程序的set-user-identification位为on,则它可以访问文件,尽管该访问可能被给定程序的用户调用的其他程序禁止。由于任何程序的调用者的实际用户ID始终可用,因此set-user-ID程序可以采取所需的任何措施来满足自己的调用者凭据。此机制用于允许用户执行精心编写的调用特权系统条目的命令。例如,有一个系统条目只能由“超级用户”(如下)调用,从而创建一个空目录。如上所述,目录应具有的条目有.
和..
。创建目录的命令由超级用户拥有,并具有set-user-ID位集。在检查其调用者的授权以创建指定目录后,它会创建该目录并设置进出方式为 .
和 ..
。
由于任何人都可以在自己的文件中设置“设置用户ID”位,因此通常无需管理即可使用此机制。例如,这种保护方案可以轻松解决[7]中提出的MOO计费问题。
系统将一个特定的用户ID(“超级用户”的ID)识别为不受通常文件访问限制的用户;因此,可以编写程序在解除保护系统不必要的干扰的情况下,转储和重新加载文件系统。
系统的I/O调用旨在消除各种设备和访问方式之间的差异。 “随机”和“顺序”的I/O之间没有区别,系统也不施加任何逻辑记录大小。普通文件的大小由写入文件的最高字节决定,不需要也不可能预先确定文件的大小。
为了说明UNIX中I/O的本质,下面以匿名语言总结了一些基本调用,这些匿名调用将指示所需的参数,而不会引起机器语言编程的复杂性。对系统的每次调用都可能导致错误返回,为简单起见,在调用序列中未对此进行表示。
要读取或写入假定已经存在的文件,必须通过以下调用将其打开:
filep = open(name, flag)
name
表示文件名。可以给出任意路径名。 flag
参数指示要读取,写入或“更新”文件,即同时读取和写入文件。
返回的值filep
称为文件描述符。它是一个小整数,用于在随后的调用中识别文件,以进行读取,写入或其他操作。
要创建一个新文件或完全重写一个旧文件,有一个create
系统调用,如果给定文件不存在,它将创建一个文件,如果存在,则将其截断为零长度。 create
还打开新文件进行写入,并且像open
一样,返回文件描述符。
文件系统中没有用户可见的锁,对于可以打开文件进行读取或写入的用户数量也没有任何限制;尽管当两个用户同时写入文件时文件的内容可能会被打乱,但实际上不会出现困难。我们认为,在我们的环境中,锁定既不是必需的也不是足够的,以防止同一文件的用户之间发生干扰。它们是不必要的,因为我们不必面对由独立进程维护的大型单文件数据库。它们是不足够的,因为普通意义上的锁定(例如,两个用户都正在使用制作该文件副本的编辑器来编辑文件时),无法防止一个用户在另一用户正在读取的文件上进行写操作,从而无法避免混淆。正在编辑。
应该说,当两个用户同时从事诸如写同一文件,在同一目录中创建文件或删除彼此的打开文件之类的不便活动时,该系统具有足够的内部联锁来维持文件系统的逻辑一致性。
除以下指示外,读取和写入是顺序的。这意味着,如果文件中的特定字节是最后写入(或读取)的字节,则下一个I / O调用隐式地引用了下一个字节。对于每个打开的文件,都有一个由系统维护的指针,该指针指示要读取或写入的下一个字节。如果读取或写入了n个字节,则指针前进n个字节。
打开文件后,可以使用以下调用:
n = read(filep, buffer, count)
n = write(filep, buffer, count)
在filep指定的文件和buffer指定的字节数组之间最多传输字节数。返回值n是实际传输的字节数。在写情况下,n与计数相同,除非在特殊情况下,例如I / O错误或特殊文件上的物理介质结尾。但是,在读取中,n可以毫无错误地小于count。如果读取指针非常靠近文件的末尾,以至于读取计数字符会导致读取超出末尾,则仅传输足够的字节才能到达文件的末尾;同样,类似键盘输入的设备绝不会返回多行输入。当读调用返回的n等于零时,它指示文件的结尾。对于磁盘文件,这在读取指针变得等于文件的当前大小时发生。通过使用取决于所用设备的转义序列,可以从打字机生成文件结尾。
写在文件上的字节仅影响写指针位置和计数所隐含的字节。文件的其他部分均未更改。如果最后一个字节位于文件末尾之外,则文件将根据需要增长。
要执行随机(直接访问)I / O,仅需要将读取或写入指针移动到文件中的适当位置:
location = seek(filep, base, offset)
取决于基数,与filep关联的指针从文件的开头,指针的当前位置或文件的结尾移到位置偏移字节。偏移量可能为负。对于某些设备(例如,纸带和键盘),寻线被忽略。从指针开始移动到的文件开头的实际偏移量返回到位置。
还有其他一些与I / O和文件系统有关的系统条目,将不再讨论。例如:关闭文件,获取文件状态,更改保护模式或文件所有者,创建目录,建立指向现有文件的链接,删除文件。
正如上面文件系统-目录中提到的,一个目录条目包含一个关联到文件的名字和一个指向自身的指针。这个指针是一个叫i-number
的整型数。当文件被访问时,它的i-number
被用作系统表(i-list
)的索引——系统表存储在目录所在设备的已知部分。由此找到的条目(文件的i-node
)包含文件的以下描述信息:
‘open’或‘create’系统调用的目的是,通过查询显式或隐式命名的目录把用户指定的路径名称转换为’i-number‘。文件一旦被打开,它所在的设备、i-number
、读/写指针都被存储在系统表中。系统表通过open和create返回的文件描述符索引。因此文件描述符能够很容易的与随后可能用到的read、write系统调用访问文件时所需的信息关联。
创建一个新文件时会分配一个i-node
给新文件,同时创建一个包含文件名和i-node编号的目录条目。建立一个到现有文件的链接时,会用新文件名创建一个新的目录条目,从源目录条目拷贝i-number
,增加i-node
中的link-count值。删除文件时,会减少目录条目对应的i-node
中的link-count,然后抹去目录条目。如果link-count减为0,文件的所有磁盘块都被释放,i-node
也被重新分配。
所有固定或可移除磁盘的空间都被划分为512字节的块,地址编号从0到设备本身的容量上限。每个文件的i-node中都为8个设备地址留有空间。一个小文件(非特殊文件)适合于8个或更少的块,在此情况下块的地址本身被存储。对于大文件(非特殊文件),8个设备地址中的每一个, 都指向一个256个块地址组成的文件本身。 这些文件可以大到8256512=1,048,576(2的10次方)字节。
前面的讨论都是针对普通文件的。当对一个i-node
显示是特殊文件的I/O请求时,其他的7个设备地址字就不重要了。列表被解释为组成内部设备名称的字节。这些字节确定了各自的设备类别,并由此决定由哪个系统程序来处理该设备的I/O。子设备编号选择了例如附加在一个指定的控制器上的1个磁盘驱动器,或多个相同的键盘输入接口之一。
在此情况下,mount
系统调用的实现非常简单。mount
维护一个系统表,其参数是mount
过程中,i-number
和指定的普通文件设备名称,匹配的值是设备名称。这个表用于在open和create时扫描的路径名称所转换的(i-number
,device)对的查询。如果找到匹配的对,i-number
被替换为1(这是所有文件系统上的根目录的i-number
),设备名被替换为表的中的值。
对于用户而言,文件的读写都是同步和没有缓存的。在read系统调用返回后,数据马上可用。而且很方便的是,在write系统调用之后,用户的工作空间可以重复使用。实际上系统维护了一个复杂的缓存机制,来大幅度降低访问文件所需要的I/O操作数。假定write系统调用是传输指定的单一字节。
Unix会查找缓冲区以确定受影响的磁盘块当前是否在核心内存中。如果不是,将从设备上读取。缓冲区中的对应的字节被替换,然后在待写入块列表中创建一个条目。write系统调用返回,尽管实际上I/O会晚一点才完成。相反的是,如果读取一个单独的字节,系统会确定该具有该字节的二级存储块是否已经在缓冲区中,如果是,该字节会被立刻返回。如果不是,这个块将被读取到缓冲区,然后取出该字节。
以512字节为一个单元读取文件的程序优于一次读写一个字节的程序,但获益不是很高,它主要来自于避免过多的系统开销。一个极少使用或者没有巨大的卷的I/O,以较小的单元读写就比较合理。
i-list是Unix一个非同寻常的的概念。实际上,这种组织文件系统的方法更加可靠和易于处理。对于系统自身而言,有一个优势是每个文件都有一个短的,无歧义的名字,可以简单的方式与保护、寻址、及其他访问文件所需的信息相关联。其同样允许通过一个简单快速的算法,来检查文件系统一致性,例如,验证每个设备包含有用信息的部分、分离或合并设备上已使用的空间。这个算法不依赖目录的层级关系,它只需要扫描线性的i-list。同时,i-list引起了某些单独的特性,这在其他文件系统中是没有的。例如,既然一个文件的所有目录条目都具有相同的状态,谁应该负责文件所占用的空间。文件的所有者负责是不公平的,总的来说,既然一个用户可能会创建文件,另一个用户可能会链接到它,而第一个用户可能会删除文件。第一个用户仍然是文件的所有者,但是他应该对第二个用户负责。最简单公平的算法是由链接到文件的用户均摊。当前版本的Unix避免了这个问题。
为了提供对Unix和文件系统的指示,我们分析一个7621行的汇编程序的时间。该汇编程序独自在机器上运行,总体的时钟时间是35.9秒,每秒运行212行。时间被按照如下方式划分:63.5%的时间用于汇编的执行,16.5%的时间是系统开销,20%的时间是磁盘的等待时间。我们不视图解释这些数字,也不会去和其他系统对比,只是说我们总体上对于这样的系统开销是满意的。
映像(image)是一个计算机执行环境。它包括核心映像,通用寄存器值,打开的文件状态,当前目录,以及与此相似的东西。印象是一个伪计算机的当前状态。
进程是一个映像的执行。当处理器代表一个进程去执行时,印象要驻留在核心内存中。在其他进程的执行过程中,它仍然驻留在核心内存中,除非一个激活的,更高优先级的进程强制性的把它从内存中交换到固定磁头的磁盘驱动器上。
一个映像的用户核心部分在逻辑上分成3段。程序文本段从虚拟地址空间的0开始。在执行过程中,这个段是写保护的,而且它的单一拷贝被所有执行相同程序的进程所共享。从最初开始,程序文本段的8K字节处,是一个可写的、非共享的数据段。这个段的体积可以通过系统调用扩展。在虚拟地址的最高处开始的是栈段,当栈指针变动时,它自动向下增长。
除了Unix引导它自身进入运行中,只能通过使用fork系统调用创建一个新的进程。
processid = fork(label)
当一个进程执行fork系统调用,它会被分离成两个独立的执行进程。这两个进程有各自独立的源印象的拷贝,并共享所有打开的文件。新进程唯一不同于父进程的是:在父进程中,控制从fork直接返回,在子进程中,控制被传递给label.fork系统调用返回的processid是相对于其他进程的标识。
因为父进程和子进程的返回点不同,fork后的每个印象都可以决定它是父进程还是子进程。
进程可以与相关的进程用和文件系统相同的read/write系统调用方式通信。
filep = pipe()
这个系统调用返回一个文件描述符filep,并创建一个叫做pipe的进程间通道。这个通道就像打开的文件一样,在印象中通过fork调用从父进程传递到子进程。read调用使用管道文件描述符,等待其他进程使用同样的管道文件描述符写入。在这点上,数据是在两个进程的印象之间传递的。进程不需要了解管道比普通文件的区别,只需要调用它。
另一个主要的系统原语通过一下方式调用
execute(file,arg1,arg2,...,argn)
该调用请求系统读入并执行名为file
的程序,并传递字符串类型的参数arg1,arg2,...argn
。一般的arg1
与file
相同,因此程序可以确定被调用的名称。用execute
执行的进程中所有的代码和数据,都被file所替代。但是打开的文件、当前目录、进程间的关系是不变的。只有当调用失败时,例如找不到file
对应的程序文件,或由于这个文件的执行许可位(execute-permission bit)没有被置位,才会从execute
原语返回。这很像机器指令中的"jump",而不是子程序的调用。
另一个进程控制的系统调用
processid = wait()
这个调用导致它的调用者将被挂起,直到它的某个子进程执行结束。wait返回终止进程的processid。如果调用的进程没有后代进程,会返回error。子进程的某些状态也是可用的,wait可以获取到孙子或更远祖先进程的状态。
最后,
exit(status)
这个系统调用终止一个进程,销毁其印象,关闭打开的文件,抹去这个进程。当父进程通过wait原语被通知,参数status所指示的状态就可用了。如果父进程已终止,其状态对祖父进程可用,以此类推。进程也可能由于一些非法动作或用户产生的信号终止。
对于大部分用户,与Unix的沟通都是通过一个叫做Shell的程序辅助完成的。Shell是一个命令行解释器:它读取用户输入的行,然后将他们解释为需要请求执行的程序。在最简单的情况下,一个命令行由命令名和跟随的参数组成,命令名和参数之间通过空格分隔。
command arg1 arg2 ... argn
Shell将命令名和参数分割为独立的字符串,这样就能找到名为command的文件。command可能是一个包含"/"的路径名以指出系统中的任何文件。如果command被找到,它将被引入到核心内存中被执行。Shell收集到的参数对于command是可访问的。Shell重新回到自己的执行当中,并立刻准备接受用户输入的下一条命令。
如果没有找到command对应的文件,Shell会自动在command前添加/bin/,来试图从/bin目录下再次寻找。/bin目录中包含了常用的命令。
前面讨论过的I/O表明每个程序用到的文件都必须通过程序打开或创建以获取文件描述符。通过Shell执行的程序,是以两个打开的文件描述符为0和1的文件开始的。这样的程序开始执行时,文件1用于写,可以理解为标准的输出文件。除了以下情况,即文件是用户的打印机。程序希望通过文件描述符1写入有用的,或调试用的信息。相反的,文件0用于读取,程序希望读取用户输入的信息。
Shell能修改标准分配的键盘或打印机文件描述符。如果command的一个参数是以">"开头,文件描述符1在command执行中就会被替换为>后面跟随的文件名。例如:
ls
上诉一般会在打印机上列出当前目录中的文件。
下面的命令:
ls > there
会创建一个there文件,然后把文件列表作为文件内容填进去。因此参数 ">"表示把输出放到there中。
另一面
ed
一般会进入编辑器,通过用户的键盘输入获取请求。命令:
ed < script
把script作为编辑器命令的文件解释。"<script"意味着,从script获取输入。
尽管"<",">"后面跟随的文件名作为命令参数出现,事实上它会被Shell作为命令解释,而不会作为参数传递给前面的命令。因此,没有在每个command中处理重定向的必要。命令只要在适当的时候使用标准的文件描述符0和1即可。
标准I/O概念的一个扩展,是将一个命令的输出用做另一个的输入。1个用竖线分隔的命令序列会使Shell同时执行所有的命令,并同时把每个命令的输出作为下一个命令的输入。在命令行中:
ls | pr –2 | opr
ls列出当前目录中的所有文件名;它的输出被传递给pr,这个命令可以把输入分页,并在页眉显示日期。参数“-2”表示显示2列。同样的,pr的输出作为opr的输入。这个命令把它的输入假脱机到一个离线打印的文件上。
这个过程也可以通过一个更笨拙的方式实现
ls >temp1
pr –2 <temp1 >temp2
opr <temp2
在没有重定向input和output的能力时,一个更笨拙的办法是接受用户的请求来分页其输出,把它按照多列打印,然后将输出传输到脱线。事实上期望ls
命令的作者提供如此之多的命令选项,这会让人很诧异,而且既不高效也不明智。
类似pr
的程序把它的标准输入拷贝到标准输出的操作,叫做过滤器(filter)。过滤器在我们处理字符串的直译、输入的排序、编码和解码上是很有用的。
Shell提供的另一个功能则相对简单:命令不必位于不同的行,并且它们可以用分号来分隔。
ls ; ed
首先将列出当前目录的内容,然后进入编辑器。
一个相关的功能更有趣。如果命令后跟&,那么命令行管理程序将不等待命令完成再提示,而是准备立即执行新命令,例如:
as source >output &
source文件将会被as程序编译,输出的结果将放入output文件;无论as花费多长时间,Shell都会立即返回。当前命令行管理程序不会等待as命令完成,将显示运行该命令的进程的标识(Pid),该标识可以用于等待命令完成或终止它。 &可以在一行中多次使用:
as source >output & ls >files &
同时在后台进行as编译命令和ls命令。在上面使用&的示例中,提供了除键盘输入之外的output文件;如果不这样做,那么各种不同的命令输出结果将会混合在一起。
Shell还允许在上述操作中加上括号,例如:
(date; ls)>x &
在文件x
上打印当前日期和时间,后跟当前目录列表。Shell也将会立即返回并准备运行其他请求。
Shell本身是一个命令,可以递归调用。假设文件tryout
包含以下几行
as source
mv a.out testprog
testprog
mv
命令使文件a.ou
t重命名为testprog
。 a.out
是汇编器的(二进制)输出,准备用于执行。因此,如果在控制台上键入以上三行,则将用汇编器编译source
,生成名为testprog
的程序,并执行testprog
。当输入行为tryout
,那么命令:
sh < tryout
将会使Shellsh
按顺序执行以上命令。
Shell还具有其他功能,包括替换参数以及从目录中文件名的指定子集构造参数列表的能力,也可以根据字符串比较或给定文件的存在,从而有条件地执行命令,并在已归档的命令序列内执行控制转移。
现在可以了解Shell的操作概述。在大多数情况下,命令行管理程序正在等待用户键入命令。键入换行符后的换行符,Shell的read
调用返回。命令行管理程序将分析命令行,并将参数放入适合execute
(执行)的形式。然后调用fork
。子进程(其代码当然仍然是Shell的代码)会尝试使用适当的参数进行execute
(执行)。如果成功,它将引入并开始执行给出名称的程序。同时,由fork
产生的另一个进程(即父进程)wait
(等待)子进程死亡。发生这种情况时,命令行管理程序就会知道命令已完成,因此它会键入提示符并读取键盘输入以获取另一个命令。
在这种框架下,后台流程的实现非常简单。每当命令行包含&
时,命令行管理程序就不会等待它创建的执行命令的进程。
幸运的是,所有这些机制都与标准输入和输出文件的概念很好地结合在一起。当由fork
原语创建一个进程时,它不仅继承其父级的核心映像,而且还继承其父级中当前打开的所有文件,包括那些文件描述符为0和1的文件。Shell当然会使用这些文件来读取命令行并编写其提示和诊断信息,在通常情况下,其子级(命令程序)会自动继承它们。但是,给定带有<
或>
的参数时,子进程将在执行执行之前使标准I / O文件描述符0或1分别引用命名文件。这很容易,因为根据协议,在打开(或创建)新文件时会分配最小的未使用文件描述符。仅需要关闭文件0(或1)并打开命名文件。因为命令程序运行的过程只是简单地终止,所以在<或>之后指定的文件与文件描述符0或1之间的关联将在该过程结束时自动结束。因此,命令行管理程序不需要知道文件的实际名称,它们是它自己的标准输入和输出,因为它不需要重新打开它们。
Filters是标准I/O重定向的直接扩展,使用的是管道(pips)而不是文件(files)。
在通常情况下,命令行管理程序的主循环永远不会终止。 (主循环包括属于父进程的fork返回的分支;即,该分支先进行等待,然后读取另一条命令行。)导致Shell终止的一件事是发现结尾输入文件的-file条件。因此,当使用指定的输入文件将Shell作为命令执行时,如下所示:
sh < comfile
comfile
中的命令将一直执行到comfile
结束为止;那么sh
调用的Shell实例将终止。由于此Shell进程是Shell另一个实例的子级,因此将返回在后者中执行wait
(等待),并可以处理另一个命令。
用户向其键入命令的Shell实例本身就是另一个进程的子级。 UNIX初始化的最后一步是创建单个进程,并调用(通过执行)名为init的程序。 init的作用是为每个打字机通道创建一个进程,用户可以拨打该进程。 init的各种子实例为输入和输出打开适当的打字机。由于调用init时没有打开文件,因此在每个进程中,打字机键盘将分别接收文件描述符0和打印机文件描述符1。每个进程都会键入一条消息,要求用户登录并等待阅读打字机,以获取回复。首先,没有人登录,因此每个进程都挂起了。最后,有人键入他的名字或其他身份证明。适当的init实例将被唤醒,接收登录行,并读取密码文件。如果找到了用户名,并且能够提供正确的密码,则init会更改为用户的默认当前目录,将进程的用户ID设置为登录用户的ID,然后执行命令行管理程序。此时,命令行管理程序已准备就绪,可以接收命令,并且登录协议已完成。
同时,init
的主流路径(其自身所有子实例的父级,后来将成为Shell)都在wait
(等待)。如果子进程之一终止,要么是因为命令行管理程序找到了文件的末尾,要么是因为用户键入了错误的名称或密码,因此,此初始化路径只是重新创建了已终止的进程,这反过来又重新打开了相应的输入和输出文件及类型另一个登录消息。因此,用户可以简单地通过键入文件结尾序列来代替对Shell的命令来注销。
如上所述的Shell被设计为允许用户完全访问系统的设施,因为它将以适当的保护模式调用任何程序的执行。但是,有时需要与系统使用不同的接口,并且可以轻松安排此功能。
回想一下,在用户通过提供其名称和密码成功登录后,init
通常会调用Shell解释命令行。用户在密码文件中的输入可能包含登录后要调用的程序的名称,而不是Shell。该程序可以自由地以其希望的任何方式解释用户的消息。
例如,秘书编辑系统用户的密码文件条目指定使用编辑器ed
而不是命令行管理程序。因此,当编辑系统用户登录时,他们位于编辑器内部并且可以立即开始工作。同样,可以防止它们调用不适合其使用的UNIX程序。在实践中,事实证明,允许暂时退出编辑器以执行格式化程序和其他实用程序是合乎需要的。
在UNIX上可用的几种游戏(例如国际象棋,二十一点,3D井字游戏)要求更为严格的限制环境。对于每个密码,密码文件中都有一个条目,指定要调用适当的游戏程序而不是Shell。以其中一款游戏的玩家身份登录的人发现自己仅限于该游戏,无法整体上研究可能更有趣的UNIX产品。
PDP-11硬件检测到许多程序错误,例如对不存在的内存的引用,未实现的指令以及在需要偶数地址的地方使用的奇数地址。此类故障会导致处理器陷入系统例程。当发现非法行为时,除非做出其他安排,否则系统会终止该过程,并将用户的映像写入当前目录的文件核心中。调试器可用于确定发生故障时程序的状态。
循环的程序会产生不需要的输出,或者引起用户重新思考的循环程序可以通过使用interrupt
信号来停止,该中断信号是通过键入“delete”字符生成的。除非采取了特殊措施,否则该信号只会导致程序停止执行而不会生成核心映像文件。
还有一个quit
信号,用于强制生成核心图像。因此,可能会暂停意外循环的程序,并且无需预先安排即可检查核心映像。
硬件生成的故障以及中断和退出信号可以通过请求被过程忽略或捕获。例如,命令行管理程序忽略退出以防止退出使用户注销。编辑器捕获中断并返回其命令级别。这对于在不丢失正在进行的工作的情况下停止较长的打印输出很有用(编辑器将操纵它正在编辑的文件的副本)。在没有浮点硬件的系统中,将捕获未实现的指令,并解释浮点指令。
也许自相矛盾的是,UNIX的成功很大程度上归因于它并非旨在满足任何预定义的目标。当我们中的一个人(汤普森/Thompson)对可用的计算机设施不满意,发现了一个很少使用的系统PDP-7并着手创建一个更友好的环境时,就编写了第一版。这种本质上的个人努力非常成功,引起了其余作者和其他人的兴趣,后来又证明了购买PDP-11 / 20的合理性,特别是支持文本编辑和格式化系统。后来11/20的数量已不多了,事实证明UNIX足以说服管理层投资PDP-11 / 45。我们在努力中的目标,即使是明确表达的,也始终与建立与机器的舒适关系以及探索操作系统中的思想和发明有关。我们没有面临满足别人要求的需求,对于这种自由,我们深表感谢。
现在回想起来,影响UNIX设计的三个因素如下:
首先,由于我们是程序员,所以我们自然而然地设计了该系统以使其易于编写,测试和运行程序。对编程便利性的渴望的最重要表达是该系统被安排用于交互使用,即使原始版本仅支持一个用户。我们认为,设计合理的交互式系统比“批处理”系统更具生产力和使用满意度。而且,这样的系统相当容易适应于非交互使用,而反之则不成立。
其次,系统及其软件始终存在相当严格的大小限制。考虑到局部性对合理效率和表达能力的渴望,尺寸约束不仅鼓励了经济性,而且还鼓励了设计的某种优雅。这可能只是“通过苦难得救”哲学的一个变相的变体,但在我们看来,它是有效的。
第三,几乎从一开始,系统就能够并且确实自我维护。这个事实比看起来更重要。如果系统的设计者被迫使用该系统,他们会很快意识到其功能和表面上的缺陷,并会极力主动地进行纠正,以免为时已晚。由于所有源程序始终可用,并且可以轻松地在线修改,因此我们愿意在他人发明,发现或提出新想法时修改和重写系统及其软件。
本文讨论的UNIX方面至少清楚地显示了这些设计考虑因素中的前两个。例如,从编程的角度来看,文件系统的接口非常方便。最低的接口级别旨在消除各种设备和文件之间以及直接访问和顺序访问之间的区别。不需要大型“访问方法”例程即可使程序员与系统调用隔离开来;实际上,所有用户程序要么直接调用系统,要么使用一个小型的库程序,该程序只有几十条指令,该程序可以缓冲许多字符并一次读取或写入所有字符。
编程便利性的另一个重要方面是,没有“控制块”具有复杂的结构,该结构部分地由文件系统或其他系统调用维护,并受其依赖。一般来说,程序地址空间的内容是程序的属性,我们试图避免对该地址空间内的数据结构施加限制。
考虑到所有程序都应可与任何文件或设备一起用作输入或输出的要求,从节省空间的角度出发,也希望将与设备有关的注意事项推入操作系统本身。唯一的选择似乎是使用所有程序加载用于与每个设备打交道的例程,这在空间上是昂贵的,或者取决于在实际需要时动态链接到适合于每个设备的例程的某种方式,这或者是昂贵的在开销或硬件中。
同样,过程控制方案和命令界面已被证明既方便又有效。由于Shell作为普通的可交换用户程序运行,因此它不会占用系统适当的有线空间,并且可以以很少的成本实现所需的强大功能,特别是考虑到Shell作为框架执行时所需要的框架。产生其他进程来执行命令的进程,I / O重定向,后台进程,命令文件和用户可选的系统接口的概念,在实现上都变得微不足道。
UNIX的成功不仅仅在于它是一个全新的操作系统,还在于对精心选择的丰富思想的充分利用,尤其是表明它们可以成为实现小型而强大的操作系统的关键。
伯克利分时系统[8]中存在分叉操作,本质上是我们执行时的操作。在许多方面我们受到了Multics的影响,Multics提出了I/O系统调用的特殊形式[9]以及Shell的名称及其常规功能。Shell应该为每个命令创建一个进程的概念Multics的早期设计也向我们建议了此功能,尽管后来出于效率原因将其删除。TENEX [10]使用了类似的方案。
以下展示了有关UNIX的统计信息,用于显示当前系统的使用规模以及在哪些方面被使用。我们那些不在文档准备工作中被包含的用户倾向于将系统用于程序开发,尤其是语言的工作。很少有重要的“应用程序”程序。
数量 | 描述 |
---|---|
72 | 使用者 |
14 | 最大并行用户 |
300 | 目录 |
4400 | 文件 |
34000 | 512字节的辅助存储块 |
有一个“后台”进程以最低的优先级运行;它用于吸收任何空闲的CPU时间。它已用于产生常数e – 2的百万位近似值,并且现在正在生成复合伪素数(基数2)。
180 | Commands |
---|---|
4.3 | CPU 小时(aside from background) |
70 | 连接小时 |
30 | 不同用户 |
75 | 登陆 |
比例 | 程序 | 比例 | 命令 |
---|---|---|---|
15.7% | C 编译 | 1.7% | Fortran 编译 |
15.2% | users’ programs | 1.6% | remove file |
11.7% | editor | 1.6% | tape archive |
5.8% | Shell (used as a command including command times) | 1.6% | file system consistency, check |
5.3% | chess | 1.4% | library maintainer |
3.3% | list directory | 1.3% | concatenate/print files |
3.1% | document formatter | 1.3% | paginate and print file |
1.6% | backup dumper | 1.1% | print disk usage |
1.8% | assembler | 1.0% | copy file |
15.3% | editor | 1.6% | debugger |
---|---|---|---|
9.6% | list directory | 1.6% | Shell (used as a command) |
6.3% | remove file | 1.5% | print disk availability |
6.3% | C compiler | 1.4% | list processes executing |
6.0% | concatenate/print file | 1.4% | assembler |
6.0% | users’ programs | 1.4% | print arguments |
3.3% | list people logged on system | 1.2% | copy file |
3.2% | rename/move file | 1.1% | paginate and print file |
3.1% | file status | 1.1% | print current date/time |
1.8% | library maintainer | 1.1% | file system consistency check |
1.8% | document formatter | 1.0% | tape archive |
1.6% | execute another command conditionally |
我们关于可靠性的统计数据比其他数据更为主观。以下结果是我们合并后的记录中最好的,时间跨度超过一年,年份非常早,年份为11/45。
由于软件无法应对重复的电源故障而导致奔溃的硬件问题,导致文件系统丢失了一次(五个磁盘中的一个磁盘)。该磁盘上的文件已备份三天。
“崩溃(crash)”是指计划外的系统重新引导或停止,每隔一天大约发生一次奔溃,其中约三分之二是由硬件问题引起,例如电源中断和莫名其妙的随机位置的处理器中断,其余是软件故障。最长的不间断运行时间约为两周。服务呼叫平均每三周进行一次,但每次的群集非常密集,总的正常运行时间约为我们计划中的每天24小时,共365天中的98%。
致谢。我们感谢R.H. Canaday, L.L. Cherry和L.E. McMahon为UNIX做出的贡献。我们特别感谢R.Morris,M.D.McIlroy和J.F.Ossanna的提供的创意,深思熟虑的批评和不断的支持。
最后修改于2020-11-26,今后会继续修改
Django是一个基于Python的Web框架,只需要一条命令就可以安装好,提供了许多开箱即用的工具
pip install django
# 或者 python3 -m pip instal django
使用Django建立新的项目,下面命令会建立一个mystie目录
django-admin startproject mysite
# django-admin startproject [项目名]
# Django默认的项目树
# .
# ├──manage.py
# └──mysite/
# ├── __init__.py
# ├── settings.py
# ├── urls.py
# ├── asgi.py
# └── wsgi.py
启动Django项目,默认是开发者模式启动,需要在setting.py里修改DEBUG的值并添加ALLOWED_HOSTS来以生产模式部署
python3 manage.py runserver
# python3 manage.py runserver [可选端口]
# python3 manage.py runserver 8080
# python3 manage.py runserver 0:8000
为Django项目添加新的应用
python3 manage.py startapp polls
# python3 manage.py startapp [应用名]
# 单个应用的文件结构
# .
# ├── __init__.py
# ├── admin.py
# ├── apps.py
# ├── models.py
# ├── tests.py
# ├── urls.py
# └── views.py
为每个应用创建需要的数据表,每次运行时都会在数据库里创建新定义的模型的数据表:
python3 manage.py migrate
每个应用如果需要加入到Django项目中的话,需要在setting.py中的INSTALLED_APPS添加设置类。
检测对数据库模型的修改并把已经修改的部分做迁移
python3 manage.py makemigrations [应用名]
自动执行数据库的迁移并同步管理数据库结构,sqlmigrate 命令接收一个迁移的名称,然后返回对应的 SQL
python3 manage.py sqlmigrate [应用名] 0001
# 命令的输出会是一段SQL命令
一般来说,改变模型需要这三步:
models.py
文件,改变模型。最后修改日期 2020-10-20
在此之前,我已经阅读过类似的书籍,刘涵宇的《解构产品经理》,起因是我在来到武汉理工大学就读时,就很有幸了解到了一个互联网团队,叫做TOKEN,并且加入了产品部并参与完成了多个项目的需求调研到最终完成开发,当时舟学长在我提交简历后不久,就发邮件推荐我在《人人都是产品经理》和《解构产品经理》中选择一本作为了解产品经理这一职位的入门书籍。
阅读了《人人都是产品经理2.0》后,我发现这本书和《人人都是产品经理1.0》的区别还是有点大,相较于《解构产品经理》一书的差别就更大了。1.0主要介绍个人成长相关的知识,2.0主要介绍产品成长相关的知识,而《解构产品经理》则更加偏向于从基本概念、方法论和工具的解构入手进行介绍。我在阅读完这几本书后,可能有部分是先入为主的影响,我还是觉得《解构产品经理》这本书讲的内容更加的容易让人理解产品经理需要注意什么,以及在一个开发团队中所需要扮演的角色,因为后者在介绍中加入了许多案例,基本上每一个知识点都配合了作者之前的实际开发中所遇到的问题都截图,并且提供了许多可以用来自我练习的例子和解析。
这本书其实是入门级别的科普读物,作者试图用简洁的语言、生动的例子和必要的术语来诠释产品经理的日常。
我先来简要的把这本书的各个章节的内容概括一下吧。
第一章主要讲述了产品经理的大概发展历程,主要思考方式,基本的工作内容等信息,核心观点有从现象到本质的思考方式,用心听、但不照做的做事方式等等。这一章也是本书的核心所在。
第二章主要讲述了产品的定位与分类,即定义产品的定义,在不同角度将产品分类。核心的观点是定位问题,抓住问题的主要矛盾。
第三章主要讲述产品的核心概念的提出与筛选,即如何并且依据相应信息对产品的概念进行判定。这是判定产品大方向问题的章节,属于战略层面的信息。
第四章主要讲述需求的采集和用户研究。包含采集需求方法,比如Z字型分析法,定性和定量,分析用户方法,比如用户故事,人性思考等。核心观点有具体到抽象再到具体,人性的七宗罪。
第五章主要讲述需求分析,即筛选需求,定义需求的方法论。主要方法是Y模型,一个从用户需求,到用户目标价值观,再到设计出功能的模型,核心的观点由表及里,由浅入深的分析方法。
第六章主要讲述功能的细化与打包,即如何判断与划分功能。主要方法就是KANO模型。核心观点是少即是多的设计理念,抓住矛盾的主要方面。
第七章主要讲述执行事情,即如何组队,如何生产。本章的定位偏于创业团队的思考,核心的观点就是尊重每个人的工作。
第八章主要讲述规划迭代的事情,核心的观点是做产品规划制作短期和长期规划,做产品要快。
第九章是运营的方法,核心观点是产品与运营要合为一体。
第十章是商业模式的一些看法,属于战略层面的一些事情。
第十一章是讲产品经理自身的发展的路径,核心的观点就是产品经理的七个层次。
作者在前几章主要讲的是产品规划到产出的详细过程,而产品经理本身又是怎么样的?作者提出了产品经理的四大修养:爱生活、有理想、会思考、能沟通。爱生活让我们充满动力,有理想让我们目标明确,会思考让我们方法得当,能沟通让我们团结前进。从这几方面作者通过几个案例进行了简单的分享,让我们明白了其实产品思维来源于生活,我们要善于观察、制定目标、不断学习、理解他人,善于合作,作者也通过几个小案例来分享了作为产品经理解决问题的通用方法和角度。
在许多的产品经理的书籍中,被反复强调的一个概念就是,发现用户真正的需求,并且根据需求去设计功能,最终形成产品的概念设计。这也是《人人都是产品经理2.0》一书中也花费了大量的篇幅去展开讲述,我个人感觉这也是产品经理最核心的能力。
在《人人都是产品经理2.0》中,个人感觉其实最多的对于产品经理相关概念的介绍,对于新手了解产品经理的职责和概念有挺好的帮助,但是感觉如果想要更加深入的了解产品经理,还是通过实践能跟好的学到知识,或者通过阅读在职人员写的博客。这本书里还有个很棒的一点是配了很多精心设计的思维导图。
总的来说,这本书算是给我自己对产品的概念进行了一次温习,毕竟已经快半年没有翻过相关的书籍了,这篇文章不算是很完全的读后感吧,只是我在读完这本书后进行的一次回顾和简单梳理。
最后来一段我很喜欢的原书摘录:
不是每个人都能以产品经理为业,但在我看来,产品经理是一类人,他做事的思路与方法可以解决很多实际的生活问题。 只要你能够发现问题并描述清楚,能将其转化为一个需求,进而转化为一个任务,还能争取到支持,发动起一批人,将这个任务完成,并持续不断以主人翁的心态去跟踪、维护这个产物,那么你就是产品经理。 至少,你已经是自己的产品经理,这才是“人人都是产品经理”的真谛。
]]>部分内容摘自知乎,豆瓣
最开始扮演的是一个帮助垂死之人完成临终愿望的公司的两个雇员,夏娃和尼尔博士,然后所谓完成临终愿望,是并不依赖现实生活中的任何东西,通过一个特殊的仪器,深入老人的记忆,追根溯源到老人的童年,通过改写其中一部分记忆来让老人产出其后一生的经历,并由此在脑海中亲手完成自己的愿望。
然后这次的对象是John,他的愿望是去月球,一个住在海边灯塔旁的老人
夏娃和尼尔博士一步步深入John的过去,从老年到中年,再到童年,通过仪器进行记忆旅途,但这趟旅程却并未发现John愿望背后的原因,他们知道的越多,反而却越迷惑,在约翰尼的一生中,几乎没有出现任何与月球相关的事物,他自己也从未对太空表达过任何兴趣,倒是知道了他与妻子River后期危机重重的婚姻,John经常不理解River经常做出的一些奇怪的事情,但是John任然一直守护在River身边,觉得自己就应该这样,自己也没想明白。
River做的的奇怪的事情:折了很多纸兔子,并且放在特别明显的地方,要求晚年时搬到海边灯塔居住,一直执着的带着一只鸭嘴兽毛绒玩具等
对了,River在几年前去世了
夏娃和尼尔觉得很奇怪,John为什么那么执着的想要去月球,他深爱但又矛盾重重的River行为很奇怪,所以他们继续层层深入John的记忆探寻
后来他们到达了John的童年,为当时的john注入了去月球这个梦想的强烈想法,但是John的未来并未发生改变,并没有成功去月球(对,这里的世界观是这个技术通过编织人工记忆,来实现愿望,但只能小幅度修改,但是会极大的影响后续发展,一般来说,如果小时候注入了当总统的强烈愿望,那么往往在记忆模拟中,最终真的可以当上
他们觉得非常奇怪,在继续的探寻中,他们发现了关键,John小时候被注射过β-受体阻滞剂(有同人曲,可以去听),这是一种会阻断记忆的药剂,一般用于让人忘记一段致命的打击记忆
最后突破了β-受体阻滞剂的封锁,两位博士看到了造成这个悲剧的原因,John小时候目击了自己最好的亲兄弟的车祸,为了让他恢复,他妈妈同意了医生开的β-受体阻滞剂
在车祸之前,John和River就已经相遇了,他们在一次嘉年华中遇到,在角落里一同仰望星空,认为星星组合起来像是一只兔子,而月球则像它的肚子。两人约定明年在同一个地点再见,如果John忘记了或迷路了,那么就约定在月球见面。
“那如果你忘记了……或者走丢了呢?”
“那么,我们总会在月亮上相见的,傻瓜!“
当晚John还把自己的鸭嘴兽送给了River
对了,他们相遇的地点,就是那座灯塔旁
海边的灯塔,旁边是溪流(River)
他们当时聊了挺多的(我印象深刻),从不喜欢自己的名字聊到满天星星,聊到对大人那无趣世界的厌恶,聊到天上的月亮就是复活节兔子那圆鼓鼓的肚子,聊到天上的星星是一座座灯塔。
“我一直认为他们是灯塔,成千上万的灯塔……闪耀着屹立于世界的尽头。他们看得到每座灯塔,他们想彼此谈天,可他们无能为力。他们天各一方,遥遥相对,因此无法听清对方的呼唤。他们所能做的,只有努力的绽放光芒……这就是他们唯一能做的。让那些光芒照耀其他灯塔,也照耀着我,因为总有一天……我也会成为他们的朋友”
John当晚如是说道,并且和River立下了明年的约定
之后,就是那场车祸,John目击了哥哥的丧生。通过β-受体阻滞剂,John忘记了那场悲剧,但同时,也失去了与River初次相遇的记忆,包括那个约定
第二年,灯塔下只有River到了,不过River并没有说什么,这个之后再说。后来他们再次相遇,在学校里面,并且最终结婚。虽然John不幸失忆,但是他们依然在相爱中获得了幸福,比如骑马,比如弹琴。只是River再也唤不回她心中最美好的那个John。对了,River在结婚当天知道了真实的情况,非常的伤心,因此之后她一只只的折着纸兔子,希望John能想起来,折了满满一屋子的兔子。她一遍遍的追问John你看见这些想起来了什么,不过可惜她的爱人John再也想不起来。
River身患亚氏保加症(大概就是社交互动有困难、自我局限且重复,也称为天才病/自闭症的一种?),如果John和River没有在那个月下相遇,River的童年可能就会一直活在被同学嘲笑中,可以说John当时给River带来了光。可惜River再也唤不回她心中最美好的那个John。
River的病导致了她的孤单和执着,也让她的爱拿的起,放不下,这就是她悲剧的原因,她过分想追回那个月下的John,让现实中John的对自己的一切爱意都伴随着对她自己的折磨。她没能直接向John坦白,因此剪短了自己的头发、折了许多纸兔子,希望借此间接唤醒他的记忆。但River直到去世时都没办法让John回忆起当初的约定,这也令不明真相的John留下深深的自责,不过John潜意识中也隐约残存着当时的约定,因此产生了去月球的愿望。
于是,当River将自己的生命化成灯塔,Johnny也启程前往月球,前往他们约定好相遇的地方。 —— 这句话来自知乎
现实中,知道真相的两位博士终于成功为John植入记忆。在这段新的记忆中,John和River在美国国家航空航天局相遇。弥留之际,John想象出与River一同乘坐航天飞机飞往月球的场景,他们最终结婚,建造了与现实中一样的房子。John的哥哥也没有因意外去世,反而成为了有名的作家,出席了John和River的婚礼、一同聚会、帮忙建造他们的房子。
“等你好久了……你忘记了……”
“嗯,不过不会再忘了,我说过,我们会在月亮上再见的!就在小兔子的肚子那里……”
在去月球的路上的对话
]]>正则表达式经常被应用于大规模批量的文本查找和修改,得到如C++,Java,php,Python,Perl等许多高级编程语言的支持,其中本文中将会使用Python语言作为例子来作为的正则表达式的应用语言。
正则表达式一般功能字符如下,如果希望使用原本字符需要转义字符转义
| \ | . | ? | + | * | ( | ) | [ | ] |
匹配对象如果有下划线,则真正匹配到的只有下划线部分
字符 | 描述 | 例子 | 匹配对象 |
---|---|---|---|
普通字符 | 匹配相同的单个字符 | bcd | abcde |
特殊字符 | 特殊功能,介绍如下 | \. | . |
| | 或运算,选择左右其中一个 | re1|re2 | re1 或 re2 |
. | 匹配除\n之外的任意字符 | . | a |
^ | 匹配字符串起始 | ^str | string |
$ | 匹配字符串末尾 | ing$ | string |
? | 匹配0/1次左端的正则式 | a?b | ab 或者 b |
* | 匹配0次及以上左端正则式 | a* | aaa |
+ | 匹配1次及以上左端正则式 | b+ | bbbb |
() | 对正则进行分组 | (ab)+ | ababab |
[...] | 匹配其中字符集的任意一个 | [abc] | a 或 b 或 c |
[.-。] | 匹配.到。之间任意一个字符 | [a-z] | a 或 d 等 |
[^...] | 不匹配此字符集中任何一个字符 | [^a-z] | 1 等非字母 |
{N} | 匹配N次左端的正则式 | (ab){2} | abab |
{N,M} | 匹配M-N次左端的正则式 | (ac){1,2} | ab 或 abab |
字符 | 描述 | 例子 | 匹配对象 |
---|---|---|---|
\d | \d匹配任何一个十进制数字,\D不匹配任何数字 | \d\d\d | 123 |
\w | \w匹配任何一个字母字符,\W不匹配任何字母字符 | \w\w | ab |
\s | \s匹配任何空格字符,\S不匹配任何空格字符 | I\sLike | I Like |
\b | \b匹配任何单词边界,\B不匹配任何单词边界 | \bLike | I Like |
\A(\Z) | \A匹配字符串的开始,\Z匹配字符串的结尾 | \Acar | Hey.Car stop. |
匹配对象如果有下划线,则真正匹配到的只有下划线部分
例子 | 匹配对象举例 | 描述 |
---|---|---|
Hello | Hello | 匹配特定单词 |
^[0-9]*$ | 123 | 单独的数字 |
^\d{8}$ | 12345678 | 单独的8位的数字 |
^(-|+)?\d+(.\d+)?$ | -12.33 | 正数、负数、和小数 |
^[A-Za-z0-9]+$ | Kingfish404 | 英文和数字 |
^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)*$ | jinyu929@qq.com | Email地址 |
# 引入python自带的正则包
import re
m = re.search('def', 'abcdef')
print(m.group(0))
# 输出结果: 'def'
import re
text = "He was carefully disguised but captured quickly by police."
regExp = "\w+ly" # 正则表达式
result = re.findall(r"\w+ly", text) # 参数为(r"正则式",待匹配式子)
print(result)
# 输出结果:['carefully', 'quickly']
# 下方语句功能与上方相同
result = re.findall(regExp, text)
print(result)
result = re.compile(regExp).findall(text)
print(result)
# 输出结果均为:['carefully', 'quickly']
更详细的应用见REF中的Python文档
PS:本文其实很早就建好了文件,不过之前一直忘记写了。
]]>下面是一个爬虫的简单例子,使用到了requets包和BeautifulSoup包。其中
import requests
from bs4 import BeautifulSoup
# 搜索关键字为“数据挖掘”,工作地区为北京的url
# dps为工作地区的参数,010为猎聘网为北京地区指定的区域号
url="https://www.liepin.com/zhaopin/?key=数据挖掘&dqs=010"
# 发起访问GET请求
page = requests.get(url = url)
# 输出返回信息
print(page.url)
# 初始化 soup 对象,page.text为爬取到的带有html标签页面
soup = BeautifulSoup(page.text,"html.parser")
# 找到<h3>标签,实质是获取所有包含职位名称及链接的标签内容
soup = soup.find_all("h3")
#在每个<h3>中进行抽取链接信息
times =0
for i in soup:
#有些<h3>标签不包含求职信息,做简要判断
if i.has_attr("title"):
#抽取链接内容
href=i.find_all("a")[0]["href"]
print(href)
if times ==1 :
print("i = -----")
print(i)
times=times+1
# 输出为:
# https://www.liepin.com/job/1929399435.shtml
# https://www.liepin.com/job/1925362683.shtml
# https://www.liepin.com/job/1924144611.shtml
# https://www.liepin.com/job/1919418647.shtml
一般情况下也可以用正则表达式替代如果你对正则表达式感兴趣,可以去看我的另外一篇博客:Regular-Express01。
需要学习爬虫,首先要了解计算机网络上的基础知识。
我们一般通过url,使用浏览器来访问互联网上的网页信息。
HTTP 定义了一组请求方法, 以表明要对给定资源执行的操作。指示针对给定资源要执行的期望动作. 虽然他们也可以是名词, 但这些请求方法有时被称为HTTP动词. 每一个请求方法都实现了不同的语义。
一个完整的启用了ssh加密的url结构如下(如过未启用ssh加密,则将https替换为http)
使用百度搜索spide的urlr: https://www.baidu.com/s?wd=spider&pn=90#button
我们来对上面的url进行拆解,可以得到以下的组成公式
[协议名] + [域名] + [路径] + [可选-参数] + [可选-浏览器锚点]
其中,必选参数:[协议名] + [域名] + [路径] 为https://www.baidu.com/s,一般情况下我们不需要特别的进行关心,我们需要关心的是[可选-参数],这里为wd=spyder和pn=90,[可选-参数]和前面的内容用?来进行分隔,[可选-参数]中的变量使用&来进行分隔。
这里wd=spyder就是我们传送给服务器的参数,百度的服务器通过这个来知道我们查询的内容是spyder,以此来返回我们需要的查询结果。
#button是浏览器锚点,一般用于同一个页面内的移动,我们可以不需要关心。
一个简单的html结构如下,我们使用爬虫每次所获取的,就是一个类似下面结构的文本数据,一般情况下,我们所需要获取的内容就在body标签内,一般使用正则表达式或者XPATH等技术获取自己期望获得的信息。
<!-- 当前网页的标准规范声明 -->
<!DOCTYPE html>
<html>
<!-- 网页头信息 -->
<head>
<title>这里是网站的标题</title>
</head>
<!-- 网页css样式 -->
<style>
*{
text-align:center;
}
</style>
<!-- 网页内容信息 -->
<body>
<h1>这是一个标题</h1>
<div>这里是网页的一个块</div>
<a href='https://example.com'>这里是一个超链接</a>
</body>
<script>
console.log('hello world') // 网页的JavaScript脚本
</script>
</html>
Python爬虫要经历爬虫、爬虫被限制、爬虫反限制的过程。当然后续还要网页爬虫限制优化,爬虫再反限制的一系列道高一尺魔高一丈的过程。爬虫的初级阶段,添加headers和ip代理可以解决很多问题。
服务器通过用户请求中的User-Agent信息来识别用户的浏览器信息,所以我们可以通过自定义headers中的User-Agent的值来将我们的爬虫伪装成浏览器。不过此方法仅限于解决部分网站简单的反爬虫机制,一些网站需要使用代理IP解决。
IP代理通过使用随机的IP来爬取目标网站,让服务器误以为是来自不同的电脑的正常访问,从而达到反-反爬虫的目的。具体细节可以参考这篇博客。
对于某些网站,内容会通过判断用户Cookies来进行动态加载,这时我们还需要在headers信息中添加自己浏览器的Cookies来进行内容的爬取,具体方式和UA模拟很像,不过也要注意防止同一Cookies访问次数过多而导致Cookies被封。
猎聘网是我们本次的目标爬取网站,计划爬取其中北京,上海,深圳,广州,武汉,杭州等地区的数据挖掘,图像算法工程师,java后端,互联网产品经理职位等信息。
首先我们对猎聘网的url进行分析:
下方是未选择城市时数据挖掘职位的url链接:
https://www.liepin.com/zhaopin/?key=数据挖掘
当对城市如北京,进行选择后,浏览器上面的url变成了
https://www.liepin.com/zhaopin/?dqs=010?key=数据挖掘
你实际上看到的url可能比我上面的url要复杂,原因是我将其中不重要的变量进行了删除,只保留了我们需要的变量,即城市和职位,一般情况下,手动删除url中的变量,尝试几次即可找到我们需要的变量。
通过上方的分析,我们可以知道猎聘网查询职位的url构成为
https://www.liepin.com/zhaopin/?key=[职位名]&[dqs=城市码]
这样我们可以使用Python根据上方的公式来构建url爬取不同地点的不同职位,这里我通过手动尝试的方式,找到了不同城市的dqs值并构建数组,代码如下:
# 猎聘网基础url
url = 'https://www.liepin.com/zhaopin/?'
# 待爬取的职位
jobs = ['数据挖掘', '图像算法工程师', 'java后端', '互联网产品经理']
# 待爬取的城市,和对应的dqs
citys = ['北京', '上海', '深圳', '广州', '武汉', '杭州']
cityIds = ['010', '020', '050090', '050020', '170020', '070020']
有了以上的信息,我们通过循环即可构造不同城市不同职位的url,如果想要实现翻页,原理也类似,这里不再进行叙述。
我们可以对代码进行一个简单的测试
import requests
# 这里填入上方的几个数组
# 构建url
target_url = url+'key='+jobs[0]+'&dqs='+cityIds[0]
# 用get方法爬取数据
getData = requests.get(url=target_url)
# 将获得的数据使用utf-8格式进行转码
finalHtmlData = getData.content.decode("utf-8")
# 预期的输出为爬取到的html内容
print(finalHtmlData)
上方就是一个最简单的爬取方式,你可以通过循环结构来构建不同职位的url来进行爬取,同时也可以通过正则或者其他包来对获取的finalHtmlData进行解析,获取我们需要的数据。
下面再来演示通过正则输出目标数据,假如我们想找到finalHtmlData中所有的职位url
regExpUrl = '<a[^>]*href=\"(https://www.liepin.com/job[^"]*)\"[^>]*'
import re
# 正则表达式匹配
finalAData = re.compile(regExpUrl).findall(finalHtmlData)
# 预期输出为一堆的url
for i in finalAData:
print(i)
正则表达式及其使用说明见我的另外一篇博客
上方代码的完整整理如下:
import requests
# 这里填入上方的几个数组
# 猎聘网基础url
url = 'https://www.liepin.com/zhaopin/?'
# 待爬取的职位
jobs = ['数据挖掘', '图像算法工程师', 'java后端', '互联网产品经理']
# 待爬取的城市,和对应的dqs
citys = ['北京', '上海', '深圳', '广州', '武汉', '杭州']
cityIds = ['010', '020', '050090', '050020', '170020', '070020']
# 构建url
target_url = url+'key='+jobs[0]+'&dqs='+cityIds[0]
# 用get方法爬取数据
getData = requests.get(url=target_url)
# 将获得的数据使用utf-8格式进行转码
finalHtmlData = getData.content.decode("utf-8")
# 预期的输出为爬取到的html内容
# print(finalHtmlData)
regExpUrl = '<a[^>]*href=\"(https://www.liepin.com/job[^"]*)\"[^>]*'
import re
finalAData = re.compile(regExpUrl).findall(finalHtmlData)
for i in finalAData:
print(i)
完整代码的Github仓库地址:FindJobsApp
智能算法在大多数的情况下都只能得到最优解的近似值,并且可能会陷入到局部最优解当中。
模拟退火算法总的来说还是一种优化算法,就如同名称一样,打铁后,钢铁冷却的过程就是其逐渐成形的过程. 模拟的是淬火冶炼的一个过程,通过升温增强分子的热运动,然后再慢慢降温,使其达到稳定的状态。 当温度很高时,钢铁容易发生形变,因此我们可以将其塑造成我们想要的形状;而当温度逐渐降低时,钢铁发生形变的容易程度降低,更加的趋向于稳定.
模拟退火算法的关键解释:
建立初始解
通常是以一个随机解作为初始解. 并保证理论上能够生成解空间中任意的解,也可以是一个经挑选过的较好的解,初始解不宜“太好”, 否则很难从这个解的邻域跳出,针对问题去分析。
生成扰动邻解
邻解生成函数应尽可能保证产生的侯选解能够遍布解空间,邻域应尽可能的小,能够在少量循环步中允分探测.,但每次的改变不应该引起太大的变化。
Metropolis准则
Metropolis法则是SA接受新解(扰动邻解)的概率。
$$ P(x=>x')=\begin{cases} 1 & f(x')<f(x) \ e^{-\dfrac{f(x')-f(x)}{T}} & f(x')>=f(x) \end{cases} $$
注:x表示当前解,x'为新解,这也是模拟退火区别于贪心的一点,我们在更新新解的时候对于不满足条件的情况,我们也有一定的概率来进行选取,从而可以使得退火模拟可以跳出局部最优解.
降温公式
经典模拟退火算法的降温方式:
$$ T(t)=\dfrac{T_0}{log(1+t)} $$
快速模拟退火算法的降温方式:
$$ T(t)=\dfrac{T_0}{1+t} $$
常用的模拟退火算法的降温方式还有(通常0.8<α<0.99):
$$ T(t+\Delta{t})=\alpha{T(t)} $$
终止条件自己设定阈值即可。
流程图如下:
粒子群算法的关键解释:
建立初始解
首先,我们需要设置最大的速度区间,防止超出最大的区间。位置信息即为整个搜索空间,我们在速度区间和搜索空间上随机初始化速度和位置。设置群体规模。
求个体极值与全局最优解
个体极值为每个粒子找到的历史上最优的位置信息,并从这些个体历史最优解中找到一个全局最优解,并与历史最优解比较,选出最佳的作为当前的历史最优解。
更新速度和位置的公式
更新公式为:
$$ V_{id}=\omega{V_{id}}+C_{1}random(0,1)(P_{id}-X_{id})+C_{2}random(0,1)(P_{id}-X_{id}) \ X_{id}=X_{id}+V_{id} $$
算法的流程图如下:
其中,$\omega$称为惯性因子,$C_{1}$和$C_2$称为加速常数,一般取$C_{1}=C_{2}\in[0,4]$。$random(0,1)$表示区间上的随机数。$P_{id}$表示第$i$个变量的个体极值的第$d$维。$P_{gd}$表示全局最优解的第$d$维。
流程图如下:
算法流程描述如下:
觅食行为:一般情况下鱼在水中随机地自由游动,当发现食物时,则会向食物逐渐增多的方向快速游去
聚群行为: 鱼在游动过程中为了保证自身的生存和躲避危害会自然地聚集成群,鱼聚群时所遵守的规则有三条:
-- 分隔规则:尽量避免与临近伙伴过于拥挤;
-- 对准规则:尽量与临近伙伴的平均方向一致;
-- 内聚规则:尽量朝临近伙伴的中心移动。
追尾行为:当鱼群中的一条或几条鱼发现食物时,其临近的伙伴会尾随其快速到达食物点
随机行为:单独的鱼在水中通常都是随机游动的,这是为了更大范围地寻找食物点或身边的伙伴
以上三种算法的代码见我的GIthub仓库:https://github.com/Kingfish404/Algorithm
]]>GIMP是可用于GNU/Linux,OS X,Windows和更多操作系统的跨平台图像编辑器。它是免费软件,您可以更改其源代码并分发更改。
无论您是平面设计师,摄影师,插图画家还是科学家,GIMP都能为您提供完善的工具来完成您的工作。借助许多自定义选项和第三方插件,您可以使用GIMP进一步提高生产力。
Krita 是一款由社区驱动的自由开源数字绘画软件,可免费使用,无任何商用限制,让每一位画师都可以随心所欲地表达创意。
开源视频编辑器。永久免费且易于使用。
一个小巧易用的剪辑软件
一个超级优秀的3D建模软件,同时还可以制作动画
Matlab的替代品,兼容Matlab的基础语法
]]>Qt开发中,既可以手动通过代码的方式构建Qt桌面应用,也可以通过图形化窗口拖放设计界面,而后者主要通过.ui文件来实现。
Qt在编译时,会自动检测项目中的.ui文件,并对其转换成源代码文件,一般情况下,C++开发环境下的widgets.ui
文件会被转换成ui_widgets.h
和ui_widgets.cpp
,这两个文件可以直接编译成我们需要的应用,默认情况下不会直接编译这两个文件,而是将其包含进我们自己写的类文件里面,通过指针的方式,实现两个类(.ui文件自动生成的类和我们自己编写的类)组合成一个完整的类,最终编译成一个完整的应用。
Qt使用信号(SIGNAL)与槽(SLOT)来建立事件的响应机制。因为有了信号(SIGNAL)与槽(SLOT)的编程机制,在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单。信号就是在特定情况下被发射的事件,槽就是对信号响应的函数。信号(SIGNAL)与槽(SLOT)关联是用 QObject::connect() 函数实现的,其基本格式是:
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
通过上述函数就可以将signal()函数与slot函数相关联,这里要注意的是slot函数在类定义时需要在函数修饰符后加入slots标记。如private slots:
。通过上述connect
函数就可以实现当signal()
函数被调用时,slot()
收到响应并执行。
Qt的GUI项目在windows平台下运行时总是弹出控制台
然后我想到了将以下命令加到CMakeLists.txt里面来实现隐藏控制台的效果
IF (CMAKE_SYSTEM_NAME MATCHES "Windows")
set( CMAKE_EXE_LINKER_FLAGS -Wl,-subsystem,windows)
ENDIF (CMAKE_SYSTEM_NAME MATCHES "Windows")
Qt官方好像是为了更好的推广Qt,最新的5.15版本不在提供离线安装版本,必须去官网注册安装,不过源代码任然提供下载。
我下载了Qt 5.15的源代码,不过不知道为什么,在win10 2004上使用Qt的minGW进行编译一直编译出错,在执行mingw32-make
与mingw32-make install
命令时总是出错。如果有成功编译的同学看到了,希望能在评论区留下教程。
这是一个简单粗暴的git简明教程,只是提供简单的使用方法参考,基本原理建议看官方文档或者菜鸟教程
Git软件建议从Git 官网下载并安装,记得要把git路径添加到Path
提前说明,本人截止202-06-02日对git底层原理尚且一窍不通,只是搬砖搬多了熟练了罢了
用 git init 在目录中创建新的 Git 仓库。 你可以在任何时候、任何目录中这么做,完全是本地化的。
在目录中执行 git init,就可以创建一个 Git 仓库了。
执行该命令后,会在当前目录下创建一个名为目标url仓库名称的目录,其中包含一个**.git**的目录(在window平台上一般是隐藏文件夹),用于保存下载下来的所有版本记录。
例子如下:
git clone https://github.com/Kingfish404/DataStructrue-Algorithm.git
如果要自己定义要新建的项目目录名称,可以在上面的命令末尾指定新的名字.
使用git pull命令可以从远程仓库拉去更新
如果你本地有未提交的更改,可能会有错误提示信息,那么这时候建议你执行以下命令,提交当前更改再拉去远程仓库并合并更改
git add *
git commit -m "你命名的提交的值"
git pull
git add 命令可将该文件添加到缓存,一般为了方便常用
git add * # add后面接需要提交的文件,如果为*则默认全选
使用git add命令将想要保存的的内容写入缓存区, 而执行git commit将缓存区内容添加到仓库中。
例子:
git commit -m "你命名的提交的值" # -m 后面为提交的名称,为可选参数
清除错误和构建用于替换的历史
$ git reset [commit]
撤销所有 [commit] 后的的提交,在本地保存更改
$ git reset --hard [commit]
放弃所有历史,改回指定提交。
通过git push命令可以将本地已经提交(commit)的更改推送到远程仓库,这个仓库的地址在.git目录下FETCH_HEAD文件中有记录
结束了一天的工作,最喜欢的三连如下:
git add *
git commit -m "daily" # -m 后面为提交的名称,为可选参数
git push
分支是使用 Git 工作的一个重要部分。你做的任何提交都会发生在当前“checked out”到的分支上。使用 git status 查看那是哪个分支。
$ git branch [branch-name]
创建一个新分支
$ git checkout [branch-name]
切换到指定分支并更新工作目录(working directory)
$ git merge [branch]
将指定分支的历史合并到当前分支。这通常在拉取请求(PR)中完成,但也是一个重要的 Git 操作。
$ git branch -d [branch-name]
删除指定分支
以上就是最常用的git命令,更多的操作可以直接在命令行输入git可以查看自带的说明,或者去下面的文档进行学习.
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
<command> [<args>]
...省略...
See 'git help git' for an overview of the system.
最后更新于: 2020-07-06
好几个月没有写博客了,期间经历的最大的事情可能就是新冠疫情了吧,本来以为几个月就会过去了,没想到一直持续到今天,国内还好,国外已经有几百万的确诊病例了,我其实挺期待开学的,想念学校的南湖图书馆了。
在家里待着的确相比学校更安全,也节约了挺多通勤的时间,但是随着时间的推移,我的学习效率也开始下降了,家里还是没法提供一个比得上学校的环境让我好好学习,唉。
人往往等到失去后才懂得珍惜
上个月匆匆忙忙组了一个队伍,参加计算机设计大赛,因此临时学了1天的Django框架,做出了一个Demo,并且居然幸运的通过了校赛,不过感觉省赛不会那么的顺利了。
Django是一个基于Python的Web框架,只需要一条命令就可以安装好,提供了许多开箱即用的工具
pip install django
# 或者 python3 -m pip instal django
使用Django建立新的项目,下面命令会建立一个mystie目录
django-admin startproject mysite
# django-admin startproject [项目名]
# Django默认的项目树
# manage.py
# mysite/
# __init__.py
# settings.py
# urls.py
# asgi.py
# wsgi.py
启动Django项目,默认是开发者模式启动,需要在setting.py里修改DEBUG的值并添加ALLOWED_HOSTS来以生产模式部署
python manage.py runserver
# python manage.py runserver [可选端口]
# python manage.py runserver 8080
# python manage.py runserver 0:8000
为Django项目添加新的应用
python manage.py startapp polls
# python manage.py startapp [应用名]
更多Django相关的笔记,见我的另外一篇博客。
最后编辑于 2020-10-16
2020年初,我有幸加入了杜教授的图像与算法实验室,开始学习一些机器学习和图像识别的知识。最近学完了机器学习的入门算法知识,就在这里写写笔记吧。
最后更新于: 2020-03-05
最近人工智能挺火的,特别是基于深度学习的图像识别领域,现在的成功率已经远远高于人类的水平了。不过要学习深度学习,肯定是要先学习先修课程机器学习,还有一些基本的数理知识,比如高数,线代,概率论,数分等等。
在不直接针对问题进行编程的情况下,赋予计算机学习能力的一个研究领域 --Arthur Samuel,1959
计算机算法的研究,并通过经验自动进行改善 --Tom M.Mitchell,1996
学术性来说,就是:对于某类任务T和性能度量P,如果一个计算机程序在T上以P衡量的性能随经验E而自我完善,那么我们称这个计算机程序在从经验E学习
通俗一点,就是:机器通过统计学算法,对大量的历史数据进行学习从而生成经验模型,利用经验模型指导业务。
21世纪三巨头:Yoshua Bengio,Yann LeCun,Geoffrey Hinton
KNN: K-Nearest Neighbour K个最近邻接点
循环:
计算已知类别数据集中的点与 当前点 之间的距离
按照距离递增次序排序
if 样本点遍历完成:
跳出循环
返回前 K 个样本点
统计 K 个样本点中出现频率最高的类别标签
ID3: Iterative Dichotomiser 3 迭代树三代
由香农提出的,信息论里的概念,描述混乱程度的度量,取值范围0~1,值越大越混乱
信息熵计算公式,$p_i$为情况$i$的概率:
$$ H(U)=E[-log_2 p_i]=-\sum_{i=1}^{n}p_{i}log_2 p_i $$
循环:
选取当前最佳特征
按照取值产生分支
if 满足分支终止条件:
if 无待处理分支:
跳出循环
生成结果并处理
选择K个点作为初始类簇中心
循环:
将每个样本点指派到最近的类簇中心,形成K个类簇
重新计算每个类簇的中心
if 类簇不发生变化 or 达到最大迭代次数:
跳出循环
分类完成
确定最小支持度,最小置信度
确定1-频繁项集
确定2-频繁项集
确定3-频繁项集
确定关联规则
博弈论是ACM比赛中的一个很重要的理论,虽然很多情况可以套用公式
本文主要介绍博弈论的经典类型和公式
一堆n个物品,两个人轮流从中取出1~m个,最后取光者胜(不能继续取的人输)。
同余定理:n=k∗(m+1)+r,先者拿走r个,那么后者无论拿走1 m个先者只要的数目使和为m+1,那么先手必赢。反之若n=k∗(m+1),那么先手无论怎样都会输。
if (n % (m + 1)) return false;
else return true;
一堆石子有n个,两人轮流取,先取者第一次可以去任意多个,但是不能取完,以后每次取的石子数不能超过上次取子数的2倍。取完者胜。
同样是一个规律:先手胜当且仅当n不是斐波那契数。
复制代码
f[0] = f[1] = 1;
for (int i = 0; f[i - 1] < n; i++)
{
f[i] = f[i - 1] + f[i - 2];
if (f[i] == n) return true;
}
return false;
有两堆各若干物品,两个人轮流从任意一堆中至少取出一个或者从两堆中取出同样多的物品,规定每次至少取一个,至多不限,最后取光者胜。
这里的必输局势:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。从这些必输局势可以发现,每组的第一个是前面没有出现的最小正整数,ak=[k∗(1+5–√)/2], bk=ak+k, k=0,1,2,3...。
所以,先求出差值,差值*黄金分割比 == 最小值的话后手赢,否者先手赢。
double r = (sqrt(5) + 1) / 2;
int d = abs(a - b) * r;
if (d != min(a, b)) return true;
else false;
注:如果a,b的值非常大的话,需要高精度来计算这个double类型的r。
有n堆物品,两人轮流取,每次取某堆中不少于1个,最后取完者胜。
假如有3堆物品(a,b,c) (0,0,0)状态时先手是一个必输局势因为没有东西可取,(0,n,n) 状态时也是必输局势只要后者在另一堆取得物品与前者一样多时那么前者也就是必输局势。慢分析(1,2,3)也是一个必输局势。如果我们将其转化为二进制形式并通过异或运算(^)我们会发现:
0001^0010^0011=0000
通过验证所有的堆数量累^后只要为0就都是必输局势,所以我们就只要记住这个规则:将n堆物品数量全部异或后结果为0先手必败,否则必胜。
int res = 0;
for (int i = 1; i <= n; i++)
res ^= arr[i];
if (res) return true;
else return false;
问:但是,实际问题中不可能给出如此标准的博弈模型,对于更加一般的博弈问题,我们该如何求解呢?
答:通过SG函数转换为尼姆博弈。
首先给出一种ICG博弈游戏模型,给定一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿着有向边进行移动,无法移动者判负。
将ICG问题进行转化:任何一个ICG都可以通过把每个局面看作一个顶点,对每个局面和它的子局面连一条有向边来抽象这个“有向图游戏”。
于是我们将ICG问题转化为上述这个游戏,再通过寻找这个游戏的一般解法来处理ICG问题。
首先定义mex(minimal excludant)运算,这是定义于一个集合的运算,表示最小的不属于这个集合的最小非负整数。例如mex{0,1,2,4}=3,mex{2,3,4}=0,mex{}=0.
SG函数(Sprague-Grundy):对于一个给定的有向无环图,定义关于这个图的每个顶点的SG函数如下:
$$ sg(x)=mex {, sg(y) , | , \text{y is the successor of x} , } $$
SG函数的求法:
按上述步骤建成的树如下:
这颗树有什么意义呢?比如说我们将一个顶点放在根节点上,当前这个点的sg值为0,说明当前这个点是必败态。为什么这么说呢?我们将这个点交替进行移动,先手有两种选择,往右移动,显然后手再移动一步就进入必败态;往左移动,后手会选择往右移动,先手同样进入必败态。
如何通过SG函数值来解决之前的有向图问题呢?对于n个棋子,设它们对应的顶点的SG函数值分别为{a1,a2,...an},再设局面{a1,a2,...an}时的Nim游戏的必胜策略是把ai变成k,那么原游戏的一种必胜策略就是把第i枚棋子移动到一个SG值为k的顶点。
简单来说,我们让每个结点都拥有一个SG值(假设这个值为x),那么对于任何一个玩家操作(移动到当前结点的某个后继结点)实际上就是把棋子移动到0~x-1的某个结点上,等价的就是从x个物品中取走一个,最多x个!。
不是是觉得有点不对,单根据mex的定义,可能出现如下情况,移动到比自身SG值大的结点:
其实这种情况是不存在的,博弈问题中先手不会移动到对自己不利的局面的,在这里也就是不会移动到SG值为4的结点。
SG定理:所以我们可以定义有向图游戏的和。设G1,G2,...Gn为n个“有向图”游戏的和(Sum),游戏G的移动规则是:任选一个子游戏Gi并移动上面的棋子。SG定理就是:sg(G)=sg(G1)∧sg(G2)∧....∧sg(Gn)。也就是说,游戏的SG函数值就是它的所有子游戏的SG函数值的异或。
因此,当我们面对n个不同游组成的游戏时,只需要求出每个游戏的SG函数值,把这些SG值都看作Nim的石子堆,然后依照找Nim游戏的必胜策略的方法来找这个游戏的必胜策略。
参考链接:
]]>用集合中的某个元素来代表这个集合,,该元素称之为集合的代表元
每个集合可以理解为一个树,对于集合中的每个元素(如x),都有一个值(如parent[x])指向其在结构上的父节点。如果x为集合的代表元,即根节点,则令parent[x]=x;
对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元。可以沿着parent[x]不断在树形结构中向上移动,直到到达根节点。
判断两个元素是否属于同一集合,只需要看他们的代表元是否相同即可。
包括对所有单个的数据建立一个单独的集合(即根据题目的意思自己建立的最多可能有的集合,为下面的合并查找操作提供操作对象)
#define MAX 10000
struct Node
{
int parent; // 集合index的类别
int data; // 集合index的数据类型
int rank; // 集合index的层次,通常初始化为0
}node[MAX];
// 初始化i集合的函数
void init(int i){
node[i].parent=i; // 初始化的时候,一个集合的parent都是这个集合自己的标号。
// 没有跟它同类的集合,那么这个集合的源头只能是自己了。
node[i].rank=0;
}
int parent[max];
int rank[max];
int data[max];
void init(int i)
{
set[i]=i;
rank[i]=0;
}
/**
*查找集合i(一个元素是一个集合)的源头(递归实现)。
如果集合i的父亲是自己,说明自己就是源头,返回自己的标号;
否则查找集合i的父亲的源头。
**/
int get_Parent(int x)
{
if(node[x].parent==x)
{
return x;
}
// 在进行查找时,顺便压缩路径
node[x].parent = get_Parent(node[x].parent);
return node[x].parent;
}
// 查找集合i(一个元素是一个集合)的源头(递归实现)
int find_set(int i)
{
// 如果集合i的父亲是自己,说明自己就是源头,返回自己的标号
if(set[i]==i)
{
return set[i];
}
// 否则查找集合i的父亲的源头
return find_set(set[i]);
}
// 查找的同时进行集合的优化的函数(减少树的高度)
int unifind(int a){
int root = a;
// 找到根节点
while(root != parent[root] ){
root = parent[root];
}
// compress the path
while( a != root){
int parentOfA = parent[a];
parent[a] = root; // 将当前节点的父节点直接设置为父节点
a = parentOfA;
}
return root;
}
Talk is cheep,show the code.
这里在合并时按照秩进行了路径压缩,将秩较小的树合并到大的上。
void Union(int a,int b)
{
a=get_parent(a);
b=get_parent(b);
if(node[a].rank>node[b].rank)
node[b].parent=a;
else
{
node[a].parent=b;
if(node[a].rank==node[b].rank)
{
node[b].rank++;
}
}
}
void Union(int i,int j)
{
i=Find_Set(i);
j=Find_Set(j);
if(i==j) return ;
if(rank[i]>rank[j])
{
set[j]=i;
}
else
{
if(rank[i]==rank[j])
{
rank[j]++;
}
set[i]=j;
}
}
计算最后有多少元素父元素仍然为自己parent[x]==x,就算出有多少个不相交的集合
// 这里只放结构体的函数了
int count(int i){ // i为有效元素数目
int c=0;
while(i--){
if(node[i].parent==i){
c++;
}
}
return c;
}
ACM典型例题:acm_hdu_1213
笨蛋么,我们要谈人生还早着呢 ——龙与虎
女孩子就是用砂糖,香辛料和某种美好的东西做成的 ——大老师
游荡的孤高灵魂不需要羁绊之地 ——大老师
面包的味道是香甜 ——路人
那天,我听到了种子破土的声音,又细微又坚定 ——子不语
我们一起走,然后各自幸福 ——朋友
曾经发生过的事情不可能忘记,只不过是想不起而已 ——千与千寻
"我们正是那朝圣者,主人;我们总会向前迈进;也许会超越那白雪阻碍的蓝色山脉又或是那大浪汹涌波光粼粼的大海" ——Soup
小时候处处都充满惊喜,现在都去了哪里 ——刺客伍六七
如果你还可以再天真一次,你是否还会做你自己 ——刺客伍六七
一只船孤独的航行在海上,它既不寻求幸福,也不逃避幸福,它只是向前航行,底下是沉静碧蓝的大海,而头顶是金色的太阳。 ——莱蒙托夫
在遭遇人生低谷的时候,不要灰心,至少曾经有人被你的魅力所吸引,曾经是,以后也会是 ——村上春树
竹杖芒鞋轻胜马,谁怕? 一蓑烟雨任平生 ——苏轼《定风波》
一切都会过去 All will pass ——自己
前方还有很长的路要走,我们互相加油吧 一言为定 ——友人
心中一颗语法树 走遍天下都不怕 ——脉脉好友
一个人如果不耕作,就必须写作! ——博客网友
cpp,精准、高效、强大、难搞! ——自己
凡事预则立,不预则废 ——礼记
基础不牢,地动山摇 ——华科老师
]]>如果下方没有加载文字,可能是因为网站的屏蔽,F12去控制台打开即可
]]>摘自:《一份不太简短的LATEX2介绍》或112分钟学会LATEX2 原版作者:Tobias Oetiker link from here
是⼀组⽤来定义计算机程序的语法规则
⽤⼆进制代码表示的计算机能直接识别和执⾏的机器指令的集合
汇编语言(assembly language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
高级语言(High-level programming language)相对于机器语言(machine language)是一种指令集的体系。在这种语言下,其语法和结构更类似汉字或者普通英文,且由于远离对硬件的直接操作,使得一般人更容易学习。高级语言通常按其基本类型、代系、实现方式、应用范围等分类。
过程式编程模式 模块化,结构化
⾯向对象编程模式
函数式编程模式 程序被看做⼀个函数
说明式编程模式 在逻辑推理的基础上 发展⽽来
功能更强,简单易⽤ 作为⼀种教学语⾔,在⾼校计算 机教学中曾经占主导
⼴泛使⽤的⾼级语⾔,适合作为 系统描述语⾔ 简洁 丰富 可移植性强
应⽤较⼴的⾯向对象程序设计语
⾔
相⽐c⽀持⾯向对象抽象,⽀持⾯向对象设计和编程,增加了安全性,引⼊了引⽤概念
特点
⼀种表现能⼒很强的通⽤程序设
计语⾔,是美国国防部指定的唯⼀⼀种⽤于开发军⽤系统的语⾔
我国军⽅也将Ada语⾔作为军内开发标准
绘图是logo语⾔中最主要的功能
称为扩建的语⾔或动态语⾔
显式并⾏语⾔是具有并⾏编译功能的串⾏语⾔
OpenMP是由OpenMP Architecture Review Board牵头提出的,并已被广泛接受,用于共享内存并行系统的多处理器程序设计的一套指导性编译处理方案(Compiler Directive)。
OpenMP支持的编程语言包括C、C++和Fortran;而支持OpenMp的编译器包括Sun Compiler,GNU Compiler和Intel Compiler等。
OpenMp提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的pragma来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。当选择忽略这些pragma,或者编译器不支持OpenMp时,程序又可退化为通常的程序(一般为串行),代码仍然可以正常运作,只是不能利用多线程来加速程序执行。
数据结构的定义:数据元素依据某种逻辑联系组织起来
分类
指由基本的运算及规定的运算序所构成的完整的阶梯步骤
算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。
如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。
特性:
输⼊ 输出 有穷性 确定性 可⾏性
算法实例:
算法优化
数据中⼼的定义 是⼀个物理空间内实现数据集中
处理,存储,传输,交换,管理
的⼀整套复杂的设施。
⼀个数据中⼼的主要⽬的是运⾏
应⽤系统来处理组织的数据。
核⼼计算机机房
其他⽀持空间
传统数据中⼼的困惑
新⼀代数据中⼼简介
在今后的发展中,数据中心也将会成为企业竞争的资产,商业模式也会因此发生改变。随着数据中心应用的广泛化,人工智能、网络安全等也相继出现,更多的用户都被带到了网络和手机的应用中。
随着计算机和数据量的增多,人们也可以通过不断学习积累提升自身的能力,是迈向信息化时代的重要标志。
数据库是⼀个组织内被应⽤程序使⽤的逻辑相⼀致的相关数据的集合
特点:
总结一下,想要利用 mysql-connector-java 与 MySQL 8.X 版本建立连接,有以下四个方面与 MySQL 5.X 版本相比发生了变化。
MySQL 8.0 以上版本驱动包版本 mysql-connector-java-8.X.jar
MySQL 版本和 mysql-connector-java 版本对应关系如下,MySQL官方也是推荐使用 mysql-connector-java-8.X.jar 去连接 MySQL 8.0 的版本
Connector/J version Driver Type JDBC version MySQL Server version Status
5.1 4 3.0, 4.0, 4.1, 4.2 5.6*, 5.7*, 8.0* General availability
8.0 4 4.2 5.6, 5.7, 8.0 General availability. Recommended version
com.mysql.jdbc.Driver 更换为 com.mysql.cj.jdbc.Driver。
MySQL 8.0 以上版本不需要建立 SSL 连接的,需要显式关闭。
MySQL 5.7 之前版本,安全性做的并不够好,比如安装时生成的root空密码账号、存在任何用户都能连接上的 test 库等,导致数据库存在较大的安全隐患。从5.7版本开始MySQL官方对这些问题逐步进行了修复,到了 MySQL 8.0 以上版本已经不需要使用 SSL 进行连接加密了。但是高版本仍然保留了这个接口,所以需要在连接的时候手动写明是否需要进行 SSL 连接,这里我们手动关闭 SSL 连接加密就OK。
useSSL=false
最后还需要设置 CST。也就是设置时区。
serverTimezone=UTC
例子
String url="jdbc:mysql://localhost:3306/document?useSSL=false&serverTimezone=UTC";
Class.forName("com.mysql.cj.jdbc.Driver");
conn=DriverManager.getConnection(url,"root","password");
]]>本文转自 作者:JYRoy
出处:https://www.cnblogs.com/jyroy/p/11276152.html
本站使用「署名 4.0 国际」创作共享协议,转载请在文章明显位置注明作者及出处。
软件是用户与硬件之间的接口
软件是计算机系统中与硬件互相依存的一个部分(非硬件成分)——传统意义,它包括程序、数据及其相关文档的完整集合。
软件(中国大陆及香港用语,台湾称作软体,英文:Software)是一系列按照特定顺序组织的计算机数据和指令的集合。一般来讲软件被划分为系统软件、应用软件和介于这两者之间的中间件。
软件并不只是包括可以在计算机(这里的计算机是指广义的计算机)上运行的电脑程序,与这些电脑程序相关的文档一般也被认为是软件的一部分。
简单的说软件就是程序加文档的集合体。另也泛指社会结构中的管理系统、思想意识形态、思想政治觉悟、法律法规等等。
程序是一组计算机能识别和执行的指令
数据(data)是事实或观察的结果,是对客观事物的逻辑归纳,是用于表示客观事物的未经加工的原始素材
文档是软件开发使用和维护中的必备资料
软件架构四层结构:系统软件、中间件、业务专用、应用程序子系统
目前软件系统的开发越来越庞大复杂,要满足设计目标越来越困难,而应用分而治之的管理是一种行之有效的分法
dos,是磁盘操作系统的缩写,是个人计算机上的一类操作系统。从1981年直到1995年的15年间,磁盘操作系统在IBM PC 兼容机市场中占有举足轻重的地位。而且,若是把部分以DOS为基础的Microsoft Windows版本,如Windows 95、Windows 98和Windows Me等都算进去的话,那么其商业寿命至少可以算到2000年。微软的所有后续版本中,磁盘操作系统仍然被保留着。
MicrosoftWindows操作系统是美国微软公司研发的一套操作系统,它问世于1985年,起初仅仅是Microsoft-DOS模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也当前应用最广泛的操作系统。
Unix是20世纪70年代初出现的一个操作系统,除了作为网络操作系统之外,还可以作为单机操作系统使用。Unix作为一种开发平台和台式操作系统获得了广泛使用,目前主要用于工程应用和科学计算等领域。
Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和Unix的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的Unix工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。
我个人就租了一台腾讯云的服务器,系统为基于Linux的CentOS,用来作为自己的博客搭建平台,感觉性能很不错,最近学生优惠也很便宜
软件危机是指落后的软件生产方式无法满足迅速增长的计算机软件需求,从而导致软件开发与维护过程中出现一系列严重问题的现象。
项目经理: 是项目的总负责人,指导小组在预算内按时按量完成任务
质量保证(QA)工程师 负责保证工程的质量
软件需求分析师
软件开发工程师 用编程语言和开发技术进行软件开发
系统分析师 是抽象模型的建立者
系统架构师 最终确认和评估系统需求,给出开发规范,搭建系统实现的核心架构,明确技术细节,解决主要难点。
测试工程师
软件产品测试经理
网络工程师:对计算机网络系统进行设计、建设、运行及维护
IT基础设施工程师:负责IT设施的规划、设计、实施、管理、沟通、组织
网络安全管理员
安全开发工程师
数据库管理员
系统管理员:网络系统管理员和信息系统管理员
售前工程师:开发人员与销售人员的桥梁
售后技术工程师:满足用户的售后需求,解决用户的问题
系统集成工程师:用户买下软件后,分析用户的业务需求,完成产品的实施。 包括硬件集成,系统初始化,系统配置,高可用性软件安装,应用软件安装,与产品规划人员沟通,掌握产品需求及变更。
1、命令式语言;2、函数式语言;3、逻辑式语言;4、面向对象语言
移动终端是指能够执行与无线接口上的传输有关的所有功能的终端装置。
它包括的范围很广,如果按照工作原理划分,可以分为模拟移动终端和数字移动终端,如模拟蜂窝手机和数字蜂窝手机;如果按应用领域区分,可以分为公众网移动终端和专业网移动终端,如GSM手机、CDMA手机、寻呼机等属于公众网移动终端。
但是,尽管大量通信终端厂家和芯片厂商涌入各种不同的移动终端领域,最为重要的移动终端主要还是手机这种公众网移动终端,即手机终端。
计算机网络系统就是利用通信设备和线路将地理位置不同、功能独立的多个计算机系统互联起来,以功能完善的网络软件实现网络中资源共享和信息传递的系统。
通过计算机的互联,实现计算机之间的通信,从而实现计算机系统之间的信息、软件和设备资源的共享以及协同工作等功能,其本质特征在于提供计算机之间的各类资源的高度共享,实现便捷地交流信息和交换思想。
构成计算机网络系统的要素 :
计算机系统:工作站(终端设备,或称客户机,通常是PC机)、网络服务器(通常都是高性能计算机)。
网络通信设备(网络交换设备、互连设备和传输设备):包括网卡、网线、集线器(HUB)、交换机、路由器等。
网络外部设备:如高性能打印机、大容量硬盘等
网络软件:包括网络操作系统,如Unix、NetWare、Windows NT等;客户连接软件(包括基于DOS、Windows、Unix操作系统的等);网络管理软件等。
按信息类型分
按调制方式分
按传输信号特征分
网络传输媒体 传输媒体(Transmission Medium) 也称传输介质或传输媒介,它就是数据传输系统中在发 送器和接收器之间的物理通路。它可分为两大类,即导向传输媒体和非导向传输媒体。在导向传输媒体中,电磁波被导向沿着固体媒体(铜线或光纤)传播,而非导向传输媒体就是指自由空间,在非导向传输媒体中电磁波的传输常称为无线传播。网络传输媒介的质量的好坏会影响数据传输的质量,包括速率、数据丢包等。
中继器 中继器(RP repeater)是工作在物理层上的连接设备。适用于完全相同的两类网络的互连,主要功能是通过对数据信号的重新发送或者转发,来扩大网络传输的距离。 中继器是对信号进行再生和还原的网络设备:OSI模型的物理层设备。
集线器 集线器的英文称为“Hub”。“Hub”是“中心”的意思,集线器的主要功能是对接收到的信号进行再生整形放大,以扩大网络的传输距离,同时把所有节点集中在以它为中心的节点上。它工作于OSI(开放系统互联参考模型)参考模型第一层,即“物理层”。
集线器与网卡、网线等传输介质一样,属于局域网中的基础设备,采用CSMA/CD(即带冲突检测的载波监听多路访问技术)介质访问控制机制。集线器每个接口简单的收发比特,收到1就转发1,收到0就转发0,不进行碰撞检测。
交换机 交换机(Switch)意为“开关”是一种用于电(光)信号转发的网络设备。它可以为接入交换机的任意两个网络节点提供独享的电信号通路。最常见的交换机是以太网交换机。其他常见的还有电话语音交换机、光纤交换机等。
路由器 路由器是连接两个或多个网络的硬件设备,在网络间起网关的作用,是读取每一个数据包中的地址然后决定如何传送的专用智能性的网络设备。 路由器能够理解不同的协议,例如某个局域网使用的以太网协议,因特网使用的TCP/IP协议。这样,路由器可以分析各种不同类型网络传来的数据包的目的地址,把非TCP/IP网络的地址转换成TCP/IP地址,或者反之;再根据选定的路由算法把各数据包按最佳路线传送到指定位置。所以路由器可以把非TCP/ IP网络连接到因特网上。
总线型拓扑结构
星型拓扑结构
环形拓扑结构
树形拓扑结构
云计算本质上是一种共享服务。
云计算(cloud computing)是分布式计算的一种,指的是通过网络“云”将巨大的数据计算处理程序分解成无数个小程序,然后,通过多部服务器组成的系统进行处理和分析这些小程序得到结果并返回给用户。
云计算早期,简单地说,就是简单的分布式计算,解决任务分发,并进行计算结果的合并。因而,云计算又称为网格计算。通过这项技术,可以在很短的时间内(几秒种)完成对数以万计的数据的处理,从而达到强大的网络服务。
物联网(The Internet of Things)的概念是在1999年提出的,它的定义很简单:把所有物品通过射频识别等信息传感设备与互联网连接起来,实现智能化识别和管理。物联网通过智能感知、识别技术与普适计算、泛在网络的融合应用,被称为继计算机、互联网之后世界信息产业发展的第三次浪潮。
物联网被视为互联网的应用拓展,应用创新是物联网发展的核心,以用户体验为核心的创新2.0是物联网发展的灵魂。
全面感知,即利用RFID,传感器,二维码等随时随地获取物体的信息;
可靠传递,通过各种电信网络与互联网的融合,将物体的信息实时准确地传递出去;
智能处理,利用云计算,模糊识别等各种智能计算技术,对海量的数据和信息进行分析和处理,对物体实施智能化的控制。
目前,物联网还没有一个被广泛认同的体系结构,但是,我们可以根据物联网对信息感知、传输、处理的过程将其划分为三层结构,即感知层、网络层和应用层,具体体系结构下图所示。
图灵机是计算机的数学模型
它有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色。有一个机器头在纸带上移来移去。机器头有一组内部状态,还有一些固定的程序。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后结合自己的内部状态查找程序表,根据程序输出信息到纸带方格上,并转换自己的内部状态,然后进行移动。
与数字逻辑的联系:图灵机存在状态的转换,用状态的变化驱动
状态控制器,三个状态:q1:起始状态,q2:当前状态,q3:终止状态
可以通过进行图灵测试来测试一个计算机是否具有智能
图灵测试方法:一个人和计算机分隔开并进行交流,如果人无法直接通过计算机回复的内容判断与自己交流的是计算机,则可以认为该计算机具有人工智能
理论,抽象,设计是计算机设计中的三个重要的组成部分
模型的拟合有好坏性和可解释性,这两方面都很重要
主键盘指法,最好能盲打,下面是盲打的键位推荐
计算机科学(Computer Science, CS)是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。
它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究。计算机科学包含很多分支领域;
其中一些,比如计算机图形学强调特定结果的计算,而另外一些,比如计算复杂性理论是学习计算问题的性质。
还有一些领域专注于挑战怎样实现计算。比如程序设计语言理论学习描述计算的方法,而程序设计是应用特定的程序设计语言解决特定的计算问题,人机交互则是专注于挑战怎样使计算机和计算变得有用、可用,以及随时随地为人所用。
现代计算机科学( Computer Science)包含理论计算机科学和应用计算机科学两大分支。
目前的社会,计算机网络(internet)无处不在,万维网是目前最大的互联网,又许多的服务器或者说计算机连接组成
目前许多大型互联网公司都有自己的服务器阵列,目前服务器如何省电并节约能源已经成为各大服务器制造商与应用商研究的重点之一。
计算机的结构
主要分为五个部分:控制器,运算器,存储器,输入设备,输出设备。
冯·诺伊曼结构图示:
个人计算机的组成:存储,CPU,输入输出设备等
其中:cpu高速运转,硬盘很慢,内存读取快
计算机体系结构
WINDOWS 有根目录,子目录,硬盘有分区
LINUX没有分区
常用CMD命令:
cd 打开文件夹
cd\ 返回根目录
dir 展示文件夹中的内容
type 显示文本文件内容
help 显示帮助信息,有关某个命令的详细信息,键入 HELP 命令名,
ASSOC 显示或修改文件扩展名关联。
ATTRIB 显示或更改文件属性。
BREAK 设置或清除扩展式 CTRL+C 检查。
BCDEDIT 设置启动数据库中的属性以控制启动加载。
CACLS 显示或修改文件的访问控制列表(ACL)。
CALL 从另一个批处理程序调用这一个。
CD 显示当前目录的名称或将其更改。
CHCP 显示或设置活动代码页数。
CHDIR 显示当前目录的名称或将其更改。
CHKDSK 检查磁盘并显示状态报告。
CHKNTFS 显示或修改启动时间磁盘检查。
CLS 清除屏幕。
CMD 打开另一个 Windows 命令解释程序窗口。
COLOR 设置默认控制台前景和背景颜色。
COMP 比较两个或两套文件的内容。
COMPACT 显示或更改 NTFS 分区上文件的压缩。
CONVERT 将 FAT 卷转换成 NTFS。你不能转换
当前驱动器。
COPY 将至少一个文件复制到另一个位置。
DATE 显示或设置日期。
DEL 删除至少一个文件。
DIR 显示一个目录中的文件和子目录。
DISKPART 显示或配置磁盘分区属性。
DOSKEY 编辑命令行、撤回 Windows 命令并
创建宏。
DRIVERQUERY 显示当前设备驱动程序状态和属性。
ECHO 显示消息,或将命令回显打开或关闭。
ENDLOCAL 结束批文件中环境更改的本地化。
ERASE 删除一个或多个文件。
EXIT 退出 CMD.EXE 程序(命令解释程序)。
FC 比较两个文件或两个文件集并显示
它们之间的不同。
FIND 在一个或多个文件中搜索一个文本字符串。
FINDSTR 在多个文件中搜索字符串。
FOR 为一组文件中的每个文件运行一个指定的命令。
FORMAT 格式化磁盘,以便用于 Windows。
FSUTIL 显示或配置文件系统属性。
FTYPE 显示或修改在文件扩展名关联中使用的文件
类型。
GOTO 将 Windows 命令解释程序定向到批处理程序
中某个带标签的行。
GPRESULT 显示计算机或用户的组策略信息。
GRAFTABL 使 Windows 在图形模式下显示扩展
字符集。
HELP 提供 Windows 命令的帮助信息。
ICACLS 显示、修改、备份或还原文件和
目录的 ACL。
IF 在批处理程序中执行有条件的处理操作。
LABEL 创建、更改或删除磁盘的卷标。
MD 创建一个目录。
MKDIR 创建一个目录。
MKLINK 创建符号链接和硬链接
MODE 配置系统设备。
MORE 逐屏显示输出。
MOVE 将一个或多个文件从一个目录移动到另一个
目录。
OPENFILES 显示远程用户为了文件共享而打开的文件。
PATH 为可执行文件显示或设置搜索路径。
PAUSE 暂停批处理文件的处理并显示消息。
POPD 还原通过 PUSHD 保存的当前目录的上一个
值。
PRINT 打印一个文本文件。
PROMPT 更改 Windows 命令提示。
PUSHD 保存当前目录,然后对其进行更改。
RD 删除目录。
RECOVER 从损坏的或有缺陷的磁盘中恢复可读信息。
REM 记录批处理文件或 CONFIG.SYS 中的注释(批注)。
REN 重命名文件。
RENAME 重命名文件。
REPLACE 替换文件。
RMDIR 删除目录。
ROBOCOPY 复制文件和目录树的高级实用工具
SET 显示、设置或删除 Windows 环境变量。
SETLOCAL 开始本地化批处理文件中的环境更改。
SC 显示或配置服务(后台进程)。
SCHTASKS 安排在一台计算机上运行命令和程序。
SHIFT 调整批处理文件中可替换参数的位置。
SHUTDOWN 允许通过本地或远程方式正确关闭计算机。
SORT 对输入排序。
START 启动单独的窗口以运行指定的程序或命令。
SUBST 将路径与驱动器号关联。
SYSTEMINFO 显示计算机的特定属性和配置。
TASKLIST 显示包括服务在内的所有当前运行的任务。
TASKKILL 中止或停止正在运行的进程或应用程序。
TIME 显示或设置系统时间。
TITLE 设置 CMD.EXE 会话的窗口标题。
TREE 以图形方式显示驱动程序或路径的目录
结构。
TYPE 显示文本文件的内容。
VER 显示 Windows 的版本。
VERIFY 告诉 Windows 是否进行验证,以确保文件
正确写入磁盘。
VOL 显示磁盘卷标和序列号。
XCOPY 复制文件和目录树。
WMIC 在交互式命令 shell 中显示 WMI 信息。
记——11月14日夜在南湖学习的计算机科学导论
这周学习了许多内容,包括且不限于计算机专业的方向,前景还有信息论等以前从未听说过的理论知识,感觉收获很多。
上个星期 宋华珠老师 请了一位留学生来给我们进行讲课仍历历在目。
我通过查阅资料,知道了计算机科学这个专业主要是做什么,未来的发展,下面是我找到的部分定义。
计算机专业是指计算机硬件与软件相结合、面向系统、更偏向应用的宽口径专业。 理论性强,实践性强,通过基础教学与专业训练,培养基础知识扎实、知识面宽、工程实践能力强,具有开拓创新意识,在计算机科学与技术领域从事 科学研究 、教育、开发和应用的高级人才。
这星期我们主要学习的是信息论,下面是我根据我所看书与和学长讨论了解到的知识对信息的内容进行的总结:
信息是认知主体对物质运动的本质特征、运动方式、运动状态以及运动的有序性的反映和揭示,是事物之间相互联系、相互作用的状态的描述。通俗地讲,信息泛指包含于消息、情报、指令、数据、图像、信号等形式之中的新的知识和内容。
特征:普遍性、动态性、依附性、相对性、可传递性、共享性、可加工性。
分类:
(1)按照产生信息的物体的性质,可以将信息分为自然信息(声、光、热、电等),生物信息(生物为繁衍生存而表现出来的各种形态和行为,如遗传信息、生物体内的交流信息、动物种群内的交流信息等),机器信息和社会信息等。
(2)按照人类活动领域,可以将信息分为科技信息、经济信息、政治信息、军事信息、文化信息等。
(3)按照信息所依附的载体,可以将信息分为文献信息、口头信息、电子信息、生物信息等。
(4)按照携带信息的信号性质,可以将信息分为连续信息、半连续信息和离散信息。
(5)按照信息所起的作用,可以将信息分为无用信息、有用信息和干扰信息。
位模式是一个序列,有时也称为位串,它是0和1的组合。通常长度为8的位模式被称为1个字节(byte),一个字由若干字节组成。
(1)文本的表示:文本格式的信息常用编码的方法来表示,文本中每一个不同的符号(如字母表中的字母或标点符号)被指定为一个唯一的位模式。这样,文本就被表示成一个长的位串,其中相应的位模式代表了原文中相应的符号。英文一般采用ASCII(America standard code for information interchange)编码、Unicode编码等,而中文则大多采用与前者兼容的GB 2312-1980编码。
(2)数值的表示:以二进制形式存储数值。对于二进制而言,数据分为无符号数和有符号数(约定最高位为符号位,“1”表示负数,“0”表示正数)两类。又根据编码的不同,将其分为原码、补码、反码(正数的原码、补码、反码相同,负数的反码为其原码数值部分各位取反,补码为其原码除符号位以外各位变反加一)
(3)音频的表示:要在计算机上表示音频信息,必须对声波进行数字化处理,即把模拟的声波转换成离散的数字信号。数字化处理的过程包括采样、量化和编码三个步骤。
(4)图像的表示:计算机处理图像有两种方式:矢量图和位图。在位图中,一个图像被看作是点的集合,每个点叫做像素。位图所需的存储容量较大。此外,缩放和旋转位图容易造成图像失真。矢量图是计算机图形学中用点、直线或者多边形等基于数学方程的几何图元表示的图像,它是根据客观事物而形成的。
(5)视频的表示:视频即运动图像,是指内容随时间变化的一组动态图像,它是由一幅接一幅的静止的图像组成的,也就是说,它由一系列帧组成。如果想保存一部25帧/秒、时长为90分钟、分辨率为1024*768、24位真彩色的电影,则需要大约296GB的存储空间。
(1)接收:信息接收包括信息感知、信息测量、信息识别、信息获取以及信息输入等。
(2)存储:信息存储就是把计算机接收到的信息或计算机处理的中间信息通过存储设备进行缓冲、保存、备份等处理。
(3)转化:信息转化就是将信息根据人们的特定需要进行分类、计算、分析、检索、管理和综合等处理。
(4)传输:信息传输时通过计算机内部的指令或计算机之间的网络把信息从一个位置传送到另外一个位置。
(5)发布:信息发布就是把信息通过各种表示形式展示出来。
不是真实的机器,是一种理论模型。可以视为现代数学计算机的数学模型。
图灵机有一个可以向左右两端无限伸展的纸带。有一个能在纸带上左右移动的读写头HEAD。还有一个控制器,存有控制规则和一个状态寄存器。
信息,指音讯、消息、通讯系统传输和处理的对象,泛指人类社会传播的一切内容。
人通过获得、识别自然界和社会的不同信息来区别不同事物,得以认识和改造世界。
在一切通讯和控制系统中,信息是一种普遍联系的形式。
1948年,数学家香农在题为“通讯的数学理论”的论文中指出:“信息是用来消除随机不定性的东西”。创建一切宇宙万物的最基本单位是信息。
信息一般通过信息熵来度量。
离散型随机变量X~p(Xi)的信息熵是从平均意义上对信息不确定性的度量,也称为平均自信息量,定义为:
其中,随机变量X由n个事件Xi构成,事件Xi出现的概率为p(Xi )。
文本:文本格式的信息常用编码的方法来表示,文本中每一个不同的符号(如字母表中的字母或标点符号)被指定为一个唯一的位模式。这样,文本就被表示成一个长的位串,其中相应的位模式代表了原文中相应的符号。英文一般采用ASCII(America standard code for information interchange)编码、Unicode编码等,而中文则大多采用与前者兼容的GB 2312-1980编码。
数值:以二进制形式存储数值。对于二进制而言,数据分为无符号数和有符号数(约定最高位为符号位,“1”表示负数,“0”表示正数)两类。又根据编码的不同,将其分为原码、补码、反码(正数的原码、补码、反码相同,负数的反码为其原码数值部分各位取反,补码为其原码除符号位以外各位变反加一)
音频的表示:要在计算机上表示音频信息,必须对声波进行数字化处理,即把模拟的声波转换成离散的数字信号。数字化处理的过程包括采样、量化和编码三个步骤。
图像:计算机处理图像有两种方式:矢量图和位图。在位图中,一个图像被看作是点的集合,每个点叫做像素。位图所需的存储容量较大。此外,缩放和旋转位图容易造成图像失真。矢量图是计算机图形学中用点、直线或者多边形等基于数学方程的几何图元表示的图像,它是根据客观事物而形成的。
视频:视频即运动图像,是指内容随时间变化的一组动态图像,它是由一幅接一幅的静止的图像组成的,也就是说,它由一系列帧组成。如果想保存一部25帧/秒、时长为90分钟、分辨率为1024*768、24位真彩色的电影,则需要大约296GB的存储空间。
四种,分别为:
文本
数值
图像
视频
以个人博客中的博文为例进行说明:
(1)接收:信息接收包括信息感知、信息测量、信息识别、信息获取以及信息输入等,用户通过标准输入设备如键盘鼠标等向计算机内输入信息。
(2)存储:信息存储就是把计算机接收到的信息或计算机处理的中间信息通过存储设备进行缓冲、保存、备份等处理。计算机保存用户的输入。
(3)转化:信息转化就是将信息根据人们的特定需要进行分类、计算、分析、检索、管理和综合等处理。将保存的信息转换为H5格式。
(4)传输:信息传输时通过计算机内部的指令或计算机之间的网络把信息从一个位置传送到另外一个位置。将博文上传服务器
(5)发布:信息发布就是把信息通过各种表示形式展示出来。在服务器发布。
图灵机模型是由英国数学家图灵提出的,是计算机的数学模型。
所谓的图灵机就是指一个抽象的机器,它有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色。有一个机器头在纸带上移来移去。机器头有一组内部状态,还有一些固定的程序。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后结合自己的内部状态查找程序表,根据程序输出信息到纸带方格上,并转换自己的内部状态,然后进行移动。
计算机科学(Computer Science, CS)是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。
它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究。计算机科学包含很多分支领域;其中一些,比如计算机图形学强调特定结果的计算,而另外一些,比如计算复杂性理论是学习计算问题的性质。还有一些领域专注于挑战怎样实现计算。比如程序设计语言理论学习描述计算的方法,而程序设计是应用特定的程序设计语言解决特定的计算问题,人机交互则是专注于挑战怎样使计算机和计算变得有用、可用,以及随时随地为人所用。
现代计算机科学( Computer Science)包含理论计算机科学和应用计算机科学两大分支。
计算机科学的基本问题是:什么能(有效地)自动进行。
个人感觉,计算机专业所学习的程序的编写,主要就是为了处理各种各样的的数据,通过对信息的收集与处理,产生能够作用于实际生活并产生价值的信息。
Markdown语法的完整介绍在这里:http://daringfireball.net/projects/markdown/syntax。下面整理的这些为了方便写博客时参考。
以一个或多个空行来隔开段落;以两个或多个空格来段内换行。
This is an H1
=============
This is an H2
-------------
# This is an H1
## This is an H2
###### This is an H6
在每一行前面写一个>
:
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
>
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
> id sem consectetuer libero luctus adipiscing.
效果:
This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse id sem consectetuer libero luctus adipiscing.
或者在每一段前面写一个>
:
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
id sem consectetuer libero luctus adipiscing.
> This is the first level of quoting.
>
> > This is nested blockquote.
>
> Back to the first level.
效果:
This is the first level of quoting.
This is nested blockquote.
Back to the first level.
列表项占一行,以*、+、-开头即可:
* Red
* Green
* Blue
效果:
有序列表只需要将上述标记符换成数字加句点。而且顺序由书写顺序决定,与数字无关,但数字需要从1开始。例如:
1\. Bird
3. McHale
2. Parish
效果:
每一个列表项可以多行:
* Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
viverra nec, fringilla in, laoreet vitae, risus.
* Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
Suspendisse id sem consectetuer libero luctus adipiscing.
效果:
每一行前面缩进四个或以上个空格,就认为是开始了一段代码块。代码块内原样输出。
This is a normal paragraph:
This is a code block.
效果:
This is a normal paragraph:
This is a code block.
三个或更多个*
、-
(它们之间可以有空格)会产生横线:
* * *
效果:
内嵌链接:
I get 10 times more traffic from [Google](http://google.com/ "Google")
than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or
[MSN](http://search.msn.com/ "MSN Search").
或参考文献式链接(缺省的链接标记认为与文本一致):
I get 10 times more traffic from [Google] [1] than from
[Yahoo] [2] or [MSN] [3].
[1]: http://google.com/ "Google"
[2]: http://search.yahoo.com/ "Yahoo Search"
[3]: http://search.msn.com/ "MSN Search"
I get 10 times more traffic from [Google][] than from
[Yahoo][] or [MSN][].
[google]: http://google.com/ "Google"
[yahoo]: http://search.yahoo.com/ "Yahoo Search"
[msn]: http://search.msn.com/ "MSN Search"
效果:
I get 10 times more traffic from Google than from Yahoo or MSN.
如果直接以链接地址作为链接文本,可以用如下快捷写法:
<http://www.shengbin.me> 效果:
单个*
或_
产生斜体,两个(**
、__
)则产生粗体。例如:
*like* _this_
**like** **this**
效果:
like this
like this
code: `echo hello`
效果:
code: echo hello
图片与链接类似,只需在文本前面加上感叹号!
即可。图片位置和大小无法通过Markdown来指定。
以下特殊字符需要用\
转义得到。
\ backslash
` backtick
* asterisk
_ underscore
{} curly braces
[] square brackets
() parentheses
# hash mark
+ plus sign
- minus sign (hyphen)
. dot
! exclamation mark
]]>