一、C语言

1. inline 和 static inline

inline的优缺点

inline是c99的特性。在c99中,inline是向编译器建议,将被inline修饰的函数以内联的方式嵌入到调用这个函数的地方。 而编译器会判断这样做是否合适,以此最终决定是否这么做。

  1. 好处:减少调用函数时的开销,如:减少传参时可能引起的压栈出栈的开销。减少PC跳转时对流水线的破坏。
  2. 坏处: 以空间换时间,代码所占体积会更大。

static inline和inline的区别

显然我们希望它们被其他文件访问。那么我们的inline函数原型就应该定义在头文件中,注意是函数原型而不是函数声明。因为开发者决定不了一个函数是否被内联,开发者只有建议权,只有编译器具有决定权。这就造成了一个很困扰的事情:除非你看到一个函数的反汇编代码,否则你很难确定他是不是内联函数。试想,如果头文件中的Inline函数在没有被编译成内联函数的情况下,被include到了多个源文件中,势必会产生函数重复定义的问题。 因此,我们要再加一个关键字static,才能避免这个问题。

static inline使用时机

static inline什么时候用比较好:

  1. 所修饰的函数语句较少时,尤其是只有一两条语句的函数。
  2. 所修饰的函数不是递归函数,因为递归不支持内联。
  3. 内联函数定义了但没有使用的时候不会和普通函数那样warning。

2. struct和union内存对齐

内存对齐的原因

  1. 平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因 :数据结构( 尤其是栈 ) 应该尽可能地在自然边界上对齐。 原因在于,我们在访问结构体变量时,是按结构体中成员所占的最大字节数对结构体变量进行读取的。为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。这里归根结底的来说就是以空间换时间

struct对齐的规则

  1. 结构体中的第一个成员在与结构体变量偏移量为0的地址处。
  2. 其它成员变量要对齐到一个名叫“对齐数”的整数倍的地址处。补充:对齐数=编译器默认的一个对齐数与该成员变量类型中的较小值。
  3. 结构体总大小为最大对齐数(每个成员变量类型都有一个对齐数)的整数倍。
  4. 如果存在了嵌套结构体的情况,嵌套的结构体对齐到自己的成员变量类型中最大对齐数的整数倍处,包含该嵌套结构体的结构体大小为其成员变量类型(包含嵌套结构体)最大对齐数的整数倍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//结构体内存对齐
struct s1
{
char c1;
int a;
char c2;
}; //32位平台为12字节

struct s2
{
char c1;
char c2;
int a;
}; //32位平台为8字节

struct s3
{
int d;
char c;
int i;
struct s2 t; //结构体嵌套,32位平台为20字节
};

1.2.1
1.2.2
1.2.3

取消内存对齐

  1. #pragma pack(1) … #pragma pack()
  2. 一定要在结构体末尾加上#pragma pack()进行取消自定义字节对齐的命令,如果不取消可能会导致整个程序存在问题,因为会影响到其他结构体的字节对齐方式。
  3. __attribute__((packed)),GNU C特有的编译机制。

位域存储规则

  1. 若相邻的位域成员的类型相同,且其占用二进制位数未超过该类型可容纳的范围,则后面的成员紧接着前一个成员进行存储;
  2. 若相邻的位域成员的类型相同,但其占用二进制位数超过该类型可容纳的范围,则后面的成员从该类型占用空间之后的内存单元开始存储;
  3. 若相邻的位域成员的类型不同,则取决于编译器的实现。对于GCC编译器会尽量利用空闲的位对数据进行存储;
  4. 若位域之间定义有匿名位域成员,则匿名位域成员指定的空闲位不用于后续成员的数据存储,注意,他也要遵守条件2;
  5. 特别地,如果匿名成员占用的位数为0,当前类型的内存单元剩余空间都将不会被使用;
  6. 混合使用位域和普通成员变量,也要遵守结构体对齐的规则。

1.2.4

  1. 对于成员a、b、c再加上匿名成员占用的总内存空间并未超过unsigned short类型空间的大小,因此在unsigned short类型可容纳的范围内,这些成员可以紧挨着存放;
  2. 当后续存放成员d时,前一个unsigned short类型数据剩余的空间已不足以容纳d,因此选择下一个内存单元进行存放。
  3. 整个结构体的总大小为最宽基本类型成员大小的整数倍。所以总大小为4字节。
    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
    //验证匿名位域也要遵守规则2:
    struct foo{
    unsigned short a : 3;
    unsigned short b : 4;
    unsigned short : 9;
    unsigned short c : 3;
    unsigned short d : 5;
    }; //匿名位域9,当前类型剩余空间刚好放的下,总空间为4字节

    struct foo{
    unsigned short a : 3;
    unsigned short b : 4;
    unsigned short : 10;
    unsigned short c : 3;
    unsigned short d : 5;
    }; //匿名位域10,当前类型剩余空间放不下,从byte2开始放匿名的,总空间为6字节

    //验证位域的规则5:
    struct foo{
    unsigned short a : 3;
    unsigned short b : 4;
    unsigned short : 5;
    unsigned short c : 3;
    int d;
    }; //int d内存对齐,从byte4开始存储,byte2、byte3不用,总空间为8字节

