❝下面我将分享一位同学在Bilibili一面的面试经历,对于这次面试,他的评价是,「很有难度」,你试试呢?
❞
【提醒】通过这次面试经验,你将可以复习到以下知识点,注意汇总,不超过10个
-
RPC调用的网络中断处理 -
DPDK -
C++的多态实现和应用场景 -
C++类的大小及成员变量初始化 -
unique_ptr的用法 -
Golang的内存分配器 -
项目的压力测试方法 -
CPU负载过高的排查方法 -
HTTP2.0和HTTP3.0的优势 -
进程、线程、协程的区别 -
内存分配大小的限制 -
STL Vector的线程安全问题 -
C++的内存序 -
算法题:k个一组反转链表
「面试官」: 你好,我们先从你的项目开始谈起。项目中RPC调用在网络中断后,作为client端的状态应该是怎么样的?
「求职者」: 网络中断后,客户端在发起RPC调用时会因为不能建立连接而失败。这时候,一般会有重试机制,例如重试一定次数或者在一定时间内重试。如果重试都失败的话,那么这次RPC调用就会报错。
「面试官」: 明白了。那你对DPDK有了解吗?
「求职者」: DPDK,全称Data Plane Development Kit,是英特尔开源的一套数据平面开发套件。它主要用于网络数据包的高速处理,可以绕过操作系统内核,直接在用户态处理网络数据包,避免了内核态和用户态之间的切换开销,提高了数据包处理的效率。
「面试官」: 很好。接下来,谈谈「C++多态」的实现,以及它的应用场景。
「求职者」: C++的多态主要是「通过虚函数和继承」来实现的。在基类中定义虚函数,然后在派生类中重写这个虚函数,通过基类的指针或引用来调用这个虚函数,就可以实现多态。多态的主要应用场景是当我们需要处理一组对象,这组对象都是同一个基类的派生类,但是具体是哪个派生类在编译时并不确定。这时候,我们就可以利用多态,将这组对象都当作基类对象来处理,而具体的行为会根据对象的实际类型来确定。
「面试官」: 那你知道一个空类的大小是多少吗?
「求职者」: 在C++中,一个空类的大小不是0,而是1字节。「这是为了保证两个不同的对象有不同的地址」。
「面试官」: 好的。那么,如果在class里面定义了int a,但是没有实现构造函数,实例化这个类后,a的值是什么?
「求职者」: 如果没有实现构造函数,那么a的值是未定义的,也就是说,它的值是随机的,取决于这块内存之前的状态。
「面试官」: 明白了。接下来,你能告诉我unique_ptr可以作为函数返回值吗?
「求职者」: 是的,unique_ptr可以作为函数返回值。因为C++11支持移动语义,所以在返回时,unique_ptr会被移动,而不是复制。这样就避免了资源的泄漏。
「面试官」: 那你对golang的内存分配器有了解吗?
「求职者」: golang的内存分配器是基于tcmalloc实现的,它将内存分为小对象和大对象两种。小对象使用fix-size的内存池,大对象使用页堆。golang的内存分配器还有一些特性,比如支持并发分配,支持分代回收等。
「面试官」: 了解了。那么,你的项目的压力测试是怎么做的?
「求职者」: 我们的压力测试是使用JMeter来进行的。「JMeter可以模拟大量用户并发访问」,我们会设置不同的并发数和请求频率,然后观察系统的响应时间、吞吐量、错误率等指标,以此来评估系统的性能。
「面试官」: 那如果项目中的CPU负载过高,你会怎么排查呢?
「求职者」: CPU负载过高,「首先我会使用top或者htop命令来查看CPU的使用情况」,看看是哪个进程占用的CPU最高。然后,我会使用perf或者gprof这样的性能分析工具,对这个进程进行性能分析,找出CPU使用高的函数。接下来,我会深入到这个函数的代码中,看看是否有可以优化的地方。如果这个函数是一个循环或者递归,我会检查是否有冗余的计算,是否可以通过缓存结果来减少计算量。如果这个函数是一个阻塞操作,我会看看是否可以通过异步或者并发来提高效率。
「面试官」: 很好。那么,你的项目的瓶颈在哪个Server上呢?
「求职者」: 我们的项目的「瓶颈主要在数据库服务器上」。因为我们的业务逻辑比较复杂,涉及到大量的数据库操作,而且有些查询非常复杂,导致数据库的响应时间较长。所以,我们现在正考虑对数据库进行优化,比如使用索引来提高查询速度,使用读写分离来提高并发性能,或者使用分片来分散数据库的压力。
「面试官」: 明白了。接下来,我想问一下,快手直播流媒体是走长连接网关推送的吗?
「求职者」: 一般的流媒体服务,比如直播,是需要维持一个长连接的,这样才能保证实时性。至于是否通过网关推送,我想应该是的,因为通过网关可以进行负载均衡,提高系统的可用性。
「面试官」: 好的。那你能讲一下HTTP3.0对比HTTP 2.0的优势吗?
「求职者」: HTTP3.0的最大优势是引入了QUIC协议,替代了TCP协议。QUIC协议解决了TCP的头阻塞问题,实现了全双工的多路复用。此外,QUIC协议还支持0-RTT的快速握手,提高了连接的建立速度。而且,QUIC协议还实现了更好的拥塞控制和丢包恢复。
「面试官」: 明白了。那HTTP 2.0 对比 HTTP 1.1的优势又是什么呢?
「求职者」: HTTP 2.0的主要优势是实现了多路复用,解决了HTTP 1.1的头阻塞问题。此外,HTTP 2.0还引入了服务器推送和首部压缩等特性,进一步提高了效率。
「面试官」: 很好。接下来,讲一下进程、线程、协程的区别。
「求职者」: 进程是操作系统进行资源分配的最小单位,线程是操作系统进行调度的最小单位。每个进程有自己独立的地址空间和系统资源,而线程则共享所属进程的资源。协程又称为轻量级线程,它是一种用户态的线程,不需要操作系统参与调度,完全由程序自己控制,因此开销更小,切换更快。
「面试官」: 好的。一个进程调用malloc最大能分配多大的内存?
「求职者」: 一个进程调用malloc能分配的最大内存主要取决于操作系统的位数和进程的地址空间。例如,在32位操作系统中,理论上最大可以分配4GB的内存,但实际上会小于这个值,因为操作系统还需要一部分地址空间来管理硬件和操作系统自身。在64位操作系统中,理论上可以分配的内存会更大,但实际可用内存还是要受到物理内存和操作系统限制。
「面试官」: 明白了。如果有一个8G物理内存的机器,调用malloc(10G)会发生什么?
「求职者」: 如果在一个只有8G物理内存的机器上尝试分配10G内存,操作系统会使用虚拟内存来满足这个请求。虚拟内存通常使用硬盘作为额外的存储空间。但是,如果硬盘的交换空间(swap space)不足,malloc调用可能会失败,并返回NULL指针。
「面试官」: 好的,来谈谈STL Vector线程安全吗,不安全在哪?
「求职者」: STL Vector本身不是线程安全的。如果有多个线程同时对同一个Vector对象进行操作,比如一个线程正在添加元素,而另一个线程正在删除元素或迭代元素,那么就可能会导致数据竞争和不可预期的行为。
「面试官」: 那在多线程下使用Vector一定要加锁吗?
「求职者」: 是的,如果在多线程环境中使用Vector,为了保证操作的原子性和数据的一致性,通常需要对Vector的操作加锁。
「面试官」: 如果两个线程同时对Vector下相同索引的元素进行修改会发生什么?
「求职者」: 如果两个线程同时对Vector下相同索引的元素进行修改,那么最终的结果取决于哪个线程最后完成写入操作。这是典型的竞态条件,可能会导致程序的行为难以预测和调试。
「面试官」: 那么,能介绍一下C++的内存序吗?
「求职者」: C中的内存序是指在多线程环境下,对内存的读写操作的可见性和顺序。在多处理器系统中,不同处理器对内存的读写操作可能会有不同的顺序,这就导致了内存顺序问题。为了解决这个问题,C11引入了原子操作和内存序的概念,提供了几种不同的内存序选项,例如memory_order_relaxed
、memory_order_consume
、memory_order_acquire
、memory_order_release
、memory_order_acq_rel
和memory_order_seq_cst
。
「面试官」: 最后一个问题,手撕:k个一组反转链表。
「求职者」: 好的,这个问题可以通过迭代或递归来解决。我可以给出一个迭代的解法。
ListNode* reverseKGroup(ListNode* head, int k) {
if(head == nullptr || k == 1) return head;
ListNode dummy(0);
dummy.next = head;
ListNode *curr = &dummy, *nex, *pre = &dummy;
int count = 0;
while(curr->next != nullptr) {
curr = curr->next;
count++;
}
while(count >= k) {
curr = pre->next;
nex = curr->next;
for(int i = 1; i < k; i++) {
curr->next = nex->next;
nex->next = pre->next;
pre->next = nex;
nex = curr->next;
}
pre = curr;
count -= k;
}
return dummy.next;
}
「面试官」: 很好,你的回答很全面。谢谢你,这就结束了我们今天的面试,等消息吧。