FreeRTOS---堆内存管理(一)

FreeRTOS的堆内存管理

  • 简介
    • 动态内存分配及其与 FreeRTOS 的相关性
    • 动态内存分配选项
  • 内存分配方案
    • Heap_1
    • heap_2
    • Heap_3
    • Heap_4
      • 设置heap_4的起始地址
    • Heap_5
      • vPortDefineHeapRegions()
  • 堆相关的函数
    • xPortGetFreeHeapSize
    • xPortGetMinimumEverFreeHeapSize
    • Malloc调用失败的Hook函数

这篇文章先说原理,下一遍文章说代码的具体实现

简介

从FreeRTOS V9.0.0开始,FreeRTOS应用程序可以完全静态分配,无序包含堆内存管理器

动态内存分配及其与 FreeRTOS 的相关性

从FreeRTOS V9.0.0开始,内核对象可以在编译时静态分配,也可以在运行时动态分配,内核对象有任务、队列、信号量和事件组。为了使FreeRTOS尽可能容易使用,这些内核对象不是在编译时静态分配,而是在运行时动态分配。 FreeRTOS会在每次创建内核对象时分配RAM,并在每次删除内核对象时释放RAM。该策略减少了设计和规划工作,简化了API,并最大限度减少了RAM占用空间。
动态内存分配是一个C编程概念,而不是特定于FreeRTOS或多任务处理的概念。它与FreeRTOS相关,因为内核对象是动态分配的,通用编译器提供的动态内存分配方案不总是适合应用程序。可以使用C标准库malloc()和free()分配内存,但由于一些原因,它们不合适:

  • 它们在小型嵌入式系统上并不总是可用。
  • 它们的实现可能相对较大,占用宝贵的代码空间。
  • 它们很少是线程安全的。
  • 它们不是确定性的; 执行函数所花费的时间会因调用而异。
  • 它们可能会受到碎片化 1 的影响。
  • 它们会使链接器配置复杂化。
  • 如果允许堆空间增长为其他变量使用的内存,它们可能是难以调试错误的根源。

动态内存分配选项

FreeRTOS现在将内存分配视为可移植层的一部分(而不是核心代码库的一部分)。这是因为不同的嵌入式系统具有不同的动态内存分配和时序要求,从核心代码库中删除动态内存分配使应用程序编写者能够在适当的时候提供他们自己的特定实现。
当FreeRTOS需要RAM时,它不会调用malloc(),而是调用pvPortMalloc(),当RAM释放内存时,内核不会调用free(),而是调用vPortFree()。pvPortMalloc()与标准C库malloc函数原型相同,vPortFree()与标准C库free()函数原型相同。
pvPortMalloc()和vPortFree()是公共函数,因此也可以从应用程序代码中调用。
FreeRTOS提供了pvPortMalloc()和vPortFree()的五个示例实现,FreeRTOS应用程序可以使用示例实现之一,也可以提供自己的实现。这五个实例分别定义在heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c源文件中,均位于FreeRTOS/Source/portable/MemMang目录中。

内存分配方案

Heap_1

小型专用嵌入式系统通常只在调度程序启动前,只创建任务和其他内核对象,在这种情况下,内存仅在应用程序开始执行任何实时功能之前由内核动态分配,并且内存在应用程序的生命周期内保持分配状态。 这意味着所选择的分配方案不必考虑任何更复杂的内存分配问题,例如确定性和碎片,而可以只考虑代码大小和简单性等属性。 也就是说,只分配内存,其他像把两个相邻的空闲块整合到一块,这个是不管的。
Heap_1.c实现了一个非常基本的pvPortMalloc()版本,并没有实现vPortFree(),所以从不删除任务或其他内核对象的应用程序可以使用heap_1。当调用pvPortMalloc()时,heap_1分配方案将一个堆细分为更小的块,堆的总大小由FreeRTOSConfig.h中的宏configTOTAL_HEAP_SIZE设置,但是为字节。
每个创建的任务都要两个内存块,一个是任务控制块(TCB),另一个堆栈,这两个都从堆分配。下图演示来了heap_1如何在创建任务时细分堆
在这里插入图片描述

  • A 显示创建任何任务之前的堆——整个堆都是空闲的。
  • B 显示创建一项任务后的堆。
  • C 显示创建三个任务后的堆。
    heap_1适用于那些一旦创建好任务、信号量、队列和事件组就再也不会删除的应用。一些禁止使用动态内存分配的商业关键和安全关键系统也有可能使用 heap_1。由于与非确定性、内存碎片和分配失败相关的不确定性,关键系统通常禁止动态内存分配——但 Heap_1 始终是确定性的,不能对内存进行碎片化。heap_1代码实现和内存分配过程都很简单,内存是从一个静态数组(堆)分配到的,也就是适合那些不需要动态内存分配的应用

