字节面经
一、
1.C++ 11 特性
智能指针
2.读写锁底层是怎么实现的?
读写锁可以由俩部分组成:count(计数器)+ 等待队列
计数器:
count > 0 说明有人持有读锁
count = 0 说明没有人持有锁
count < 0 说明有人持有写锁
因为我们读写锁的特性是写独占,读共享,也就是说当我们有线程持有读锁的时候,另外一个线程继续获取写锁是可以的,但是获取写锁是不可以的
获取读锁
当前线程要获取读锁的时候,有两种情况
1.有线程持有读锁,则count + 1
2.当线程持有写锁,将该读者放到等待队列中
获取写锁
当线程要获取写锁的时候,有俩种情况
1.没有线程持有锁,则count < 0
2.有线程持有读锁/写锁,都会被放到等待队列中
读优先/写优先
当没有线程持有读写锁的时候,就绪队列中是读优先还是写优先决定了最终的读写优先顺序
3.多线程崩溃,如何通过 debug 的方式去发现问题
进程崩溃会生成一个coredump文件,我们可以使用gdb去调试这个coredump文件
gdb main coredump
使用gdb设置断点单步调式,到崩溃位置,用info threads查看线程的状态信息,bt查看当前线程的堆栈信息
客户端出现程序卡死,如何定位到的,如何解决的这个问题
出现问题 (客户端程序出现假死现象了)
解决问题(如何定位的、如何解决的说明白)
1.使用ps -aux 找到卡死进程的进程pid
2.使用gdb attach -pid,让gdb附加在这个卡死进程中,进行gdb调试
观察当前线程的状态(info threads)
在这个进程上设置断点。
检查变量的值和内存状态。
查看栈帧和调用堆栈(bt)
分析异常或崩溃的原因
3.通过info threads 查看当前进程中线程的信息
发现当前进程中有俩个线程,一个是主线程 id =1,一个是子线程 id = 2
4.通过bt打印一下主线程的堆栈信息
发现它卡死在main.cpp:119行
5.通过thread 2切换到子线程中,通过bt打印一下子线程的堆栈信息
发现子线程卡死在main.cpp:289行
6.定位到程序卡死的位置了,就是主线程阻塞在recv中,子线程也阻塞在recv中
7.解决问题
主线程就专注干一件事 就是发送请求给我们的客户端,直接接收请求交给我们的子线程。
那么我们子线程启动的时机就不能是登录成功之后了,因为他要接收服务器发来的登录响应,需要在一开始就启动子线程
也就是说子线程现在专门处理登录模块的响应,也处理我们主界面的一些业务的响应
OK了 问题解决了
总结
出现程序卡死现象了 =>
如何定位(ps -aux 、gdb attach -pid、info threads、bt、thread 2、bt、定位成功 )=>
分析问题产生原因(主线程既发送请求又接收请求、子线程也接收请求、导致服务器发送的响应被主线程和子线程争夺)= >
如何解决(主线程专门发送请求、子线程专门接收请求、并引入一个线程通信方式)
4.开辟一个线程,它会连带的为这个线程创建哪些资源
我们在Linux中通常通过pthread_create创建线程,默认的栈空间大小是8MB,C++11中提供用thread库创建线程
Windows中通常栈空间大小是1MB
栈空间
栈空间中主要存储函数栈中的局部变量、形参、返回值等等
栈空间是线程私有的,不能与其他线程共享
TCB线程控制块(Thread Control Block)
存储栈的上下文信息(上下文在线程切换的过程中会被使用到)
tid、线程状态、优先级等等
程序计数器
记录当前执行的指令位置
每个线程独立维护
要区分栈空间和栈帧
栈空间是在线程创建的时候,OS为线程创建的,用于存放一写参数、局部变量、返回值等等信息
栈帧是在函数调用时候,在栈上分配的一块内存空间,用于存放函数的参数、返回值、局部变量等等
它在函数调用的时候存在,函数返回的时候销毁
5.创建一个线程跟创建一个进程相比,哪个成本更高,哪个成本更低
创建进程是通过fork函数进行的
创建进程的成本
1.复制父进程的资源
复制页表
复制fds文件描述符表
创建新的虚拟内存空间
代码段
2.PCB
进程间都共享什么?
fds 文件描述符表
代码段
通过mmap映射的内容
创建线程的成本
1.开辟栈空间(8MB/1MB)
2.TCB
线程间都共享什么?
除了栈空间里面的内容,基本都会共享
BSS
DATA
TEXT
fds
mmap
heap
所以说创建进程的代价要远远大于线程
6.进程有独立的地址空间,这个地址空间是由哪块帮进程去做的
因为我们都知道进程之间数据是独立的,每个进程都有一个虚拟地址空间,当我们想访问数据的时候,首先访问虚拟地址空间,然后通过MMU(内存管理单元)将虚拟地址映射到响应的物理地址上,映射的方式有很多(段表映射、页表映射、先段表再页表,对应了不同的内存管理方式 分段管理、分页管理和段页式管理)
7.从虚拟地址到物理地址映射的这个单元叫什么
MMU(Memory Management Unit),主要功能有什么?
提供地址映射
内存保护(检查访问权限、地址的有效性、当缺页的时候触发缺页中断)
权限控制
8.有了多态在,对于我们编程来讲我们可以做哪些更灵活的事情
实例
Poller(抽象类)
里面给多用I/O复用保留了统一的接口
想多用IO复用中注册事件、修改事件、删除事件
开启事件循环
EPollPoller(子类)
必须重写这些接口
PollPoller(子类)
必须重写这些接口
好处在哪?
1.实现解耦操作(使用者、和实现者的分离)
我们的使用者只需要关注有哪些抽象接口,如何去调用,而不需要关心这些接口子类是如何实现的,哪怕我这个子类实现这个接口的方式改变了,在我使用者这里看来还是原来的样子
2.统一接口
9.多态的实现
同8一样
底层实现原理是通过虚表、虚表指针、父类指针指向子类对象的方式
10.怎么知道找这个虚函数表里面哪个位置?它是我想要的调用的函数呢
我们虚函数表(是一个指针数组)里面存储着虚函数的地址,顺序和虚函数在类中的声明顺序一致
11.new和malloc 区别
new - 运算符
返回值 - 不需要强转的
参数列表 - 不需要传递参数
异常结果 - 自动抛出一个异常
malloc - 函数
返回值 - 需要强转
参数列表 - 要申请的字节数
异常结果 - 返回一个void*0
new - malloc - 构造函数
malloc
brk - 堆段
mmap - 文件应映射区 私有内存映射
12.智能指针
13.memory order有了解吗
14.力扣 905.按奇偶排序数组
二、
1.负载均衡具体做法
因为我当时是在网上找的一个版本相对较老的Nginx.1.12.2版本的压缩包,解压后进行源码编译的时候他没有附带TCP负载均衡模块,需要加入–with-stream去激活这个模块,并且在nginx配置文件中也需要自己手动配置TCP模块
我记得那个配置文件几个比较重要的参数
第一个是我们的负载均衡器工作在哪个端口上(客户端的请求去往这里面去发)
第二个是一个MyServer 就是业务服务器的名字 里面具体写的是 部署了几台业务服务器 ip 地址都是什么
第三个是和故障检测有关的,也就是心跳计数机制有关
stream{
upstream MyServer{
// 如果心跳计数超过3次了,就默认我们的服务器发生故障了
server 127.0.0.1:6000 weight=1 max_fails=3 fail_timeout=30s;
server 127.0.0.1:6002 weight=1 max_fails=3 fail_timeout=30s;
}
server{
listen 8000; # 负载均衡器 工作在哪个端口上
proxy pass MyServer
}
}
./nginx -s reload
./nginx -s stop
2.负载均衡要解决什么问题啊?
我使用的Nginx的TCP负载均衡模块只要是做三个事情
做反向代理服务器
接收客户端发来的请求,然后通过响应的轮询算法给他分发到后面的业务服务器上
轮询算法都有什么?
平均轮询
默认的负载均衡算法
按照顺序将请求分发给后端服务器
每个服务器被轮流选择,权重相等
服务器性能差不多
加权轮询
权值大的服务器接收到的请求多
主要适用于服务器性能不均衡的情况
IP哈希
同一个ip地址发送的请求总是落在同一个服务器上
做业务服务器的故障检查
Nginx会与每个建立连接的业务服务器保持一个心跳机制,当信号计数达到一定阈值的时候,就会检测出该业务服务器出现故障了,后面的请求就不会再分发给该服务器了
当部署新服务器的时候,Nginx能够实现平滑加载
在不中断服务器的情况下,去平滑的加载配置文件
命令
./nginx -s reload # 平滑重新加载配置文件
./nginx -s stop # 停止nginx服务
3.你知道它的选择策略是怎么样的吗?
轮询策略
4.为什么会要有不同权重?
因为服务器的性能不同,平均的轮询可能导致性能较弱的服务器过载
我们每个请求服务器的处理时间也不一样,有的请求可能非常的复杂,特表耗时,没等处理完那,有轮询到他这个服务器了
同一个用户不同请求可能打到不同的服务器上
5.就是 1:1 轮询策略会有什么劣势?或者说会有什么问题吗?
6.怎么保证 tcp 连接的成功率?
就是我们TCP连接本身是有保证可靠性的方式的:面向连接、超时重传、流量控制、拥塞控制等等
这是对于他自身而言
我们可以通过给他增加一个TCP连接池,减少断开连接、建立连接等带来的开销,提高整体的成功率
可以增加保活机制
7.你怎么保证整个TCP长链过程中它都是稳定可用?
应用层的话我们可以·使用心跳机制
传输层的话我们可以开启TCP自带的保活机制
TCP自带的有面向连接、流量控制、拥塞控制、超时重传等等
8.io多路复用的概念
9.手撕 LRU Cache
三、
1.cpp程序在运行时的地址模块(代码段、数据段、bss、堆、栈)3.内存溢出
2.OOM内存泄漏
new/delete/malloc/free
父类指针指向子类对象的方式,父类没有设置为虚函数
智能指针的循环引用
指针的重新赋值
C++中有个valgrind
mtrace函数
3.设计模式五大原则(solid)
4.依赖倒置原则怎么理解 抽象怎么定义的,基于低层实现抽象会有什么问题
比如我的rpc项目中,本地服务如何快速部署成rpc服务,他需要本地服务类去继承一个框架提供的rpc类,并重写里面的rpc方法。并且调用rpc框架提供的一个接口,输入这个rpc服务类,才能完成rpc服务的部署。
那么这个发布rpc服务的接口的参数,必须是一个抽象类,因为我们的rpc服务类可能是不同的类型,框架提供的接口参数必须是它们的基类,这样才能使用多态(父类指针指向子类对象)。如果我们发布rpc服务的接口参数是一个具体的细节,那么我们rpc服务接口可能会写很多个,耦合度太高,这就是高层模块不依赖于抽象类,而依赖于底层类的具体实现的后果
5.Linux进程和线程的区别
进程是运行起来的程序,它是操作系统进行资源分配的基本单位,所以进程与进程之间数据是独立的
虚拟内存
1.好处
2.虚拟内存的管理方式 - 映射方式
IPC
1.管道
2.消息队列
3.mmap共享内存
4.信号
5.信号量
6.socket
线程你可以理解为是进程下的子任务,它是操作系统进行CPU调度的基本单位,线程与线程之间数据是共享的
同步
1.互斥锁 + 条件变量(lock_guard + unique_lock + condition_variable)
2.读写锁(底层实现)
3.信号量(sem)
上下文切换
6.进程的上下文切换都会保存什么信息,线程呢
进程
1.虚拟内存空间(代码段、数据段、栈段、堆段)
2.进程所处CPU的状态信息(通用寄存器、指令寄存器、地址寄存器(程序计数器)、栈指针)
3.fds
线程
1.虚拟内存空间(栈段)
2.线程所处CPU的状态嘻嘻(通用寄存器、指令寄存器、地址寄存器、栈指针)TCB
7.进程和线程的上限由什么决定,为什么会有限制
首先于内存
创建一个进程需要为他分配虚拟内存空间,PCB
文件描述符的限制
进程中可能有打开的文件 socekt
考虑任务上下文切换问题
8.设计一个线程池考虑的因素
9.线程池里的阻塞队列会有上限吗 为什么
10.对于线程池里保证稳定性,线程出现异常了怎么处理(try catch)5进程终止的时候如何让线程安全的退出
11.编程题:N个数找前K个最大的数(我用的优先级队列 问有没有优化的方法)
四、
1.项目相关问题
2.为什么做这个项目?
3.struct和class区别
4.重载和重写的区别
5.C++内存管理
6.为什么会有栈溢出,怎么解决
局部变量占用空间过大
int a[1000000] => 全局区申请 => 堆区中申请
函数递归深度过深
可能时终止调节没有设置好把
dfs
枚举方案数、判断是否需要编辑、搜索、回溯、递归终止条件
可以调整栈空间的大小
7.网络的四层模型
8.HTTP报文格式,响应码
9.pb的实现
10.pb对比json的优势
11.代码:线程安全的单例
12.算法:计算一个数的n次方
五、
1.m是如何实现跨平台
2.Socket 服务使用的什么协议2
3.TsR udk http 区别以及使用场景3.
TCP(可靠性)
文件传输
网页浏览(HTTP)
163邮箱 传输邮件
UDP(时效性)
视频
游戏
DNS域名解析(一应一答 数据量较小)
4.http 和 https的区别
5.对称加密 和 非对称加密
6.密钥的存储在哪
7.ipv4和 ipv6 区别
地址空间
4B - 32位 - 43亿
16B - 128位 - 超级大
地址形式
192.168.0.1
采用冒号十六进制表示法。例如,2001:0db8:85a3:0000:0000:8a2e:0370:7334
8.c++初始化对象 new关键字,new 一个对象需要几个步骤
9.c++的内存空间
10.栈区存什么
11.面向对象,多态,通过 C+实现多态的方式
12.纯虚函数介绍下
13.模版介绍-下
14.指针和引用区别
15.多级指针说说
16.引用的话可以为空吗 指针呢
17.串行队列和并行队列
18.同步 和 异步
19.生产者和消费者
20.代码题:写一个算法判断输入字符串是否为!地址。要求只遍历一次
21.(1-255).10-255).10-255).10-255)
22.要求尽量降低算法复杂度,提高代码执行效率
六、
1.主修课程和个人技术栈
2.大学期间的业余爱好
3.聊博客:部署、内容、框架、Nginx
4.平时是怎么学习的
5.为什么选择做客户端
6.介绍项目
7.define和const的区别,define在调试时怎么处理的
预处理阶段 - 不会进行语法检查 所有要谨慎使用 宏函数(日志宏函数)
生命周期/作用域