程序员的自我修养 day1
链接、装载与库 计算机框架:应用层->运行库->操作系统层->硬件层 层与层之间需要通信,而通信需要协议,这个协议就叫接口 下层为上层提供接口,上层作为接口的使用者 开发工具与应用层属于同一层次,他们都用操作系统应用编程接口 应用接口的提供者是运行库,不同的运行库提供不同的API,如windows运行库提供windows API 操作系统与硬件接口叫做:硬件规格 操作系统的功能是提供抽象接口和管理硬件资源 所有应用程序都以进程的方式运行,每个进程都有自己独立的地址空间,使得进程间相互隔离,要通过通信访问 程序通过虚拟地址映射到实际的物理地址,这样多程序切换代码编写的函数跳转地址问题就能解决了 进程隔离是通过虚拟地址实现的 分段: 分段对内存区域的映射还是按照程序为单位,如果内存不足,要进行换入换出到磁盘,这时候整个程序都要被换,造成大量磁盘访问,从而影响速度 分页: 一般按照4KB/页分,4GB内存可以分成1e6+页; 虚拟空间的页:虚拟页 物理内存的页:物理页 磁盘中的页:磁盘页 虚拟空间的页可以映射到同一物理页实现内存共享; 当程序需要用到磁盘页时,虚拟页找不到,硬件捕获页错误,然后从磁盘页中读出数据并装入内存; 这种机制能大量利用内存空间; 虚拟存储需要靠硬件支持,几乎所有的硬件都采用一个叫MMU的部件进行页映射(memory management unit) (CPU)->virtual address->(MMU)->physical address->(physical memory)括号内的是真实器件 线程: 线程被称为轻量级进程,是程序执行流的最小单元,由线程ID,当前指令指针寄存器集合和堆栈组成 线程共享进程的一些资源,也有自己独立的内存空间(寄存器、堆栈) 线程调度: 线程调度有三种状态: 运行: 线程正在执行 就绪: 线程可以运行,但是CPU被占用了 等待: 此时线程正在等待某一时间(I/O或同步),无法执行 一般把频繁等待的线程称为IO密集型线程 把很少等待的成为CPU密集型线程 IO密集型线程总是比CPU密集型线程容易得到优先级提升,因为CPU可以经常放养他们,从而不费力的完成这些任务; 饿死现象:高优先级的CPU密集型线程一直占用CPU导致低优先级的线程无法运行; 优先级改变三种情况: 1.用户指定优先级 2.根据进入等待状态的频繁程度提升或降低优先级 3.长时间得不到执行的而被提升优先级; 信号量、互斥量、临界区; 信号量,有多个满足线程的资源,可以由任意线程获取任意线程释放 互斥量必须由获取的线程去释放。 信号量,互斥量能被多个线程获取该锁,而临界区只能被本进程获取 条件变量针对的是批量线程 一个条件变量可以被多个线程等待,当条件发生时,可以唤起批量等待线程; 第二章 编译和链接 一段代码从写完到运行分为四个过程 预处理->编译->汇编->链接 静态语义:浮点数赋给整型变量,如果把浮点型赋给指针,运行前就能报错 动态语义:除数为0,这里只有运行时才能确定报错; 由汇编转成二进制文件形成的obj文件,最终需要链接才能形成/a.out文件 因为有些代码引用了其他源文件的去外部全局变量,需要后面链接才能确定它的绝对地址 现代编译器可以将一个源文件代码编译成一个未连接的目标文件,然后由连接器最终将这些目标文件链接起来形成可执行文件; 链接: 人们把每个源代码模块独立编译,然后组装起来的过程称为链接 链接的主要过程包括地址和空间的分配、符号决议、重定位 比如在main.c模块调用fun.c模块的foo函数,在编译器编译main.c的时候它并不知道foo的函数地址 于是它把调用foo的指令的目标地址搁置,等到最后链接的时候由链接器自动将目标地址修正,如果没有连接器,当foo函数改变地址后,需要人工跟着改变会造成很麻烦的修改过程; 比如目标A.c文件有个var变量,目标B文件想要访问var并修改var=42; 由于编译目标B文件时,编译器不知道var的目标地址,所以编译器在无法确定地址时,将Mov指令的目标地址置为0 等A,B,链接后再修改指令的目标地址,这个地址的修改也被叫做“重定向” 目标文件:源代码编译后未链接的文件(windows下.obj;linux下.o) 可执行文件:目标文件链接后的文件(windows下是.exe;linux下是.out) 动态链接库(windows下是.dll;linux下是.so) 静态链接库(windows下是.lib;linux下是.a) 程序源代码编译后的指令经常放在代码段,这个段与存储方式分段的段不是一个概念 代码段常见的名字由.code或.text 全局变量和巨变静态变量数据经常放在数据段 数据段一般名字叫.data 未初始化的全局变量和局部静态局部变量放在.bss段,因为未初始化可能不用用上,因为不用为它们分配内存浪费空间 总的来说程序源代码被编译后主要分为两种段:程序指令和程序数据,代码段属于程序指令,而数据段和.bss段属于程序数据 把指令和数据分开的好处是程序被装载后,数据和指令分别映射到两个虚拟内存区域,数据区对于进程来说是可读写,而指令区域对于进程来说是只读的,可以方便设置权限 还有就是当系统运行多个该程序的副本时,指令是相同的,数据是不同的,因此只需要单独 的一份指令即可 关于extern "C"{ void add(int,int); } 如果add是A.c编译的函数,此时我们在B.cpp编译的文件里要引用这个函数 由于c和cpp链接机制原因,如果不加extern "C",add的函数名可能会在B.cpp链接时找不到函数名 在A.c中add编译时函数名字可能是_add,而在cpp规则下它编译时可能是_ZNaddEii,此时链接时就找不到add这个函数名从而报错 cpp的这种机制也造就了重载函数的可行性 #ifdef __cplusplus //这是c++的内置宏 extern "c" { #endif void add(int,int); #if_def __cplusplus } #endif 这样就能在两种方式下正确链接了 编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号; 如int a = 2;//全局变量 可以通过gcc的__attribute__((a)) a = 1;//把强符号主动变成弱符号,目的是为了能让同名全局变量共存,了解知识,实用性不大 规则1.不允许强符号多次定义(即不同的目标文件或同一文件不能有同名强符号,即重定义) 规则2.如果一个符号在某个目标文件是强符号,在其他文件都是弱符号,那么选择强符号; 规则3.如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的 如在A定义a为int,在B定义a为double则目标A与B链接后a占用8字节
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理