🚙

💨 💨 💨

×

  • Categories

  • Archives

  • Tags

  • About

编译过程

Posted on 05-07-2015 | In Misc


通常我们使用gcc来生成可执行程序,命令为:gcc hello.c,默认生成可执行文件a.out



其实编译(包括链接)的命令:gcc hello.c 可分解为如下4个大的步骤:




    • 预处理(Preprocessing)
    • 编译(Compilation)
    • 汇编(Assembly)
    • 链接(Linking)




gcc_compilation


预处理




1.       预处理(Preproceessing)



预处理的过程主要处理包括以下过程:



  • 将所有的#define删除,并且展开所有的宏定义
  • 处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
  • 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有注释 “//”和”/ /”.
  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们


 



通常使用以下命令来进行预处理:



gcc -E hello.c -o hello.i



参数-E表示只进行预处理 或者也可以使用以下指令完成预处理过程



cpp hello.c > hello.i      /  cpp – The C Preprocessor  /



直接cat hello.i 你就可以看到预处理后的代码



 

编译


2.       编译(Compilation)



编译过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。



$gcc –S hello.i –o hello.s



或者



$ /usr/lib/gcc/i486-linux-gnu/4.4/cc1 hello.c



注:现在版本的GCC把预处理和编译两个步骤合成一个步骤,用cc1工具来完成。gcc其实是后台程序的一些包装,根据不同参数去调用其他的实际处理程序,比如:预编译编译程序cc1、汇编器as、连接器ld



可以看到编译后的汇编代码(hello.s)如下:

 .file   "hello.c"
.section .rodata
.LC0:
.string "Hello, world."
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits


 

汇编


3.       汇编(Assembly)



汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。



$ gcc –c hello.c –o hello.o



或者



$ as hello.s –o hello.co



由于hello.o的内容为机器码,不能以普通文本形式的查看(vi 打开看到的是乱码)。



 

链接

4.       链接(Linking)



通过调用链接器ld来链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件。



ld -static crt1.o crti.o crtbeginT.o hello.o -start-group -lgcc -lgcc_eh -lc-end-group crtend.o crtn.o (省略了文件的路径名)。



 

编译器和链接器具体做了什么


helloworld的大体编译和链接过程就是这样了,那么编译器和链接器到底做了什么呢?



 



编译过程可分为6步:

  • 词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
  • 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。
  • 语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
  • 源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
  • 目标代码生成:代码生成器(Code Generator).
  • 目标代码优化:目标代码优化器(Target Code Optimizer)。


 



链接的主要内容是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确地衔接。



链接的主要过程包括:地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution),重定位(Relocation)等。

链接分为静态链接和动态链接

  1. 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。

  2. 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。


静态链接的大致过程如下图所示:

static_linking

关于Valgrind所报的4种内存丢失

Posted on 05-02-2015 | In Misc

官方解释及分析

摘自http://valgrind.org/docs/manual/faq.html#faq.deflost

5.2.With Memcheck’s memory leak detector, what’s the difference between “definitely lost”, “indirectly lost”, “possibly lost”, “still reachable”, and “suppressed”?

The details are in the Memcheck section of the user manual.
In short:

  • “definitely lost” means your program is leaking memory – fix those leaks!

  • “indirectly lost” means your program is leaking memory in a pointer-based structure. (E.g. if the root node of a binary tree is “definitely lost”, all the children will be “indirectly lost”.) If you fix the “definitely lost” leaks, the “indirectly lost” leaks should go away.

  • “possibly lost” means your program is leaking memory, unless you’re doing unusual things with pointers that could cause them to point into the middle of an allocated block; see the user manual for some possible causes. Use –show-possibly-lost=no if you don’t want to see these reports.

  • “still reachable” means your program is probably ok – it didn’t free some memory it could have. This is quite common and often reasonable. Don’t use –show-reachable=yes if you don’t want to see these reports.

  • “suppressed” means that a leak error has been suppressed. There are some suppressions in the default suppression files. You can ignore suppressed errors.

