操作系统内存最全解析!!!(内附超全思维导图)-程序员宅基地

技术标签: 程序员  stm32  嵌入式硬件  单片机  

使用基址寄存器和变址寄存器是给每个进程提供私有地址空间的一种非常好的方法,因为每个内存地址在送到内存之前,都会先加上基址寄存器的内容。在很多实际系统中,对基址寄存器和变址寄存器都会以一定的方式加以保护,使得只有操作系统可以修改它们。在 CDC 6600 中就提供了对这些寄存器的保护,但在 Intel 8088 中则没有,甚至没有变址寄存器。但是,Intel 8088 提供了许多基址寄存器,使程序的代码和数据可以被独立的重定位,但是对于超出范围的内存引用没有提供保护。

所以你可以知道使用基址寄存器和变址寄存器的缺点,在每次访问内存时,都会进行 ADD 和 CMP 运算。CMP 指令可以执行的很快,但是加法就会相对慢一些,除非使用特殊的加法电路,否则加法因进位传播时间而变慢。

交换技术

如果计算机的物理内存足够大来容纳所有的进程,那么之前提及的方案或多或少是可行的。但是实际上,所有进程需要的 RAM 总容量要远远高于内存的容量。在 Windows、OS X、或者 Linux 系统中,在计算机完成启动(Boot)后,大约有 50 - 100 个进程随之启动。例如,当一个 Windows 应用程序被安装后,它通常会发出命令,以便在后续系统启动时,将启动一个进程,这个进程除了检查应用程序的更新外不做任何操作。一个简单的应用程序可能会占用 5 - 10MB 的内存。其他后台进程会检查电子邮件、网络连接以及许多其他诸如此类的任务。这一切都会发生在第一个用户启动之前。如今,像是 Photoshop 这样的重要用户应用程序仅仅需要 500 MB 来启动,但是一旦它们开始处理数据就需要许多 GB 来处理。从结果上来看,将所有进程始终保持在内存中需要大量内存,如果内存不足,则无法完成。

所以针对上面内存不足的问题,提出了两种处理方式:最简单的一种方式就是交换(swapping)技术,即把一个进程完整的调入内存,然后再内存中运行一段时间,再把它放回磁盘。空闲进程会存储在磁盘中,所以这些进程在没有运行时不会占用太多内存。另外一种策略叫做虚拟内存(virtual memory),虚拟内存技术能够允许应用程序部分的运行在内存中。下面我们首先先探讨一下交换

交换过程

下面是一个交换过程

刚开始的时候,只有进程 A 在内存中,然后从创建进程 B 和进程 C 或者从磁盘中把它们换入内存,然后在图 d 中,A 被换出内存到磁盘中,最后 A 重新进来。因为图 g 中的进程 A 现在到了不同的位置,所以在装载过程中需要被重新定位,或者在交换程序时通过软件来执行;或者在程序执行期间通过硬件来重定位。基址寄存器和变址寄存器就适用于这种情况。

交换在内存创建了多个 空闲区(hole),内存会把所有的空闲区尽可能向下移动合并成为一个大的空闲区。这项技术称为内存紧缩(memory compaction)。但是这项技术通常不会使用,因为这项技术回消耗很多 CPU 时间。例如,在一个 16GB 内存的机器上每 8ns 复制 8 字节,它紧缩全部的内存大约要花费 16s。

有一个值得注意的问题是,当进程被创建或者换入内存时应该为它分配多大的内存。如果进程被创建后它的大小是固定的并且不再改变,那么分配策略就比较简单:操作系统会准确的按其需要的大小进行分配。

但是如果进程的 data segment 能够自动增长,例如,通过动态分配堆中的内存,肯定会出现问题。这里还是再提一下什么是 data segment 吧。从逻辑层面操作系统把数据分成不同的段(不同的区域)来存储:

  • 代码段(codesegment/textsegment):

又称文本段,用来存放指令,运行代码的一块内存空间

此空间大小在代码运行前就已经确定

内存空间一般属于只读,某些架构的代码也允许可写

在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

  • 数据段(datasegment):

可读可写

存储初始化的全局变量和初始化的 static 变量

数据段中数据的生存期是随程序持续性(随进程持续性)随进程持续性:进程创建就存在,进程死亡就消失

  • bss段(bsssegment):

可读可写

存储未初始化的全局变量和未初始化的 static 变量

bss 段中数据的生存期随进程持续性

bss 段中的数据一般默认为0

  • rodata段:

只读数据比如 printf 语句中的格式字符串和开关语句的跳转表。也就是常量区。例如,全局作用域中的 const int ival = 10,ival 存放在 .rodata 段;再如,函数局部作用域中的 printf(“Hello world %d\n”, c); 语句中的格式字符串 “Hello world %d\n”,也存放在 .rodata 段。

  • 栈(stack):

可读可写

存储的是函数或代码中的局部变量(非 static 变量)

栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束,就自动回收空间

  • 堆(heap):

可读可写

存储的是程序运行期间动态分配的 malloc/realloc 的空间

堆的生存期随进程持续性,从 malloc/realloc 到 free 一直存在

下面是我们用 Borland C++ 编译过后的结果

_TEXT segment dword public use32 ‘CODE’

_TEXT ends

_DATA segment dword public use32 ‘DATA’

_DATA ends

_BSS segment dword public use32 ‘BSS’

