momenta面经总结

2023-12-07 07:20
文章标签 总结 面经 momenta

本文主要是介绍momenta面经总结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一. C++

01 堆和栈的区别,栈的静态分配和动态分配?

由操作系统分配释放,用于存放函数的参数值、局部变量等,栈中存储的数据的生命周期随着函数的执行完成而结束。

由开发人员分配和释放,若开发人员不释放,程序结束时由操作系统回收。

区别:

**(1)管理方式不同。**栈由操作系统分配释放,无需我们手动控制。堆的申请和释放工作由程序员控制,容易产生内存泄露。

**(2)空间大小不同。**每个进程拥有的栈大小要远远小于堆大小。

**(3)生长方向不同。**堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
**(4)分配方式不同。**堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca()函数分配,但是栈的动态分配和堆是不同的,它的动态分配是由操作系统进行释放,无需我们手工实现。

**(5)分配效率不同。**栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。

**(6)存放内容不同。**栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。

从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。

无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。

02 栈溢出的场景举例,嵌套调用函数会出现什么问题?

定义:栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致栈中与其相邻的变量的值被改变。

发生栈溢出的情况:

(1)最常见的就是递归。每次递归就相当于调用一个函数,函数每次调用时都会将局部数据放入栈中。递归10000次,就将10000份这样的数据放入栈中。然而只有当递归结束时,这些数据占用的内存才会被释放。如果递归次数过多,并且局部数据也多,那么将占用大量的栈内存,很容易造成栈溢出。

(2)在函数内部定义超大数组也会导致栈溢出。

解决方法:

(1)不要静态分配,用new动态创建,从堆中分配,堆的空间足够大

(2)改变默认栈的空间大小

可以嵌套调用函数,不能嵌套定义函数,嵌套调用函数也容易导致栈溢出

03 如何判断两个浮点数相等

不能直接用==判断

所以判断浮点数是否相等的常用方法是:

