面试常见问题
# linux
- 用户内存空间分布、mmap: https://colderleo.github.io/pages/44a400/ (opens new window)
# 页表、内存的访问:
- https://colderleo.github.io/pages/e9c8c3/ (opens new window)
- 虚拟地址转换为物理地址: 先访问tlb,如果tlb miss了,则要查页表。
- 根据物理地址访问cache或内存,内存如果不在实际内存上,则产生缺页异常,从硬盘上加载
# 堆和栈内存是如何分配的,哪个访问更快?
* 栈内存通过栈指针下移分配,分配速度非常快,一个指令即可。
* 堆内存通过malloc分配,分配速度慢,
* 栈内存访问较快,定义在栈上的变量直接访问,不需要指针跳转;栈不容易产生cache miss
* 堆内存访问较慢,一是访问时通过指针访问,要多一次跳转,二是堆内存更容易产生cache miss
* malloc申请的内存较小时使用brk(),较大时使用mmap()
# volatile
# 无锁队列: spsc、mpmc等
# memory order、内存同步: https://colderleo.github.io/pages/76ca3b/ (opens new window)
* volatile是编译阶段生效的,不是运行时的属性。相当于是std::memory_order_relaxed
# 动态链接和静态链接: https://colderleo.github.io/pages/02cb3a/ (opens new window)
* c/c++编译单元是c或cpp文件
* preload生效的原理,示例: <https://colderleo.github.io/pages/d5a573/>
# 常见程序行为耗时: https://colderleo.github.io/pages/a4e7dc/ (opens new window)
# 线程通信:
* 条件变量 <https://colderleo.github.io/pages/813113>
* 信号量 和条件变量类似,只不过条件变量更丰富一些
* mutex mutex如果不发生竞争,则不会陷入内核,耗时较短,该特性称为futex
* spinlock
* 无锁队列: 数据经过无锁队列大概耗时50-100ns
* eventfd、pipe
* 信号: <https://colderleo.github.io/pages/a59851>
# 编译、链接和hook
# 在源文件和静态库中同时定义同名全局变量会怎么样?
会编译报错。 - 如何编译不报错? 将其全部定义为弱符号 weak,或者除了其中一个,其他全部定义为弱符号 - 在主程序和so中同时定义同名全局变量会怎么样? 主程序中的全局变量会覆盖so的全局变量,主程序和so中对全局变量的修改都指向主程序中的变量,仿佛so中的全局变量定义不存在。 - 在两个so中同时定义同名全局变量会怎么样? 会使用链接指令中靠前的那个so中的变量。
# 如何hook一个so中的函数?
方法一: preload、 方法二: 在主程序代码中hook: 修改主程序中该函数的plt条目。 - 如何获取plt地址? 可通过jump指令或者主程序的elf加载信息获取函数的plt地址。 - 该方法与preload相比有何风险? 主程序对该函数的调用会被hook,但是so中的调用不会被hook。如果想hook so中对该函数的调用,需要修改so的plt条目。 方法三: 热补丁,在程序运行时,通过ptrace依附到目标进程,然后采用方法二进行修改。
# preload原理
动态连接器在符号重定位(symbol resolution/relocation)时,将preload指定的so放在查找队列的前面,从而覆盖主程序和其原本依赖的so的符号。
# 延迟重定位的原理
只有导出的符号可以被重定位 只有函数可以被延迟重定位,变量不可以。 可重定位的函数在执行时,跳转到plt条目中的地址,也就是真实的函数地址。延迟重定位就是plt条目中的初始地址指向符号重定位函数,而不是真实的函数地址,在函数第一次执行的时候,先执行了符号重定位函数,该函数将plt修改为真实函数地址并执行。在以后执行该函数时就是直接跳转到真实函数地址了。
# 进程调度
# 介绍下RCU的原理机制
参考: https://blog.csdn.net/zhoutaopower/article/details/86646688 (opens new window)
使用示例,from exanic源码
//reader
rcu_read_lock();
tcp = exasock_tcp_lookup(iph->saddr, iph->daddr, th->source, th->dest);
rcu_read_unlock();
// writer
spin_lock(&tcp_wait_death_lock);
//临界区,更新rcu链表,这个链表是用户自己定义的,更新也可以按照自己的方式更新。
list_del_rcu(&tcp->death_link);
//其他rcu相关函数: rcu_assign_pointer(ptr, new_ptr); list_replace_rcu(old, new); 等等
spin_unlock(&tcp_wait_death_lock);
// 更新链表之后调用synchronize_rcu()来释放旧节点。
synchronize_rcu();
2
3
4
5
6
7
8
9
10
11
12
13
14
rcu_read_lock()使当前cpu变为不可抢占(不能响应中断)的状态。
synchronize_rcu(),在writer更新节点后调用,该函数会向其他所有cpu发起rcu同步请求,该请求是一个中断信号,其他cpu如果都响应了,则表明它们都没有在访问旧节点,这时就可以释放旧节点了。
为什么所有cpu都响应就表明没有reader在读旧节点: 因为如果有reader在读旧节点,rcu_read_lock()会禁用cpu中断,它就无法响应。而更新节点后如果又有reader读取,它会直接读新节点,不会读旧节点。
# 如何修改时间片的长度?内核如何得知一个进程的时间片用完?
可通过修改内核CONFIG_HZ参数并重新编译内核来设置时间片长度。一般是在100 ~ 1000之间,默认250hz,即4ms https://blog.csdn.net/weixin_52622200/article/details/122992817 (opens new window)
由cpu时钟中断触发,在中断处理函数中判断其时间片是否用完;
-- 是否可以禁用该中断? 可以针对不同cpu设置nohz参数以屏蔽该中断。
查看时间片长度(通常是100ms): cat /proc/sys/kernel/sched_rr_timeslice_ms
进程何时进行调度(抢占)?
参考: https://blog.csdn.net/gatieme/article/details/51872618 (opens new window)
源码级分析: <https://blog.csdn.net/sfrysh/article/details/5809869>
首先进程会检查是否需要调度,并设置调度标记 检查发生在以下情况: scheduler_tick 时钟中断时、wake_up_process 唤醒进程时、do_fork 创建新进程时、set_user_nice 修改进程nice值时、smp_send_reschedule 负载均衡时
进程调度(抢占)就是执行schedule()函数,主要在以下时机:
进程被阻塞、 进程从中断处理程序或系统调用返回用户空间时、 时间片用完时、 进程主动调用系统调度相关函数时、进程在内核态调用解锁等函数使其变为可抢占时。进程调度时机详细情况如下,分为用户抢占和内核抢占:
用户抢占发生几下情况:
- 从系统调用返回用户空间;
- 从中断(异常)处理程序返回用户空间 从这里我们可以看到, 用户抢占是发生在用户空间的抢占现象. 更详细的触发条件如下所示, 其实不外乎就是前面所说的两种情况: 从系统调用或者中断返回用户空间
- 时钟中断处理例程检查当前任务的时间片,当任务的时间片消耗完时,scheduler_tick()函数就会设置need_resched标志;
- 信号量、等到队列、completion等机制唤醒时都是基于waitqueue的,而waitqueue的唤醒函数为default_wake_function,其调用try_to_wake_up将被唤醒的任务更改为就绪状态并设置need_resched标志。
- 设置用户进程的nice值时,可能会使高优先级的任务进入就绪状态;
- 改变任务的优先级时,可能会使高优先级的任务进入就绪状态;
- 新建一个任务时,可能会使高优先级的任务进入就绪状态;
- 对CPU(SMP)进行负载均衡时,当前任务可能需要放到另外一个CPU上运行
内核抢占发生的时机,一般发生在:
- 当从中断处理程序正在执行,且返回内核空间之前。当一个中断处理例程退出,在返回到内核态时(kernel-space)。这是隐式的调用schedule()函数,当前任务没有主动放弃CPU使用权,而是被剥夺了CPU使用权。
- 当内核代码再一次具有可抢占性的时候,如解锁(spin_unlock_bh)及使能软中断(local_bh_enable)等, 此时当kernel code从不可抢占状态变为可抢占状态时(preemptible again)。也就是preempt_count从正整数变为0时。这也是隐式的调用schedule()函数
- 如果内核中的任务显式的调用schedule(), 任务主动放弃CPU使用权
- 如果内核中的任务阻塞(这同样也会导致调用schedule()), 导致需要调用schedule()函数。任务主动放弃CPU使用权
# c++
# c++ 没有默认构造函数的类,不能调用无参构造。
<https://colderleo.github.io/pages/a1b204/>
* 虚析构函数、explicit、inline的特性、
# 左值和右值: https://colderleo.github.io/pages/6a25c0/ (opens new window)
# std::function、 lambda表达式 底层原理
# nullptr和NULL的区别
# stl中map/set、unordered_map/unordered_set、vector、deque、priority_queue等底层实现
- priority_queue: 堆, TopK问题
- unordered_set: 哈希表,容量不够时rehash,会导致迭代器失效。可通过reserve() 预设容量
- vector: 也有resize导致迭代器失效的问题。因此标准库的容器都不是线程安全的。
- 遍历vector比list快,因为cache命中率高
- emplace()减少一次拷贝
# 智能指针底层原理: https://colderleo.github.io/pages/681d3b/ (opens new window)
- 多线程中智能指针的引用计数如何统一
- 多线程中使用智能指针是否是线程安全的
# 网络通信
- kernel bypass: 智能网卡收包或者发包大概700ns https://colderleo.github.io/pages/85ea0b (opens new window)
- 常见智能网卡: https://colderleo.github.io/pages/74bbb4 (opens new window)
- tcp 基础知识: 湖科大教书匠-计算机网络,他的教程特别好
# 对端地址可以ping通,但是connect连接失败提示no route to host,可能是什么原因
1)本机自己开了防火墙 2)本机的etc/hosts 里面没有配置本机的机器名和ip
# epoll中et和lt的区别与实现原理
LT: 水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是: 只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。 ET: 边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。 参考: https://blog.csdn.net/ptgood/article/details/106845651
# tcp延迟确认
# systemtap/bpftrace原理和使用
# gdb+汇编
函数开始调用和调用结束时栈指针是怎么变化的
函数调用时参数如何传递(x86下): 前六个参数用寄存器传(6个参数分别为 rdi rsi rdx rcx r8 r9),后面用栈
查看某个地址内存数据: x/nfu n-数量 f-format u-单位 比如
x/20xb 0x400000
x/s addr 打印地址addr处的字符串gdb调试release版的C++程序,如何打断点。 一个release版的so,如何根据反汇编在其某一行代码处打断点
# 大小端和网络字节序
- 小端机器,发送一个结构体时,网络包中的字节序是大端还是小端。
- 写一下0x12345678在小端内存中的分布
# 如何在头文件中定义全局变量,被多个cpp包含时而不出错
- attribute((weak))
- 在头文件中定义模板类,类中包含static成员变量的声明,类外写成员变量的定义(可初始化)。声明和定义都在头文件中。被多个cpp包含时,不会出现冲突。
- 定义为static类型,这种做法实际上相当于每个cpp定义一个,多个cpp会存在多个不同的变量,而不是全局变量。
- 定义为inline类型(c++17), 不要加static,可以加__thread, 如
inline int a = 5;
- 类内定义为inline static类型(c++17), 类内的static变量本身就是全局唯一的变量,但是之前不能在头文件中定义, c++17之后加上inline就可以了。
# 算法题
- 用两个队列实现一个栈
# 基础题目
- 给一个so,怎么用
- 如何定义一个全局变量
- 局部static变量如何初始化
# 智力题
有100枚平放在桌上的硬币,10枚正面朝上,90枚反面朝上 不看不摸不能用任何方式区分硬币正面。如何将它们分成两堆 且每堆正面朝上的硬币数目相同(可翻面)
有100个硬币,开始正面朝下,第一次把1的倍数变向,第二次把2的倍数变向,第三次3的倍数,以此类推,进行100次,问朝上的有多少个?
3388算24, 3377算24
有两个一样的玻璃球,从100楼往下扔,判断它恰好会在哪一层碎。请设计一个方案,使得最坏情况所扔的次数最少。
有12个球,其中1个跟其他11个质量不一样,现在有一个天平,称三次,找出这个质量不一样的球。
圆内任取两条弦,两条弦在在圆内相交的概率是1/3
# 模板
- std::remove_reference_t 是怎么实现的
- typename的作用
- std::forward使用场景,为什么不直接使用右值引用
# 问答题
请写出以下代码执行输出结果,并解释为什么?
1. const std::string s1 = "abc";
std::string s2 = std::move(s1);
std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
std::move(s2);
std::cout << s2 << std::endl;
2
3
4
5
6
- 请写出以下代码执行后,a和b的结果分别是多少?
auto a = -1 << 1;
auto b = -1 >> 1;
2
- 如何将数组做为参数传递而不会导致类型退化,请写出func的具体实现?
int array[10];
func(array);
2
答案:
#include<iostream>
using namespace std;
template<class T>
int func(T& arr)
{
return sizeof(arr);
}
int main()
{
int Arr[] = { 1,2,3,4,5,6 };
cout << func(Arr) << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15