_BSS ends

段定义( segment ) 是用来区分或者划分范围区域的意思。汇编语言的 segment 伪指令表示段定义的起始,ends 伪指令表示段定义的结束。段定义是一段连续的内存空间

所以内存针对自动增长的区域,会有三种处理方式

  • 如果一个进程与空闲区相邻,那么可把该空闲区分配给进程以供其增大。

  • 如果进程相邻的是另一个进程,就会有两种处理方式:要么把需要增长的进程移动到一个内存中空闲区足够大的区域,要么把一个或多个进程交换出去,已变成生成一个大的空闲区。

  • 如果一个进程在内存中不能增长,而且磁盘上的交换区也满了,那么这个进程只有挂起一些空闲空间(或者可以结束该进程)

上面只针对单个或者一小部分需要增长的进程采用的方式,如果大部分进程都要在运行时增长,为了减少因内存区域不够而引起的进程交换和移动所产生的开销,一种可用的方法是,在换入或移动进程时为它分配一些额外的内存。然而,当进程被换出到磁盘上时,应该只交换实际上使用的内存,将额外的内存交换也是一种浪费,下面是一种为两个进程分配了增长空间的内存配置。

如果进程有两个可增长的段,例如,供变量动态分配和释放的作为堆(全局变量)使用的一个数据段(data segment),以及存放局部变量与返回地址的一个堆栈段(stack segment),就如图 b 所示。在图中可以看到所示进程的堆栈段在进程所占内存的顶端向下增长,紧接着在程序段后的数据段向上增长。当增长预留的内存区域不够了,处理方式就如上面的流程图(data segment 自动增长的三种处理方式)一样了。

空闲内存管理

在进行内存动态分配时,操作系统必须对其进行管理。大致上说,有两种监控内存使用的方式

  • 位图(bitmap)

  • 空闲列表(free lists)

下面我们就来探讨一下这两种使用方式

使用位图的存储管理

使用位图方法时,内存可能被划分为小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0 表示空闲, 1 表示占用(或者相反)。一块内存区域和其对应的位图如下

图 a 表示一段有 5 个进程和 3 个空闲区的内存,刻度为内存分配单元,阴影区表示空闲(在位图中用 0 表示);图 b 表示对应的位图;图 c 表示用链表表示同样的信息

分配单元的大小是一个重要的设计因素,分配单位越小,位图越大。然而,即使只有 4 字节的分配单元,32 位的内存也仅仅只需要位图中的 1 位。32n 位的内存需要 n 位的位图,所以1 个位图只占用了 1/32 的内存。如果选择更大的内存单元,位图应该要更小。如果进程的大小不是分配单元的整数倍,那么在最后一个分配单元中会有大量的内存被浪费。

位图提供了一种简单的方法在固定大小的内存中跟踪内存的使用情况,因为位图的大小取决于内存和分配单元的大小。这种方法有一个问题是,当决定为把具有 k 个分配单元的进程放入内存时,内容管理器(memory manager) 必须搜索位图,在位图中找出能够运行 k 个连续 0 位的串。在位图中找出制定长度的连续 0 串是一个很耗时的操作,这是位图的缺点。(可以简单理解为在杂乱无章的数组中,找出具有一大长串空闲的数组单元)

使用链表进行管理

另一种记录内存使用情况的方法是,维护一个记录已分配内存段和空闲内存段的链表,段会包含进程或者是两个进程的空闲区域。可用上面的图 c 来表示内存的使用情况。链表中的每一项都可以代表一个 空闲区(H) 或者是进程§的起始标志,长度和下一个链表项的位置。

在这个例子中,段链表(segment list)是按照地址排序的。这种方式的优点是,当进程终止或被交换时,更新列表很简单。一个终止进程通常有两个邻居(除了内存的顶部和底部外)。相邻的可能是进程也可能是空闲区,它们有四种组合方式。

当按照地址顺序在链表中存放进程和空闲区时,有几种算法可以为创建的进程(或者从磁盘中换入的进程)分配内存。我们先假设内存管理器知道应该分配多少内存,最简单的算法是使用 首次适配(first fit)。内存管理器会沿着段列表进行扫描,直到找个一个足够大的空闲区为止。除非空闲区大小和要分配的空间大小一样,否则将空闲区分为两部分,一部分供进程使用;一部分生成新的空闲区。首次适配算法是一种速度很快的算法,因为它会尽可能的搜索链表。

首次适配的一个小的变体是 下次适配(next fit)。它和首次匹配的工作方式相同,只有一个不同之处那就是下次适配在每次找到合适的空闲区时就会记录当时的位置,以便下次寻找空闲区时从上次结束的地方开始搜索,而不是像首次匹配算法那样每次都会从头开始搜索。Bays(1997) 证明了下次算法的性能略低于首次匹配算法。

另外一个著名的并且广泛使用的算法是 最佳适配(best fit)。最佳适配会从头到尾寻找整个链表,找出能够容纳进程的最小空闲区。最佳适配算法会试图找出最接近实际需要的空闲区,以最好的匹配请求和可用空闲区,而不是先一次拆分一个以后可能会用到的大的空闲区。比如现在我们需要一个大小为 2 的块,那么首次匹配算法会把这个块分配在位置 5 的空闲区,而最佳适配算法会把该块分配在位置为 18 的空闲区,如下