分析

  • “definitely lost”:确认丢失。程序中存在内存泄露,应尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存则会报这个错误。

  • “indirectly lost”:间接丢失。当使用了含有指针成员的类或结构时可能会报这个错误。这类错误无需直接修复,他们总是与”definitely lost”一起出现,只要修复”definitely lost”即可。例子可参考我的例程。

  • “possibly lost”:可能丢失。大多数情况下应视为与”definitely lost”一样需要尽快修复,除非你的程序让一个指针指向一块动态分配的内存(但不是这块内存起始地址),然后通过运算得到这块内存起始地址,再释放它。例子可参考我的例程。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存的起始地址,但可以访问其中的某一部分数据,则会报这个错误。

  • “still reachable”:可以访问,未丢失但也未释放。如果程序是正常结束的,那么它可能不会造成程序崩溃,但长时间运行有可能耗尽系统资源,因此笔者建议修复它。如果程序是崩溃(如访问非法的地址而崩溃)而非正常结束的,则应当暂时忽略它,先修复导致程序崩溃的错误,然后重新检测。

  • “suppressed”:已被解决。出现了内存泄露但系统自动处理了。可以无视这类错误。这类错误我没能用例程触发,看官方的解释也不太清楚是操作系统处理的还是valgrind,也没有遇到过。所以无视他吧~

代码示例

#include <stdio.h>
#include <stdlib.h>

void *g_p1;
int *g_p2;
int ** fun1(void)
{
//付给了局部变量, 函数结束而不释放,为肯定丢失.
//把函数尾部语句return p; 改为return 0;更能说明这个问题.
int **p=(int **)malloc(16);
g_p1=malloc(20); //付给了全局变量, 内存可以访问
g_p2=(int*)malloc(30);
g_p2++; //付给了全局变量, 内存可以访问,但是指针被移动过,为可能丢失
p[1]=(int *)malloc(40); //如果p丢失了,则p[1]为间接丢失.
return p;
}
int main(int argc, char *argv[])
{

int **p=fun1();
// free(g_p1); //如果不free, 将会有 still reachable 内存泄露
// free(--g_p2);//如果不free, 将会有 possibly lost 内存泄露
// free(p[1]); //如果不free, 将会有 indirectly lost 内存泄露
// free(p); //如果不free, 将会有 definitely lost内存泄露
return 0;
}

执行编译命令g++ val_test.cpp -o v, 然后

当执行valgrind ./v 命令之后的简易内存错误报告 :

==4765== Memcheck, a memory error detector
==4765== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4765== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==4765== Command: ./v
==4765== 
==4765== 
==4765== HEAP SUMMARY:
==4765==     in use at exit: 106 bytes in 4 blocks
==4765==   total heap usage: 4 allocs, 0 frees, 106 bytes allocated
==4765== 
==4765== LEAK SUMMARY:
==4765==    definitely lost: 16 bytes in 1 blocks
==4765==    indirectly lost: 40 bytes in 1 blocks
==4765==      possibly lost: 30 bytes in 1 blocks
==4765==    still reachable: 20 bytes in 1 blocks
==4765==         suppressed: 0 bytes in 0 blocks
==4765== Rerun with --leak-check=full to see details of leaked memory
==4765== 
==4765== For counts of detected and suppressed errors, rerun with: -v
==4765== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
b@b-VirtualBox:~/tc/valgrind_test$ valgrind --leak-check=full
valgrind: no program specified
valgrind: Use --help for more information.

当执行valgrind --leak-check=full ./v 命令之后的详细内存错误报告 :

==4767== Memcheck, a memory error detector
==4767== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4767== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==4767== Command: ./v
==4767== 
==4767== 
==4767== HEAP SUMMARY:
==4767==     in use at exit: 106 bytes in 4 blocks
==4767==   total heap usage: 4 allocs, 0 frees, 106 bytes allocated
==4767== 
==4767== 30 bytes in 1 blocks are possibly lost in loss record 2 of 4
==4767==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4767==    by 0x40055E: fun1() (val_test.cpp:12)
==4767==    by 0x4005AB: main (val_test.cpp:20)
==4767== 
==4767== 56 (16 direct, 40 indirect) bytes in 1 blocks are definitely lost in loss record 4 of 4
==4767==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4767==    by 0x40053F: fun1() (val_test.cpp:10)
==4767==    by 0x4005AB: main (val_test.cpp:20)
==4767== 
==4767== LEAK SUMMARY:
==4767==    definitely lost: 16 bytes in 1 blocks
==4767==    indirectly lost: 40 bytes in 1 blocks
==4767==      possibly lost: 30 bytes in 1 blocks
==4767==    still reachable: 20 bytes in 1 blocks
==4767==         suppressed: 0 bytes in 0 blocks
==4767== Reachable blocks (those to which a pointer was found) are not shown.
==4767== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==4767== 
==4767== For counts of detected and suppressed errors, rerun with: -v
==4767== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

