后台开发核心技术与应用读书笔记
4.1
编译链接的4个步骤
预处理 编译 汇编 链接
/*
过程
假设存在一个源代码helloworld.cpp
头问价iostream
预处理.cpp文件得到helloworld.i
用g++编译
编译完成得到helloworld.s
用as汇编
得到helloworld.o
与静态库libc.a
链接得到目标文件a.out
*/
- 预处理的第一步
g++ -E helloworld.cpp -o helloword.i
-E
意味着只执行到预编译,直接输出预编译结果
主要处理规则如下
(1)将所有的#define删除,并且展开所有的宏定义
(2)处理所有的条件预编译指令,如#if #ifdef #elif #else #endif
(3)处理#include预编译指令,将被包含的文件插入到改预编译指令的位置,过程递归进行,被包含的文件肯跟嗨包含其他文件
(4)过滤所有的注释"//"和“/**/”的内容
(5)添加行号和文件名标识
(6)保留所有的#pragma编译器指令,因为编译器需要使用
2. 编译
编译过程就是把预处理完的文件进行一系列的语法分析,词法分析,语义分析以及优化后产生的相应的汇编代码文件
g++ -S helloworld.i -o helloworld.s
-S 表示只执行到源代码到汇编代码的转换 输出汇编代码
一般分为一下的6步
源代码
1. 词法分析
产生一堆单词
2. 语法分析
语法树
3. 语义分析
带变量的语法树
4. 源代码优化
中间语言
5. 代码生成
目标代码
6. 目标代码优化
最终目标代码
定义其他模块的全局变量和函数在最终运行时间的绝对地址都要在最终链接的时候才能确定。
所以现代编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件
3. 链接
把每个源代码模块独立地编译,然后将他们组装起来,这些组装的过程就是链接
链接过程主要包括了地址和空间的分配,符号决议和重定位等这些步骤
每个目标文件还提供了3个表
(1)未解决符号表
(2)导出符号表
(3) 地址重定向表
静态链接
对函数库的链接是放在编译时期完成的是静态链接。所有相关的目标文件与牵扯到的函数库被链接合成一个可执行文件。
1)无论是静态库文件还是动态库文件都是由.o文件创建的
2)由.o文件创建静态库(.a文件),执行命令:ar cr libmymath.a sub.o add.o 会生成ibmymath.a文件
3)在程序中使用静态库
g++ 会在静态库名前加上前缀lib,然后将会从静态库中将公用函数链接到目标文件。g++ 会在静态库名前加上前缀lib,然后追加扩展名.a得到的静态库文件名来查找静态库文件。
动态链接
除了静态链接,也可以把一些库函数的链接载入推迟到程序运行时期,也就是动态链接库技术。在动态库名前增加前缀lib,但其文件拓展名为.so
如果动态库与静态库同时存在的话,那么就会先查询动态库,再查询静态库
静态链接库、动态链接库的各自特点
1)同台链接有利于进程间资源共享。
2)将一些程序升级变得更简单
3) 甚至可以真正做到链接载入完全由程序员在程序代码中控制
4) 静态库在编译的时候,就将库函数装载到程序中去了,而动态库函数必须在运行的时候才被装载。所以静态库的效率优秀与动态库。
g++ 与 gcc的区别 参考书P143
makefile的使用 P147
目标文件是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式。
ELF标准的目的是为软件开发人员提供一组二进制接口定义
ELF文件的三种类型P150
内存检查原理
可以补充一个内存检查原理
1) valid-value表,对于进程的整个地址空间中的每一个字节Byte,都有对应的8bit;对于CPU的每个寄存器,也有一个与之对应的bit向量,这些bit负责记录该字节或者寄存器值是否具有有效的,已初始化的值
2) valid-address表,对于进程整个地址空间的每一个字节Byte,还有对应的1bit,负责计入该地址是否能被读写
tcp连接过程
参考P194
简单描述一下tcp连接的建立,可以简单称为3次握手,而连接的中止则可以成为4次挥手
TCP建立连接的过程
1)第一次握手:建立连接时,客户端发送SYN包(SYN = j)到服务器,并进入SYN_SEND状态,等待服务器确认
2)第二次握手:服务器收到SYN包,必须确认客户的SYN(ACK = j + 1),同时自己也发送一个SYN包(SYN=K),即SYN + ACK包,此时服务器进入SYN_RECV状态
3) 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK = K + 1),包发送完毕,服务器进入ESTABLISHED状态
为什么说建立连接要3次握手,断连接需要4次挥手呢?
答: 对于建连接的3次握手,主要是初始化Sequence Number的初始值,通信的双方要互相通知对方自己初始化的Sequence Number。这个编号要作为以后的数据通信的序号,保证应用层收到的数据不会因为网络上的传输问题而乱序。
TCP结束连接的过程
TCP有个特别的概念叫做半关闭,这个概念就是说,TCP的连接是全双工连接,因此在关闭连接的时候,必须关闭传和送两个方向上的连接,客户机给服务器一个FIN的TCP报文,服务器给客户端返回一个ACK报文,并且发送一个FIN报文,当客户机回复ACK报文后,连接就结束了
为什么要三次握手?
答:第三次握手是为了防止失效的连接请求到达服务器,让服务器错误的打开连接
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接
四次挥手的原因:
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
可以参考
这是一个超链接
RTO的设置对于重传非常重要:
1)设长了,重发就慢,没有效率,性能差
2)设短了,重发就快,会增加网络拥塞
TCP引入了RTT,也就是连接往返时间,发送端从发送TCP包开始到接收他的立即响应时间
自适应算法的关键就是对于当前RTT的准确估计,用来调整RTO
1)先采样RTT,记录最近几次的RTT值
2)然后做平滑计算SRTT。公式中的alpha为0.8~0.9
SRTT = alpha * SRTT + (1 - alpha)* RTT
接着计算RTO
RTO = min{UBOUND, max[LBOUND, (beta * SRTT)]}
UBOUND是最大的timeout时间
LBOUND是最小的timeout时间
beta一般在1.3 ~ 2.0
1987年出现了Karn算法用了一个取巧的方法--只要一发送重传,就对先有的RTO值翻倍
所以通过上述可知,重传的时间为1s,2s,4s,8s,16s
TCP滑动窗口 参考P215
TCP拥塞控制 参考P218
TCP 网络变成 API 参考P219
四种网络IO模型
-
阻塞IO模型
-
非阻塞IO模型
-
多路IO复用模型
-
异步IO模型
- 阻塞IO模型
阻塞的概念是指得是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间,而非阻塞是指IO操作被调用后立即返回给用户一个状态值,不需要等到IO操作彻底完成
举个例子可以参考《后台开发 核心技术与应用读书笔记》P249
当应用进程调用了 recvfrom 这个系统调用后,系统内核就开始了 IO 的第一个阶段 :准
备数据 对于网络 IO 来说,很多时候数据在一开始还没到达时(比如还没有收到一个完整的
TCP 包),系统内核就要等待足够的数据到来 而在用户进程这边,整个进程会被阻塞 当系
统内核 直等到数据准备好了,它就会将数据从系统内核中拷贝到用户内存中,然后系统内
核返回结果,用户进程才解除阻塞的状态,重新运行起来 所以,阻塞 IO 模型的特点就是
IO 行的两个阶段(等待数据和拷贝数据)都被阻塞了
程序员可能会考虑使用“线程池”或“连接池” “线程池”旨在降低创建和销毁
线程的频率,使其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务 “连
接池”是指维持连接的缓存池,尽量重用已有的连接,降低创建和关闭连接的频率。这两
技术都可以很好地降低系统开销,都被广泛应用于很多大型系统 但是,“线程池”和“连
接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用 而且,所谓“池”
始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有“池”
的时候效果好多少 所以使用叫池”必须考虑其面临的响应规模,并根据响应规模调整“池”
的大小现实生活所面临的可能是同时出现的上千甚至上万次的客户端请求,“线程池”或“连
接池”或许可以缓解部分压力,但是不能解决所有问题 总之,多线程模型可以方便高效的
解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻
塞模型来尝试解决这个问题
2. 非阻塞IO模型
当用户进程发出 read 操作时,如果内核中的数据还没有准备好,那
么它并不会 block 用户进程,而是立刻返回一个错误 从用户进程角度讲,它发起 read
操作后,并不需要等待,而是马上就得到了一个结果 当用户进程判断结果是一个错误时,
它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,
并且又再次收到了用户进程的系统调用,那么它马上就将数据复制到了用户 内存中,然后返
回正确的返回值
所以,在非阻塞式 IO 中,用户进程其实需要不断地主动询问 kernel 数据是否准备好
非阻塞的接口相比于阻塞型接口的显著差异在于被调用之后立即返回 使用如下的函数可以
将某句柄归设为非阻塞状态 但是这种形式会导致CPU的资源被占用过高
3. 多路IO复用
多路 IO 复用,有时也称为事件驱动 IO 它的基本原理就是有个函数( select )会不断
地轮询所负责的所有 socket ,当某个 socket 有数据到达了,就通知用户进程, IO 复用
模型的流程如图 7-3
当用户进程调用了 se lect ,那么整个进程会被阻塞,而同时,内核会“监视”所有 select
负责的 socket ,当任何一个 socket 中的数据准备好了, select 就会返回 这个时候用户进程
再调用 rea 操作,将数据从内核拷贝到用户进程
这个模型和阻塞 IO 的模型其实并没有太大的不同, 事实上还更差一些 因为这里需要
使用两个系统调用( select recv om ),而阻塞 IO 只调用了 个系统调用 recvfrom
是,用 se lect 的优势在于它可以同时处理多个连接 所以,如果处理的连接数不是很高的话,
使用 select/epoll Web server 定比使用多线程的阻塞 IO Web server 性能更好,可能
延迟还更大; select poll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的
连接
在多路复用 IO 模型中,对于每 socket 般都设置成为非阻 的,但 ,如图 7-3
所示,整个用户的进程其实是一直被阻塞的 只不过进程是被 select 这个函数阻 ,而不
socket IO 阻塞 因此使用 select()的效果与非阻 IO
大部分 UNIX/Linux 都支持 select 函数,该函数用于探测多个文件句柄的状态变
面给出 select 接口的原型:
4. 异步 IO 模型
异步 IO 模型的流程如图 7-6 所示
用户进程发起 read 操作之后,立刻就可以开始去做其他的事;而另一方面,从内核的角
度,当它收到一个异步的 read 请求操作之后,首先会立刻返回,所以不会对用户进程产生任
何阻塞 然后,内核会等待数据准备完成,然后将数据拷贝到用户内存中,当这 切都完成
之后,内核会给用户进程发送 个信号,返回 read 操作已完成的信息
调用阻塞 IO 一直阻塞住对应的进程直到操作完成,而非阻塞 IO 在内核还在准备数
据的情况下会立刻返回 两者的区别就在于同步 IO 进行 IO 操作时会阻塞进程 按照这个
定义,之前所述的阻塞 IO 、非阻塞 IO 及多路 IO 复用都属于同步 IO 实际上,真实的 IO
操作,就是例子中的 recvfrom 这个系统调用 非阻塞 IO 在执行 recvfrom 这个系统调用的
时候,如果内核的数据没有准备好,这时候不会阻塞进程 但是当内核中数据准备好时,
recvfrom 会将数据从内核拷贝到用户内存中,这个时候进程则被阻塞 而异步 IO 则不 样,
当进程发起 IO 操作之后,就直接返回,直到内核发送 个信号,告诉进程 IO 已完成,则在
这整个过程中,进程完全没有被阻塞。 - 阻塞IO模型
多线程与多进程
进程在多数早期多任务操作系统中是执行工作的基本单元 进程是包含程序指
令和相关资源的集合,每个进程和其他进程一起参与调度,竞争 CPU 、内存等系统资源
次进程切换,都存在进程资源的保存和恢复动作,这称为上下文切换 进程的引人可以解决
多用户支持的问题,但是多进程系统也在如下方面产生了新的问题 进程频繁切换引起的额
外开销可能会严重影响系统性能 进程间通信要求复杂的系统级实现 在程序功能日趋复杂
的情况下,上述缺陷也就凸显出来
系统资源看上去都是其独占的,比如内存空间,每个进程都认为自己的内存空间是独
有的 次切换,这些独立资源都需要切换 由此就演化出了利用分配给同一个进程的资
源,尽量实现多个任务的方法,这也就引入了线程的概念
个进程内部的多个线程,共享的是同一个进程的所有资源 比如,与每个进程
独有自己的内存空间不同,同属 个进程的多个线程共享该进程的内存空间 例如在进
程地址空间巾有一个全局变量 Global Var ,若 线程将其赋值为 ,则另 一线程 可以看到该变量值为 ,但两个线程看到的全局变量 Global Var 本质是同 个变量 通过线程可
以支持同一个应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务间通信
也更简单
一个栈中只有最下方的帧可被读写,相应的,也只有该帧对应
的那个函数被激活,处于 作状态 为了实现多线程,则必须绕开械的限制 为此,在创建
个新的线程时, 要为这个线程建 个新的枝,每个技对应 个线程 当某个械执行到全
部弹出时,对应线程 成任务, 结束 所以,多线程的进程在内存中有多个拢,多个战之
间以 定的 域隔开,以备战的增长 每个线程可调用自己樵 下方的帧中的 数和
,并与其他线程共 内存中的 Text heap global data 区域
4.1
编译链接的4个步骤
预处理 编译 汇编 链接
/*
过程
假设存在一个源代码helloworld.cpp
头问价iostream
预处理.cpp文件得到helloworld.i
用g++编译
编译完成得到helloworld.s
用as汇编
得到helloworld.o
与静态库libc.a
链接得到目标文件a.out
*/
- 预处理的第一步
g++ -E helloworld.cpp -o helloword.i
-E
意味着只执行到预编译,直接输出预编译结果
主要处理规则如下
(1)将所有的#define删除,并且展开所有的宏定义
(2)处理所有的条件预编译指令,如#if #ifdef #elif #else #endif
(3)处理#include预编译指令,将被包含的文件插入到改预编译指令的位置,过程递归进行,被包含的文件肯跟嗨包含其他文件
(4)过滤所有的注释"//"和“/**/”的内容
(5)添加行号和文件名标识
(6)保留所有的#pragma编译器指令,因为编译器需要使用
2. 编译
编译过程就是把预处理完的文件进行一系列的语法分析,词法分析,语义分析以及优化后产生的相应的汇编代码文件
g++ -S helloworld.i -o helloworld.s
-S 表示只执行到源代码到汇编代码的转换 输出汇编代码
一般分为一下的6步
源代码
1. 词法分析
产生一堆单词
2. 语法分析
语法树
3. 语义分析
带变量的语法树
4. 源代码优化
中间语言
5. 代码生成
目标代码
6. 目标代码优化
最终目标代码
定义其他模块的全局变量和函数在最终运行时间的绝对地址都要在最终链接的时候才能确定。
所以现代编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件
3. 链接
把每个源代码模块独立地编译,然后将他们组装起来,这些组装的过程就是链接
链接过程主要包括了地址和空间的分配,符号决议和重定位等这些步骤
每个目标文件还提供了3个表
(1)未解决符号表
(2)导出符号表
(3) 地址重定向表
静态链接
对函数库的链接是放在编译时期完成的是静态链接。所有相关的目标文件与牵扯到的函数库被链接合成一个可执行文件。
1)无论是静态库文件还是动态库文件都是由.o文件创建的
2)由.o文件创建静态库(.a文件),执行命令:ar cr libmymath.a sub.o add.o 会生成ibmymath.a文件
3)在程序中使用静态库
g++ 会在静态库名前加上前缀lib,然后将会从静态库中将公用函数链接到目标文件。g++ 会在静态库名前加上前缀lib,然后追加扩展名.a得到的静态库文件名来查找静态库文件。
动态链接
除了静态链接,也可以把一些库函数的链接载入推迟到程序运行时期,也就是动态链接库技术。在动态库名前增加前缀lib,但其文件拓展名为.so
如果动态库与静态库同时存在的话,那么就会先查询动态库,再查询静态库
静态链接库、动态链接库的各自特点
1)同台链接有利于进程间资源共享。
2)将一些程序升级变得更简单
3) 甚至可以真正做到链接载入完全由程序员在程序代码中控制
4) 静态库在编译的时候,就将库函数装载到程序中去了,而动态库函数必须在运行的时候才被装载。所以静态库的效率优秀与动态库。
g++ 与 gcc的区别 参考书P143
makefile的使用 P147
目标文件是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式。
ELF标准的目的是为软件开发人员提供一组二进制接口定义
ELF文件的三种类型P150
内存检查原理
可以补充一个内存检查原理
1) valid-value表,对于进程的整个地址空间中的每一个字节Byte,都有对应的8bit;对于CPU的每个寄存器,也有一个与之对应的bit向量,这些bit负责记录该字节或者寄存器值是否具有有效的,已初始化的值
2) valid-address表,对于进程整个地址空间的每一个字节Byte,还有对应的1bit,负责计入该地址是否能被读写
tcp连接过程
参考P194
简单描述一下tcp连接的建立,可以简单称为3次握手,而连接的中止则可以成为4次挥手
TCP建立连接的过程
1)第一次握手:建立连接时,客户端发送SYN包(SYN = j)到服务器,并进入SYN_SEND状态,等待服务器确认
2)第二次握手:服务器收到SYN包,必须确认客户的SYN(ACK = j + 1),同时自己也发送一个SYN包(SYN=K),即SYN + ACK包,此时服务器进入SYN_RECV状态
3) 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK = K + 1),包发送完毕,服务器进入ESTABLISHED状态
为什么说建立连接要3次握手,断连接需要4次挥手呢?
答: 对于建连接的3次握手,主要是初始化Sequence Number的初始值,通信的双方要互相通知对方自己初始化的Sequence Number。这个编号要作为以后的数据通信的序号,保证应用层收到的数据不会因为网络上的传输问题而乱序。
TCP结束连接的过程
TCP有个特别的概念叫做半关闭,这个概念就是说,TCP的连接是全双工连接,因此在关闭连接的时候,必须关闭传和送两个方向上的连接,客户机给服务器一个FIN的TCP报文,服务器给客户端返回一个ACK报文,并且发送一个FIN报文,当客户机回复ACK报文后,连接就结束了
为什么要三次握手?
答:第三次握手是为了防止失效的连接请求到达服务器,让服务器错误的打开连接
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接
四次挥手的原因:
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
可以参考
这是一个超链接
RTO的设置对于重传非常重要:
1)设长了,重发就慢,没有效率,性能差
2)设短了,重发就快,会增加网络拥塞
TCP引入了RTT,也就是连接往返时间,发送端从发送TCP包开始到接收他的立即响应时间
自适应算法的关键就是对于当前RTT的准确估计,用来调整RTO
1)先采样RTT,记录最近几次的RTT值
2)然后做平滑计算SRTT。公式中的alpha为0.8~0.9
SRTT = alpha * SRTT + (1 - alpha)* RTT
接着计算RTO
RTO = min{UBOUND, max[LBOUND, (beta * SRTT)]}
UBOUND是最大的timeout时间
LBOUND是最小的timeout时间
beta一般在1.3 ~ 2.0
1987年出现了Karn算法用了一个取巧的方法--只要一发送重传,就对先有的RTO值翻倍
所以通过上述可知,重传的时间为1s,2s,4s,8s,16s
TCP滑动窗口 参考P215
TCP拥塞控制 参考P218
TCP 网络变成 API 参考P219