嵌入式笔记-C

嵌入式C语言查漏补缺,参考https://github.com/xiaowenxia/embedded-notes

数据类型

数据类型 16位 32位 64位
char 1 1 1
pointer 2 4 8
short 2 2 2
int 4 4 4
float 4 4 4
double 8 8 8
long 4 4 8
long long 8 8 8

static与volatile

C语言中,static有两个作用:1、文件作用域,2、函数作用域
文件作用域关键字static 的作用是,以static 申明的全局变量、函数不得被其他文件所引用。
static 另外一个用途是函数内部静态变量,只会被初始化一次,而且变量存储在全局数据段中而不是函数栈中,所以其生命期会一直持续到程序退出。但注意static并不是const,static只是存储在静态存储区

C语言中volatile表示这个变量很可能会被意想不到地改变,因此需要小心对待。也就是说,优化器在用到这个变量时必须每次重新从虚拟内存中读取这个变量的值,而不是使用保存在寄存器里的备份。
volatile只在以下三种场合是适合的:

  • 和信号处理(signal handler)相关的场合;
  • 和内存映射硬件(memory mapped hardware)相关的场合;
  • 和非本地跳转(setjmp 和 longjmp)相关的场合。

参考https://zhuanlan.zhihu.com/p/33074506:

  • naive case:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// global shared data
bool flag = false;

thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
while (true) {
if (flag == true) {
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}

thread2(Type* value) {
// do some evaluations
value->update(/* parameters */);
flag = true;
return;
}

这段代码将 thread1 作为主线程,等待 thread2 准备好 value。因此,thread2 在更新 value 之后将 flag 置为真,而 thread1 死循环地检测 flag。简单来说,这段代码的意图希望实现 thread2 在 thread1 使用 value 之前执行完毕这样的语义。

对多线程编程稍有了解的人应该知道,这段代码是有问题的。问题主要出在两个方面。其一,在 thread1 中,flag = false 到 while 死循环里,没有任何机会对 flag 的值做修改,因此编译器可能会将 if (flag == true) 的内容全部优化掉。 其二,在 thread2 中,尽管逻辑上 update 需要发生在 flag = true 之前,但编译器和 CPU 并不知道;因此 flag = true 可能发生在 update 完成之前,因此 thread1 执行 apply(value) 时可能 value 还未准备好。

  • 错误理解:将flag加上volatile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// global shared data
volatile bool flag = false; // 1.

thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
while (true) {
if (flag == true) { // 2.
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}

thread2(Type* value) {
// do some evaluations
value->update(/* parameters */);
flag = true;
return;
}

在1处定义了flag为volatile,所以if condition不会被优化掉。但value并不是。编译器仍有可能在优化时将 thread2 中的 update 和对 flag 的赋值交换顺序。此外,由于 volatile 禁止了编译器对 flag 的优化,这样使用 volatile 不仅无法达成目的,反而会导致性能下降。

  • 再加一个volatile在value上:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// global shared data
volatile bool flag = false;

thread1() {
flag = false;
volatile Type* value = new Type(/* parameters */); // 1.
thread2(value);
while (true) {
if (flag == true) {
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}

thread2(volatile Type* value) {
// do some evaluations
value->update(/* parameters */); // 2.
flag = true;
return;
}

看起来对两个变量都进行了volatile,但是volatile 只作用在编译器上,但我们的代码最终是要运行在 CPU 上的。尽管编译器不会将 (2) 处换序,但 CPU 的乱序执行(out-of-order execution)已是几十年的老技术了;在 CPU 执行时,value 和 flag 的赋值仍有可能是被换序了的(store-store)。此外,(2) 处的 value = new Type() 一行代码并不如想象中那么简单。它至少做了:

  • 分配一块 sizeof(Type) 大小的内存;
  • 在这块内存上,执行 Type 类型的初始化;
  • 将这块内存的首地址赋值给 value。

在 CPU 乱序执行之下,甚至有可能发生 value 和 flag 已赋值完毕,但内存里尚未完成 Type 初始化的情况。此时若 thread1 中使用 value,则程序可能崩溃。

  • 应该怎么做
  1. 使用原子操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// global shared data
std::atomic<bool> flag = false; // #include <atomic>

thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
while (true) {
if (flag == true) {
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}

thread2(Type* value) {
// do some evaluations
value->update(/* parameters */);
flag = true;
return;
}
  1. 使用互斥量和条件变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// global shared data
std::mutex m; // #include <mutex>
std::condition_variable cv; // #include <condition_variable>
bool flag = false;

thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
std::unique_lock<std::mutex> lk(m); //1互斥锁
cv.wait(lk, [](){ return flag; });
apply(value);
lk.unlock();
thread2.join();
if (nullptr != value) { delete value; }
return;
}

thread2(Type* value) {
std::lock_guard<std::mutex> lk(m); //1互斥锁
// do some evaluations
value->update(/* parameters */);
flag = true;
cv.notify_one();
return;
}

这样一来,由线程之间的同步由互斥量和条件变量来保证,同时也避免了 while (true) 死循环空耗 CPU 的情况。

在单任务的环境中,一个函数体内部,如果在两次读取变量的值之间的语句没有对变量的值进行修改,那么编译器就会设法对可执行代码进行优化。由于访问寄存器的速度要快过RAM(从RAM中读取变量的值到寄存器),以后只要变量的值没有改变,就一直从寄存器中读取变量的值,而不对RAM进行访问。

嵌入式变成中volatile的作用:

  1. 告诉compiler不能做任何优化,比如要往某一地址送两个指令。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    比如:
    int *ip = ...;
    *ip = 1;
    *ip = 2;

    优化后:
    int *ip = ...;
    *ip = 2;

    会造成第一个指令丢失。
  2. volatile定义的变量如果在程序外被改变,则每次都必须从内存中读取,而不能把他放在cache或寄存器中重复使用。
    1
    2
    3
    4
    5
    6
    volatile char a;
    a = 0;
    while(!a){ //某个任务把a的值改掉后
    // do something
    }
    doother();
    如果没有volatile, doother()就不会执行。

volatile能够避免编译器优化带来的错误,但使用volatile的同时,也需要注意频繁的使用volatile很可能会降低代码尺寸和降低性能。

const

const T 是一个常量,表示该变量不可更改。
const T * 是一个指向常量的指针,该指针指向的变量不能变。
从右向左读:

1
2
3
char * const cp;  // cp is a const pointer to char, 指针的内容(也就是变量地址)不能变
const char * p; // p is a pointer to const char;
char const * p; // 同上,因为C++里面没有const*的运算符,所以const只能属于前面的类型。