取两数差值的绝对值判断其是否在某一范围内(作差

04 内存泄露

定义:

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费

C++中的内存泄露,总的来说,就是new出来的内存没有通过delete合理的释放掉!

后果:

性能不良(并且逐渐降低)到内存完全用尽

更糟的是,泄漏的程序可能会用掉太多内存,以致另一个程序失败,而使用户无从查找问题的真正根源。

此外,即使无害的内存泄漏也可能是其他问题的征兆。

内存泄露类型:

(1)堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.

(2)系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

解决方法:

智能指针!!!

因为智能指针可以自动删除分配的内存

05 面向对象三大特性,挑一个讲一下

多态,就是指当完成某个行为时,不同的对象去完成会产生出不同的状态。(定义

多态分为静态多态和动态多态。

静态多态主要指编译时期的多态,例如函数重载模板

动态多态是指运行时期的多态,比如虚函数。

比如,将基类中的类成员函数定义成一个虚函数,在派生类中可以定义这个函数的不同实现,然后通过基类的指针或者引用指向派生类对象。

出现了虚函数,那个它会生成一个虚函数指针,存储在对象的头4个字节(vfptr 32位 4字节 64位 8字节),然后通过这个虚函数指针指向虚函数表(编译阶段产生的,运行时加载到.rodata段),虚函数表里存储了重写后虚函数的地址,最后进行动态绑定调用实现动态多态

虚函数=》vfptr=》vftable=》重写=》动态绑定调用=》动态多态

06 智能指针

利用上的对象出作用域自动析构的特征,来做到资源的自动释放,能够很好的解决内存泄漏问题。

但是普通智能指针解决不了浅拷贝问题,因为在析构的时候只析构了一次,将拷贝的智能指针析构掉,原先的指针没有被析构,造成了野指针的存在。

不带引用计数的智能指针

auto_ptr scoped_ptr unique_ptr

带引用计数的智能指针,使用资源的时候引用计数加1,不使用资源的时候引用计数减1,使用多个智能指针管理同一个资源

shared_ptr 可以改变资源的引用计数

weak_ptr 不可以改变资源的引用计数

避免只使用强指针导致的循环引用(只使用强指针交叉引用,会导致new出来的资源无法释放),定义对象的时候用强指针,引用对象的时候用弱指针

强弱指针一起用可以保证线程安全,通过引用计数避免已经析构的对象仍然访问了他的方法

07 指针操作还会出现哪些问题

(1)指针未初始化

(2)数组指针越界

(3)释放内存后没有把指针指向null

解决方法:
定义的时候就初始化;

严格限定变量范围,防止数组指针越界;

内存释放后将指针指向空。

08 下面四行代码含义

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9bBI4G9q-1657069471273)(https://image-1312312327.cos.ap-shanghai.myqcloud.com/94A15F94F345E8D4C2AC22BC4D1CB4E1)]

int *p[10]:存放了10个int * 类型的数组

int(*p)[10]:指向整型数组的指针

int *p(int);
p(int) 说明p为函数,返回值为 int类型的指针 (指向int的指针)。

int ( *p)(int);
( * p)说明 p 为指针,(*p)() 说明该指针指向函数, 函数的返回值为in

09 虚函数用法,动态绑定与静态绑定的区别

虚函数

虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)

静态类型:对象在声明时采用的类型,在编译期既已确定;
动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

image-20220701103539144

10 共享指针(shared_ptr)

shared_ptr 主要的功能是,管理动态创建的对象的销毁。它的基本原理就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向某对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。

shared_ptr 的构造要求比较高,如果对象在创建的时候没有使用共享指针存储的话,之后也不能用共享指针管理这个对象了。如果有引用循环 (reference cycle), 也就是对象 a 有指向对象 b 的共享指针,对象 b 也有指向对象 a 的共享指针,那么它们都不会被析构。

11 模板

函数模板和类模板

函数模板是一类函数的抽象,代表了一类函数,这一类函数具有相同的功能,代表一 具体的函数,能被类对象调用,而函数模板绝不能被类对象调用.
类模板是对类的抽象,代表一类类,这些类具有相同的功能,但数据成员类型及成员函数返回类型和形参类型不同

12 强制类型转换,用过怎样的转换

const_cast

1、常量指针被转化成非常量的指针,并且仍然指向原来的对象;
2、常量引用被转换成非常量的引用,并且仍然指向原来的对象;
3、const_cast一般用于修改指针。如const char *p形式。

static_cast

static_cast 作用和C语言风格强制转换的效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和C语言风格的强制转换都有安全隐患。
用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。注意:进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性需要开发者来维护。
static_cast不能转换掉原有类型的const、volatile、或者 __unaligned属性。(前两种可以使用const_cast 来去除)
在c++ primer 中说道:c++ 的任何的隐式转换都是使用 static_cast 来实现

reinterpret_cast: C风格的类型转换

reinterpret_cast是强制类型转换符用来处理无关类型转换的,通常为操作数的位模式提供较低层次的重新解释!

dynamic_cast:支持RTTI信息识别的类型转换

dynamic_cast强制转换,应该是这四种中最特殊的一个,因为他涉及到面向对象的多态性和程序运行时的状态,也与编译器的属性设置有关.所以不能完全使用C语言的强制转换替代,它也是最常有用的,最不可缺少的一种强制转换.

13 deque的底层原理

deque:双端队列容器

底层数据结构:动态开辟的二维数组

扩容方式:一维数组从2开始,以2倍的方式进行扩容,每次扩容后,原来第二维的数组,从新的第一维数组的下标oldsize/2开始存放,上下都预留相同的空行,方便支持deque的首尾元素添加

deque deq;

增加:

deq.push_back(20); 从末尾添加元素 O(1)

deq.push_front(20); 从首部添加元素 O(1) // vec.insert(vec.begin(), 20) O(n)

deq.insert(it, 20); it指向的位置添加元素 O(n)

删除:

deq.pop_back(); 从末尾删除元素 O(1)

deq.pop_front(); 从首部删除元素 O(1)

deq.erase(it); 从it指向的位置删除元素 O(n)

查询搜索:

iterator(连续的insert和erase一定要考虑迭代器失效的问题)

14 array/vector/list/map/deque的区别和应用场景

底层数据结构:

增删查改:

随机访问:

内存扩容方式:

vector:底层数据结构为数组 ,支持快速随机访问。

list:底层数据结构为双向链表,支持快速增删。
deque:底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问。
stack:底层一般用deque/list实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时。
queue:底层一般用deque/list实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时。
priority_queue:的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现。
set:底层数据结构为红黑树,有序,不重复。 multiset:底层数据结构为红黑树,有序,可重复。
map:底层数据结构为红黑树,有序,不重复。 multimap:底层数据结构为红黑树,有序,可重复。
unordered_set:底层数据结构为hash表,无序,不重复。
unordered_multiset:底层数据结构为hash表,无序,可重复 。

unordered_map:底层数据结构为hash表,无序,不重复。 unordered_multimap:底层数据结构为hash表,无序,可重复。

15 链表和数组的区别

数组:增加删除O(n) 随机访问O(1)

链表:(考虑搜索的时间)增加删除O(1) 查询O(n)

1.底层数据结构:数组 双向循环链表

16 C++标准库了解么,用的多么

image-20220704083924213

17 介绍STL容器

img

18 map相关操作及其底层红黑树

插入删除效率高:

对于关联容器来说,不需要做内存拷贝和内存移动。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点

(底层数据结构)红黑树性质:

1.每个节点都是红色或黑色
2.根节点为黑色
3.叶节点为黑色的NULL结点。
4.如果结点为红,其子节点必须为黑
5.任一节点到NULL的任何路径,所含黑结点数必须相同

相关操作:

1.构造(默认构造 拷贝构造)

​ 赋值

2.插入

删除

清空

3.查找 find(key)

统计 count(key)

(1) 他们的底层都是以红黑树的结构实现,因此**插入删除等操作都在O(logn)**时间内完成,因此可以完成高效的插入删除;

(2) 在这里我们定义了一个模版参数,如果它是key那么它就是set,如果它是key+value,那么它就是map;底层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value

(3) 因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。

19 lambda表达式咋用,在哪用,有什么好处

(1) 利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;

(2) 每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。

(3) lambda表达式的语法定义如下:

[capture] (parameters) mutable ->return-type {statement};

(4) lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;

语法:

(1) [函数对象参数]

**标识一个 Lambda 表达式的开始,这部分必须存在,不能省略。**函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量(包括 Lambda 所在类的 this)。函数对象参数有以下形式:

函数对象参数有以下形式:

空。没有任何函数对象参数。
=。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
&。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
this。函数体内可以使用 Lambda 所在类中的成员变量。
a。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的,要修改传递进来的拷贝,可以添加 mutable 修饰符。
&a。将 a 按引用进行传递。
a,&b。将 a 按值传递,b 按引用进行传递。
=,&a,&b。除 a 和 b 按引用进行传递外,其他参数都按值进行传递。
&,a,b。除 a 和 b 按值进行传递外,其他参数都按引用进行传递。
(2) (操作符重载函数参数)

**标识重载的 () 操作符的参数,没有参数时,这部分可以省略。**参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b)) 两种方式进行传递。

