# 1. 自我介绍
# 2. 项目的介绍
# 3. 进程线程协程是什么,区别
进程是资源的分配和调度的独立单元。进程拥有完整的虚拟地址空间,当发生进程切换时,不同的进程拥有不同的虚拟地址空间。而同一进程的多个线程是可以共享同一地址空间
线程是 CPU 调度的基本单元,一个进程包含若干线程。
线程比进程小,基本上不拥有系统资源。线程的创建和销毁所需要的时间比进程小很多
由于线程之间能够共享地址空间,因此,需要考虑同步和互斥操作
一个线程的意外终止会影像整个进程的正常运行,但是一个进程的意外终止不会影像其他的进程的运行。因此,多进程程序安全性更高。
协程(Coroutine,又称微线程)是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制。
协程可以比作子程序,但执行过程中,子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。协程之间的切换不需要涉及任何系统调用或任何阻塞调用
协程只在一个线程中执行,是子程序之间的切换,发生在用户态上。而且,线程的阻塞状态是由操作系统内核来完成,发生在内核态上,因此协程相比线程节省线程创建和切换的开销
协程中不存在同时写变量冲突,因此,也就不需要用来守卫关键区块的同步性原语,比如互斥锁、信号量等,并且不需要来自操作系统的支持。
协程适用于 IO 阻塞且需要大量并发的场景,当发生 IO 阻塞,由协程的调度器进行调度,通过将数据流 yield 掉,并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去运行。
# 4. 用户态和内核态
因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以,为了减少有限资源的访问和使用冲突,Unix/Linux 的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel 的 X86 架构的 CPU 提供了 0 到 3 四个特权级,数字越小,特权越高,Linux 操作系统中主要采用了 0 和 3 两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如 C 函数库中的内存分配函数 malloc (),它具体是使用 sbrk () 系统调用来分配内存,当 malloc 调用 sbrk () 的时候就涉及一次从用户态到内核态的切换,类似的函数还有 printf (),调用的是 wirte () 系统调用来输出字符串,等等。
1)系统调用:原因如上的分析。
2)异常事件: 当 CPU 正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。
3)外围设备的中断:当外围设备完成用户的请求操作后,会像 CPU 发出中断信号,此时,CPU 就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。
# 5. 死锁
什么是死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程 A,按照先锁 a 再获得锁 b 的的顺序获得锁,而在此同时又有另外一个线程 B,按照先锁 b 再锁 a 的顺序获得锁
产生死锁的原因?
a. 竞争资源
系统中的资源可以分为两类:
可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU 和主存均属于可剥夺性资源;
另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程 P1 使用,假定 P1 已占用了打印机,若 P2 继续要求打印机打印将阻塞)
产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
b. 进程间推进顺序非法
若 P1 保持了资源 R1,P2 保持了资源 R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
例如,当 P1 运行到 P1:Request(R2)时,将因 R2 已被 P2 占用而阻塞;当 P2 运行到 P2:Request(R1)时,也将因 R1 已被 P1 占用而阻塞,于是发生进程死锁
死锁产生的 4 个必要条件?
产生死锁的必要条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程 -- 资源的环形链。
# 6.jwt 实现原理,改进
JWT 包包含三部分数据 Header + Payload + Signature:
①Header: 头部,通常头部有两部分信息:申明类型,这里是 JWT,会对头部进行 base64 编码,得到第一部分数据。
②Payload: 载荷,就是有效数据,一般包含下面信息:a. 用户身份信息(注意,因为这里采用的是 base64 编码,可解码,所以不要存放敏感信息);b. 注册申明:比如 token 的签发时间,过期时间,签发人等;这一部分也是 base64 编码,得到第二部分数据。
③Signature: 签名,是整个数据的认证信息,一般根据前两部的数据,再加上服务的秘钥(secret)(最好是周期性的更换),通过加密算法生成,用于验证整个数据的完整性和可靠性。
如果加强 JWT 的安全性?
根据我的使用,总结以下几点:
- 缩短 token 有效时间
- 使用安全系数高的加密算法
- token 不要放在 Cookie 中,有 CSRF 风险
- 使用 HTTPS 加密协议
- 对标准字段 iss、sub、aud、nbf、exp 进行校验
# 7.cookie 窃取会发生什么
跨站请求伪造,获得用户登录态,伪造身份
# 8. 开源社区关注吗
# 9.dockerfile 打包容器
dockerfile 文件编写
# 10. 使用 docker 时候遇到什么困难
# 11.mysql 优化的方法
- 为查询缓存优化查询,有一些函数例如 now,rand 等不会开启查询缓存,需要用一个变量来替换函数
- 当只要一条数据时候使用 limit 1,已经知道结果只有 1 时候
- 为搜索字段创建索引
- 在 join 表的时候,为两个表中的 join 的字段建立索引
- 不使用 order by rand ()
- 避免 select * ,需要什么就取什么
- 为每个表设置一个 ID 为主键
- 使用 enum 而不是 varchar,如果确定这些字段的取值是有限且固定情况下
- 尽可能使用 not null,null 需要额外空间,而且会使得进行比较时候程序变得很复杂
- 固定长度的表查询更快
- 拆分大的 delete 或 insert,因为这两个操作会锁表
# 12. 索引失效原因
- 在索引列有其他操作
- 使用了覆盖操作,如 select *,访问了非索引列的查询
- 使用了!= 和 <> 时候无法使用索引,会看进行全表扫描
- is null,is not null 无法使用索引
- 字符串不加单引号
- 使用 or 会使索引失效
# 13.mysql 主从机
MySQL 主从配置又叫 Replication 或者 AB 复制,简单讲就是 A 和 B 两台机器做主从后,在 A 上写数据,另一台 B 也会跟着写数据,两台数据实时同步。
MySQL 主从是基于 binlog 的,主上须开启 binlog 才能进行主从。
主从过程大致有 3 个步骤主将更改操作记录到 Binlog 里
从将主的 Binlog 事件 (sql 语句) 同步到从本机上并记录在 relaylog 里
从根据 relaylog 里面的 sql 语句按顺序执行主上有一个 logdump 线程,用来和从的 i/o 线程传递 binlog
从上有两个线程,其中 i/o 线程用来同步主的 binlog 并生成 relaylog,另外一个 SQL 线程用来把 relaylog 里面的 sql 语句执行一遍。
适用场景:
(1) 做数据库数据备份,仅仅是备份,当主机宕机时,马上从机可以代替主机。
(2) 还是做数据备份,但是和主机一样,会向 web 服务器提供服务。但是不能往从机上写数据。
# 14. 悲观锁乐观锁
** 悲观锁:** 认为很悲观,每次去拿数据的时候都认为别人回修改,所以每次都会在拿数据的时候上锁,这样别人想拿数据的时候就会阻塞,直到她拿到锁。
在关系型数据库当中就用到了这种锁机制,如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁
** 乐观锁:** 乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
# 15.TCP 为什么可靠
ACK 和超时重发机制
滑动窗口
拥塞窗口
一般来说,计算机网络都处在一个共享的环境中。因此也有可能因为其他主机之间的通信使得网络拥堵。如果网络拥堵的时候,我们发送端再发送一个较大的数据,极有可能就使得整个网络瘫痪了。又或者路由器没有足够的缓存空间,那么就会丢弃一些新来到的分组。然后 TCP 又进行了多次重传,造成了网络拥堵家具。
TCP 为了防止这类问题,引入了一个新的窗口变量称为 “拥塞窗口”,拥塞窗口的大小取决于网络环境。
在调节拥塞窗口大小的时候,首先会经历一个慢启动过程,首先将这个拥塞窗口的大小设置一个较小值,之后受到每一次 ACK 拥塞窗口的值会慢慢增加。发送数据包的时候,将拥塞窗口的大小与接收端主机通知的滑动窗口的大小作比较,然后取他们当中较小的值来确定发送端滑动窗口的值。
# 16. 滑动窗口
现在假设我们有一个 http 报文被分成了 9 组发送 1、2、3、4、5、6、7、8、9。如果按照刚刚的逻辑来看,肯定先发送 1。当 1 确认了之后再发送 2 以此类推。
这时候我们限定滑动窗口的大小为 5,就规定了滑动窗口覆盖这个范围 1~5 的数据可以并行发送出去
如果这时候接收端收到了 1,并且返回了 1 的 ACK,这时候窗口就会滑动一下,这时候 6 又可以被发送了.
当然接收方也可以进行累计确认,并不一定对每个分组都发送 ACK。假设接收方收到了 1、2、3 这 3 个分组,接收方只需要对分组 3 进行确认。这时候发送方收到了 3 的 ACK,就代表 3 之前的数据你都已经收到了,我就将窗口移动到 4。
当然滑动窗口也有不足的地方,如果接收方收到分组 1,2,4,5 的数据而分组 3 丢失了,那么这时候只能返回分组 2 的 ACK。4,5 不能返回。因为如果一旦返回了 4 或 5 就代表 4 或 5 之前的数据我都已经收到了。当然发送发需要重新发送 3、4、5。
TCP 的窗口分为发送窗口和接受窗口,发送窗口的大小由接受端的能力决定。一般接收端报文确认的时候,会在 TCP 首部设置窗口大小。发送端接收到了确认会调整自己发送端涌口的大小,直到发送窗口的大小变为 0 就会暂停发送数据。这种发送暂停的状态会持续到发送端发送一个新的窗口值为止。
# 17. 算法题
# 剑指 Offer27: 二叉树镜像原题
func mirrorTree(root *TreeNode) *TreeNode { | |
if root==nil{ | |
return nil | |
} | |
tmp:=root.Left | |
root.Left=mirrorTree(root.Right) | |
root.Right=mirrorTree(tmp) | |
return root | |
} |