heap_2

为了向后兼容,FreeRTOS保留了Heap_21,但不建议使用它,使用heap_4,因为heap_4提供了增强的功能。
heap_2.c的大小也通过configTOTAL_HEAP_SIZE确定,它使用最佳拟合算法来分配内存,并且与heap_1不同,它允许释放内存。同样数组(堆)是静态声明的,因此会消耗大量的RAM,最佳拟合算法确保PVPortMalloc()使用大小与请求的字节数最接近的空闲内存块,比如,考虑以下场景:堆包含三个空闲内存块,分为为5字节、25字节和100字节,调用pvPortMalloc来请求20字节的RAM。可以容纳请求的字节数最小RAM空闲块是25字节,因此pvPortMalloc()将25字节块分为一个20字节块和一个5字节块,然后返回指向20字节的块的指针,新的5字节块依然可以被pvPortMalloc调用。
与heap_4不同,heap_2不会将相邻的空闲块合并为一个更大的块,因此容易发生碎片。 Heap_2适用于重复创建和删除任务的应用程序,前提是分配给创建的任务和堆栈大小不变。
下图演示了在创建、删除和再次创建任务时最佳拟合算法的工作原理:
在这里插入图片描述

  1. A 显示创建三个任务后的数组。 一个大的空闲块保留在阵列的顶部。
  2. B 显示其中一个任务被删除后的数组。 数组顶部的大空闲块仍然存在。 现在还有两个较小的空闲块,它们以前分配给已删除任务的 TCB 和堆栈。
  3. C 显示创建另一个任务后的情况。 创建任务导致对 pvPortMalloc() 的两次调用,一次分配 新的 TCB,一次分配任务堆栈。 任务是使用 xTaskCreate() API 函数创建的。 对 pvPortMalloc() 的调用发生在 xTaskCreate() 内部。
    Heap_2 不是确定性的,但比 malloc() 和 free() 的大多数标准库实现要快。

Heap_3

Heap_3使用标准库malloc()和free()函数,因此堆的大小由链接器配置定义,configTOTAL_HEAP_SIZE设置对其没有影响,Heap_3通过暂时挂起FreeRTOS调度程序使malloc()和free()线程安全。

Heap_4

