11.程序员的自我修养---运行库

11.1 入口函数和程序初始化

	11.1.1 程序是从 main 开始的吗

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

	int a = 3;

	int main(int argc, char* argv[])
	{
	    int *p = (int *) malloc(sizeof(int));
	    scanf("%d", p);
	    printf("%d", a + *p);
	    free(p);
	}

	从代码可以看出,在程序刚刚执行到 main 的时候,全局变量的初始化过程已经结束了(a 的值已经确定了),main 函数的2个参数(argc,argv)
  也被正确的传了进来。此外,在你不知道的时候,堆和栈的初始化已经完成了,一些系统IO也被初始化了,因此可以放心的使用 printf 和 malloc。
    操作系统在装载程序之后,首先运行的代码不是 main 的第一行,而是某些其他代码,这些代码负责准备好 main 函数执行所需要的环境,并且
  负责调用 main 函数,这时候你才可以在 main 函数里面大胆的写各种代码:申请内存,使用系统调用,触发异常,访问IO。在 main 函数返回之后,
  它会记录 main 函数的返回值,调用 atexit 注册的函数,然后结束进程。
    运行这些代码的函数称为称为入口函数或入口点。程序的入口点实际上是一个程序初始化和结束部分,它往往是运行库的一部分。一个典型的程序运行步骤
  如下:
  	1.操作系统在创建进程后,把控制权交给了程序的入口,这个入口往往是运行库中的某个入口函数
  	2.入口函数对运行库和程序运行环境进行初始化,包括堆,IO,线程,全局变量构造等等
  	3.入口函数在完成初始化之后,调用 main 函数,正式开始执行程序主题部分
  	4.main 函数执行完毕以后,然后返回到入口函数,入口函数进行清理工作,包括全局变量习惯,堆销毁,关闭 IO 等,然后进行系统调用结束进程。

  11.1.2 入口函数如何实现

  	glibc 入口函数
  		glibc 的启动过程在不同情况下差别很大,比如静态的 glibc 和动态的 glibc 的差别,glibc 用于可执行文件和用于共享库的差别,这样的差别可以组合
  	  出4种情况。
  	    glibc 的程序入口是 _start(这个入口是由 ld 链接器默认的连接脚本指定的,我们也可以通过相关参数设定自己的入口)。

  	    glibc 最终调用了 __lib_start_main 的函数.加粗部分的代码是对该函数的完整调用过程,其中开始的7个压栈指令用于给函数传递参数。在最开始的地方还有
  	  3条指令,它们的作用分别是:
  	  1. xor %ebp, %ebp : 这其实是让 ebp 寄存器清零。xor 的用处是把后面的2个操作数异或,结果存储在第一个操作数里。这样做的目的表明当前是程序的最外层函数。
  	  2. pop %esi 以及 mov %esp, %ecx : 在调用 _start 前,装载器会把用户的参数和环境变量压入栈中,按照其压栈的方法,实际上栈顶的元素是 argc, 而接着其下
  	  就是 argv 和环境变量的数组。
  	    pop %esi 将 argc 存入了 esi,而 mov %esp, %ecx 将栈顶地址(此时就是 argv 和环境变量 (env) 数组的起始地址)传给 %ecx。现在 %esi 指向 argc, %ecx
  	  指向 argv 以及环境变量数组。

  	环境变量:
  		环境变量是存在于系统中的一些公用数据,任何程序都可以访问。通常来说,环境变量存储的都是一些系统的公共信息,例如系统搜索路径,当前 OS 版本等。环境变量的格式
  	  为 key=value 的字符串,C 语言可以使用 getenv 这个函数来获取环境变量信息。
  	  _start -> __libc_start_main : 
  	  	__libc_start_main 和 _start 函数里的调用一致,一共有7个参数,其中 main 又第一个参数传入,紧接着是 argc 和 argv。除了 main 函数的指针外,还需要传入3个
  	 函数指针,分别是:
  	 	1. init : main 调用之前的初始化工作
  	 	2. fini : main 结束后的收尾工作
  	 	3. rtld_fini : 和动态加载有关的收尾工作,rtld 是 runtime loader 的缩写
  	 	最后的 stack_end 标明了栈低的地址,即最高的栈地址。让 __environ 指针指向原来紧跟在 argv 数组之后的环境变量数组。

  	 	_start -> __libc_start_main -> exit : 
  	 	  其中 __exit_funcs 是存储由 __cxz_atexit 和 atexit 注册的函数的链表,而这里的循环则会遍历该链表并逐个调用这些注册的函数。最后的 _exit 函数由汇编实现,
  	 	且与平台相关。
  	 	  _exit 的作用仅仅是调用了 exit() 这个系统调用。也就是说,_exit 调用后,进程就会直接结束。程序正常结束有2种情况,一种是 main 函数正常返回,一种是程序中调用
  	 	exit 退出。在 __libc_start_main 里我们可以看到,即使 main 函数返回了,exit 也会被调用。exit 是进程正常退出的必经之路,因此把调用 atexit 注册的函数任务交给
  	 	exit 来完成可以说是万无一失的。

  	 	  我们看到 _start 和 _exit 的结尾都有一个 hlt 指令,这是什么作用呢? 在 Linux 里,进程必须使用 exit 系统调用结束。一旦 exit 被调用,程序的运行就会终止,因此
  	 	实际上 _exit 末尾的 hlt 不会执行,从而 __libc_start_main 永远不会返回,以致 _start 末尾的 hlt 指令不会执行。 _exit 里的 hlt 指令是为了检测 exit 系统调用是否
  	 	成功。如果失败,程序就不会终止, hlt 指令就可以发挥作用强制把程序停止下来。而 _start 里的 hlt 的用处也是如此,但是为了预防某种没有调用 exit 就返回到 _start 的情况。

  11.1.3 运行库与IO
  		IO 的全称是 Input/Output,即输入输出。对于计算机来说,IO代表了计算机与外界交互,交互的对象可以是人或者其他设备。
  		对于程序来说,IO覆盖的范围还要宽广一些。一个程序的IO指代了程序与外界的交互,包括文件,管理,网络,命令行,信号等。更广义的讲,IO指代任何操作系统理解为 '文件' 的事务。许多
      操作系统,包括Linux 和 Windows,都将各种输入和输出概念的实体---包括设备,磁盘文件,命令行等---统称为文件,因此这里所说的文件是一个广义的概念。
        对于一个任意类型的文件,操作系统会提供一组操作函数,这包括打开文件,读文件,写文件,移动文件指针等。
        在操作系统层面上,文件操作也有类似于 FILE 的一个概念,在 Linux 里,这叫做文件描述符,而在 Windows 里面,叫做 句柄(Handle)。用户通过某个函数打开文件以获得句柄,以后用户操作
      文件皆通过该句柄进行。
        设计这么一个句柄的原因在于,句柄可以防止用户随意读写操作系统内核的文件对象。无论是 Linux 还是 Windows,文件句柄总是和内核的文件对象相关联的,但如何关联细节用户并不可见。
      内核可以通过句柄计算出内核文件对象的地址,但此能力并不对用户开放。

        在Linux 中,值为 0,1,2的 fd 分别代表输入,输出和标准错误。在程序中打开文件得到的 fd 从 3 开始增长。fd 具体是什么呢?在内核中,每个进程都有一个私有的 '打开文件表',这个表
      是一个指针数组,每个元素都指向一个内核的打开文件对象。而 fd,就是这个表的下标。当用户打开一个文件时,内核会在内部生产一个打开文件对象,并在这个表中找到一个空闲,让这一项指向生成
      的打开问对象,并返回这一项的下标作为 fd.由于这个表处于内核,并且用户无法访问到,因此用户及时拥有 fd,也无法得到打开文件的对象,只能通过操作系统提供的函数来操作。
        在 C 语言中,操作文件的渠道则是 FILE 结构,不难想象,C 语言中的 FILE 结构必定和 fd 有着一对一的关系,每个 FILE 结构都会记录自己唯一对应的 fd 。
        内核指针 p 指向该进程的打开文件表,所以只要有 fd,就可以用 fd + p 来得到打开文件表的某一项地址。 stdin,stdout,stderr 均是FILE结构的指针。
        对于 Windows 中的句柄,与 Linux 中的 fd 大同小异,不过 Windows 的句柄并不是打开文件表的下标,而是其经过某种线性变换之后的结果。

        在大概了解了 IO 为何物之后,我们就知道 IO 初始化的职责是什么了。首先 IO 初始化函数需要在用户空间建立 stdin,stdout,stderr 以及对应的 FILE 结构,使得程序进入 main
      之后可以直接使用 printf,scanf等函数。

  11.1.4 MSVC CRT 的入口函数初始化
  	MSVC 的入口函数初始化主要包含2个部分,堆初始化和 IO 初始化。堆初始化由函数 _heap_init 完成。
