0%

变长参数

在c语言中使用变长参数最常见的就是下面两个函数了:

int printf(const char *format, ...);
int scanf(const char *format, ...);

在使用变长参数的函数(这里假设是func)实现部分其实用到了stdarg.h里面的多个宏来访问那些不确定的参数,它们分别是:

void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);

假设lastarg是func的最后一个具名参数,即在func函数定义中…之前的那个参数(在printf中lastarg是format),
在func中首先定义一个变量:

va_list ap

这个变量以后会依次指向各个可变参数。ap在使用之前必须用宏va_start初始化一次,如下所示:

va_start(ap, lastarg);

其中lastarg是func中的最后一个具名参数。然后就可以用va_arg来获得下一个不定参数(前提是知道这个不定参数的类型type):

type next = va_arg(ap, type)

最后就是用宏va_end来清理现场。例子:

void _printf(char *format, ...)                                                                                               
{
    #define BUFFER_LEN  256
    char Buf[BUFFER_LEN];
    va_list args;
    memset(Buf,0, BUFFER_LEN);

    va_start(args, format);
    vsnprintf(Buf, BUFFER_LEN, format, args);
    va_end(args);

    printf("%s", Buf);
}

另一个例子:

#include <stdio.h>
#include <stdarg.h>

void func(char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);

    while (*fmt)
    {
        switch(*fmt)
        {
            case 'd':
                fprintf(stdout, "%d\n", (int)va_arg(ap, int));
                break;
            case 'c':
                fprintf(stdout, "%c\n", (char)va_arg(ap, int));
                break;
            case 's':
                fprintf(stdout, "%s\n", (char *)va_arg(ap, char *));
                break;
            default:
                fprintf(stderr, "error fmt\n");

        }
        fmt ++;
    }
    va_end(ap);
}

int main ( int argc, char *argv[]  )
{
    func("dcs", 10, 's', "hello");
        return 0;

}               /* ----------  end of function main  ---------- */

函数参数的传递原理

函数参数是以数据结构:栈的形式存取,从右至左入栈。

首先是参数的内存存放格式:参数存放在内存的堆栈段中,在执行函数的时候,从最后一个开始入栈。
因此栈底高地址,栈顶低地址,举个例子如下:

void func(int x, float y, char z);

那么,调用函数的时候,实参 char z 先进栈,然后是 float y,最后是 int x,
因此在内存中变量的存放次序是 x->y->z,
因此,从理论上说,我们只要探测到任意一个变量的地址,
并且知道其他变量的类型,通过指针移位运算,则总可以顺藤摸瓜找到其他的输入变量。
va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。

  1. 在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
  2. 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,
    这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“…”之前的那个参数;
  3. 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,
    然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
  4. 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,
    他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。
    通常va_start和va_end是成对出现。

默认编译

默认编译环境下.c文件会编译为.o文件,当链接时会链接.o文件,整个.o文件为一个section,当此section存在无用函数时会造成控件浪费。

gcc -o main main.c x.o

可以使用命令来查看二进制可执行文件中的符号表,所有函数都链接进去:

nm -S main
readelf -S x.o

GCC链接时,按照section来链接,不论section中符号是否都使用到!

拆分默认section

-ffunction-sections (为每个function函数分配独立的section)
-fdata-sections (为每个data item数据项分配独立的section)

gcc -c -ffunction-sections -fdata-sections x.c x.o

x.o大小会明显变大,因为section大量增加。

排除不链接section -Wl,–gc-sections

-Wl,的意思是将后面的内容传递给链接器
--gc-sections是链接器参数,不链接未使用的section

gcc -Wl,--gc-sections -o main main.c x.o

可以保证代码量最优。

align属性

align属性不仅可以修饰变量,类型, 还可以修饰函数, 举个例子:

修饰变量

int a = 0;

int main(void)
{
    printf("a = %d\n", a);
}

用gdb看下变量a的地址:

(gdb) p/x &a
$1 = 0x60087c

变量a地址是编译器随意生成的, 这个例子碰巧分配到了8字节对齐的位置上。
用align属性修饰下:

int a __attribute__((aligned(8))) = 0;

(gdb) p/x &a
$1 = 0x600880

编译器会把变量a生成在8字节对齐的内存地址上。

修饰类型

int a __attribute__((aligned(8))) = 0;

struct test {
    int a;
} __attribute__((aligned(8)));

struct test aa;

int main(void)
{
    printf("a = %d\n", a);
}

经过align修饰后,struct test数据结构定义的所有变量都会出现在8字节对齐的内存上。

(gdb) p/x &aa
$1 = 0x600888

修饰函数

void test1(void) __attribute__((aligned(2)));

void  test1(void)
{
    printf("hello, world.\n");
}

int main(void)
{
    test1();
}

经过align修饰后,函数出现在8字节对齐的内存上。

(gdb) p test1 
$1 = {<text variable, no debug info>} 0x400538 <test1>

always_inline属性

gcc有个inline关键字,可以将一个函数定义为内嵌形式:

static inline void test2(void);

void test1(void)
{
    printf("hehe.\n");
}

void test2(void)
{
    asm("nop");
}

void test(void)
{
    test1();
    test2();
}

int main(void)
{
    test();
}

按照inline的定义, test在调用test2函数的时候, 会直接将test2函数的代码展开在test函数内部,
这样就减少了一次函数调用过程, 加快了代码的执行速度, 用gdb反汇编看下:

(gdb) disass test
Dump of assembler code for function test:
0x00000000004004a8 :    push   %rbp
0x00000000004004a9 :    mov    %rsp,%rbp
0x00000000004004ac :    callq  0x400498 
0x00000000004004b1 :    callq  0x4004b8 
0x00000000004004b6 :   leaveq
0x00000000004004b7 :   retq
End of assembler dump.

你可以看到,gcc并没有把test2函数展开,明明已经使用inline修饰了。
因为虽然用inline做了修饰,但是gcc会根据代码的逻辑来优化到底有没有必要使用inline代码。
像这种简单的代码,用inline代码效果不大,因此gcc并没有按照inline要求来生成代码。

static inline void test2(void) __attribute__((always_inline));
加入always_inline属性后呢?
(gdb) disass test
Dump of assembler code for function test:
0x00000000004004a8 :    push   %rbp
0x00000000004004a9 :    mov    %rsp,%rbp
0x00000000004004ac :    callq  0x400498 
0x00000000004004b1 :    nop
0x00000000004004b2 :   leaveq
0x00000000004004b3 :   retq
End of assembler dump.

这次你会看到在调用完test1后, 直接把test2的代码, 也是nop语句加入在了test函数内。

constructor & destructor

很犀利的2个属性,用于修饰某个函数,经过constructor属性修饰过的函数,
可以在main函数运行前就可以先运行完毕,同理destructor在进程exit之前执行。

#include<stdio.h>

void test1(void) __attribute__((aligned(8)));

void  test1(void)
{
    printf("hello, world.\n");
}

void __attribute__((constructor)) test2(void)
{
    printf("hehe.\n");
}

void __attribute__((destructor)) test3(void)
{
    printf("haha.\n");
}

int main(void)
{
    test1();
}

执行输出如下:

➜  /data/test/1  > ./main 
hehe.
hello, world.
haha.

fastcall & regparm属性

在c语言中,通过函数传递参数通常使用堆栈的方式,如:
test(a, b, c);
参数从右到左依次压入堆栈c, b, a。
函数执行完后,还要把这3个参数从堆栈中弹出来,如果一个函数每秒钟有上万次调用,
这将非常耗时,为了加快代码运行速度,gcc扩展了fastcall和regparm2个属性,
对于fastcall属性,一个函数的前2个参数分别通过ecx和edx来传递,剩下的则是使用堆栈来传递。
对于regparm,它的用法如下regparm(n), 函数的1到n个参数,分别通过eax, edx, ecx来传递,最多就使用3个寄存器,
其余参数通过堆栈来传递。注意这2个属性只在x86平台有效。很对编译平台fastcall默认开启。

int __attribute__((fastcall)) test1(int a, int b)
{
    return a + b;
}

int __attribute__((regparm(2))) test2(int a, int b)
{
    return a + b;
}

int test3(int a, int b)
{
    return a + b;
}

int main(void)
{
    test1(1, 2);
    test2(1, 2);
    test3(1, 2);
}

(gdb) disass test1
Dump of assembler code for function test1:
0x08048354 :   push   %ebp
0x08048355 :   mov    %esp,%ebp
0x08048357 :   sub    $0x8,%esp 在堆栈里先分配了8个字节的空间
0x0804835a :   mov    %ecx,-0x4(%ebp) 将ecx的值放入第一个变量里
0x0804835d :   mov    %edx,-0x8(%ebp) 将edx的值放入第二个变量里
0x08048360 :  mov    -0x8(%ebp),%eax
0x08048363 :  add    -0x4(%ebp),%eax
0x08048366 :  leave
0x08048367 :  ret
End of assembler dump.

