https://pic4.zhimg.com/v2-e263475646652726731e13e44b5d2479_im.jpg

中断

中断分类 中断按照是否由CPU内部发生分为 外部中断 (由CPU外部引发,比如外设、网卡等) 内部中断 (由CPU内部引发,比如系统调用) 外部中断分为 可屏蔽中断 (比如网卡发出数据读取中断,可以屏蔽) 不可屏蔽中断 (比如掉电、设备损坏等) 内部中断分为 软中断 (由软件程序触发,比如系统调用0x80号中断) 异常 (异常也是一种特殊的中断,比如除0异常、缺页异常) ​ 中断产生和调用过程 触发中断其实就是给出一个 中断号 ,然后CPU根据中断号去查找中断对应的程序进行执行的一个过程 操作系统会设置一个 IDT中断描述符表 ,里面记载了各个中断号对应的程序的入口地址,操作系统内核在运行初始化的时候必须初始化 中断寄存器指向的位置 ,因为一旦发生中断CPU就会去中断寄存器指向的IDT中去寻找中断程序 发生中断时用户态就会转化为内核态,CPU会保存程序当前执行的状态,比如会将栈指针、各个寄存器值、PC指针压入内核栈,然后开始执行中断处理程序,执行完毕之后再恢复现场,也就是将栈中的值都出栈然后放到各个寄存器中继续执行之前的代码 这里需要注意中断的进程状态管理和进程调度的进程状态管理这两个是不一样的 中断发生时的进程状态是保存在内核栈中,而进程调度发生时需要将进程的状态保存在PCB中 那么这个中断是如何引发的呢? 软中断和异常容易理解,CPU有一个int指令,后面跟给一个中断号,程序只需要执行这个指令即可触发软中断或异常,最常见的软中断就是int 0x80中断,此中断是系统调用中断,所有的系统调用都会触发这个中断,系统调用中断还需要给出 系统调用号 ,系统调用中断处理程序会根据系统调用号去执行相应的系统调用代码 外中断需要经过一个叫 中断代理 的硬件,该硬件连接了所有外设接口的中断线,外设如果需要引发中断则只需要往这个中断线里面传输信号到中断代理即可,中断代理会根据不同的信号转化为相应的 中断号 ,中断代理和CPU只连接了两条线: INTR和NMI INTR表示可屏蔽中断,比如网卡发出的中断、NMI表示不可屏蔽中断 可屏蔽中断的话只需要CPU设置状态寄存器中相应的位就表示屏蔽这些中断,注意: 软中断、异常都是会忽略该位的 也就是说软中断和异常都是不可屏蔽的,软中断通常来说是系统调用,用户进程需要系统调用则操作系统必须响应,否则会造成用户程序卡死无法获得操作系统提供的帮助,会造成用户体验不好,异常更不用说了,需要立即处理 中断代理将相应的中断号输出到INTR线中通知CPU,CPU没执行一条指令都会检查该线是否有中断信号(注意:CPU检查中断是由硬件电路来实现的,并不会有任何的损耗) 如果有中断信号则CPU会立即检测到 ​ 中断的上半部分和下半部分 一个中断程序可以分为 上部分和下部分 ,中断处理时可以先执行上半部分,这个部分一般来说比较紧急,等CPU空闲之后再继续执行下半部分 比如网卡发出数据读取操作,此时CPU先执行上半部分的中断程序:先将网卡缓存区里的数据拷贝到内核数据区,这个网卡就不会因为缓存区满而频繁丢包了(相当于一个紧急救火),然后等CPU空闲了之后再执行中断下半部分,也就是将内核数据区域的数据拷贝到用户空间供用户使用 ​ 操作系统时钟中断 这个时钟和CPU的时钟不是一个概念,但是他们的功能都是差不多的,都是为了使操作系统或则CPU能正确的工作,时钟就相当于一个 节拍器,很多硬件设备也有时钟,为的就是让各个电路有序的进行操作 操作系统的时钟中断由 可编程的脉冲器 产生的,因为操作系统需要和硬件配合的,所以一般和硬件结合的比较紧密,该硬件脉冲频率是和操作系统厂商商量好的,并且可以人为的编程去改变脉冲的频率 时钟中断会定时的引发然后传递到CPU的NMI引脚,时钟中断属于不可屏蔽中断 时钟中断的作用非常大,可以说没有时钟中断操作系统就没有控制权,时钟中断的作用如下: 更新系统的时间 定时的将控制权转移到操作系统的程序中,以便操作系统进行各个调度工作比如进程时间片调度,每经过一个时钟则时间片减一 (如果没有时钟中断,那么如果用户程序如果一直不进行系统调用、不引发异常那么操作系统可能永远也拿不到CPU的控制权) ​ 参考 《操作系统真象还原》 https://www.zhihu.com/question/320636133

