8000 00LT00.github.io/atom.xml at master · 00LT00/00LT00.github.io · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
3649 lines (3543 loc) · 153 KB

atom.xml

File metadata and controls

3649 lines (3543 loc) · 153 KB
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>http://www.zerokirin.online</id>
<title>个人学习</title>
<updated>2022-03-27T09:01:38.592Z</updated>
<generator>https://github.com/jpmonette/feed</generator>
<link rel="alternate" href="http://www.zerokirin.online"/>
<link rel="self" href="http://www.zerokirin.online/atom.xml"/>
<subtitle>记录</subtitle>
<logo>http://www.zerokirin.online/images/avatar.png</logo>
<icon>http://www.zerokirin.online/favicon.ico</icon>
<rights>All rights reserved 2022, 个人学习</rights>
<entry>
<title type="html"><![CDATA[字节二面面经]]></title>
<id>http://www.zerokirin.online/post/zi-jie-er-mian-mian-jing/</id>
<link href="http://www.zerokirin.online/post/zi-jie-er-mian-mian-jing/">
</link>
<updated>2021-08-31T09:15:00.000Z</updated>
<content type="html"><![CDATA[<h2 id="问题">问题</h2>
<ul>
<li>dns工作方式</li>
<li>dns多个ip地址怎么选择</li>
<li>http长连接怎么控制</li>
<li>长连接怎么关闭呢</li>
<li>https是什么</li>
<li>rsa握手流程</li>
<li>给证书签名的作用是什么</li>
<li>tcp传输的可靠性是靠什么保证的</li>
<li>什么是聚簇索引呢</li>
<li>myisam是否是聚簇索引,他的索引保存的是什么</li>
<li>索引是通过什么数据结构保存的</li>
<li>B+树和B树的优缺点</li>
<li>有用过事务吗,是什么场景下使用呢</li>
<li>事务有几个特性</li>
<li>caid是什么</li>
<li>原子性是怎么保证的</li>
<li>undo log 里面有哪些东西</li>
<li>持久性是怎么保证的</li>
<li>隔离性是怎么保证的</li>
<li>mvcc是怎么实现的</li>
<li>lru怎么实现</li>
</ul>
<h2 id="浅析">浅析</h2>
<h3 id="计网">计网</h3>
<h4 id="dns">dns</h4>
<ol>
<li>
<p>dns工作方式</p>
<ul>
<li>dns可以理解为一个转换域名到ip地址的服务器,默认端口是53,默认使用udp协议</li>
<li>主要的工作方式有两种,迭代和递归</li>
<li>迭代就是服务器去一个一个问其他上级dns服务器,直到根域名或者有缓存过的记录</li>
<li>递归是服务器只管向上级问一次,上级dns再向上级问,对每个服务器来说,只有一次请求,一次返回</li>
<li>目前常采用迭代方式,也就是向dns服务器发送解析请求,服务器自己挨个向上级查询,然后返回结果</li>
</ul>
</li>
<li>
<p>dns多个ip地址怎么选择</p>
<ul>
<li>随机,我也不知道这有什么好问的,就是随机啊。。。</li>
</ul>
</li>
</ol>
<h4 id="http长连接">http长连接</h4>
<p>http/1.0引入了长连接的方式,也就是在一个tcp连接中尽可能地多传输数据,减少tcp握手挥手的性能损耗</p>
<ul>
<li>
<p>http怎么控制长连接</p>
<pre><code>Connection: keep-alive
Connection: close
</code></pre>
<p>通过在请求头中增加字段,http/1.0 默认是<code>close</code>,到http/1.1时,默认为<code>keep-alive</code></p>
<ul>
<li>当设置为keep-alive时,tcp连接不会主动断开,只有超过了服务器设置的超时时间才会主动停止</li>
<li>close时就断开tcp连接</li>
</ul>
</li>
</ul>
<h4 id="https是什么">https是什么</h4>
<p>在http的基础上,增加了tls作为加密协议,将原来的明文通信变成密文通信</p>
<p>最早是ssl,套接字安全层,ssl3.0完善后成为为tls1.0,传输层安全协议,主要目的就是建立双方的加密通信</p>
<p>目前主流是1.2版本,1.3去年才刚刚上线,还没有普及</p>
<ul>
<li>
<p>https握手流程怎么样的</p>
<ul>
<li>1.2中的握手主要有两类,rsa和dh,但rsa在1.3版本中被废弃,dh的实际实现也换成了安全性更高的ecdh(用椭圆曲线的计算取缔了素数分解问题,效率更高,安全性更高)</li>
<li>握手流程可以看那篇讲https的文章,不赘述了</li>
</ul>
</li>
<li>
<p>数字签名的作用</p>
<ul>
<li>保证信息完整性,验证信息来源是否真实</li>
<li>对摘要做一次私钥加密,这样只有公钥才可以解开,再验证一次摘要是否正确,可以保证未被修改</li>
<li>CA证书的验证也是基于数字签名,每台电脑会内置受信任的CA机构的公钥,对其解密再验证摘要就可以保证CA证书没有被篡改</li>
</ul>
</li>
</ul>
<h4 id="tcp">TCP</h4>
<ul>
<li>tcp传输的可靠性靠什么保证
<ul>
<li>确认和重传机制
<ul>
<li>建立连接时约定好的起始序列号seq</li>
<li>确认号ack</li>
<li>校验和checksum</li>
<li>丢包时重传</li>
</ul>
</li>
<li>数据排序
<ul>
<li>依据滑动窗口接受机制可以确定最后一个正确到达的数据包,定位错误数据</li>
</ul>
</li>
<li>流量控制
<ul>
<li>双方通过约定滑动窗口的大小,控制发送速率</li>
</ul>
</li>
<li>拥塞控制
<ul>
<li>慢启动</li>
<li>拥塞避免</li>
<li>快速重传</li>
<li>快速恢复</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="mysql">mysql</h4>
<ul>
<li>
<p>什么是聚簇索引呢</p>
<p>数据和索引放在一起,索引记录的就是数据本身</p>
</li>
<li>
<p>myisam是否是聚簇索引,他的索引保存的是什么</p>
<p>不是,它里面存放的是数据的文件地址</p>
</li>
<li>
<p>索引是通过什么数据结构保存的</p>
<p>innoDB和myisam都是B+Tree,也支持hash</p>
</li>
<li>
<p>B+树和B树的优缺点</p>
<ul>
<li>B+树的数据都在叶子节点,因此可以将索引放到一起,减少磁盘io更快的定位数据,实现范围查找更加容易</li>
<li>B树在索引到数据时可以快速返回,无需到达叶子节点,对于单一查询来说性能更好</li>
</ul>
</li>
<li>
<p>事务有几个特性</p>
<p>四个,acid</p>
</li>
<li>
<p>caid是什么</p>
<p>原子性和隔离性保证了一致性,最后可以持久化到磁盘中</p>
<ul>
<li>
<p>原子性</p>
<p>要么全部成功,要么全部失败</p>
</li>
<li>
<p>隔离性</p>
<p>A事务的操作对B不可见,B事务的操作也不应该影响到A的正确性</p>
</li>
<li>
<p>一致性</p>
<p>完整性保持一致(p话)</p>
<p>其实就是说,没有并发错误,所有的事情都是按照预想的来的,没有破坏任何对完整性的保护</p>
</li>
<li>
<p>持久性</p>
<p>事务正确结束后,所做的更改就应该永久留在数据库中</p>
</li>
</ul>
</li>
<li>
<p>原子性是怎么保证的</p>
<p>undo log</p>
</li>
<li>
<p>undo log 里面有哪些东西</p>
<p>事务操作的反向操作,比如:</p>
<pre><code>insert a
insert b
insert c
undo log中应该是:
delete c
delete b
delete a
</code></pre>
</li>
<li>
<p>持久性是怎么保证的</p>
<p>redo log,每次先写日志,再写磁盘,这样就算断电,也能保证以重做日志为准进行恢复</p>
<ul>
<li>
<p>为什么是redo log而不是直接写到数据中</p>
<p>因为磁盘的io,不管你读多少数据,都要从磁盘中找到对应的页块,一般来说是16kb,然后将其整体读入,那每次修改都直接修改磁盘,就会导致整个数据库对于磁盘的io非常依赖,而众所周知,磁盘的读写速度慢得离谱,所以为了保证性能,将所有记录优先记录到日志中,因为日志是连续读写的,没有随机读写所带来的io问题(其实也有,但已经很少了)</p>
</li>
</ul>
</li>
<li>
<p>隔离性是怎么保证的</p>
<p>在mysql中,隔离分为四个级别</p>
<ul>
<li>读未提交</li>
<li>读已提交</li>
<li>可重复读</li>
<li>串行化</li>
</ul>
<p>真正与隔离性有关的主要是读已提交和可重复读,这里要引入个概念叫做封锁协议,它描述的是加锁时机,实际上可以把它当作一个思想来看</p>
<p>三级封锁协议:</p>
<ol>
<li>任何写操作需要加X锁,直到事务结束</li>
<li>在一级封锁协议的基础上,读取数据时必须加S锁,<strong>读取完马上</strong>释放S锁</li>
<li>在一级封锁协议的基础上,读取数据时必须加S锁,<strong>直到事务结束</strong>才释放</li>
</ol>
<p>笔者认为可以把2,3级分别看作是读已提交和可重复读的实现思想,而锁和mvcc分别对应着悲观锁和乐观锁的具体实现</p>
<p>当使用for update 等查询语句时,我们称此次查询为<strong>当前</strong>读,读已提交和可重复读正好对应着封锁协议中的二三级,通过添加对应的行锁(可重复读下有gap和next-key锁)来实现隔离</p>
<p>若是直接使用select查询,则会在该事务的第一次查询时,创建一个readview,也因此其被称之为<strong>快照读</strong>,同时每个数据行里都额外的保存了三个字段:最后一个对其改动的事务id,行的id(也就是主键,或者是union id),回滚指针,分别为trx,id,roll</p>
<p>读已提交的readview是每次查询都创建一个新的readview,可重复读是在事务开始的第一次查询创建,分别对应着二三级封锁协议的思想,每次查询时根据readview和隐藏字段的对比,选择出可以展示的数据行</p>
</li>
<li>
<p>mvcc是怎么实现的</p>
<p>readview中,主要包括了<strong>当前活跃</strong>的所有事务的事务组trxs,以及trxs的最小事务id,最大事务id,创建readview的事务id,我们分别称之为min,max,cur</p>
<p>数据行的隐藏字段主要是对其更改的事务id,我们称为trx,还有个回滚指针,他们的关系如下图</p>
<figure data-type="image" tabindex="1"><img src="https://i.loli.net/2021/09/21/pyok36AFfZcONtu.png" alt="image-20210921002134858" loading="lazy"></figure>
<p>因为min,max是以创建readview时活跃的事务的最大最小来划分的,所以</p>
<ul>
<li>绿色的区域代表着创建时,一定是提交了的事务,所以他的提交一定是可以看到的</li>
<li>红色代表着,创建时还没开始的事务,那么它的任何改动都无法看到</li>
<li>蓝色区域则表示存在活跃的事务,同时也存在后来才开始的但已提交了的事务,因此蓝色部分的判断为,该trx是否存在于活跃事务组中
<ul>
<li>若trx存在于trxs中,则创建时还未提交,因此不能展示</li>
<li>若不存在,则证明已经提交,所以可以展示</li>
<li>若trx正好就是cur,那自己对自己做的修改肯定可以看到</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="设计思路">设计思路</h4>
<ul>
<li>
<p>lru是怎么实现的</p>
<p>首先,lru的意思是,Least Recently Used,最近最少使用,是一种缓存淘汰机制,由于是缓存淘汰机制,因此对性能有很高的要求,要求O(1)的读取速度,O(1)的删除速度,那么符合条件的数据结构就很清楚了</p>
<p>读取方面使用hashmap,而lru中,每次将读取的数据推后,保证最近最少使用的那个处于队头,这就涉及到空间的转移,那毫无疑问就是链表了</p>
<p>因此hashmap中存储对应元素项的指针,链表可以使用双向链表,每次使用的时候将该数据存放到队头或队尾,与此同时若长度超过限制则删除队尾或队头的数据</p>
</li>
</ul>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[字节一面面经]]></title>
<id>http://www.zerokirin.online/post/zi-jie-yi-mian-mian-jing/</id>
<link href="http://www.zerokirin.online/post/zi-jie-yi-mian-mian-jing/">
</link>
<updated>2021-08-21T12:05:14.000Z</updated>
<content type="html"><![CDATA[<h2 id="问题">问题</h2>
<ul>
<li>进程线程区别</li>
<li>进程间通信</li>
<li>线程间通信</li>
<li>go的channel是怎么实现的</li>
<li>go的调度模型</li>
<li>p的大小通常设置为多少,如果是io密集型应用呢</li>
<li>go的map实现方式</li>
<li>map退化成链表了怎么办</li>
<li>tcp的time wait状态</li>
<li>如果有太多的time wait状态该怎么办</li>
<li>mysql索引有哪几种</li>
</ul>
<h2 id="浅析">浅析</h2>
<ol>
<li>
<p>OS</p>
<ul>
<li>进程线程区别?
<ul>
<li>线程依赖于进程,比进程更轻量</li>
<li>进程是资源分配的基本单位</li>
<li>线程是调度的基本单位</li>
<li>线程可以共享同一个进程的资源</li>
<li>进程必须依靠IPC来通信</li>
</ul>
</li>
<li>进程间通信方式
<ul>
<li>信号</li>
<li>信号量</li>
<li>有名管道</li>
<li>匿名管道</li>
<li>共享内存</li>
<li>消息队列</li>
<li>套接字</li>
</ul>
</li>
<li>线程间通信
<ul>
<li>线程间通信的主要目的是用于多线程同步</li>
<li>互斥锁,条件变量(casp操作),读写锁,自旋锁</li>
<li>信号</li>
<li>信号量</li>
</ul>
</li>
</ul>
</li>
<li>
<p>go</p>
<ul>
<li>channel怎么实现的
<ul>
<li>核心是由发送队列,接收队列,和缓冲区组成</li>
<li>目前是用锁机制保护了先进先出的特性</li>
<li>发送的时候如果接收队列中有等待的goroutine则直接复制过去</li>
<li>接收的时候为了保证先进先出,会先从缓冲区复制到当前接收的goroutine,然后再把等待发送的goroutine唤醒,把内容复制到缓冲区,然后把阻塞的发送goroutine放回p队列</li>
<li>发送的时候还有一个保活操作,因为在阻塞的时候,该数据地址有可能已经没有引用了,gc扫描后会为白色,直接清理掉,因此在唤醒操作的后满做一次无意义的引用,可以使得该地址免于gc扫描</li>
</ul>
</li>
<li>go的调度模型
<ul>
<li>其实就是gmp模型,车轱辘话来回说,细节详见后续博文</li>
<li>P的大小设置为多少,io密集型应用呢?
<ul>
<li>默认设置为操作系统线程数量,例如我的4800h就是8核16线程,所以会初始化16个P队列</li>
<li>在调度过程中如果发生系统调用,P队列会主动与M解绑,由M去等待操作系统的io结束后,再自行寻找P队列进行操作</li>
<li>但是P和M的解绑是有延迟的,只有每次调度发生的时候才会彻底解绑,因此在io密集型应用中,会有大量的P处于等待状态,等待M的解绑,等待获取新的M</li>
<li>所以在io密集型应用中,需要将P设置的更大,更多,这样即使有很多很多P都在等待,依然有足够的P和M绑定,能够运行G,压榨cpu</li>
</ul>
</li>
</ul>
</li>
<li>map退化成链表后?
<ul>
<li>go里通过多个hash函数,每次初始化的时候有个随机的种子,保证最大限度地随机,如果还是这么头铁,那go里面没有任何优化手段,实质上就是在一个bucket列表中挨个查找了</li>
</ul>
</li>
</ul>
</li>
<li>
<p>tcp time-wait状态</p>
<ul>
<li>time-wait太多会怎样?
<ul>
<li>占用太多端口</li>
<li>占用大量无意义的资源,即每次传输的数据远小于一次连接的建立断开所花费的经历</li>
</ul>
</li>
<li>time-wait状态太多,怎么办?
<ul>
<li>tcp连接复用,人为在应用层将数据汇总发送,尽量让一个tcp连接承担更多的短数据传输&l 10BC8 t;/li>
<li>使用linux内核参数<code>tcp_tw_reuse</code>,这个参数能够在创建tcp连接的时候随机选一个处于time-wait状态的连接进行复用,此时,之前如果有延迟到来的数据,则会被抛弃,因为相当于开启了一个新的连接</li>
</ul>
</li>
</ul>
</li>
<li>
<p>mysql</p>
<ul>
<li>索引有哪几种
<ul>
<li>主键索引</li>
<li>唯一索引</li>
<li>联合索引</li>
<li>普通索引</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2 id="算法">算法</h2>
<ol>
<li>如何判断二叉树是否对称?
<ul>
<li>层次遍历同时遍历左右节点就行</li>
</ul>
</li>
</ol>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[端点面经]]></title>
<id>http://www.zerokirin.online/post/duan-dian-mian-jing/</id>
<link href="http://www.zerokirin.online/post/duan-dian-mian-jing/">
</link>
<updated>2021-07-19T10:51:11.000Z</updated>
<content type="html"><![CDATA[<h2 id="问题">问题</h2>
<ul>
<li>有缓冲的channel 读取的时候是否会阻塞</li>
<li>如果没有数据还想读取应该怎么办</li>
<li>如果没有数据select去读会不会阻塞</li>
<li>函数中对一个slice做append,在没有触发扩容的情况下,函数外部是否会改变</li>
<li>协程什么时候会切出去</li>
<li>协程是怎么主动放弃执行权的</li>
<li>http中path在请求头还是请求体</li>
<li>http 怎么指定json呢</li>
<li>http 怎么指定body编码格式</li>
<li>https 握手流程</li>
<li>什么是restful api</li>
<li>客户端怎么保证证书是服务端发来的</li>
<li>内连接左连接和右连接有什么区别</li>
<li>什么情况下需要用事务</li>
<li>事务的隔离级别有哪些</li>
<li>系统调用的过程是怎样的</li>
<li>怎么样从用户态切换到内核态</li>
<li>进程间的内存隔离</li>
<li>进程间通信方式</li>
<li>有名管道和匿名管道有什么区别呢</li>
</ul>
<h2 id="浅析">浅析</h2>
<ol>
<li>
<p>channel</p>
<ul>
<li>有缓冲的channel在读取的时候如果没有数据依然是会阻塞的</li>
<li>读取channel的函数原型中有个形参叫做block,只有在select语句中,这个block的值是false,它控制了是否会进入自旋或是将读取的goroutine添加到接收队列中,如果是false的话,无法读取数据就直接返回了</li>
<li>函数的返回值有两个,selected和received
<ul>
<li>received控制着是否有数据返回,也就是具体有没有读取到数据</li>
<li>selected只在select中用来判断是否执行该case</li>
</ul>
</li>
</ul>
</li>
<li>
<p>slice</p>
<p>slice的结构已经是老生常谈的话题了,它包括指向了具体是数据地址的<code>unsafe.Pointer</code>,len和cap</p>
<p>常问的一般是函数外调用及扩容问题,这次的问题是在函数内进行了一次没有扩容的append操作会怎样,首先看看底层实现</p>
<figure data-type="image" tabindex="1"><img src="http://img.zerokirin.online/blog/4txn95.png" alt="slice" loading="lazy"></figure>
<ul>
<li>
<p>如果是直接传值调用的话,相当于对s做了一次拷贝,结果如图</p>
<figure data-type="image" tabindex="2"><img src="http://img.zerokirin.online/blog/02ejm3.png" alt="slice2" loading="lazy"></figure>
</li>
<li>
<p>在函数内部,直接修改s1的值的话,可以看到,其指向的地址和原变量s是相同的,因此外部也会跟着变化</p>
</li>
<li>
<p>但是执行append的时候稍有不同,在函数内部,对s1做append,势必会改变len和cap的值,而这两个值存在于新的变量s1中,在原变量s中是没有变化的,也就是说,哪怕数据已经写到了unsafe.Pointer指向的地址中,但是由于s无法看到len和cap的更改,因此他并不知道新的数据进来</p>
</li>
<li>
<p>要想在外部读取到数据,就需要用到unsafe.Pointer</p>
</li>
<li>
<pre><code class="language-go">len := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&amp;s)) +uintptr(8)))
//为什么是+8,因为unsafe.Pointer的最底层也是个int,而go中int的大小不固定,最小是32,随操作系统位数而定,因此不能看做int32的别名,在这里我们通过 fmt.Println(unsafe.Sizeof(s)) 的结果是24,可以推断出,每个int占8个字节,因此后移8位
*len = 5
</code></pre>
</li>
</ul>
</li>
<li>
<p>http</p>
<ul>
<li>
<p>path在哪个部分</p>
<figure data-type="image" tabindex="3"><img src="http://img.zerokirin.online/blog/jxkhp1.png" alt="url构成" loading="lazy"></figure>
<p>上面的url转换到http请求中就是如下格式</p>
<pre><code class="language-http">GET /path/to/myfile.html?key1=value1&amp;key2=value2 HTTP/1.1
Host: www.example.com
</code></pre>
</li>
<li>
<p>http协议版本,path和query在请求行中</p>
</li>
<li>
<p>域名(host)在请求头中(host是唯一必备的请求头)</p>
</li>
<li>
<p>锚点不进行http传输,仅仅为浏览器定位页面使用</p>
</li>
<li>
<p>通过Content-Type指定传输的数据格式,常见的有</p>
<ul>
<li>text/html 文本方式的html</li>
<li>text/plain 纯文本</li>
<li>text/xml 文本方式的xml</li>
<li>application/x-www-from-urlencoded 表单提交(普通表单,非上传)</li>
<li>application/json 数据以json形式编码</li>
<li>application/xml 数据以xml形式编码</li>
<li>multipart/form-data 表单上传图片、文件类型等附件时必须用该类型</li>
</ul>
</li>
<li>
<p>编码格式通过Content-Encoding和Content-Type 中的字段charset指定</p>
<ul>
<li>charset 设置的是文本具体用什么编码,例如utf-8,gbk</li>
<li>Content-Encoding 设置的是消息体通过什么编码方式压缩</li>
</ul>
</li>
<li>
<p>https握手流程,目前tls1.1刚刚被废除,tls1.3还没有普及,tls1.2还是主流,主要分为两种形式,rsa和dh</p>
<ul>
<li>
<p>rsa版本如下</p>
<ol>
<li>客户端初始化一个随机数client_random,向服务端发送client hello消息</li>
<li>服务端返回server hello消息,包括ca证书(ca证书里有服务端公钥)和随机数server_random,数字签名</li>
<li>客户端根据数字签名验证ca证书,同时生成新的随机数PreMaster,将PreMaster用服务端公钥加密,发送给服务端</li>
<li>现在双方都有client_random,server_random,PreMaster,通过这三个算出真正的主密钥</li>
<li>客户端算出来后给服务端发送 finished消息</li>
<li>服务段收到finished消息后验证是否有误,同时给客户端发送finished</li>
<li>双方都无误则成功握手</li>
</ol>
</li>
<li>
<p>dh版本,主要是前面交换key的步骤不同</p>
<ol>
<li>客户端初始化随机字符串client_random,向服务器发送client hello消息</li>
<li>服务端返回server hello,包括随机数server_random,再发送ca证书,server_pubkey</li>
<li>客户端验证证书及server_pubkey无误后向服务端发送client_pubkey</li>
<li>因为双方此时都有了下面这些,因此主密钥是可以通过dh算法算出来的(现在更多的用更安全的ECDHE算法)
<ul>
<li>server_pubkey和client_pubkey可以生成PreMaster</li>
<li>client_random和server_random</li>
</ul>
</li>
<li>所以在客户端发送pubkey的时候就可以把自己的finished一起发送出去</li>
<li>服务端收到后验证是否有误,同时发送finished给客户端</li>
<li>双方确认无误,成功握手</li>
</ol>
</li>
<li>
<p>客户端如何保证证书是服务端发来的</p>
<p>进行签名算法,上一步tls握手阶段的server hello消息会携带双方约定好的加密流程,例如</p>
<pre><code>Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
</code></pre>
<figure data-type="image" tabindex="4"><img src="http://img.zerokirin.online/blog/image-20210715010753948.png" alt="image-20210715010753948" loading="lazy"></figure>
<p>可以看到这里密钥交换用了ECDHE方法因此就是dh流程,而身份验证则是RSA算法,这是一种非对称加密方式,而私钥只有服务器才有,所以只要服务端对发送的证书等信息做一次摘要加密(hash)后,再对摘要用私钥加密,然后将密文和摘要同时发送给客户端,客户端收到后,自行计算一次摘要得到hash-1,再用公钥解密服务端发送来的密文得到hash-2,如果两个相同,说明服务器的确有和证书里说的公钥的对应私钥,那么这个证书的来源就是可靠的</p>
</li>
</ul>
</li>
<li>
<p>什么是restful api</p>
<blockquote>
<p>看Url就知道要什么<br>
看http method就知道干什么<br>
看http status code就知道结果如何</p>
<p>——<a href="https://www.zhihu.com/question/28557115/answer/41267240">怎样用通俗的语言解释REST,以及RESTful? - 知乎 (zhihu.com)</a></p>
</blockquote>
<p>这是我见过最简单,最清楚的描述,也是restful的核心思想,并不应该拘泥于patch,post等方法,重点是将api设计为人类高度可读的形式</p>
<p>url就是资源,method就是操作方式,code就是结果</p>
</li>
</ul>
</li>
<li>
<p>Mysql</p>
<ul>
<li>
<p>左连接和右连接有什么区别</p>
<ul>
<li>内连接是指,左右两张表中相同的数据组成的集合</li>
<li>左连接是内连接和左边全表的并集</li>
<li>右连接是内连接和右边全表的并集</li>
</ul>
</li>
<li>
<p>事务的隔离级别有哪些</p>
<ul>
<li>读未提交</li>
<li>读已提交</li>
<li>可重复读</li>
<li>可串行化</li>
</ul>
</li>
<li>
<p>什么时候使用事务</p>
<p>一次性对多个数据用多条sql语句修改时,在一个事务中,要么都执行,要么都不执行,这是原子性的体现,隔离性保证了多个事务之间互不相关,进而在原子性和隔离性的作用下,保证了数据库整体的一致性(这里的一致性指的是数据库的状态一直都是符合所有约束的)</p>
</li>
</ul>
</li>
<li>
<p>OS</p>
<ul>
<li>
<p>系统调用的过程是怎样的,以printf函数为例</p>
<ol>
<li>printf调用编写好的系统库函数(api)</li>
<li>api找到该系统调用的系统调用号,将它还有该调用所需的参数一同放入各个寄存器中</li>
<li>用int指令,像系统发送软中断,中断号是 0x80</li>
<li>cpu收到中断,切换到内核态,准备函数运行需要的栈空间,同时保存cpu上下文</li>
<li>根据系统调用号执行对应的函数</li>
<li>准备函数返回值,并从内核态中恢复cpu上下文</li>
<li>执行ret指令切回用户态</li>
</ol>
</li>
<li>
<p>怎样从用户态切换到内核态</p>
<ul>
<li>每个进程在运行的时候会在用户空间和内核空间分别分配栈,将<strong>用户空间的栈指针</strong>保存到<strong>内核空间的栈</strong>中,将<strong>cpu的栈指针寄存器</strong>的值改为<strong>内核空间的栈</strong></li>
<li>同时修改cpu的特权等级,将ring3(用户态)修改为ring0(内核态)</li>
</ul>
</li>
<li>
<p>进程间的内存是如何保证相互隔离的</p>
<p>虚拟内存技术,有如下优点:</p>
<ul>
<li>
<p>保证内存安全,防止用户空间访问内核空间</p>
</li>
<li>
<p>对进程透明,每个进程都有自己独立的内存空间,将进程的内存空间隔离,防止访问其他内存</p>
</li>
<li>
<p>可以灵活分配内存,例如,可以将一段连续的虚拟地址映射到分散的物理地址空间中</p>
</li>
<li>
<p>将部分内存中的数据存入磁盘,该磁盘被称为swap device,这样原内存就空出来了,可以存放更多的新数据,说白了就是强行扩充内存</p>
</li>
</ul>
</li>
<li>
<p>进程间通信方式</p>
<ul>
<li>管道(匿名管道) pipe</li>
<li>有名管道 fifo</li>
<li>信号 signal</li>
<li>消息队列 message</li>
<li>信号量 semaphore</li>
<li>共享内存 share memory</li>
<li>套接字 socket</li>
</ul>
</li>
<li>
<p>有名管道和匿名管道的区别(翻译)</p>
</li>
</ul>
<table>
<thead>
<tr>
<th></th>
<th><strong>PIPE</strong></th>
<th><strong>FIFO</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>描述</strong></td>
<td>无名的IPC对象</td>
<td>有名的IPC对象</td>
</tr>
<tr>
<td><strong>通信</strong></td>
<td>PIPE是系统本地的,不能用于网络的通信</td>
<td>FIFO可以在不同设备和网络间通信</td>
</tr>
<tr>
<td><strong>可见</strong></td>
<td>不存在于文件系统之中</td>
<td>在文件系统中存在</td>
</tr>
<tr>
<td><strong>Process</strong></td>
<td>在子进程和父进程之间通信</td>
<td>多个进程间,类似于服务器和客户端</td>
</tr>
<tr>
<td><strong>Creation</strong></td>
<td>PIPE is created by pipe () function.</td>
<td>FIFO is created by mkfifo () function.</td>
</tr>
<tr>
<td><strong>Read And Write Operation</strong></td>
<td>读写需要同时进行</td>
<td>无需同时进行</td>
</tr>
<tr>
<td><strong>Existence</strong></td>
<td>被关闭或者父子进程退出时,PIPE消亡</td>
<td>FIFO一直存在,即使进程退出,它仍会存在直到系统重启</td>
</tr>
<tr>
<td><strong>Control Over Ownership And Permissions</strong></td>
<td>无法控制所有权和权限</td>
<td>FIFO就像文件一样,你可以控制所有权和他的权限</td>
</tr>
<tr>
<td><strong>流通方向</strong></td>
<td>PIPE只能单项流通</td>
<td>FIFO是双向的,同一个FIFO可以被用作读写</td>
</tr>
<tr>
<td><strong>Mode Of Communication</strong></td>
<td>PIPE 提供简单数据流</td>
<td>FIFO 提供半双工数据流</td>
</tr>
<tr>
<td><strong>Communication</strong></td>
<td>具有共同祖先的进程的交流方式</td>
<td>共同祖先不是必要的</td>
</tr>
</tbody>
</table>
</li>
</ol>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Channel底层如何实现阻塞的]]></title>
<id>http://www.zerokirin.online/post/channel-di-ceng-ru-he-shi-xian-zu-sai-de/</id>
<link href="http://www.zerokirin.online/post/channel-di-ceng-ru-he-shi-xian-zu-sai-de/">
</link>
<updated>2021-06-29T06:24:06.000Z</updated>
<content type="html"><![CDATA[<blockquote>
<p>每日一问系列(一)</p>
</blockquote>
<p>我们都知道,channel在缓冲区满或者无缓冲的时候,发送数据会阻塞,缓冲区空或无缓冲的时候,接受也会阻塞</p>
<p>那么这个阻塞,是怎么实现的呢?</p>
<p>之前我们只是粗略的讲解了channel的实现的结构体,这次我们详细的看看发送和接受的源码</p>
<p>源码位于<code>go 1.15.4 src/runtime/chan.go</code></p>
<h2 id="读取的两种方式">读取的两种方式</h2>
<pre><code class="language-go">ch := make(chan int, 10)
a := &lt;-ch
b,ok := &lt;-ch
</code></pre>
<p>golang有两种读取channel 的方式,</p>
<pre><code class="language-go">// entry points for &lt;- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) { //看这里多了个返回值,received
_, received = chanrecv(c, elem, true)
return
}
</code></pre>
<p>这两个函数也就是读取的两个方法,第二个带有一个名为received的返回值</p>
<p>他们同时都调用了<code>chanrecv</code>函数,所以重点就在这个函数中</p>
<h2 id="读取函数">读取函数</h2>
<p>函数原型</p>
<pre><code class="language-go">// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
***重点在这,如果block是false,且无法获取到数据,则直接返回(false,false)
***这个的另一个意思就是,即便缓冲区buf为空,也不会阻塞,而是直接返回,那么这个的使用场景是?
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
</code></pre>
<p>上面调用chanrecv的函数里面都是写死了block为true,那么什么时候是false呢?</p>
<pre><code class="language-go">// compiler implements
//
// select {
// case v = &lt;-c:
// ... foo
// default:
// ... bar
// }
//
// as
//
// if selectnbrecv(&amp;v, c) {
// ... foo
// } else {
// ... bar
// }
//
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}
// compiler implements
//
// select {
// case v, ok = &lt;-c:
// ... foo
// default:
// ... bar
// }
//
// as
//
// if c != nil &amp;&amp; selectnbrecv2(&amp;v, &amp;ok, c) {
// ... foo
// } else {
// ... bar
// }
//
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
// TODO(khr): just return 2 values from this function, now that it is in Go.
selected, *received = chanrecv(c, elem, false)
return
}
</code></pre>
<p>select的用法就是,哪个可以取到数据就执行哪个case,而如果无法读取到则不会执行,所以肯定不能是阻塞的,所以两个用法的block的都是false</p>
<p>chanrecv的返回值名字也很直白,selected和received</p>
<p>selected只在执行select语句的时候判断case是否可以执行,至于数据是否获取到,则要看第二个参数received</p>
<p>那么我们详细来看chanrecv的内容吧,过程中我删掉了部分race检测的代码以及原有的英文注释</p>
<pre><code class="language-go">//原子操作检测是否能读取或者缓冲是否为空
func empty(c *hchan) bool {
// c.dataqsiz is immutable.
if c.dataqsiz == 0 { //无缓冲,检测发送队列是否是nil
return atomic.Loadp(unsafe.Pointer(&amp;c.sendq.first)) == nil
}
//有缓冲,检测当前数量有多少
return atomic.Loaduint(&amp;c.qcount) == 0
}
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
//如果是nil
if c == nil {
//在select语句中block是false,所以会在这直接返回(false,false),也就是说select永远不会进入一个对nil的chan操作的步骤
if !block {
return
}
//阻塞了,所以读取nil并不会直接报错
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw(&quot;unreachable&quot;)
}
// chan中是空的,没有办法直接获取值,且在select中,无需等待取值
if !block &amp;&amp; empty(c) {
//这里官方用了很长一段来描述为什么会这么写,大意是说
//在竞争检测关闭时,可能会有多个goroutine对channel进行操作
//比如说,一开始是,没有close的,非空的
//后来被关闭了,然后把数据排空了
//这时候由于检测语句的不合理安排以及并发访问
//很有可能会得出一个,没有close的,空了的结果
//所以对empty和closed都是原子操作去检测
if atomic.Load(&amp;c.closed) == 0 { //未关闭
//由于channel不能关了再开,所以如果检测到未关闭
//则证明在一开始就是未关闭的
//返回false,false
//select不会继续下去,因为他还有收到值的可能性
return
}
//执行到这一定是closed的了,再检测一次empty是因为
//有可能在上面判断closed之前,有了新的数据,那么可能会是closed和非空
if empty(c) {
//确定是空,若接受体不为空,也就是不用_抛弃,则直接置为0值
if ep != nil {
typedmemclr(c.elemtype, ep)
}
//selected,可以执行这条路,但是v,ok=&lt;-ch的ok是false
return true, false
}
}
//后文初始化sudog的条件(暂时看不懂干嘛的,下次研究)
var t0 int64
if blockprofilerate &gt; 0 {
t0 = cputicks()
}
//加锁
lock(&amp;c.lock)
//closed,且 缓冲buf 是空的
//无论有无缓冲区,都是这个流程
if c.closed != 0 &amp;&amp; c.qcount == 0 {
unlock(&amp;c.lock)
if ep != nil { //清空接收者
typedmemclr(c.elemtype, ep)
}
return true, false
}
//如果发送队列不为空的话执行,注意
//也就是要么无缓冲,要么缓冲buf满了,两种情况处理方法不一样,recv函数待会再讲
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&amp;c.lock) }, 3)
return true, true
}
//数据个数大于0,那肯定是有缓冲类型的
if c.qcount &gt; 0 {
//通过这个chanbuf,获取到数据地址,存到qp中
qp := chanbuf(c, c.recvx)
//把qp移动到ep中
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
//原来的数据置空
//还有一点就是,如果上一个ep是nil,也就是用_忽略了的话,这一步也会把数据清掉
typedmemclr(c.elemtype, qp)
//循环链表,所以下标正常增长
c.recvx++
//涨到最大值后变为0,循环嘛
if c.recvx == c.dataqsiz {
c.recvx = 0
}
//取数据了,所以肯定要减小
c.qcount--
unlock(&amp;c.lock)
return true, true
}
/* 前面我们已经排除了
* closed的,有发送队列的(缓冲类型溢出了,无缓冲类型有被阻塞的发送),缓冲区有值的
* 剩下的就是,未closed的,缓冲区没数据且发送队列空的
* 人话就是,轮到阻塞等待有缘人发送数据了
*/
//select中没必要阻塞,直接就略过了
if !block {
unlock(&amp;c.lock)
return false, false
}
//从这里开始就是阻塞的流程
//获取当前运行的g的指针
gp := getg()
//获取一个sudog结构体
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
//当前g正在等待的sudog
gp.waiting = mysg
//对应的g
mysg.g = gp
//是否在select语句中
mysg.isSelect = false
//标识是哪个chan
mysg.c = c
//唤醒时传递的参数,一般都是空
gp.param = nil
//将这个sudog放到接收队列中,等待唤醒
c.recvq.enqueue(mysg)
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
//这里设置个标志位,通知任何想要缩小栈空间情况,告诉他们这个g现在在等待channel的唤醒
//这里会有线程安全问题,所以用atomic操作
atomic.Store8(&amp;gp.parkingOnChan, 1)
//暂停当前g,也就是实质上的阻塞,函数原型放在最后
gopark(chanparkcommit, unsafe.Pointer(&amp;c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// someone woke us up
//唤醒,检查下有没有呗错误唤醒
if mysg != gp.waiting {
throw(&quot;G waiting list is corrupted&quot;)
}
//等待的置空
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime &gt; 0 {
blockevent(mysg.releasetime-t0, 2)
}
closed := gp.param == nil
gp.param = nil
mysg.c = nil
//释放sudog
releaseSudog(mysg)
return true, !closed
}
// Puts the current goroutine into a waiting state and calls unlockf.
// If unlockf returns false, the goroutine is resumed.
// unlockf must not access this G's stack, as it may be moved between
// the call to gopark and the call to unlockf.
// Reason explains why the goroutine has been parked.
// It is displayed in stack traces and heap dumps.
// Reasons should be unique and descriptive.
// Do not re-use reasons, add new ones.
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int)
</code></pre>
<p>再来让我们看看上面说的recv函数,上面的调用方式是这样的</p>
<p><code>recv(c, sg, ep, func() { unlock(&amp;c.lock) }, 3)</code></p>
<pre><code class="language-go">// recv processes a receive operation on a full channel c.
// There are 2 parts:
// 1) The value sent by the sender sg is put into the channel
// and the sender is woken up to go on its merry way.
// 2) The value received by the receiver (the current G) is
// written to ep.
// For synchronous channels, both values are the same.
// For asynchronous channels, the receiver gets its data from
// the channel buffer and the sender's data is put in the
// channel buffer.
// Channel c must be full and locked. recv unlocks c with unlockf.
// sg must already be dequeued from c.
// A non-nil ep must point to the heap or the caller's stack.
//上面注释写的很清楚,两个情况,一个是缓冲区满了,一个是没有缓冲区的
//总归都是sg不为空,也就是发送队列有人在等着发送数据
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
//缓冲区0,也就是无缓冲
if c.dataqsiz == 0 {
if ep != nil {
// copy data from sender
//直接从发送者的sg中提取内容
0