(3)mutable 或 exception 声明

**这部分可以省略。**按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)。

(4)返回值类型

标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

(5) {函数体}

20 emplace_back和push_back区别

push_back()方法要调用构造函数和复制构造函数,这也就代表着要先构造一个临时对象,然后把临时的copy构造函数拷贝或者移动到容器最后面。
而emplace_back()在实现时,则是直接在容器的尾部创建这个元素,省去了拷贝或移动元素的过程。

emplace_back() 函数在原理上⽐ push_back() 有了⼀定的改进,包括在内存优化⽅⾯和运⾏效率⽅⾯。内存优化主要体现在使⽤了就地构造(直接在容器内构造对象,不⽤拷⻉⼀个复制品再使⽤) + 强制类型转换 的⽅法来 实现,在运⾏效率⽅⾯,由于省去了拷⻉构造过程
结论:在C++11情况下,果断用emplace_back代替push_back

21 C++11五个特性

(1)关键字

(2)智能指针

(3)多线程

(4)函数对象、绑定器、lamda表达式

(5)容器

set和map:红黑树 O(lgn)

unordered_setunordered_map:哈希表 O(1) 提高增删查的效率

array:数组 vector

forward_list:前向链表 list

22 static和const 特性和作用

const就是只读的意思,只在声明中使用,意即其所修饰的对象为常量((immutable)),它不能被修改,并存放在常量区。
static一般有两个作用,规定作用域和存储方式(静态存储)。对于局部变量,static规定其为静态存储方式每次调用的初始值为上一次调用后的值,调用结束后存储空间不释放;对于全局变量,如果以文件划分作用域的话,此变量只在当前文件可见,对于static函数也是如此。static修饰的变量如果没有初始化,则默认为0.