完全二叉树和满二叉树

满二叉树 如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树 满二叉树有如下几个性质: 满二叉树第i层节点的个数2^(i-1) 深度为n的满二叉树必须有2^(n)-1个节点,叶子节点有2^(n-1) (也就是最后一层的节点数量) 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树 具有 n 个节点的满二叉树的深度为 log2(n+1) 第i个节点的左右孩子分别为i*2+1 i*2+2 (孩子节点从0开始算起,如果孩子节点从1开始算起的话就是i*2 i*2+1 ) 第i个节点的父亲节点i/2-1 ,如果从1开始算起就是i/2 ​ 完全二叉树 结点依次从左到右分布,中间无法断开,则此二叉树被称为完全二叉树,完全二叉树适合用数组来存储 第i个节点的左右孩子分别为i*2+1 i*2+2 (孩子节点从0开始算起,如果孩子节点从1开始算起的话就是i*2 i*2+1 ) 第i个节点的父亲节点i/2-1 ,如果从1开始算起就是i/2 注意上面的计算要注意范围 堆就是用完全二叉树来实现的

Redis命令总结

Keys COPY 复制key COPY name username #name->username username必须不存在 COPY name username DB 1 #name->1:username COPY name username REPLACE #存在则更新 DEL 删除key 返回值是删除的个数 UNLINK非阻塞删除,重新开辟一个线程去回收内存,立即返回 DEL key1 key2 EXISTS 查看可以是否存在 返回值是存在键的数量 EXISTS age username EXPIRE EXPIREAT 设置key的过期时间,如果重新设置的key的值,前者是设置n秒后过期,后者设置一个Unix时间戳,表示在指定时间戳后过期 TTL 查看key剩余的时间·秒,返回-1则表示永久 -2则表示key不存在,PTTL则是毫秒 PERSIST 解除timeout时间 EXPIRE name 10 EXPIREAT name 8233132131 TTL name PERSIST name KEYS 模式匹配展示出keys KEYS name* # * 匹配所有 KEYS name? # ? 匹配一个 MOVE 移动key到指定的db MOVE age 2 OBJECT 展示redis每个key的对象相关的信息,比如对象底层的数据结构是什么 redis有五大常见对象:

WebSocket协议

WebSocket要解决的问题 由于HTTP是请求应答模式,所以如果是聊天室这样的项目的话,那么为了立即获取到信息浏览器就必须进行不断的轮询服务器,不仅效率低下,而且还无法立即获取到数据 于是就有了WebSocket协议,WebSocket是通过HTTP协议进行握手然后再进行通讯的,因为浏览器无法同服务器直接建立TCP连接,所以只能先通过HTTP协议建立一个TCP连接通道,之后再升级协议采用WebSocket协议,这样WebSocket就和HTTP采用相同的端口进行和服务器通讯了 ws://www.chrono.com ws://www.chrono.com:8080/srv wss://www.chrono.com:445/im?user_id=xxx #加密的websocket协议 ​ WebSocket握手 WebSocket握手是通过HTTP协议进行的 首先浏览器请求升级协议 GET /xx HTTP/1.1 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: sdadsxxada== Sec-WebSocket-Version: 13 Sec-WebSocket-Key 是一个 Base64的16byte的随机数 用于简单认证,防止误连 然后浏览器返回101响应 HTTP/1.1 101 Switching Protocols Sec-WebSocket-Accept: dsdada Sec-WebSocket-Accept 是把请求头里Sec-WebSocket-Key的值,加上一个专用的 UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,再计算 SHA-1 摘要,客户端接受到响应之后使用相同的方法计算出摘要然后判断是否相等,相等表示认证成功 握手成功之后传递的就是WebSocket报文了 ​ WebSocket报文格式 域 说明 FIN 1bit,是否为信息的最后一帧 RSV 1-3 1bit,备用,默认为0 opcode 4bit,帧类型 1 表示帧内容是纯文本,2 表示帧内容是二进制数据,8 是关闭连接,9 和 10 分别是连接保活的 PING 和 PONG MASK 1bit 掩码,是否用XOR进行简单加密数据。 客户端发送给服务端时,mask必须为1,否则断开连接。 服务端发送给客户端时,mask必须为0,否则断开连接。 payload length 表示帧的长度。它是另一种变长编码,最少 7 位,最多是 7+64 位,一个 WebSocket 帧最大是 2^64 Masking-key 0或32 bit掩码值(Mask为1时才有),是一个4byte的随机数 Playload data 长度为Payload len的数据,如果有掩码,需要用Masking-Key来异或解密 ​