(gdb) disass test2
Dump of assembler code for function test2:
0x08048368 :   push   %ebp
0x08048369 :   mov    %esp,%ebp
0x0804836b :   sub    $0x8,%esp
0x0804836e :   mov    %eax,-0x4(%ebp)
0x08048371 :   mov    %edx,-0x8(%ebp)
0x08048374 :  mov    -0x8(%ebp),%eax
0x08048377 :  add    -0x4(%ebp),%eax
0x0804837a :  leave
0x0804837b :  ret
End of assembler dump.

(gdb) disass test3
Dump of assembler code for function test3:
0x0804837c :   push   %ebp
0x0804837d :   mov    %esp,%ebp
0x0804837f :   mov    0xc(%ebp),%eax 堆栈形式来传递参数,0×c(%ebp)保存第二个参数
0x08048382 :   add    0x8(%ebp),%eax 堆栈形式来传递参数,0×8(%ebp)保存第一个参数
0x08048385 :   pop    %ebp
0x08048386 :  ret
End of assembler dump.

packed属性

用于修饰struct, union, enum数据结构,看如下的例子:

struct test {
    char a;
    int b;
};

struct test1 {
    char a;
    int b;
}__attribute__((packed));

int main(void)
{
    printf("%d, %d\n", sizeof(struct test), sizeof(struct test1));
}

struct test结构,理论来说一共有1+4=5字节的大小,但是gcc默认编译出来的大小是8,也就是说char是按照4字节来分配空间的。
加上packed修饰后,就会按照实际的类型大小来计算。

➜  /data/test/1  > ./main 
8, 5

section属性

gcc编译后的二进制文件为elf格式,代码中的函数部分会默认的链接到elf文件的text section中,
变量则会链接到bss和data section中。如果想把代码或变量放到特定的section中,就可以使用section属性来修饰。

int __attribute__((section("TEST"))) test1(int a, int b)
{
    return a + b;
}

int test2(int a, int b)
{
    return a + b;
}

int main(void)
{
    test1(1, 2);
    test2(1, 2);
}

使用readelf来观察下test二进制格式。

➜  /data/test/1  > readelf -S main
[13] .text             PROGBITS         0000000000400400  00000400
00000000000001a2  0000000000000000  AX       0     0     16
[14] TEST              PROGBITS         00000000004005a2  000005a2
0000000000000014  0000000000000000  AX       0     0     1

文件多出了一个TEST section,它的起始地址为0x4005a2, 大小为0x14, 它的地址范围在 0x4005a2 – 0x4005b6。
text section的起始地址为0x400400, 大小为0x1a2, 它的地址范围在0x400400 – 0x4005a2。

在来看下test1, test2符号表的地址:

➜  /data/test/1  > readelf -s main
57: 00000000004005a2    20 FUNC    GLOBAL DEFAULT   14 test1
68: 00000000004004f6    20 FUNC    GLOBAL DEFAULT   13 test2

可以看到test2确实被链接在text section中, 而test1链接在TEST section中。

PS

更多关于gcc attribute的介绍请看gcc手册

#pragma pack(n)和__attribute__((aligned(m)))的区别:

前者告诉编译器结构体或类内部的成员变量相对于第一个变量的地址的偏移量的对齐方式,
缺省情况下,编译器按照自然边界对齐,当变量所需的自然对齐边界比n大时,按照n对齐,
否则按照自然边界对齐;
后者告诉编译器一个结构体或者类或者联合或者一个类型的变量(对象)分配地址空间时的地址对齐方式。
也就是说,如果将__attribute__((aligned(m)))作用于一个类型,那么该类型的变量在分配地址空间时,
其存放的地址一定按照m字节对齐(m必须是2的幂次方)。
并且其占用的空间,即大小,也是m的整数倍,以保证在申请连续存储空间的时候,
每一个元素的地址也是按照m字节对齐。 __attribute__((aligned(m)))也可以作用于一个单独的变量。举例说明:

#include<stdio.h>
#pragma pack(4)

typedef struct{
    uint32_t f1;
    uint8_t f2;
    uint8_t f3;
    uint32_t f4;
    uint64_t f5;
}__attribute__((aligned(1024))) ts;

int main()
{
    printf("Struct size is: %d, aligned on 1024\n",sizeof(ts));
    printf("Allocate f1 on address: 0x%x\n",&(((ts*)0)->f1));
    printf("Allocate f2 on address: 0x%x\n",&(((ts*)0)->f2));
    printf("Allocate f3 on address: 0x%x\n",&(((ts*)0)->f3));
    printf("Allocate f4 on address: 0x%x\n",&(((ts*)0)->f4));
    printf("Allocate f5 on address: 0x%x\n",&(((ts*)0)->f5));
    return 0;
}

输出:

Struct size is: 1024, aligned on 1024
Allocate f1 on address: 0x0
Allocate f2 on address: 0x4
Allocate f3 on address: 0x5
Allocate f4 on address: 0x8
Allocate f5 on address: 0xc

注意:

绿色部分表明了__attribute__((aligned(1024))) 的作用

红色部分说明#pragma pack(4)只对大小大于4的成员变量的地址偏移起作用

紫色部分说明对于大小大于4的成员变量,其地址偏移按照4字节对齐

  • toc
    {:toc}

Cppcheck

Cppcheck is a static analysis tool for C/C++ code.
Unlike C/C++ compilers and many other analysis tools it does not detect syntax errors in the code.
Cppcheck primarily detects the types of bugs that the compilers normally do not detect.
The goal is to detect only real errors in the code (i.e. have zero false positives).

  1. 官网
  2. documentation

安装:

sudo apt-get install cppcheck cppcheck-gui

用法:

cppcheck-gui

cppcheck -h

# Recursively check the current folder. Print the progress on the screen and
# write errors to a file:
cppcheck . 2> err.txt

# Recursively check ../myproject/ and don't print progress:
cppcheck --quiet ../myproject/

# Check test.cpp, enable all checks:
cppcheck --enable=all --inconclusive --std=posix test.cpp

# Check f.cpp and search include files from inc1/ and inc2/:
cppcheck -I inc1/ -I inc2/ f.cpp

Understand

Understand® is an IDE built from the ground up to help you fully comprehend your source code. Analyze it, measure it, visualize it, maintain it - Understand it.

  1. 官网
  2. support
  3. Running Understand on a headless linux server

Understand软件的功能主要定位于代码的阅读理解

具备如下特性:

  1. 支持多语言:Ada, C, C++, C#, Java, FORTRAN, Delphi, Jovial, and PL/M ,混合语言的project也支持
  2. 多平台: Windows/Linux/Solaris/HP-UX/IRIX/MAC OS X
  3. 代码语法高亮、代码折叠、交叉跳转、书签等基本阅读功能。
  4. 可以对整个project的architecture、metrics进行分析并输出报表。
  5. 可以对代码生成多种图(butterfly graph、call graph、called by graph、control flow graph、UML class graph等),
    在图上点击节点可以跳转到对应的源代码位置。
  6. 提供Perl API便于扩展。作图全部是用Perl插件实现的,直接读取分析好的数据库作图。
  7. 内置的目录和文件比较器。
  8. 支持project的snapshot,并能和自家的TrackBack集成便于监视project的变化。

valgrind

  1. valgrind
  2. Alleyoop
  3. Valkyrie
  4. Valgrind User Manual
  5. Valgrind’s Tool Suite
  6. C语言代码检测:valgrind的使用
  7. 应用 Valgrind 发现 Linux 程序的内存问题
  8. 如何使用Valgrind memcheck工具进行C/C++的内存泄漏检测

Valgrind是一套Linux下,开放源代码(GPL V2)的仿真调试工具的集合。
Valgrind由内核(core)以及基于内核的其他调试工具组成。
内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;
而其他工具则类似于插件 (plug-in),利用内核提供的服务完成各种特定的内存调试任务。

它支持 C/C++,支持各类 Unix 系统。

安装:

sudo apt-get install valgrind

工具集:

  • Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,
    能够发现开发中绝大多数内存错误使用情况,
    比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。
  • Callgrind。它主要用来检查程序中函数调用过程中出现的问题。
  • Cachegrind。它主要用来检查程序中缓存使用出现的问题。它与Cachegrind的功能有重叠,但也收集Cachegrind不收集的一些信息。
  • Helgrind。它主要用来检查多线程程序中出现的竞争问题。
  • Massif。它主要用来检查程序中堆栈使用中出现的问题。它有助于使你的程序使用更少的内存。
  • DRD也是一个线程错误检测器。它和Helgrind相似,但使用不同的分析技术,所以可能找到不同的问题。

编译选项使用-g -O0, 交叉编译之后在目标机上执行内存检测:

valgrind --tool=memcheck --leak-check=full ./a.out
==2888== Memcheck, a memory error detector
==2888== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==2888== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==2888== Command: ./val
==2888==

[a]
==2888==
==2888== HEAP SUMMARY:
==2888==     in use at exit: 1 bytes in 1 blocks
==2888==   total heap usage: 1 allocs, 0 frees, 1 bytes allocated
==2888==
==2888== 1 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2888==    at 0x4C274A8: malloc (vg_replace_malloc.c:236)
==2888==    by 0x400575: main (valgrind.c:6)
==2888==
==2888== LEAK SUMMARY:
==2888==    definitely lost: 1 bytes in 1 blocks
==2888==    indirectly lost: 0 bytes in 0 blocks
==2888==      possibly lost: 0 bytes in 0 blocks
==2888==    still reachable: 0 bytes in 0 blocks
==2888==         suppressed: 0 bytes in 0 blocks
==2888==
==2888== For counts of detected and suppressed errors, rerun with: -v
==2888== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)

