内存
使代码同时支持Linux和Windows平台,使用宏WIN32
#ifdef WIN32
//Windows代码
#else
//Linux代码
#endif
Linux程序的内存布局
- 栈:用于维护应用程序函数调用的上下文
- 堆:用来容纳应用程序动态分配的内存区域
- 可执行文件映像:存储可执行文件在内存里的映像
- 动态链接库映射区:用于映射装载的动态链接库,在Linux下,如果可执行文件依赖其他共享库,那么系统就会从0x40000000开始分配相应的空间,并将共享库载入该空间,不同Linux内核,共享库的装载地址也有所不同
- 保留区:不是单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称
运行库
运行库是平台相关的,与操作系统结合非常紧密
进程栈
满足下列条件的进程栈(小端字节序):
- 栈底部地址是0xBF802000
- 两个环境变量字符串:“HOME=/home/user” “PATH=/usr/bin”
- 两个命令行参数字符串:“prog” “123”
main程序运行步骤
- 操作系统创建进程(包括进程映射、动态链接等)后,把控制权交给程序的入口,这个入口往往是运行库中的某个入口函数
- 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等
- 入口函数完成初始化后,调用mian函数,正式开始执行程序主体部分
- main函数执行完毕,返回入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程
入口函数
- 境变量的格式为key=value的字符串,C语言可以使用getenv函数获取环境变量信息
- GCC支持bounded类型指针,用 _ _ bounded关键字标出,普通指针用 _ _ unbounded标出,这种指针占用3个指针空间,第一个空间存储原指针的值,第二空间存储下限值,第三个空间存储上限值。_ _ ptrvalue、_ _ ptrlow、_ _ ptrhigh分别返回这3个值,有了3个值后,内存越界很容易查出来,并且要定义 _ _ BOUNDED_POINTERS _ _ 宏才起作用,不过该功能在2003年被去掉了
glibc入口函数
- glibc的启动过程在不同情况下差别很大,比如静态的glibc和动态的glibc的差别,glibc用于可执行文件和用于共享库的差别
- glibc的启动代码在源代码的子目录libc/scu中
- gilbc的程序入口为_start(入口由ld链接器默认的链接脚本指定,也可以通过相关参数设定自己的入口)
- _start由汇编实现,并且和平台有关
MSVC CRT入口函数
- MSVC的CRT默认入口函数为mainCRTStartup,位于Microsoft Visual Studio的crt0.c(VC安装目录的crt、src)
- _setargv:初始化main函数的argv参数
- _setenv:设置环境变量
- _cinit:其他的C库设置
mainCRTStartup的总体流程:
- 初始化和OS版本有关的全局变量
- 初始化堆,由_heap_init函数完成,定义于heapinit.c
- 初始化I/O
- 获取命令行参数和环境变量
- 初始化C库的一些数据
- 调用main并记录返回值
- 检查错误并将main的返回值返回
运行库与I/O
- 在Linux中,值为0、1、2的fd分别代表标准输入、标准输出、标准错误输出,在程序中打开文件得到的fd从3开始
- 在内核中,每一个进程都有一个私有的文件描述符表,文件描述符fd就是该表的索引,每个表项都有一个指针指向已打开文件对象file结构体
- 当用户进程打开一个文件,该进程生成一个文件描述符表,已打开的文件在内核中用file结构体表示
- 由于该数组处于内核,用户即使拥有fd,页无法得到打开文件对象的地址,只能通过系统提供的函数操作
- 在C语言,操作文件的是FILE数据结构
FILE、fd、打开文件表和打开文件对象关系如下:
进程结构体与文件描述符关系:
- files_struct结构体保存了进程打开的所有文件表数据
struct files_struct{
atomic_t count;//自动增量
spinlock_t file_lock;//低位成员保护标识
int max_fds;//最大文件句柄数目
int max_fdset;//最大的fd集合容量
int next_fd;//下一个空空闲的fd
struct file **fd;//当前fd对应的文件结构体指针列表
fd_set *close_on_exec;//可执行close的fd集合
fd_set *open_fds;//打开的fd集合
fd_set close_on_exec_init;
fd_set open_fds_init;
struct file *fd_array[NR_OPEN_DEFAULT];//默认打开的fd队列
Linux的VFS子系统图:
- 图中,因为对同一个文件系统进行操作,故三个进程打开文件的file结构体的f_op指向同一个file_operations结构体
- 如果不同的进程打开不同一个文件系统进行操作,那么进程对应的f_op指向不同的file_operations结构体
- 每个file结构体都有一个指向dentry结构体指针,每个dentry结构体都有一个指针指向inode结构体,inode结构体保存着从磁盘inode读取的信息,图中/home/akaedu/a和/home/akaedu/b指向同一个inode,表示这两个文件互为硬链接
- 每个inode结构体都有一个指向inode_operations结构体指针,该结构体是一组完成目录操作的内核函数,例如添加/删除文件和目录、跟踪符号链接等,属于同一文件系统的各个inode结构体可以指向同一个inode_operations结构体
- inode结构体有一个指向super_block结构体的指针i_sb,super_block结构体保存着磁盘分区的超级块信息,例如文件系统类型、块大小等。super_block结构体的成员s_root指向一个dentry结构体,该dentry结构体表示这个文件系统的根目录被mount的位置
C/C++运行库
一个C语言运行大致包含如下功能:
- 启动与退出:包括入口函数以及入口函数所依赖的其他函数等
- 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现
- I/O:I/O功能的封装和实现
- 堆:堆的封装和实现
- 语言实现:语言中一些特殊功能的实现
- 调试:实现调试功能的代码
C语言标准库
- 标准输入输出(stdio.h)
- 文件操作(stdio.h)
- 字符操作(ctype.h)
- 字符串操作(string.h)
- 数学函数(math.h)
- 资源管理(stdlib.h)
- 格式转换(stdlib.h)
- 时间/日期(time.h)
- 断言(assert.h)
- 各种类型上的常数(limits.h & float.h)
- 变长参数(stdarg.h)
#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)
- 非局部跳转(setjmp.h)
运行库与多线程
线程的访问权限
- 栈,尽管并非完全无法被其他线程访问
- 线程局部存储(Thread Local Storage TLS),是某些操作系统为线程单独提供有限尺寸的私有空间
- 寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此线程私有
- 从C程序的角度看,数据在线程之间的关系如下表
线程私有 | 线程之间共享(进程所有) |
---|---|
局部变量 函数的参数 TLS数据 | 全局变量 堆上的数据 函数的静态变量 程序代码 打开文件 |
CRT的多线程困扰
C/C++运行库在多线程环境下的困扰
- errno:在C标准库中,大多数错误代码实在函数返回之前存储到errno全局变量里。在多线程并发时,有可能被其他线程覆盖,从而获取错误的出错信息
- 函数内部存在局部静态变量,如strtok()等函数,不同线程调用该类型函数时,会把内部的局部静态变量弄混乱
- malloc/new与free/delete:堆分配/释放在不加锁的情况下是线程不安全的
- 异常处理:在C++运行库里,不同线程抛出的异常彼此冲突,从而造成信息丢失
- printf/fprintf及其他I/O函数:输出函数是线程不安全的,因为共享同一个控制台或文件输出,不同的输出并发时,信息混杂一起
- 其他线程不安全函数:包括与信号相关的一些函数
C标准库在不进行线程安全包含情况下,具有线程安全的属性函数有(不考虑errno的因素):
- 字符处理(ctype.h),包括isdigit、toupper等,这些函数同时还是可重入的
- 字符串处理函数(string.h),包括strlen、strcmp等,其中涉及对参数中的数组进行写入的函数(如strcpy)仅在参数中的数组各不相同时,才可以并发
- 数学函数(math.h),包括sin、pow等,这些函数同时还是可重入的
- 字符串转换整数/浮点数(stdlib.h),包括atof、atoi、atol、strtol、strtoul
- 获取环境变量(stdlib.h),包括getenv,这个函数还是可重入
- 变长数组辅助函数(stdarg.h)
- 非局部跳转函数,包括setjmp和longjmp,前提longjmp仅跳转到本线程设置的jmpbuf上
CRT改进
1、使用TLS
- errno必须成为各个线程的私有成员,在glibc中,errno被定义为:#define errno (*__errno_location())
- 函数__errno_location在单线程版本,仅直接返回全局变量errno的地址,而在多线程版本中,不同线程调用__errno_location返回的地址则各不相同
2、加锁
- 在多线程版本的运行库中,线程不安全的函数内部自动进行加锁,包括malloc、printf等
3、改进函数调用方式
- 修改所有的线程不安全的函数的参数列表,改成某种线程安全的版本,如strtok()改为strtok_s()
TLS线程局部存储实现
全局变量被定义TLS类型时,每个线程都拥有该变量的一个副本,任何线程对该变量的修改不会影响其他线程该变量副本
隐式TLS
程序员无须关心TLS变量的申请、分配赋值和释放,由编译器、运行库以及操作系统处理
1、GCC,定义线程局部存储变量的关键字是__thread
__thread int number;
2、MSVC ,定义线程局部存储变量的关键字是__declspec(thread)
__declspec(thread) int number;
显示TLS
程序员必须手工申请TLS变量,并且每次访问该变量都要调用相应的函数获取该变量的地址,并且在访问完成后需要释放该变量,不推荐使用
1、LInux下对应的库函数为pthread库的
- pthread_key_create()
- pthread_getspecific()
- pthread_setspecific()
- pthread_key_delete()
2、Windows平台对应的API
- TlsAlloc()
- TlsGetValue()
- TlsSetValue()
- TlsFree()