与heap_1和heap_2一样,heap_4的工作原理是将数组细分为更小的块,数组是静态分配的,并有configTOTAL_HEAP_SIZE确定尺寸,heap_4使用first fit算法来分配内存,与heap_2冉,heap_4将合并相邻的空闲内存块,然后组合成一个更大的块,从而最大限度的降低内存碎片的风险。
the first fit算法确保pvPortMalloc()使用第一个能够容纳请求字节数的空闲块,例如,考虑以下场景:
堆包含三个空闲内存块,按照它们在数组中出现的顺序,分别为5字节、200字节和100字节,调用pvPortMalloc()来请求20字节的RAM,第一个适合请求的字节数的空闲块是200字节,因此pvPortMalloc在返回指针之前将200字节拆分为一个20字节的块和一个180字节的块,返回的指针指向20字节的块,新的180字节块仍可以被pvPortMalloc调用。
Heap_4将合并相邻的空闲块组合成一个更大的块,最大限度的降低碎片的风险,并使其适用于重复分配和释放不同大小的RAM块的应用程序。
下图演示了具有内存合并的 heap_4 首次拟合算法的工作原理,如内存被分配和释放:
在这里插入图片描述

  1. A 显示创建三个任务后的数组。一个大的空闲块保留在阵列的顶部。
  2. B 显示其中一个任务被删除后的数组。数组顶部的大空闲块仍然存在。还有一个空闲块,其中先前分配了已删除任务的 TCB 和堆栈。请注意,与演示 heap_2 时不同,删除 TCB 时释放的内存和删除堆栈时释放的内存不会保留为两个单独的空闲块,而是组合在一起以创建更大的单个空闲块。
  3. C 显示了创建 FreeRTOS 队列后的情况。队列是使用 xQueueCreate() API 函数创建的。 xQueueCreate() 调用 pvPortMalloc() 来分配队列使用的 RAM。由于 heap_4 使用the first fit算法,pvPortMalloc() 将从第一个足够大以容纳队列的空闲 RAM 块分配 RAM,使用的是删除任务时释放的 RAM。然而,队列不会消耗空闲块中的所有 RAM,因此该块被分成两部分,未使用的部分仍然可用于未来对 pvPortMalloc() 的调用。
  4. D 显示了直接从应用程序代码调用 pvPortMalloc() 后的情况,而不是通过调用 FreeRTOS API 函数间接调用的情况。用户分配的块足够小,可以放入第一个空闲块中,该块是分配给队列的内存和分配给后续 TCB 的内存之间的块。
    删除任务时释放的内存现在已拆分为三个单独的块;第一个块保存队列,第二个块保存用户分配的内存,第三个块保持空闲。
  5. E显示队列被删除后的情况,自动释放已分配给删除队列的内存。现在用户分配块的两侧都有空闲内存。
  6. F 显示用户分配的内存也被释放后的情况。用户分配块已使用的内存已与任一侧的空闲内存组合以创建更大的单个空闲块。

Heap_4 不是确定性的,但比 malloc() 和 free() 的大多数标准库实现要快。

设置heap_4的起始地址

有时,应用程序编写者需要将 heap_4 使用的数组放置在特定的内存地址。 例如,FreeRTOS 任务使用的堆栈是从堆分配的,因此可能有必要确保堆位于快速内部内存中,而不是位于慢速外部内存中。
默认情况下,heap_4 使用的数组在 heap_4.c 源文件中声明,其起始地址由链接器自动设置。 但是,如果 FreeRTOSConfig.h 中的 configAPPLICATION_ALLOCATED_HEAP 设置为 1,则该数组必须改为由使用 FreeRTOS 的应用程序声明。 如果数组被声明为应用程序的一部分,那么应用程序的编写者可以设置它的起始地址。
如果 configAPPLICATION_ALLOCATED_HEAP 在 FreeRTOSConfig.h 中设置为 1,则必须在应用程序的源文件之一中声明一个名为 ucHeap 并由 configTOTAL_HEAP_SIZE 设置大小的 uint8_t 数组。

Heap_5

heap_5用于分配和释放内存的算法和heap_4相同,不同的是heap_5不限于从单个静态声明的数组分配内存,heap_5可以从多个独立的内存空间分配内存,当运行 FreeRTOS 的系统提供的 RAM 没有在系统内存映射中显示为单个连续(没有空间)块时,Heap_5 很有用。
在撰写本文时,heap_5 是唯一提供的内存分配方案,必须在调用 pvPortMalloc() 之前显式初始化。 Heap_5 使用 vPortDefineHeapRegions() API 函数初始化。 使用 heap_5 时,必须在创建任何内核对象(任务、队列、信号量等)之前调用 vPortDefineHeapRegions()。

vPortDefineHeapRegions()

vPortDefineHeapRegions用于指定每个单独的内存区域的起始地址和大小,这些区域构成heap_5使用的总内存。

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );

每个单独的内存区域由HeapRegion_t类型的结构体描述**,所有可用内存区域的描述**作为 HeapRegion_t 结构数组传递到 vPortDefineHeapRegions()。

typedef struct HeapRegion
{
/* The start address of a block of memory that will be part of the heap.*/
uint8_t *pucStartAddress;
/* The size of the block of memory in bytes. */
size_t xSizeInBytes;
} HeapRegion_t;