那么最佳适配算法的性能如何呢?最佳适配会遍历整个链表,所以最佳适配算法的性能要比首次匹配算法差。但是令人想不到的是,最佳适配算法要比首次匹配和下次匹配算法浪费更多的内存,因为它会产生大量无用的小缓冲区,首次匹配算法生成的空闲区会更大一些。

最佳适配的空闲区会分裂出很多非常小的缓冲区,为了避免这一问题,可以考虑使用 最差适配(worst fit) 算法。即总是分配最大的内存区域(所以你现在明白为什么最佳适配算法会分裂出很多小缓冲区了吧),使新分配的空闲区比较大从而可以继续使用。仿真程序表明最差适配算法也不是一个好主意。

如果为进程和空闲区维护各自独立的链表,那么这四个算法的速度都能得到提高。这样,这四种算法的目标都是为了检查空闲区而不是进程。但这种分配速度的提高的一个不可避免的代价是增加复杂度和减慢内存释放速度,因为必须将一个回收的段从进程链表中删除并插入空闲链表区。

如果进程和空闲区使用不同的链表,那么可以按照大小对空闲区链表排序,以便提高最佳适配算法的速度。在使用最佳适配算法搜索由小到大排列的空闲区链表时,只要找到一个合适的空闲区,则这个空闲区就是能容纳这个作业的最小空闲区,因此是最佳匹配。因为空闲区链表以单链表形式组织,所以不需要进一步搜索。空闲区链表按大小排序时,首次适配算法与最佳适配算法一样快,而下次适配算法在这里毫无意义。

另一种分配算法是 快速适配(quick fit) 算法,它为那些常用大小的空闲区维护单独的链表。例如,有一个 n 项的表,该表的第一项是指向大小为 4 KB 的空闲区链表表头指针,第二项是指向大小为 8 KB 的空闲区链表表头指针,第三项是指向大小为 12 KB 的空闲区链表表头指针,以此类推。比如 21 KB 这样的空闲区既可以放在 20 KB 的链表中,也可以放在一个专门存放大小比较特别的空闲区链表中。

快速匹配算法寻找一个指定代销的空闲区也是十分快速的,但它和所有将空闲区按大小排序的方案一样,都有一个共同的缺点,即在一个进程终止或被换出时,寻找它的相邻块并查看是否可以合并的过程都是非常耗时的。如果不进行合并,内存将会很快分裂出大量进程无法利用的小空闲区。

虚拟内存


尽管基址寄存器和变址寄存器用来创建地址空间的抽象,但是这有一个其他的问题需要解决:管理软件的不断增大(managing bloatware)。虽然内存的大小增长迅速,但是软件的大小增长的要比内存还要快。在 1980 年的时候,许多大学用一台 4 MB 的 VAX 计算机运行分时操作系统,供十几个用户同时运行。现在微软公司推荐的 64 位 Windows 8 系统至少需要 2 GB 内存,而许多多媒体的潮流则进一步推动了对内存的需求。

这一发展的结果是,需要运行的程序往往大到内存无法容纳,而且必然需要系统能够支持多个程序同时运行,即使内存可以满足其中单独一个程序的需求,但是从总体上来看内存仍然满足不了日益增长的软件的需求(感觉和xxx和xxx 的矛盾很相似)。而交换技术并不是一个很有效的方案,在一些中小应用程序尚可使用交换,如果应用程序过大,难道还要每次交换几 GB 的内存?这显然是不合适的,一个典型的 SATA 磁盘的峰值传输速度高达几百兆/秒,这意味着需要好几秒才能换出或者换入一个 1 GB 的程序。

SATA(Serial ATA)硬盘,又称串口硬盘,是未来 PC 机硬盘的趋势,已基本取代了传统的 PATA 硬盘。

那么还有没有一种有效的方式来应对呢?有,那就是使用 虚拟内存(virtual memory),虚拟内存的基本思想是,每个程序都有自己的地址空间,这个地址空间被划分为多个称为页面(page)的块。每一页都是连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,硬件会立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。

在某种意义上来说,虚拟地址是对基址寄存器和变址寄存器的一种概述。8088 有分离的基址寄存器(但不是变址寄存器)用于放入 text 和 data 。

使用虚拟内存,可以将整个地址空间以很小的单位映射到物理内存中,而不是仅仅针对 text 和 data 区进行重定位。下面我们会探讨虚拟内存是如何实现的。

虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中,当一个程序等待它的一部分读入内存时,可以把 CPU 交给另一个进程使用。

分页

大部分使用虚拟内存的系统中都会使用一种 分页(paging) 技术。在任何一台计算机上,程序会引用使用一组内存地址。当程序执行

MOV REG,1000

这条指令时,它会把内存地址为 1000 的内存单元的内容复制到 REG 中(或者相反,这取决于计算机)。地址可以通过索引、基址寄存器、段寄存器或其他方式产生。

这些程序生成的地址被称为 虚拟地址(virtual addresses) 并形成虚拟地址空间(virtual address space),在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存中线上,读写操作都使用同样地址的物理内存。在使用虚拟内存时,虚拟地址不会直接发送到内存总线上。相反,会使用 MMU(Memory Management Unit) 内存管理单元把虚拟地址映射为物理内存地址,像下图这样

下面这幅图展示了这种映射是如何工作的

