《嵌入式Linux内存与性能详解》笔记2——进程内存优化

羽林君 2023-12-23 00:29

一、前言

我们上文《linux应用程序——内存测量》说了如何测量分析系统内存和进程内存的使用情况。当我们大概知道进程的使用情况后,我们可以针对性地做一些优化,那么本文将简单地说几种内存优化的方法。

二、堆栈优化

在讲解内存优化前,这里简单地说明一下一个程序的组成

  • 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。

  • 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由 OS 回收。

  • 数据段:初始化的全局变量和静态变量在一块区域,程序结束后由系统释放

  • BSS段:未初始化的全局变量 和未初始化的静态变量所在区域。程序结束后由系统释放。

  • 代码段:存放程序的二进制代码和文字常量

其中我们讲解 的优化方法,其余的更加深入我们找机会再做讲述

2.1 堆优化

提起堆,熟悉的读者应该就会想起 malloc 或者 new 。开辟内存是我们常见的使用手法,但使用得不好容易造成内存泄露和内存空洞,导致系统无法正常回收内存,造成的结果就是系统内存不足,严重的导致程序崩溃退出。

2.1.1 malloc

  • 当程序 malloc 申请内存时:

  1. 如果堆的地址空间还在 1G 以下,进程会通过系统调用 brk,来让 Linux 内核扩展堆顶的内存空间(堆是向上增长)。

  2. 如果堆的地址空间还在 1G 以上,那么会使用 mmap 映射空闲的物理页内存来获取内存

  • 当使用 free 释放内存时,进程又会通过系统调用 brkummap,来让内核缩减堆的内存空间或去映射

  • 用户态 使用 malloc 申请的内存是以 字节 为单位,而在内核中内存的管理是以 页(4K) 为单位

  • malloc 开辟的内存都是 8字节对齐

  • 太多的 brk系统调用,会使进程的速度变得很慢。

  • malloc 最小的分配长度为 16字节,如果分配 1、2个字节会造成浪费

  • 使用 malloc 时并不会直接向内核请求内存,而是先通过 glibc 的堆管理,再获得内存。而 glibc 的堆管理使用使用一种结构体。该结构体来定义 malloc 分配或释放的内存块,如下所示:

    struct malloc_chunk {    INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */    INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */    struct malloc_chunk* fd;         /* double links -- used only if free. */    struct malloc_chunk* bk;    /* Only used for large blocks: pointer to next larger size.  */    struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */    struct malloc_chunk* bk_nextsize;}

    我们以一段小程序为例子来讲解:

    #include #include  int main() {      char* p;    p = malloc(20);    strcpy(p, "Hello,world!");    printf("%s\n", p);     printf("%#x\n", *(p-4));     free(p);     return 0; }


    运行结果

    (p-4) 这个地址,其实记录着 malloc空间 的大小,我们看以下这段内存的分布图,如下:

    内存分布图:

    会发现在数据前面还有 2 个 4字节 的空间,在将其与我们前面的结构体对比就会发现前 2 个成员其实是一样的,而后面的 4 个指针成员被复用为内存空间了,为什么呢?
    因为如果当前 chunk 是使用中的,那么 fdbkfd_nextsizebk_nextsize都是无效的,它们都是关于空闲链表的指针,那么这些指针的空间全部被认为是空闲空间,所以直接复用这些成员的内存。

    注意:这里面有两个标志位:

    • P=1:表示上一块正在被使用,这时 prev_size 通常为 0

    • P=0:表示上一块空闲,这时 prev_size 通常为上一块的大小。

    • M=1:表示该内存块通过 mmap 来分配。在分配 大块内存 时,会采用 mmap 的 方式,那么在释放时会由 munmap_chunk 去释放;否则,释放时由 chunk_free 完成。

    • M=0:则表示该内存块不采用 mmap 方式分配。

    我们再看看程序的计算结果。0x19 去掉 3 个比标志位就是 0x18 等于十进制的 24,我们分配了 20 字节,其 8 字节对齐的结果就是 24

    2.1.2 mallopt

    glibc 为我们提供了了 堆管理 ,同时也保留了一些策略设置的接口,让我们自行定义策略。我们可以通过使用 mallopt 来定义完成策略的设置,其原型如下:

    /*    * param:想要设置的属性项    * value:设置值,单位为字节*/int mallopt(int param, int value);

    param 参数意义如下:

    • M_MXFAST:定义 fastbins小块内存 阀值,小于该阀值的小块空闲内存在释放后将不会去尝试合并,并且会在进程中继续使用(可以提高内存分配的速度)。其 缺省值 为 64,设置为 0 则禁止掉 fastbinsfastbins 生效时,对于一些小块的内存在释放后不去尝试合并以节省 CPU 和内存,因为 glibc 合并内存块需要去跟踪每个内存块。跟踪内存是需要代价的,如果关闭 fastbins 可以则会为每个内存块付出代价,无论内存块的大小。所以设置 M_MXFAST ,需要适度,太大太下都有可能产生不良影响


    • M_MMAP_THRESHOLDlibc大块内存的阀值。申请大于该阀值的内存,内存管理器将使用 mmap 系统调用申请内存;如果申请小于该阀值的内存,内存管理使用 brk系统调 用来扩展堆顶指针。该阀值缺省值为 128kB

    int main(){    char *p=malloc(1024*521);     pid_t pid=getpid();       printf("pid = %d\n", pid);      pause();     return 0; }


    如上所示的代码,会发现进程的 maps 多了一个段,该段的地址不在 ,这就是使用 mmap 开辟    的内存空间,如下所示:

    • M_MMAP_MAX:该进程中多使用 mmap 分配地址段的数量。

    • M_TRIM_THRESHOLD:堆顶内存回收阀值,当堆顶连续空闲内存数量大于该阀值时,libc 的内存管理其将调用系统调用 brk,来调整堆顶地址,释放内存。该值缺省为 128k

    int main(){  char *p[11];       int i;       /* 开辟 11 片内存 */  for(i = 0; i < 11; i++)       {               p[i]=(char*)malloc(1024*2);               strcpy(p[i], "123");       }        /* 释放 11 片内存 */  for(i = 0; i < 11; i++)   {               free(p[i]);       }   
    pid_t pid=getpid(); printf("pid = %d\n", pid); pause();
    return 0; }


    如上所示的代码,在运行后我们查看 smaps 属性,如下:

    smaps属性

    可见,我们的内存并没有被系统回收,而是一直还存在内存中,如果我们在前面加上下面的代码:

     /* 当堆顶空闲内存大于 1Kb 时回收 */
    mallopt(M_TRIM_THRESHOLD, 1024); /* 一定要加入打印才能生效,不然无法生效,笔者也不清楚为何,求大神告知 */ printf("finished M_TRIM_THRESHOLD\n"); /* 设置堆顶无空闲内存块 */ mallopt(M_TOP_PAD, 0);

    运行后查看 smaps 属性如下所示,可见内存已经被回收了

    • M_TOP_PAD:该参数决定了当 libc 内存管理器调用 brk 释放内存时,堆顶还需要保留的空闲内存数量。该值缺省为 0

    2.1.2 内存空洞

    内存泄露 是程序常见的一种漏洞,但这种漏洞我们往往能够通过跟踪或者其他手段找出来,但 内存空洞 比较不容易发现,我们看看下面这段代码:

    int main(){    mallopt(M_TRIM_THRESHOLD, 1024);    printf("finished M_TRIM_THRESHOLD\n");         mallopt(M_TOP_PAD, 0);
    char *p[11]; int i; /* 开辟 11 片内存 */ for(i = 0; i < 11; i++) { p[i]=(char*)malloc(1024*2); strcpy(p[i], "123"); } /* 只释放10片内存 */ for(i = 0; i < 10; i++) { free(p[i]); }
    pid_t pid=getpid(); printf("pid = %d\n", pid); pause();
    return 0; }

    我们通过设置 堆顶空闲内存块大小1KB堆定空闲内存0 的方法去掉因为策略造成影响,与上面不同的是我们开辟 11 块内存,但只释放了 10 块,我们看看运行后的 smaps 的情况:

    smaps

    发现我们释放堆顶下面的 10 块内存并没有被系统回收,与前面的相比,仅仅只是少释放了一块内存。这样的现象就是 内存空洞,也就是 只要堆顶部还有内存在使用,堆顶下方不管释放了多少内存都不会被释放。

    假设我们把开辟的内存放大一点,如下所示:


    int main(){    mallopt(M_TRIM_THRESHOLD, 1024);    printf("finished M_TRIM_THRESHOLD\n");         mallopt(M_TOP_PAD, 0);
    char *p[11]; int i; for(i = 0; i < 5; i++) { p[i]=(char*)malloc(1024*512); strcpy(p[i], "123"); }
    for(i = 0; i < 4; i++) { free(p[i]); }
    pid_t pid=getpid(); printf("pid = %d\n", pid); pause();
    return 0; }

    上面的代码使用了 512KB 大小的内存块,那么我们看看运行后的 smaps

    smaps


    因为我们使用了大内存块,所以系统是通过 mmap 来开辟内存的,所以 堆段 的使用情况没有变,而下面新增了一个内存块,可见该段实际使用的物理内存只有 4K,以为我们为其赋值了字符串,所以内核为该段开辟了一个内存页,而其他内存已经被释放掉,这样就没有造成 内存空洞但因为使用了更多的系统调用,其性能会有所下降。

    《嵌入式Linux内存与性能详解》建议:在申请分配内存时,本着就近原则 就可以了,需要的时候才分配内存,不需要了立刻释放。不必去严格的追求申请和释放的顺序。

    2.1.3 内存跟踪

    这里简单的介绍一种排查 内存泄露 的方法,使用 mtrace 来进行内存跟踪。使用步骤如下:

    1. 引入头文件 #include

    2. 在需要跟踪的程序中需要包含头文件,而且在 main函数开始 调用函数 mtrace。这样进程后面一切分配和释放内存的操作都可以由 mtrace 来跟踪和分析。

    3. 定义一个 环境变量,用来指示一个文件。该文件用来输出 log 信息。如下的例子:
      export MALLOC_TRACE=mtrace.log

    4. 正常运行程序,此时程序中的关于内存分配和释放的操作都可以记录下来。


    代码如下:

    #include #include #include  #include  #include #include  int main(){    mtrace(); 
    mallopt(M_TRIM_THRESHOLD, 1024); printf("finished M_TRIM_THRESHOLD\n"); mallopt(M_TOP_PAD, 0);
    char *p[11]; int i; for(i = 0; i < 5; i++) { p[i]=(char*)malloc(1024*512); strcpy(p[i], "123"); }
    for(i = 0; i < 4; i++) { free(p[i]); }
    pid_t pid=getpid(); printf("pid = %d\n", pid); muntrace(); pause();
    return 0; }

    mtrace.log


    我们开辟了 5 块内存,却释放了 4 块,所以图中一共有 5 个 +号 和 4 个 -号

    2.1.4 堆优化总结

    • 堆内存的小单位为 16Byte,所以尽量减少小块内存的申请,避免内存浪费。

    • 调整 M_MMAP_THRESHOLD,降低 mmap 的门槛,会降低内存空洞的风险,但也会增加系统调用,降低性能。

    • 调整 M_TRIM_THRESHOLD,减少堆顶连续内存门槛,释放更多的堆顶内存。

    以上是从书中获取到的经验,但无论如何还是需要结合实际的工程需求来做优化,希望可以帮到各位读者

    2.2 栈优化

    进程的 栈 是由程序自动来维护的,不需要手动申请和释放。一般情况下,栈是一段线性分布的内存,不会出现碎片问题。它是给函数存放跳转时的环境的。

    2.2.1 栈分配内存

    虽然栈的使用我们一般不需要去操心,但在某些情况下我们可以使用函数 alloc 来获取栈上的内存。同理,这块内存是不需要我们释放的。

    • 使用 alloc 分配的内存跟通过定义变量获取的内存有什么不同呢?

    我们看看下面 2 段程序及其结果:


    int main(){    int n = 0;     char* p = NULL;     for(int i = 0; i < 1024; i++)    {        p = (char*)alloca(1024*5);     }    pid_t pid = getpid();    printf("pid:%d\n",pid);    pause();    return 0;}


    不赋值

    #include #include #include  #include  #include #include  #include 
    int main(){ int n = 0; char* p = NULL; for(int i = 0; i < 1024; i++) { p = (char*)alloca(1024*5); memcpy(p, "123", 4); } pid_t pid = getpid(); printf("pid:%d\n",pid); pause(); return 0;}


    复制

    可以看到,复制前后我们使用的物理内存完全不一样,也就是说使用 alloc 开辟的内存未必就是物理内存,只有在复制后产生 缺页异常 后才能获取内存

    再看看 通过变量获取内存,同理还是看看 2 段程序:

    int main(){    int n = 0;     char p[1204*1024*5];     pid_t pid = getpid();    printf("pid:%d\n",pid);    pause();    return 0;}


    非初始化赋值

    #include #include #include  #include  #include #include  #include 
    int main(){ int n = 0; char p[1204*1024*5] = {0}; pid_t pid = getpid(); printf("pid:%d\n",pid); pause(); return 0;}


    初始化赋值

    可见,通过定义变量获取的内存也是需要对其进行赋值才会有物理内存产生。

    可见也就是说栈的申请不会通过系统调用来获取物理内存。而是随着压栈的操作,栈顶指针访问不存在的物理内存后发生 缺页异常 从而获取内存。因为没有通过系统调用,所以这样的内存获取也比较快捷和方便

    综上所述,所以按照笔者的理解 两者应该是没有区别。

    2.2.2 栈释放内存

    那么我们在栈上面申请的物理内存是如何释放的,我们通过一个代码片段看一看:

    int num=10000;   int func() {     num--;     if(num == 0)     {             return 0;     }     func(); }int main(){    func();    pid_t pid = getpid();    printf("pid:%d\n",pid);    pause();    return 0;}


    递归栈使用情况

    我们发现:在函数退出后,栈依旧没被释放掉,还是被进程抢占着。这个发现似乎有点惊讶,我们以为的是在函数退出后,物理内存会被释放掉返回给系统。这意味着 栈内存的使用只会增加不会减少。

    毕竟在栈的内存并不通过系统调用,有触发申请跟回收的事件。但栈只是通过 缺页异常 这种硬件异常来获取内存,且我们并没有合适的时间来让栈进行释放。但是这样做的好处就是函数使用栈的时候不用频繁使用内存,对于使用频繁的函数来说这样的效率会更加高。

    2.2.3 栈内存总结

    • 尽量避免在使用频率低的栈空间申请大量内存

    • 尽量避免使用递归函数

    最后附上一张 函数栈帧结构图

    函数栈帧结构图


    三、ELF文件瘦身

    3.1 ELF文件介绍

    ELF文件 是 linux 下的 可执行文件格式,包括 可定位文件(.o)、静态库(.a)、共享库(.so) 和 核心转储文件(core dump)等。
    那么在查看 ELF文件 时我们有 2 种角度来查看:

    • 链接视图:将 ELF文件 中所需要保存的信息按照信息的类型、格式的不同,分别保存在文件中不同的 节区(section) 。为了访问这些 section,在 ELF文件 中又包含了一个 section 位置的索引,我们称这个索引为 节区头部表(section headers)

    • 执行视图:按照运行时的需要,执行视图把 section 按照规则分类,并且划分为不同的 段(segment)。为了说明 segment 和 section 的关系,在 ELF 文件中又引入了 程序头部表(program headers),又称为 段表。

    我们可以看看 section 和 segment 之间的关系,如下图所示:

    链接视图与执行视图比较


    3.1.1 ELF文件头分析

    我们使用 readelf -h 来查看 ELF文件头。
    ELF文件头 各个字段的含义可以查看文章 《linux应用程序——ELF查看工具》。我们主要简单看一下需要注意的地方

    • Magic:该字段的值为 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00

    1. 7f  45  4c  46 表明该文件是 ELF文件,

    2. 01 表明该文件运行在32位的操作系统上

    3. 01表明数据采用小端排序,高位在前

  • Size of this header:ELF文件头 大小,值为 52

  • Start of program headers:注意这个字段的值为 52,即 程序头部表(program header) 在 ELF文件 中的位置位 52,而 ELF文件头 大小的值也是为 52,说明 程序头部表 在文件中紧挨着 ELF文件头。

  • 3.1.2 ELF文件节区信息

    使用 readelf -S 查看文件的节区信息,如下图所示

    节区信息


    其输出细节可以查看《linux应用程序——ELF查看工具》,这里我们看常用的节区:

    • .hash:符号哈希表

    • .dynsym:动态链接符号表

    • .dynstr:动态链接字符串表

    • .rel.dyn:节区中包含了重定位信息

    • .rel.plt:节区中包含了重定位信息

    • .init:此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始 调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码

    • .plt:此节区包含过程链接表(procedure linkage table)

    • .text:此节区包含程序的可执行指令。

    • .fini:此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行 这里的代码。

    • .rodata:这些节区包含只读数据,这些数据通常参与进程映像的只读代码段。

    • .init_array:进程初始化的所运行的函数指针数组

    • .fini_array:进程退出时的所运行的函数指针数组

    • .dynamic:此节区包含动态链接信息。

    • .got:此节区包含全局偏移表,其与 plt 一起协作完成符号的动态查找

    • .data:节区包含初始化了的数据,将出现在程序的内存映像中。

    • .bss:包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这 些数据初始化为 0。此节区不占用文件空间。

    • .comment:符号调试信息

    • .debug_aranges:符号调试信息

    • .debug_pubnames:符号调试信息

    • .debug_info:符号调试信息

    • .debug_abbrev:符号调试信息

    • .debug_line:符号调试信息

    • .debug_frame:符号调试信息

    • .debug_str:符号调试信息

    • .note.gnu.arm.ide:符号调试信息

    • .debug_ranges:符号调试信息

    • .shstrtab:节区包含 节区名称

    • .symtab:节区包含一个 符号表

    • .strtab:节区包含一个 字符串表

    Flags属性 为 A 和 AX 的节,在 ELF文件 的分布是连续的,中间没有穿插 AW 或 不需要内存 的节。同样地,所有属性为 AW 的节都顺序的排列在一起。这是为进程运行时划分为 只读代码段 和 可读写数据段 奠定基础。

    3.1.3 ELF文件段信息

    使用 readelf -l 查看文件的段信息,如下图所示

    段信息


    其输出细节可以查看《linux应用程序——ELF查看工具》,这里我们来分析其中常用的段:

    • PHDR:其偏移值为 0x34,十进制为 52,正好是 程序头部表 在 ELF文件 中的位置

    • INTERP:该段给出了运行该文件所需要的解释器,如图所示,该程序使用的解释器为 ld-linux-armhf.so.3。对 ELF文件 来讲,loader的作用是加载 ELF文件 到内存后做 符号解析,直到把程序指针返还给进程

    • LOAD:段的内容将会被加载到内存中,可以根据权限来判断是代码段还是数据段。权限为 只读、可执行 ,该段对应于 代码段,权限为 可读、可写 的,对应于我们常说的 数据段。

    • DYNAMIC:该段给出了此 ELF文件 所需要的 动态链接信息。

    再看看下面的 节与段的映射关系,可以看到我们一共有 9 个段,而下面的映射也将一些节区分别映射到了这 9 个段中,有些节区没有映射进来是因为不需要参与到程序的运行。同时可以看到,在 代码段 中的各个 节区 在文件中是 顺序排列 的;数据段 也是一样。这是因为在程序运行前期,loader 会将 ELF文件 的 代码段 和 *数据段 使用 mmap 将其映射到内存中,这就要求各段所包含的 节区 在文件中必须是连续的。

    3.1.4 ELF文件动态链接信息

    使用 readelf -d 查看文件的动态信息,如下图所示:

    动态链接信息


    其中 Type 属性为 NEEDEN 的代表程序依赖该 动态库,这样 loader 就知道加载哪些动态库到内存中来支持我们的程序。

    3.1.5 ELF文件瘦身

    在知晓了 ELF文件 的一些构成后,我们可以使用 strip 工具来删除 ELF文件 一些没有用的节区。
    比如先使用 strip 工具直接对程序进行删减:

    strip


    我们还可以加入选项 --remove-section 来指定要删除的节区,比如 comment节区 我们不需要,我们可以使用 strip --remove-section=.comment,如下所示:

    删除指定段

    注意:图中使用 交叉编译链 是因为 strips 无法识别 ARM 平台的程序。

    四、数据段及代码段优化

    4.1 数据段说明

    在我们的程序中,与 数据段 相关的节区有:

    • .init_array

    • .fini_array

    • .dynamic

    • .got

    • .data

    • .bss

    各个节区的作用我们之前已经有简单说过了,下面我们重点关注 .data 和 .bss 段。他们 2 者的作用如下:

    • .bss:主要用来保存 未初始化 或 初始化为 0 的全局变量和静态变量。

    • .data: 主要用来保存初 始化不为 0 的全局变量或静态变量。

    他们之间显著的区别就是变量是否 初始化不为 0:

    • 因为初始化不为 0 ,所以程序需要记录他们的初始值。这样就需要在程序中开辟空间来记录他们的值

    • 初始化为 0 ,则将这些变量所在的段映射到一个全 0 的页面即可,所以 .bss段 不占空间

    我们先看看 初始化为 0 的实例代码,如下所示的:

    int bss_array[1024 * 1024] = {0}; int main(int argc, char* argv[]) {    pause();     return 0; }


    bss段大小

    maps

    readelf

    初始化不为 0 的实例代码:

    int data_array[1024 * 1024] = {1}; int main(int argc, char* argv[]) {    pause();     return 0; }


    data段大小

    maps

    readelf

    两段代码都在程序中开辟了 4M大小 的数组,他们的区别如下:

    • 初始化为0:

    1. 其 maps 属性中,堆区 的范围是经过拓展的,是大于 4M的

    2. readelf 读取出来的都是数据段大小不符合 4M大小 的要求

  • 初始化不为0:

    1. 其 maps 属性中,堆区 的范围并没有经过拓展。

    2. maps 属性中的 数据段 大小符合 4M 的要求

    3. readelf 读取出来的都是数据段大小符合 4M大小 的要求

    Loader 在处理数据段时,其首先根据 FileSiz 的大小来创建 数据段。初始化为 0 进程的数据段只有 4k,而 初始化不为 0进程的数据段就有 4M 多。初始化为 0 进程为了容纳 bss段 中 4M大小 的数据,loader 在进程的 堆段 中申请出足够的内存来容纳它。所以我们发现在 初始化为 0 进程中,虽然我们没有申请内存,但却有了一个 4M 的堆段。初始化不为 0进程则是数据段就直接开辟为 4M大小 以供程序使用

    某种程度上来说,使用 bss段 自动使用了 malloc 来帮我们自动开辟内存了,所以在我们访问此段时是不会产生 缺页异常 的。而使用 data段 则会触发并分配物理内存

    以上是只有 bss段 和 data段 的情况,如果同时有 bss段 和 data段,代码如下:

    #include #include #include  #include  #include #include  #include  int data_array[1024*4 - 2] = {1}; int bss_array[4] = {0};int main(int argc, char* argv[]) {    pid_t pid = getpid();    printf("pid:%d\n",pid);       printf("data_array = %p, bss_array = %p\n", data_array, bss_array);    pause();     return 0; }


    运行结果

    maps

    我们发现 2 个不同段的数组,其地址居然都在数据段。在这种情况下, loader 将 数据段 中的 .data 节进行填充后,使用 .bss 节数据对 剩余的字节进行填充,并将这些剩余的字节全部填充为 0,这同时会造成对后一个页面的 写操作,从而产生 dirty page。

    4.2 数据段优化

    关于 数据段 的优化,并不是针对进程本身,而是针对 动态库。如果我们在编写动态库时能尽量优化 数据段 ,那么可以节省比较多的内存空间。这里有一些方法步骤可供参考:

    • 尽可能的减少全局变量和静态变量。我们可以使用“nm”来列出所有在.data 和.bss 节的变量

    • 将 只读的全局变量,加上 const,从而使其转移到 代码段。因为代码段的共享特性,可以节省内存。

    • 对频繁引用的字符串可以将其定义在 代码段
      *不要在 头文件 定义变量(但可以声明,比如使用 extern 声明模块内部的变量),除了能够有效减少编译错误外还能减少因为多个重复引用而浪费内存。

    五、代码段

    代码段在内存优化方面作用并不大,因为代码段是整个进程共享的,而且在内存不足的时候会回收。这里简要地说几点重要的。

    5.1 删除冗余代码

    我们尽可能的删除不必要的代码及变量,因为冗余代码有可能会导致物理内存的使用增加。比如我们定义一变量,却不使用它。在程序运行的时候,因为这种变量的存在,可能会导致 缺页异常 发生的概率增加,因此进程的运行效率会下降。下面 2 个编译选项有助于显示此类冗余代码:

    • -Wunused:检查无用代码

    • --Wunreachable-code :检查从未使用的代码

    5.2 使用 Thumb指令

    我们知道 Thumb指令 是一种指令高级密集的指令集,它与 ARM指令集 之间的关系大致如下:

    • 在功能相同的情况下,Thumb指令集 比 ARM指令集 占用的内存空间小,因为它是一种 16位 的指令集。

    • 所有的 ARM指令 多有对应的 Thumb指令,两者在一定的情况下可以互相调用。

    • 大多数 Thumb指令 是无条件执行的,而大多数 ARM指令 是有条件执行的

    高密度就以为在同等功能的情况下,Thumb指令集 要使用更多的指令来完成功能,从而有可能导致运行的时间比较长,有说法 Thumb指令集 和 ARM指令集 之间的效率关系如下:

    • Thumb 代码所需的存储空间约为 ARM 代码的 60%-70%

    • Thumb 代码使用的指令数比 ARM 代码多约 30%-40%

    • 若使用 32位 的存储器,ARM 代码比 Thumb 代码 快约 40%

    • 若使用 16位 的存储器,Thumb 代码比 ARM 代码快约 40%-50%

    • 与 ARM 代码相比较,使用 Thumb 代码,存储器的 功耗 会降低约 30%

    综上所述,如果系统的 性能 有较高要求,应应该使用 32位存储系统 和 ARM指令集。如果系统的 成本及功耗有较高要求,则应使用 16位存储系统 和 Thumb指令集。

    我们可以添加编译选项 -mthumb ,让编译器使用 Thumb指令 来编译程序。

    当然了,在一些极端场合,可能我们不得不同时使用 ARM指令集 和 Thumb指令集,编译器是支持同时使用 2 种指令集的。但是有些情况是只有在 ARM状态下才能执行

    • 使用或者禁止异常中断 只能在 ARM状态 下完成

    • ARM处理器 总是从 ARM状态 开始执行。按照笔者理解 main函数 所在的文件需要用 ARM指令集编译

    下面是笔者的例子,与 《嵌入式Linux内存与性能详解》 描述不符,但作为笔记还是记下来。

    /* thumb.c */#include void func_thumb(){    printf("I'm thunmb\n");}
    /* arm.c */#include extern void func_thumb();void func_arm(){ func_thumb(); printf("I'm arm\n");}
    int main(){ func_arm(); }


    第一次分别使用下面的指令进行编译:

    arm-linux-gnueabihf-gcc  -mthumb -o thumb.o -c thumb.carm-linux-gnueabihf-gcc  -o arm.o -c arm.carm-linux-gnueabihf-gcc  -o arm_and_thumb arm.o thumb.o

    然后在使用 objdump 反编译 arm_and_thumb,返现他们之间的调用并没有 Thumb状态 和 **ARM状态之间切换,其汇编如下图所示:

    反汇编


    可以看到在各个函数之间的跳转并没使用 bx 或者 blx 这样的会更改状态的指令
    注:关于 bx 或者 blx 请各位读者自行查阅资料

    在笔者经过一番查找后发现有一个编译选项 -mthumb-interwork,其意义是 生成的目标文件,允许在ARM和Thumb之间交叉调用。
    第一次分别使用下面的指令进行编译:

    arm-linux-gnueabihf-gcc -mthumb-interwork -mthumb -o thumb.o -c thumb.c
    arm-linux-gnueabihf-gcc -mthumb-interwork -o arm.o -c arm.c
    arm-linux-gnueabihf-gcc -mthumb-interwork -o arm_and_thumb arm.o thumb.o

    反编译之后情况依旧相同,与书中描述不符。这里笔者猜想可能是编译器不支持该选项,如果有读者可以解答该问题,还请不吝赐教。

    六、参考链接

    《嵌入式Linux内存与性能详解》
    malloc_chunk边界标记法和空间复用https://blog.csdn.net/sim120/article/details/39373229
    对于GNU编译器中-mthumb-interwork和-mthumb的理解https://blog.csdn.net/moqingxinai2008/article/details/53909051

    来源:https://www.jianshu.com/p/bc61df40d85d

       ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

    推荐阅读

    【1】jetson nano开发使用的基础详细分享

    【2】Linux开发coredump文件分析实战分享

    【3】CPU中的程序是怎么运行起来的 必读

    【4】cartographer环境建立以及建图测试

    【5】设计模式之简单工厂模式、工厂模式、抽象工厂模式的对比

    本公众号全部原创干货已整理成一个目录,回复[ 资源 ]即可获得。



    羽林君 某嵌入式程序猿分享技术、生活、人生云云文字。如有诗云:去年今日此门中,人面桃花相映红。人面不知何处去,桃花依旧笑春风。
    评论
    • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
      知白 2025-01-07 15:02 66浏览
    • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
      GIRtina 2025-01-07 11:02 63浏览
    •     为控制片内设备并且查询其工作状态,MCU内部总是有一组特殊功能寄存器(SFR,Special Function Register)。    使用Eclipse环境调试MCU程序时,可以利用 Peripheral Registers Viewer来查看SFR。这个小工具是怎样知道某个型号的MCU有怎样的寄存器定义呢?它使用一种描述性的文本文件——SVD文件。这个文件存储在下面红色字体的路径下。    例:南京沁恒  &n
      电子知识打边炉 2025-01-04 20:04 98浏览
    • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
      华普微HOPERF 2025-01-06 17:23 141浏览
    • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
      华普微HOPERF 2025-01-06 15:29 125浏览
    • 随着市场需求不断的变化,各行各业对CPU的要求越来越高,特别是近几年流行的 AIOT,为了有更好的用户体验,CPU的算力就要求更高了。今天为大家推荐由米尔基于瑞芯微RK3576处理器推出的MYC-LR3576核心板及开发板。关于RK3576处理器国产CPU,是这些年的骄傲,华为手机全国产化,国人一片呼声,再也不用卡脖子了。RK3576处理器,就是一款由国产是厂商瑞芯微,今年第二季推出的全新通用型的高性能SOC芯片,这款CPU到底有多么的高性能,下面看看它的几个特性:8核心6 TOPS超强算力双千
      米尔电子嵌入式 2025-01-03 17:04 55浏览
    • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
      优思学院 2025-01-06 12:03 113浏览
    • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
      知白 2025-01-06 12:04 167浏览
    • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
      GIRtina 2025-01-06 11:10 103浏览
    • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
      hai.qin_651820742 2025-01-07 14:52 40浏览
    • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
      丙丁先生 2025-01-07 09:25 80浏览
    • 自动化已成为现代制造业的基石,而驱动隔离器作为关键组件,在提升效率、精度和可靠性方面起到了不可或缺的作用。随着工业技术不断革新,驱动隔离器正助力自动化生产设备适应新兴趋势,并推动行业未来的发展。本文将探讨自动化的核心趋势及驱动隔离器在其中的重要角色。自动化领域的新兴趋势智能工厂的崛起智能工厂已成为自动化生产的新标杆。通过结合物联网(IoT)、人工智能(AI)和机器学习(ML),智能工厂实现了实时监控和动态决策。驱动隔离器在其中至关重要,它确保了传感器、执行器和控制单元之间的信号完整性,同时提供高
      腾恩科技-彭工 2025-01-03 16:28 170浏览
    • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
      Industio_触觉智能 2025-01-06 10:43 87浏览
    • PLC组态方式主要有三种,每种都有其独特的特点和适用场景。下面来简单说说: 1. 硬件组态   定义:硬件组态指的是选择适合的PLC型号、I/O模块、通信模块等硬件组件,并按照实际需求进行连接和配置。    灵活性:这种方式允许用户根据项目需求自由搭配硬件组件,具有较高的灵活性。    成本:可能需要额外的硬件购买成本,适用于对系统性能和扩展性有较高要求的场合。 2. 软件组态   定义:软件组态主要是通过PLC
      丙丁先生 2025-01-06 09:23 83浏览
    • 光耦合器,也称为光隔离器,是一种利用光在两个隔离电路之间传输电信号的组件。在医疗领域,确保患者安全和设备可靠性至关重要。在众多有助于医疗设备安全性和效率的组件中,光耦合器起着至关重要的作用。这些紧凑型设备经常被忽视,但对于隔离高压和防止敏感医疗设备中的电气危害却是必不可少的。本文深入探讨了光耦合器的功能、其在医疗应用中的重要性以及其实际使用示例。什么是光耦合器?它通常由以下部分组成:LED(发光二极管):将电信号转换为光。光电探测器(例如光电晶体管):检测光并将其转换回电信号。这种布置确保输入和
      腾恩科技-彭工 2025-01-03 16:27 180浏览
    我要评论
    0
    点击右上角,分享到朋友圈 我知道啦
    请使用浏览器分享功能 我知道啦