TCP协议

TCP协议 面向连接、可靠的字节流传输协议 全双工 可相互传递字节流 只可用于 一对一通信 ,不能用于多播和组播 TCP 使用校验和,确认和重传机制来保证可靠传输 TCP 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制 TCP使用 (源地址,源端口,目的地址,目的端口)来标识一个连接 TCP报文 源端口和目的端口 端口大小为16位,可见端口范围为:0~2^16 也就是[0~65536] IP层则用ip地址来标识一台主机,TCP层用端口来标识一个应用,使用(源地址,源端口,目的地址,目的端口)唯一确定一个连接,所以对于IPV4的话单台主机最大的连接数为 2^(32+16+32+16) 个 序号 每个TCP包都有一个序号,编号是为了解决TCP包乱序问题,给每个包编上序号就知道了每个包的顺序,这样就可以完整的拼好所有的TCP包了而不造成乱序,TCP给个字节都有一个序号,序号是整个TCP包数据段的第一个字节的序号,比如现在一个包的起始序号为101,数据长度为500byte ,那么最后一个字节的序号就为600,这个TCP包的序号为第一个字节的序号101 ,所以下一个TCP包的第一个字节的序号应该为601,也就是下一个TCP包的序号为601 如果序号达到最大值2^32,则回卷到0 由于初始的seq号是随机生成的,所以TCP回绕到0之后怎么继续保持字节序呢?怎么判断字节序呢? 回绕之后遇到相同的seq号则可以根据TCP头部携带的时间戳来判断前后 确认号 设置TCP包的确认号是为了告诉对方上一个数据包已经确认收到了,你可以继续发送下一个TCP包了,这个确认号填的就是期望对方发送的下一个TCP包的 序号 比如B收到了A发的序号为101的TCP包,该包长度为500byte,B现在收到了A发的600byte之前的数据,期望A发送下一个序号为601的TCP包,所以确认号为601 A收到B的回复的确认号为601得知B已经收到了A发的600byte之前的数据,现在要发下一个序号为601的TCP包了 首部长度 长度为4位 指出TCP首部的大小,因为TCB首部中有可选择字段,所以每个TCB首部都是不定长的,所以需要指定首部长度,单位是4字节 ,最大为2^4 ,所以首部最大长度为16*4byte=60byte 保留位 6位,保留以后使用,目前全部置0 五大标志位 6位 标志 含义 URG 紧急位,为1的话则表示这是个紧急的TCP包,应该放到发送队列的最前面去立即发送 ACK ACK=1表示确认号有效,ACK=0表示确认号无效,TCP连接建立成功后所有的传输报文必须把ACK置为1 PSH 很少用到,一般为0 RST 复位 RST=1 表示TCP连接出错,必须释放连接重新建立 SYN 同步 SYN=1表示这是一个请求建立连接或接受建立连接请求报文 SYN=1 ACK=0表示这是一个建立连接请求,SYN=1 ACK=1表示这是一个应答建立连接请求的TCP报文,详情请参考TCP三次握手 FIN 用来释放连接, FIN=1 表示数据已经发送完毕并且请求释放TCP连接 窗口大小 TCP使用 滑动窗口 来实现拥塞控制和流量控制,详情请见下文,比如B响应给A的报文中窗口字段的大小为1000 ,确认号为101 目的就是为了告诉A可以发送101开始的数据包了,缓存空间大小为1000byte,也就是还可以接受101~1101字节范围的数据包 发送双方都会建立一个窗口来提醒对方自己能接受的最大数据量,展示自己的处理能力