页表给出虚拟地址与物理内存地址之间的映射关系。每一页起始于 4096 的倍数位置,结束于 4095 的位置,所以 4K 到 8K 实际为 4096 - 8191 ,8K - 12K 就是 8192 - 12287

在这个例子中,我们可能有一个 16 位地址的计算机,地址从 0 - 64 K - 1,这些是虚拟地址。然而只有 32 KB 的物理地址。所以虽然可以编写 64 KB 的程序,但是程序无法全部调入内存运行,在磁盘上必须有一个最多 64 KB 的程序核心映像的完整副本,以保证程序片段在需要时被调入内存。

存在映射的页如何映射

虚拟地址空间由固定大小的单元组成,这种固定大小的单元称为 页(pages)。而相对的,物理内存中也有固定大小的物理单元,称为 页框(page frames)。页和页框的大小一样。在上面这个例子中,页的大小为 4KB ,但是实际的使用过程中页的大小范围可能是 512 字节 - 1G 字节的大小。对应于 64 KB 的虚拟地址空间和 32 KB 的物理内存,可得到 16 个虚拟页面和 8 个页框。RAM 和磁盘之间的交换总是以整个页为单元进行交换的。

程序试图访问地址时,例如执行下面这条指令

MOV REG, 0

会将虚拟地址 0 送到 MMU。MMU 看到虚拟地址落在页面 0 (0 - 4095),根据其映射结果,这一页面对应的页框 2 (8192 - 12287),因此 MMU 把地址变换为 8192 ,并把地址 8192 送到总线上。内存对 MMU 一无所知,它只看到一个对 8192 地址的读写请求并执行它。MMU 从而有效的把所有虚拟地址 0 - 4095 映射到了 8192 - 12287 的物理地址。同样的,指令

MOV REG, 8192

也被有效的转换为

MOV REG, 24576

虚拟地址 8192(在虚拟页 2 中)被映射到物理地址 24576(在物理页框 6 中)上。

通过恰当的设置 MMU,可以把 16 个虚拟页面映射到 8 个页框中的任何一个。但是这并没有解决虚拟地址空间比物理内存大的问题。

上图中有 8 个物理页框,于是只有 8 个虚拟页被映射到了物理内存中,在上图中用 X 号表示的其他页面没有被映射。在实际的硬件中,会使用一个 在/不在(Present/absent bit)位记录页面在内存中的实际存在情况。

未映射的页如何映射

当程序访问一个未映射的页面,如执行指令

MOV REG, 32780

将会发生什么情况呢?虚拟页面 8 (从 32768 开始)的第 12 个字节所对应的物理地址是什么?MMU 注意到该页面没有被映射(在图中用 X 号表示),于是 CPU 会陷入(trap)到操作系统中。这个陷入称为 缺页中断(page fault) 或者是 缺页错误。操作系统会选择一个很少使用的页并把它的内容写入磁盘(如果它不在磁盘上)。随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷入的指令。有点不太好理解,举个例子来看一下。

例如,如果操作系统决定放弃页框 1,那么它将把虚拟机页面 8 装入物理地址 4096,并对 MMU 映射做两处修改。首先,它要将虚拟页中的 1 表项标记为未映射,使以后任何对虚拟地址 4096 - 8191 的访问都将导致陷入。随后把虚拟页面 8 的表项的叉号改为 1,因此在引起陷阱的指令重新启动时,它将把虚拟地址 32780 映射为物理地址(4096 + 12)。

下面查看一下 MMU 的内部构造以便了解它们是如何工作的,以及了解为什么我们选用的页大小都是 2 的整数次幂。下图我们可以看到一个虚拟地址的例子

虚拟地址 8196 (二进制 0010000000000100)用上面的页表映射图所示的 MMU 映射机制进行映射,输入的 16 位虚拟地址被分为 4 位的页号和 12 位的偏移量。4 位的页号可以表示 16 个页面,12 位的偏移可以为一页内的全部 4096 个字节。

可用页号作为页表(page table) 的索引,以得出对应于该虚拟页面的页框号。如果在/不在位则是 0 ,则引起一个操作系统陷入。如果该位是 1,则将在页表中查到的页框号复制到输出寄存器的高 3 位中,再加上输入虚拟地址中的低 12 位偏移量。如此就构成了 15 位的物理地址。输出寄存器的内容随即被作为物理地址送到总线。

页表

在上面这个简单的例子中,虚拟地址到物理地址的映射可以总结如下:虚拟地址被分为虚拟页号(高位部分)和偏移量(低位部分)。例如,对于 16 位地址和 4 KB 的页面大小,高 4 位可以指定 16 个虚拟页面中的一页,而低 12 位接着确定了所选页面中的偏移量(0-4095)。

虚拟页号可作为页表的索引用来找到虚拟页中的内容。由页表项可以找到页框号(如果有的话)。然后把页框号拼接到偏移量的高位端,以替换掉虚拟页号,形成物理地址。

因此,页表的目的是把虚拟页映射到页框中。从数学上说,页表是一个函数,它的参数是虚拟页号,结果是物理页框号。

通过这个函数可以把虚拟地址中的虚拟页转换为页框,从而形成物理地址。

页表项的结构

下面我们探讨一下页表项的具体结构,上面你知道了页表项的大致构成,是由页框号和在/不在位构成的,现在我们来具体探讨一下页表项的构成

