程序员的自我修养_装载、链接与库_第一部分

1.从hello world说起

#include<stdio.h>
int main(){
	printf("Hello World\n");
	return 0;
}
  • 设备信息:兼容x86指令集的32位CPU个人计算机。
  • 最核心的硬件:中央处理器CPU、内存和I/O控制芯片

2.软件体系结构

  • 系统软件的分类:
    1. 平台性的:操作系统内核、驱动程序、运行库和数以千计的系统软件
    2. 用于程序开发的:编译器、汇编器、链接器等开发工具和开发库
  • 软件体系结构:
    在这里插入图片描述
  • 层次结构:
    • 接口:每个层次之间相互通信的协议。接口的下层是接口的提供者,由它定义接口;上层是接口的使用者,它使用接口来实现所需要的功能。
    • 正是中间层的存在,使得应用软件和硬件之间保持相对独立。
    • 虚拟机:在硬件和OS之间增加了一层虚拟层,使得计算机上可以同时运行多个OS。
    • 层次结构的好处:在尽可能少改变其他层的情况下,新增加一个层可以提供前所未有的功能。
  • 接口
    1. API:开发工具与应用程序使用的接口,即操作系统应用程序编程接口(Application Programming Interface)。
    2. 系统调用接口:运行库使用操作系统提供的系统调用接口(System call interface),系统调用接口在实现中往往采用软件中断方式提供。
    3. 硬件规格:硬件是接口的定义者,硬件接口决定了OS内核。硬件的生产厂商负责提供硬件规格,OS和驱动程序的开发者通过阅读硬件规格所定义的接口标准来编写OS和驱动程序。

3.文件系统

  • OS的功能:
    1. 提供抽象的接口
    2. 管理硬件资源
  1. 文件系统管理着磁盘中文件的存储方式。
  2. 文件的存储方式:链式
    在这里插入图片描述
  3. LBA:整个硬盘中所有扇区从0开始编号,一直到最后一个扇区,这个扇区编号叫做逻辑扇区号。它屏蔽了磁道、盘面等复杂的概念。当我们给出一个逻辑扇区号时,硬盘的电子设备会将其转换为实际的盘面、磁道等位置。
  4. 例如:如何读取Linux文件的前4096个字节?
    使用read系统调用——>文件系统收到read后,判断出文件的前4096个字节位于磁盘的1000号逻辑扇区~1007号逻辑扇区——>文件系统向硬盘驱动发出一个读取逻辑扇区为1000号开始的8个扇区的请求——>磁盘驱动程序收到请求后向硬件发出硬件命令。
  5. 向硬件发送I/O命令的方式:有很多种,其中最常见的一种是通过读写I/O端口寄存器来实现。CPU提供了两条专门的指令"in"和"out"来实现对硬件端口的读写。

4.线程

4.1 线程的概念

  • 定义:线程是程序执行流的最小单元。
  • 组成:线程ID、当前指令指针PC、寄存器集合和堆栈
  • 进程与线程的关系:一个进程由一个到多个线程组成,各个线程之间共享程序的内存单元(包括代码段、数据段、堆等)以及一些进程级的资源(如打开文件和信号)。
    在这里插入图片描述
  • 多线程的优点:
    1. 某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行。多线程可以有效利用等待时间。
    2. 某个操作(通常是计算)会消耗大量时间。如果只有一个线程,程序和用户之间的交互会中断;多线程可以让一个线程负责交互,另一个线程负责计算。
    3. 程序本身要求并发操作。
    4. 多CPU/多核计算机本身就具备同时执行多个线程的能力,因此单线程程序无法全面发挥计算机的全部计算能力。
    5. 相对于多进程应用,多线程在数据共享方面的效率高很多。
  • 线程的访问权限
    1. 线程的访问非常自由,几乎可以访问进程内存中的所有数据,甚至包括其他线程的堆栈。
    2. 但线程也拥有自己的私有存储空间:
      • 栈:并非完全无法被其他线程访问
      • 线程局部存储(TLS):某些OS为线程单独提供的私有空间,通常只有有限的容量。
      • 寄存器:执行流的基本数据,为线程私有
        在这里插入图片描述