总结

  • 由局部变量指向的内存,如果不释放为肯定丢失,
  • 由此指针而引起的后续内存泄露,为间接丢失.
  • 由全局变量指向的内存如果不被释放,为still reachable,
  • 如果该变量改动过, 为可能丢失.

是啊,局部变量是栈变量,如果你不能把这个栈变量处理好,出了这个函数,指针地址就丢失了,这时那是肯定丢失了.

如果你付给的地址是全局变量,倒是可以访问,叫still reachable

但是如果你这个全局变量的值改动过, 那只有你知道怎样正确访问这块内存,别人可能就访问不到了,这叫可能丢失.

由肯定丢失而引起的进一步的内存丢失为间接丢失.

解决内存泄漏的顺序

所以碰到问题你首先要解决什么问题?

肯定丢失,
然后是可能丢失,
然后间接丢失,
然后still reachable!!!

多线程开发的一些基本概念

Posted on 04-27-2015 | In Linux

竞态(race condition)

软件层面上,竞态是指多个线程或进程读写一个共享资源 (或共享设备) 时的输出结果依赖于线程或进程的先后执行顺序或者时间;
( 更权威的介绍可以看 wiki )

至于为什么会发生竞态呢?很简单,因为并发,并发使多线程,多进程环境变成可能。

竞态具体场景:假如我们有 2 个进程会对一个全局变量进行 ++ 操作,理想时,程序会这样执行:


Thread 1



Thread 2





Integer value









0



read value





←



0



increase value







0



write back





→



1





read value



←



1





increase value





1





write back



→



2


然而,由于并发的普遍存在,使得情况有时” 不受控制”(不如工程师预期那样工作),可能会变成这样:


Thread 1



Thread 2





Integer value









0



read value





←



0





read value



←



0



increase value







0





increase value





0



write back





→



1





write back



→



1


并发(concurrency)

并发 (concurrency) 指的是多个执行单元同时、并行被执行。而并发的执行单元对共享资源 (硬件资源和软件上的全局、静态变量) 的访问则容易导致竞态 (race conditions), 可能导致并发 (即竞态?) 的情况有:

  • SMP(Symmetric Multi-Processing),对称多处理结构。SMP 是一种紧耦合、共享存储的系统模型,它的特点是多个 CPU 使用共同的系统总线,因此可访问共同的外设和存储器。

  • 中断. 中断可以打断正在执行的进程 (哪怕是在中断上下文),若中断处理程序对共享资源进程访问,则竞态也会发生.

  • 内核抢占.2.6 以后内核提供了内核可抢占特性,虽然是作为一个配置选项,但我们写程序时还是要考虑周全,故内核抢占也是作为伪并发的表现,也可能发生竞态;

临界区(critical section)

多个线程进程对共享资源进行访问在软件表现为一个程序片段,如何避免竞态的发生呢?

一个执行路径在对共享资源进行访问时禁止其他执行路径进行访问,当有一个执行路径(A)对共享资源进行访问时,如有其他执行路径想访问共享资源,须睡眠等待 A 执行路径退出。

那么这时这个程序片段就是临界区。那么具体如何来实现临界区呢?linux 内核提供了多种同步互斥机制.(如信号量,互斥量,自旋锁,RCU,原子操作等).

什么是RAII技术

我们在C++中经常使用new申请了内存空间,但是却也经常忘记delete回收申请的空间,容易造成内存溢出,于是RAII技术就诞生了,来解决这样的问题。

RAII(Resource Acquisition Is Initialization)机制是Bjarne Stroustrup首先提出的,是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 我们知道在函数内部的一些成员是放置在栈空间上的,当函数返回时,这些栈上的局部变量就会立即释放空间,于是Bjarne Stroustrup就想到确保能运行资源释放代码的地方就是在这个程序段(栈)中放置的对象的析构函数了,因为stack winding会保证它们的析构函数都会被执行。RAII就利用了栈里面的变量的这一特点。

RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

借此,我们实际上把管理一份资源的责任托管给了一个存放在栈空间上的局部对象。
这种做法有两大好处:

  • (1)不需要显式地释放资源。
  • (2)采用这种方式,对象所需的资源在其生命期内始终保持有效。

MySQL入门三之GroupBy

Posted on 04-17-2015 | In DB

SQL GROUP BY 实例

我们拥有下面这个 “Orders” 表:

O_Id OrderDate OrderPrice Customer
1 2008/12/29 1000 Bush
2 2008/11/23 1600 Carter
3 2008/10/05 700 Bush
4 2008/09/28 300 Bush
5 2008/08/06 2000 Adams
6 2008/07/21 100 Carter

现在,我们希望查找每个客户的总金额(总订单)。
我们想要使用 GROUP BY 语句对客户进行组合。
我们使用下列 SQL 语句:

SELECT Customer,SUM(OrderPrice) FROM Orders GROUP BY Customer

结果集类似这样:

Customer SUM(OrderPrice)
Bush 2000
Carter 1700
Adams 2000

很棒吧,对不对?
让我们看一下如果省略 GROUP BY 会出现什么情况:

SELECT Customer,SUM(OrderPrice) FROM Orders

结果集类似这样:

Customer SUM(OrderPrice)
Bush 5700
Carter 5700
Bush 5700
Bush 5700
Adams 5700
Carter 5700

上面的结果集不是我们需要的。
那么为什么不能使用上面这条 SELECT 语句呢?

解释如下:
上面的 SELECT 语句指定了两列(Customer 和 SUM(OrderPrice))。
“SUM(OrderPrice)” 返回一个单独的值(”OrderPrice” 列的总计),而 “Customer” 返回 6 个值(每个值对应 “Orders” 表中的每一行)。
因此,我们得不到正确的结果。不过,您已经看到了,GROUP BY 语句解决了这个问题。

对象模型之内存对齐基础

Posted on 04-12-2015 | In Misc

本文不讨论类的虚函数, 请参考 C++对象模型之虚函数讲解

内存对齐规则

首先我们明确内存对齐规则

我们设

A = #pragma pack()指定的数
B = 这个数据成员的自身长度
C = 结构(或联合)中最大数据成员长度

在解释内存对齐的作用前,先来看下内存对齐的规则:

1. 对于结构的各个成员,第一个成员位于偏移为0的位置,
以后每个数据成员的偏移量必须是 min( A,B ) 的倍数。

2. 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照 min( A, C) 进行。

问题

32位机器上, 下列代码的sizeof(a)的值是多少?

#pragma pack(2)

class A
{
int i;

union U
{

char buff[13];

int i;

}u;

void foo() { }

typedef char* (*f)(void*);

enum{ red, green, blue } color;
}a;
#pragma pack()

答案

答案是sizeof(a)的值为22.

  • void foo() { } ,typedef char* (f)(void);不占字节,
  • 枚举占4个字节,
  • union按最大的变量所占字节算,占14个字节,
  • int占4个字节,

4+14+4=22。

如果把#pragma pack(2)改为 #pragma pack(4), sizeof(a)的值就为 24。

解析

分为三部分来解析:

  • 枚举所占内存计算方法
  • #pragma pack用法
  • 共用体(union)所占内存计算方法

枚举所占内存计算方法

枚举变量,由枚举类型定义的变量。枚举变量的大小,即枚举类型所占内存的大小。

由于枚举变量的赋值,一次只能存放枚举结构中的某个常数。

所以枚举变量的大小,实质是常数所占内存空间的大小(常数为int类型,当前主流的编译器中一般是32位机器和64位机器中int型都是4个字节),枚举类型所占内存大小也是这样。

#pragma pack用法

#pragma pack(a)规定的对齐长度(a可选值为1,2,4,8,16),实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
而结构整体的对齐,则按照结构体中最大的数据成员 和 #pragma pack指定值 之间,较小的那个进行。
而 #pragma pack() 表示恢复默认的内存对齐(与#pragma pack(a)指令配对使用)

#pragma pack(4)

class TestB
{
public:

int aa; //第一个成员,放在[0,3]偏移的位置,

char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。

short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。

char c; //第四个,自身长为1,放在[8]的位置。

};
#pragma pack()

这个类实际占据的内存空间是9字节
类之间的对齐,是按照类内部最大的成员的长度,和#pragma pack规定的值之中较小的一个对齐的。
所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。
9按照4字节圆整的结果是12,所以sizeof(TestB)是12。

如果

#pragma pack(2)
class TestB
{
public:
int aa; //第一个成员,放在[0,3]偏移的位置,
char a; //第二个成员,自身长为1,#pragma pack(2),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
short b; //第三个成员,自身长2,#pragma pack(2),取2,按2字节对齐,所以放在偏移[6,7]的位置。
char c; //第四个,自身长为1,放在[8]的位置。
};
#pragma pack()

可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。
所以 sizeof(TestB)是10。