页表项的结构是与机器相关的,但是不同机器上的页表项大致相同。上面是一个页表项的构成,不同计算机的页表项可能不同,但是一般来说都是 32 位的。页表项中最重要的字段就是页框号(Page frame number)。毕竟,页表到页框最重要的一步操作就是要把此值映射过去。下一个比较重要的就是在/不在位,如果此位上的值是 1,那么页表项是有效的并且能够被使用。如果此值是 0 的话,则表示该页表项对应的虚拟页面不在内存中,访问该页面会引起一个缺页异常(page fault)。

保护位(Protection) 告诉我们哪一种访问是允许的,啥意思呢?最简单的表示形式是这个域只有一位,0 表示可读可写,1 表示的是只读

修改位(Modified) 和 访问位(Referenced) 会跟踪页面的使用情况。当一个页面被写入时,硬件会自动的设置修改位。修改位在页面重新分配页框时很有用。如果一个页面已经被修改过(即它是 脏 的),则必须把它写回磁盘。如果一个页面没有被修改过(即它是 干净的),那么重新分配时这个页框会被直接丢弃,因为磁盘上的副本仍然是有效的。这个位有时也叫做 脏位(dirty bit),因为它反映了页面的状态。

访问位(Referenced) 在页面被访问时被设置,不管是读还是写。这个值能够帮助操作系统在发生缺页中断时选择要淘汰的页。不再使用的页要比正在使用的页更适合被淘汰。这个位在后面要讨论的页面置换算法中作用很大。

最后一位用于禁止该页面被高速缓存,这个功能对于映射到设备寄存器还是内存中起到了关键作用。通过这一位可以禁用高速缓存。具有独立的 I/O 空间而不是用内存映射 I/O 的机器来说,并不需要这一位。

在深入讨论下面问题之前,需要强调一下:虚拟内存本质上是用来创造一个地址空间的抽象,可以把它理解成为进程是对 CPU 的抽象,虚拟内存的实现,本质是将虚拟地址空间分解成页,并将每一项映射到物理内存的某个页框。因为我们的重点是如何管理这个虚拟内存的抽象。

加速分页过程

到现在我们已经虚拟内存(virtual memory) 和 分页(paging) 的基础,现在我们可以把目光放在具体的实现上面了。在任何带有分页的系统中,都会需要面临下面这两个主要问题:

  • 虚拟地址到物理地址的映射速度必须要快

  • 如果虚拟地址空间足够大,那么页表也会足够大

第一个问题是由于每次访问内存都需要进行虚拟地址到物理地址的映射,所有的指令最终都来自于内存,并且很多指令也会访问内存中的操作数。

操作数:操作数是计算机指令中的一个组成部分,它规定了指令中进行数字运算的量 。操作数指出指令执行的操作所需要数据的来源。操作数是汇编指令的一个字段。比如,MOV、ADD 等。

因此,每条指令可能会多次访问页表,如果执行一条指令需要 1 ns,那么页表查询需要在 0.2 ns 之内完成,以避免映射成为一个主要性能瓶颈。

第二个问题是所有的现代操作系统都会使用至少 32 位的虚拟地址,并且 64 位正在变得越来越普遍。假设页大小为 4 KB,32 位的地址空间将近有 100 万页,而 64 位地址空间简直多到无法想象。

对大而且快速的页映射的需要成为构建计算机的一个非常重要的约束。就像上面页表中的图一样,每一个表项对应一个虚拟页面,虚拟页号作为索引。在启动一个进程时,操作系统会把保存在内存中进程页表读副本放入寄存器中。

最后一句话是不是不好理解?还记得页表是什么吗?它是虚拟地址到内存地址的映射页表。页表是虚拟地址转换的关键组成部分,它是访问内存中数据所必需的。在进程启动时,执行很多次虚拟地址到物理地址的转换,会把物理地址的副本从内存中读入到寄存器中,再执行这一转换过程。

所以,在进程的运行过程中,不必再为页表而访问内存。使用这种方法的优势是简单而且映射过程中不需要访问内存。缺点是 页表太大时,代价高昂,而且每次上下文切换的时候都必须装载整个页表,这样会造成性能的降低。鉴于此,我们讨论一下加速分页机制和处理大的虚拟地址空间的实现方案

转换检测缓冲区

我们首先先来一起探讨一下加速分页的问题。大部分优化方案都是从内存中的页表开始的。这种设计对效率有着巨大的影响。考虑一下,例如,假设一条 1 字节的指令要把一个寄存器中的数据复制到另一个寄存器。在不分页的情况下,这条指令只访问一次内存,即从内存取出指令。有了分页机制后,会因为要访问页表而需要更多的内存访问。由于执行速度通常被 CPU 从内存中取指令和数据的速度所限制,这样的话,两次访问才能实现一次的访问效果,所以内存访问的性能会下降一半。在这种情况下,根本不会采用分页机制。

什么是 1 字节的指令?我们以 8085 微处理器为例来说明一下,在 8085 微处理中,一共有 3 种字节指令,它们分别是 1-byte(1 字节)、2-byte(2 字节)、3-byte(3 字节),我们分别来说一下

1-byte:1 字节的操作数和操作码共同以 1 字节表示;操作数是内部寄存器,并被编码到指令中;指令需要一个存储位置来将单个寄存器存储在存储位置中。没有操作数的指令也是 1-byte 指令。