memcheck可以检测下列与内存相关的问题 :

  • 未释放内存的使用
  • 对释放后内存的读/写
  • 对已分配内存块尾部的读/写
  • 内存泄露
  • 不匹配的使用malloc/new/new[] 和 free/delete/delete[]
  • 重复释放内存

KCachegrind

  1. Callgrind and KCachegrind
  2. Screenshots
  3. Documentation

安装:

sudo apt-get install kcachegrind

valgrind安装之后有工具callgrind,使用此工具生成性能日志,使用kcachegrind进行分析:

/data/test  > gcc -g -O0 main.c -o perf
/data/test  > valgrind --tool=callgrind ./perf 
==27075== Callgrind, a call-graph generating cache profiler
==27075== Copyright (C) 2002-2015, and GNU GPL'd, by Josef Weidendorfer et al.
==27075== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==27075== Command: ./perf
==27075== 
==27075== For interactive control, run 'callgrind_control -h'.
hello world!
hello world!
hello world!
test 
hello world!
test 
hello world!
test 
==27075== 
==27075== Events    : Ir
==27075== Collected : 155985
==27075== 
==27075== I   refs:      155,985
/data/test  > ls
1  a.out  callgrind.out.27075  main.c  output  perf  photorec.ses  recup_dir.1  rgb  testdisk.log
/data/test  > kcachegrind callgrind.out.27075 

Call Graph中,你能看到每一个函数的开销百分比包括Inclusive,即涵带子函数的总开销和Self,函数自己的开销,
另外每一个函数的调用次数也列写在上面。
也可以分析出函数的调用关系和流程,这是一个非常好的辅助工具,帮助你快速理解系统的工作流Work Flow。

gprof

  1. gprof
  2. GPROF Tutorial – How to use Linux GNU GCC Profiling Tool
  3. GNU gprof
  4. 使用 GNU profiler 来提高代码运行速度
  5. GNU–gprof使用总结

Gprof 是GNU gnu binutils工具之一,默认情况下linux系统当中都带有这个工具:

  1. 可以显示“flat profile”,包括每个函数的调用次数,每个函数消耗的处理器时间,
  2. 可以显示“Call graph”,包括函数的调用关系,每个函数调用花费了多少时间。
  3. 可以显示“注释的源代码”--是程序源代码的一个复本,标记有程序中每行代码的执行次数。

Gprof基本用法:

  1. 使用 -pg 编译和链接你的应用程序。
  2. 执行你的应用程序使之生成供gprof 分析的数据。
  3. 使用gprof 程序分析你的应用程序生成的数据。
gcc gprof.c -pg -o example1 -O0 -g
./example1
✔ /data/test  > ls gmon.out
gmon.out

# flat profile
gprof example1 gmon.out -p
Flat profile:

Each sample counts as 0.01 seconds.
%   cumulative   self              self     total           
time   seconds   seconds    calls  ms/call  ms/call  name    
50.43      0.01     0.01        3     3.36     3.36  count_sum
50.43      0.02     0.01        1    10.09    13.45  my_print2
0.00      0.02     0.00        2     0.00     3.36  my_print

# call graph
gprof example1 gmon.out -q

# gprof - display call graph profile data
man gprof

带注解的源代码

如果希望获得一个 “带注解的源代码” 清单,它会将源代码输出到应用程序中,并加上每个函数被调用了多少次的注释。
要使用这种功能,请使用启用调试功能的标志来编译源代码,这样源代码就会被加入可执行程序中:

gcc example1.c -g -pg -o example1 -O2 -lc

像以前一样重新运行这个应用程序:

./example1 50000

gprof 命令现在应该是:

gprof example1 gmon.out -A

常用参数

  • -b 不再输出统计图表中每个字段的详细描述。
  • -p 只输出函数的调用图(Call graph的那部分信息)。
  • -q 只输出函数的时间消耗列表。
  • -e Name 不再输出函数Name 及其子函数的调用图(除非它们有未被限制的其它父函数)。可以给定多个 -e 标志。一个 -e 标志只能指定一个函数。
  • -E Name 不再输出函数Name 及其子函数的调用图,此标志类似于 -e 标志,但它在总时间和百分比时间的计算中排除了由函数Name 及其子函数所用的时间。
  • -f Name 输出函数Name 及其子函数的调用图。可以指定多个 -f 标志。一个 -f 标志只能指定一个函数。
  • -F Name 输出函数Name 及其子函数的调用图,它类似于 -f 标志,但它在总时间和百分比时间计算中仅使用所打印的例程的时间。可以指定多个 -F 标志。一个 -F 标志只能指定一个函数。-F 标志覆盖 -E 标志。
  • -z 显示使用次数为零的例程(按照调用计数和累积时间计算)。

用户时间与内核时间

gprof 的最大缺陷:它只能分析应用程序在运行过程中所消耗掉的用户时间,无法得到程序内核空间的运行时间。通常来说,应用程序在运行时既要花费一些时间来运行用户代码,也要花费一些时间来运行 “系统代码”,例如内核系统调用sleep()。

有一个方法可以查看应用程序的运行时间组成,在 time 命令下面执行程序。这个命令会显示一个应用程序的实际运行时间、用户空间运行时间、内核空间运行时间。

✔ /data/test  > time ./example1 
./example1  0.02s user 0.00s system 93% cpu 0.026 total

注意事项

  1. g++在编译和链接两个过程,都要使用-pg选项。
  2. 只能使用静态连接libc库,否则在初始化*.so之前就调用profile代码会引起“segmentation fault”,
    解决办法是编译时加上-static-libgcc或-static。
  3. 如果不用g++而使用ld直接链接程序,要加上链接文件/lib/gcrt0.o,如ld -o myprog /lib/gcrt0.o myprog.o utils.o -lc_p。
    也可能是gcrt1.o
  4. 要监控到第三方库函数的执行时间,第三方库也必须是添加 –pg 选项编译的。
  5. gprof只能分析应用程序所消耗掉的用户时间.
  6. 程序不能以demon方式运行。否则采集不到时间。(可采集到调用次数)
  7. 首先使用 time 来运行程序从而判断 gprof 是否能产生有用信息是个好方法。
  8. 如果 gprof 不适合您的剖析需要,那么还有其他一些工具可以克服 gprof 部分缺陷,包括 OProfile 和 Sysprof。
  9. gprof对于代码大部分是用户空间的CPU密集型的程序用处明显。
    对于大部分时间运行在内核空间或者由于外部因素(例如操作系统的 I/O 子系统过载)而运行得非常慢的程序难以进行优化。
  10. gprof 不支持多线程应用,多线程下只能采集主线程性能数据。原因是gprof采用ITIMER_PROF信号,在多线程内只有主线程才能响应该信号。
    但是有一个简单的方法可以解决这一问题
  11. gprof只能在程序正常结束退出之后才能生成报告(gmon.out)。
    • gprof通过在atexit()里注册了一个函数来产生结果信息,任何非正常退出都不会执行atexit()的动作,所以不会产生gmon.out文件。
    • 程序可从main函数中正常退出,或者通过系统调用exit()函数退出。

KProf

KProf - Profiling made easy

  1. KProf
  2. download

将gprof , GUI化了,更加友好,它也利用graphviz绘制运行时流程图,但是对于大的程序,似乎效果不太好,流程图感觉完全乱掉了

gprof2dot

  1. gprof2dot
  2. xdot.py
sudo apt-get install python graphviz
git clone https://github.com/jrfonseca/gprof2dot.git
python setup.py build
sudo python steup.py install

用法:

gcc -pg linpack.c -o linpack_gprof
gprof ./linpack_gprof | python2.7 gprof2dot.py -n0 -e0 |dot - Tpng -o output.png

还可通过以下命令得到函数调用图:

gprof linpack_gprof > prof.log  
./gprof2dot.py prof.log | dot -Tpng -o output.png  

通过对比以上两种方法,可以发现第二种方法无法得到调用时间过短的函数调用关系

gprof2dot默认是部分函数调用图,对性能影响不大的函数调用都不显示,例如上图中没有出现类的构造,析构函数,
如果想要显示全部的函数调用,可以 gprof2dot -n0 -e0 ,默认是n0.5即影响小于5%的函数就不显示了。
当然这样图片会很乱,因为显示内容很多,可以 gprof2dot -n0 -e0 -s #-s表示不显示诸如模板,函数入口参数等等,使得
函数名称显示更加精简。

dot用于生成图片,使用xdot.py不需要生成图片,直接交互

gprof ./linpack_gprof | python2.7 gprof2dot.py -n0 -e0 | ./xdot.py

python环境变量设置有问题,xdot单独可以工作,使用管道不能正常工作

function showCall()
{
    gprof $1 | python3 /data/Installer/gprof2dot/gprof2dot.py > /tmp/__call_tmp
    xdot /tmp/__call_tmp
    rm /tmp/__call_tmp
}