23 深拷贝和浅拷贝

浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变,拷贝了基本类型的数据

深复制----在计算机中开辟了一块新的内存地址用于存放复制的对象。

24 手写单例模式

25 手写智能指针、共享指针

26 new和malloc

(1) new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;

(2) 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

(3) new操作符内存分配成功时,new返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

(4) new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

(5) new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

27 设计模式

28 内联函数

在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。

使用限制:

inline的使用时有所限制的,inline只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。

如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

29 引用和指针区别

(1) 指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。

(2) 引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。(注:不能有引用的值不能为NULL)

(3) 有多级指针,但是没有多级引用,只能有一级引用。

(4) 指针和引用的自增运算结果不一样。(指针是指向下一个地址,引用是引用的变量值加1)。

(5) sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小。

(6) 引用访问一个变量是直接访问,而指针访问一个变量是间接访问。

(7) 使用指针前最好做类型检查,防止野指针的出现;

(野指针:指针变量的值未初始化;指向的地址空间已经被free/delete;超出作用域)

(8) 引用底层是通过指针实现的;

(9) 作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。

二. 操作系统

01 介绍以下线程池

线程池(thread pool)这个东西,在面试上多次被问到,一般的回答都是:“管理一个任务队列,一个线程队列,然后每次取一个任务分配给一个线程去做,循环往复。

简单来说就是有一堆已经创建好的线程(最大数目一定),初始时他们都处于空闲状态。当有新的任务进来,从线程池中取出一个空闲的线程处理任务然后当任务处理完成之后,该线程被重新放回到线程池中,供其他的任务使用。当线程池中的线程都在处理任务时,就没有空闲线程供使用,此时,若有新的任务产生,只能等待线程池中有线程结束任务空闲才能执行。

线程池的好处:

(1)避免线程开的过多,使内存耗尽

(2)避免了创建和销毁线程的代价

(3)任务和执行分离

02 请求队列上的线程是如何工作的

03 线程有绑核么?

在多核CPU中合理的调度线程在各个核上运行可以获得更高的性能。在多线程编程中,每个线程处理的任务优先级是不一样的,对于要求实时性比较高的线程或者是主线程,对于这种线程我们可以在创建线程时指定其绑定到某个CPU核上,以后这个核就专门处理该线程。这样可以使得该线程的任务可以得到较快的处理,特别是和用户直接交互的任务,较短的响应时间可以提升用户的体验感。

04 协程

一种用户态的轻量级线程,完全由用户调度控制,拥有自己的寄存器上下文和栈,协程调度切换的时候,先将寄存器上下文和栈保存到其他地方,切换回来的时候再恢复之前保存的寄存器上下文和栈。直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

但是同一时间只能执行一个协程,大致来说是一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态,适合对任务进行分时处理;而线程,一次可以执行多个线程,适合执行多任务处理。

05 半同步反应堆

在半同步/半反应堆模型下,主线程为异步线程,主要负责监听所有的事件 。1. 如果事件为连接事件:主线程负责将连接请求接收并且向epoll内核事件表中注册事件。2. 如果事件为非连接事件:主线程负责将连接请求加入请求队列,线程池中的工作线程通过竞争来执行请求队列中的任务。以上模型也存在一定的缺点:1. 首先请求队列是唯一的,那么请求队列在其中便扮演了临界资源的角色。对于临界资源的访问必须要加锁保护,消耗了一定CPU时间。2. 线程池中每一个工作线程同一时刻只能处理一个请求。如果在任务量比较大的情况下,请求队列中会堆积任务,客户端响应变慢。

