下午不准备去图书馆自习来着,中午就狠狠地多睡了一个小时,三点起床靠在椅子上剥柚子,太爽了,这秋天的下午。"邮件:小米公司邀请你预约面试时间"..........
我擦,投了一个月了,认真准备的时候没有面试,现在躺了,来了面了~~~预约到了第二天下午,怎么办呢,考不考算法哇,考不考设计模式哇,考不考命令哇.....炸裂哇~
还能说什么呢,机会不是每次都有,还能怎么着,快学,赶紧穿好衣服去自习室复习复习,开卷
开面!
「面试官」自我介绍吧,别愣着了
「朱小哥」好的哥....
「面试官」说说你的项目背景,自己做的吗
「朱小哥」不是我做的你做的吗?这就是google的tcmalloc的mini版...
「面试官」malloc函数有什么问题呢,为什么需要改进,你的tcmalloc如何解决的
「朱小哥」主要就是针对多线程环境下带来的锁竞争的问题,现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。那malloc可不就不行了
tcmalloc针对锁竞争这里采用的是,开始吟唱:
既然想要解决多线程环境下存在激烈的锁竞争的问题,就得需要加锁吧,可是如果加的锁粒度太大,是非常影响效率的,所以要想尽可能的不加锁或者少加锁的话,再结合内存池的这个思路,那我就可以实现一个分层的逻辑。
可以做一个多层架构,每一层针对不同大小的内存分配请求使用不同的方法。这种灵活性有助于提高内存的利用率和性能。
- 每一个ThreadCache是线程独有的,这个地方不需要加锁。我还使用到了一种TLS技术,线程本地缓存,通过它我还可以在第一层就不需要加锁,
- CentralCache层只有当多个线程同时访问central cache的同一个桶时才会存在锁竞争,如果是多个线程同时访问central cache的不同桶就不会存在锁竞争。
- central cache的多个桶就可能同时向page cache申请内存的,所以page cache也是存在线程安全问题的,因此在访问page cache时也必须要加锁。但是在page cache这里我们不能使用桶锁,因为当central cache向page cache申请内存时,page cache可能会将其他桶当中大页的span切小后再给central cache。
「面试官」threadcache初识的内存是怎么拿到的,你是怎么拿到的threadcache的
「朱小哥」threadcache是怎么拿到内存的?从对应的哈希桶中去获取哇!第一次申请当然哈希桶中啥也没有,你得去向下一层要,下一层中心缓存也没有,就问PageCache要,PageCache没有就会向堆申请128页的内存
「面试官」PageCache是如何解决内存碎片的问题的
「朱小哥」内碎片是设计的问题,解决不了,外碎片是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。
关于如何解决外部碎片,听好了!
- 采用一种各层遵守的规范,就是将不同大小的内存块分为不同的大小类别,每个大小类别包含一组相似大小的内存块。这样可以更有效地分配和管理内存,减少了内存碎片。
- Thread-Caching 层维护了每个线程的本地内存缓存,这意味着内存块通常在同一个线程内进行分配和释放。这种局部性有助于减少内存碎片,因为同一个线程通常会分配和释放相似大小的内存块,从而减少了碎片化的可能性。
- 对于不用的内存,也有必要来回收,实现了内存块的合并机制,可以合并相邻的空闲内存块,从而减少内存碎片。当内存块被释放时,tcmalloc会尝试合并相邻的空闲块,以创建更大的可用内存块,提高内存的利用率。
- 如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
「面试官」PageCache这里是如何合并内存的呢
「朱小哥」......
「面试官」你到底会不会啊
「朱小哥」忘了忘了
「面试官」噗~~~~,给你个双向链表,你有三个节点,a b c 如何删除节点b,说说操作步骤
「朱小哥」小意思
「面试官」常见的进程间通信的方式
「朱小哥」匿名管道、命令管道、消息队列、共享内存、信号、信号量、socket,你想让我说说哪个~
「面试官」说说进程间通信中的socket通信吧,使用socket进行俩个主机通信的步骤
「朱小哥」王德发!!!选了个最不熟悉的,好你......Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。
创建Socket:
通信的第一步是创建一个Socket对象,它充当通信的端点。使用socket函数创建一个Socket。
绑定Socket:
为了让Socket能够在网络上被其他计算机访问,需要将其绑定到一个IP地址和端口号。bind函数用于绑定Socket。
监听(对于服务器):
如果你正在编写服务器应用程序,需要使用listen函数来等待客户端的连接请求。
接受连接(对于服务器):
当客户端尝试连接到服务器时,服务器使用accept函数来接受客户端的连接请求,并创建一个新的Socket来处理与该客户端的通信。
建立连接(对于客户端):
客户端使用connect函数来连接到服务器的Socket。
发送数据:
使用send函数来将数据从一个Socket发送到另一个Socket。
接收数据:
使用recv函数来从一个Socket接收数据。
关闭Socket:
当通信结束时,使用close函数来关闭Socket连接。对于服务器端,还需要关闭原始的监听Socket。
这是Socket通信的基本流程。可以使用socket、bind、listen、accept、send、recv和close等系统调用函数来执行这些操作。
「面试官」C++中的默认构造函数有哪些,移动构造呢
「朱小哥」无参构造、一般构造、赋值运算符重载、拷贝构造函数、移动构造(忘了)
「面试官」你给我写一个拷贝构造,什么场景使用拷贝构造
「朱小哥」厚礼蟹!!!拷贝构造怎么写!!!
「面试官」这都不会,
「朱小哥」...
「面试官」拷贝构造是浅拷贝还是深拷贝,说说深浅拷贝的区别
「朱小哥」软了...,肯定是深拷贝哇
- 浅拷贝, 又叫做值拷贝. 将源对象的值拷贝到目标对象中去. 本质上来说源对象和目标对象共用同一份实体, 只是所引用的变量名不同, 指向的地址是相同的.
- 浅拷贝中, 如果目标对象的值修改了, 则源对象的值也会相应改变
- 深拷贝, 拷贝的时候先开辟出和源对象大小一样的空间, 然后将源对象里的内容拷贝到目标对象当中去, 这样俩个指针指向了不同的内存位置, 并且里面的值是一样的.
- 深拷贝的情况下, 不会出现重复释放同一块内存的错误
- 深拷贝的实现: 拷贝构造和赋值运算符重载实现
「面试官」指针和引用的区别
「朱小哥」简单哇
- 指针是一个实体,需要分配内存空间,引用只是个别名,不需要内存空间
- 指针在定义的时候不一定要初始化,引用必须初始化,并且不能改变所引用的变量
- 指针可以时空指针,引用不能是空
- 有多级指针,没有多级引用
- 引用的底层是指针
「面试官」一个类中是有成员变量和有构造函数和析构函数的,这些函数会不会占用内存空间呢
「朱小哥」我觉得你说的有问题,成员函数是存放在代码段的,当然占用内存,但是类对象的大小只包含成员变量,不包含成员函数,会将类对象的地址传递给this指针,当调用一个对象的非静态成员函数时, 系统会把该对象的起始地址赋给成员函数的 this 指针, 另外,静态成员函数不属于任何一个对象, 所以静态成员函数没有 this 指针, 既然它没有指向某一对象, 也就无法对一个对象中的非静态成员进行访问
「面试官」memcpy和memmove使用过吗,具体说说
「朱小哥」不会
memcpy 函数用于从一个内存区域复制一定数量的字节到另一个内存区域。它将源内存区域的内容按顺序复制到目标内存区域,无论它们是否重叠。
void *memcpy(void *dest, const void *src, size_t n);
memmove 函数也用于从一个内存区域复制一定数量的字节到另一个内存区域,但与 memcpy 不同,它会处理重叠的内存区域。memmove 会检查内存区域是否重叠,如果重叠,通过使用一个临时缓冲区,将源内存区域的数据复制到这个缓冲区,然后再将缓冲区的数据复制到目标内存区域。这种方法确保数据的正确复制而不会破坏原始数据。
void *memmove(void *dest, const void *src, size_t n);
由于需要使用额外的内存来存储临时缓冲区,所以 memmove 可能比 memcpy 性能稍差,但它保证了数据的完整性和正确性。
「面试官」呼~,反问吧,有什么就问
「朱小哥」组是做什么的
「面试官」巴拉巴拉........
「朱小哥」我可以进入二面吗
「面试官」别问,问就是回家等消息~,怎么说呢,回答还凑活,指针和引用是背八股的吧,简直和网上回答是一样,你不得理解理解?不能光背哇,背还背不熟.....拷贝构造都不会写就有点太离谱了
「朱小哥」面试结束我学还不行嘛,找个实习太难了