pxHeapRegions 指向 HeapRegion_t 结构数组开头的指针。 数组中的每个结构都描述了内存的起始地址和长度,数组中的 HeapRegion_t 结构体必须按起始地址排序; 描述起始地址最低的内存区域的 HeapRegion_t 结构必须是数组中的第一个结构,描述起始地址最高的内存区域HeapRegion_t 结构必须是数组中的最后一个结构。数组的末尾由 HeapRegion_t 结构标记,该结构的 pucStartAddress 成员设置为 NULL。

例如,考虑下图中所示的假设内存映射,其中包含三个独立的RAM块:RAM1、RAM2和RAM3,假设可执行代码放置在只读寄存器中,没有显示。
在这里插入图片描述
下面的代码描述了整个RAM的三个块

/* Define the start address and size of the three RAM regions. */
#define RAM1_START_ADDRESS ( ( uint8_t * ) 0x00010000 )
#define RAM1_SIZE ( 65 * 1024 )
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
/* Create an array of HeapRegion_t definitions, with an index for each of the three RAM regions, and terminating the array with a NULL address. The HeapRegion_t structures must appear in start address order, with the structure that contains the lowest start address appearing first. */
const HeapRegion_t xHeapRegions[] =
{
{ RAM1_START_ADDRESS, RAM1_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
int main( void )
{
/* Initialize heap_5. */
vPortDefineHeapRegions( xHeapRegions );
/* Add application code here. */
}

虽然正确地描述了 RAM,但它没有展示一个可用的示例,因为它将所有 RAM 分配给堆,没有空闲 RAM 可供其他变量使用。
构建项目时,构建过程的链接阶段会为每个变量分配一个 RAM 地址。 可供链接器使用的 RAM 通常由链接器配置文件(例如链接描述文件)描述。假设链接描述文件包含关于 RAM1 的信息,但不包含关于 RAM2 或 RAM3 的信息。 因此,链接器将变量放置在 RAM1 中,只留下地址 0x0001nnnn 以上的 RAM1 部分可供 heap_5 使用。 0x0001nnnn 的实际值将取决于所链接的应用程序中包含的所有变量的组合大小。 链接器让所有 RAM2 和所有 RAM3 未使用,留下整个 RAM2 和整个 RAM3 可供 heap_5 使用。
如果使用上述 中所示的代码,分配给地址 0x0001nnnn 下的 heap_5 的 RAM 将与用于保存变量的 RAM 重叠。为了避免这种情况,xHeapRegions[] 数组中的第一个 HeapRegion_t 结构可以使用 0x0001nnnn 的起始地址,而不是 0x00010000 的起始地址。**但是,这不是推荐的解决方案,**因为:

  1. 起始地址可能不容易确定。
  2. 链接器使用的 RAM 量可能会在未来的构建中发生变化,因此需要更新 HeapRegion_t 结构中使用的起始地址。
  3. 如果链接器使用的 RAM 和 heap_5 使用的 RAM 重叠,构建工具将不知道,因此无法警告应用程序编写者。
    下面 展示了一个更方便和可维护的示例。它声明了一个名为 ucHeap 的数组。 ucHeap 是一个普通变量,因此它成为链接器分配给 RAM1 的数据的一部分。 xHeapRegions 数组中的第一个 HeapRegion_t 结构描述了 ucHeap 的起始地址和大小,因此 ucHeap 成为 heap_5 管理的内存的一部分。 ucHeap 的大小可以增加,直到链接器使用的 RAM 耗尽所有 RAM1
linker. */
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
/* Declare an array that will be part of the heap used by heap_5. The array will be placed in RAM1 by the linker. */
#define RAM1_HEAP_SIZE ( 30 * 1024 )
static uint8_t ucHeap[ RAM1_HEAP_SIZE ];
/* Create an array of HeapRegion_t definitions. Whereas in Listing 6 the first entry described all of RAM1, so heap_5 will have used all of RAM1, this time the first entry only describes the ucHeap array, so heap_5 will only use the part of RAM1 that contains the ucHeap array. The HeapRegion_t structures must still appear in start address order, with the structure that contains the lowest start address appearing first. */
const HeapRegion_t xHeapRegions[] =
{
{ ucHeap, RAM1_HEAP_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};

堆相关的函数

xPortGetFreeHeapSize

xPortGetFreeHeapSize函数返回堆中的空闲字节数,它可用于优化堆大小。 例如,如果 xPortGetFreeHeapSize() 在所有内核对象创建后返回 2000,那么 configTOTAL_HEAP_SIZE 的值可以减少 2000。

size_t xPortGetFreeHeapSize( void );

xPortGetMinimumEverFreeHeapSize

xPortGetMinimumEverFreeHeapSize返回自FreeRTOS应用程序开始执行以来,堆中存在最小未分配字节数。xPortGetMinimumEverFreeHeapSize返回值表明应用程序接近耗尽堆空间的程序, 例如,如果 xPortGetMinimumEverFreeHeapSize() 返回 200,那么在应用程序开始执行后的某个时间,它会在 200 字节内耗尽堆空间。
xPortGetMinimumEverFreeHeapSize() 仅在使用 heap_4 或 heap_5 时可用。

size_t xPortGetMinimumEverFreeHeapSize( void );

Malloc调用失败的Hook函数

pvPortMalloc() 可以直接从应用程序代码中调用。每次创建内核对象时,它也会在 FreeRTOS 源文件中调用。内核对象的例子包括任务、队列、信号量和事件组。
像标准库 malloc() 函数一样,如果 pvPortMalloc() 因为请求大小的块不存在而无法返回 RAM 块,那么它将返回 NULL,则不会创建内核对象。
如果对 pvPortMalloc() 的调用返回 NULL,则所有示例堆分配方案都可以配置一个调用挂钩(或回调)函数。如果在 FreeRTOSConfig.h 中将 configUSE_MALLOC_FAILED_HOOK 设置为 1,那么应用程序必须提供一个 malloc 失败的钩子函数。该函数可以以任何适合应用程序的方式实现,该函数如下:

void vApplicationMallocFailedHook( void );

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/379708.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python中生成随机整数_在Python中生成0到9之间的随机整数

python中生成随机整数Following are the few explanatory illustrations using different python modules, on how to generate random integers? Consider the scenario of generating the random numbers between 0 and 9 (both inclusive). 以下是使用不同的python模块的一…

愚人节恶搞网站谨防遭黑客攻击

金山毒霸云安全中心日前发出预警,在近期拦截的大量“挂马”、钓鱼等恶意网页中,与“愚人节”相关的,在近一周数量急剧增加。 愚人节将至,怎么整人好玩?近期许多恶搞网站、相关的网络论坛的流量不断攀升。金山毒霸云安全中心日前发…

JavaScript中的String()函数与示例

String()函数 (String() function) String() function is a predefined global function in JavaScript, it is used to convert an object to the string. String()函数是JavaScript中预定义的全局函数,用于将对象转换为字符串。 Example: 例: In thi…

ASCII码排序(C++)

题目描述: 输入三个字符(可以重复)后,按各字符的ASCII码从小到大的顺序输出这三个字符。 输入描述: 第一行输入一个数N,表示有N组测试数据。后面的N行输入多组数据,每组输入数据都是占一行,有三个字符组成,…

FreeRTOS--堆内存管理(二)

堆内存管理代码具体实现heap_1内存申请函数内存释放函数heap_2内存块内存堆初始化函数内存块插入函数内存申请函数判断是不是第一次申请内存开始分配内存内存释放函数heap_3heap_4内存堆初始化函数内存块插入函数heap_5上一篇文章说了FreeRTOS实现堆内存的原理,这一…

在查询的结果中添加自增列 两种方法

解决办法《一》: 在SQL Server数据库中表信息会用到Identity关键字来设置自增列。但是当有数据被删除的话,自增列就不连续了。如果想查询出这个表的信息,并添 加一列连续自增的ID,可用如下查询语句: SELECT Row_Nu…

一个非常简单的C#面试题

怎样实现对所有类可读但是在同一个assembly可写那? 答案: 同一个assembly namespace ClassLibrary1 { public class Class1 { public string Name { get; internal set; } } public class Class2 { public void GS() { Class1 cc new Class1(); cc.Name…

css中的node.js_在Node App中使用基本HTML,CSS和JavaScript

css中的node.jsYou may think this is not important, but it is!. As a beginner in node.js, most coding exercises are always server sided. 您可能认为这并不重要,但确实如此! 作为node.js的初学者,大多数编码练习始终都是服务器端的。…

Binary String Matching(C++)

题目描述: Given two strings A and B, whose alphabet consist only ‘0’ and ‘1’. Your task is only to tell how many times does A appear as a substring of B? For example, the text string B is ‘1001110110’ while the pattern string A is ‘11’, you should…

由一次代码优化想到的Js 数据类型

引子: 上周三进行了代码优化,其中有一个很普遍的代码,例如: if(test "") {dothis();}else{dothat()} ----->可以简化为 !test ? dothis():dothat(); if(test "") {dothis()}     ----->可以简化为…

VisualStudio2019配置OpenCV

VisualStudio2019配置OpenCV配置0x01 准备0x02 配置系统环境0x03 复制文件0x04 配置VisualStudio2019测试配置 0x01 准备 下载opencv,官网地址:https://opencv.org/releases/# 下载之后,自行安装 0x02 配置系统环境 找到高级系统设置 …

转载 Javascript的IE和Firefox兼容性汇编

微软关于IE、Firefox、Opera和Safari的JavaScript兼容性研究曾经发表过一份草案,可以点击下载《JScript Deviations from ES3》 以下为网上的一些搜集和整理(FF代表Firefox) 集合类对象问题现有代码中存在许多 document.form.item("itemName") 这样的语句&#xff0c…

存储器间接寻址方式_8086微处理器的程序存储器寻址模式

存储器间接寻址方式The Program Memory Addressing mode is used in branch instructions. These branch instructions are instructions which are responsible for changing the regular flow of the instruction execution and shifting the control to some other location…

Servlet的配置

1&#xff0c;基本配置 <!-- Servlet类的配置 --><servlet><servlet-name>sq</servlet-name><servlet-class>beyond.servlet.QuickStartServlet</servlet-class></servlet><!-- Servlet的虚拟路径的配置 --> <servlet-mapp…

Asp.net页面生存周期

# 事件或方法 功能 描述   1 Init 事件 页面初始化 初始化设置。   2 LoadViewState 方法 加载视图状态 填充ViewState属性。   3 LoadPostData 方法 处理回发数据 处理传入窗体数据。   4 Load 事件 加载页面 页面控件初始化完成并反映了客户端的数据。   5 RaisePo…

你正确关闭WCF链接了吗?

通常情况下我们关闭一个WCF链接都是简单地写把ICommunicationObject.Close()方法&#xff0c;但是这个方法有个问题就是当调用发生异常时&#xff0c;Close()会发生次生的异常&#xff0c;导致链接不能正常关闭。如果当这种异常很多时&#xff0c;必然对系统的稳定性有很大的影…

Visual Studio进行linux远程开发

目录准备工作创建一个项目配置远程项目准备工作 查看linux IP地址 安装了工具 sudo apt-get install openssh-server g gdb make ninja-build rsync zip开启ssh服务&#xff1a; sudo service ssh startVS2019按装了linux功能&#xff0c;如果没有&#xff0c;找到Visual S…

在给定总和K的二叉树中找到级别

Description: 描述&#xff1a; The article describes how to find the level in a binary tree with given sum K? This is an interview coding problem came in Samsung, Microsoft. 本文介绍了如何在给定总和K下在二叉树中找到级别 &#xff1f; 这是一个面试编码问题&a…

PostgreSQL学习手册(数据库维护) 转

原文&#xff1a; PostgreSQL学习手册(数据库维护)一、恢复磁盘空间&#xff1a;在PostgreSQL中&#xff0c;使用delete和update语句删除或更新的数据行并没有被实际删除&#xff0c;而只是在旧版本数据行的物理地址上将该行的状态置为已删除或已过期。因此当数据表中的数据变化…

++i与i++的根本性区别(两个代码对比搞定)

首先来看i 代码如下&#xff1a; #include <stdio.h> #include <stdlib.h> int main() {int i0;int ai;printf("%d\n",a);printf("%d\n\n\n",i);return 0; }输出结果如下&#xff1a; 解释&#xff1a;i其实是两行代码的简写形式&#xff0c…