06 Reactor和Proactor的区别

传统的IO编程一般使用一个while循环不断地监听端口是否有新的连接,如果有,就调用一个处理函数来完成传输处理。

这种模式有一个缺点,因为是阻塞的,所以前一个请求没有处理完成,那个后一个就无法处理,所以就诞生了一个线程处理一个连接的IO编程模式。早期的Tomcat就是这么做的。

对于大量的连接,需要耗费大量的线程资源,对线程资源要求太高。

线程池为了防止频繁的创建线程

Reactor模型

(1)等待IO

(2)处理IO

image-20220703082259125

image-20220703082430678

image-20220703082453221

image-20220703082523262

Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。

Proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。

因此,Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。这里的「事件」就是有新连接、有数据可读、有数据可写的这些 I/O 事件这里的「处理」包含从驱动读取到内核以及从内核读取到用户空间。

举个实际生活中的例子,Reactor 模式就是快递员在楼下,给你打电话告诉你快递到你家小区了,你需要自己下楼来拿快递。而在 Proactor 模式下,快递员直接将快递送到你家门口,然后通知你。

无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。

07 epoll和select、poll之间的区别

select:

将文件描述符收集过来,交给内核,让内核来判断哪一个有数据,有数据的会在bitmap里置位,同时将select函数返回,不再阻塞,最后进行遍历查询即可。

缺点:

(1)bitmap只有1024

(2)用户态和内核态切换开销过大

image-20220702150832616

poll:

image-20220702151448404

epoll:

image-20220702152147536

08 Linux下线程同步的机制有哪些,用过哪些锁?

当多个线程同时访问其所共享的进程资源时,需要相互协调,以防止出现数据不一致、不完整的问题。这就叫线程同步。

用锁解决

互斥锁、自旋锁、读写锁

09 共享内存实现原理,解决共享内存弊端,一般给内存加锁还是进程?

内存共享: 两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

4aaffcbdfd78c0ee743aa4fdd0eef0bf.png

10 临界区和锁相比优势在哪

由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区,它是访问共享资源的代码片段,一定不能给多线程同时执行。

互斥

首先明白临界区是指一段代码,这段代码是用来访问临界资源的。临界资源可以是硬件资源,也可以是软件资源。但它们有一个特点就是,一次仅允许一个进程或线程访问。当有多个线程试图同时访问,但已经有一个线程在访问该临界区了,那么其他线程将被挂起。临界区被释放后,其他线程可继续抢占该临界区。

临界区是一种轻量级的同步机制,与互斥和事件这些内核同步对象相比,临界区是用户态下的对象,即只能在同一进程中实现线程互斥。因无需在用户态和核心态之间切换,所以工作效率比较互斥来说要高很多。
image-20220704104443362

11 进程通信与线程通信

进程通信:

管道、信号、信号量、套接字、消息队列、共享内存

12 进程控制

进程、线程基本原理

多进程和多线程

线程池

进程同步、进程通信、进程调度

13 内存管理

虚拟地址、物理地址

用户空间、内核空间

一、操作系统让进程访问的是虚拟地址空间,而不是物理地址
1.任何程序在编译时都会产生指令和数据,进行地址编号,但是如果地址不连续,就会程序运行不起来,编译器的地址管理比较麻烦(无法动态的获知物理空间的使用情况,也就无法为数据进行编号)

2.进程直接访问物理地址,如果此时有一个野指针,那么在进行操作野指针的时候可能会改变其他空间的数据,造成不安全的事件发生(无法进行内存访问控制)

3.程序运行空间通常需要一块连续的空间,空间利用率低,通过虚拟地址空间映射到物理内存上进行数据存储,可以实现数据在物理空间上离散式存储,提高内存的利用率,并且每个进程都有一份属于自己的连续空间使用