gperftools

  1. gperftools
  2. 用gperftools对C/C++程序进行profile
  3. Google performance Tools (gperftools) 使用心得
  4. gperftools使用详解

在Linux的C/C++编程的世界里,性能调优一直是个让人头疼的事。
最出名的gprof虽然大家都知道, 其用法比较单一(只支持程序从启动到结束的profile),
而且对程序的运行时间会有比较大的影响, 所以其profile不一定准确。

valgrind功能十分强大,但profile也一般针对整个程序的运行,很难只对程序运行中的某段时间进行profile。
而且也多少会影响程序的运行,且使用的难度也较大。

如何profile

在gperftools的文档中,就简单的说了下面的方式来进行profile:

gcc [...] -o myprogram -lprofiler
CPUPROFILE=/tmp/profile ./myprogram

是的,在编译和安装了gperftools之后,只需要上面的步骤就可以进行profile了,非常简单。
而profile的结果就保存在/tmp/profile。查看结果只需要用gperftools自带的一个pprof脚本来看就可以:

$ pprof --text ./myprogram /tmp/profile
14   2.1%  17.2%       58   8.7% std::_Rb_tree::find

pprof的输出也很直观,不过也还不够好,从这个输出中还不好看出调用关系,包括caller和callee。
而pprof也可以输出图示,还可以输出callgrind兼容的格式,这样就可以用kcachegrind来看profile结果了。

$ pprof --callgrind ./myprogram /tmp/profile > callgrind.res

然后利用kcachegrind打开这个callgrind.res文件

动态profile

上面说到的方式是通过环境变量来触发profile,而跨度也是整个程序的生命周期。
那如果是想要在程序运行的某段时间进行profile呢?如果我想在程序不结束的情况下就拿到profile的结果呢?

这种情况下就需要用到动态profile的方式了。要实现这种方式,就需要改动程序的代码了,不过也比较简单:

#include <gperftools/profiler.h>

int main()
{
    ProfilerStart("/tmp/profile");
    some_func_to_profile();
    ProfilerStop();

    return 0;
}

没错,你只需要在你想要profile的函数的开头和结尾加上ProfilerStartProfilerStop调用就可以了。
在ProfilerStop结束之后,profile的结果就会保存在/tmp/profile里面了。
利用这种方式就可以在指定的时间点对程序进行profile了。

最后需要说的一点是,gperftools的profile过程采用的是采样的方式,而且对profile中的程序性能影响极小,
这对于在线或者离线profile都是一个极其重要的特点。

include-what-you-use

  1. include-what-you-use
  2. 找出C文件中不必要的头文件
  3. 使用I.W.Y.U整理头文件引用

首先安装CLang开发包,然后编译IWYU:

sudo apt-get install llvm llvm-devel llvm-clang llvm-clang-devel
git clone https://github.com/include-what-you-use/include-what-you-use.git
mkdir build && cd build
cmake -G "Unix Makefiles" -DIWYU_LLVM_ROOT_PATH=/usr/lib/llvm-3.8 ../../include-what-you-use
make

使用:

make -k CC=/data/OpenSourceCode/include-what-you-use/build/include-what-you-use -I/usr/lib/llvm-3.8/lib/clang/3.8.0/include

-e compiling [/data/OpenSourceCode/include-what-you-use/build/include-what-you-use]: ./ca/thinew/app_tnt_baseinfo.c
In file included from ca/thinew/app_tnt_baseinfo.c:19:
In file included from ./include/app_module.h:22:
In file included from ./include/module/app_play_control.h:22:
In file included from /home/workspace/Solution-for-debug/dishcas/solution/../library/goxceed/csky-ecos/include/gxcore.h:4:
In file included from /home/workspace/Solution-for-debug/dishcas/solution/../library/goxceed/csky-ecos/include/common/gxcore_common.h:4:
In file included from /home/workspace/Solution-for-debug/dishcas/solution/../library/goxceed/csky-ecos/include/stdlib.h:70:
/home/workspace/Solution-for-debug/dishcas/solution/../library/goxceed/csky-ecos/include/stddef.h:63:15: fatal error: 
    'stddef.h' file not found
#include_next <stddef.h>
^

ca/thinew/app_tnt_baseinfo.c should add these lines:

ca/thinew/app_tnt_baseinfo.c should remove these lines:
- #include "app_module.h"  // lines 19-19

The full include-list for ca/thinew/app_tnt_baseinfo.c:
#include "app_config.h"  // for TNTCAS_SUPPORT
---
deps:17: recipe for target '/home/workspace/Solution-for-debug/dishcas/solution/output/objects/app_tnt_baseinfo.o' failed
make[1]: *** [/home/workspace/Solution-for-debug/dishcas/solution/output/objects/app_tnt_baseinfo.o] Error 3

fatal error: ‘stddef.h’ file not found待排除,可能与CLang环境设置有关,或与include_next有关
How to Install

可以将 IWYU 保存到文件中,之后使用其附带的 fix_includes.py 自动对代码进行修复。 但应该慎重….

$ make -B -k CXX=include-what-you-use > iwyu.out
$ fix_includes.py < iwyu.out

OCLint

  1. OCLint
  2. The OCLint Static Code Analysis Tool
  3. OCLint 安装与使用
  4. 代码度量:帮助写出高质量代码的工具
  5. oc代码静态检查方案
  6. Building OCLint
  7. Installation
  8. downloading pre-compiled binaries

下载预编译版本,然后将PATH添加到.zshrc中,测试如下:

oclint
oclint: Not enough positional command line arguments specified!
Must specify at least 1 positional arguments: See: oclint -help

如果使用make来构建系统,使用方式如下:

sudo apt-get install bear

#compile_commands.json
bear make 

#oclint-json-compilation-database for code analysis
oclint-json-compilation-database

oclint -p <build path> <source0> [... <sourceN>]

OCLint配置规则

Clang Static Analyzer

  1. Clang Static Analyzer
  2. Building the Analyzer from Source
  3. How to use Clang Static Analyzer
  4. 静态代码分析工具

在编译安装 llvm/clang 或者 sudo apt-get install clang 之后,scan-buildscan-view 分别在

$(SRC)/llvm/tools/clang/tools/scan-build
$(SRC)/llvm/tools/clang/tools/scan-view

使用:

scan-build make
.
scan-build: 3 bugs found.
scan-build: Run 'scan-view /tmp/scan-build-2016-06-23-124956-25166-1' to examine bug reports.
scan-view /tmp/scan-build-2016-06-23-124956-25166-1

checker – 检查规则

内置的 checker 存放在 $(SRC)/llvm/tools/clang/lib/StaticAnalyzer/Checkers 目录下。
这些 checker 默认情况下并没有全部开启,所以需要根据情况启用合适的 checker。
可以使用 -enable-checker-disable-checker 开启和禁用具体的 checker 或者 某种类别的 checker。

$ scan-build -enable-checker alpha.security.ArrayBoundV2 make # 启用数组边界检查

所有支持的 checkers 可以使用如下命令查看:

$ clang -cc1 -analyzer-checker-help
alpha.core.BoolAssignment       Warn about assigning non-{0,1} values to Boolean variables
alpha.core.CastSize             Check when casting a malloced type T, whether the size is a multiple of the size of T
alpha.core.CastToStruct         Check for cast from non-struct pointer to struct pointer
alpha.core.FixedAddr            Check for assignment of a fixed address to a pointer
alpha.core.IdenticalExpr        Warn about unintended use of identical expressions in operators
alpha.core.PointerArithm        Check for pointer arithmetic on locations other than array elements
alpha.core.PointerSub           Check for pointer subtractions on two pointers pointing to different memory chunks
alpha.core.SizeofPtr            Warn about unintended use of sizeof() on pointer expressions
alpha.cplusplus.NewDeleteLeaks  Check for memory leaks. Traces memory managed by new/delete.
alpha.cplusplus.VirtualCall     Check virtual function calls during construction or destruction
...
alpha.security.ArrayBound       Warn about buffer overflows (older checker)
alpha.security.ArrayBoundV2     Warn about buffer overflows (newer checker)
alpha.security.MallocOverflow   Check for overflows in the arguments to malloc()
alpha.security.ReturnPtrRange   Check for an out-of-bound pointer being returned to callers
...
core.CallAndMessage             Check for logical errors for function calls and Objective-C message expressions (e.g., uninitialized arguments, null function pointers)
core.DivideZero                 Check for division by zero
core.DynamicTypePropagation     Generate dynamic type information
core.NonNullParamChecker        Check for null pointers passed as arguments to a function whose arguments are references or marked with the 'nonnull' attribute
core.NullDereference            Check for dereferences of null pointers
core.StackAddressEscape         Check that addresses to stack memory do not escape the function
...
unix.API                        Check calls to various UNIX/Posix functions
unix.Malloc                     Check for memory leaks, double free, and use-after-free problems. Traces memory managed by malloc()/free().
unix.MallocSizeof               Check for dubious malloc arguments involving sizeof
unix.MismatchedDeallocator      Check for mismatched deallocators.
unix.cstring.BadSizeArg         Check the size argument passed into C string functions for common erroneous patterns
unix.cstring.NullArg            Check for null pointers being passed as arguments to C string functions

在使用 -enable-checker 或者 -disable-checker 时,不需要完整的指定某个 checker 的名称, 也可以是某一类的,如:

$ scan-build -enable-checker alpha ...
$ scan-build -enable-checker alpha.security ...

例如:

$ scan-build -enable-checker alpha.security make
-e compiling [/usr/share/clang/scan-build-3.8/bin/../libexec/ccc-analyzer]: ./channel_edit.c
channel_edit.c:202:15: warning: Value stored to 'data' during its initialization is never read
    list_data data = {0};
            ^~~~   ~~~
1 warning generated.
scan-build: 150 bugs found.
scan-build: Run 'scan-view /tmp/scan-build-2016-06-23-125458-27549-1' to examine bug reports.

# scan-view-3.8 ImportError: No module named ScanView
$ scan-view-3.4 /tmp/scan-build-2016-06-23-125458-27549-1

相比 cppcheck,它提供了更多的检查规则。

Purify

  1. Purify使用体验
  2. Rational Purify 使用及分析实例
  3. 使用 IBM Rational PurifyPlus

Infer

Facebook 的 Infer 是一个静态分析工具。Infer 可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题。

  1. Infer
  2. documentation

coverity

  1. coverity
  2. Coverity 代码静态安全检测

PVS-Studio

C/C++/C++11 静态代码分析工具

PC-lint

PC-lint

Ref

  1. List of tools for static code analysis

Malloc函数和OOM Killer

Linux下malloc函数主要用来在用户空间从heap申请内存,申请成功返回指向所分配内存的指针,申请失败返回NULL。
默认情况下,Linux内核使用“乐观的”分配内存策略,首先粗略估计系统可使用的内存数,然后分配内存,
但是在使用的时候才真正把这块分配的内存给你。
这样一来,即使用malloc申请内存没有返回NULL,你也不一定能完全使用这块内存,
特别是在一次或连续多次申请很多内存的时候。

如果一直连续用malloc申请内存,而不真正使用,所申请的内存总数可以超过真正可以使用的内存数。
但是当真正使用这块内存,比如用memset或bzero函数一次性把所申请到的大块内存“使用掉”,
Linux系统就会Out Of Memory,这个时候OOM Killer就会kill掉用户空间的其他进程来腾出更多可使用内存。

OOM Killer根据OOM score来决定kill哪个进程,OOM score可以看/proc//oom_score,
score由badness函数计算得出,根据进程运行时间长短,进程优先级,进程所使用的内存数等等。
可以通过/proc//oom_adj来干预计算socre,这个值的取值范围是-17~15,
如果是-17该进程就永远不会被kill(这个可能也和内核版本有关,不见得所有内核版本都支持,得实际试试)。

“默认情况”Linux是这种做的,“默认情况”是指/proc/sys/vm/overcommit_memory为0的时候。
这个参数也可以调整,如果为1表示“来着不拒”,只要你malloc过来申请,我啥都不做,立马给你分配内存,
这样的话性能就会有大幅度的提高;
如果为2表示Linux会精确计算所有可使用的内存和所申请的内存,如果所申请的超过的可使用的内存数就返回NULL。
可使用的内存值计算方法,虚拟内存(swap)+ /proc/sys/vm/overcommit_memory(百分比) × 物理内存。
/proc/sys/vm/overcommit_memory默认值为50,计算起来就是50%的物理内存数。

Linux自身内核会占一部分内存,还有buffer/cache所占用的内存,
所以实际上能被malloc申请后使用的内存并非物理内存大小,
demsg的输出里面包含了相关信息(如果看不到,可能是被别的信息冲掉了,重启系统,在系统起来后马上看):

Memory: 2071220k/2097152k available (2122k kernel code, 24584k reserved, 884k data, 228k init, 1179584k highmem)

关于OOM Killer的proc文件系统

下面开始介绍与OOM Killer相关的proc文件系统。

/proc/PID/oom_adj

为/proc/PID/oom_adj设置值就可以调整得分。
调整值的范围为–16~15。正的值容易被OOM Killer选定。负值可能性较低。
例如,当指定3时,得分就变为23倍;当指定–5时,得分就变为1/25。

“–17”是一个特殊的值。如果设置为–17,就会禁止OOM Killer发出的信号(从Linux 2.6.12开始支持设置–17)。

在OOM Killer运行的情况下,为了实现远程登录而想要将sshd排除在对象外时,可以执行下列命令。

# cat /proc/'cat /var/run/sshd.pid'/oom_score
15
# echo -17 >  /proc/'cat /var/run/sshd.pid'/oom_adj
# tail /proc/'cat /var/run/sshd.pid'/oom_*
==> /proc/2278/oom_adj <==
-17
==> /proc/2278/oom_score <==
0                               /*得分变成0*/

从Linux 2.6.18开始可以使用/proc/PID/oom_adj。内容记载在Documentation /filesystems/proc.txt中。

/proc/sys/vm/panic_on_oom

将/proc/sys/vm/panic_on_oom设置为1时,在OOM Killer运行时可以不发送进程信号,而是使内核产生重大故障。

# echo 1 > /proc/sys/vm/panic_on_oom

/proc/sys/vm/oom_kill_allocating_task

从Linux 2.6.24开始proc文件系统就有oom_kill_allocating_task。
如果对此设置除0以外的值,则促使OOM Killer运行的进程自身将接收信号。此处省略对所有进程的得分计算过程。

# echo 1 > /proc/sys/vm/oom_kill_allocating_task

这样就不需要参照所有进程,但是也不会考虑进程的优先级和root权限等,只发送信号。

/proc/sys/vm/oom_dump_tasks

从Linux 2.6.25开始,将oom_dump_tasks设置为除0以外的值时,在OOM Killer运行时的输出中会增加进程的列表信息。

# echo 1 > /proc/sys/vm/oom_dump_tasks

列表信息显示如下,可以使用dmesg或syslog来确认。

[ pid ]   uid  tgid total_vm      rss cpu oom_adj name
[    1]     0     1     2580        1   0       0 init
[  500]     0   500     3231        0   1     -17 udevd
[ 2736]     0  2736     1470        1   0       0 syslogd
[ 2741]     0  2741      944        0   0       0 klogd
[ 2765]    81  2765     5307        0   0       0 dbus-daemon
[ 2861]     0  2861      944        0   0       0 acpid
...
[ 3320]     0  3320   525842   241215   1       0 stress

/proc/PID/oom_score_adj

从Linux 2.6.36开始都安装了/proc//oom_score_adj,
此后将替换为/proc/PID/oom_adj。
即使当前是对/proc/PID/oom_adj进行的设置,在内核内部进行变换后的值也是针对/proc/PID/oom_score_adj设置的。

可以设置–1000~1000之间的值。设置为–1000时,该进程就被排除在OOM Killer强制终止的对象外。

在内核2.6.36以后的版本中写入oom_adj,只会输出一次如下的信息。

# dmesg
.....
udevd (60): /proc/60/oom_adj is deprecated, please use /proc/60/oom_score_adj instead.

如何查看进程发生缺页中断的次数?

ps -o majflt,minflt -C program

majflt代表major fault,中文名叫大错误,minflt代表minor fault,中文名叫小错误。
这两个数值表示一个进程自启动以来所发生的缺页中断的次数。

发成缺页中断后,执行了那些操作?

当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:

  1. 检查要访问的虚拟地址是否合法
  2. 查找/分配一个物理页
  3. 填充物理页内容(读取磁盘,或者直接置0,或者啥也不干)
  4. 建立映射关系(虚拟地址到物理地址)
  5. 重新执行发生缺页中断的那条指令

如果第3步,需要读取磁盘,那么这次缺页中断就是majflt,否则就是minflt。

内存分配的原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

  1. brk是将数据段(.data)的最高地址指针_edata往高地址推;
  2. mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。
在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

以例子来说明内存分配的原理

情况一

malloc小于128k的内存,使用brk分配内存,
将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),
第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:

brk

  • 进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。
    其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。
    _edata指针(glibc里面定义)指向数据段的最高地址。

  • 进程调用A=malloc(30K)以后,内存空间如图2:
    malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。
    你可能会问:只要把_edata+30K就完成内存分配了?
    事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,
    等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。
    也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。

  • 进程调用B=malloc(40K)以后,内存空间如图3。

情况二

malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

mmap

  • 进程调用C=malloc(200K)以后,内存空间如图4:
    默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),
    那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。
    这样子做主要是因为:
    brk分配的内存需要等到高地址内存释放以后才能释放
    (例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),
    而mmap分配的内存可以单独释放。

    当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。

  • 进程调用D=malloc(100K)以后,内存空间如图5;

  • 进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放。

free

  • 进程调用free(B)以后,如图7所示:
    B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?
    当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。

  • 进程调用free(D)以后,如图8所示:
    B和D连接起来,变成一块140K的空闲内存。

  • 默认情况下:
    当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。
    在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。

Script

grep XXYYZZ * -Rn --include='*.c'
grep XXYYZZ * -Rn --include='*.h'
grep XXYYZZ * -Rn --include='*.[ch]'

Script

find . -name "*.c" | xargs sed -i "s/$1/$2/g"

VIM

多文件查找

grep

直接在vim中输入

:grep abc * 

这是直接调用unix下的grep命令