例如:MOV B,C 、LDAX B、NOP、HLT(这块不明白的读者可以自行查阅)

2-byte: 2 字节包括:第一个字节指定的操作码;第二个字节指定操作数;指令需要两个存储器位置才能存储在存储器中。

例如 MVI B, 26 H、IN 56 H

3-byte: 在 3 字节指令中,第一个字节指定操作码;后面两个字节指定 16 位的地址;第二个字节保存低位地址;第三个字节保存 高位地址。指令需要三个存储器位置才能将单个字节存储在存储器中。

例如 LDA 2050 H、JMP 2085 H

大多数程序总是对少量页面进行多次访问,而不是对大量页面进行少量访问。因此,只有很少的页面能够被再次访问,而其他的页表项很少被访问。

页表项一般也被称为 Page Table Entry(PTE)。

基于这种设想,提出了一种方案,即从硬件方面来解决这个问题,为计算机设置一个小型的硬件设备,能够将虚拟地址直接映射到物理地址,而不必再访问页表。这种设备被称为转换检测缓冲区(Translation Lookaside Buffer, TLB),有时又被称为 相联存储器(associate memory) 。

​ TLB 加速分页

TLB 通常位于 MMU 中,包含少量的表项,每个表项都记录了页面的相关信息,除了虚拟页号外,其他表项都和页表是一一对应的

是不是你到现在还是有点不理解什么是 TLB,TLB 其实就是一种内存缓存,用于减少访问内存所需要的时间,它就是 MMU 的一部分,TLB 会将虚拟地址到物理地址的转换存储起来,通常可以称为地址翻译缓存(address-translation cache)。TLB 通常位于 CPU 和 CPU 缓存之间,它与 CPU 缓存是不同的缓存级别。下面我们来看一下 TLB 是如何工作的。

当一个 MMU 中的虚拟地址需要进行转换时,硬件首先检查虚拟页号与 TLB 中所有表项进行并行匹配,判断虚拟页是否在 TLB 中。如果找到了有效匹配项,并且要进行的访问操作没有违反保护位的话,则将页框号直接从 TLB 中取出而不用再直接访问页表。如果虚拟页在 TLB 中但是违反了保护位的权限的话(比如只允许读但是是一个写指令),则会生成一个保护错误(protection fault) 返回。

上面探讨的是虚拟地址在 TLB 中的情况,那么如果虚拟地址不再 TLB 中该怎么办?如果 MMU 检测到没有有效的匹配项,就会进行正常的页表查找,然后从 TLB 中逐出一个表项然后把从页表中找到的项放在 TLB 中。当一个表项被从 TLB 中清除出,将修改位复制到内存中页表项,除了访问位之外,其他位保持不变。当页表项从页表装入 TLB 中时,所有的值都来自于内存。

软件 TLB 管理

直到现在,我们假设每台电脑都有可以被硬件识别的页表,外加一个 TLB。在这个设计中,TLB 管理和处理 TLB 错误完全由硬件来完成。仅仅当页面不在内存中时,才会发生操作系统的陷入(trap)。

在以前,我们上面的假设通常是正确的。但是,许多现代的 RISC 机器,包括 SPARC、MIPS 和 HP PA,几乎所有的页面管理都是在软件中完成的。

精简指令集计算机或 RISC 是一种计算机指令集,它使计算机的微处理器的每条指令(CPI)周期比复杂指令集计算机(CISC)少

在这些计算机上,TLB 条目由操作系统显示加载。当发生 TLB 访问丢失时,不再是由 MMU 到页表中查找并取出需要的页表项,而是生成一个 TLB 失效并将问题交给操作系统解决。操作系统必须找到该页,把它从 TLB 中移除(移除页表中的一项),然后把新找到的页放在 TLB 中,最后再执行先前出错的指令。然而,所有这些操作都必须通过少量指令完成,因为 TLB 丢失的发生率要比出错率高很多。

无论是用硬件还是用软件来处理 TLB 失效,常见的方式都是找到页表并执行索引操作以定位到将要访问的页面,在软件中进行搜索的问题是保存页表的页可能不在 TLB 中,这将在处理过程中导致其他 TLB 错误。改善方法是可以在内存中的固定位置维护一个大的 TLB 表项的高速缓存来减少 TLB 失效。通过首先检查软件的高速缓存,操作系统 能够有效的减少 TLB 失效问题。

TLB 软件管理会有两种 TLB 失效问题,当一个页访问在内存中而不在 TLB 中时,将产生 软失效(soft miss),那么此时要做的就是把页表更新到 TLB 中(我们上面探讨的过程),而不会产生磁盘 I/O,处理仅仅需要一些机器指令在几纳秒的时间内完成。然而,当页本身不在内存中时,将会产生硬失效(hard miss),那么此时就需要从磁盘中进行页表提取,硬失效的处理时间通常是软失效的百万倍。在页表结构中查找映射的过程称为 页表遍历(page table walk)。