image-20220325135414636

虚拟内存主要是解决内存空间不足的问题,但其实它面对的问题又是内存空间利用率低的问题。

产生这个矛盾的原因在于,进程的创建的时候操作系统会给进程分配内存空间,但是进程在程序运行过程中,在每一个时间段内,只会用到程序中的一部分数据,而存储的其他数据实际上被没有被用到。但是还是要给每个进程划分一块内存,并且确保这一块内存能把进程在执行过程中的所有数据都存储起来,所有内存又显得很不足。

这种内存分配的方式是一种对外抠门,对内大方的方式,在分配时,尽量多拿,拿到之后用不用是另一回事,正就像某些社会团体的表现了。

虚拟内存就是解决这个问题的,它首先认识到每个进程分配到内存没有被有效利用的问题,第二,它有效利用了硬盘这个存储空间大,但是读写性能较差的存储设备。

虚拟内存的方案是:把内存分为很多大小相同,空间连续的页框,然后给每一个进程分配一个4G的逻辑地址,逻辑地址也是按照页去划分的,页和页框之间存在一个映射关系,但是每一个进程中,只有一部分的页对应的页框中存储了对应的数据,大多数的页框其实并不会存储映射的数据,这些数据交给硬盘来存储,当发生缺页中断时,从硬盘中读取相应的数据,并按照一个策略将页框中每一个数据与之置换(页面置换),同时会修改对应的页表的标志位。

这时候4G的内存被充分的利用了起来,并且对于每个进程来说,自己似乎独占了4G的内存,双赢的方案,膜拜计算机的前辈吧。

14 死锁、自旋锁

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件;
  • 持有并等待条件;
  • 不可剥夺条件;
  • 环路等待条件;

互斥锁、自旋锁、读写锁、乐观锁、悲观锁

加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。

  • 互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
  • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;

自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。

读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。

所以,读写锁适用于能明确区分读操作和写操作的场景

乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

15 进程调度

image-20220704104626798

16 线程和进程区别 多线程和多进程

进程是资源分配和调度的一个独立单位;而线程是进程的一个实体,是CPU调度和分配的基本单位。

同一个进程中的多个线程的内存资源是共享的,各线程都可以改变进程中的变量。因此在执行多线程运算的时候要注意执行顺序。

进程与线程最大的区别在于上下文切换过程中,线程不用切换虚拟内存,因为同一个进程内的线程都是共享虚拟内存空间的,线程就单这一点不用切换,就相比进程上下文切换的性能开销减少了很多。

多进程和多线程的主要区别是:线程是进程的子集(部分),一个进程可能由多个线程组成。多进程的数据是分开的、共享复杂,需要用IPC;但同步简单。多线程共享进程数据,共享简单;但同步复杂。

17 什么是上下文切换

进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。

上下文切换指的是CPU从一个进程(线程)切换到另一个进程(线程)。

进程是正在执行的一个程序的实例,在Linux中,线程可以算作轻量级进程,线程可以并发执行,并且同一进程创建的线程可以共享同一片地址空间及其它资源,即该进程的进程地址空间及属于该进程的其它资源。

进程和线程加以区分

18 内存池

内存池(Memory Pool)是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请内存,这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。

内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。

三. 计算机网络

01 介绍一下NAT的种类

02 讲一下VLAN作用

03 VLAN 0作用

04 通过交换机广播转发MAC,MAC有做绑定么?

05 三次握手、四次挥手

06 http和https

07 http1.1和http2

08 tcp和udp的区别

09 长连接和短链接

定义

短连接:例如普通的web请求,在三次握手之后建立连接,发送数据包并得到服务器返回的结果之后,通过客户端和服务端的四次握手进行关闭断开。

长连接:区别于短连接,由于三次握手链接及四次握手断开,在请求频繁的情况下,链接请求和断开请求的开销较大,影响效率。采用长连接方式,执行三次握手链接后,不断开链接,保持客户端和服务端通信,直到服务器超时自动断开链接,或者客户端主动断开链接。

适用场景