vimgrep

基本用法就是

:vimgrep /匹配模式/[g][j] 要搜索的文件/范围 
:vim[grep][!] /{pattern}/[g][j] {file} ...

g 和 j 是两个可选的标志位,g表示是否把每一行的多个匹配结果都加入。j表示是否搜索完后定位到第一个匹配位置。
要搜索的文件可以是具体的文件路径,也可以是带通配符的路径比如 .as **/.as ,**表示递归所有子目录。
要搜索的文件和或搜索范围都可以写多个,用空格分开。 例子:

:vimgrep /\/ **/*.as 搜索当前目录以及所有子目录内as文件中的 "flash"
:vimgrep /an error/ *.c 就是在所有的.c文件中搜索an error。
:vimgrep/an error/* 意思是查找当前目录下的文件中的an error,不包括子目录
定位

输入上述的命令后,可以像输入:make命令,那样定位匹配到的文件位置

:cnext (:cn)           下一个匹配位置
:cprevious (:cp)     上一个匹配位置
:cwindow (:cw)     quickfix窗口,可以选择匹配的文件位置
:cl(:clist)                查看所有匹配的位置

多文件替换(arg)

  1. 加入要处理的文件 :args *.txt
  2. 输入对上述文件的动作 :argdo %s/hate/love/gc | update (这里将hate替换成love,update表示要写入到文件中,否则只作替换而不写入)

安装

sudo apt-get install doxygen doxygen-doc doxygen-gui graphviz
doxywizard
doxygen -g

生成函数调用图

DoxyWizard

打开DoxyWizard,弹出Doxygen配置界面。如下图,标出了主要需要设置的选项:

wizard

  1. Step1:设置doxygen的工作目录,这里主要是生成doxygen运行的目录
  2. Step2:选项设置,wizard和expert选项可以同时设置。

wizard选项卡中,选择Project Name作为工程名称,将来会显示在文档的标题中;
选择Source code directory,设置源代码所在目录,Destination directory设置文档的生成目录;
选择Scan recursively则递归分析源代码目录中的子目录内的源代码。

build

需要从没有任何标记的源代码中分析出函数调用关系,所以还需要设置expert选项卡:

build

勾选Build选项中的与函数有关的选项,EXTRACT_ALL必须勾选;
sourcebrowser: 需要查看代码,勾选Inline sources和souce Browser。

dot

由于使用到了Graphviz,所以要设置Dot选项,勾选HAVE_DOT,并设置DOT_PATH为Graphviz的bin目录。

dot1

Dot: 这里可以勾选CLASS_DIAGRAMS/HAVE_DOT/CALL_GRAPH/CALLER_GRAPH/DOT_PATH

dot2

run

然后就可以点RUN标签,运行后,会生成HTML,查看INDEX.HTML既可以看到结果

ubuntu 14.04 64位无法安装ia32-libs,此包已被移除

sudo apt-get install ia32-libs

替换为

sudo apt-get install libc6:i386
sudo apt-get install lib32z1 lib32ncurses5 lib32bz2-1.0

上面这些包基本上都是基本必备库,另外编译中需要到的各个库可以搜索相应的版本,进行安装,例如:

➜  /home/yanwzh  > sudo apt-cache search libelf

libelf-dev - libelf1 development libraries and header files
libelf1 - library to read and write ELF files
libelfg0 - an ELF object file access library
libelfg0-dev - an ELF object file access library: development files
libelf-freebsd-1 - library to read and write ELF files
libelf-freebsd-dev - Development files for libelf (FreeBSD version)

➜  /home/yanwzh  > sudo apt-get install libelf-dev:i386

使用sudo apt-get install somepkg:i386 来强制安装32位版本


如何获得eCos系统的线程堆栈和中断堆栈使用情况。
eCos是开源免版税的抢占式实时操作系统。其最大亮点是可配置,
与其配套的图形化配置工具提供组件管理、选项配置、自动化单元测试等。
eCos官网http://ecos.sourceware.org。

在嵌入式系统中,堆栈是静态分配的,
不会依据堆栈的使用情况自动增加堆栈深度,存在堆栈溢出的风险。
一旦发生堆栈溢出,后果很严重,可能会立即导致死机,也可能埋了一颗定时炸弹,
在随后的开发过程随时导致死机。
事实上,堆栈溢出导致死机还算是不错的状况,
更糟糕的情况是,有时候正常,有时候功能错误,捉摸不定,
如果又是在修改了与这个堆栈压根就没什么关系的代码的情况下出现,
那真的要吐血,因为第一感觉会告诉你,刚刚修改的代码存在错误。
好在可以预防这种情况,稍微专业点的RTOS就会提供堆栈检查API,
eCos当然也不例外,在有堆栈检查API的RTOS中,
即使已经发生了堆栈溢出,也可以快速诊断哪个堆栈产生溢出。

堆栈检查选项

堆栈检查通常是发生在开发和测试阶段,
因此在eCos中堆栈检查功能是一个可选项,
要使用eCos堆栈检查功能,首先要打开对应的选项。

> eCos kernel  
    > Thread-related options  
        > Measure stack usage  

堆栈检查API

使能Measure stack usage选项后,cyg_thread_measure_stack_usage函数可用,函数原型如下:

cyg_uint32 cyg_thread_measure_stack_usage(cyg_handle_t thread)  

这个函数以线程句柄为入参,返回该线程已使用的堆栈深度,以字节为单位。

仅有已使用堆栈深度是不够的,
使用cyg_thread_get_stack_size获取线程堆栈大小,
使用cyg_thread_get_stack_base获取线程堆栈基址。
通过这三个函数可以确定线程堆栈存储位置,分配空间大小,已使用堆栈大小,
通过已使用堆栈大小与分配空间大小相比可以评估线程堆栈是否存在堆栈溢出的风险。
此外,可以使用cyg_thread_get_info函数一次性获取上述三个参数。

代码示例

使用cyg_thread_get_info可以一次性地获取上述三个参数,
而且还可以获取其他线程信息,使用cyg_thread_get_next可以枚举当前运行环境下的所有线程。
下面的代码使用cyg_thread_get_next函数枚举所有线程,
然后调用cyg_thread_get_info获取线程信息,
并打印线程名称和堆栈使用情况。
如果堆栈检查选项未使能,那么thread_info.stack_used为0。

cyg_handle_t thread = 0;  
cyg_uint16 id;  
cyg_thread_info thread_info;  

while(cyg_thread_get_next(&thread, &id))  
{  
    if(cyg_thread_get_info(thread, id, &thread_info))  
    {  
        diag_printf("name: %s, handle: 0x%08x, id: 0x%04x, "  
                "stack_base: 0x%08x, stack_size: %d, stack_used: %d\n",  
                thread_info.name, thread_info.handle, thread_info.id,  
                thread_info.stack_base, thread_info.stack_size, thread_info.stack_used);  
    }  
    else  
    {  
        diag_printf("ERROR: get thread info failed, handle: 0x%08x, id: 0x%04x\n",  
                thread, id);  
    }  
}  

影响堆栈深度的因素

  1. 函数调用深度,每调用1次函数至少需要8字节堆栈空间用来保存LR寄存器以及堆栈对齐,
    因此嵌入式系统应当尽量避免递归调用,递归调用将大大增加对堆栈需求并且引入不确定性,最终可能导致堆栈溢出。

  2. 函数复杂度,函数越复杂使用的临时变量越多,
    临时变量可以保存在寄存器或堆栈中,如果要保存在寄存器中,
    那么首先要将原寄存器内容压栈保存,因此临时变量无论是保存在寄存器或堆栈都会增加堆栈深度。

  3. 编译选项,不同的编译参数会影响堆栈深度,
    例如使用-O0编译出的代码要比-O2编译出的代码使用更多的堆栈。
    因为使用-O0时,会将所有变量压栈,而不仅是保存在寄存器,这有利于调试,但会增加堆栈深度。

  4. 中断,产生中断时,中断服务函数将被中断线程的上下文保存到被中断线程的堆栈,
    因此中断也会加深线程堆栈深度,中断嵌套将会加深中断堆栈。

堆栈检查原理

在线程初始化时,将线程堆栈所有空间填入预设值,eCos预设值为0xDEADBEEF,
在调用cyg_thread_measure_stack_usage或者cyg_thread_get_info时从堆栈底部开始检查堆栈存储的内容是否为预设值,
如果是预设值说明该地址可能未被使用,如果不是那么说明该地址已经被使用。
预设值不是0而是0xDEADBEEF的原因是:变量值为0的可能性非常大,
而为0xDEADBEEF的可能性非常小,减少因为变量值与预设值相同而导致计算偏差的可能性。

// kernel/<version>/include/thread.inl:222  
inline void Cyg_HardwareThread::attach_stack(CYG_ADDRESS s_base, cyg_uint32 s_size)  
{  
    ......  
#ifdef CYGFUN_KERNEL_THREADS_STACK_MEASUREMENT  
    {  
        CYG_WORD *base = (CYG_WORD *)s_base;  
        cyg_uint32 size = s_size/sizeof(CYG_WORD);  
        cyg_ucount32 i;  

        for (i=0; i<size; i++) {  
            base[i] = 0xDEADBEEF;  
        }  
    }  
#endif  
    ......  
}  
</version>  

// kernel/<version>/include/thread.inl:157  
inline cyg_uint32 Cyg_HardwareThread::measure_stack_usage(void)  
{  
    CYG_WORD *base = (CYG_WORD *)stack_base;  
    cyg_uint32 size = stack_size/sizeof(CYG_WORD);  
    cyg_ucount32 i;  

    for (i=0; i<size; i++) {  
        if (base[i] != 0xDEADBEEF)  
            break;  
    }  
    return (size - i)*sizeof(CYG_WORD);  
}  
</version>  

中断堆栈

eCos提供了线程堆栈检查,但没有发现中断堆栈检查,
中断堆栈同样会有溢出的风险,因此编写了stkinfo组件对中断堆栈进行检查,
此外该组件还可以对不包含内核情况下的应用程序主堆栈进行检查。

stkinfo组件

目前该组件仅支持Cortex-M架构,可能支持其他架构,但未经验证。

stkinfo扩展组件包提供的接口如下:

typedef struct _cyg_stack_info  
{  
CYG_ADDRWORD    base;  
cyg_uint32      size;  
cyg_uint32      used;  
}cyg_stack_info;  

// initialize interrupt stack with known value.  
void cyg_interrupt_stack_measure_init(void);  

// Measure the stack usage of the interrupt.  
void cyg_get_interrupt_stack_info(cyg_stack_info* info);  

// initialize main stack with known value.  
void cyg_main_stack_measure_init(void);  

// Measure the stack usage of the user program.  
void cyg_get_main_stack_info(cyg_stack_info* info);  
  1. 堆栈信息数据结构,包括堆栈基址,堆栈大小,已使用堆栈大小。

  2. 中断堆栈检查初始化,这个函数将中断堆栈写入预设值,
    但是仅写到堆栈的下半段,因为eCos使用中断堆栈作为初始化过程的堆栈,
    因此中断的上半段已经在使用中,不能写入预设值,
    从这里可以看出,只有在中断堆栈使用量超过堆栈大小一半时检查结果才比较精确,
    但是用来判断堆栈溢出已经足够啦。

  3. 获取中断堆栈信息,包括堆栈已使用大小,
    如果堆栈已使用量小于堆栈的一半,
    那么总是返回堆栈的一半大小值,例如堆栈分配1024字节,实际仅使用378字节,
    该函数返回的使用量是512字节,如果堆栈已使用量超过堆栈的一半,那么返回堆栈的实际使用量。

  4. 应用程序主堆栈初始化,与中断堆栈一样仅初始化下半段,如果当前配置包含内核,那么该函数什么都不做。

  5. 获取应用程序主堆栈信息,包括堆栈已使用大小,如果当前配置包含内核,那么info数据结构的所有值为0。

安装stkinfo组件

使用ecosadmin.tcl脚本安装stkinnfo组件,该脚本在eCos源代码的packages子目录下。

cd <ecos-prefix>/packages  
tclsh ecosadmin.tcl add <download-prefix>/stkinfo-<version>.epk  
</version></download-prefix></ecos-prefix>  

也可以手动安装该组件。

tar -xf <download-prefix>/stkinfo-<version>.epk  
cat <download-prefix>/pkgadd.db >> <ecos-prefix>/packages/ecos.db  
cp -R <donwload-prefix>/services <ecos-prefix>/packages/  
</ecos-prefix></donwload-prefix></ecos-prefix></download-prefix></version></download-prefix>  

使用stkinfo组件

在对堆栈进行检查之前,首先要对堆栈进行预设值填充,
为了保证预设值不会覆盖当前正在使用的堆栈空间,
要在初始化的早期对堆栈进行预设值填充,
因此在hal_system_init函数中调用cyg_interrupt_stack_measure_init进行堆栈预设值填充。
hal_system_init是平台HAL的一部分,
在系统初始化早期被架构HAL调用。这里以Olimex LPC-1766-STK目标机为例。

// hal/cortexm/lpc17xx/lpc1766stk/<version>/src/lpc1766stk_misc.c:78  
......  
#ifdef CYGPKG_STKINFO  
# include <cyg/stkinfo/stkinfo.h>  
#endif  
......  
    __externC void  
hal_system_init(void)  
{  
#ifdef CYGPKG_STKINFO  
    cyg_interrupt_stack_measure_init();  
    cyg_main_stack_measure_init();  
#endif  
    ......  
}  
......  

初始化过后,可以在任意时刻调用cyg_get_interrupt_stack_info获取堆栈信息,
并打印堆栈的使用情况,
将上面打印堆栈使用情况的代码示例添加上中断堆栈后的示例如下,
这个示例的完整代码见stkinfo组件的测试用例。

cyg_handle_t thread = 0;  
cyg_uint16 id;  
cyg_thread_info thread_info;  
cyg_stack_info  stack_info;  

cyg_get_interrupt_stack_info(&stack_info);  
diag_printf("name: %s, "  
        "stack_base: 0x%08x, stack_size: %d, stack_used: <=%d\n",  
        "ISR/DSR",  
        stack_info.base, stack_info.size, stack_info.used);  

while(cyg_thread_get_next(&thread, &id))  
{  
    if(cyg_thread_get_info(thread, id, &thread_info))  
    {  
        diag_printf("name: %s, handle: 0x%08x, id: 0x%04x, "  
                "stack_base: 0x%08x, stack_size: %d, stack_used: %d\n",  
                thread_info.name, thread_info.handle, thread_info.id,  
                thread_info.stack_base, thread_info.stack_size, thread_info.stack_used);  
    }  
    else  
    {  
        diag_printf("ERROR: get thread info failed, handle: 0x%08x, id: 0x%04x\n",  
                thread, id);  
    }  
}  

Line4声明中断堆栈信息变量。

Line6调用cyg_get_interrupt_stack_info获取堆栈信息。

Line7打印中断堆栈基址、大小、已使用大小。

Statue

static const unsigned char * const thread_state_str[] = {
    "RUN",      /* 0 - Running */
    "SLEEP",    /* 1 - Sleeping */
    "CNTSLP",   /* 2 - Counted sleep */
    "SLPSET",   /* 3 - Sleep set */     
    "SUSP",     /* 4 - Suspended */
    NULL,       /* 5 - INVALID */
    NULL,       /* 6 - INVALID */
    NULL,       /* 7 - INVALID */
    "CREAT",    /* 8 - Creating */
    NULL,       /* 9 - INVALID */
    NULL,       /* 10 - INVALID */
    NULL,       /* 11 - INVALID */
    NULL,       /* 12 - INVALID */
    NULL,       /* 13 - INVALID */
    NULL,       /* 14 - INVALID */
    NULL,       /* 15 - INVALID */
    "EXIT"      /* 16 - Exiting */
};

