[笔记]程序员的自我修养。

以下是本人这两周看的《程序员的自我修养》这本书的笔记。
对其中的内容,本人会持续更新。。。。。。。。。。。
注:由于此文章是从我的本地笔记迁移过来的,里面缺少图片,等有时间了,我再补上。。。
1. 内存不够怎么办,使用中间层,使用一种间接的地方访问方法,增加中间层往往能解决计算机的任何问题。
2. MMU(Memory Management Unit)一般都集成在cpu内部了,它把虚拟地址转换成物理地址。
3. 线程又线程ID,当前指令指针,寄存器集合和堆栈组成。
4. 线程之间共享的数据有:全局变量,堆上的数据,函数里的静态变量,程序代码,打开的文件,A线程打开的文件可以又b线程读写。
5. 时间片,处于运行中线程拥有一段可以执行的时间即为时间片
6. 线程调度的优先级调度和轮转法。
7. IO密集型和CPU密集型线程,
8.同步与锁
9. 二元信号量,信号量和互斥锁,
二元信号量和互斥锁的区别是,前者可以在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取后由另外一个线程释放。
临界区是比互斥量更加严格的同步手段,区别是,互斥量和信号量在系统的任何进程都是可见的,而临界区的作用范围仅限于本进程。
可重入和线程安全
不使用任何静态或全局非const变量。
仅依赖调用方提供的参数
不依赖任何单个资源的锁
不调用任何不可重入的函数
可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下使用
即使使用了锁也不能保证线程安全,因为编译器的落后,如何避免呢:
使用volatile关键字试图阻止过度优化,volatile可做两件事:
1. 阻止编译器为了提高速度将一个变量缓存到寄存器而不写回
2. 阻止编译器调整操作volatile变量的指令顺序。
但volatile不能阻止cpu动态调度换序。
单例模式的设计问题,涉及到cpu的动态调度和pInst = new T, new分两步,一步是分配内存,二步是初始化,三赋值,但2和3可以颠倒。
很多cpu都有阻止换序的指令,但没有可移植的方法,基本都是barrier函数
if(!pInst)
{
T* temp = new T;
barrier();
pInst = temp;
}
三种线程模式
一对一模式: 一个用户使用的线程就唯一对应一个内核使用的线程。
一般直接使用API或系统调用创建的线程均是一对一的线程,
多对一:将多个用户线程映射为一个内核线程。线程之间的切换由用户态的代码来进行。
多对多模式:
第二章 静态链接
gcc -E hello.c -o hello.i
hello.c—->hello.i—>hello.s—>hello.o—>a.out
as/ld
编译: 编译就是经过语法分析,语义分析产生出汇编代码
gcc -S hello.i -o hello.s
上面的两步,现代的编译器合并成一个了,用这个程序/usr/lib/gcc/x86_64-linux-gnu/4.8/cc1
gcc只是一些程序的包装,如编译程序cc1,汇编器as,链接器ld
链接过程主要包括,地址和空间分配,符号决议(符号绑定,名称绑定),重定位
目标文件从结构上来说。它就是编译后的可执行文件格式,
目标文件的格式:
  1 ^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^A^@>^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@0^A^@^@^@^@^@^@^@^@^@^@@^@^@^@^@^@@^@^M^@
  2 ^@UH<89>å¿^@^@^@^@è^@^@^@^@¸^@^@^@^@]Ãhello world^@^@GCC: (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4^@^@^@^@^@^@^T^@^@^@^@^@^@^@^AzR^@^Ax^P^A^[^L^G^H<90>^A^@^@^\^@^@^@^\^@^@^@^@^@^@^@^    U^@^@^@^@A^N^P<86>^BC^M^FP^L^G^H^@^@^@^@.symtab^@.strtab^@.shstrtab^@.rela.text^@.data^@.bss^@.rodata^@.comment^@.note.GNU-stack^@.rela.eh_frame^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^    ^@^@^@^R^@^A^@^@^@^@^@^@^@^@^@^U^@^@^@^@^@^@^@^N^@^@^@^P^@^@^@    ^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@hello.c^@main^@puts^@^@^@^@^@^@^E^@^@^@^@^@^@^@
  3 ^@^@^@^E^@^@^@^@^@^@^@^@^@^@^@
  4 ^@^@^@^@^@^@^@^B^@^@^@
  5 ^@^@^@üÿÿÿÿÿÿÿ ^@^@^@^@^@^@^@^B^@^@^@^B^@^@^@^@^@^@^@^@^@^@^@
DLL/LIB  .so/.a都是安装可执行文件的格式存储的
程序源代码编译后的机器指令经常放在代码段code section里,代码段常见的名称.code/.text
全局变量和局部静态变量数据常放在数据段 .data
没有初始化的全局变量或者局部静态变量放在.bss
说明:本来。bss里面的内容也可以放在.data里面,但因为他们的值都是0,所以为他们在.data段分配空间并且存放数据0是没有必要的,
所以bss段只是为未初始化的全局变量和局部静态变量预留位而已,它并没有内容,所以并不会占用空间。

From Assembly Language Step-by-Step: Programming with Linux by Jeff Duntemann, regarding the .data section:

The .data section contains data definitions of initialized data items. Initialized data is data that has a value before the program begins running. These values are part of the executable file. They are loaded into memory when the executable file is loaded into memory for execution.

The important thing to remember about the .data section is that the more initialized data items you define, the larger the executable file will be, and the longer it will take to load it from disk into memory when you run it.

and the .bss section:

Not all data items need to have values before the program begins running. When you’re reading data from a disk file, for example, you need to have a place for the data to go after it comes in from disk. Data buffers like that are defined in the .bss section of your program. You set aside some number of bytes for a buffer and give the buffer a name, but you don’t say what values are to be present in the buffer.

There’s a crucial difference between data items defined in the .data section and data items defined in the .bss section: data items in the .data section add to the size of your executable file. Data items in the .bss section do not. A buffer that takes up 16,000 bytes (or more, sometimes much more) can be defined in .bss and add almost nothing (about 50 bytes for the description) to the executable file size.

SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000054  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000094  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  0000000000000000  0000000000000000  0000009c  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  0000009c  2**0 //只读数据段
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002a  0000000000000000  0000000000000000  000000a0  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000ca  2**0 //堆栈提示段
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000d0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
另外一个工具是readelf
CONTENTS等表示段的属性,CONTENTS表示该段在文件中存在,从中可以看出bss段在elf文件中不存在内容,
➜  src git:(dev) size SimpleSection.o
   text   data    bss    dec    hex filename
    176      8      4    188     bc SimpleSection.o
➜  src git:(dev) objdump -s -d SimpleSection.o //打印代码段的内容,即text section内容
static int x1 = 0;
static int x2 = 0;
变量x1和x2存放的段是不一样的。
__attribute__((section(“name”)) 属性可以把相应的函数和变量放到以name作为段名的段中
c++filt 工具可以用来把修饰后的C++函数或者变量名转换为本来的面目,当然像这样的工具还很多,可以参考binutils工具集合。
extern “C”
C++为了与C进行兼容,C++有一个用来声明或定义一个C的符号 extern “C”
extern “C”
{
int fun(int);
int var;
}
编译器会把大括号里面的当成C语言处理,目的就是不会按照C++的函数签名来处理里面的符号,而是按照C的规则来处理。一般主要是可能一些库是C语言的库,如果用在C++代码里,直接引用会出现找不到库里面的函数的情况。
#include <stdio.h>
namespace myname
{
    int var = 42;
}
extern “C” int _ZN6myname3varE;
extern “C” int var1 = 43; //此处不妥,因为这是一个c++的文件,extern “C”的本意是引用一个C的函数或DLL
int main()
{
    printf(“%d,%d,%d\n”,_ZN6myname3varE,myname::var,var1);
    return 0 ;
}
#ifdef __cplusplus
extern “C”
{
#endif
void ……………….
#ifdef __cplusplus
}
#endif
弱符号和强符号
C++来说,编译器默认函数和初始化了的全局变量为强符号,没有被初始化的全局变量是弱符号。
__attribute__ ((weak)) weak2 = 2;
弱引用和弱符号主要用于库的链接过程。
__attribute__ ((weakref)) void foo();
int main()
{
foo();
}
这种弱符号和弱引用对库非常有用,如果库中定义的弱符号可以被用户的强符号覆盖。
静态链接
ld a.o b.o -e main -o ab
-e main表示将main函数作为程序入口,ld默认将_start作为入口
-o ab表示链接输出文件名为ab,默认是a.out
符号解析
ar -t libc.a //查看静态库里面有哪些目标文件。
gcc -c -fno-builtin hello.c // -fno-builtin的作用是防止gcc自作聪明的把printf换成puts
collect2可以看出ld的包装
两种装入方式:覆盖装入和页映射
进程建立的三个步骤:
1. 创建一个独立的虚拟地址空间。(其实是创建相应的映射函数所需要的数据结构,
2. 读取可执行文件头,并且建立虚拟地址空间和可执行文件的映射关系。
3. 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
对于相同权限的段,把他们合并到一起当作一个段进行映射。
堆栈
操作系统通过使用VMA来对进程的地址空间进行管理。
很多进程都会在/proc文件夹下有个与其进程ID同名的文件夹,因为linux把什么都当成文件看待,所以这也是能够理解的。
什么是主设备号和次设备号??
如果有VMA的主次设备号都为0说明他们没有被映射到文件,这种VMA叫做匿名虚拟内存区域。
操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间,基本原则是将相同权限属性的,有相同映像文件的映射成一个VMA,一个进程基本上可以分为如下几个VMA区域:
代码VMA,只读可执行,有映像文件
数据VMA,可读写,可执行,有映像文件。
堆VMA,可读写,可执行,无映像文件,匿名,向上拓展
栈VMA,可读写,不可执行,无映像文件,匿名,向下拓展。
进程栈的初始化
Linux内核装载ELF过程简介
动态链接
共享对象的最终装载地址在编译时是不确定的。
共享对象在编译时不能假设自己在进程虚拟空间中的位置。
地址无关代码 -fPIC   (Position-independent Code)
装载时重定位是解决动态模块中有绝对地址引用的方法之一,但是它有一个很大的缺点是指令部分无法在多个进程之间共享,这就失去了动态链接节省内存的一大优势。
共享模块的全局变量问题

问题4:如果一个共享对象lib.so中定义了一个全局变量G,而进程A和进程B都使用了lib.so,那么当进程A改变这个全局变量G时,进程B中的G会受到影响吧?

不会。因为当lib.so被两个进程加载时,它的数据段部分在每个进程中都有独立的副本,而线程是共享数据段的,对他的修改,线程是可以看到的。

延迟绑定PLT
两个原因导致动态链接比静态链接性能低,1是间接寻址,2是动态链接工作在运行时完成,即程序开始执行时,动态连接器都要进行一次链接工作。
优化动态链接的方法:
延迟绑定的方法,思想是函数第一次被用时才进行绑定(符号查找,重定位等)
在linux下面,动态连接器ld.so实际上是一个共享对象,操作系统同样通过映射的方式将它加载到进程的地址空间。
操作系统把动态链接库加载后,就把控制权交给动态连接器的入口地址,当动态连接器获得控制权后开始执行一系列的初始化操作,然后根据当前的环境,可是对可执行文件进行链接工作,然后将控制权交给可执行文件的入口地址,程序开始正式的执行。
每个可执行文件都有一个.interp段,如下,表明了连接器的位置
➜  dynamic_so git:(dev) readelf -l Program1|grep interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
.dynamic段
?  dynamic_so git:(dev) ldd Program1
 linux-vdso.so.1 =>  (0x00007ffe8c57d000)
 ./Lib.so (0x00007f15aa21f000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f15a9e5a000)
 /lib64/ld-linux-x86-64.so.2 (0x00007f15aa421000)
有个很诡异的动态链接库,linux-gate.so.1 ,它实际上是内核虚拟共享对象,DSO,这涉及到linux的系统调用和内核(现在改名成linux-vdso.so.1了??)
动态符号表
动态链接时进程堆栈初始化信息
进程初始化的时候,堆栈里保存了关于进程执行环境和命令行参数等信息。
动态链接的步骤和实现,分三步:
1. 启动动态链接器本身。
2. 装载所有需要的共享对象。
3. 重定位和初始化。
这种具有一定限制条件的启动代码往往被称为自举(Bootstrap)
gcc main.c b1.so b2.so -o main -Xlinker -rpath ./
-Xlinker -rpath ./  在当前目录搜寻
当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号会被忽略。从动态链接器的装载顺序可以看到,它是按照广度优先的顺序进行装载的。
➜  dynamic_so git:(dev) ✗ ldd dl_function
 linux-vdso.so.1 =>  (0x00007ffe567cd000)
 libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f6d25f97000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6d25bd2000)
 /lib64/ld-linux-x86-64.so.2 (0x00007f6d2619b000)
➜  dynamic_so git:(dev) ✗ ./dl_function
sysbol sin not found: ./dl_function: undefined symbol: sin
➜  dynamic_so git:(dev) ✗ ./dl_function /lib/x86_64-linux-gnu/libm.so.6
1.000000
问题:
so和a库的本质区别是?
dlopen函数的搜索路径?
execve()函数的作用?
linux共享库的组织
SO-NAME规则和软连接的使用。
ldconfig,当系统中安装或更新一个共享库时,就需要运行这个工具,它会遍历所有默共享库目录,/lib, /usr/lib等,然后更新所有的软连接,使他们指向最新版的共享库,如果安装了最新版的共享库,那么ldconfig会为其创建相应的软连接。
链接名: libXXX.so.2.6.1 ,一般系统有静态和动态两个库,那如何选择呢?连接器会根据输出文件的情况来选择合适的版本。
Linux中的符号版本
共享库系统路径
开源操作系统遵守FHS标准,当然共享库的存放也遵守FHS,它们放在如下位置:
/lib 存放系统最关键和基础的共享库。
/usr/lib 这个目录存放一些非系统运行时所需要的关键性共享库,主要是一些开发时用到的共享库。
/usr/local/lib 第三方应用程序的库,
共享库查找过程:
我们知道任何一个动态链接模块所依赖的模块路径保存在.dynamic段里面。
链接器会在 /lib /usr/lib和由/etc/ld.so.conf配置文件指定的目录中查找共享库。
➜  /lib  cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf
➜  ld.so.conf.d  ls
fakeroot-x86_64-linux-gnu.conf  libc.conf                    x86_64-linux-gnu_EGL.conf
i386-linux-gnu_GL.conf          vmware-tools-libraries.conf  x86_64-linux-gnu_GL.conf
i686-linux-gnu.conf             x86_64-linux-gnu.conf        x86_64-linux-gnu_mirclientplatform.conf
➜  ld.so.conf.d  pwd
/etc/ld.so.conf.d
环境变量
LD_LIBRARY_PATH 可以用来临时改变某个应用程序的共享库的查找路径,而不会影响系统中的其他程序。
当然你也可以直接运行动态链接器来启动程序
/lib/ld-linux.so.2 -library-path /home/user /bin/ls
动态链接器按照如下的顺序来装载和查找共享对象:
1. 由环境变量LD_LIBRARY_PATH指定的路径。
2. 路径缓存文件/etc/ld.so.cache指定的路径。
3. 默认共享库目录,先/usr/lib 然后/lib
LD_PRELOAD
类似LD_LIBRARY_PATH,但会LD_LIBRARY_PATH先加载,同时也存在/etc/ld.so.preload文件。
LD_DEBUG
➜  dynamic_so git:(dev) LD_DEBUG=files ./Program1   //打印各种详细信息
      1096:
      1096: file=./Lib.so [0];  needed by ./Program1 [0]
      1096: file=./Lib.so [0];  generating link map
      1096:  dynamic: 0x00007f9ec7bf2e18  base: 0x00007f9ec79f2000   size: 0x0000000000201040
      1096:    entry: 0x00007f9ec79f25e0  phdr: 0x00007f9ec79f2040  phnum:
LD_DEBUG还有其他各种版本
 
共享库的创建和安装
gcc -shared -fPIC  -W1, -soname, my_soname -o library_name source_files library_files
参数 -W1的作用可以将指定的参数传递给链接器。此处-soname ,my_soname 用来指定输出共享库的SO-NAME。
-rpath选项,可以指定链接产生的目标程序的共享库查找路径
清除符号信息
正常情况下编译出来的共享库或可执行文件里带有符号信息和调试信息,这些信息占用的空间非常大,并且这些信息对于发布版来说没有多大的用处,所以可以使用strip工具清除掉。
strip libfoo.so
另外也可以使用:
ld -s/-S
或者
gcc -W1,-s
gcc -W1,-S
共享库的安装:
1. 最简单的方法就是把共享库复制到/lib等目录下面,然后运行ldconfig。
如果没有权限也可以用:ldconfig -n shared_library_directory ,编译时指定共享库的位置,GCC提供了两个参数-L和-l,前者为搜索路径,后者为共享库路径,当然也可以使用-rpath
共享库构造和析构函数
void __attribute__ ((constructor)) init_function(void)  //dlopen之前运行
void __attribute__ ((destructor)) fini_function(void)  // dlclose之后运行
windows下的动态链接
ELF默认导出所有的全局符号,但DLL不同,我们需要显示的告诉编译器我们需要导出的符号,
__declspec(dllexport)

__declspec(dllimport

)
内存:
1. malloc是如何分配内存的?
2. 局部变量存放在哪里?
3. 为什么一个编译好的简单的helloword程序也需要占用好几KB的空间?
4. 为什么程序一启动就有堆,I/O或异常系统调用?
5. 程序和内存是什么关系?
 
平坦的内存模型
直接访问地址,int* p = (int*) 0x12345678; ++*p; 但不能访问内核占用的内存区域。
栈:栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。一般在用户空间的最高地址分配。
堆:用来容纳应用程序动态分配的内存区域,malloc或new分配。
可执行文件映像:
保留区:是内存中受到保护而禁止访问的内存区域的总称。
 
–从图中可以看到‘Memory Mapping Segment’,动态链接库映射区
 
什么是栈?
没有栈就没有函数,没有局部变量,在经典的操作系统中,栈总是向下增长的。
栈保存了一个函数调用所需要的维护信息,通常称为堆栈帧,Stack Frame,或活动记录,Activate Record。
声名狼藉的C++返回对象
在C++里返回一个对象的时候,对象要经过两次拷贝构造函数的调用,1次拷贝到栈上的临时对象里,另一次把临时对象拷贝到存储返回值的对象里。
传值与传址。
运行库相当于向操作系统批发了一块较大的堆空间,然后零售给程序用。当时运行库需要一个算法来管理堆空间,这个算法就是堆的分配算法。
linux进程堆管理提供了两种分配方式,即两个系统调用,一个是brk() 另一个是 nmap()
int brk(void* end_data_segment);
brk设置进程数据段的结束地址,即扩大或者缩小数据段(数据段和BSS统称为数据段)
nmap向操作系统申请一段虚拟地址空间,当然这块内存可以映射到某个文件,当它不映射到某个文件时候,我们称这块空间是匿名的,匿名空间也可以拿来作为堆空间。
glibc的malloc函数是这样处理用户的空间请求的:
1. 小于128KB的请求,直接在现有的堆空间里,分配一块出去。
2. 大于128KB的请求,使用nmap函数分配一块匿名空间。
void *malloc(size_t nbytes)
{
        void* ret = nmap(0, nbytes, PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,0,0);
        if(MAP_FAILED == ret)
            return 0;
        return ret;
}
 
Q&A
1. 我可以重复释放两次堆里的同一片内存吗?
不能,几乎所有的堆实现里,都会在重复释放同一片堆里的内存时产生错误,glibc甚至能检测到这样的错误,并给出确切的错误信息。
2. 堆的增长总是向上的?
非也,linux算是,windows不是。
3. 调用malloc会不会最后调用系统调用或者API?
不一定,这个取决于当前进程向操作系统批发的空间是否够用,如果够用,他就直接从仓库里取出来卖给用户,如果不够,只能通过系统调用或者API向操作系统批发了。
4. malloc申请的内存,进程结束后还在不在?
不会存在,进程结束后,所有进程使用的资源,进程地址空间,物理内存,打开的文件,网络连接等都会被收回。
5. malloc申请的内存是不是连续的?
如果指虚拟空间,那每次申请的内存肯定是连续的。
如果指物理空间,不一定连续,因为一块连续的虚拟空间可能由很多不连续的物理空间组成的。
堆分配算法。
问题可以归结为:如何管理一大块连续的内存空间,按需分配,释放其中的空间?
1. 空闲链表:
2. 位图:
3. 对象池
入口函数和程序初始化
在main函数开始执行的时候,全局变量的初始化已经结束了,main函数的两个参数也被正确的传进来了,并且堆栈的初始化也悄悄的完成了,一些系统IO也被初始化了。
 1 #include <stdio.h>
  2
  3 void foo(void)
  4 {
  5     printf(“bye\n”);
  6 }
  7
  8 int main()
  9 {
 10     atexit(&foo); //相当于一个清理函数,在main函数执行结束后,被调用。
 11     printf(“endof main\n”);
 12 }
 13
➜  main_an git:(dev) ✗ ./atexit
endof main
bye
入口函数或者入口点,常常是运行库的一部分,一个典型的程序运行步骤大概如下:
1. 操作系统在创建进程后,把控制权交给程序的入口,这个入口往往是运行库的某个入口函数。
2. 入口函数对运行库和程序运行环境进行初始化,包括,堆,IO,线程,全局变量构造等等。
3. 入口函数在初始化后,调用main,正式开始运行程序主题部分。
4. main后,返回入口函数,进行清理工作,包括全局变量析构等,关闭IO,然后进行系统调用结束进程。
入口函数如何实现?
/* fwrite example : write buffer */#include <stdio.h>int main ()
{
  FILE * pFile;
  char buffer[] = { 'x' , 'y' , 'z' };
  pFile = fopen ("myfile.bin", "wb");
  fwrite (buffer , sizeof(char), sizeof(buffer), pFile);
  fclose (pFile);
  return 0;
}

每个进程都有一个私有的‘打开文件表’。

简单的实现va_list的方法:
#define va_list char*
#define  va_start (ap, arg) (ap=(va_list)&arg + sizeof(arg))
#define va_arg(ap,t) (*(t*)(ap+=sizeof(t)) – sizeof(t)) ) //达到两个目的,把指示指针移到下一个元素,同时取出当前元素的值。
#define va_end(ap) (ap = (va_list)0)
线程局部存储实现
假设我们要在线程中使用一个全局变量,但希望这个全局变量是线程私有的,而不是所有线程共享的,怎么办?
TLS,Thread Local Storage.
__thread int number;
__thread是GCC内置的线程局部存储设施(Thread-Local Storage),它的实现非常高效,与pthread_key_t向比较更为快速,其存储性能可以与全局变量相媲美,而且使用方式也更为简单。创建线程局部变量只需简单的在全局或者静态变量的声明中加入__thread说明即可。列如:
    static __thread char t_buf[32] = {‘\0′};
    extern __thread int t_val = 0;
凡是带有__thread的变量,每个线程都拥有该变量的一份拷贝,且互不干扰。线程局部存储中的变量将一直存在,直至线程终止,当线程终止时会自动释放这一存储。__thread并不是所有数据类型都可以使用的,因为其只支持POD(Plain old data structure)[1]类型,不支持class类型——其不能自动调用构造函数和析构函数。同时__thread可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量。另外,__thread变量的初始化只能用编译期常量,例如:
    __thread std::string t_object_1 (“Swift”);                   // 错误,因为不能调用对象的构造函数
    __thread std::string* t_object_2 = new std::string (); // 错误,初始化必须用编译期常量
    __thread std::string* t_object_3 = nullptr;                // 正确,但是需要手工初始化并销毁对象
 
除了以上之外,关于线程局部存储变量的声明和使用还需注意一下几点:
  1. 如果变量声明中使用量关键字static或者extern,那么关键字__thread必须紧随其后。
  2. 与一般的全局变量或静态变量一样,线程局部变量在声明时可以设置一个初始化值。
  3. 可以使用C语言取地址符(&)来获取线程局部变量的地址。
C++全局构造和析构
glibc,.init和.finit段,这两个段最终会拼成_init和_finit函数。它们先于/后于main执行。
fread的实现。
声明:
 _Check_return_opt_ _CRTIMP size_t __cdecl fread(
_Out_writes_bytes_(_ElementSize*_Count) void * _DstBuf,
_In_ size_t _ElementSize,
 _In_ size_t _Count,
 _Inout_ FILE * _File);
缓冲: 一个显而易见的方案是将对控制台连续的多次写入放在一个数组里,等到数组被填满后再一次完成系统调用而写入,实际上这就是缓冲最基本的想法。
读文件的缓冲,读文件的时候,先看看在缓冲区有没有,没有则一次性读取一大块内容到缓冲区,这样就可以避免大量的实际文件访问。
写文件缓冲,与写缓冲区相关的几个函数:
1。 fflush
2.  setvbuf // _IONBF, _IOLBF,行缓冲,就是每次遇到换行符就flush,_IOFBF,仅当缓冲区满才flush
3. setbuf 设置文件的缓冲。
Linux\UNIX : \n
Mac OS: \r
Windows: \r\n
系统调用和API
中断 是系统调用的入口, linux下是 0x80 ,windows是0x2E
系统调用的原理
特权级与中断
中断?中断就是硬件或者软件发出一个请求,要求CPU暂停当前的工作区处理更加重要的事情,举个例子,当你按下键盘上的一个按键的时候,键盘上的芯片会发送一个信号给CPU,CPU接收到信号后就知道键盘被按下了,然后再去询问键盘那个键被按下了?
中断一般有两个属性,一个是中断号,另一个是中断处理程序,类似信号。
内核中有一个数组称为中断向量表(Interrupt Vector Table)这个数组的第n项包含了指向第n号中断的中断处理程序。
中断有两种类型: 硬件中断和软件中断,
由于中断号是有限的,所以一般一个中断号或几个中断号对应所有的系统调用。例如linux上使用0x80来触发所以的系统掉用,哪问题来了,操作系统如何知道哪个系统调用被触发了?每个系统调用都有一个系统调用号,在中断前这个系统调用号会被存放到某个固定的寄存器里面。
基于int的Linux的经典系统调用实现。
Linux的新型系统调用机制
我们可以看到linux-vdso.so.1没有跟任何实际的文件相对应,它是linux用于支持新型系统调用的“虚拟”共享库,这个库总是被加载在oxffffe000的位置上。
➜  makefile_ll git:(master) cat /proc/self/maps
00400000-0040b000 r-xp 00000000 08:01 793818                             /bin/cat
0060a000-0060b000 r–p 0000a000 08:01 793818                             /bin/cat
。。。。。。。。。。。。。。。。。。。。            /lib/x86_64-linux-gnu/ld-2.19.so
7f9a8a70a000-7f9a8a70b000 rw-p 00000000 00:00 0
7fff1ba20000-7fff1ba41000 rw-p 00000000 00:00 0                          [stack]
7fff1bad0000-7fff1bad2000 r-xp 00000000 00:00 0                          [vdso] 占用4096字节
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
➜  makefile_ll git:(master) dd if=/proc/self/mem of=linux-gate.dso bs=4096 skip=1048574 count=1
/proc/self/mem总是等价于当前进程的内存快照,bs代表一次性搬运的字节数,skip代表要从文件开头跳过多少块,count表示要搬运多少块。

 

发布者

690130229

coder,喜欢安静,喜欢读书,wechat: leslie-liya

发表评论