Linux程序与运行库

内存

使代码同时支持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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值