参考代码<ecos3.0/packages/net/httpd/v3_0/src/monitor.c>

Ref

  1. 获取eCos堆栈使用情况
  2. eCos Reference Manual


嵌入式系统的内存资源是非常有限的,
如果配置不当可导致eCos应用程序因为存储空间不足而链接失败。
解决这个问题的办法可以是增加更多的内存或者是减少软件对内存资源的使用,通常是后一种办法。
既然要减少内存使用量,那么首先要找出都是哪些变量吃光了内存,
就是说要对映像符号进行分析,找出最占内存的变量,
看看能否避免使用该变量或减少该变量的占用容量。
GNU工具链中的nm就是做这个事情用的,按照用户手册的说法,
nm是用来解析映像符号的,包括符号使用容量和所在的文件。

查找最占内存变量

$ nm --print-size --line-numbers --size-sort app.elf | grep " [dDbB] "  
......  
10004154 00000800 b var_data    /cygdrive/f/ecos/hg/packages/net/lwip_tcpip/current/src/ecos/sys_arch.c:84  
10002fb0 00000b00 b timer_table  
10001f98 00001000 b main_stack  
10004994 00001800 b stack_data  /cygdrive/f/ecos/hg/packages/net/lwip_tcpip/current/src/ecos/sys_arch.c:95  
2007c000 000018b8 b emac_ahb_ram        /cygdrive/f/ecos/hg/packages/devs/eth/arm/lpc2xxx/current/src/if_lpc2xxx.c:357  
20080388 0000283b b memp_memory /cygdrive/f/ecos/hg/packages/net/lwip_tcpip/current/src/core/memp.c:146  
  • –print-size:打印符号符号占用内存量
  • –line-numbers:打印符号所在的源文件名及行号
  • –size-sort:以符号占用内存量来排序
  • app.elf:eCos应用映像文件名
  • 输出内容按列分别为:符号地址、符号占用内存量、符号类型、所在文件及行号
  • grep的作用是过滤不需要的符号,我们这里仅需要存储在RAM中的变量,将常量和函数等过滤掉,
    注意grep参数的引号和方括号之间有空格。
    输出结果按照变量内容占用量排序输出,最后输出的是占用量最大的变量。
    从上面的输出可以看出memp_memory占用的内存最多,通过文件名可以判断这是lwIP内存池,
    修改lwIP配置减少连接数和缓存数目等可以减少该变量的内存占用量。

链接失败时的办法