11.2 C/C++ 运行库
	11.2.1 C 语言运行库
		任何一个 c 程序,它的背后都有一套庞大的代码来进行支撑,以使得该程序能够正常运行。这套代码至少包含入口函数,以及其所依赖的
	  函数所构成的函数集合。当然,它还理应包括各种标准库的函数实现。
	    这样的一个代码库集合称之为运行时库(Runtime Library)。而 c 语言的运行库,即被称为 c 运行库(CRT)。

	    一个 c 语言运行库大致包含如下功能:
	    	1.启动与退出:包含入口函数以及入口函数所依赖的其他函数等
	    	2.标准函数:由 c 语言标准规定的 c 语言标准库所拥有的函数实现
	    	3.IO:IO功能的封装和实现
	    	4.堆:堆的实现和封装
	    	5.语言实现:语言中一些特殊功能的实现
	    	6.调试:实现调试功能的代码

	11.2.2 c 语言标准库
		c 语言的标准库非常轻量:
			1.标准输入输出(stdio.h)
			2.文件操作(stdio.h)
			3.字符操作(ctype.h)
			4.字符串操作(string.h)
			5.数学函数(math.h)
			6.资源管理(stdlib.h)
			7.格式转换(stdlib.h)
			8.时间/日期(time.h)
			9.断言(assert.h)
			10.各种类型上的常数(limits.h & flost.h)
		除此之外,c 语言标准库还有一些特殊的库,用于执行一些特殊的操作,例如:
			1.变长参数(stdarg.h) 
			2.非局部跳转(setjmp.h)

	    1.变长参数
	    	变长参数是 c 语言的特殊参数形式,函数声明如下:
	    		int printf(const char * format, ...);
	    	表示 printf 函数除了第一个参数类型为 const char* 之外,其后可以追加任何数量,任意类型的参数。在实现部分,可以使用 stdarg.h
	      里的多个宏来访问各个额外的参数 :假设 lastarg 是变长参数函数的最后一个具体参数(例如 printf 里的 format),那么在函数内部定义的类型为 
	      va_list 的变量:
	      	va_list ap;
	        该变量以后将会依次指向各个可变参数。ap 必须用宏 va_start 初始化一次,其中 lastarg 必须是函数的最后一个具体的参数。
	     	 va_start(ap, lastarg);
	        此后,可以使用 va_arg 宏来获得下一个不定参数(假设已知其类型为 type):
	        type next = va_arg(ap, type);
	        在函数结束之前,还必须用宏 va_end 来清理现场。在研究这几个宏之前,我们要先了解变长参数的实现原理。变长参数的实现原理得益于c语言默认的 cdecl
	      调用惯例的自右向左压栈传递方式。设想如下的函数:
	      	int sum(unsigned num, ...);
	      	其语义如下:
	      	  第一个参数传递一个整数 num, 紧跟着会传递 num 个整数,返回 num 个整数的和。
	      	当我们调用:
	      		int n = sum(3, 16, 38. 53);
	      		可以通过 num 的地址计算出其他几个参数的地址。

	        下面我们来看下 va_list 等宏是如何实现的。
	        va_list 实际上是一个指针,用来指向各个不定参数。由于类型不明,因此这个 va_list 以 void* 或 char* 为最佳选择。va_start 将 va_list 定义的指针
	      指向函数最后一个参数后面的位置,这个位置就是第一个不定参数。va_arg 获取当前不定参数的值,并根据不定参数的大小将指针移到下一个参数。va_end 将指针清0.

	      变长参数的宏:
	      	很多时候我们希望在定义宏的时候也能够像 printf 一样可以使用变长参数,即宏的参数也可以是任意个,这个功能可以由编译器的变长参数宏实现。在 gcc 编译器下,
	      变长参数可以使用 '##' 宏字符串来连接操作实现。如: 
	        #define printf(args ...) fprintf(stdout, ##args)
	        那么 printf("%d%s", 123, 'hello') 就会被展开为:
	        fprintf(stdout, "%d%s", 123, 'hello');

	    2.非局部跳转
	    	使用非局部跳转,可以实现从一个函数体内向另一个事先登记过的函数体内跳转,而不用担心堆栈混乱。
	    	#include <setjmp.h>
	    	#include <stdio.h>

	    	jmp_buf b;

	    	void f()
	    	{
	    		longjmp(b,1);
	    	}

	    	int main()
	    	{
	    		if (setjmp(b)) 
	    			printf("world");
	    		else
	    		{
	    			printf("hello");
	    			f();
	    		}
	    	}

	    	输出: hello world
	    	实际上,当 setjmp 正常发挥的时候,会返回0,因此会打印 'hello'。而 longjmp 的作用,就是让程序的执行流回到当初 setjmp 返回的时刻,并且返回由 longjmp 
	      指定的返回值(longjmp 的参数是2),也就是1,自然接着会打出 'world' 并退出。换句话说,longjmp 可以让程序 '时光倒流'回 setjmp 返回的时刻,并改变其行为。

	11.2.3 glibc 与 MSVS CRT
		运行库是平台相关的,因为它与操作系统的结合非常紧密。c 语言的运行库从某种程度来说是 c 语言的程序和不同操作系统平台之间的抽象层,它将不同的操作系统 API 抽象成
	  相同的库函数。比如我们可以在不同的操作系统平台下使用 fread 来读取文件,而事实上 fread 在不同的操作系统平台下的实现是不同的,但作为运行库的使用者我们不需要关心
	  这一点。虽然各个平台下的 c 语言运行库提供了很多功能,但很多时候它毕竟有限,比如用户的权限控制,操作系统线程创建等都不是属于标准的 c 语言运行库。于是我们不得不通过
	  其他办法,诸如绕过c语言运行库直接调用操作系统 API 或者使用其他的库。Linux 和 Windows 平台下的两个主要 c 语言运行库分别为 glibc(GNU C Library) 和 MSVCRT。

	  glibc : GNU C Library。
	  	glibc 的发布版本主要由2个部分组成,一部分是 头文件,比如 stdio.h,stdlib,h等,它们往往位于 /usr/include;另外一部分则是库的二进制文件部分。二进制部分主要就是
	  c 语言标准库,它有静态和动态2个部分。动态库位于 /lib/libc.so.6;而静态库位于 /usr/lib/libc.a。事实上,glibc 除了 c 标准库之外,还有几个辅助程序运行的运行库,
	  这几个文件可以称得上是真正的 '运行库'。他它们就是 /usr/lib/crt1.o, /usr/lib/crti.o 和 /usr/lib/crtn.o。

	  glibc 启动文件:
	  	crt1.o 里面包含的就是程序的入口函数 _start,由它负责调用 __libc_start_main 初始化 libc 并且调用 main 函数进入真正的程序主体。实际上最初开始的时候它并不叫
	  crt1.o,而是叫 crt.o,包含了基本的启动,退出代码。由于当时有些链接器对链接时目标文件和库的顺序有依赖,crt.o 这个文件必须被放在链接器命令行中的所有输入文件中的第一个,
	  为了强调这一点,crt.o 被更名为 crt0.o,表示它是链接时输入的第一个文件。
	    后来由于 C++ 的出现和 ELF 文件的改进,出现了必须在 main() 函数之前执行的全局/静态对象构造和必须在 main() 函数之后执行的 全局/静态对象析构。为了满足类似的要求,
	  运行库在每个目标文件中引入了2个与初始化相关的段 '.init' 和 '.finit'。运行库会保证所有位于这2个段中的代码会先于/后于 main() 函数执行,所以用它们来实现全局构造和析构
	  就是很自然的事情。在链接器进行连接时,会把所有的输入目标文件中的 '.init' 和 '.finit' 按照顺序收集起来,然后将它们合并成输出文件中的 '.init' 和 '.finit'。但是2这个输出
	  的段中所包含的指令还是需要一些辅助代码来帮助它们启动(比如计算 GOT 之类的),于是引入了2个目标文件分别来帮助实现初始化函数的 crti.o 和 crtn.o。
	    与此同时,为了支持新的库和可执行文件格式,crt0.o 也进行了升级,变成了 crt1.o。 crt0.o 和 ctr1.o 之间的区别是 crt0.o 为原始的,不支持 '.init' 和 '.finit'的启动
	  代码,而 ctr1.o 是改进过后的,支持 '.init' 和 '.finit' 的版本。
	  	为了方便运行库调用,最终输出文件中的 '.init' 和 '.finit' 两个段实际上分别包含的是 _init() 和 _finit() 这2个函数。crti.o 和 crtn.o 这2个目标文件中包含的代码实际
	  上是 _init() 函数和 _finit() 函数的开始和结尾部分,当这2个文件和其他目标文件安装顺序连接起来以后,刚好形成2个完成的函数 _init() 和 _finit()。
	    objdump -dr /usr/lib64/crti.o
	    于是在最终连接完成之后,输出的目标文件中的 '.init' 段只包含一个函数 _init(),这个函数的开始部分来自于 crti.o 的 '.init' 段,结束部分来自crtn.o 的 '.init' 段。为了
	  保证最终的输出文件中的 '.init' 和 '.finit' 的正确性,我们必须保证在链接时,crti.o 必须在用户目标文件和系统库之前,而 crtn.o 必须在用户目标文件和系统库之后.链接器的输入文件
	  顺序一般是 ld crt1.o crti.o [user_objects] [system_libraries] crtn.o
	    由于 crt1.o(crt0.o) 不包含 '.init' 段和 '.finit' 段,所以不会影响最终生成 '.init' 和 '.finit' 段时的顺序。
	    gcc -nostartfile -nostdlib //分别用来取消默认的启动文件和 c 语言运行库

	    其实 C++ 全局对象的构造函数和习惯函数并不是直接放在 .init 和 .finit 段里面的,而是把一个指向所有构造函数/习惯函数的调用放在里面,由这个函数进行真正的析构和构造的。
	    除了全局对象构造和析构之外,.init 和 .finit 还有其他作用。由于他们的特殊性(在 main 之前/之后执行),一些用户监控程序的性能,调试等工具经常利用它们进行一些初始化和
	  反初始化的工作。当然我们也可以用 '__attribute__((section(".init")))' 将函数放到 .init 段里面,但是要注意的是普通函数放在 '.init' 是会破坏它们的结构的,因为普通
	  函数的返回指令使得 _init() 函数会提前返回,必须使用汇编指令,不能让编译器产生 'ret' 指令。

	  gcc 平台相关目标文件:
	  	还有 crtbeginTo.o, libgcc.a, libgcc_eh.a, crtend.o。严格来说这几个文件实际上不属于 glibc,它是 gcc 的一部分。它们都位于 gcc 的安装目录下.
	  	首先是 crtbeginTo.o 及 crtend.o,这2个文件是真正用于 c++ 全局构造和析构的目标文件。那么为什么有了 crti.o 和 crtn.o 之后,还需要2个文件呢?我们知道,c++这样
	  的语言的实现是跟编译器密切相关的,而 glibc 只是c语言的运行库,它对 c++ 的实现并不了解。而 gcc 是 c++ 的真正实现者,它对 c++ 的全局构造和析构了如指掌。于是它提供了
	  2个目标文件 crtbeginTo.o 和 crtend.o 来配合 glibc 实现c++的全局构造和析构。事实上真正的全局构造和析构则是由crtbeginTo.o 和 crtend.o 来实现。

	  