4.2 线程的调度

  • 线程的并发执行:当CPU数量>线程数量时,每个线程运行在不同的CPU上,是真正的并发执行;当CPU数量<线程数量时,至少有一个处理器会运行多个线程。
  • 线程调度:不断在处理器上切换不同线程的行为。
  • 线程的状态:
    1. 运行:线程在CPU上执行
    2. 就绪:线程已经准备好,就差CPU
    3. 等待:线程在等待某一事件(通常是I/O或同步)发生,无法执行
      在这里插入图片描述
  • 线程类型:
    1. IO密集型线程:需要频繁等待的线程
    2. CPU密集型线程:很少等待的线程
    3. IO密集型线程总是比CPU密集型线程容易得到更高的优先级
  • 在优先级调度的环境下,线程优先级改变的方式:
    1. 用户指定优先级
    2. 根据进入等待状态的频繁程度提升/降低优先级
    3. 长时间得不到执行而提升优先级
  • 在不可抢占线程中,线程主动放弃执行的情况:
    1. 线程试图等待某事件
    2. 线程主动放弃时间片

4.3 Linux的多线程

  • 在Windows中,有明确的API:CreateProcess和CreateThread来创建进程和线程;但在Linux中,所有执行实体都称为任务,每一个任务都类似于一个单线程的进程,都具有内存空间、执行实体、文件资源等。
  • Linux下不同任务可以共享一个内存空间,因而共享了同一个内存空间的多个任务就构成了一个进程,这些任务就成了这个进程里的线程。
  • Linux下创建任务:
    在这里插入图片描述
  • fork函数:
    • fork函数产生一个和当前进程完全相同的新进程,并和当前进程一样从fork函数里返回。
    pid_t pid;
    if(pid = fork()){
    ...
    }
    
    • 在fork函数调用后,新任务将启动并和本任务一起从fork函数返回;但本任务的fork将返回新任务pid,而新任务的fork将返回0。
    • 写时复制:fork并不复制原任务的内存空间,而是和原任务一起共享一个写时复制的内存空间。即:两个任务可以自由读取内存,当任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响其他任务的使用。
      在这里插入图片描述
  • exec:fork只能产生本任务的镜像,必须使用exec配合才能启动别的新任务。exec可以用新的可执行映像来替换当前可执行映像。
  • clone
    • fork和exec通常用于产生新任务,clone可以产生新线程。
    • 函数原型:int clone(int (fn)(void),void* child_stack,int flags,void* arg);
    • 使用clone可以产生一个新任务,从指定位置开始执行,并且(可选)共享当前进程的内存空间和文件。这样可以在实际效果上产生一个线程。