如果容量超限,压根就不能编译完成,也就谈不上映像文件了,
这时候可以修改target.ld文件,修改内存容量,内存容量可以修改成比目标机实际容量更大的值,
修改target.ld的目的是可以正确地生成映像文件然后使用nm来分析内存使用情况并进行调整,
而不是下载到目标机运行,如果target.ld设定的内存容量比目标机实际内存容量大,
即使下载到目标机也不能正常运行。根据nm输出结果及应用需求调整内存使用量,
当映像使用的内存容量小于目标机实际内存容量后,恢复target.ld的内存容量设置。

使用size查看总内存量

nm打印出每个符号占用的内存量,而size打印映像的总内存量,
可以使用size输出快速判断容量是否超限的问题,size还可以显示srec和ihex格式的映像容量。

$ size app.elf  
text    data     bss     dec     hex filename  
158712    1852   43503  204067   31d23 app.elf  

使用哪个nm?

nm和size只是对ELF格式的映像文件符号和加载段进行分析,
因此跟硬件架构没多大关系,
使用nm或arm-eabi-nm的效果是一样的,如果不放心,那就用arm-eabi-nm吧,size同理。

nm常用参数

-C, –demangle

逆向解析C++符号转换,编译C++源代码时,
会将C++符号转换成符号汇编器要求的符号,
如果不对C++符号进行逆向解析,那么看到的是汇编符号而不是C++源文件中的符号名。

使用该参数前,输出汇编符号,晦涩难懂

$ arm-eabi-nm app.elf  
......  
00006adc T _ZN10Cyg_Thread5delayEy  
......  

使用该参数后,输出C++符号,与源文件符号一致

$ arm-eabi-nm -C app.elf  
    ......  
00006adc T Cyg_Thread::delay(unsigned long long)  
    ......  

-l, –line-numbers

输出符号所在的文件名和行号。使用该参数前

$ arm-eabi-nm app.elf  
......  
0001a0c4 T write  

使用该参数后,行尾追加文件名和行号

$ arm-eabi-nm -l app.elf  
......  
0001a0c4 T write        /cygdrive/f/ecos/hg/packages/io/fileio/current/src/io.cxx:169  

-S, –print-size

输出符号占用内存量。使用该参数前

$ arm-eabi-nm app.elf  
......  
0001a0c4 T write  

使用该参数后,第2列数字为符号占用内存量

$ arm-eabi-nm -S app.elf  
......  
0001a0c4 00000034 T write  

-n, –numeric-sort

输出结果按照符号地址排序。使用该参数前

$ arm-eabi-nm app.elf  
......  
000202c8 T udp_sendto_if  
00018ec0 t update_arp_entry  
10004154 b var_data  
1000497c b var_handle  
10004954 b var_mempool  
00009a60 T vfnprintf  
0001a0c4 T write  

使用该参数后,根据第1列数值排序

$ arm-eabi-nm -n app.elf  
......  
100070b0 A __heap1  
10007fe0 A hal_startup_stack  
2007c000 A __ahb_sram0_start  
2007c000 b emac_ahb_ram  
2007d8b8 A __ahb_sram0_end  
20080000 A __ahb_sram1_start  
20080000 b ram_heap  
20080388 b memp_memory  
20082bc3 A __ahb_sram1_end  

–size-sort

按照内存使用量排序。使用该参数前

$ arm-eabi-nm -S app.elf  
......  
10004154 00000800 b var_data  
1000497c 00000004 b var_handle  
10004954 00000028 b var_mempool  
00009a60 000015f4 T vfnprintf  
0001a0c4 00000034 T write  

使用该参数后,根据第2列数值进行排序

$ arm-eabi-nm -S --size-sort app.elf  
......  
10001f98 00001000 b _ZL10main_stack  
0001cf38 000011dc t tcp_receive  
00009a60 000015f4 T vfnprintf  
10004994 00001800 b stack_data  
2007c000 000018b8 b emac_ahb_ram  
20080388 0000283b b memp_memory 


cpuload组件包提供了一种估算CPU负载的方式。它可以估算最近0.1秒、1秒和10秒内的CPU负载百分比。

负载测量API

首先,必须在被测目标机上对测量算法进行校准,一旦校准完成后就可以开始测量。
测量是一个连续过程,因此总是提供最近的测量数据,测量过程可以根据需要随时停止。
一旦开始测量过程,就可以获取测量结果。

需要注意的是:如果目标机或CPU执行任何节能措施,例如降低时钟频率或者挂起CPU等,
这些节能措施将干扰CPU负载测量,在这种情况下,测量结果是未定义的。
Synthetic Target就是这样的情况之一。阅读本文后续实现细节章节可以了解更多。

『译注』Cortex-M架构的默认实现会在执行空闲任务时挂起CPU,
因此默认情况下,测量结果并非是实际的CPU负载,
需要重定义HAL_IDLE_THREAD_ACTION宏取消空闲时挂起,
HAL_IDLE_THREAD_ACTION宏的默认实现位于hal/cortexm/arch//include/hal_arch.h:336。

负载测量不支持SMP系统,仅支持单CPU系统。

负载测量API可以在cyg/cpuload/cpuload.h中找到。

『译注』源代码位于services/cpuload

cyg_cpuload_calibrate

这个函数用来校准CPU负载测量算法。它执行一次特别的测量过程确定空闲时的CPU性能。

void cyg_cpuload_calibrate(cyg_uint32* calibration);  

该函数通过calibration指针返回校准值。
这个函数是非常特别的,为了获得正确的校准结果需要满足若干条件。
该函数使用了2个最高线程优先级,当该函数被使用时,其它线程不能使用这两个优先级。
调用该函数时,内核调度器必须已经启动而且调度器未加锁。
该函数将花费0.1秒的时间完成校准操作,在这0.1秒校准期间不能有任何其它线程执行。

『译注』这个函数使用了线程优先级1和2,为了获得正确的测量结果,
其它线程不能使用优先级1和2。eCos的最高线程优先级是0,如果有优先级为0的线程,
必须保证在校准过程该线程处于挂起状态,
否则校准过程可能被优先级为0的线程抢占而导致错误的校准结果。

『译注』该函数将会创建一个线程,线程堆栈大小为CYGNUM_HAL_STACK_SIZE_MINIMUM,
在Cortex-M架构下约1.5KB,该线程仅在校准过程执行一次,
随后被删除永远都不会再执行,如果目标机内存非常有限,
应当知晓CPU负载测量校准时使用了1.5KB的堆栈空间。

cyg_cpuload_create

这个函数启动CPU负载测量。

void cyg_cpuload_create(cyg_cpuload_t* cpuload,  
        cyg_uint32 calibrate,  
        cyg_handle_t* handle);  

调用该函数启动测量过程,handle返回操作句柄,通过该句柄可以读取测量结果以及停止测量过程。

cyg_cpuload_delete

这个函数停止CPU负载测量。

void cyg_cpuload_delete(cyg_handle_t handle);  

handle必须是cyg_cpuload_create函数返回的操作句柄。

cyg_cpuload_get

这个函数返回最近的测量结果。

void cyg_cpuload_get(cyg_handle_t handle,  
        cyg_uint32* average_point1s,  
        cyg_uint32* average_1s,  
        cyg_uint32* average_10s);  

handle必须是cyg_cpuload_create函数返回的操作句柄。
最近0.1秒、1秒和10秒的负载测量结果通过average_point1s、average_1s和average_10s返回。

实现细节

这一节给出一些测量过程的实现细节,这些细节可以帮助我们理解测量结果的意义。
当没有其它线程可以执行时,eCos将执行空闲线程,空闲线程总是可执行的而且使用最低线程优先级。
空闲线程只做一点点事情,它有一个永远都不会退出的循环,每次循环将idle_thread_loops变量加1,
然后调用HAL_IDLE_THREAD_ACTION宏。
CPU负载测量算法就是使用了idle_thread_loops变量,
测量算法周期性地检查idle_thread_loops变量,并记录前后两次检查的差值,
系统越空这个差值就越大,通过这个简单的手段就可以确定系统的负载。

cyg_cpuload_calibrate函数执行空闲任务0.1秒,
从而确定系统空闲0.1秒的时间内idle_thread_loops会被累加多少次。
cyg_cpuload_create函数启动一个闹钟,这个闹钟每隔0.1秒调用回调函数,
回调函数计算idle_thread_loops从上次检查到本次检查的差值,然后根据这个差值以及校准值计算出CPU负载。
回调函数用新的计算结果更新cyg_cpuload结构,
0.1秒负载只是简单地复制最近的测量结果,然后通过一个简单的滤波计算1秒和10秒负载。
由于舍入误差的存在,即使系统满负载,1秒和10秒测量值也永远都不会达到100%,通常看到的是99%。

如前所述,电源管理代码将干扰上述测量结果。
CPU负载测量的基本假设是:空闲线程可以无障碍地运行,而且运行条件与校准时的运行条件保持一致。
如果降低CPU时钟频率,那么空闲线程的计数器累加速率将变慢,
因此CPU负载测量结果值将偏高,如果CPU被完全挂起,那么CPU负载测量结果将是100%。

Ref

  1. Chapter 68. CPU Load Measurements
  2. eCos Reference Manual