现在去掉第一个成员变量为如下代码:

#pragma pack(4)
class TestC
{
public:
  char a;//第一个成员,放在[0]偏移的位置,
  short b;//第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
  char c;//第三个,自身长为1,放在[4]的位置。
};
#pragma pack()

整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果是6,所以sizeof(TestC)是6。

共用体(union)所占内存计算方法

共用体又名”联合体”, 英文名为union.

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。在C Programming Language 一书中对于联合体是这么描述的:

  • 联合体是一个结构;
  • 它的所有成员相对于基地址的偏移量都为0;
  • 此结构空间要大到足够容纳最”宽”的成员;
  • 其对齐方式要适合其中所有的成员;

下面解释这四条描述:

由于联合体中的所有成员是共享一段内存的,因此每个成员的存放首地址相对于于联合体变量的基地址的偏移量为0,即所有成员的首地址都是一样的。为了使得所有成员能够共享一段内存,因此该空间必须足够容纳这些成员中最宽的成员。对于这句“对齐方式要适合其中所有的成员”是指其必须符合所有成员的自身对齐方式。

下面举例说明:

union U
{
char s[9];
int n;
double d;
};

s占9字节,n占4字节,d占8字节,因此其至少需9字节的空间。然而其实际大小并不是9,用运算符sizeof测试其大小为16.这是因为这里存在字节对齐的问题,9既不能被4整除,也不能被8整除。

因此补充字节到16,这样就符合所有成员的自身对齐了。从这里可以看出联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:

  • 大小足够容纳最宽的成员;
  • 大小能被其包含的所有基本数据类型的大小所整除。

若问题为#pragma pack(4)的情况

  • void foo() { } ,typedef char* (f)(void);不占字节,
  • 枚举占4个字节,
  • union按最大的变量buff[13]所占字节算为13, 在#pragma pack(2)的情况, 得补齐1个字节变为14才能被2整除, 而#pragma pack(4)的情况得补齐3个字节, 总占16个字节,才可以被4整除,
  • int占4个字节

所以#pragma pack(4)的情况, sizeof(A)为4+16+4=24。

练习

注意有陷阱, 32位环境下

# pragma pack(2)
class test_class
{
public:
static float i;

union test_union
{
int bb;
char aa[13];
short cc;
};

enum test_enum
{
monday,
tuesday,
sunday
};

virtual void testFunc() {}

char xmly;
};

# pragma pack()

int main()
{
cout << "sizeof(test_class) : " << sizeof(test_class) << endl;
return 0;
}

请问打印结果?

sizeof(test_class) : 6

为什么呢?
注意看共用体 test_union 和枚举 test_enum其实并没有声明变量, 如果写成

#include <iostream>

using namespace std;

# pragma pack(2)
class test_class
{
public:
static float i;

union test_union
{
int bb;
char aa[13];
short cc;
}uVar;

enum test_enum
{
monday,
tuesday,
sunday
}eVar;

virtual void testFunc() {}

char xmly;
};
# pragma pack()

enum enum_x
{
x1=5,
x2,
x3,
x4,
};
enum enum_x x=x3;

int main()
{
cout << "sizeof(test_class) : " << sizeof(test_class) << endl;

cout << "x : " << x << endl;

test_class::test_enum i;
i = test_class::monday;
cout << "i : " << i << endl;

test_class test_obj;
test_obj.eVar = test_class::sunday;
cout << test_obj.monday << endl;

cout << test_class::sunday << endl;
return 0;
}

打印结果就为

sizeof(test_class) : 24
x : 7
i : 0
0
2

GCC的原子操作函数

Posted on 04-11-2015 | In Linux

linux支持的哪些操作是具有原子特性的?知道这些东西是理解和设计无锁化编程算法的基础。

下面的东西整理自网络。先感谢大家的分享!

原子操作的api函数

gcc从4.1.2以后提供了 __sync_* 系列的下面几类的内嵌函数,提供用于针对数字或布尔型变量的原子操作。

n++类

这组返回更新前的值

type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)

++n类

这组返回更新后的值

type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)

type可以是1,2,4或8字节长度的int类型,即:

int8_t / uint8_t
int16_t / uint16_t
int32_t / uint32_t
int64_t / uint64_t

后面的可扩展参数(…)用来指出哪些变量需要memory barrier,因为目前gcc实现的是full barrier(类似于linux kernel 中的mb(),表示这个操作之前的所有内存操作不会被重排序到这个操作之后),所以可以略掉这个参数。

