运行库和运行环境
任何一门语言,包括其运行库和运行环境,都不可能创造出操作系统不支持的功能,Go语言也是这样,不管它的特性描述看起来多么炫丽,那必然都是其他语言也可以做到的,只不过Go提供了更方便更清晰的语义和支持,提高了开发的效率。
go运行库是go运行环境的充分条件。
并行和并发
并发是指程序的逻辑结构。非并发的程序就是一根竹竿捅到底,只有一个逻辑控制流,也就是顺序执行的(Sequential)程序,在任何时刻,程序只会处在这个逻辑控制流的某个位置。而如果某个程序有多个独立的逻辑控制流,也就是可以同时处理(deal)多件事情,我们就说这个程序是并发的。这里的“同时”,并不一定要是真正在时钟的某一时刻(那是运行状态而不是逻辑结构),而是指:如果把这些逻辑控制流画成时序流程图,它们在时间线上是可以重叠的。
并行是指程序的运行状态。如果一个程序在某一时刻被多个CPU流水线同时进行处理,那么我们就说这个程序是以并行的形式在运行。(严格意义上讲,我们不能说某程序是“并行”的,因为“并行”不是描述程序本身,而是描述程序的运行状态,但这篇小文里就不那么咬文嚼字,以下说到“并行”的时候,就是指代“以并行的形式运行”)显然,并行一定是需要硬件支持的。
并发是并行的必要条件,如果一个程序本身就不是并发的,也就是只有一个逻辑控制流,那么我们不可能让其被并行处理。
并发不是并行的充分条件,一个并发的程序,如果只被一个CPU流水线进行处理(通过分时),那么它就不是并行的。
并发只是更符合现实问题本质的表达方式,并发的最初目的是简化代码逻辑,而不是使程序运行的更快。
互斥锁/自旋锁/读写锁/条件锁
互斥锁:进程/线程操作加锁资源时,发现加锁资源被占用,则线程挂起,等待加锁资源状态改变由操作系统唤醒,抢占资源。互斥量是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程,让其投入运行。
自旋锁:非阻塞锁,发现加锁资源被占用后会一直尝试获取加锁资源控制权(忙等待)。也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。(自旋锁一般只在内核层存在,因为操作系统是不能被阻塞的,操作系统阻塞了就没有人能唤醒了,用户层想用自旋锁,需要自己实现)
互斥锁和自旋锁两种锁适用于不同场景
如果是多核处理器,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。
如果是多核处理器,如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用互斥量。
如果是单核处理器,一般建议不要使用自旋锁。因为,在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等到运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。
如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥量的开销较大。
读写锁:简单归为读(共享)-写(独占)锁。相对互斥量只有加锁和不加锁两种状态,读写锁有三种状态:读模式下的加锁,写模式下的加锁,不加锁。
读写锁的使用规则
只要没有写模式下的加锁,任意线程都可以进行读模式下的加锁;
只有读写锁处于不加锁状态时,才能进行写模式下的加锁;
排队自旋锁的设计原理
uint64的变量 slock
- 0-15 owner
- 16-31 next
- 32-63 reserve
初始化时,slock 置为0, owner = 0, next = 0。
当有线程想要执行自旋锁的时候,next原子++,并返回给线程一个票据id。当线程释放自旋锁的时候,owner原子++。(只有owner == 自己票据id 表示此时锁对自己可用,否则忙等待)
与传统自旋锁对比
解决了传统自旋锁的不公平性(因为当临界资源的锁被释放时,具体next锁占用的线程是谁,没人知道,不可预估,完全由操作系统决定),而排队自旋锁一定程度上保证了线程对锁资源的顺序。
阻塞/非阻塞/同步/异步
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
同步和异步关注的是消息通信机制(synchronous communication/ asynchronous communication)。
阻塞:执行某个操作会导致当前进程阻塞状态。
非阻塞:执行某个操作不会导致当前进程阻塞状态。
同步:调用者会等待调用返回,并获取到调用结果。
异步:调用者会立即返回,获取不到结果。
异步设计能避免阻塞。
进程状态图:
创建:新创建的进程会先创建一个PCB(进程控制块),然后等待内核给他分配资源(进程处于创建状态),当分配资源准备好了,此时进程状态由创建->就绪。
就绪:进程资源准备就绪,处于就绪队列,等待内核调度分配cpu控制权。
执行:进程被内核调度,获得cpu控制权,执行程序逻辑。
阻塞:进程发现某些事情(i/o操作),无法申请资源等等,无法继续执行下去,系统则调用阻塞原语,使进程陷入阻塞状态。等待内核调用唤醒原语再次唤醒。
终止:进程结束,内核回收PCB以及清理工作。
进程切换
进程切换的过程如下:
保存处理机上下文,包括程序计数器和其他寄存器。(处理机信息保存)
更新PCB信息。
把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
选择另一个进程执行,并更新其PCB。
更新内存管理的数据结构。
恢复处理机上下文。
注意,进程切换与处理机模式切换是不同的,模式切换时,处理机逻辑上可能还在同一进程中运行。如果进程因中断或异常进入到核心态运行,执行完后又回到用户态刚被中断的程序运行,则操作系统只需恢复进程进入内核时所保存的CPU现场,无需改变当前进程的环境信息。但若要切换进程,当前运行进程改变了,则当前进程的环境信息也需要改变。
进程间通信
共享内存:由于进程间用户空间是相互独立的,必须通过特殊系统调用实现两个进程用户空间共享。而线程内是天然共享用户空间。(linux下使用shmget实现)
消息队列:
管道:
信号:
信号量:
套接字:
线程间通信
- 操作全局变量
线程与进程:
在没有线程的进程:进程是拥有资源和独立调度的基本单位
线程:调度基本单位
进程:资源的基本单位
多线程之间共享进程的用户空间(线程只拥有自己的独立栈空间),进程是系统资源的基本单位。
线程属性
在多线程操作系统中,把线程作为独立运行(或调度)的基本单位,此时的进程,已不再是一个基本的可执行实体。但进程仍具有与执行相关的状态,所谓进程处于“执行”状态,实际上是指该进程中某线程正在执行。
线程是一个轻型实体,它不拥有系统资源,但每个线程都应有一个唯一的标识符和一个线程控制块,线程控制块记录了线程执行的寄存器和栈等现场状态。
不同的线程可以执行相同的程序,即同一个服务程序被不同的用户调用时,操作系统为它们创建成不同的线程。
同一进程中的各个线程共享该进程所拥有的资源。
线程是处理机的独立调度单位,多个线程是可以并发执行的。在单CPU的计算机系统中,各线程可交替地占用CPU;在多CPU的计算机系统中,各线程可同时占用不同的CPU,若各个CPU同时为一个进程内的各线程服务则可缩短进程的处理时间。
—个线程被创建后便开始了它的生命周期,直至终止,线程在生命周期内会经历阻塞态、就绪态和运行态等各种状态变化。
tcp滑动窗口协议
接收窗口大小:
发送窗口大小:
ack包:
当前发送seq:
停等协议:滑动窗口接收方与发送方一致1。(发1个,停下来等一下,接收ack)
后退n协议:滑动窗口接收方1,发送方n。(发送方一次发送n个,接收方一次接受1个,当发现第x个丢包了,则需要重发x-n个包)
重传协议:后退协议问题上,丢哪个包,重传哪个包。
tcp建立连接
第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
-
第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
完成了三次握手,客户端和服务器端就可以开始传送数据。以上就是TCP三次握手的总体介绍。
那四次分手呢?
当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,肯定是要断开TCP连接的啊。那对于TCP的断开连接,这里就有了神秘的“四次分手”。
第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
为什么需要2MSL等待时间:
MSL定义的规则如下:在TIME_WAIT状态下,客户端发出ACK以后需要等待2个MSL的时间。假如ACK在传输中丢失,服务端会重新发送FIN,客户端收到以后会重新发ACK。假如在2MSL时间中客户端都没有收到服务端重发的FIN,那么认为服务端已经收到了客户端发送的ACK。此时客户端可以安全断开连接。
客户端发送的ACK segment存活期1MSL,服务端重发FIN segment存活期1MSL,加一起2MSL。2MSL是一个临界值,利用尽量大的等待时间来确保TCP连接断开的可靠性。
假设握手需要两次:c->s 和 s->c建立连接
有一种情况:
c->s 发起连接,服务端收到之后过了很长时间才响应(c端的连接已经失效),c端因为连接已经失效,不理会,然后s端以为建立起连接,等待c端传输数据流, 会造成连接资源的浪费。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)
CLOSE_WAIT:这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FINWAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)
CLOSED: 表示连接中断。
非抢占式(Nonpreemptive)
让进程运行直到结束或阻塞的调度方式 容易实现 适合专用系统,不适合通用系统
抢占式(Preemptive)
允许将逻辑上可继续运行的在运行过程暂停的调度方式 可防止单一进程长时间独占CPU 系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序
go 语言中的抢占式调度(总结):
GOMAXPROCS == 1时 tight loop 情况下的抢占式调度
引用一句话:Currently goroutines are only preemptible at function call points. 现在goroutine抢占式调度只在函数调用点插入调度埋点。 (而且现在go语言只能算得上半抢占半协作式调度, 因为所谓的在函数调用点插入调用埋点不准确,准确的说法是,在morestack->newstack(连续栈的扩展)这个过程才会插入调度埋点,实现抢占式调度)
DDOS解决办法
限制同时打开的SYN半连接数目,缩短SYN半连接的time out 时间,限制SYN/ICMP流量
用户应在路由器上配置SYN/ICMP的最大流量来限制SYN/ICMP封包所能占有的最高频宽,这样,当出现大量的超过所限定的SYN/ICMP流量时,说明不是正常的网络访问,而是有黑客入侵。早期通过限制SYN/ICMP流量是最好的防范DOS的方法,虽然目前该方法对于Ddos效果不太明显了, 不过仍然能够起到一定的作用