11.3 运行库与多线程
	11.3.1 CRT 的多线程困扰

		线程的访问权限:
			线程的访问能力非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,然后这是很少见的情况)
		  但实际运用中线程也拥有自己的私有财产空间,包括:
		  	1.栈(尽管并非完全无法被其他线程访问,但一般情况下仍然可以认为是私有的数据)
		  	2.线程局部存储(thread local storage, TLS)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的大小。
		  	3.寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此为线程私有的

		 从 c 程序员角度来看,数据在线程之间是否私有如下:
		 线程私有  		线程之间共享(进程所有)
		 局部变量			全局变量
		 函数的参数 		堆上的数据
		 TLS 数据		函数里的静态变量
		 				程序代码,任何线程都有权利读取并执行任何代码;打开文件,A线程打开的文件可以由B线程读写

		 多线程运行库:
		 	现有的版本的 c/c++ 标准(c89,c99) 对多线程可以说是只字不提,因此相应的 c/c++ 运行库也无法针对线程提供什么帮助。对于 c/c++ 标准库来说,
		 线程相关的部分是不属于标准库的内容的,它跟网络,图形图像等一样,属于标准库之外的系统相关库。这里所说的 '多线程相关'主要有2个方面:一方面是提供
		 那些多线程操作的接口;另外一方面是 c 运行库本身要能够在多线程的环境下正确运行。

		    对于第一方面,主流的 CRT 都会有相应的功能。比如 windows下,MSVC CRT 提供了诸如 _begintread(), _endthread()等函数用于线程的创建和退出;
		 Linux 下, glibc 也提供了一个可选的线程库 pthread,它提供了诸如 pthread_create()等函数。很明显,这些函数都不属于标准的运行库,它们都是平台相关的。
		    对于第二方面,c 语言运行库必须支持多线程的环境,这是什么意思呢?实际上,最初 CRT 在设计的时候是没有考虑多线程环境的,因为当时根本没有多线程这样的概念。
		    (1) errno:在 c 标准库里,大多数错误代码都是在函数返回之前赋值给名为 errno 的全局变量里面的。多线程并发的时候,有可能 A 线程的 errno 的值在获取之前就
		       被 B 线程给覆盖掉了
		    (2) strtok() 等函数都会使用函数内部的局部静态变量来存储字符串的位置,不同的线程调用这个函数都会将它内部的静态局部变量弄混乱了
		    (3) malloc/new 与 free/delete : 堆分配/释放 函数或关键字在不加锁的情况下是线程不安全的。由于这些函数或关键字的调用十分频繁,因此在保证线程安全的时候
		        显得十分繁琐
		    (4) 异常处理:在早期的 c++ 运行库里面,不同的线程跑出的异常会彼此冲突,从而造成信息丢失的情况
		    (5) printf/fprintf 及其他的 IO 函数:流除数函数同样是线程不安全的,因为它们共享了同一个控制台或者文件输出。不同的输出并发时,信息会混杂在一起。
		    (6) 其他线程不安全函数:包括与信号相关的一些函数

		    通常情况下,c 标准库在不进行线程安全保护的情况下自然的具有线程安全属性的函数由(不考虑 errno 的因素):
		    (1) 字符处理(ctype.h)
		    (2) 字符串处理函数(string.h)
		    (3) 数学函数(math.h)
		    (4) 字符串转整数/浮点数(stdlib.h)
		    (5) 获取环境变量(stdlib.h)
		    (6) 变长数组辅助函数(stdarg.h)
		    (7) 非局部跳转函数(setjmp.h)

    11.3.2 CRT 改进
    	使用 TLS:
    		多线程运行库具有什么样的改进呢?首先,errno 必须称为各个线程的私有成员。在 glibc 中,errno 被定义为一个宏,如下:
    		#define errno (*__errno_location())
    		函数 __errno_location 在不同的库版本下有不同的定义,在单线程版本中,它仅直接返回全局变量 errno 地址。而在多线程版本中,不同线程调用 __errno_location
    	  返回的地址则各不相同。

    	加锁:
    		在多线程版本的运行库中,线程不安全的函数内部都会自动进行加锁,包括 malloc,printf等,而异常处理的错误也早早的解决了。因此在使用多线程版本的运行库时,即使在
    	  malloc/new 前后不进行加锁,也不会出现并发冲突。

    	改进函数调用方式:
    		strtok() => strtok_r();

    11.3.3 线程局部存储实现
    	很多时候,开发者在编写多线程程序的时候都希望存储一些线程私有的数据。我们知道,属于每个线程的私有数据包括线程的栈和当前的寄存器,但是这2种存储都是非常不可靠的,栈会在
      每个函数退出和进入的时候改变;而寄存器更是少的可怜,我们不可能拿寄存器去存储所需要的数据。假设我们希望在线程中使用一个全局变量,但希望这个全局变量是线程私有的,而不是所有
      线程共享的。怎么办? 这时候就需要用到线程局部存储(TLS)这个机制了。TLS 的用法很简单,如果要定义一个全局变量为 TLS 类型的,只需要在它定义前面加上相应的关键字即可。对于 gcc
      来说,这个关键字就是 __thread,比如我们定义一个 TLS 的全局整形变量:
        __thread int number;
        一旦一个全局变量被定义为 TLS 类型的,那么每个线程都会拥有这个变量的一个副本,但任何线程对该变量的修改都不会影响其他线程中该变量的副本。
        我们知道对于一个 TLS 变量来说,它有可能是一个 C++ 的全局对象,那么每个线程在启动的时候不仅仅是复制 '.tls' 的内容那么简单,还需要把这些 TLS 对象初始化,必须逐个的调用它们
      的全局构造函数,而当线程退出时,还要逐个地将它们析构,正如普通的全局对象在进程启动和退出时都要构造和析构一样。