CAS类

CAS 即 compare-and-swap ,

下面这两个函数提供原子的比较和交换,如果 *ptr == oldval,就将 newval 写入 *ptr

  • 此函数在相等并写入的情况下返回 true

    bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...)
    /* 对应的伪代码 */
    { if (*ptr == oldval) { *ptr = newval; return true; } else { return false; } }
    
  • 此函数在返回 oldval

    type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)
    /* 对应的伪代码 */
    { if (*ptr == oldval) { *ptr = newval; } return oldval; }
    

其他原子操作

type __sync_lock_test_and_set (type *ptr, type value, ...)

将 *ptr 设为value并返回 *ptr 操作之前的值。

void __sync_lock_release (type *ptr, ...)

将 *ptr 置 0

内存栅障

内存栅障主要是处理不同cpu运作时(主要是执行不同线程代码时),一个cpu对内存的操作的原子性,也就保证其他cpu见到的内存单元数据的正确性。

内存栅障介绍

如果有对某一变量上写锁, 就不能在不获得相应的锁时对其进行读取操作。
内存栅的作用在于保证内存操作的相对顺序, 但并不保证内存操作的严格时序。

内存栅并不保证 CPU 将本地快取缓存或存储缓冲的内容刷写回内存, 而是在锁释放时确保其所保护的数据, 对于能看到刚释放的那个锁的 CPU 或设备可见。
持有内存栅的 CPU 可以在其快取缓存或存储缓冲中将数据保持其所希望的、 任意长的时间, 但如果其它 CPU 在同一数据元上执行原子操作,
则第一个 CPU 必须保证, 其所更新的数据值, 以及内存栅所要求的任何其它操作, 对第二个 CPU 可见。

__sync_synchronize (...)

发出一个full barrier.

内存栅障应用

对于执行一条指令,操作到4个寄存器时,如:

write1(dev.register_size,size);
write1(dev.register_addr,addr);
write1(dev.register_cmd,READ);
write1(dev.register_control,GO);

最后一个寄存器是控制寄存器,在所有的参数都设置好之后向其发出指令,设备开始读取参数.

如果最后一条write1被换到了前几条语句之前,那么肯定不是我们所期望的,这时候我们可以在最后一条语句之前加入一个memory barrier,强制cpu执行完前面的写入以后再执行最后一条:

write1(dev.register_size,size);
write1(dev.register_addr,addr);
write1(dev.register_cmd,READ);
__sync_synchronize();
write1(dev.register_control,GO);

memory barrier有几种类型:

  • acquire barrier : 不允许将barrier之后的内存读取指令移到barrier之前(linux kernel中的wmb())。
  • release barrier : 不允许将barrier之前的内存读取指令移到barrier之后 (linux kernel中的rmb())。
  • full barrier : 以上两种barrier的合集(linux kernel中的mb())。

原子操作应用范围

原子操作只允许一次更新或读一个内存单元。 需要原子地更新多个单元时, 就必须使用锁来代替它了。

例如, 如果需要更新两个相互关联的计数器时, 就必须使用锁, 而不是两次单独的原子操作了。

原子操作例子

例子代码:

#include <stdio.h>  
#include <pthread.h>
#include <stdlib.h>

static int count = 0;

void *test_func(void *arg)
{
int i=0;
for(i=0;i<20000;++i){
__sync_fetch_and_add(&count,1);
}
return NULL;
}

int main(int argc, const char *argv[])
{
pthread_t id[20];
int i = 0;

for(i=0;i<20;++i){
pthread_create(&id[i],NULL,test_func,NULL);
}

for(i=0;i<20;++i){
pthread_join(id[i],NULL);
}
printf("%d\n",count);
return 0;
}

原子操作封装使用

根据常用的原子操作,封装一些实用的接口 :

//原子操作  
//设置值
int lock_set(volatile int &a, int value)
{
__sync_val_compare_and_swap(&a, a, value);
return a;
}
//加1
int lock_inc(volatile int &n)
{
return __sync_fetch_and_add(&n, 1);
}
//减1
int lock_dec(volatile int &n)
{
return __sync_fetch_and_sub(&n, 1);
}

//加值value
int lock_add(volatile int &n, int value)
{
return __sync_fetch_and_add(&n, value);
}

//减值value
int lock_sub(volatile int &n, int value)
{
return __sync_fetch_and_sub(&n, value);
}

//位或value
int lock_or(volatile int &n, int value)
{
return __sync_fetch_and_or(&n, value);
}