短连接:适用于网页浏览等数据刷新频度较低的场景。

长连接:适用于客户端和服务端通信频繁的场景,例如聊天室,实时游戏等。

四. 项目

五. 平台工具

工具一 git

工具二 shell

工具三 cmake

六. Linux常用指令

01 cp指令

02 mv指令

03 ls指令

04 pwd指令

05 mount指令

06 vim指令

07 cd指令

08 rm指令

09 mkdir指令

10 touch指令

11 ps指令

12 kill指令

13 su指令

14 top指令

15 tar指令

16 ifconfig指令

17 telnet指令

18 ping指令

19 halt指令

20 rebot指令

21 ssh指令

七. 自我介绍

八. 激光雷达、摄像头

这篇关于momenta面经总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/464991

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

git使用的说明总结

Git使用说明 下载安装(下载地址) macOS: Git - Downloading macOS Windows: Git - Downloading Windows Linux/Unix: Git (git-scm.com) 创建新仓库 本地创建新仓库:创建新文件夹,进入文件夹目录,执行指令 git init ,用以创建新的git 克隆仓库 执行指令用以创建一个本地仓库的

二分最大匹配总结

HDU 2444  黑白染色 ,二分图判定 const int maxn = 208 ;vector<int> g[maxn] ;int n ;bool vis[maxn] ;int match[maxn] ;;int color[maxn] ;int setcolor(int u , int c){color[u] = c ;for(vector<int>::iter

整数Hash散列总结

方法:    step1  :线性探测  step2 散列   当 h(k)位置已经存储有元素的时候,依次探查(h(k)+i) mod S, i=1,2,3…,直到找到空的存储单元为止。其中,S为 数组长度。 HDU 1496   a*x1^2+b*x2^2+c*x3^2+d*x4^2=0 。 x在 [-100,100] 解的个数  const int MaxN = 3000

状态dp总结

zoj 3631  N 个数中选若干数和(只能选一次)<=M 的最大值 const int Max_N = 38 ;int a[1<<16] , b[1<<16] , x[Max_N] , e[Max_N] ;void GetNum(int g[] , int n , int s[] , int &m){ int i , j , t ;m = 0 ;for(i = 0 ;

go基础知识归纳总结

无缓冲的 channel 和有缓冲的 channel 的区别? 在 Go 语言中,channel 是用来在 goroutines 之间传递数据的主要机制。它们有两种类型:无缓冲的 channel 和有缓冲的 channel。 无缓冲的 channel 行为:无缓冲的 channel 是一种同步的通信方式,发送和接收必须同时发生。如果一个 goroutine 试图通过无缓冲 channel

9.8javaweb项目总结

1.主界面用户信息显示 登录成功后,将用户信息存储在记录在 localStorage中,然后进入界面之前通过js来渲染主界面 存储用户信息 将用户信息渲染在主界面上,并且头像设置跳转,到个人资料界面 这里数据库中还没有设置相关信息 2.模糊查找 检测输入框是否有变更,有的话调用方法,进行查找 发送检测请求,然后接收的时候设置最多显示四个类似的搜索结果

java面试常见问题之Hibernate总结

1  Hibernate的检索方式 Ø  导航对象图检索(根据已经加载的对象,导航到其他对象。) Ø  OID检索(按照对象的OID来检索对象。) Ø  HQL检索(使用面向对象的HQL查询语言。) Ø  QBC检索(使用QBC(Qurey By Criteria)API来检索对象。 QBC/QBE离线/在线) Ø  本地SQL检索(使用本地数据库的SQL查询语句。) 包括Hibern

暑期学习总结

iOS学习 前言无限轮播图换头像网络请求按钮的configuration属性总结 前言 经过暑期培训,完成了五个项目的仿写,在项目中将零散的内容经过实践学习,有了不少收获,因此来总结一下比较重要的内容。 无限轮播图 这是写项目的第一个难点,在很多项目中都有使用,越写越熟练。 原理为制造两个假页,在首和尾分别制作最后一页和第一页的假页,当移动到假页时,使用取消动画的方式跳到