上面的这两种情况都是理想情况下出现的现象,但是在实际应用过程中情况会更加复杂,未命中的情况可能既不是硬失效又不是软失效。一些未命中可能更软或更硬(偷笑)。比如,如果页表遍历的过程中没有找到所需要的页,那么此时会出现三种情况:

  • 所需的页面就在内存中,但是却没有记录在进程的页表中,这种情况可能是由其他进程从磁盘掉入内存,这种情况只需要把页正确映射就可以了,而不需要在从硬盘调入,这是一种软失效,称为 次要缺页错误(minor page fault)。

  • 基于上述情况,如果需要从硬盘直接调入页面,这就是严重缺页错误(major page falut)。

  • 还有一种情况是,程序可能访问了一个非法地址,根本无需向 TLB 中增加映射。此时,操作系统会报告一个 段错误(segmentation fault) 来终止程序。只有第三种缺页属于程序错误,其他缺页情况都会被硬件或操作系统以降低程序性能为代价来修复

针对大内存的页表

还记得我们讨论的是什么问题吗?(捂脸),可能讨论的太多你有所不知道了,我再提醒你一下,上面加速分页过程讨论的是虚拟地址到物理地址的映射速度必须要快的问题,还有一个问题是 如果虚拟地址空间足够大,那么页表也会足够大的问题,如何处理巨大的虚拟地址空间,下面展开我们的讨论。

多级页表

第一种方案是使用多级页表(multi),下面是一个例子

32 位的虚拟地址被划分为 10 位的 PT1 域,10 位的 PT2 域,还有 12 位的 Offset 域。因为偏移量是 12 位,所以页面大小是 4KB,公有 2^20 次方个页面。

引入多级页表的原因是避免把全部页表一直保存在内存中。不需要的页表就不应该保留

多级页表是一种分页方案,它由两个或多个层次的分页表组成,也称为分层分页。级别1(level 1)页面表的条目是指向级别 2(level 2) 页面表的指针,级别2页面表的条目是指向级别 3(level 3) 页面表的指针,依此类推。最后一级页表存储的是实际的信息。

下面是一个二级页表的工作过程

在最左边是顶级页表,它有 1024 个表项,对应于 10 位的 PT1 域。当一个虚拟地址被送到 MMU 时,MMU 首先提取 PT1 域并把该值作为访问顶级页表的索引。因为整个 4 GB (即 32 位)虚拟地址已经按 4 KB 大小分块,所以顶级页表中的 1024 个表项的每一个都表示 4M 的块地址范围。

由索引顶级页表得到的表项中含有二级页表的地址或页框号。顶级页表的表项 0 指向程序正文的页表,表项 1 指向含有数据的页表,表项 1023 指向堆栈的页表,其他的项(用阴影表示)表示没有使用。现在把 PT2 域作为访问选定的二级页表的索引,以便找到虚拟页面的对应页框号。

倒排页表

针对分页层级结构中不断增加的替代方法是使用 倒排页表(inverted page tables)。采用这种解决方案的有 PowerPC、UltraSPARC 和 Itanium。在这种设计中,实际内存中的每个页框对应一个表项,而不是每个虚拟页面对应一个表项。

虽然倒排页表节省了大量的空间,但是它也有自己的缺陷:那就是从虚拟地址到物理地址的转换会变得很困难。当进程 n 访问虚拟页面 p 时,硬件不能再通过把 p 当作指向页表的一个索引来查找物理页。而是必须搜索整个倒排表来查找某个表项。另外,搜索必须对每一个内存访问操作都执行一次,而不是在发生缺页中断时执行。

解决这一问题的方式时使用 TLB。当发生 TLB 失效时,需要用软件搜索整个倒排页表。一个可行的方式是建立一个散列表,用虚拟地址来散列。当前所有内存中的具有相同散列值的虚拟页面被链接在一起。如下图所示

如果散列表中的槽数与机器中物理页面数一样多,那么散列表的冲突链的长度将会是 1 个表项的长度,这将会大大提高映射速度。一旦页框被找到,新的(虚拟页号,物理页框号)就会被装在到 TLB 中。

页面置换算法


当发生缺页异常时,操作系统会选择一个页面进行换出从而为新进来的页面腾出空间。如果要换出的页面在内存中已经被修改,那么必须将其写到磁盘中以使磁盘副本保持最新状态。如果页面没有被修改过,并且磁盘中的副本也已经是最新的,那么就不需要进行重写。那么就直接使用调入的页面覆盖需要移除的页面就可以了。

当发生缺页中断时,虽然可以随机的选择一个页面进行置换,但是如果每次都选择一个不常用的页面会提升系统的性能。如果一个经常使用的页面被换出,那么这个页面在短时间内又可能被重复使用,那么就可能会造成额外的性能开销。在关于页面的主题上有很多页面置换算法(page replacement algorithms),这些已经从理论上和实践上得到了证明。

需要指出的是,页面置换问题在计算机的其他领域中也会出现。例如,多数计算机把最近使用过的 32 字节或者 64 字节的存储块保存在一个或多个高速缓存中。当缓存满的时候,一些块就被选择和移除。这些块的移除除了花费时间较短外,这个问题同页面置换问题完全一样。之所以花费时间较短,是因为丢掉的高速缓存可以从内存中获取,而内存没有寻找磁道的时间也不存在旋转延迟。

第二个例子是 Web 服务器。服务器会在内存中缓存一些经常使用到的 Web 页面。然而,当缓存满了并且已经引用了新的页面,那么必须决定退出哪个 Web 页面。在高速缓存中的 Web 页面不会被修改。因此磁盘中的 Web 页面经常是最新的,同样的考虑也适用在虚拟内存中。在虚拟系统中,内存中的页面可能会修改也可能不会修改。