//位与value
int lock_and(volatile int &n, int value)
{
return __sync_fetch_and_and(&n, value);
}

//异或value
int lock_xor(volatile int &n, int value)
{
return __sync_fetch_and_xor(&n, value);
}

//无符号类型(函数重载)
//设置值
unsigned int lock_set(volatile unsigned int &a, unsigned int value)
{
__sync_val_compare_and_swap(&a, a, value);
return a;
}

//加1
unsigned int lock_inc(volatile unsigned int &n)
{
return __sync_fetch_and_add(&n, 1);
}

//减1
unsigned int lock_dec(volatile unsigned int &n)
{
return __sync_fetch_and_sub(&n, 1);
}

//加值value
unsigned int lock_add(volatile unsigned int &n, unsigned int value)
{
return __sync_fetch_and_add((int*)&n, value);
}

//减值value
unsigned int lock_sub(volatile unsigned int &n, unsigned int value)
{
return __sync_fetch_and_sub((int*)&n, value);
}

//位或value
unsigned int lock_or(volatile unsigned int &n, unsigned int value)
{
return __sync_fetch_and_or((int*)&n, value);
}

//位与value
unsigned int lock_and(volatile unsigned int &n, unsigned int value)
{
return __sync_fetch_and_and((int*)&n, value);
}

//异或value
unsigned int lock_xor(volatile unsigned int &n, unsigned int value)
{
return __sync_fetch_and_xor((int*)&n, value);
}

智能指针笔记

Posted on 04-11-2015 | In Misc

有些错误是编译器查不到的, 这种错误是最可怕的, 当项目大了之后,
即使用 Valgrind 也很难定位,
因为裸指针在团队合作中使用很容易导致其他成员忘记释放或多次释放, 所以在团队合作中一般使用智能指针.

而智能指针用的不好, 结果可能适得其反.

所以我们聊一下智能指针的几点注意事项.

总结自 C++ Primer.

一个简单的包含删除器的例子演示

#include<iostream>
#include<functional>
#include<memory>

using std::cout;
using std::endl;
using std::bind;
using namespace std::placeholders;

int testBind( int* a, int b, int c )
{
cout << *a + b + c << endl;
return *a;
}

struct Foo
{
Foo() = default;
Foo( const Foo & a )
{
data = a.data;
std::cout << "复制构造" << std::endl;
}
void print_sum( int n1, int n2 )
{
std::cout << n1 + n2 << '\n';
}
int data = 10;
};

int main()
{
//绑定类成员函数用对象的指针
Foo foo;
auto f3 = std::bind( &Foo::print_sum, &foo, 95, _1 );
f3( 5 );

auto check_testBind = std::bind( testBind, std::placeholders::_1, 3, 9 );
int * p = new int( 7 );
cout << check_testBind( p ) << endl;

shared_ptr<int> pi( new int(),
check_testBind );
*pi = 88;

shared_ptr<int> pii( new int( 12 ),
std::bind( testBind, std::placeholders::_1, 32, 19 ) );

std::function< int( int* ) > test_function_bind =
std::bind( testBind, std::placeholders::_1, 331, 9 );

cout << "test_function_bind( p, 331, 9 ) = " << test_function_bind( p ) << endl;;

shared_ptr<int> piii( new int( 112 ),
test_function_bind );

return 0;
}

打印结果 :

100
119
7
347
test_function_bind( p, 331, 9 ) = 7
452
63
100
请按任意键继续. . .

智能指针陷阱

智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的
前提下 。 为了正确使用智能指针,我们必须坚持一些基本规范 :

  • 不使用相同的内置指针值初始化(或 reset) 多个智能指针 。
  • 不 delete get ( ) 返 回的指针 。
  • 不使用 get () 初始化或 reset 另 一 个智能指针 。
  • 如果你使用 get () 返 回的指针,记住当最后一个对应的智能指针销 毁 后, 你 的
    指 针就 变为无 效 了 。
  • 如果你使用智能指针管理的资源不是 new 分配的内存 , 记住传递给它一个删除
    器( 参见 12. 1.4 节 , 第 415 页和 12. 1.5 节 , 第 419 页)。

尽量用make_shared而非new

shared_ptr 可以协调对象的析构 , 但这仅限于其自身的拷贝 ( 也 是 shared_ptr)
之间。

这也是为什么我们推荐使用 make_shared 而不是 new 的原因 。

