C语言基础知识
函数指针和指针函数的区别
指针函数:指针函数本质是一个函数,只不过返回值为某一类型的指针(地址值)。函数返回值必须用同类型的变量来接受,也就是说,指针函数的返回值必须赋值给同类型的指针变量。指针函数的定义格式:类型名 *函数名(函数参数列表);
函数指针:函数指针本质是一个指针,只不过这个指针指向一个函数。常见的函数都有其入口,比如main()函数是整个程序的入口,我们调用的其他函数都有其特定的入口,正如我们可以通过地址找到相应的变量一样,我们也可以通过地址找到相应的函数。而这个存储着函数地址的指针就是函数指针。换言之,我们所说的指针变量通畅指向一个整形、字符型、或者数组等变量,而函数指针指向的是函数。正如我们可以通过指针访问相应的变量,函数指针也可以像函数一样用于调用函数、传递参数。
函数指针定义格式:类型名 (*函数名)(函数参数列表)
epoll vs select
程序编译知识
高级语言程序 → 预处理 → 编译程序 → 目标汇编代码 → 汇编程序 → 可再装配的机器码 → 可在装配的目标文件 → 绝对的机器码
编译过程需要:词法分析(lex)
、语法分析(yacc, Bison)
、语义分析
、中间代码生成
、代码优化
和目标代码生成
这几个阶段。
程序分段
Linux中目标文件遵循ELF文件格式。程序加载进内存之后,特别喜欢提及的段有代码段
、数据段
、BSS段
、堆
、栈
。
代码段:在内存中被映射为只读。它是由编译器在编译链接时自动计算的。通常是用来存放程序执行的指令。代码段属于静态内存分配
数据段:通常用来存放程序中已初始化的(非0)全局变量和静态局部变量。数据段的起始位置由链接定位文件确认,大小在编译链接时自动分配。数据段属于静态内存分配
BSS段:通常用来存放程序中未初始化和初始化为0的全局变量的一块内存区域,在程序载入时由内核清零。数据段属于静态内存分配
堆:保存函数内部动态分配(malloc或new)的内存,是另外一种用来保存程序信息的数据结构。堆是先进先出(FIFO)数据结构。堆的地址空间是向上增加,即当堆上保存的数据越多,堆的地址越高。动态内存分配(注意:堆内存需要程序员手动管理内存,通常适用于较大的内存分配,如频繁的分配较小的内存,容易导致内存碎片化)
栈:栈保存函数的局部变量(不包括 static 修饰的变量),参数以及返回值。是一种后进先出(LIFO)的数据结构。在调用函数或过程后,系统会清除栈上保存的局部变量、函数调用信息及其他信息。栈的另外一个重要特征是,它的地址空间向下减少,即当栈上保存的数据越多,栈的地址越低。静态内存分配(注意,由于栈的空间通常比较小,一般linux程序为8M(ulimit -s
查看),故局部变量,函数入参应该避免出现超大栈内存使用,比如超大结构体,数组等,避免出现stack overflow
)
静态全局变量和非静态全局变量的区别
静态变量与非静态变量的区别在作用域:静态变量只在定义它的文件内有效,而非静态全局变量的作用域是整个源程序。
内存分配有哪几种方式
内存的三种分配方式:
- 从静态存储区分配:此时的内存在程序编译的时候已经分配好,并且在程序的整个运行期间都存在。全局变量,static变量等在此存储。
- 在栈区分配:相关代码执行时创建,执行结束时被自动释放。局部变量在此存储。栈内存分配运算内置于处理器的指令集中,效率高,但容量有限。Linux中默认的栈大小为8M,可通过
ulimit -s
查看 - 在堆区分配:动态分配内存。用
new/malloc
时开辟,delete/free
时释放。生存期由用户指定,灵活。但有内存泄露等问题。
Linux多线程与多进程的区别
Linux中的线程分为LinuxThreads
、NGPT(Next Generation POSIX Threads)
和NPTL(Native POSIX Threads Library)
三种模型。LinuxThreads
和NPTL
都是采用一对一的线程模型,NGPT
采用的是多对多的线程模型。
多对一用户线级程模型:多对一线程模型中,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。用户态线程的很多操作对内核来说都是透明的,因为不需要内核来接管,这意味不需要内核态和用户态频繁切换。线程的创建、调度、同步处理速度非常快。当然线程的一些其他操作还是要经过内核,如IO读写。这样导致了一个问题:当多线程并发执行时,如果其中一个线程执行IO操作时,内核接管这个操作,如果IO阻塞,用户态的其他线程都会被阻塞,因为这些线程都对应同一个内核调度实体。在多处理器机器上,内核不知道用户态有这些线程,无法把它们调度到其他处理器,也无法通过优先级来调度。这对线程的使用是没有意义的。
一对一内核极线程模型:一对一模型中,每个用户线程都对应各自的内核调度实体。内核会对每个线程进行调度,可以调度到其他处理器上面。当然由内核来调度的结果就是:线程的每次操作会在用户态和内核态切换。另外,内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。但该模型的实用性还是高于多对一的线程模型。
多对多两极线程模型:多对多模型中,结合了1:1和M:1的优点,避免了它们的缺点。每个线程可以拥有多个调度实体,也可以多个线程对应一个调度实体。听起来好像非常完美,但线程的调度需要由内核态和用户态一起来实现。可想而知,多个对象操作一个东西时,肯定要一些其他的同步机制。用户态和内核态的分工合作导致实现该模型非常复杂。NPTL曾经也想使用该模型,但它太复杂,要对内核进行大范围改动,所以还是采用了一对一的模型。
现在版本的Linux中一般使用的是NPTL
模型,可以使用命令getconf GNU_LIBPTHREAD_VERSION
查看当前使用的线程库。
我们从创建
、上下文切换
和共享内存的缓存一致性
分别分析其区别:
创建:创建进程和线程的过程类似,都是用clone syscall
实现的。差别只是创建进程时会把相关的数据结构都复制一份,而创建线程时部分数据结构是共享的,如页表。因此,创建线程自然比创建进程快。
进程上下文切换:进程上下文切换会更换页表(cr3),且导致相关缓存失效。线程上下文切换不需要。
共享内存的缓存一致性:共享内存的缓存一致性和线程或进程没有本质的关联,只和是否使用可修改的共享内存有关。
信号
数组指针和指针数组的区别
数组指针表示变量是个指针,指向一个数组;指针数组表示变量是个数组,里面存储的数据为指针
sizeof和strlen的区别
sizeof
是一个操作符,可以用来获取一个变量或类型的字节数,不受变量值的影响。例如,sizeof(int)
返回4,sizeof(char)
返回1。strlen
是一个函数,用于获取一个字符串的长度,即字符数组中的字符个数,不包括字符串结束符'\0'
。例如,strlen("hello")
返回5。
const和static变量存放位置
我们将以下面的例子介绍const变量和static变量的存放位置:
static int val_a = 1 ; // 初始化的静态变量
int val_b = 2 ; // 全局变量
const int val_c = 3 ; // const 全局变量
static int val_d ; // 未初始化的静态变量
int val_e ; // 未初始化的全局变量
int main() {
static int val_f = 5; // 初始化的局部静态变量
static int val_g; //未初始化局部静态变量
int val_h = 6; //初始化局部变量
int val_i; //未初始化局部变量
const int val_j = 7; //const局部变量
return 0;
}
- static无论是全局变量还是局部变量都存储在全局/静态区域,在编译期就为其分配内存,在程序结束时释放,例如:val_a、val_d、val_f、val_g。
- const全局变量存储在只读数据段,编译期最初将其保存在符号表中,第一次使用时为其分配内存,在程序结束时释放,例如:val_c;const局部变量存储在栈中,代码块结束时释放,例如:val_j。
- 全局变量存储在全局/静态区域,在编译期为其分配内存,在程序结束时释放,例如:val_b、val_e。
- 局部变量存储在栈中,代码块结束时释放,例如:val_h、val_i。
注:当全局变量和静态局部变量未赋初值时,系统自动置为0。
- 我们说的
data(初始化的段)
和bbs段(未初始化的段)
都是针对全局变量和静态变量来说的 - data又分为
读写数据段(rw data)
和只读数据段(ro data)
- bbs是未初始化的全局变量和静态变量和初始化为0的全局变量和静态变量
- 我们在函数里面的定义的局部变量(除static xxx)都是在stack or heap里面
- bbs段不影响a.out的大小,bbs段的数据用占位符标示而已,不在a.out里面运行才产生
- stack heap也都是在运行的时候产生
堆栈区(stack),堆栈是由编译器自动分配释放,存放函数的参数值,局部变量的值等。栈的申请是由系统自动分配,如在函数内部申请一个局部变量 int h,同时判别所申请空间是否小于栈的剩余空间,如若小于的话,在堆栈中为其开辟空间,为程序提供内存,否则将报异常提示栈溢出。
其次是堆(heap),堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。堆的申请是由程序员自己来操作的,在C中使用malloc函数,而C++中使用new运算符,但是堆的申请过程比较复杂。
HTTPS如何保证数据的安全
HTTPS通过使用SSL/TLS协议来保证数据传输的安全性。这个协议主要提供三个基本安全保护:加密(确保数据的隐私性和安全性,防止数据在传输过程中被窃听)、身份验证(通过证书来验证服务器身份,确保数据发送到正确的服务器和客户端)、完整性(确保数据在传输过程中未被更改)。
TCP建立连接需要3次握手?为什么不是2和4
TCP使用三次握手来建立一个可靠的连接,确保双方都准备好发送和接收数据。三次握手的过程是:
- 客户端发送一个带SYN标志的数据包到服务器以建立连接
- 服务端回应一个带SYN/ACK标志的数据包以确认连接
- 客户端再次发送ACK包响应,此时连接建立成功
如果是两次握手,无法确认客户端接收能力是否准备好。四次握手则是多余的,会增加延迟和复杂性,而且不会带来额外的好处。
UDP需要握手吗
不需要。UDP是一种无连接的协议,不像TCP那样在发送数据之前需要建立连接。UDP直接发送数据包,不保证数据包的顺序或是否丢失,适用于实时性要求高的应用。
TIME_WAIT的作用
TIME_WAIT
是TCP连接断开后的一个状态,主要目的是确保所有数据包都正确完成传输。在这个状态下,连接会保持打开一段时间(通常是2分钟,可通过sysctl net.ipv4.tcp_fin_timeout
命令查看),以确保网络上的任何延迟或重复的数据包都可以被正确处理,并且确保连接的可靠终止。
TIME_WAIT
的作用是确保最后一个ACK包能够到达对方。当客户端发送完最后一个ACK包后,它会进入TIME_WAIT
状态,等待一段时间,以确保服务器能够收到这个ACK包。如果服务器没有收到这个ACK包,客户端收到后会重新发送ACK包。这样可以确保数据的可靠性。
TCP拆包粘包这种说法对吗
这种说法是不对的,粘包并不是TCP协议造成的,它的出现是因为应用层协议设计者对TCP协议的错误理解,忽略了TCP协议的定义并且缺乏设计应用层协议的经验。
- TCP 协议是面向字节流的协议,它可能会组合或者拆分应用层协议的数据;
- 应用层协议的没有定义消息的边界导致数据的接收方无法拼接数据;
TCP协议是面向连接的、可靠的、基于字节流的传输层通信协议,应用层交给TCP协议的数据并不会以消息为单位向目的主机传输,这些数据在某些情况下会被组合成一个数据段发送给目标的主机。当应用层协议通过TCP协议传输数据时,实际上待发送的数据先被写入了 TCP 协议的缓冲区,如果用户开启了Nagle
算法,那么TCP协议可能不会立刻发送写入的数据,它会等待缓冲区中数据超过最大数据段(MSS)或者上一个数据段被ACK时才会发送缓冲区中的数据。除了 Nagle 算法之外,TCP 协议栈中还有另一个用于延迟发送数据的选项TCP_CORK
,如果我们开启该选项,那么当发送的数据小于 MSS 时,TCP 协议就会延迟 200ms 发送该数据或者等待缓冲区中的数据超过MSS。
TCP 协议是基于字节流的,这其实就意味着应用层协议要自己划分消息的边界。在应用层协议中,最常见的两种解决方案就是基于长度或者基于终结符(Delimiter)。
TCP RST包的作用
RST: TCP协议中重置、复位连接的标志位,用来关闭异常连接。发送RST包关闭连接时,不等缓冲区的包发送完成,会丢弃缓冲区中的包,直接发送RST。同样接收端收到RST包后,也不必发送ACK确认包。RST的使用场景有:
- 当服务器收到没有监听端口的连接请求时会返回RST包
- 异常终止一个连接
- 检测半打开连接
TCP是可靠传输吗?如果是还需要在业务层保证幂等吗
是的,TCP是一种可靠的传输协议,它通过序列号、确认响应、重传等机制保证数据的正确传输。尽管如此,在业务层面上还是需要保证幂等性,以防止应用层的操作(如数据库操作)由于网络重试等原因重复执行。
Linux已经提供了Keep-Alive,为什么还需要应用层做心跳检测
虽然TCP的Keep-Alive机制可以帮助检测死链接,但这通常是在非活动连接上才会进行。应用层的心跳检测可以更灵活和及时的处理更多种类的断开情况,比如应用程序挂起、进程卡死等,从而提供更高层次上的保活功能。这对于需要高可靠性的应用来说尤为重要。
TCP理论带宽计算
以千兆网为例,首先要明确千兆网的速度为1000Mbps
,即每秒可传输1000M个bit,且这儿1M = 1000 K = 1000000
。那么千兆网的速度就是125MB/s
,即每秒传输125000000
个字节。(如果按MiB计算,就是119MiB/s)
在TCP/IP网络,一般使用以太网传输,在以太网中传输的是以太网帧。而TCP段是封装在IP包中然后封装在以太网帧中的。所以需要计算每秒中能传输多少个以太网帧。
通常以太网帧的最大长度是1518字节(不考虑jumbo frame),而以太网传输时需要在帧前传输7个字节的preamble
和一个字节的SFD
,帧之间还需要96 bit的IFG(Inter-Frame Gap)
,即12字节,这样千兆网每秒可以传输的以太网帧的数量至少为125000000/(1518+7+1+12)=81274
个。(另外,由于以太网帧最小为64字节,所以千兆网每秒最多能传输的以太网帧数量为125000000/(64+20)=1488095
,这个值是帧速率)
回到TCP带宽的计算,带宽是针对负载的,所以要去掉TCP,IP头,加上TCP的timestamp option,共52字节,那么千兆网的TCP带宽 = 81274*(1500-52) = 117684752
,约为117MB/s (或112 MiB/s)。
AIMD的目的和局限性
AIMD:additive increase multiplicative decrease,加法增加乘法减少。用于TCP拥塞控制
(目标是缓解并解除网络拥塞,让所有流量公平共享带宽,合在一起就是公平收敛)
- 拥塞窗口cwnd(Congestion Window)
- 发送方维持一个叫做
拥塞窗口
的状态变量,拥塞窗口的大小取决于网络的拥塞程度且动态变化。发送方让自己的发送窗口等于拥塞窗口,如果还考虑接收方的接受能力,那么发送窗口还可能小于拥塞窗口 - 发送方控制拥塞窗口的原则:只要网络没有出现拥塞,就增大拥塞窗口,以便将更多的分组发送出去;只要网络出现拥塞(发送方没有按时收到应当到达的确认报文),就减少拥塞窗口,以减少注入到网络中的分组数
- 发送方维持一个叫做
- 传输轮次:一个传输轮次所经历的时间是往返时间RTT。这里更加强调是将拥塞窗口cwnd所允许发送的报文段都连续发送出去,并收到对发送的最后一个字节的确认
TCP拥塞控制
分为慢启动
、拥塞避免
、快速重传
、快速恢复
。
- 慢启动:初始化一个很大的
慢开始门限(ssthresh)
和通常为一个最大分段大小(MSS)
的拥塞窗口(cwnd)
。开始传输,每经过一个传输轮次
(transmission round),将cwnd
加倍,呈指数增长,以快速探测网络的容量。 - 拥塞避免:当发送方达到
慢开始门限(ssthresh)
时,进入拥塞避免阶段。此时,发送方以线性增加的速率逐渐增加发送窗口的大小,以缓慢探测网络的容量,并避免网络拥塞。 - 快速重传:当接收方收到乱序分组时,立即发送最新的合法ACK,再收到乱序分组再发最新的合法ACK(暗示说该ACK的下一个分组未到)。当发送方连续收到三次相同的ACK时,就会意识到该ACK的下一个分组未到,则重发按个分组,而无需等待超时。
- 快速恢复:发送端响应快速重传,重发分组后,将
ssthresh
减半,将cwnd
从该值开始加法增长(拥塞避免算法)。
流程为:
- 初始化一个很大的
慢开始门限ssthresh
(slow start thresh),初始化拥塞窗口cwnd
(congestion window),通常为一个最大分段大小MSS
(maximum segment size)。 - 新建的TCP连接会尝试慢开始,每经过一个
传输轮次
(transmission round),将cwnd
加倍,呈指数增长。 - 当遭遇一个拥塞,将
ssthresh
设置为当前cwnd
的一半,然后从头开始慢开始
即乘法减小
。 - 当
cwnd
大于ssthresh
之后还没有遭遇拥塞,cwnd
的增长策略切换为拥塞避免
,每经历一个传输轮次后cwnd
加一个MSS
,呈线性增长,即加法增大
。
AIMD算法就是上面所说的加法增大和乘法减少的结合。
AIMD拥塞窗口更新策略也存在一些缺陷,加法增加
策略使发送方发送数据流的拥塞窗口在一个往返时延(RTT)内增加了一个MSS
的大小,因此,当不同的数据流对网络瓶颈带宽进行竞争时,具有较小RTT的TCP数据流的拥塞窗口增加速率将会快于具有大RTT的TCP数据流,从而将会占有更多的网络带宽资源。
ACK为何要延迟?带来什么问题?怎么解决?
TCP延迟确认是传输控制协议的一些实现用于提高网络性能的一种技术。本质上,几个ACK响应可以合并为单个响应,从而减少协议开销。
在Linux中,#define TCP_DELACK_MAX ((unsigned)(HZ/5))
和#define TCP_DELACK_MIN ((unsigned)(HZ/25))
。Linux内核每隔固定周期会发出timer interrupt(IRQ 0)
,HZ是用来定义每秒有几次timer interrupts
的。举例来说,HZ为1000,代表每秒有1000次timer interrupts。HZ可在编译内核时设置,一般HZ值为250。以此可知,最小的延迟确认时间为40ms。随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。具体调整算法,可以参考linux-2.6.39.1/net/ipv4/tcp_input.c, Line 564的tcp_event_data_recv函数
。
带来的问题:
延迟ACK引入的额外等待时间在与某些应用程序和配置交互时可能导致进一步的延迟。如果发送方正在使用Nagle的算法,则数据将由发送方排队,直到收到ACK。如果发送方未发送足够的数据来填充最大分段大小(例如,如果它执行两次小的写操作,然后执行阻塞读取),则传输将暂停到ACK延迟超时。
Linux内核从版本2.4.4开始支持禁用延迟ACK的TCP_QUICKACK套接字选项。 由此可以得知,如果发送方的套接字层的数据少于一个完整数据包,根据Nagle的算法,在收到已发送数据的ACK之前,是不会发送这段数据的。与此同时,接收方的应用层在获取所有数据之前不会发送响应;如果接收方启用了延迟的ACK,则它的套接字层将不会发送ACK,直到达到超时。
如果应用程序以较小的块发送数据并期望定期ACK,则可能发生这种负面交互。为防止此延迟,应用程序层需要连续发送数据而无需等待ACK,或者Nagle的算法可能被发送方的应用程序禁用。
解决办法有:
Linux2.4.4
开始,可以修改代码,在程序每次recv
之后立即调用setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int));
每次重新设定的原因是设定的TCP_QUICKACK
不是永久的(man 7 tcp
查看手册)- 在RHEL系发行版本中,可调整系统级别的最小延迟确认时间:
echo 1 > /proc/sys/net/ipv4/tcp_delack_min
一端持续发送另一端不recv会发生什么?一段时间后恢复recv又会发生什么?
在TCP(传输控制协议)通信中,数据传输是双向的,并且每一方都有自己的接收缓冲区。当一端(我们称之为发送方)持续发送数据,而另一端(接收方)不调用recv
来读取数据时,会发生以下几个步骤:
-
发送方的发送缓冲区填满:发送方会继续将数据放入其本地的发送缓冲区,直到该缓冲区被填满。当缓冲区被填满后,如果继续尝试写入数据,会发生以下几种情况,具体取决于该套接字的设置和行为:
- 阻塞模式:如果套接字处于阻塞模式(默认设置),那么当发送缓冲区满时,写入会阻塞。这意味着写入将停止执行,直到有足够的空间在发送缓冲区中释放出来,使得新的数据能够被写入。
- 非阻塞模式:如果套接字被设置为非阻塞模式,那么在发送缓冲区满的情况下,写入将不会阻塞调用线程。相反,它会立即返回一个错误,通常是
EWOULDBLOCK
或EAGAIN
,表明操作当前无法完成,建议稍后重试。 - 使用select或poll:在开发中,可以使用
select
、poll
或epoll
等系统调用来检查套接字的状态。这些工具可以用来检测何时发送缓冲区有可用空间,从而避免在写入操作上阻塞或错误地退出。
因此,继续尝试写入数据时的具体行为将取决于套接字的配置以及你的错误处理策略。在实际应用中,通常需要准备处理这种情况,确保数据的正确发送和应用的稳定运行。
-
接收方的接收缓冲区填满:接收方的TCP栈会不断地从网络中读取数据到其本地的接收缓冲区,即使应用程序没有调用
recv
函数。当接收缓冲区被填满后,TCP栈将不再确认新的数据包,这导致发送方的TCP栈停止发送更多数据。 -
TCP流量控制:TCP使用流量控制机制(如滑动窗口协议)来确保发送方不会溢出接收方的缓冲区。当接收方的缓冲区满时,它的窗口大小将会被设置为0,发送方将停止发送数据直到接收方再次读取数据并释放缓冲区空间。
-
持续发送方的重传超时:如果发送方在发送缓冲区满的情况下仍然有未被确认的数据,它可能会因为超时重传机制而尝试重传这些数据。但是,如果接收方的窗口大小仍为0,这些重传也不会被确认。
如果在一段时间后,接收方开始调用recv
函数:
- 接收缓冲区释放空间:接收方的应用程序通过
recv
读取数据,释放接收缓冲区中的空间。 - 窗口大小更新:接收方的TCP栈将新的窗口大小(即可用的缓冲区大小)告知发送方,使得发送方可以恢复发送数据。
- 数据传输恢复:发送方根据接收到的窗口更新开始发送数据,数据传输恢复正常。
需要注意的是,如果接收方长时间不调用recv
,而发送方的TCP栈持续尝试重传未被确认的数据,这可能会导致发送方的TCP栈最终超时并关闭连接。这个超时时间通常是可配置的,并且会因操作系统和网络配置的不同而异。如果连接被关闭,那么后续的recv
调用将会返回错误,表明连接已经不再可用。
从网卡中断到recv中间发生了什么?
当网卡上收到数据以后,首先会以DMA的方式把网卡上收到的帧写到内存的RingBuffer
中。再向CPU发起一个中断,以通知CPU有数据到达。当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数。 网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU。ksoftirqd
检测到有软中断请求到达,调用网卡驱动注册的poll
开始轮询收包,收到后交由各级协议栈处理。Linux内核驱动会处理链路层数据、协议栈会处理网络层和传输层的数据。对于UDP包来说,会被放到用户socket
的接收队列中等待recv
调用读取。
可以参阅**图解Linux网络包接收过程**
bbr/reno/cubic对比
- TCP Reno:
- 慢开始(慢启动)
- 拥塞避免
- 快重传
- 快恢复
- TCP Cubic:
- cubic与Reno的区别在于,Reno在
ssthresh
之后的增长为固定值,而bic使用的是二分查找,这样就能快速到达收敛的Wmax,但是bic在短RTT
的劣网环境下,激进的增窗会引发带宽的争抢,使得拥塞控制不具备公平性。cubic是一种改进的BIC,BIC在短RTT下增窗快,本质是因为其算法依赖RTT。CUBIC的实现方式整体和BIC一样,只不过增窗不再依赖RTT,而是根据一个公式来决定增窗的值,从而使得整个增窗曲线还是会和BIC保持差不多的形态,只不过不会在短RTT场景下再有快速增窗的问题了。
- cubic与Reno的区别在于,Reno在
- TCP BBR:
- bbr是google 2016年发布的拥塞算法,2019年更新成bbrv2,也是google内部默认使用的tcp拥塞算法,算是当前最强的拥塞算法了。google论文中提到在youtube中应用后有整体有53%的延迟降低,发展中国家的请求更是降低了80%的延迟。
- bbr算法显然是更加基于数学模型了,摒弃了一些在现代网络环境下并不是特别严谨的假设,例如遇到重传则认为网络拥堵需要开始减速。
- 以往大部分拥塞算法是基于丢包来作为降低传输速率的信号,都属于基于丢包反馈的被动式机制(Loss-Based)。主要缺点是:
- 丢包即拥塞的假设不一定完全正确:现实中网络环境很复杂会存在错误丢包,很多算法无法很好区分拥塞丢包和错误丢包,因此在存在一定错误丢包的前提下在某些网络场景中并不能充分利用带宽。
- bufferbloat问题:路由器、交换机等设备缓冲区在Loss-Based策略中也计算了,然而现在网络设备都有比较大的网络设备缓冲区,TCP发送端真正感知到拥塞和时机产生的时间会产生比较大的误差。网络已经拥塞,但是发送端在发现之前还在增大窗口,这就使得网络更加拥堵不堪了,在弱网、高延迟的环境中,这个问题会更加严重。
- 网络负载高、无丢包时Loss-Based策略仍然增窗:增加了网络带宽的竞争
- 高负载丢包、网络抖动:reno、cubic这种windows based的都有这个问题,就是带宽侵略性,不断增加窗口,最终必然导致乘法减小,网络抖动。
- 低负载高延迟丢包时带宽利用率低:有些丢包时因为弱网环境下延迟导致,而不是带宽争抢导致的拥塞导致,此时根据loss-based策略,带宽利用率比较低
- BBR基于模型主动探测。该算法使用网络最近出站数据分组当时的最大带宽和往返时间来创建网络的显式模型。数据包传输的每个累积或选择性确认用于生成记录在数据包传输过程和确认返回期间的时间内所传送数据量的采样率。
分片发生在udp层还是ip层
UDP和ICMP协议么有考虑分片的问题,它的协议可以认为网络层没有长度限制,所以如果我们在UDP协议中,发送大的数据时,就必然触发IP层分片。TCP协议本身支持分段,TCP协议在建立连接时会协商MSS(Maxitum Segment Size),这个协商的过程发生在建立TCP连接的过程中。如下图:
MSS加包头数据就等于MTU。拿TCP包做例子,报文传输MSS=1460字节的数据的话,再加上20字节IP包头,20字节TCP包头,那么MTU就是(1500)。
client
和real server
分别根据自己MTU计算出支持的最大MSS发送给对方,双方根据最小的MSS达成协议。下图用来说明,TCP协议分段和UDP协议触发的IP层分片的区别。
但是也不能认为,TCP协议就不会触发IP层分片;因为在TCP协议握手时,client
和real server
只是取了自己网卡的MTU, 但是它们之间可能经过了很多的路由器,这些子网的MTU可能小于它们的MTU,所以在外部的复杂网络上,很多TCP协议也会触发IP层分片。需要注意的是,被IP层分片的数据包不会带上4层的协议头(TCP头、UDP头)。
网卡硬件的DMA对齐要求和什么有关?
网卡硬件的DMA(Direct Memory Access)对齐要求通常与以下几个因素有关:
- 硬件架构: 不同的CPU和平台架构对内存访问的对齐要求不同。例如,有些CPU可能要求数据必须按4字节或8字节边界对齐。
- 网络卡设计: 网卡硬件本身可能有特定的对齐要求,这通常是为了最大化数据传输效率和降低处理延迟。比如一些网卡可能要求数据缓冲区按32字节或64字节对齐。
- 操作系统: 操作系统中的DMA和驱动程序实现也会影响对齐要求。操作系统可能会提供API或设置,以支持硬件对齐约束。
- 总线接口协议: PCI或PCIe等总线接口标准对DMA传输中的数据对齐也有规定。不正确的对齐可能导致性能下降或更高的CPU使用率。
- 缓存线大小: 在许多系统中,为了提高缓存效率,DMA缓冲区可能需要按缓存行大小对齐,例如通常是64字节对齐。
为了确保最优性能,通常需要根据具体的硬件文档和开发指南来确定DMA缓冲区的对齐方式。不遵守这些对齐要求可能导致性能下降,甚至在某些硬件上可能导致数据传输错误。
scatter-gather传输对驱动的冲击在哪里?
中断处理应该放在tasklet还是work queue中?
中断处理通常应该放在tasklet中而不是work queue中。这是因为中断处理需要尽可能快速地完成,以尽快响应外部事件并减少系统的延迟。Tasklet是一种轻量级的延迟处理机制,适合用于快速处理中断。它们在中断上下文中执行,不会被抢占,因此可以保证相对较低的延迟。
相比之下,work queue适用于较长时间的延迟处理,因为它们在内核线程上下文中执行,并且可以被抢占。将中断处理放在work queue中可能会导致不确定的延迟,从而影响系统的实时性能。
总的来说,对于需要快速响应的中断处理,应该优先考虑使用tasklet。但是对于一些需要较长时间处理或不需要实时响应的情况,可以考虑将处理放在work queue中。
为什么Linux的网络传输效率比windows好?
Linux和Windows在网络传输效率方面的差异可以归因于几个关键因素:
- 网络堆栈实现:Linux的网络堆栈被广泛认为是高度优化和高效的。它经常被更新以利用最新的网络技术和协议优化。此外,Linux内核允许更深入的定制和优化,这对于需要高性能网络传输的应用场景(如数据中心、高性能计算等)特别有利。
- 系统架构和优化:Linux系统通常包括多种可以调整和优化以提高网络性能的工具和选项。例如,网络参数(如TCP窗口大小、队列长度等)可以根据具体需求调整,这有助于提升大规模网络操作的效率。
- 社区和开发模型:Linux作为开源操作系统,拥有一个活跃的开发社区,社区成员包括来自全球的众多网络专家和高级程序员。这种开放的发展模型促进了创新和快速采纳新技术,有助于不断改进网络性能。
- 驱动程序和硬件支持:Linux在网络硬件驱动方面也具有较强的适应性和性能。许多网络设备制造商和大型云服务提供商都直接为Linux提供优化的驱动程序和工具,这进一步提高了其在服务器和专业应用中的网络效率。
- 定制性和灵活性:Linux提供了高度的定制性和灵活性,使得系统管理员可以根据具体的网络环境和性能需求对系统进行精细调整。这种灵活性在高需求的网络环境中尤其重要,比如调整内核行为以减少延迟或增加吞吐量。
然而,实际上在某些应用场景中,Windows可能因其与特定硬件和软件的良好集成而提供相当或更优的网络性能。因此,两者在网络传输效率上的比较往往取决于具体的使用场景、系统配置和网络环境。在企业环境中,选择哪个操作系统还可能受到其他因素的影响,如现有的技术栈、支持的应用程序和企业政策。
TCP滑动窗口在什么情况下会导致发送阻塞?解决方法有哪些?
TCP滑动窗口会在以下情况下导致发送阻塞:
- 窗口大小为零: 当接收方的窗口大小变为零时,发送方必须停止发送数据,等待接收方窗口再次开放。这通常发生在接收方处理数据较慢,无法及时消耗接收缓冲区中的数据时。
- 网络拥塞: 如果网络中的拥塞导致数据包延迟或丢失,发送方的拥塞窗口大小会减小,可能达到一个点使得发送方不能发送更多的数据,直到网络状况改善。
- 接收窗口未及时更新: 如果接收方未能及时发送包含新窗口大小的确认(ACK)包,发送方将不知道已经有更多的缓冲区可用于发送数据。
解决TCP滑动窗口导致的发送阻塞的方法包括:
- 窗口更新机制: 接收方可以通过发送窗口更新包来通知发送方其可用窗口大小已增加,这可以在数据处理速度增加后立即通知发送方继续发送数据。
- TCP拥塞控制算法: 实现和优化TCP的拥塞控制算法(如Cubic, Reno, BBR等)可以帮助调整拥塞窗口的大小,更有效地处理网络中的拥塞情况。
- 调整窗口大小: 调整TCP窗口的大小可以帮助更好地利用网络带宽,尤其是在高延迟网络环境(如卫星通信)中。较大的窗口可能允许在等待确认的同时发送更多的数据。
- 使用选择确认(SACK): 通过使用选择确认,即使在发生部分数据包丢失的情况下,也可以避免不必要的数据重传,从而提高整体传输效率。
通过这些方法,可以在多种网络和系统条件下有效管理TCP窗口,避免发送阻塞,从而提高数据传输的效率和稳定性。
在TCP传输中,零窗口是什么意思?它如何影响数据流?
在TCP传输中,"零窗口"是指接收方告知发送方其接收窗口的大小为零,即接收方的缓冲区已满,无法再接受新的数据。这是一种流量控制机制,用来避免发送方发送的数据超过接收方能够处理的速度。
零窗口的影响包括:
- 暂停数据传输:当发送方收到零窗口通知时,它必须停止发送数据,直到接收方的窗口重新打开(即接收方处理了部分数据,并通过发送非零窗口大小的确认告知发送方)。
- 延迟:零窗口导致数据传输中断,这会增加整个通信的延迟。特别是在高速网络或高数据率应用中,频繁的零窗口事件会显著影响性能。
- TCP效率下降:频繁的零窗口事件可能导致TCP连接的效率下降,因为连接多数时间处于等待状态,而不是传输状态。
为了应对零窗口的问题,TCP协议包括一种称为“零窗口探测”(Zero Window Probe, ZWP)的机制。当发送方在接收到零窗口通知后一段时间内没有收到窗口更新,它会定期发送零窗口探测包,这是一种小的数据包,用来检查接收方是否已准备好接收更多数据。一旦接收方的窗口重新打开,它将回复一个更新的窗口大小,发送方随后可以继续数据传输。
这种机制有助于确保即使在接收方窗口长时间关闭的情况下,连接也能维持活性并在可能的情况下恢复数据传输。