Go语法细节基础

byte和rune byte是uint8的内置别名,代表一个字节,我们可以将byte和uint8看作是同一个类型 rune是int32的内置别名,代表一个字符, 我们可以将rune和int32看作是同一个类型,代表一个Unicode字符,Unicode字符目前还没超过32位 var b1 rune ='你' // var b byte = '你' 报错 var b2 rune = 20320 //你的Unicode值 var b3 byte = 'a' uintptr、int、uint uintptr代表一个地址,所以必须要能寻找到计算机所能表示的最大地址,随意其大小随计算机的位数而确定,如果是64位则该类型的大小也是64位的 同理int、uint等也是相似的,随本机类型而定,64位机器则是int64 type自定义类型 type MyInt int64 //此类型是一个新的自定义类型 和int64类型不一样 type Int64=int64 //此类型和int64类型是一样的 类型0值 类型 默认值 bool false int、float64等数值类型 0 string "" 整数类型的表示 d1:=0x10F //16进制 d2:=0b111 //二进制 d3:=10_000_000 //可以用下划线分割方便观看 10000000 浮点数的表示 //[float64] d1:= .32 //0.32 d2:= 2. //2.0 d3:=2.3e3 //2300 d4:=2.3e-3 //0.0023 字符串的``反引号 s:=`1111 2222 3333` //反引号不会进行转义 常量const和itoa const定义的一组变量可以设置相同的值

基金

基金分类 (分险依次降低) 股票型 混合型 债权型 货币型 A类基金:买入有手续费 长期投资 卖出需要手续费 C类基金:买入没有手续费 短期投资 卖出需要手续费 专业术语 https://zhuanlan.zhihu.com/p/348003436 ​ 基金交易时间 交易时间 净值确认日 交易确认日 盈亏查看日 周一15点前 周一 周二 周三 周一15点后—周二15点前 周二 周三 周四 周二15点后—周三15点前 周三 周四 周五 周三15点后—周四15点前 周四 周五 下周一 周四15点后—周五15点前 周五 下周一 下周二 周五15点、周六、周日 下周一 下周二 下周三 工作日时间: 9:30~11:30 13:00~15:00(建议交易在后面的时间段,这样最接近当然的价格) 周日晚上~周四15:00之前申购基金的最好时间 每次买卖一定要在当日下午15: 00前操作(最好是在14:00左右这样最接近当然的价格),超过15:00就不要操作了,因为超过15:00就是次日的价格了 ​ 参考 http://fund.eastmoney.com/a/20180918947972298.html

MySql约束