这样 , 我们就能在分配对象的同时就将 shared_ptr 与之绑定,
从而避免了无意中将同一块内存绑定到多个独立创建的 shared_ptr 上 。(这是最容易犯的错)

总结 : 所以我们要尽量一开始就用make_shared来分配动态内存, 而不是先new一个出来, 再找机会将它转为智能指针.

考虑下面对 shared_ptr 进行操作的函数 :

// 在函数被调用时 ptr 被创建并初始化
void process(shared_ptr<int> ptr)
{
// 使用 ptr
} // ptr 离 开作用域,被销毁

process 的参数是传值方式传递的,因此实参会被拷贝到 ptr 巾 。 拷贝 一 个 shared_ptr
会递增其引用讨数,因此, 在 process 运行过程中,引用七| 数值至少为 2 。 当 process
结束时, ptr 的引用计数会边喊,但不会变为 0 。 因此,当用音11变 11 ptr 被销毁时, ptr
指向的内存不会被释放 。

使用此函数的正确方法是传递给它一个 shared_ptr :

shared_ptr<int> p(new int(42)) ; // 引用计数为 1
process(p); // 拷贝 p 会递增它的引用计数 ;在 process 中引用计数位为 2
int i = *p; // 正确:引用计数位为 1

虽然不能传递给 process 一 个内置指针,但可以传递 给它 一 个(临时的)
shared_ptr , 这个 shared_ptr 是用 一个内 置指针显式构造的 。 但是,这样做很可能
会导致错误 :

int *x(new int(1024)); // 危险 x 是一个普通指针,不是一个智能指针
process(x) ; // 错误 : 不能将 int* 转换为 一个 shared_ptr<int>
process(shared_ptr<int>(x)); // 合法的,但内存会被释放!
int j = *x ; //未定义的 x 是一个空悬指针!

在上面的调用中 , 我们将一个临时 shared_ptr 传递给 process 。 当这个调用所在的表
达式结束时,这个临时对象就被销毁了 。 销毁这个临时变量会递减引用计数,此时引用计
数就变为 0 了 。 因此,当临时对象被销毁时 , 它所指向的内存会被释放 。
但 x 继续指 向 (已经释放的)内存,从而变成一个空悬指针。如果试图使用 x 的值,
其行为是未定义的 。

当将一个 shared_ptr 绑定到一个普通指针时 , 我们就将内存的管理责任交给了这
个 shared_ptr 一旦这样做 了 , 我们就不应该再使用内置指针来访问 shared_ptr 所
指向的内存了

不要使用 get 初始化另一个智能指针或为智能指针赋值

智能指针类型定义了 一个名为 get 的函数(参见表 1 2. J ),它返回一个内置指针,
指向智能指针管理的对象 。 此函数是为了这样一种情况而设计的 : 我们需要向不能使用智
能指针的代码传递一个内置指针。使用 get 返回的指告| 的代码不能 delete 此指针 。
虽然编译器不会给出错误信息 , 但将另一个智能指针也绑定到 get 返回的指针上是
错误的 :

shared_ptr<int> p(new int(42)) ; // 引 用计数为 1
int *q = p . get() ; // 正确 · 但使用 q 时妥注意,不要让它管理的指针被释放
{ // 新程序块
// 未定义:两个独立的 shared_ptr 指 向 相同的内存
shared ptr<int> (q) ;
} // 程序块结束, q 被销毁 , 它指向的内存被待放
int foo = *p ; // 未定义 p 指向的内存 已 经被释放了

在本例中, p 和 q 指 向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都
是 1。 当 q 所在的程序块结束时 , q 被销毁 , 这会导致 q 指向的内存被释放 。 从而 p 变成
一个空悬指针,意味着当我们试图使用 p 时,将发生未定义的行为 。 而且 , 当 p 被销毁时 ,
这块内存会被第二次 delete 。

get 用来将指针的访问权限传递给代码,你只有在确定代码不会 get.
特别是,永远不要用 get 初始化另一个智能指针 del ete 指针或者为另一个智能指针赋值.

1…2122232425262728293031323334353637
Mike

Mike

🚙 🚗 💨 💨 If you want to create a blog like this, just follow my open-source project, "hexo-theme-neo", click the GitHub button below and check it out ^_^ . It is recommended to use Chrome, Safari, or Edge to read this blog since this blog was developed on Edge (Chromium kernel version) and tested on Safari.

11 categories
289 posts
111 tags
about
GitHub Spotify
© 2013 - 2025 Mike