米哈游 面试下来感觉还行吧,挺注重基础的,面试官水平也很高。但是感觉不是在招人的样子,我有好多同学都是简历挂(
无笔试,很奇怪(
进程 | 线程 |
---|---|
系统中正在运行的一个应用程序 | 系统分配处理器时间资源的基本单元 |
程序一旦运行就是进程 | 进程之内独立执行的一个单元执行流 |
资源分配的最小单位 | 程序执行的最小单位 |
进程有独立的地址空间
,一个进程崩溃后,在保护模式下不会对其它进程产生影响
,而线程只是一个进程中的不同执行路径。
线程有自己的堆栈和局部变量,但线程没有单独的地址空间
,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换
时,耗费资源较大,效率要差一些。但对于一些 要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程
管道、消息队列、共享内存、信号量、信号、socket
信号:
在Linux中,为了响应各种事件,提供了几十种信号,可以通过kill -l
命令查看。
如果是运行在 shell终端 的进程,可以通过键盘组合键来给进程发送信号,例如使用Ctrl+C 产生SIGINT 信号
,表示终止进程。
如果是运行在后台的进程,可以通过命令来给进程发送信号,例如使用kill -9 PID
产生SIGKILL
信号,表示立即结束进程。
信号量:
当使用共享内存的通信方式,如果有多个进程同时往共享内存写入数据,有可能先写的进程的内容被其他进程覆盖了。
因此需要一种保护机制,信号量本质上是一个整型的计数器,用于实现进程间的互斥和同步
。
信号量代表着资源的数量,操作信号量的方式有两种:
G:表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是可以重用
的。
P:表示逻辑processor,P 的数量决定了系统内最大可并行的 G 的数量(前提:系统的物理cpu核数 >= P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。
M:M 代表着真正的执行计算资源,物理 Processor。
G 如果想运行起来必须依赖 P,因为 P 是它的
逻辑处理单元
,但是 P 要想真正的运行,他也需要与 M 绑定,这样才能真正的运行起来,P 和 M 的这种关系就相当于 Linux 系统中的用户层面的线程和内核的线程是一样的
。
go的map不保证有序性,所以按key排序需要取出key,对key排序,再遍历输出value
map 的底层是一个结构体
// Go map 的底层结构体表示
type hmap struct {
count int // map中键值对的个数,使用len()可以获取
flags uint8
B uint8 // 哈希桶的数量的log2,比如有8个桶,那么B=3
noverflow uint16 // 溢出桶的数量
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // 指向哈希桶数组的指针,数量为 2^B
oldbuckets unsafe.Pointer // 扩容时指向旧桶的指针,当扩容时不为nil
nevacuate uintptr
extra *mapextra // 可选字段
}
const (
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits // 桶数量 1 << 3 = 8
)
// Go map 的一个哈希桶,一个桶最多存放8个键值对
type bmap struct {
// tophash存放了哈希值的最高字节
tophash [bucketCnt]uint8
// 在这里有几个其它的字段没有显示出来,因为k-v的数量类型是不确定的,编译的时候才会确定
// keys: 是一个数组,大小为bucketCnt=8,存放Key
// elems: 是一个数组,大小为bucketCnt=8,存放Value
// 你可能会想到为什么不用空接口,空接口可以保存任意类型。但是空接口底层也是个结构体,中间隔了一层。因此在这里没有使用空接口。
// 注意:之所以将所有key存放在一个数组,将value存放在一个数组,而不是键值对的形式,是为了消除例如map[int64]所需的填充整数8(内存对齐)
// overflow: 是一个指针,指向溢出桶,当该桶不够用时,就会使用溢出桶
}
当向 map 中存储一个 kv 时,通过k 的 hash 值与 buckets 长度取余
,定位到 key 在哪一个bucket中,hash 值的高8位存储在 bucket 的 tophash[i] 中,用来快速判断 key是否存在。当一个 bucket 满时,通过 overflow 指针链接到下一个 bucket。
在源码runtime.mapiterinit
中
func mapiterinit(t *maptype, h *hmap, it *hiter) {
...
it.t = t
it.h = h
it.B = h.B
it.buckets = h.buckets
if t.bucket.kind&kindNoPointers != 0 {
h.createOverflow()
it.overflow = h.extra.overflow
it.oldoverflow = h.extra.oldoverflow
}
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
it.bucket = it.startBucket
...
mapiternext(it)
}
通过对mapiterinit 方法
阅读,可得知其主要用途是在 map 进行遍历迭代时进行初始化动作。共有三个形参,用于读取当前哈希表的类型信息、当前哈希表的存储信息和当前遍历迭代的数据
源码中 fastrand
...
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
// iterator state
it.bucket = it.startBucket
在这段代码中,它生成了随机数。用于决定从哪里开始循环迭代。更具体的话就是根据随机数,选择一个桶位置作为起始点进行遍历迭代。
因此每次重新for range map
,你见到的结果都是不一样的。那是因为它的起始位置根本就不固定!
因为通信的前提是确保双方都是接收和发送信息是正常
的。
三次握手是为了建立可靠的数据传输通道,
第一次握手就是让 接收方 知道 发送方 有发送信息的能力
第二次握手就是让 发送方 知道 接收方 有发送和接受信息的能力
第三次握手就是让 接收方 知道 发送方 有接受信息的能力
四次挥手则是为了保证等数据完成的被接收完再关闭连接。
既然提到需要保证数据完整的传输完,那就需要保证双方 都达到关闭连接的条件才能断开。
第一次挥手:客户端发起关闭连接
的请求给服务端;
第二次挥手:服务端收到关闭请求的时候可能这个时候数据还没发送完,所以服务端会先回复一个确认报文
,表示自己知道客户端想要关闭连接了
,但是因为数据还没传输完,所以还需要等待;
第三次挥手:当数据传输完了,服务端会主动发送一个FIN 报文
,告诉客户端,表示数据已经发送完了
,服务端这边准备关闭连接了
。
第四次挥手:当客户端收到服务端的 FIN 报文过后,会回复一个ACK 报文
,告诉服务端自己知道了,再等待一会就关闭连接。
粘包就是两个或者多个以上的包粘在一起,拆包就是为什么解决粘包问题。
出现粘包原因有两个,一个是发送方发送不及时,导致多个包在发送端粘黏。另一个是接收方接受不及时,导致包在接收端堆叠导致。
设计一个带包头的应用层报文结构就能解决
。包头定长,以特定标志开头,里带着负载长度,这样接收侧只要以定长尝试读取包头,再按照包头里的负载长度读取负载就行了,多出来的数据都留在缓冲区里即可。其实ip报文和tcp报文都是这么干的。
事务具有原子性、一致性、隔离性、持久性
特性,并且 指将一系列数据操作捆绑成为一个整体进行统一管理,如果某一事务执行成功,则在该事物中进行的所有数据更改均会提交,成为数据库中的永久组成部分。
redis 是不支持事务的,因为 他不支持原子性,但是支持一致性,是最终一致性。
我以为过了简历,免了笔试,一面算法写出来,八股也能吹了,应该有二面,但还是挂了(
真的是 海量 hc 吗。。。