下面我们就来探讨一下有哪些页面置换算法。

最优页面置换算法

最优的页面置换算法很容易描述但在实际情况下很难实现。它的工作流程如下:在缺页中断发生时,这些页面之一将在下一条指令(包含该指令的页面)上被引用。其他页面则可能要到 10、100 或者 1000 条指令后才会被访问。每个页面都可以用在该页首次被访问前所要执行的指令数作为标记。

最优化的页面算法表明应该标记最大的页面。如果一个页面在 800 万条指令内不会被使用,另外一个页面在 600 万条指令内不会被使用,则置换前一个页面,从而把需要调入这个页面而发生的缺页中断推迟。计算机也像人类一样,会把不愿意做的事情尽可能的往后拖。

这个算法最大的问题时无法实现。当缺页中断发生时,操作系统无法知道各个页面的下一次将在什么时候被访问。这种算法在实际过程中根本不会使用。

最近未使用页面置换算法

为了能够让操作系统收集页面使用信息,大部分使用虚拟地址的计算机都有两个状态位,R 和 M,来和每个页面进行关联。每当引用页面(读入或写入)时都设置 R,写入(即修改)页面时设置 M,这些位包含在每个页表项中,就像下面所示

因为每次访问时都会更新这些位,因此由硬件来设置它们非常重要。一旦某个位被设置为 1,就会一直保持 1 直到操作系统下次来修改此位。

如果硬件没有这些位,那么可以使用操作系统的缺页中断和时钟中断机制来进行模拟。当启动一个进程时,将其所有的页面都标记为不在内存;一旦访问任何一个页面就会引发一次缺页中断,此时操作系统就可以设置 R 位(在它的内部表中),修改页表项使其指向正确的页面,并设置为 READ ONLY 模式,然后重新启动引起缺页中断的指令。如果页面随后被修改,就会发生另一个缺页异常。从而允许操作系统设置 M 位并把页面的模式设置为 READ/WRITE。

可以用 R 位和 M 位来构造一个简单的页面置换算法:当启动一个进程时,操作系统将其所有页面的两个位都设置为 0。R 位定期的被清零(在每个时钟中断)。用来将最近未引用的页面和已引用的页面分开。

当出现缺页中断后,操作系统会检查所有的页面,并根据它们的 R 位和 M 位将当前值分为四类:

  • 第 0 类:没有引用 R,没有修改 M

  • 第 1 类:没有引用 R,已修改 M

  • 第 2 类:引用 R ,没有修改 M

  • 第 3 类:已被访问 R,已被修改 M

尽管看起来好像无法实现第一类页面,但是当第三类页面的 R 位被时钟中断清除时,它们就会发生。时钟中断不会清除 M 位,因为需要这个信息才能知道是否写回磁盘中。清除 R 但不清除 M 会导致出现一类页面。

NRU(Not Recently Used) 算法从编号最小的非空类中随机删除一个页面。此算法隐含的思想是,在一个时钟内(约 20 ms)淘汰一个已修改但是没有被访问的页面要比一个大量引用的未修改页面好,NRU 的主要优点是易于理解并且能够有效的实现

先进先出页面置换算法

另一种开销较小的方式是使用 FIFO(First-In,First-Out) 算法,这种类型的数据结构也适用在页面置换算法中。由操作系统维护一个所有在当前内存中的页面的链表,最早进入的放在表头,最新进入的页面放在表尾。在发生缺页异常时,会把头部的页移除并且把新的页添加到表尾。

还记得缺页异常什么时候发生吗?我们知道应用程序访问内存会进行虚拟地址到物理地址的映射,缺页异常就发生在虚拟地址无法映射到物理地址的时候。因为实际的物理地址要比虚拟地址小很多(参考上面的虚拟地址和物理地址映射图),所以缺页经常会发生。

先进先出页面可能是最简单的页面替换算法了。在这种算法中,操作系统会跟踪链表中内存中的所有页。下面我们举个例子看一下(这个算法我刚开始看的时候有点懵逼,后来才看懂,我还是很菜)

  • 初始化的时候,没有任何页面,所以第一次的时候会检查页面 1 是否位于链表中,没有在链表中,那么就是 MISS,页面1 进入链表,链表的先进先出的方向如图所示。

  • 类似的,第二次会先检查页面 2 是否位于链表中,没有在链表中,那么页面 2 进入链表,状态为 MISS,依次类推。

  • 我们来看第四次,此时的链表为 1 2 3,第四次会检查页面 2 是否位于链表中,经过检索后,发现 2 在链表中,那么状态就是 HIT,并不会再进行入队和出队操作,第五次也是一样的。

  • 下面来看第六次,此时的链表还是 1 2 3,因为之前没有执行进入链表操作,页面 5 会首先进行检查,发现链表中没有页面 5 ,则执行页面 5 的进入链表操作,页面 2 执行出链表的操作,执行完成后的链表顺序为 2 3 5。

第二次机会页面置换算法

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

中高级开发必知必会:

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
dUWy-1713707519082)]

[外链图片转存中…(img-S7KcFYdk-1713707519083)]

[外链图片转存中…(img-4YMy8Yfl-1713707519083)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

中高级开发必知必会:

[外链图片转存中…(img-lYHGGPDb-1713707519084)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/2401_83330193/article/details/138046783

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签