约束分类 约束就是给字段设置一定的规则,约束这个字段 主要有如下几类约束 主键约束 外键约束 非空约束 唯一约束 默认值约束 检查约束 ​ 主键约束 主键约束是非空的,最好使用 AUTO_INCREMENT来自动增加防止重复,并且能用int就不要用 bigint ,因为这和索引有关系,主键数据越小越好 如果一张表没有设置主键,那么Mysql就会拿一个非空约束字段当做主键,如果都没有则会自己生成一个RowID的列作主键 CREATE TABLE tb( id int PRIMARY KEY AUTO_INCREMENT ) CREATE TABLE tb( id int AUTO_INCREMENT, name varchar(20), PRIMARY KEY pk(id) ) #添加主键约束 ALTER TABLE 表名 ADD CONSTRAINT 约束名 PRIMARY KEY(列名); #删除主键约束 ALTER TABLE 表名 DROP PRIMARY KEY 约束名; ​ 外键约束 外键是指引用另一个表中的一列或多列,被引用的列应该具有主键约束或唯一约束 外键约束可以设置联级: 联级删除: 外键表中被引用的记录删除之后则连同所有拥有这个外键的记录都删除 联级更新: 外键表中被引用的记录更新之后则连同所有拥有这个外键的记录都更新 如果不设置则不允许删除还被引用的记录 CREATE TABLE IF NOT EXISTS category ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(50) NOT NULL, UNIQUE uq_title (title) ); CREATE TABLE article ( id INT AUTO_INCREMENT, title VARCHAR(100) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, deleted_at DATETIME DEFAULT NULL, author VARCHAR(20) NOT NULL, status ENUM ('published','auditing','draft','deleted') NOT NULL, cid INT NOT NULL, PRIMARY KEY pk_id (id), #联级删除 category记录删除了 所有引用该记录的article记录也一并删除 #如果没设置此联级策略则不允许删除还被引用的category记录 FOREIGN KEY fk_cid (cid) REFERENCES category (id) ON DELETE CASCADE, UNIQUE uq_title (title) COMMENT '会自动创建唯一索引' ); #添加外键 ALTER TABLE 表名 ADD CONSTRAINT 约束名 FOREIGN KEY (外键字段名) REFERENCES 外键表名(列名); #删除外键 ALTER TABLE 表名 DROP FOREIGN KEY 外键名; ​

快速排序

快排思想 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列 就是需要确定一个 pivot支点,然后将小于pivot的放到左边,大于pivot的放到右边,此时pivot就已经排好序了,然后再分别对两别进行递归排序 所以快排主要需要解决下面两个问题: pivot的选择,最好的pivot就是选择当前数组中最中间大小的数 如何把元素放到左边和右边,也就是如何确定pivot放置的位置 ​ 复杂度分析 最优时间复杂度: O(nlogn) 最优空间复杂度: O(logn) pivot每次都平分,计算时间复杂度过程和 归并排序一样计算 空间复杂度主要是递归栈的空间,看递归树的高度,比如50,10,90,30, 70,40,80,60,20 这个序列,递归深度如下,这是一颗平衡二叉树,高度是数组个数的 logn倍 最差时间复杂度: O(n^2) 最差空间复杂度: O(n) pivot每次取最大最小值,退化为 冒泡排序 冒泡排序的时间复杂度: T[n] = n * (n-1) = n^2 + n = O(n^2) 空间复杂度就是树的高度,单边树的高度就是元素的个数,所以空间复杂度为O(n) ​ pivot支点的选择 支点只要能将两边平均分就是最好的支点,主要有如下几个选法: 选第一个/最后一个 选中间一个 随机选一个 三数取中法: 选 开头、结尾、中间 的数中大小排中间的数 为了便于算法实现,需要取中间某个Pivot时,可以通过交换元素,转换成取第一个(或最后一个)来方便实现 //随机 pivot := rand.Intn(end+1-start) + start //三数取中 func getMiddle(arr []int, start, end int) int { mid := start + (end-start)/2 if arr[start] > arr[mid] { mid, start = start, mid } if arr[mid] > arr[end] { mid, end = end, mid } return mid } //取数组位置中间的数 pivot:=len(arr)/2 ​

十大排序算法

O(n^2)排序算法 适合小规模数据排序 冒泡排序 稳定排序 最好时间复杂度: O(n) 基本有序状态,进行1次冒泡 最坏时间复杂度: O(n^2) 逆序状态,需要进行n次冒泡 平均时间复杂度: O(n^2) 空间复杂度: O(1) func BubbleSort(arr []int) { flag := true for i := 0; i < len(arr); i++ { for j := 0; j < len(arr)-i-1; j++ { if arr[j] > arr[j+1] { flag = false arr[j], arr[j+1] = arr[j+1], arr[j] } } if flag { break } } } ​ 插入排序 稳定排序 只有元素相同不会进行交换 最好时间复杂度: O(n) 基本有序状态 最坏时间复杂度: O(n^2) 逆序状态 平均时间复杂度: O(n^2) 空间复杂度: O(1) 将数组分成两个区间: 有序区间、无序区间 然后寻找插入位置,算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束