4.4 线程安全

  • 单指令的操作称为原子的,单指令的执行是不会被打断的。
  • 临界区:每个进程中访问临界资源的那段程序称之为临界区。临界区的作用范围仅限于本进程,其他进程是无法获取该锁的;而互斥量和信号量在系统的任何进程里都是可见的。此外,临界区和互斥量有着相同的作用。
  • 可重入
    • 一个函数被重入,表示这个函数没有执行完成,由于外部/内部因素调用,又一次进入该函数执行。一个函数被重入,只有两种情况:
      1. 多个线程同时执行该函数
      2. 函数自身调用自身
    • 一个函数是可重入的,表示该函数被重入后不会产生任何不良后果。
    • 可重入函数的特点:
      1. 不使用任何(局部)静态/全局的非const变量
      2. 不返回任何(局部)静态/全局的非const变量的指针
      3. 仅依赖于调用方提供的参数
      4. 不依赖任何单资源的锁
      5. 不调用任何不可重入的函数
    • 可重入是并发安全的强力保障,一个可重入函数可以在多线程环境下放心执行。
  • 过度优化
    • 过度优化的问题:
      1. CPU的动态调度:在执行程序时,为了提高效率有可能交换指令的顺序。
      2. 编译器在优化时,也可能为了提高效率而交换不相关的相邻指令的执行顺序。
    • volatile关键字阻止编译器:
      1. 阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回
      2. 阻止编译器调整操作volatile变量的指令顺序
      •   volatile T* pInst = 0;
          T* GetInstance(){
          	if(pInst == NULL){
          		lock();
          		if(pInst == NULL){
          			pInst = new T;
          			unlock();
          		}
          	return pInst;
          }
        
    • barrier指令阻止CPU动态调度
      • 一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后。
      •   volatile T* pInst = 0;
          T* GetInstance(){
          	if(!pInst){
          		lock();
          		if(!pInst){
          			T* temp= new T;
          			barrier();
          			pInst = temp;
          		}
          		unlock();
          	}
          return pInst;
          }
        

4.4用户线程与内核线程之间的映射关系

  • 内核线程:线程的并发执行是由多处理器/OS调度来实现的,大多数OS,都在内核里提供线程的支持,内核线程由多处理器或调度来实现并发。
  • 用户线程:用户实际使用的线程是用户线程,用户线程并不一定在OS内核中对应同等数量的内核线程。
  1. 一对一模型
    • 一个用户线程对应一个内核线程,但一个内核线程并不一定有对应的用户线程。
    • 用户线程具有和内核线程一样的优点,线程之间是真正的并发,一个线程的阻塞并不影响其他线程的执行。
    • 一般直接使用API/系统调用创建的线程都是一对一的线程,例如Linux的clone。
    •   int thread_function(void*){...}
        char thread_stack[4096];
        void foo{
        	clone(thread_function,thread_stack,CLONE_VM,0);
        }
      
    • 缺点:
      1. 由于许多OS限制内核线程数量,因此一对一线程会让用户线程的数量收到限制。
      2. 许多OS内核线程调度时,上下文切换的开销大,导致用户线程的执行效率下降。 在这里插入图片描述
  2. 多对一模型
    - 优点:多对一模型的线程切换要快很多。
    - 缺点:如果一个用户线程阻塞,则所有的线程都无法执行,内核线程也随之阻塞。
    在这里插入图片描述
  3. 多对多模型
    • 多对多模型将多个用户线程映射到少数但不止一个的内核线程上。
      在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
程序员自我修养:链接,装载》是一本由林锐、郭晓东、郑蕾等人合著的计算机技术书籍,在该书中,作者从程序员的视角出发,对链接装载等概念进行了深入的阐述和解析。 在计算机编程中,链接是指将各个源文件中的代码模块组合成一个可执行的程序的过程。链接可以分为静态链接和动态链接两种方式。静态链接是在编译时将所有代码模块合并成一个独立的可执行文件,而动态链接是在运行时根据需要加载相应的代码模块。 装载是指将一个程序从磁盘上加载到内存中准备执行的过程。在装载过程中,操作系统会为程序分配内存空间,并将程序中的各个模块加载到相应的内存地址上。装载过程中还包括解析模块之间的引用关系,以及进行地址重定位等操作。 是指一组可重用的代码模块,通过链接装载的方式被程序调用。可以分为静态和动态。静态是在编译时将的代码链接到程序中,使程序与的代码合并为一个可执行文件。动态则是在运行时通过动态链接的方式加载并调用。 《程序员自我修养:链接,装载》对于理解链接装载的原理和机制具有极大的帮助。通过学习这些概念,程序员可以更好地优化代码结构和组织,提高程序的性能和可维护性。同时,了解链接装载的工作原理也对于进行调试和故障排除具有重要意义。 总之,链接装载是计算机编程中的重要概念,对于程序员来说掌握这些知识是非常必要的。《程序员自我修养:链接,装载》这本书提供了深入浅出的解释和实例,对于想要学习和掌握这些知识的程序员来说是一本非常有价值的参考书籍。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值