11.4 C++ 全局构造与析构
	在 c++ 的世界里,入口函数还肩负着另外一个艰巨的使命,那就是在 main 的前后完成全局变量的构造和析构。

	11.4.1 glibc 全局构造和析构
		前面介绍 glibc 的启动文件时已经介绍了 '.init' 和 '.finit' 段,我们知道这2个段中的代码最终会被拼成两个函数 _init() 和 _finit(),
	  这2个函数会先于/后于 main 函数执行。

	    __start -> __libc_start_main -> __libc_csu_init -> _init -> __do_global_ctors_aux;
	    __init 调用了一个叫做 __do_global_ctors_aux 的函数,如果你在 glibc 源代码里面查找这个函数,是不可能找到的。因为它不属于 glibc,而是
	  来自 gcc 提供的一个目标文件 crtbegin.o。链接器在进行最终链接时,有一部分目标文件来自于 gcc,它是那些与语言相关的支持函数。很明显,c++ 的全局
	  对象构造与语言密切相关,相应负责构造的函数来自于gcc也非常容易理解。
11.5 fread 实现
	size_t fread {
		void *buffer,
		size_t elementSize,
		size_t count,
		FILE *stream
	}
	BOOL ReadFile {
		HANDLE hFile,  // 所要读取的文件句柄,对应 stread参数
		LPVOID lpBuffer,
		DWORD nNumberOfBytesToRead, // elementSize * count
		LPDWORD lpNumberOfBytesRead,
		LPOVERLAPPED lpOverLapped
	}

	11.5.1 缓冲
		缓冲最为常见于IO系统中,设想一下,当希望向屏幕输出数据的时候,由于程序逻辑的关系,需要调用多次 printf 函数,并且每次只写入
	  几个字符,如果每次写数据都要进行一次系统调用,让内核向屏幕写数据,就显得过于低效了,因为系统调用的开销是很大的,它要进行上下文
	  切换,内核参数检查,复制等,如果频繁的进行系统调用,将会严重影响程序和系统的性能。

	    一个显而易见的可行方案是将对控制台连续的多次写入放在一个数组里,等到数组被填满之后再一次性的完成系统调用写入,实际上这就是缓冲
	  最基本的思想。当读文件的时候,缓冲同样存在。我们可以在 CRT 中为文件建立一个缓冲,当要读数据的时候,首先看看这个文件的缓冲里面有没有
	  数据,如果有数据就直接从缓冲中读取。如果缓冲是空的,那么 CRT 就通过操作系统一次性读取文件一块较大的内容填充。这样,如果每次读取文件
	  都是一些尺寸很小的数据,那么这些读取操作大多直接从缓冲中取得,可以避免大量的实际文件访问。

	    除了读文件有缓冲以外,写文件也存在着同样的情况,而且写文件比读文件更复杂,以外当我们通过 fwrite 向文件写入一段数据时,此时这些数据
	  不一定被真正的写到文件中,而是可能还存在于文件的写缓冲里面,那么此时如果系统崩溃或者进程以外退出时,有可能导致数据的丢失,于是 CRT 还提供
	  了一系列与缓冲相关的操作用于弥补缓冲所带来的问题。C 语言标准库提供与缓冲相关的几个基本函数,如下:
	  	int fflush(FILE *stream);
	  	int setvbuf(FILE *stream, char *buf); // 无缓冲 _IONBF, 行缓冲 _IOLBF, 全缓冲 _IOFBF
	  	void setbuf(FILE *stream, char *buf); // 设置文件缓冲,等价于 void setvbuf(stream, buf, _IOFBF, BUFSIZE);

	  	所谓 flush 一个缓冲,是指对写缓冲而言,将缓冲内的数据全部写入实际的文件,并将缓冲清空,这样可以保证文件处于最新状态。之所以需要 flush,
	  是因为写缓冲使得文件处于一种不同步的状态,逻辑上一些数据已经写入文件,但实际上这些数据仍然在缓冲中,如果此时程序意外的退出(发生异常或者断电),
	  那么缓冲里的数据将没有机会写入文件,flush 可以在一定程度上避免这样的情况发生。

	11.5.2 fread_s
		fread -> fread_s(增加缓冲溢出保护,加锁) -> _fread_nolock_s(循环读取缓冲) -> _read(换行符换砖)-> ReadFile(Windows 文件读取API)

	11.5.3 fread_nolock_s

	11.5.4 _read

	11.5.5 文件换行
		在 Windows 的文本文件中,回车(换行)的存储方式是 0x0D(用CR表示),0x0A(用LF表示)这2个字节,以C语言字符串表示则是 '\r\n'。而在其他系统中,回车
	  的表示有区别,例如:
	  	1.Linux/Unix : 回车用 \n 表示
	  	2.Mac OS : 回车用 \r 表示
	  	3.Windows : 回车用 \r\n 表示

	  	而在 C 语言中,回车始终用 \n 来表示。因此在以文件模式读取文件的时候,不同的操作系统需要将各自的回车符表示转换为 C 语言的形式。也就是:
	  	1.Linux/Unix : 不做改变
	  	2.Mac OS : 每次遇到 \r 就将其改变为 \n
	  	3.Windows : 将 \r\n 改为 \n

11.1 入口函数和程序初始化

11.2 C/C++ 运行库

11.3 运行库与多线程

11.5 fread 实现
    

猜你喜欢

转载自blog.csdn.net/enlyhua/article/details/84967518