字节跳动面经汇总 -- C++后端
本篇博文主要介绍2021秋招时汇总的一些字节跳动后端面试过程中可能遇到的一些问题。
malloc和new的区别
new/delete 是 C++关键字,需要编译器支持。malloc/free 是库函数,需要头文件支持
使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而 malloc 则需要显式地指出所需内存的尺寸
new 操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故 new 是符合类型安全性的操作符。而 malloc 内存分配成功则是返回 void * ,需要通过强制类型转换将 void*指针转换成我们需要的类型
new 内存分配失败时,会抛出 bad_alloc 异常。malloc 分配内存失败时返回 NULL
new 会先调用 operator new 函数,申请足够的内存(通常底层使用 malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete 先调用析构函数,然后调用 operator delete 函数释放内存(通常底层使用 free 实现)。malloc/free 是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作
排序算法时间复杂度
如何用Linux shell命令统计一个文本中各个单词的个数
more log.txt | tr ' ' '\n' | sort | uniq -c
Linux下需要打开或者查看大文件
查看文件的几行到几行
sed -n '10,10000p' log # 查看第10到10000行的数据
linux 怎么查看进程,怎么结束进程?原理是什么?
top, kill
ps -e 查看进程详细信息
Linux怎么查看当前的负载情况
uptime命令主要用于获取主机运行时间和查询linux系统负载等信息
cat /proc/loadavg
tload
top
详细
Http Code
MySQL的底层索引结构,InnoDB里面的B+Tree
不同引擎索引的区别
i++是否原子操作
锁的底层实现
B Tree 和 B+ Tree的区别
b树和b+树
详细
B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B-树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。
B 树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。M阶B树具有以下特征:
- 根节点至少有两个子节点
- 每个节点有M-1个key,并且以升序排列
- 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
- 其它节点至少有M/2个子节点
B+树
B+树是对B树的一种变形树,它与B树的差异在于:
- 有k个子结点的结点必然有k个关键码
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中
- 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录
B树与B+树的区别:
- B树每个节点都存储数据,所有节点组成这棵树。B+树只有叶子节点存储数据(B+数中有两个头指针:一个指向根节点,另一个指向关键字最小的叶节点),叶子节点包含了这棵树的所有数据,所有的叶子结点使用链表相连,便于区间查找和遍历,所有非叶节点起到索引作用。
- B树中叶节点包含的关键字和其他节点包含的关键字是不重复的,B+树的索引项只包含对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
- B树中每个节点(非根节点)关键字个数的范围为m/2(向上取整)-1,m-1,并且具有n个关键字的节点包含(n+1)棵子树。B+树中每个节点(非根节点)关键字个数的范围为m/2(向上取整),m,具有n个关键字的节点包含(n)棵子树。
- B+树中查找,无论查找是否成功,每次都是一条从根节点到叶节点的路径。
B树的优点:
- B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
B+树的优点:
- 所有的叶子结点使用链表相连,便于区间查找和遍历。B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
- b+树的中间节点不保存数据,能容纳更多节点元素。
B树与B+树的共同优点:
- 考虑磁盘IO的影响,它相对于内存来说是很慢的。数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。所以我们要减少IO次数,对于树来说,IO次数就是树的高度,而“矮胖”就是b树的特征之一,m的大小取决于磁盘页的大小。
MySQL索引的发展过程?是一来就是B+Tree的么?从 没有索引、hash、二叉排序树、AVL树、B树、B+树 聊
MySQL里面的事务,说说什么是事务
数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的 ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由 DBMS 中的事务管理子系统负责事务的处理。
MySQL里面有那些事务级别,并且不同的事务级别会出现什么问题
读未提交 (脏读):最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生
读提交 (读旧数据,不可重复读问题):只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。
可重复读 (解决了脏读但是有幻影读):在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。
串行化:事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。
不可重复读和幻读的区别
- 不可重复读是读异常,但幻读则是写异常
- 不可重复读是读异常的意思是,如果你不多select几次,你是发现不了你曾经select过的数据行已经被其他人update过了。避免不可重复读主要靠一致性快照
- 幻读是写异常的意思是,如果不自己insert一下,你是发现不了其他人已经偷偷insert过相同的数据了。解决幻读主要靠间隙锁
数据库持久性是怎么实现的?
详细
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
MySQL采用了一种叫WAL(Write Ahead Logging)提前写日志的技术。意思就是说,发生了数据修改操作先写日志记录下来,等不忙的时候再持久化到磁盘。这里提到的日志就是redo log。
redo log称为重做日志,当有一条记录需要修改的时候,InnoDB引擎会先把这条记录写到redo log里面。redo log是物理格式日志,它记录的是对于每个页的修改。
redo log是由两部分组成的:一是内存中的重做日志缓冲(redo log buffer);二是用来持久化的重做日志文件(redo log file)。为了消耗不必要的IO操作,事务再执行过程中产生的redo log首先会redo log buffer中,之后再统一存入redo log file刷盘进行持久化,这个动作称为fsync
binlog记录了mysql执行更改了所有操作,但不包含select和show这类本对数据本身没有更改的操作。但是不是说对数据本身没有修改就不会记录binlog日志。
- binlog是mysql自带的,他会记录所有存储引擎的日志文件。而redo log是InnoDB特有的,他只记录该存储引擎产生的日志文件
- binlog是逻辑日志,记录这个语句具体操作了什么内容。Redo log是物理日志,记录的是每个页的更改情况
- redo log是循环写,只有那么大的空间。binlog采用追加写入,当一个binlog文件写到一定大小后会切换到下一个文件
更新一条语句的流程
- 首先执行器调用引擎获取数据,如果数据在内存中就直接返回;否则先从磁盘中读取数据,写入内存后再返回。
- 修改数据后再调用引擎接口写入这行数据
- 引擎层将这行数据更新到内存中,然后将更新操作写入redo log,这时候redo log标记为prepare状态。然后告诉执行器我处理完了,可以提交事务了。
- 执行器生成这个操作的binlog,并把binlog写入磁盘,然后调用引擎提交事务
- 引擎收到commit命令后,把刚才写入的redo log改成commit状态
这里使用了两阶段提交prepare阶段和commit阶段
数据库回表是什么?
详细
Innodb的索引存在两类,一类是聚簇索引一类是非聚簇索引,Innodb有且仅有一个聚簇索引。1. 如果表定义了PK,则PK就是聚集索引;2. 如果表没有定义PK,则第一个not NULL unique列是聚集索引;否则,InnoDB会创建一个隐藏的row-id作为聚集索引;
InnoDB 聚集索引 的叶子节点存储行记录,而普通索引的叶子节点存储主键值
在使用聚簇索引时,可以一步直接获取到记录值,而使用普通索引时,会首先获取记录的PK,然后再从聚簇索引中查找对应的记录,这个过程叫做回表
索引覆盖
理解方式一:就是select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖
理解方式二:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了(或覆盖了)满足查询结果的数据就叫做覆盖索引
理解方式三:是非聚集复合索引的一种形式,它包括在查询里的Select、Join和Where子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段,也即,索引包含了查询正在查找的数据)。
总结:要查找的数据可以都在索引中出现,而不需要再去查表获取完整记录
为了实现索引覆盖可以将被查询的字段,建立到联合索引里去
数据库读写锁发生死锁的情景
- MyISAM中不会出现死锁
在MyISAM中只用到表锁,不会有死锁的问题,锁的开销也很小,但是相应的并发能力很差。
解析:MyISAM不支持事务,即每次的读写都会隐性的加上读写锁,而我们知道读锁是共享的,写锁是独占的,意味着当一个Session在写时,另一个Session必须等待。- InnoDB中会出现死锁
InnoDB中实用了行锁和表锁,当未命中索引时,会自动退化为表锁。
解决方法为InnoDB中的MVCC机制
为什么推荐主键使用自增的整型,MySQL为什么主键自增好
为什么推荐主键:Innodb底层是B+树,数据和索引放在一起,因此需要一个主键作为索引,从而存储数据
为什么要自增:当新存储一条数据时,只需要向B+树后面的叶子节点插入即可,而不需要B+ 树为保持有序而进行旋转
为什么要整型:整形作为索引,容易直接判断大小而保持有序,使用String,相对于整数而言,不易判断大小
vector的底层实现,扩容机制
详细
使用一段连续的内存来存储数据,同时在数据结构中保存了三个指针来标记内存地址,首先是指向vector中数据的起始位置的_Myfirst指针,最后一个数据的位置的_Mylst指针,以及一个指向连续内存空间末尾的_Myend指针
当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
- 完全弃用现有的内存空间,重新申请更大的内存空间;
- 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
- 最后将旧的内存空间释放
不同的编译器,vector 有不同的扩容大小。在 vs 下是 1.5 倍,在 GCC 下是 2 倍
采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容
过大的倍数将导致大量空间的浪费
为什么扩容二倍
MySQL中如果使用like进行模糊匹配的时候,是否会使用索引
mysql在使用like查询的时候只有使用后面的%时,才会使用到索引
Volatile的作用,Volatile如何保证可见性的?以及如何实现可见性的机制
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存
volatile 用在如下的几个地方:
- 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
- 多任务环境下各任务间共享的标志应该加 volatile;
- 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能有不同意义
详细.
volatile作用:
锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序
如果大量的使用Volatile存在什么问题
操作系统的线程,以及它的状态
线程的基本状态:
1.新建
new语句创建的线程对象处于新建状态,此时它和其他java对象一样,仅被分配了内存。
2.等待
当线程在new之后,并且在调用start方法前,线程处于等待状态。
3.就绪
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态。处于这个状态的线程位于Java虚拟机的可运行池中,等待cpu的使用权。
4.运行状态
处于这个状态的线程占用CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态。只有处于就绪状态的线程才有机会转到运行状态。
5. 阻塞状态
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才会有机会获得运行状态。
6.死亡状态
当线程执行完run()方法中的代码,或者遇到了未捕获的异常,就会退出run()方法,此时就进入死亡状态,该线程结束生命周期。
进程和线程(进程与线程)的区别以及使用场景
线程产生的原因:进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:
- 进程在同一时间只能干一件事
- 进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。
- 因此,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性
进程是资源分配的最小单位,线程是操作系统进行执行和调度的最小单位
- 同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;
- 同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的;
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程崩溃,所以多进程比多线程健壮;
- 进程切换,消耗的资源大。所以涉及到频繁的切换,使用线程要好于进程;
- 两者均可并发执行;
- 每个独立的进程有一个程序的入口、程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
使用场景
- 需要频繁创建销毁的优先用线程
最常见的应用就是 Web 服务器了,来一个连接建立一个线程,断了就销毁线程- 需要进行大量计算的优先使用线程
所谓大量计算,当然就是要耗费很多 CPU,切换频繁了,这种情况下线程是最合适的
最常见的是图像处理、算法处理- 强相关的处理用线程,弱相关的处理用进程
- 可能要扩展到多机分布的用进程,多核分布的用线程
线程私有:线程栈,寄存器,程序寄存器
共享:堆,地址空间,全局变量,静态变量
进程私有:地址空间,堆,全局变量,栈,寄存器
共享:代码段,公共数据,进程目录,进程 ID
为什么线程创建和撤销开销大
当从一个线程切换到另一个线程时,不仅会发生线程上下文切换,还会发生特权模式切换。
既然是线程切换,那么一定涉及线程状态的保存和恢复,包括寄存器、栈等私有数据。另外,线程的调度是需要内核级别的权限的(操作CPU和内存),也就是说线程的调度工作是在内核态完成的,因此会有一个从用户态到内核态的切换。而且,不管是线程本身的切换还是特权模式的切换,都要进行CPU的上下文切换
线程和协程的由来和作用
协程,又称微线程。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
和多线程比,协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多
在协程上利用多核 CPU —— 多进程+协程,既充分利用多核,又充分发挥协程的高效率,
可获得极高的性能
多线程的通信和同步,多线程访问同一个对象怎么办
- 互斥锁
- 条件变量
- 读写锁
- 信号
查看端口号或者进程号,使用什么命令
查看程序对应的进程号: ps -ef | grep 进程名字
查看进程号所占用的端口号: netstat -nltp | grep 进程号
查看端口号所使用的进程号: lsof -i:端口号
信号量与mutex和自旋锁的区别
信号量(semaphore)用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。而互斥锁(Mutual exclusion,缩写 Mutex)是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源
自旋锁与前两者的区别是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁
非对称加密与对称加密
对称加密算法:加密效率高,速度快,适合大数据量加密。DES/AES
非对称加密算法:算法复杂,加密速度慢,安全性更高。结合对称加密使用。RSA、DH
在两列属性上分别建索引,则某个查询语句使用 where att1 = * and att2 = * 会怎么使用索引
一次查询只使用一个索引,因为每个索引都代表了一颗树,使用多个索引所带来的增益远小于增加的性能消耗。
对于联合索引来说会使用最左匹配 -- 在MySQL的user表中,对a,b,c三个字段建立联合索引,根据查询字段的位置不同来决定,如查询a, a,b a,b,c a,c 都可以走索引,其他条件的查询不能走索引
对于多个单列索引来说,MySQL会试图选择一个限制最严格的索引。但是,即使是限制最严格的单列索引,它的限制能力也肯定远远低于在多列上的多列索引
通过两个索引查询出来的结果,会进行什么要的操作?交集,并集
MySQL中遇到一些慢查询,有什么解决方法
- 定位慢查询
根据慢查询日志定位慢查询sql
- slow_query_log 默认是off关闭的,使用时,需要改为on 打开
- slow_query_log_file 记录的是慢日志的记录文件
- long_query_time 默认是10S,每次执行的sql达到这个时长,就会被记录
- 优化方案
优化数据库结构
分解关联查询: 很多高性能的应用都会对关联查询进行分解,就是可以对每一个表进行一次单表查询,然后将查询结果在应用程序中进行关联,很多场景下这样会更高效。
增加索引
建立视图
优化查询语句
添加存储过程
冗余保存数据
有了解过IO多路复用技术是个什么样的原理
I/O多路复用,I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。串起来理解就是很多个网络I/O复用一个或少量的线程来处理这些连接。
通过一个线程,同时连接多个线程会不会存在多个线程切换
在操作系统中,有高速缓存,主存,虚拟内存,外存,有知道它们之间有什么样的关系,以及它们的作用是啥
缓存: 在CPU同时处理很多数据,而又不可能同时进行所有数据的传输的情况,把优先级低的数据暂时放入缓存中,等优先级高的数据处理完毕后再把它们从缓存中拿出来进行处理
主存:主存就是内存,是直接与CPU交换信息的存储器,指CPU能够通过指令中的地址码直接访问的存储器,常用于存放处于活动状态的程序和数据
虚拟内存:当运行数据超过内存限度,部分数据自动“溢出”,这时系统会将硬盘上的部分空间模拟成内存——虚拟内存,并且将暂时不运行的程序或不使用的数据存放到虚拟内存中等待需要时调用
辅存就是外存: 硬盘与磁盘、光盘、软盘、U盘等
缺页的产生和换页算法
缺页中断:进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(存在位为0),那么停止该指令的执行,并产生一个页不存在异常,对应的故障处理程序可通过从外存加载该页到内存的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常
页面置换算法:将新页面调入内存时,如果内存中所有的物理页都已经分配出去,就要按某种策略来废弃某个页面,将其所占据的物理页释放出来,好的算法,让缺页率降低。
- 先进先出调度算法(FIFO)
- 最近最少调度算法(LFU,根据时间判断):利用局部性原理,根据一个作业在执行过程中过去的页面访问历史来推测未来的行为。它认为过去一段时间里不曾被访问过的页面,在最近的将来可能也不会再被访问。所以,这种算法的实质是:当需要淘汰一个页面时,总是选择在最近一段时间内最久不用的页面予以淘汰。
- 最近最不常用调度算法(LRU,根据使用频率判断
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacity;
public:
LRUCache(int _capacity): capacity(_capacity), size(0) {
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (!cache.count(key)) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (!cache.count(key)) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode* node = new DLinkedNode(key, value);
// 添加进哈希表
cache[key] = node;
// 添加至双向链表的头部
addToHead(node);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode* removed = removeTail();
// 删除哈希表中对应的项
cache.erase(removed->key);
// 防止内存泄漏
delete removed;
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
}
}
void addToHead(DLinkedNode* node) {
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}
DLinkedNode* removeTail() {
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
};
- 最佳置换算法(OPT):从主存中移出永远不再需要的页面;如无这样的页面存在,则选择最长时间不需要访问的页面。于所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率
面向对象采用的设计模式有哪些
- 在软件工程中,软件设计模式是通用的,可重用的在给定上下文中解决软件设计中常见问题的解决方案
单例模式(有的叫单元素模式,单态模式)
工厂模式
观察者模式
命令链模式
策略模式
设计模式的六大原则
为什么要有补码?
(为了更方便的实现减法运算)
一致性哈希
面向对象有哪些设计原则
OCP原则(也叫开闭原则): 开闭原则就是说对扩展开放,对修改关闭。
SRP原则(职责单一原则): 一个类只负责一项职责,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,当修改一个功能时,可以显著降低对其他功能的影响。
OCP原则(里氏替换原则):任何基类可以出现的地方,子类一定可以出现。通俗的理解即为子类可以扩展父类的功能,但不能改变父类原有的功能。
DIP原则(依赖倒置原则):高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。通俗点说:要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
LoD法则(迪米特法则):一个对象应该对其他对象保持最少的了解。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。
接口隔离原则:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
说说你对于 “不要用共享内存来通信,而应该用通信来共享内存” 的理解
在锁模式中,一块内存可以被多个线程同时看到,所以叫共享内存。线程之间通过改变内存中的数据来通知其他线程发生了什么,所以是通过共享内存来通信。锁是为了保护一个线程对内存操作的逻辑完整性而引入的一种约定,注意是一种约定而不是规则(一个线程可以不获取锁就操作内存,也可以解锁其他线程加的锁从而破坏保护,这种错误很难发现)。这种约定要每个线程的编写人员自觉遵守,否则就会出现多线程问题,如数据被破坏,死锁,饥饿等。
在go模式中,一块内存同一时间只能被一个线程看到,另外一个线程要操作这块内存,需要当前线程让渡所有权,这个所有权的让渡过程是“通信”。通信的原子性由channel封装好了,内存同一时间只能被同一线程使用,所以这种模式下不需要显示的锁。然而go模式也有约定,如果传递的是内存的指针,或者是控制消息,还是等于共享了内存,还是要保证将所有权让渡后, 不能再操作这块内存。
什么是双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
操作系统内存管理(分页、分段、段页式)
页式内存管理,内存分成固定长度的一个个页片。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。
页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。
分段存储管理方式的目的,主要是为了满足用户(程序员)在编程和使用上多方面的要求,其中有些要求是其他几种存储管理方式所难以满足的。因此,这种存储管理方式已成为当今所有存储管理方式的基础。
- 分页与分段的区别
页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提高内存的利用率。或者说,分页仅仅是由于系统管理的需要而不是用户的需要。段则是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了能更好地满足用户的需要
页的大小固定且由系统决定,而段的长度却不固定
分页的作业地址空间是一维的,即单一的线性地址空间;而分段的作业地址空间则是二维的
分页系统能有效地提高内存利用率,而分段系统则能很好地满足用户需求。
段页式系统的基本原理,是分段和分页原理的结合,即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。在段页式系统中,地址结构由段号、段内页号和页内地址三部分所组成。
浏览器输入网址到渲染的过程
- 浏览器构建HTTP Request请求
- 网络传输
- 服务器构建HTTP Response 响应
- 网络传输
- 浏览器渲染页面
DNS解析URL地址、生成HTTP请求报文、构建TCP连接、使用IP协议选择传输路线、数据链路层保证数据的可靠传输、物理层将数据转换成电子、光学或微波信号进行传输
DNS解析过程
- DNS 协议运行在 UDP 协议之上,使用端口号 53,用于将域名转换为IP地址
- 浏览器先检查自身缓存中有没有被解析过这个域名对应的 ip 地址
- 如果浏览器缓存没有命中,浏览器会检查操作系统缓存中有没有对应的已解析过的结果。在 windows 中可通过 c 盘里 hosts 文件来设置
- 还没命中,请求本地域名服务器来解析这个域名,一般都会在本地域名服务器找到
- 本地域名服务器没有命中,则去根域名服务器请求解析
- 根域名服务器返回给本地域名服务器一个所查询域的主域名服务器
- 本地域名服务器向主域名服务器发送请求
- 接受请求的主域名服务器查找并返回这个域名对应的域名服务器的地址
- 域名服务器根据映射关系找到 ip 地址,返回给本地域名服务器
- 本地域名服务器缓存这个结果
- 本地域名服务器将该结果返回给用户
https讲一下?密钥怎么交换的?私钥存储在哪里
HTTPs 是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版,即HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL
HTTPs的握手过程包含五步
- 浏览器请求连接
- 服务器返回证书:证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息,服务器采用非对称加密算法(RSA)生成两个秘钥,私钥自己保留
- 浏览器收到证书后作以下工作:
3.1 验证证书的合法性
3.2 生成随机(对称)密码,取出证书中提供的公钥对随机密码加密;浏览器即客户端使用非对称加密来加密对称加密规则,对称加密用于加密后续传输的信息
3.3 将之前生成的加密随机密码等信息发送给网站- 服务器收到消息后作以下的操作
4.1 使用自己的私钥解密浏览器用公钥加密后的消息,并验证 HASH 是否与浏览器发来的一致;获得浏览器发过来的对称秘钥
4.2 使用加密的随机对称密码加密一段消息,发送给浏览器- 浏览器解密并计算握手消息的 HASH:如果与服务端发来的 HASH 一致,此时握手过程结束,之后进行通信
http的流程
每个万维网的网点都有一个服务器进程,它不断的监听TCP端口80,以便发现是否有浏览器向它发出连接请求,一旦监听到连接建立请求,就通过三次握手建立TCP连接,然后浏览器会向服务器发出浏览某个页面的请求,服务器接着返回所请求的页面作为响应,然后TCP连接就被释放了。
这些响应和请求报文都遵循一定的格式,这就是HTTP协议所规定的。
SSL加密
- 客户端向服务器端索要并验证公钥
- 双方协商生成”对话密钥”。客户端用公钥对对话秘钥进行加密
- 服务器通过私钥解密出对话秘钥
- 双方采用”对话密钥”进行加密通信
- 对称加密算法: 加密效率高,速度快,适合大数据量加密。DES/AES
- 非对称加密算法:算法复杂,加密速度慢,安全性更高。结合对称加密使用。RSA、DH
- 私钥存储在服务器上
session和cookie
- cookie 是一种发送到客户浏览器的文本串句柄,并保存在客户机硬盘上,可以用来在某个WEB站点会话间持久的保持数据。
- Session的本质上也是cookie,但是不同点是存在服务器上的。这就导致,你如果使用cookie,你关闭浏览器之后,就丢掉Cookie了,但是如果关掉浏览器,重新打开之后,发现还有相应的信息,那就说明用的是Session。因为cookie是存在本地的,所以也会有相应的安全问题,攻击者可以去伪造他,填写相应的字段名,就能登录你的账户,还有如果cookie的有效期很长的话,也不安全。
- session 由服务器产生,对于客户端,只存储session id在cookie中
http和https的区别
- https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用
- http 是超文本传输协议,信息是明文传输,https 则是具有安全性的 ssl 加密传输协议
- http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443
- http 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全
https的安全外壳是怎么实现的
HTTPS就是在原HTTP的基础上加上一层用于数据加密、解密、校验、身份认证的安全层SSL/TSL,用于解决HTTP存在的安全隐患
信息加密:所有信息都是加密传播,第三方无法窃听;内容经过对称加密,每个连接生成一个唯一的加密密钥;
身份认证:配备了身份认证,第三方无法伪造服务端(客户端)的身份
数据完整性校验:内容传输经过完整性校验,一旦报文被篡改,通信双方会立刻发现
TCP TIMEWAIT讲一下?为啥需要这个?
当断开连接时,客户端发送完ACK将处于TIME WAIT状态,保持2MSL,之后完全断开意义在于:
- 保证最后一次握手报文能到服务端,能进行超时重传
- 2MSL 后,这次连接的所有报文都会消失,不会影响下一次连接
说一下TCP/IP
TCP/IP协议是包含TCP协议和IP协议,UDP(User Datagram Protocol)协议、ICMP(Internet Control Message Protocol) 协议和其他一些的协议的协议组
TCP/IP定义了电子设备(如计算机)如何连入因特网,以及数据如何在它们之间传输的标准.它是互联网中的基本通信语言或协议,在私网中它也被用作通信协议,当用户直接网络连接时,计算机应提供一个TCP/IP程序的标准实现,而且接受所发送的信息的计算机也应只有一个TCP/IP程序的标准实现
TCP/IP协议并不完全符合OSI 标准定制的七层参考模型,它采取了四层的层级结构
网络接口层:接收IP数据包并进行传输,从网络上接收物理帧,抽取IP 转交给下一层,对实际网络的网络媒体的管理,定义如何使用物理网络 ,如以太网。
网际层IP: 负责提供基本的数据封包传送功能,让每一块数据包都能打到目的主机,但不检查是否被正确接收,主要表现为IP协议
传输层:在此层中,它提供了节点的数据传送,应用程序之间的通信服务,主要是数据格式化,数据确认和丢失重传等。主要协议包括TCP和UDP
应用层:应用程序间沟通单层,如万维网(WWW)、简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等
fread和read的区别
read/write 操作文件描述符 (int型)
fread/fwrite 操作文件流 (FILE*型)
fread/fwrite 调用 read/write
read/write是系统调用,要自己分配缓存,也就是说效率要自己根据实际情况来控制。
fread/fwrite是标准输入/输出函数,不需要自己分配缓存,对于一般情况具有较高的效率。
什么是内存栅栏
内存栅栏(Memory Barrier)就是从本地或工作内存到主存之间的拷贝动作。
仅当写操作线程先跨越内存栅栏而读线程后跨越内存栅栏的情况下,写操作线程所做的变更才对其他线程可见。关键字 synchronized 和 volatile 都强制规定了所有的变更必须全局可见,该特性有助于跨越内存边界动作的发生,无论是有意为之还是无心插柳。
在程序运行过程中,所有的变更会先在寄存器或本地 cache 中完成,然后才会被拷贝到主存以跨越内存栅栏。此种跨越序列或顺序称为 happens-before。
写操作必须要 happens-before 读操作,即写线程需要在所有读线程跨越内存栅栏之前完成自己的跨越动作,其所做的变更才能对其他线程可见
TCP拥塞控制
拥塞控制的最终受控变量是发送端向网络一次连续写入的数据量(收到其中第一个数据报的确认之前),称之为发送窗口(SWND),SWND 受接收方接受窗口(RWND)的影响。同时也受控于发送方的拥塞窗口(CWND)。SWND = min(RWND, CWND )
- 当 cwnd < 慢开始门限(ssthresh) 时,使用慢开始算法
- 当 cwnd > ssthresh 时,改用拥塞避免算法
- 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。快重传配合使用的还有快恢复算法,是将 ssthresh减半,然后将 cwnd 设置为 ssthresh 的大小,之后执行拥塞避免算法
TCP为什么要三次握手、但是要四次挥手,握手第二步拆开行不行,挥手2 3步合并行不行,第三次握手确认的是什么能力
三次握手是为了避免僵尸连接,四次挥手是为了确保被断开方的数据能够全部完成传输,握手的第二部可以分开,不过需要增加一下状态。如果服务端没有数据要发送,挥手的2,3步可以合并,因为TCP是全双工的。第三次握手确认的是客户端时真实IP
TCP和UDP的区别
TCP 是面向连接的传输层协议,即传输数据之前必须先建立好连接, UDP 无连接
TCP 是点对点的两点间服务,即一条 TCP 连接只能有两个端点;UDP 支持一对一,一对多,多对一,多对多的交互通信
TCP 是可靠交付:无差错,不丢失,不重复,按序到达;UDP 是尽最大努力交付,不保证可靠交付
TCP 有拥塞控制和流量控制保证数据传输的安全性;UDP 没有拥塞控制,网络拥塞不会影响源主机的发送效率
TCP 是动态报文长度,即 TCP 报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的。UDP 面向报文,不合并,不拆分,保留上面传下来报文的边界
TCP 首部开销大,首部 20 个字节;UDP 首部开销小,8 字节
如果数据完整性更重要,如文件传输、重要状态的更新等,应该选用 TCP 协议。如果通信的实时性较重要,如视频传输、实时通信等,则使用 UDP 协议
用udp会有什么问题
不可靠,不稳定
因为本身没有重传的控制机制,所以丢包率的可能是其最主要的问题
TCP是可靠的,为什么UDP还要去实现可靠连接
Linux查看网络连接的命令
使用netstat查看存在的网络连接
使用ping判断主机间联通情况
tcp可靠性传输怎么实现
序列号、确认应答、超时重传
窗口控制与高速重发控制/快速重传(重复确认应答)
拥塞控制
流量控制
虚函数的实现原理,继承的时候怎么实现的
在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
局部变量分配在哪
分配在栈区
进程的栈有多大
32位Windows,一个进程栈的默认大小是1M,在vs的编译属性可以修改程序运行时进程的栈大小
inux下进程栈的默认大小是10M,可以通过 ulimit -s查看并修改默认栈大小
默认一个线程要预留1M左右的栈大小,所以进程中有N个线程时,Windows下大概有N M的栈大小
堆的大小理论上大概等于进程虚拟空间大小-内核虚拟内存大小。windows下,进程的高位2G留给内核,低位2G留给用户,所以进程堆的大小小于2G。Linux下,进程的高位1G留给内核,低位3G留给用户,所以进程堆大小小于3G
进程的最大线程数
32位windows下,一个进程空间4G,内核占2G,留给用户只有2G,一个线程默认栈是1M,所以一个进程最大开2048个线程。当然内存不会完全拿来做线程的栈,所以最大线程数实际值要小于2048,大概2000个
32位Linux下,一个进程空间4G,内核占1G,用户留3G,一个线程默认8M,所以最多380个左右线程(ps:ulimit -a 查看电脑的最大进程数,大概7000多个)
怎么快速把进程的栈用完
对函数进行递归调用
在函数中定义大对象
C++内存对齐
为什么要内存对齐:
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
如何进行内存对齐
- 分配内存的顺序是按照声明的顺序
- 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止
- 最后整个结构体的大小必须是里面变量类型最大值的整数倍
class A {
int a, b;
char c;
};
class B {
int a, b;
double c;
};
class C {
char a;
int b;
double c;
};
class D {
int a;
double b;
int c;
};
class E {
int a;
double b;
int c;
char d;
};
int main() {
cout << sizeof(A) << " " << sizeof(B) << " "
<< sizeof(C) << " " << sizeof(D) << " " << sizeof(E) << endl;
return 0;
}
// output
// 12 16 16 24 24
引用和指针的区别?对const型常量可以取引用吗
首先引用可以视作对象的别名,指针拥有自己的地址空间,其中保存着所指对象的地址
区别:
- 指针有自己的一块空间,而引用只是一个别名
- 使用 sizeof 看一个指针的大小是 4,而引用则是被引用对象的大小
- 指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象的引用
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变
- 指针可以有多级指针(**p),而引用只有一级
- 指针和引用使用++运算符的意义不一样
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露
http请求格式
HTTP 请求报文由请求行、请求头部、空行 和 请求包体 4 个部分组成
get post的区别 put delete 知道吗 put和post
http 1.X 2.0区别 ( 帧 流 推送 头部压缩 安全性等等
联合索引:b+树是什么状态
HTTP头部字段
http头部可以包含二进制吗
http2支持二进制,http1.x不支持
- http 2 和 http 1 的区别
- HTTP2使用的是二进制传送,HTTP1.X是文本(字符串)传送。
二进制传送的单位是帧和流。帧组成了流,同时流还有流ID标示- HTTP2支持多路复用
因为有流ID,所以通过同一个http请求实现多个http请求传输变成了可能,可以通过流ID来标示究竟是哪个流从而定位到是哪个http请求- HTTP2头部压缩
HTTP2通过gzip和compress压缩头部然后再发送,同时客户端和服务器端同时维护一张头信息表,所有字段都记录在这张表中,这样后面每次传输只需要传输表里面的索引Id就行,通过索引ID查询表头的值- HTTP2支持服务器推送
HTTP2支持在未经客户端许可的情况下,主动向客户端推送内容
MYSQL的事务
ACID特性,原子性、一致性、隔离性、持久性
多级缓存的由来和使用
计算机结构中CPU和内存之间一般都配有一级缓存、二级缓存来增加交换速度,这样当CPU调用大量数据时,就可避开内存直接从CPU缓存中调用,加快读取速度。
根据CPU缓存得出多级缓存的特点:
- 每一级缓存中储存的是下一级缓存的一部分
- 读取速度按级别依次递减,成本也依次递减,容量依次递增
- 当前级别未命中时,才会去下一级寻找
项目文件传输时怎么限速
在客户端进行文件传输时,每当上传限制大小数据,就sleep一下
用户态和内核态,为啥这样做,好处是什么
用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。内核态和用户态之间的转换方式主要包括:系统调用,异常和中断进程
堆和栈的区别
堆是由低地址向高地址扩展;栈是由高地址向低地址扩展
堆中的内存需要手动申请和手动释放;栈中内存是由 OS 自动申请和自动释放,存放着参数、局部变量等内存
堆中频繁调用 malloc 和 free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片
堆的分配效率较低,而栈的分配效率较高
栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由 C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低
五种IO模型
- 阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作
- 非阻塞IO:非阻塞等待,每隔一段时间就去检测 IO 事件是否就绪。没有就绪就可以做其他事
- 异步IO:
- 信号驱动IO:linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当 IO 时间就绪,进程收到 SIGIO 信号。然后处理 IO 事件
- 多路复用IO:linux 用 select/poll 函数实现 IO 复用模型,这两个函数也会使进程阻塞,但是和阻塞 IO 所不同的是这两个函数可以同时阻塞多个 IO 操作。而且可以同时对多个读操作、写操作的 IO 函数进行检测。知道有数据可读或可写时,才真正调用 IO 操作函数
bio nio aio 区别
详细
BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
NIO (New I/O):同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
AIO ( Asynchronous I/O):异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
Select poll epoll
I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回
I/O 多路复用和阻塞 I/O 其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个 system call (select 和 recvfrom),而 blocking IO 只调用了一个 system call (recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。
所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
select
是最初解决 IO 阻塞问题的方法。用结构体 fd_set 来告诉内核监听多个文件描述符,该结构体被称为描述符集。由数组来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理
存在的问题:
- 内置数组的形式使得 select 的最大文件数受限于 FD_SIZE;
- 每次调用 select 前都要重新初始化描述符集,将 fd 从用户态拷贝到内核态,每次调用select 后,都需要将 fd 从内核态拷贝到用户态
- 轮寻排查当文件描述符个数很多时,效率很低
- select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的
- select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现这个sock不用,要收回。对不起,这个select 不支持的,如果你要关掉这个sock, select的标准行为是不可预测的
poll
与 select 相比,poll 使用链表保存文件描述符,一没有了监视文件数量的限制,但其他三个缺点依然存在
epoll
epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。Epoll 是事件触发的,不是轮询查询的。没有最大的并发连接限制,内存拷贝,利用 mmap() 文件映射内存加速与内核空间的消息传递。
epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger
LT 模式是默认模式
LT(level triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的
ET 模式
ET(edge-triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个 EWOULDBLOCK 错误)。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死
LT 模式与 ET 模式的区别如下:
LT 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
ET 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
epoll的底层实现
epoll发展于介绍
epoll中就绪列表引用着就绪的socket,所以它应能够快速的插入数据。程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双向链表来实现就绪队列。
既然epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的socket。至少要方便的添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。epoll使用了红黑树作为索引结构
Linux的阻塞和非阻塞怎么体现
阻塞(休眠)调用是没有获得资源则挂起进程,被挂起的进程进入休眠状态,调用的函数只有在得到结果之后才返回,进程继续。
非阻塞(休眠)是不能进行设备操作时不挂起,或返回,或反复查询,直到可以进行操作为止,被调用的函数不会阻塞当前进程,而会立刻返回。
进程间通信
管道
具名管道
消息队列
信号
信号量
共享内存
socket
进程上下文切换
1 保存当前进程的上下文
2 恢复某个先前被抢占的进程的上下文
3 将控制传递给这个新恢复的进程
详细
保存处理器上下文环境即 PSW、PC 等寄存器和堆栈内容,保存到内核堆栈中
调整被中断进程的 PCB 进程控制块信息,改变进程状态和其它信息
将进程控制块移到相应队列即阻塞队列或就绪队列
选择另一个进程执行
更新所选择进程的 PCB 进程控制块
更新内存管理的数据结构
恢复第二个进程的上下文环境
中断是什么
中断定义:指当出现需要时,CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程。
硬件中断是由外设引发的, 软中断是执行中断指令产生的。
硬件中断的中断号是由中断控制器提供的, 软中断的中断号由指令直接指出, 无需使用中断控制器。
硬件中断是可屏蔽的, 软中断不可屏蔽。
中断处理过程
- 中断响应的事前准备
- CPU检查是否有中断/异常信号
- 根据中断向量到IDT表中取得处理这个向量的中断程序的段选择符
- 根据取得的段选择符到GDT中找相应的段描述符
- CPU根据特权级的判断设定即将运行的中断服务程序要使用的栈的地址
- 保护当前程序的现场
- 跳转到中断服务程序的第一条指令开始执行
- 中断服务程序处理完毕,恢复执行先前中断的程序
线程的上下文是什么、进程的上下文是什么
线程上下文
线程在切换的过程中需要保存当前线程 Id、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括 SP PC EAX 等寄存器,其主要功能如下
SP:堆栈指针,指向当前栈的栈顶地址
PC:程序计数器,存储下一条将要执行的指令
EAX:累加寄存器,用于加法乘法的缺省寄存器进程上下文
进程上下文包括三个,用户级上下文,寄存器上下文和系统级上下文
用户级上下文:指令,数据,共享内存、用户栈
寄存器上下文:程序计数器,通用寄存器,控制寄存器,状态字寄存器,栈指针(用来指向用户栈或者内存栈)
系统级上下文:pcb,主存管理信息(页表&段表)、核心栈
Linux文件系统,inode讲一讲?inode里存文件名称吗?
inode,中文名为索引结点,引进索引结点是为了在物理内存上找到文件块,所以 inode 中包含文件的相关基本信息,比如文件位置、文件创建者、创建日期、文件大小等,输入
stat 指令可以查看某个文件的 inode 信息
硬盘格式化的时候,操作系统自动将硬盘分成两个区域,一个是数据区,一个是 inode 区,存放 inode 所包含的信息,查看每个硬盘分区的 inode 总数和已经使用的数量,可以用 df 命令
在 linux 系统中,系统内部并不是采用文件名查找文件,而是使用 inode 编号来识别文件。查找文件分为三个过程:系统找到这个文件名对应的inode 号码,通过 inode 号码获得 inode 信息,根据 inode 信息找到文件数据所在的 block 读取数据
除了文件名之外的所有文件信息,都存储在 inode 之中
Linux chmod讲一讲,为啥有9位,分别对应什么
详细
Linux chmod(英文全拼:change mode)命令是控制用户对文件的权限的命令
Linux/Unix 的文件调用权限分为三级 : 文件所有者(Owner)、用户组(Group)、其它用户(Other Users)
9为代表三种用户的权限: rwxrwxrwx,前三位rwx表示文件所有者的权限,中间三位表示用户组的权限,最后三位表示其它用户的权限
STL中的map和unordered_map的实现
map实现使用红黑树,unordered_map使用hash表
简述
红黑树,B+树,跳表
红黑树。一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树。
性质:1. 每个节点非红即黑 2. 根节点是黑的; 3. 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的; 4. 如果一个节点是红的,那么它的两儿子都是黑的; 5. 对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点; 6. 每条路径都包含相同的黑节点。B+ 树
- 有n棵子树的非叶子结点中含有n个关键字(b树是n-1个),这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(b树是每个关键字都保存数据)。
- 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接(叶子节点组成一个链表)。
- 所有的非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
- 通常在b+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
- 同一个数字会在不同节点中重复出现,根节点的最大元素就是b+树的最大元素。
怎么查看linux下哪个进程打开了哪些文件
详细
lsof
虚拟地址怎么转换成物理地址
对于段页式系统来说,首先是查找段号,在对应段内找到页号,在页内找到页内偏移,从而
程序地址:段号+页号+页内偏移
进程的调度算法
- 先来先服务调度算法(FCFS)
在进程调度中采用 FCFS 算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行,该进程一直运行到完或发生某事件而阻塞后才放弃处理机。–非抢占式
有利于长作业,不利于短作业;有利于 CPU 繁忙的作业,不利于 I/O 繁忙的作业- 短作业(进程)优先调度算法(SJF、SPF)
短进程(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件被阻塞放弃处理机。—非抢占式
比 FCFS 改善平均周转时间和平均带权周转时间,提高系统吞吐量;对长作业不利,没能根据紧迫程度来划分执行的优先级,难以准确估计 作业或进程的执行时间。- 优先权调度算法
当该算法用于进程调度时,将把处理机分配给就绪进程队列中优先级最高的进程投入运行。分为非抢占式优先级算法和抢占式优先级算法。- 时间片轮转调度算法
系统将就绪进程按到达的顺序排成一个队列,按 FCFS 原则,进程调度程序总是选择就绪队列中的第一个进程执行,且只运行一个时间片。时间用完后,即使此进程并未完成,仍然将处理机分配给下一个就绪的进程,将此进程返回到就绪队列的末尾,等候重新运行- 多级反馈队列调度算法
设置 n 个就绪队列,优先级从 1 到 n 依次递减,即第 1 级队列优先级最高每个队列的时间也不相同,优先级高的队列,时间片越短,即从 1 到 n 时间片越来越多。一个新进程进入内存后,先插入第一级队列的末尾,按照FCFS的原则等待调度。如果某个进程可在一个时间片内完成,那么结束此进程;如果某进程在一个时间片内无法完成,就把此进程转入下一级队列的末尾,按照 FCFS 原则等待调度,一直到第 n-1 级队列。当一个很长的进程从第 1 级一直到第 n 级队列,那么它在第 n 级队列按照时间片轮转的方式等待调度。仅当第 1 级队列为空时,调度程序才调度第 2 级队列中的进程执行,依次类推。如果处理机正在处理第 i 级的队列的某进程,又有新的进程进入优先级更高的队列(第 1——i -1),则此时新的进程抢占处理机,原本正在执行第 i 级此进程停止运行,放到第 i 级就绪队列的末尾,把处理机分配给更高优先级的进程
短作业优先、短批处理作业周转时间较短、长批处理作业不会长期得不到执行
怎么解锁死锁
死锁四条件:
- 互斥
- 不可抢夺
- 占有与等待
- 循环等待
解决死锁的方案:
- 允许进程强行从占有者那里夺取某些资源,破坏不可抢占条件
- 进程在运行前一次性地向系统申请它所需要的全部资源,破坏了保持与等待条件
- 把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路,破坏了循环等待条件
死锁避免:银行家算法
OSI七层模型
- 物理层:规定通信设备的机械的、电气的、功能的和过程的特性,用以建立、维护和拆除物理链路连接。在这一层,数据的单位称为比特(bit)。属于物理层定义的典型规范代表包括:EIA/TIA RS-232、EIA/TIA RS-449、V.35、RJ-45 等
- 数据链路层:在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差
错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等。 - 网络层:在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点,确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。IP 是第 3 层问题的一部分,此外还有一些路由协议和地址解析协议(ARP)。有关路由的一切事情都在这第 3 层处理。地址解析和路由是 3 层的重要目的。网络层还可以实现拥塞控制、网际互连等功能。在这一层,数据的单位称为数据包(packet)。网络层协议的代表包括:IP、IPX、RIP、OSPF 等。
- 传输层:第 4 层的数据单元也称作数据包(packets)。但是,当你谈论 TCP 等具体的协议时又有特殊的叫法,TCP 的数据单元称为段 (segments)而 UDP 协议的数据单元称为“数据报(datagrams)”。这个层负责获取全部信息,因此,它必须跟踪数据单元碎片、乱序到达的数据包和其它在传输过程中可能发生的危险。第 4 层为上层提供端到端(最终用户到最终用户)的透明的、可靠的数据传输服务。所谓透明的传输是指在通信过程中 传输层对上层屏蔽了通信传输系统的具体细节。传输层协议的代表包括:TCP、UDP、SPX 等。
- 会话层:在会话层及以上的高层次中,数据传送的单位不再另外命名,而是统称为报文。会话层不参与具体的传输,它提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制。如服务器验证用户登录便是由会话层完成的。
- 表示层:这一层主要解决拥护信息的语法表示问题。它将欲交换的数据从适合于某一用户的抽象语法,转换为适合于 OSI 系统内部使用的传送语法。即提供格式化的表示和转换数据服务。数据的压缩和解压缩, 加密和解密等工作都由表示层负责。
- 应用层:为操作系统或网络应用程序提供访问网络服务的接口。应用层协议的代表包括:Telnet、FTP、HTTP、SNMP 等
ARP协议
ARP(地址解析)协议是一种解析协议,本来主机是完全不知道这个 IP 对应的是哪个主机的哪个接口,当主机要发送一个 IP 包的时候,会首先查一下自己的 ARP 高速缓存表(最近数据传递更新的 IP-MAC 地址对应表),如果查询的 IP-MAC 值对不存在,那么主机就向网络广播一个 ARP 请求包,这个包里面就有待查询的 IP 地址,而直接收到这份广播的包的所有主机都会查询自己的 IP 地址,如果收到广播包的某一个主机发现自己符合条件,那么就回应一个 ARP 应答包(将自己对应的 IP-MAC 对应地址发回主机),源主机拿到 ARP 应答包后会更新自己的 ARP 缓存表。源主机根据新的 ARP 缓存表准备好数据链路层的的数据包发送工作
malloc底层的实现
Malloc 函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc 其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc 采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时 malloc 采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址
当进行内存分配时,Malloc 会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc 采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并
Malloc 在申请内存时,一般会通过 brk 或者 mmap 系统调用进行申请。其中当申请内存小于128K 时,会使用系统函数 brk 在堆区中分配;而当申请内存大于 128K 时,会使用系统函数 mmap在映射区分配
逻辑地址---(分段硬件)>>> 线型地址 --- (分页硬件)>>> 物理地址 的过程,虚拟内存的实现
为什么引入虚拟内存
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的 4G 内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。
虚拟内存的好处:
- 扩大地址空间;
- 内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
- 公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
- 当进程通信时,可采用虚存共享的方式实现。
- 当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
- 虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把 CPU 交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
- 在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片
虚拟内存的代价:
- 虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
- 虚拟地址到物理地址的转换,增加了指令的执行时间。
- 页面的换入换出需要磁盘 I/O,这是很耗时的
- 如果一页中只有一部分数据,会浪费内存。
64位操作系统下,实现一个链接式的hash map,保存n组(key, value) 对,假设key, value各占8字节,问一共需要占多少字节
64位操作系统,指针8字节,假设hash函数有m个值,则8*m + (8+8+8)*n = 8m + 24n
8m表示开始的m个指针大小,(8+8+8)分别表示指针、key和value
延时队列怎么实现
详细
什么是延时队列?顾名思义:首先它要具有队列的特性,再给它附加一个延迟消费队列消息的功能,也就是说可以指定队列中的消息在哪个时间点被消费。
对于C++来说,可以直接使用优先队列,如在队列中存储消息id以及过期时间,队列自动按照时间排序,每次从队列中拿第一个消息进行消费。
怎么解决缓存击穿?怎么解决缓存雪崩?
- 缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大且不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
设置热点数据永远不过期
加互斥锁降低从数据库中读取数据频率
- 缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
- 设置热点数据永远不过期。
10M带宽,下载速度大约有多少
10 M 带宽单位是bps,而我们常使用的是Byte,因此约为10/8=1.25M。上行/上传速度会更慢,大约只有256K
客户端和服务端建立socket连接的过程,相关的方法名
- 服务端
- 创建一个socket,用函数socket();
- 设置socket属性,用函数setsockopt(); * 可选
- 绑定IP地址、端口等信息到socket上,用函数bind();
- 开启监听,用函数listen();
- 接收客户端上来的连接,用函数accept();
- 收发数据,用函数send()和recv(),或者read()和write();
- 关闭网络连接;
- 关闭监听;
closesocket
- 客户端
- 创建一个socket,用函数socket();
- 设置socket属性,用函数setsockopt();* 可选
- 绑定IP地址、端口等信息到socket上,用函数bind();* 可选
- 设置要连接的对方的IP地址和端口等属性;
- 连接服务器,用函数connect();
- 收发数据,用函数send()和recv(),或者read()和write();
- 关闭网络连接;
路由器和交换机有什么区别,分别工作在哪一层
交换机,工作在 OSI 第二层(数据链路层),根据 MAC 地址进行数据转发。
路由器,工作在 OSI 第三次(网络层),根据 IP 进行寻址转发数据包
fork 与 vfork
fork与vfork都是创建进程,vfork创建的子进程是与父进程共享地址空间,而fork创建的子进程是父进程的副本,它们的区别如下
- fork:子进程拷贝父进程的数据段,代码段
- vfork:子进程与父进程共享数据段
- fork:父子进程的执行次序不确定
- vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
vfork为什么需要exit()而不用return
如果你在vfork中return了,那么,这就意味main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就跪了。
#include<unistd.h>
#include<iostream>
#include<stdlib.h>
using namespace std;
int gdata = 1;
int main(){
pid_t pid;
pid = fork(); //更改为vfork
int tmp = 10;
if(pid == -1) cout << "fork error"<< endl;
else if(pid == 0){
cout << "son process, my parent is: " << getppid() << endl;
gdata = 11;
cout << "in son: " << gdata << endl;
tmp = 100;
cout << "son tmp: " << tmp << endl;
exit(0);
}else {
cout << "parent process, my pid is " << getpid() << endl;
cout << "in parent: " << gdata << endl;
cout << "in parent: " << tmp << endl;
}
return 0;
}
/*
当前输出:
son process, my parent is: 9743
in son: 11
son tmp: 100
parent process, my pid is 9743
in parent: 1
in parent: 10
将fork改为vfork
son process, my parent is: 9735
in son: 11
son tmp: 100
parent process, my pid is 9735
in parent: 11
in parent: 10
*/
客户端怎么校验https的证书是否合法
详细
数字证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA 的信息、有效时间、证书序列号等信息的明文,同时包含一个签名
客户端在对服务器say hello之后,服务器将公开密钥证书发送给客户端,注意这个证书里面包含了公钥+各种信息+签名(私钥对各种信息加密后生成签名),客户端收到公开密钥证书后,相当于收到了一个包裹里面有公钥+各种信息+签名,怎么样使用这三个数据来校验呢,很简单,公钥加密,私钥解,私钥加密公钥也可以解,只要利用公钥对签名进行解密,然后最和各种信息做比较就可以校验出证书的合法性。
两个进程某变量用gdb调试打印出的地址是否会一样
如果使用指针访问一个区域,指针+1 、指针-1可能会访问到什么?为什么
一致性哈希
通过hash环来实现负载均衡,将不同的服务器hash映射到一致性hash环上,当服务请求到来时,使用hash将其映射到hash环上,然后可以采用如顺时针寻找的方法选择距其最近的服务器进行服务。
当服务器较少或hash公式不够好时,可能出现大多数请求都会落在同一个服务器上,这就是数据倾斜,可以采用添加服务器、虚拟节点、更换一致性hash的方法进行解决。
Tcp: 拔网线之后连接是否存在 为什么 (记得tcp的长连接是有一个类似心跳检测的机制,忘了叫啥了,面试官问我心跳检测是在传输层吗还是应用层 ,我说应用层有心跳检测,但tcp那层也有类似的,后来回来看了下tcp的保活,
操作系统如何识别tcp连接
C++锁
互斥锁(Mutex) -- 互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。
条件锁 -- 条件锁就是所谓的条件变量
自旋锁
读写锁
vector、stack、queue这些容器是怎么实现的
stack 底层一般用 deque 实现,封闭头部即可,不用 vector 的原因应该是容量大小有限制,扩容耗时
queue 底层一般用 deque 实现,封闭头部的出口和前端的入口即可
priority_queue 的底层数据结构一般为 vector 为底层容器,堆 heap 为处理规则来管理底层容器实现
并发编程三要素
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:即程序执行的顺序按照代码的先后顺序执行,首先什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。处理器在进行重排序时也是会考虑指令之间的数据依赖性。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
短网址服务 -- 将很长的网址连接设计成短网址
- 使用分布式 ID 生成器
- 使用mysql数据库存储id与网址的对应关系
- 使用302重定向
如果用了 301,Google、百度等搜索引擎,搜索的时候会直接展示真实地址,那我们就无法统计到短地址被点击的次数了,也无法收集用户的 Cookie、User Agent 等信息,这些信息可以用来做很多有意思的大数据分析,也是短网址服务商的主要盈利来源
详细
零拷贝
“零拷贝”:在整个发送数据过程中,数据的复制是必不可少的,这里数据复制分两种类型,一种是CPU参与的一个字节一个字节处理的数据复制,一个是CPU不用参与,通过专有硬件DMA参与的,批量数据复制。自然,不用CPU参与的数据复制性能高。而“零拷贝”所说的拷贝,其实指的是,减少CPU参与的数据拷贝,最好减少到零次,但是各种实现方式里,很多种都只是减少一次两次,并没有直接让CPU参与的数据复制数归零
通过mmap实现的零拷贝I/O
发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据
发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。
write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine
通过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝
快照读在读提交和可重复读(RR和RC)模式下的问题
事务总能够读取到,自己写入(update /insert /delete)的行记录
RC下,快照读总是能读到最新的行数据快照,当然,必须是已提交事务写入的
RR下,某个事务首次read记录的时间为T,未来不会读取到T时间之后已提交事务写入的记录,以保证连续相同的read读到相同的结果集
在RC级别下每次都是读取最新的快照版本,在RR级别下是事务开启时生成一个全局快照,后续的快照读都读取这个快照
索引失效
1.有or必全有索引;
2.复合索引未用左列字段;
3.like以%开头;
4.需要类型转换;
5.where中索引列有运算;
6.where中索引列使用了函数;
7.如果mysql觉得全表扫描更快时(数据少);
内存泄漏
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制。
检查、定位内存泄漏:
检查方法:
在 main 函数最后面一行,加上一句_CrtDumpMemoryLeaks()。调试程序,自然关闭程序让其退出,查看输出:
被{}包围的 数字x 就是我们需要的内存泄漏定位值
定位代码位置:
在 main 函数第一行加上_CrtSetBreakAlloc(x);意思就是在申请 x这块内存的位置中断。然后调试程序,程序中断了,查看调用堆栈。加上头文件#include <crtdbg.h>
算法题
- 给定一些数组,例如下面的格式,他们都表示一个区间,然后你需要将区间进行合并 [1,2],[2,4],[3,7],[8,11] 如上所示, [1,2] 和 [2,4] = [1,4] ,然后 [1,4] 和 [3,7] = [1,7],最后 [1,7] 和 [8,11] 无法合并,所以最后结果应该返回 [1,7],[8,11]
先按照各个区间的开始位置排序,然后进行合并
- 数组出现次数最多的TOP N,给定一个数组,例如 [1,1,2,2,2,3,3,3,3]这样的,里面的数组不一定连续并且有序,假设我输入 2,这个2表示出现次数最高的两个, 那么你需要给我返回 2,3
使用 map 存储各个数字与它们出现的次数的数据对,使用大小为 N 的优先队列保持TOP N个数字
- 链表的两两翻转, 给定链表: 1->2->3->4->5->6->7 返回结果: 2->1->4->3->6->5->7
三个指针即可
- 两个有序数组合并 -- 多个有序数组的合并
多指针合并
- 二叉树中两个节点的最远距离 *
int FathestDistan2(TreeNode* pRoot, int &MaxValue)
{
if (pRoot == NULL)
return 0;
int LeftOfHeight = FathestDistan2(pRoot->pLeft, MaxValue);
int RightOfHeight = FathestDistan2(pRoot->pRight, MaxValue);
if (LeftOfHeight + RightOfHeight > MaxValue)
MaxValue = LeftOfHeight + RightOfHeight;
return LeftOfHeight > RightOfHeight ? LeftOfHeight + 1 : RightOfHeight + 1;
}
- 写sql语句 课程A分数大于平均分的总人数
select count(*) from test where score > (select avg(score) from test where name="a") and name ="a";
- 二维数组找单词
dfs
- top K 问题
优先队列
- 反转链表的n,m区间
三指针
- 二叉树里面两个节点的最近公共父节点
dfs,求左,求右,与求是否与当前节点重合
- A-Z的全排列
dfs
- 36进制加法
- 二叉搜索树,写节点的删除代码
- 最大连续子序列和
贪心
- 最小堆建堆 (下堆排序)
- 多个串,将含有相同字母的串放到同一个集合,返回集合向量
将字符串与排序的字符串映射,所以一个排序字符串的映射属于同一个集合
- 给一组日志文件,包含用户id,登陆时间,登出时间,假定时间范围一天之内,求出一天之内在线用户数量的峰值,并给出时间区间
把登陆时间和登出时间分别当做两个向量分别排序,然后从小到大扫描,类似归并排序的merge操作,先判断下一次操作登陆在前还是登出在前,登陆加一登出减一,得到当前时刻的在线人数
#include<iostream>
#include<vector>
#include<numeric>
#include<math.h>
#include<string>
#include<map>
#include<set>
#include<algorithm>
#include<queue>
#define pii pair<int, int>
using namespace std;
void max_num_range(vector<pair<int, int>>& vpii){
priority_queue<int, vector<int>, greater<int>> pq1, pq2;
for(pair<int, int> item: vpii){
pq1.emplace(item.first);
pq2.emplace(item.second);
}
int c_on = 0, max_on = 0, begin_time=0, end_time=0;
while(pq1.size() && pq2.size()){
if(pq1.top() <= pq2.top()){
++c_on;
if(c_on > max_on){
max_on = c_on;
begin_time = pq1.top();
}
pq1.pop();
}else{
--c_on;
if(c_on == max_on-1) end_time = pq2.top();
pq2.pop();
}
}
if(end_time == 0) end_time = pq2.top();
cout << "最大在线人数:" << max_on << "\n开始时间: " << begin_time << "\n结束时间: " << end_time << endl;
}
int main(){
vector<pair<int, int>> vpii;
pii a({1,10}), b({2,12}), c({3,9}), d({4,6}), e({5,6});
vpii.emplace_back(a);
vpii.emplace_back(b);
vpii.emplace_back(c);
vpii.emplace_back(d);
vpii.emplace_back(e);
max_num_range(vpii);
return 0;
}
- 给一个01矩阵,计算1被0分割成了几块
dfs
- 在一个表盘上,从0时刻出发,一次移动一格,顺时针逆时针均可,n步回到0时刻,问有多少种不同的路径
动态规划,0初始化向量,
dp[i] = dp[i-1]+dp[i+1]
- 手写一个LRU算法谈谈思路,要求 put 和 get 操作的时间复杂度均为 O(1)
map,双向链表
- 求二叉树深度
层序遍历
- 求第一个不连续的数,题目的意思是给定一个数组,比如[8,1,4,5,2,7],这个数组排序后是1 2 4 5 7 8,那么第一个不连续的数就是4。要求时间复杂度O(n)
找min和max,建立vector,两次遍历即可
- 二叉树的中序遍历
- 最长公共子序列
动态规划
dp[i][j] = dp[i-1][j-1] || dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- SQL查找第二高工资
select * from test where score < (select max(score) from test order by score desc limit 2) order by score desc limit 1; select * from test where name = 'a' order by score desc limit 1,1;
- leetcode39 组合总数
dfs
- leetcode7 整数反转
- 有一组数据, 2个1, 2个2,2个3, 2个4,。。。n个n 写程序找到这样一种排列, 使得 2个1之间1个数字,2个2之间2个数字,2个3之间3个数字,2个4之间4个数字,。。。2个n之间n个数字 例如n=4时, 41312432
- 最长上升子序列
class Solution {
public:
/**
* retrun the longest increasing subsequence
* @param arr int整型vector the array
* @return int整型vector
*/
vector<int> LIS(vector<int>& arr) {
// write code here
int n = arr.size();
vector<int> tail(n); //贪心的希望末尾的数字越小越好
vector<int> curLen(n); //arr的第i个元素对应的最大递增子序列长度
int ans = 0;
for(int i = 0; i < n; i ++){
int left = 0, right = ans;
while(left < right){ //找第一个大于等于arr[i]的位置
int mid = left + (right - left) / 2;
if(tail[mid] >= arr[i]) right = mid;
else left = mid + 1;
}
tail[left] = arr[i];
curLen[i] = left + 1; //当前元素对应最大递增子序列的长度
if(left == ans) ans ++;
}
vector<int> res(ans);
for(int i = arr.size() - 1, j = ans; i >= 0; i --){
if(curLen[i] == j) res[-- j] = arr[i];
}
return res;
}
};
- 输入字符串算式,里面有加减乘除和小数,计算结果
- leetcode322 零钱兑换
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
if(coins.empty()) return -1;
if(amount == 0) return 0;
vector<int> vec(amount+1, INT_MAX-10);
vec[0] = 0;
for(int item: coins){
for(int i = 1;i <= amount;++i){
if(item == i) vec[i] = 1;
else if(item < i){
vec[i] = min(vec[i], vec[i-item]+1);
}
}
}
return vec[amount] == INT_MAX-10 ? -1 : vec[amount];
}
};
- 二叉树找第k大怎么做,空间复杂度多少
- 二叉搜索树找第k大怎么做,空间复杂度多少
中序遍历+计数
- 用快排做topk怎么做,时间复杂度怎么算
void quick(vector<int>& vec, int l_index, int r_index){
// 快排 快速排序
if(l_index >= r_index) return;
int mid_index = (l_index+r_index)/2;
int key = vec[mid_index], i, j;
for(i = l_index, j = r_index; i < j;){
for(;i <= j && vec[i] < key;++i);
for(;j >= i && vec[j] >= key; --j);
if(i < j) swap(vec[i], vec[j]);
}
if(i < mid_index && vec[i] >= vec[mid_index])
swap(vec[mid_index], vec[i]);
quick(vec, l_index, i-1);
quick(vec, i+1, r_index);
}
// 快排变形.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <vector>
using namespace std;
int partion(vector<int>& nums,int left, int right)
{
int key = nums[left];
while (left < right)
{
while (left < right && nums[right] >= key) right--;
nums[left] = nums[right];
while (left < right && nums[left] <= key) left++;
nums[right] = nums[left];
}
nums[left] = key;
return left;
}
int topK(vector<int>& nums, int low, int high, int k)
{
if (low == high) return nums[low];
int pos = partion(nums, low, high);
int len = high - pos + 1;
if (len == k) return nums[pos];
else if (len > k) return topK(nums, pos+1, high, k);
else return topK(nums, low, pos -1, k-len);
}
int main()
{
vector<int> nums = { 12,52,1,59,46,49,65,58,15,34,28,9,5 };
int n = nums.size();
cout << topK(nums, 0, n - 1, 3);
}
- 实现一个循环队列,并设计一个O(1)的算法获取最大值
考虑数组+双指针+优先级队列
- 链表 奇位上升偶位下降 整合成升序链表
- LRU实现、插入操作、 描述数据结构如何变化 (说双向链表加哈希,在双向链表上做lru,加哈希表是为了快速定位要移动的节点)
- 实现哈希表 冲突过多的时候如何解决 (扩容
- 大数加法 链表
- 寻找中位数
- 堆排序
void adjust(vector<int> &arr, int len, int index)
{
int left = 2*index + 1; // index的左子节点
int right = 2*index + 2;// index的右子节点
int maxIdx = index;
if(left<len && arr[left] > arr[maxIdx]) maxIdx = left;
if(right<len && arr[right] > arr[maxIdx]) maxIdx = right;
if(maxIdx != index)
{
swap(arr[maxIdx], arr[index]);
adjust(arr, len, maxIdx);
}
}
// 堆排序
void heapSort(vector<int> &arr, int size)
{
// 构建大根堆(从最后一个非叶子节点向上)
for(int i=size/2 - 1; i >= 0; i--)
{
adjust(arr, size, i);
}
// 调整大根堆
for(int i = size - 1; i >= 1; i--)
{
swap(arr[0], arr[i]); // 将当前最大的放置到数组末尾
adjust(arr, i, 0); // 将未完成排序的部分继续进行堆排序
}
}
int main()
{
vector<int> arr = {8, 1, 14, 3, 21, 5, 7, 10};
heapSort(arr, arr.size());
for(int i=0;i<arr.size();i++)
{
cout<<arr[i]<<" ";
}
return 0;
}
- 线程的创建与使用
#include<mutex>
#include<windows.h>
#define pii pair<int, int>
using namespace std;
mutex g_mutex;
void print123()
{
//g_mutex.lock();
for (int i = 0; i < 3; i++) {
this_thread::sleep_for(chrono::milliseconds(100));
cout << i + 1;
}
//g_mutex.unlock();
}
int main()
{
thread(print123).detach();
thread(print123).detach();
system("pause");
}
- 二叉树前序遍历
class Solution { // 非递归
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode*> stk;
TreeNode* node = root;
while (!stk.empty() || node != nullptr) {
while (node != nullptr) {
res.emplace_back(node->val);
stk.emplace(node);
node = node->left;
}
node = stk.top();
stk.pop();
node = node->right;
}
return res;
}
};
class Solution { // 递归
public:
void dfs(TreeNode* root, vector<int> &vec){
if(!root) return;
vec.push_back(root->val);
if(root->left) dfs(root->left, vec);
if(root->right) dfs(root->right, vec);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> vec;
dfs(root, vec);
return vec;
}
};
- 旋转数组求最小
- 找出数字字符串中最长的连续上升子序列(连续上升:前后两数之差为1),LC 300变种
- 之字形打印二叉树
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> vvi;
if(!root) return vvi;
queue<TreeNode*> q1, q2;
vector<int> vi;
bool flag = true;
TreeNode* tmp;
q1.push(root);
while(q1.size()){
while(q1.size()){
tmp = q1.front();
vi.push_back(tmp->val);
if(tmp->left) q2.push(tmp->left);
if(tmp->right) q2.push(tmp->right);
q1.pop();
}
if(!flag) reverse(vi.begin(), vi.end());
vvi.push_back(vi);
vi.clear();
flag = !flag;
swap(q1, q2);
}
return vvi;
}
};
- 二维数组顺时针旋转90度
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
for(int i = 0;i < matrix.size()/2;++i){
for(int j = 0;j < matrix[0].size();++j){
swap(matrix[i][j], matrix[matrix.size()-1-i][j]);
}
}
for(int i = 0;i < matrix.size();++i){
for(int j = i+1;j < matrix[0].size();++j){
swap(matrix[i][j], matrix[j][i]);
}
}
}
};
- k个一组反转链表(倒置链表)
class Solution {
public:
// 翻转一个子链表,并且返回新的头与尾
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
ListNode* prev = tail->next;
ListNode* p = head;
while (prev != tail) {
ListNode* nex = p->next;
p->next = prev;
prev = p;
p = nex;
}
return {tail, head};
}
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* hair = new ListNode(0);
hair->next = head;
ListNode* pre = hair;
while (head) {
ListNode* tail = pre;
// 查看剩余部分长度是否大于等于 k
for (int i = 0; i < k; ++i) {
tail = tail->next;
if (!tail) {
return hair->next;
}
}
ListNode* nex = tail->next;
pair<ListNode*, ListNode*> result = myReverse(head, tail);
head = result.first;
tail = result.second;
// 把子链表重新接回原链表
pre->next = head;
tail->next = nex;
pre = tail;
head = tail->next;
}
return hair->next;
}
};
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
if (k <= 1) return head;
vector<ListNode* > vec;
while (head) {
vec.emplace_back(head);
head = head->next;
}
if (vec.size() < k) return head;
ListNode* newHead = vec[k - 1];
for (int i = 1; i < k; ++i) {
vec[i]->next = vec[i - 1];
}
int last_index = 0;
if (vec.size() == k) {
vec[0]->next = nullptr;
return vec[k - 1];
}
int j = k, tmp;
while (j < vec.size()) {
tmp = last_index + k - 1;
if (tmp + 1 < vec.size()) vec[tmp + 1]->next = nullptr;
if (tmp + k >= vec.size()) {
if(tmp+2 < vec.size()) vec[tmp+1]->next = vec[tmp+2];
vec[last_index]->next = vec[tmp + 1];
return newHead;
}
j = tmp + 2;
for (; j < vec.size() && j - tmp <= k; ++j) {
vec[j]->next = vec[j - 1];
}
vec[last_index]->next = vec[j - 1];
last_index = tmp + 1;
}
return newHead;
}
};
- 归并链表(链表排序)
// 优先队列
struct cmp{
bool operator()(ListNode* a, ListNode* b){
return a->val > b->val;
}
};
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(!head || !head->next) return head;
priority_queue<ListNode*, vector<ListNode*>, cmp> pq;
while(head){
pq.push(head);
head = head->next;
}
head = pq.top();
pq.pop();
ListNode* tmp = head;
while(pq.size()){
tmp->next = pq.top();
pq.pop();
tmp = tmp->next;
}
tmp->next = nullptr;
return head;
}
};
class Solution { // 归并
public:
ListNode* sortList(ListNode* head) {
if (!head || !head->next) return head;
auto slow = head, fast = head;
while (fast->next && fast->next->next)
slow = slow->next, fast = fast->next->next;
fast = slow->next, slow->next = nullptr;
return merge(sortList(head), sortList(fast));
}
private:
ListNode* merge(ListNode* l1, ListNode* l2) {
ListNode* head = new ListNode(0);
ListNode* ptr = head;
while (l1 && l2) {
ListNode* &node = l1->val < l2->val ? l1 : l2;
ptr = ptr->next = node, node = node->next;
}
ptr->next = l1 ? l1 : l2;
return head->next;
}
};
- 下一个排列(下一个更大的数)
class Solution {
public:
void nextPermutation(vector<int>& nums) {
if(nums.size() <= 1) return;
int i = nums.size()-1;
for(;i > 0 && nums[i] <= nums[i-1];--i);
if(i == 0){
sort(nums.begin(), nums.end());
return;
}
int index = i;
for(int j = i+1;j < nums.size();++j){
if(nums[j] < nums[index] && nums[j] > nums[i-1]){
index = j;
}
}
swap(nums[i-1], nums[index]);
sort(nums.begin()+i, nums.end());
}
};
- 镜像二叉树
class Solution {
public:
void exchange(TreeNode* root){
if(!root) return;
TreeNode* tmp;
tmp = root->right;
root->right = root->left;
root->left = tmp;
if(root->left) exchange(root->left);
if(root->right) exchange(root->right);
}
TreeNode* mirrorTree(TreeNode* root) {
exchange(root);
return root;
}
};
- 多线程顺序打印0到100
#include <iostream>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <map>
#include<queue>
#include <set>
#include<thread>
#include <stdio.h>
#include<mutex>
#include<chrono>
using namespace std;
int gdata = 0;
mutex mt;
void print(string s){
while(gdata < 100){
mt.lock();
if(gdata >= 100) return;
cout << s << " " << gdata << endl;
++gdata;
mt.unlock();
this_thread::sleep_for(chrono::milliseconds(5));
}
}
int main()
{
thread t1(print, "t1"), t2(print, "t2"), t3(print, "t3");
t1.detach();
t2.detach();
t3.detach();
system("pause");
return 0;
}
- 生产者消费者简易模型
#include <iostream>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <map>
#include <random>
#include<queue>
#include <set>
#include<thread>
#include <time.h>
#include <stdio.h>
#include<mutex>
#include<chrono>
using namespace std;
queue<int> que;
mutex mt;
void create_data(string s){
for(int i = 0;i < 100;++i){
mt.lock();
que.push(rand()%100);
cout << s << " " << que.back() << endl;
mt.unlock();
this_thread::sleep_for(chrono::microseconds(1000));
}
}
void eat_data(string s){
for(int i = 0;i < 100;++i){
mt.lock();
if(que.empty()){
mt.unlock();
this_thread::sleep_for(chrono::microseconds(1000));
--i;
continue;
}
cout << s << " " << que.front() << endl;
que.pop();
mt.unlock();
}
}
int main()
{
srand((unsigned) time(NULL));
thread create(create_data, "create1"), eat(eat_data, "eat1"), create2(create_data, "create2"), eat2(eat_data, "eat2");
eat.detach();
create.detach();
create2.detach();
eat2.detach();
system("pause");
return 0;
}