union对齐的规则

  1. 找到占用字节最多的成员类型;
  2. union大小必须是占最大的成员类型字节数的倍数,而且需要能够容纳其他的成员。

举个例子:

1
2
3
4
5
6
//x64
typedef union {
long i;
int k[5];
char c;
}D
  1. 首先找到占用字节最多的成员,本例中是long,占用8个字节,int k[5]中都是int类型,仍然是占用4个字节的。
  2. 然后union的字节数必须是最大的成员类型的倍数,而且需要能够容纳其他的成员,为了要容纳k(20个字节),就必须要保证是8的倍数的同时还要大于20个字节,所以是24个字节。

综合的栗子

1
2
3
4
5
6
7
8
9
10
11
typedef struct{
char m:3;
char n:5;
short s;
union{
int a;
char b;
};
int h;
}__attribute__((packed)) data_a;
//此处的总大小为11字节,因为取消了内存对齐,否则总大小为12字节

判断机器大小端

小端存储:低地址内容存放在低地址,高地址内容存放在高地址
大端存储:低地址内容存放在高地址,高地址内容存放在低地址

方法一:联合体union

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
union w{
int a;
char b;
}c;
c.a = 1;
if(c.b == 1)
printf("小端存储\n");
else
printf("大端存储\n");
return 0;
}

方法二:指针方法,将int强制类型转换成char单字节,p指向a的起始字节(低字节)

1
2
3
4
5
6
7
8
9
10
int main ()
{
int a = 1;
char *p = (char *)&a;
if(*p == 1)
printf("小端存储\n");
else
printf("大端存储\n");
return 0;
}

3. __attribute__机制

  1. GNU C的一大特色就是attribute机制。attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。
  2. attribute书写特征是:__attribute__前后都有两个下划线,并且后面会紧跟一对括弧,括弧里面是相应的attribute参数。

二、ARM架构

1. 异常处理

硬件自动完成:

  1. 保存CPSR到SPSR_mode
  2. 设置 CPSR 对应功能位:
    ①切换处理器进入ARM状态:T[5]
    ②根据需要,禁止中断位:F[6] / I[7]
    ③根据异常切换到对应的异常模式:M[4:0]
  3. 保存返回地址:把当前 PC 保存到 lr_mode
  4. 设置PC = 存放跳转到对应的异常向量表的固定首地址。

异常处理程序(手动完成):

  1. 保护现场,将r0 ~ r12寄存器压栈
  2. 真正的异常处理程序
  3. 恢复现场
    ①恢复 SPSR_mode 到 CPSR
    ②恢复 lr_mode 到 PC
    ③操作sp将栈中r0 ~ r12寄存器数据恢复
1
2
3
4
5
6
7
8
9
10
11
12
13
software_interrupt:
@保存现场,连续压栈,lr先入栈,再是r12,r11...
stmfd sp!,{r0-r12, lr}

@执行用户代码
mov r0, #0x5
mov r1, #0x6
add r3, r0,r1
......

@恢复现场:{r0-r12,pc},连续出栈,先弹出给r0,r1...pc
@"^"会自动将SPSR恢复到CPSR
ldmfd sp!, {r0-r12, pc}^

三、Linux内核

1. 不同情形能否schedule

①中断、软中断、tasklet、timer、hrtimer 执行环境下、或者“内核线程、普通线程内核态环境下调用了preempt disable() / local irq_disable() / spin lock后”,能否调用schedule()函数 (包括usleep()、wait event等系列函数)?

答:不能调用schedule()函数,否则违反了linux kernel的设计理念,导致schedule出去后系统各种不稳定的异常。

比如内核能否抢占的条件就是当前没有持有锁,而是否持有锁的判断依据是preempt_count值是否为0,调用preempt disable()后preempt_count++,本来已经禁止抢占了,但是又主动调用schedule(),精神分裂症无疑了!

再比如spin lock成功后,spin lock内部调用了preempt disable,所以spin lock后,不能调用schedulel()。当然spin unlock会调用preempt enable,由于usleep()、wait_event等函数内部调用了schedule()函数,也不能使用。

为什么local irq_disable()后不能调用schedule(),我的解释是:根据《linux内核设计与实现》所述:“如果在调用local_irq_disable()之前就已经禁止了中断,那么该例程会带来危险”。因为schedule()中也会禁止中断,所以可能会导致系统不稳定。

②workqueue、kernel thread、普通线程的内核态这些context下,在没有调用preempt disable禁止抢占和local_irq_disable禁止中断等操作时,能否调用schedule()?

答:可以调用schedule()。前提是没有调用preempt disable禁止抢占和local_irq_disable禁止中断等操作时。