面经+八股文及部分答案(未完结)

文章目录

  • 八股文
        • 项目流程
        • 为什么用线程池
        • 怎么创建线程池
    • 商汤科技
      • 1.讲讲你了解的虚函数
      • 2.你知道malloc、free和new、delete的区别吗
      • 3.讲讲右值引用
      • 请你详细介绍一下C++11中的可变参数模板、右值引用和lambda这几个新特性。
      • 4.讲讲你了解的C++的特性(auto、智能指针)
      • 5.讲一下预编译、编译、汇编、链接
      • 6.讲一下静态库和动态库的区别
      • 7.问一下各[排序]()[算法]()相关
      • 8.做题,n个数如何得到最小的k个,时间和空间复杂度是多少
      • 9.手写快排
    • 1、OSI 的七层模型分别是?各自的功能是什么?
      • #简要概括
      • #说明
      • #总结
    • 深信服
      • 一面
      • 1.讲讲进程和线程的区别
      • 2.线程间同步与互斥方法
      • 3.进程间同步与互斥方法
      • 5.问知不知道多少并发量,epoll才会比select更有效。
      • **6.详细讲讲三次握手过程、状态变化。**
        • 第一种回答
        • #第二种回答
        • 为什么需要三次握手,两次不行吗?
      • **7.四次挥手**
        • 第一种回答
        • #第二种回答
        • 挥手为什么需要四次?
        • #第一种回答
        • #第二种回答
        • 2MSL等待状态?
        • 四次挥手释放连接时,等待2MSL的意义?
        • #两个理由
        • 为什么TIME_WAIT状态需要经过2MSL才能返回到CLOSE状态?
      • #第一种回答
        • #第二种回答
      • 8.数据结构讲一下哈希,如何设计一个可以自动扩容的哈希?我讲用C++的vector
      • 9.讲一下vector,vector如何扩容
      • 10.vector扩容会时间复杂度比较高,有没有边复制边扩容的方法。
    • 二面
      • **1.TCP和UDP的区别**
      • 2.了解http吗?
      • 3.讲讲http和https的区别,https是基于什么协议的?
      • 4.讲一下OSI七层模型
      • 5.讲一下三次握手
      • 5.了解sizeof和strlen吗
      • 6.sizeof(char*)结果是什么?
      • 7.c语言如何在C++文件里使用
      • 8.C++内存分布情况
      • 9.C++map底层数据结构、unordered_map底层数据结构
      • 10.linux常见命令有哪些
      • 11.查看内存用的哪个命令?
      • 12.翻转字符串(“hello world”如何变成“world hello”),讲思路,如何优化?
    • 字节跳动
      • 用户态线程是什么
      • 一个进程能有无数个线程,无数个协程吗
      • 为什么说线程轻量,进程哪里重量级吗
      • 为什么用epoll
      • select、poll和epoll区别
      • 除了reactor还有别的模型吗
      • 实现了http的哪些方法
      • 如何处理http请求
      • http版本
      • time_wait和close_wait有什么区别
      • socket是什么
      • 数据库索引数据结构
      • 数据库主从表相关?
      • 数据库事务隔离级别
    • 腾讯测试
      • 你的项目是支持长连接还是短连接?
      • 你是如何处理HTTP的长连接的?
      • UDP如何实现可靠连接?
      • 那个信号是如何发送给进程的?
      • 进程又是如何接收到信号的?
      • 共享内存是如何实现的?
      • 共享内存是连续的吗?
      • linux查询端口
    • 快手
    • 飞书提前批四轮技术面面经
      • **1、一面**
      • **2、二面**
      • **3、三面**
      • **4、交叉面**
      • 字节飞书测开暑期实习面经
      • 字节跳动-幸福里-测开实习 一面面经
        • 8、new / delete 与 malloc / free的异同
        • #9、new和delete是如何实现的?
        • #10、malloc和new的区别?
        • #11、既然有了malloc/free,C++中为什么还需要new/delete呢?直接用malloc/free不好吗?
        • #12、被free回收的内存是立即返还给操作系统吗?
        • 野指针和悬空指针
        • static
        • 指针和const的用法
        • new和malloc的区别
        • delete p、delete [] p、allocator都有什么作用?
        • malloc与free的实现原理?
        • 类成员初始化方式?
        • 介绍面向对象的三大特性,并且举例说明
        • 初始化列表更快

八股文

项目流程

本项目是基于Linux的轻量级多线程web服务器,利用IO多路复用,实现了一个简单的HTTP服务器,可以同时监听多个请求,使用线程池处理请求,使用模拟proactor的模式,主线程负责监听,监听有事件后,从socket中循环读取数据,然后讲监听到的数据封装成请求对象放入请求队列,睡眠在请求队列上的工作线程被唤醒进行处理,使用有限状态机解析HTTP请求报文,并且使用双序双向链表来管理定时器。

为什么用线程池

为每个请求创建线程花费在创建和销毁线程上的时间、消耗的系统资源都要比花在处理用户请求的时间和资源更多

怎么创建线程池

主线程是异步线程,负责监听文件描述符,接收socket新连接,若当前监听的socket发生了读写事件,然后将任务插入到请求队列,工作线程从请求队列中取出任务,完成读写数据的请求。

临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是 同时访问同一共享资源的其他线程不应终端该片段的执行。

**线程同步:**即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进 行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处 于等待状态。

商汤科技

C++后台开发

自我介绍,然后先是问了一下个人的情况,
然后主要问了一些C++的问题,

1.讲讲你了解的虚函数

虚函数就是在基类中被关键字virtual说明,并在派生类中重新定义的函数。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

在一个派生类中重新定义基类的虚函数是函数重载的另一种形式,但它不同于一般的函数重载。当普通的函数重载时,其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同。但是,当重载一个虚函数时,也就是说在**派生类中重新定义虚函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。**如果仅仅返回类型不同,其余均相同,系统会给出错误信息;若仅仅函数名相同,而参数的个数、类型或顺序不同,系统将它作为普通的函数重载,这时虚函数的特性将丢失。

2.你知道malloc、free和new、delete的区别吗

相同点

  • 都可用于内存的动态申请和释放

不同点

  • 前者是C++运算符,后者是C/C++语言标准库函数
  • new自动计算要分配的空间大小,malloc需要手工计算
  • new是类型安全的,malloc不是。例如:
int *p = new float[2]; //编译错误
int *p = (int*)malloc(2 * sizeof(double));//编译无错误
 
  • new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后者均没有相关调用
  • 后者需要库文件支持,前者不用
  • new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象

3.讲讲右值引用

请你说说左值、右值、左值引用、右值引用、右值引用的使用场景

解题思路

标准回答 1. 左值 在 C++ 中可以取地址的、有名字的就是左值 int a = 10; // 其中 a 就是左值 

2. 右值 不能取地址的、没有名字的就是右值 int a = 10; // 其中 10 就是右值右值 

3. 左值引用 左值引用就是对一个左值进行引用。传统的 C++ 引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符 const 的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址: int n; int * pt = new int; const int b = 101; int & rn = n; int & rt = *pt; const int & rb = b; const int & rb = 10; 

4. 右值引用 右值引用就是对一个右值进行引用。C++ 11 新增了右值引用(rvalue reference),这种引用可指向右值(即可出现在赋值表达式右边的值),但不能对其应用地址运算符。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如 x + y 等表达式以及返回值的函数(条件是该函数返回的不是引用),右值引用使用 && 声明: int x = 10; int y = 23; int && r1 = 13; int && r2 = x + y; double && r3 = std::sqrt(2.0); 

5. 右值引用的使用场景 右值引用可以实现移动语义、完美转发。


右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面:
1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。

2. 能够更简洁明确地定义泛型函数。


左值和右值的概念:

左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。

右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。


右值引用和左值引用的区别:

1. 左值可以寻址,而右值不可以。

2. 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。

3. 左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)

请你详细介绍一下C++11中的可变参数模板、右值引用和lambda这几个新特性。

可变参数模板:

C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。

例如:

Template
void func(T ... args)
{
cout<<”num is”<

func();//args不含任何参数

func(1);//args包含一个int类型的实参

func(1,2.0)//args包含一个int一个double类型的实参

其中T叫做模板参数包,args叫做函数参数包

省略号作用如下:

1)声明一个包含0到任意个模板参数的参数包

2)在模板定义得右边,可以将参数包展成一个个独立的参数

C++11可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。例如:

#include using namespace std;

// 最终递归函数

void print()

{

cout << “empty” << endl;

}

// 展开函数

template void print(T head, Args... args)
{
cout << head << ","; print(args...);
}
int main()
{
print(1, 2, 3, 4); return 0;
}

参数包Args …在展开的过程中递归调用自己,没调用一次参数包中的参数就会少一个,直到所有参数都展开为止。当没有参数时就会调用非模板函数printf终止递归过程。

右值引用:

C++中,左值通常指可以取地址,有名字的值就是左值,而不能取地址,没有名字的就是右值。而在指C++11中,**右值是由两个概念构成,将亡值和纯右值。**纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值,2、true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等。

C++11中,右值引用就是对一个右值进行引用的类型。由于右值通常不具有名字,所以我们一般只能通过右值表达式获得其引用,比如:

T && a=ReturnRvale();

假设ReturnRvalue()函数返回一个右值,那么上述语句声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。

基于右值引用可以实现转移语义和完美转发新特性。

移动语义:

对于一个包含指针成员变量的类,由于编译器默认的拷贝构造函数都是浅拷贝,所有我们一般需要通过实现深拷贝的拷贝构造函数,为指针成员分配新的内存并进行内容拷贝,从而避免悬挂指针的问题。

但是如下列代码所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Plubbjw1-1662347126747)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191321535_58A744363913E79F3AD5742FA81DCBAE)]

当类HasPtrMem包含一个成员函数GetTemp,其返回值类型是HasPtrMem,如果我们定义了深拷贝的拷贝构造函数,那么在调用该函数时需要调用两次拷贝构造函数。第一次是生成GetTemp函数返回时的临时变量,第二次是将该返回值赋值给main函数中的变量a。与此对应需要调用三次析构函数来释放内存。

而在上述过程中,使用临时变量构造a时会调用拷贝构造函数分配对内存,而临时对象在语句结束后会释放它所使用的堆内存。这样重复申请和释放内存,在申请内存较大时会严重影响性能。因此C++使用移动构造函数,从而保证使用临时对象构造a时不分配内存,从而提高性能。

如下列代码所示,移动构造函数接收一个右值引用作为参数,使用右值引用的参数初始化其指针成员变量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0p3ikscn-1662347126748)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191343421_462F4D51399618549AADC7EEFD2558BD)]

其原理就是使用在构造对象a时,使用h.d来初始化a,然后将临时对象h的成员变量d指向nullptr,从而保证临时变量析构时不会释放对内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AqG7rYGi-1662347126748)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191352653_88EEF7DC9BD18B099C522481527F5CE3)]

完美转发:

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数,即传入转发函数的是左值对象,目标函数就能获得左值对象,转发函数是右值对象,目标函数就能获得右值对象,而不产生额外的开销。

因此转发函数和目标函数参数一般采用引用类型,从而避免拷贝的开销。其次,由于目标函数可能需要能够既接受左值引用,又接受右值引用,所以考虑转发也需要兼容这两种类型。

C++11采用引用折叠的规则,结合新的模板推导规则实现完美转发。其引用折叠规则如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P0lyBXlM-1662347126749)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191363531_0FAB347499C02FF2A53B2E5719DFFDCE)]

因此,我们将转发函数和目标函数的参数都设置为右值引用类型,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WIrko65T-1662347126749)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191372436_751F242F40E83204A02C51EE6E6F59C8)]

当传入一个X类型的左值引用时,转发函数将被实例为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpQIBYQV-1662347126749)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191379108_56A9F9A6063E926522868B216C5723C3)]

经过引用折叠,变为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jAI4BOU9-1662347126749)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191386307_6474E6FC6FFC82F2E36AEDF32C2A9AF7)]

当传入一个X类型的右值引用时,转发函数将被实例为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DSLLi90Z-1662347126750)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191392964_29CEF6AA3EFA9DA07FEFD9FAA986A303)]

经过引用折叠,变为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zcD7EWOw-1662347126750)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191399205_49C2489FE4420DAECF6D97BFE94F06EE)]

除此之外,还可以使用forward()函数来完成左值引用到右值引用的转换:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SCJ7pBku-1662347126751)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191407573_A2019744F42C6BE3527BE38B3D0CD1EE)]

Lambda表达式:

Lambda表达式定义一个匿名函数,并且可以捕获一定范围内的变量,其定义如下:

capturemutable->return-type{statement}

其中,

[capture]:捕获列表,捕获上下文变量以供lambda使用。同时[]是lambda寅初复,编译器根据该符号来判断接下来代码是否是lambda函数。

(Params):参数列表,与普通函数的参数列表一致,如果不需要传递参数,则可以连通括号一起省略。

mutable是修饰符,默认情况下lambda函数总是一个const函数,Mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略。

->return-type:返回类型是返回值类型

{statement}:函数体,内容与普通函数一样,除了可以使用参数之外,还可以使用所捕获的变量。

Lambda表达式与普通函数最大的区别就是其可以通过捕获列表访问一些上下文中的数据。其形式如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QZO0mzeX-1662347126751)(https://uploadfiles.nowcoder.com/images/20190911/826546_1568191417971_1765C38D76453E701E78C4FC12904FBA)]

Lambda的类型被定义为“闭包”的类,其通常用于STL库中,在某些场景下可用于简化仿函数的使用,同时Lambda作为局部函数,也会提高复杂代码的开发加速,轻松在函数内重用代码,无须费心设计接口。

4.讲讲你了解的C++的特性(auto、智能指针)

https://interviewguide.cn/notes/03-hunting_job/02-interview/01-03-01-C++11%E6%96%B0%E6%A0%87%E5%87%86.html

5.讲一下预编译、编译、汇编、链接

预处理阶段:
预处理器(cpp)将所有的#define删除,并且展开所有的宏定义。
处理所有的条件预编译指令,比如#if、#ifdef、#elif、#else、#endif等。
处理#include预编译指令,将被包含的文件直接插入到预编译指令的位置。
删除所有的注释。
添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
保留所有的#pragma编译器指令,因为编译器需要使用它们。
使用gcc -E hello.c -o hello.i命令来进行预处理, 预处理得到的另一个程序通常是以.i作为文件扩展名。
编译阶段:
编译器(ccl)将预处理完的文本文件hello.i进行一系列的词法分析、语法分析、语义分析和优化,翻译成文本文件hello.s,它包含一个汇编语言程序。如下所示

该程序包含函数main的定义,2-7行的每条语句都以一种文本格式描述了一条低级机器语言指令。

汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。

编译过程可分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化。

词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。
语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
目标代码生成:代码生成器(Code Generator).
目标代码优化:目标代码优化器(Target Code Optimizer)。
汇编阶段:
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中,hello.o是一个二进制文件。

链接阶段:
hello程序调用了printf函数,它存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。连接器(ld)就负责处理这种合并。结果就得到了hello文件,它是一个可执行目标文件(或者称为可执行文件),可以被加载到内存中,由系统执行。(链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件)。

6.讲一下静态库和动态库的区别

得分点 命名方式、链接、内存、更新 标准回答 静态库和动态库的区别:

1.命令方式不同 - 静态库命名 Linux : libxxx.a lib : 前缀(固定) xxx : 库的名字,自己起 .a : 后缀(固定) Windows : libxxx.lib - 动态库命名 Linux : libxxx.so lib : 前缀(固定) xxx : 库的名字,自己起 .so : 后缀(固定) Windows : libxxx.dll

2.链接时间和方式不同 - 静态库的链接是将整个函数库的所有数据在编译时都整合进了目标代码 - 动态库的链接是程序执行到哪个函数链接哪个函数的库

静态库和动态库的优缺点:

1.静态库优缺点

  • 优点:发布程序时无需提供静态库,移植方便,运行速度相对快些
  • 缺点:静态链接生成的可执行文件体积较大,消耗内存,如果所使用的静态库发生更新改变,程序必须重新编译,更新麻烦。

2.动态库优缺点

  • 优点:更加节省内存并减少页面交换,动态库改变并不影响使用的程序,动态函数库升级比较方便
  • 缺点:发布程序时需要提供动态库

7.问一下各排序算法相关

8.做题,n个数如何得到最小的k个,时间和空间复杂度是多少

然后让写代码,然后根据你写的代码,问你还有没有更好的解决办法。

9.手写快排

反问环节。

1、OSI 的七层模型分别是?各自的功能是什么?

#简要概括

  • 物理层:底层数据传输,如网线;网卡标准。
  • 数据链路层:定义数据的基本格式,如何传输,如何标识;如网卡MAC地址。
  • 网络层:定义IP编址,定义路由功能;如不同设备的数据转发。
  • 传输层:端到端传输数据的基本功能;如 TCP、UDP。
  • 会话层:控制应用程序之间会话能力;如不同软件数据分发给不同软件。
  • 表示层:数据格式标识,基本压缩加密功能。
  • 应用层:各种应用软件,包括 Web 应用。

#说明

  • 在四层,既传输层数据被称作(Segments);
  • 三层网络层数据被称做(Packages);
  • 二层数据链路层时数据被称为(Frames);
  • 一层物理层时数据被称为比特流(Bits)。

#总结

  • 网络七层模型是一个标准,而非实现。
  • 网络四层模型是一个实现的应用模型。
  • 网络四层模型由七层模型简化合并而来。

深信服

全怼项目,深挖,并且从中问你相关的知识。

问题大部分都是 操作系统和计算机网络相关。

一面

问题:

怼项目怼了十几分钟,挖细节。

1.讲讲进程和线程的区别

直接问到内核中fork,pthread_create,还有两者的区别。

  • 调度:线程是调度的基本单位(PC,状态码,通用寄存器,线程栈及栈指针);进程是拥有资源的基本单位(打开文件,堆,静态区,代码段等)。
  • 并发性:一个进程内多个线程可以并发(最好和CPU核数相等);多个进程可以并发。
  • 拥有资源:线程不拥有系统资源,但一个进程的多个线程可以共享隶属进程的资源;进程是拥有资源的独立单位。
  • 系统开销:线程创建销毁只需要处理PC值,状态码,通用寄存器值,线程栈及栈指针即可;进程创建和销毁需要重新分配及销毁task_struct结构。

◼ 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用 一些进程间通信方式,在进程间进行信息交换。

◼ 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如 内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销 依然不菲。

◼ 线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。

◼ 创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采 用写时复制来复制内存,也无需复制页表。

(1)调度:
        在传统的操作系统中,CPU调度和分派的基本单位是进程。而在引入线程的操作系统中,则把线程作为CPU调度和分派的基本单位,进程则作为资源拥有的基本单位,从而使传统进程的两个属性分开,线程编程轻装运行,这样可以显著地提高系统的并发性。同一进程中线程的切换不会引起进程切换,从而避免了昂贵的系统调用,但是在由一个进程中的线程切换到另一进程中的线程,依然会引起进程切换。
 
(2)并发性:
      在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间也可以并发执行,因而使操作系统具有更好的并发性,从而更有效地提高系统资源和系统的吞吐量。例如,在一个为引入线程的单CPU操作系统中,若仅设置一个文件服务进程,当它由于某种原因被封锁时,便没有其他的文件服务进程来提供服务。在引入线程的操作系统中,可以在一个文件服务进程设置多个服务线程。当第一个线程等待时,文件服务进程中的第二个线程可以继续运行;当第二个线程封锁时,第三个线程可以继续执行,从而显著地提高了文件服务的质量以及系统的吞吐量。


(3)拥有资源:
      不论是引入了线程的操作系统,还是传统的操作系统,进程都是拥有系统资源的一个独立单位,他可以拥有自己的资源。一般地说,线程自己不能拥有资源(也有一点必不可少的资源),但它可以访问其隶属进程的资源,亦即一个进程的代码段、数据段以及系统资源(如已打开的文件、I/O设备等),可供同一个进程的其他所有线程共享。


(4)独立性:
        在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多。这是因为为防止进程之间彼此干扰和破坏,每个进程都拥有一个独立的地址空间和其它资源,除了共享全局变量外,不允许其它进程的访问。但是同一进程中的不同线程往往是为了提高并发性以及进行相互之间的合作而创建的,它们共享进程的内存地址空间和资源,如每个线程都可以访问它们所属进程地址空间中的所有地址,如一个线程的堆栈可以被其它线程读、写,甚至完全清除。


(5)系统开销:
       由于在创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。因此,操作系统为此所付出的开销将显著地大于在创建或撤消线程时的开销。类似的,在进程切换时,涉及到整个当前进程CPU环境的保存环境的设置以及新被调度运行的CPU环境的设置,而线程切换只需保存和设置少量的寄存器的内容,并不涉及存储器管理方面的操作,可见,进程切换的开销也远大于线程切换的开销。此外,由于同一进程中的多个线程具有相同的地址空间,致使他们之间的同步和通信的实现也变得比较容易。在有的系统中,现成的切换、同步、和通信都无需操作系统内核的干预。

2.线程间同步与互斥方法

Mutex、条件变量、信号量

线程间的同步方式包括互斥锁、信号量、条件变量、读写锁

  1. 互斥锁:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
  2. 信号量:计数器,允许多个线程同时访问同一个资源。
  3. 条件变量:通过条件变量通知操作的方式来保持多线程同步。
  4. 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。

3.进程间同步与互斥方法

说说进程同步的方式?

参考回答

  1. 信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。P操作(递减操作)可以用于阻塞一个进程,V操作(增加操作)可以用于解除阻塞一个进程。

  2. 管道:一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。

  3. 消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。

  4. 临界区

对临界资源进行访问的那段代码称为临界区。

为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。

// entry section
// critical section;
// exit section

#2. 同步与互斥

  • 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
  • 互斥:多个进程在同一时刻只有一个进程能进入临界区。

#3. 信号量

信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。

  • down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
  • up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。

down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。

如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

typedef int semaphore;
semaphore mutex = 1;
void P1() {
    down(&mutex);
    // 临界区
    up(&mutex);
}

void P2() {
    down(&mutex);
    // 临界区
    up(&mutex);
}

使用信号量实现生产者-消费者问题

问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。

因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。

为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。

其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。

注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。

消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
    while(TRUE) {
        int item = produce_item();
        down(&empty);
        down(&mutex);
        insert_item(item);
        up(&mutex);
        up(&full);
    }
}

void consumer() {
    while(TRUE) {
        down(&full);
        down(&mutex);
        int item = remove_item();
        consume_item(item);
        up(&mutex);
        up(&empty);
    }
}

#4. 管程

使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。

c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。

monitor ProducerConsumer
    integer i;
    condition c;

    procedure insert();
    begin
        // ...
    end;

    procedure remove();
    begin
        // ...
    end;
end monitor;

管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。

管程引入了 条件变量 以及相关的操作:wait()signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。

使用管程实现生产者-消费者问题

// 管程
monitor ProducerConsumer
    condition full, empty;
    integer count := 0;
    condition c;

    procedure insert(item: integer);
    begin
        if count = N then wait(full);
        insert_item(item);
        count := count + 1;
        if count = 1 then signal(empty);
    end;

    function remove: integer;
    begin
        if count = 0 then wait(empty);
        remove = remove_item;
        count := count - 1;
        if count = N -1 then signal(full);
    end;
end monitor;

// 生产者客户端
procedure producer
begin
    while true do
    begin
        item = produce_item;
        ProducerConsumer.insert(item);
    end
end;

// 消费者客户端
procedure consumer
begin
    while true do
    begin
        item = ProducerConsumer.remove;
        consume_item(item);
    end
end;

4.问为什么用epoll

(1)epoll的优点:epoll 是一种更加高效的 IO 复用技术

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

epoll 的使用步骤及原理如下:

1)调用epoll_create()会在内核中创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数。

在这个结构体中有 2 个比较重要的数据成员:一个是需要检测的文件描述符的信息 struct_root rbr (红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发生改变的文件描述符信息 (双向链表);

2)调用epoll_ctl() 用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除

3)调用epoll_wait() 可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。

epoll 的两种工作模式:

a)LT 模式(水平触发)LT(Level - Triggered)是缺省的工作方式,并且同时支持 Block 和 Nonblock Socket。 在这种做法中,内核检测到一个文件描述符就绪了,然后应用程序可以对这个就绪的 fd 进行 IO 操作。应用程序可以不立即处理该事件,如果不作任何操作,内核还是会继续通知。

b)ET 模式(边缘触发) ET(Edge - Triggered)是高速工作方式,只支持 Nonblock socket。 在这种模式下,epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件。必须要一次性将数据读取完,使用非阻塞I/O,读取到出现EAGAIN。但是,如果一直不对这个 fd 进行 IO 操作(从而导致它再次变成未就绪 ),内核不会发送更多的通知(only once)。

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

3)EPOLLONESHOT

一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket

我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件

(2)I/O 多路复用是一种使得程序能同时监听多个文件描述符的技术,从而提高程序的性能。 Linux 下实现 I/O 复用的系统调用主要有select、poll 和 epoll。

(3)select/poll/epoll区别

1)调用函数

select和poll都是一个函数,epoll是一组函数

2)文件描述符数量

select通过线性表描述文件描述符集合,文件描述符有上限(与系统内存关系很大),32位机默认是1024个,64位机默认是2048。

poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目

epoll通过红黑树描述,最大可以打开文件的数目

3)将文件描述符从用户传给内核

select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝

epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上

4)内核判断就绪的文件描述符

select和poll通过线性遍历文件描述符集合,判断哪个文件描述符上有事件发生

epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。

epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list

5)应用程序索引就绪文件描述符

select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历

epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可

6)工作模式

select和poll都只能工作在相对低效的LT模式下

epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。

7)应用场景

当监测的fd数目较小,且全部fd都比较活跃,建议使用select或者poll

当监测的fd数目非常大,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能

条件编译:

#ifdef 标识符

程序段1

#else

程序段2

5.问知不知道多少并发量,epoll才会比select更有效。

6.详细讲讲三次握手过程、状态变化。

第一种回答

刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态,进行三次握手:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SEND 状态。

    首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

    在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。

  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

    确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。

在socket编程中,客户端执行connect()时,将触发三次握手。

#第二种回答

  • 初始状态:客户端处于 closed(关闭)状态,服务器处于 listen(监听) 状态。
  • 第一次握手:客户端发送请求报文将 SYN = 1同步序列号和初始化序列号seq = x发送给服务端,发送完之后客户端处于SYN_Send状态。(验证了客户端的发送能力和服务端的接收能力)
  • 第二次握手:服务端受到 SYN 请求报文之后,如果同意连接,会以自己的同步序列号SYN(服务端) = 1、初始化序列号 seq = y和确认序列号(期望下次收到的数据包)ack = x+ 1 以及确认号ACK = 1报文作为应答,服务器为SYN_Receive状态。(问题来了,两次握手之后,站在客户端角度上思考:我发送和接收都ok,服务端的发送和接收也都ok。但是站在服务端的角度思考:哎呀,我服务端接收ok,但是我不清楚我的发送ok不ok呀,而且我还不知道你接受能力如何呢?所以老哥,你需要给我三次握手来传个话告诉我一声。你要是不告诉我,万一我认为你跑了,然后我可能出于安全性的考虑继续给你发一次,看看你回不回我。)
  • 第三次握手: 客户端接收到服务端的 SYN + ACK之后,知道可以下次可以发送了下一序列的数据包了,然后发送同步序列号 ack = y + 1和数据包的序列号 seq = x + 1以及确认号ACK = 1确认包作为应答,客户端转为established状态。(分别站在双方的角度上思考,各自ok)

为什么需要三次握手,两次不行吗?

弄清这个问题,我们需要先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。

  • 第一次握手:客户端发送网络包,服务端收到了。 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

试想如果是用两次握手,则会出现下面这种情况:

如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。

7.四次挥手

第一种回答

刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。 即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。 即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的确认号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

在socket编程中,任何一方执行close()操作即可产生挥手操作。

#第二种回答

  • 初始化状态:客户端和服务端都在连接状态,接下来开始进行四次分手断开连接操作。
  • 第一次分手:第一次分手无论是客户端还是服务端都可以发起,因为 TCP 是全双工的。

假如客户端发送的数据已经发送完毕,发送FIN = 1 告诉服务端,客户端所有数据已经全发完了服务端你可以关闭接收了,但是如果你们服务端有数据要发给客户端,客户端照样可以接收的。此时客户端处于FIN = 1等待服务端确认释放连接状态。

  • 第二次分手:服务端接收到客户端的释放请求连接之后,知道客户端没有数据要发给自己了然后服务端发送ACK = 1告诉客户端收到你发给我的信息,此时服务端处于 CLOSE_WAIT 等待关闭状态。(服务端先回应给客户端一声,我知道了,但服务端的发送数据能力即将等待关闭,于是接下来第三次就来了。)
  • 第三次分手:此时服务端向客户端把所有的数据发送完了,然后发送一个FIN = 1,用于告诉客户端,服务端的所有数据发送完毕客户端你也可以关闭接收数据连接了。此时服务端状态处于LAST_ACK状态,来等待确认客户端是否收到了自己的请求。(服务端等客户端回复是否收到呢,不收到的话,服务端不知道客户端是不是挂掉了还是咋回事呢,所以服务端不敢关闭自己的接收能力,于是第四次就来了。)
  • 第四次分手:此时如果客户端收到了服务端发送完的信息之后,就发送ACK = 1,告诉服务端,客户端已经收到了你的信息。有一个 2 MSL 的延迟等待

感谢网友勘误,https://github.com/forthespada/InterviewGuide/issues/25 - 2022.02.22

挥手为什么需要四次?

#第一种回答

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

#第二种回答

任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。

2MSL等待状态?

TIME_WAIT状态也成为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。

对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。

这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。

四次挥手释放连接时,等待2MSL的意义?

MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。

#两个理由

  1. 保证客户端发送的最后一个ACK报文段能够到达服务端。 这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认,服务端超时重传FIN+ACK报文段,而客户端能在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态,若客户端在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到服务端重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到CLOSED状态。
  2. 防止“已失效的连接请求报文段”出现在本连接中。 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。

为什么TIME_WAIT状态需要经过2MSL才能返回到CLOSE状态?

#第一种回答

理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文

#第二种回答

对应这样一种情况,最后客户端发送的ACK = 1给服务端的过程中丢失了,服务端没收到,服务端怎么认为的?我已经发送完数据了,怎么客户端没回应我?是不是中途丢失了?然后服务端再次发起断开连接的请求,一个来回就是2MSL。

客户端给服务端发送的ACK = 1丢失,服务端等待 1MSL没收到然后重新发送消息需要1MSL。如果再次接收到服务端的消息,则重启2MSL计时器发送确认请求。客户端只需等待2MSL,如果没有再次收到服务端的消息,就说明服务端已经接收到自己确认消息;此时双方都关闭的连接,TCP 四次分手完毕

8.数据结构讲一下哈希,如何设计一个可以自动扩容的哈希?我讲用C++的vector

9.讲一下vector,vector如何扩容

请你说说 vector 的扩容机制,扩容以后,它的内存地址会变化吗?

解题思路

得分点 申请空间、拷贝数据、释放旧空间 标准回答 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步: 1. 完全弃用现有的内存空间,重新申请更大的内存空间; 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 3. 最后将旧的内存空间释放。 因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。vector 扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。

10.vector扩容会时间复杂度比较高,有没有边复制边扩容的方法。

11.了解redis吗,redis有上面的方法,可以看看。

12.有什么爱好吗?

反问环节。

给我问懵了,以为凉凉了。

挂了电话几分钟,HR打电话过来说明天二面。。。。

二面

二面过去一天以为挂了,结果第二天晚上收到通过。

这次的话是扣简历细节,写上的技能点都给你问了个遍。

问题:

1.TCP和UDP的区别

1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付

3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的

UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)

4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信

5、TCP首部开销20字节;UDP的首部开销小,只有8个字节

6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

7、UDP是面向报文的,发送方的UDP对应用层交下来的报文,不合并,不拆分,只是在其上面加上首部后就交给了下面的网络层,论应用层交给UDP多长的报文,它统统发送,一次发送一个。而对接收方,接到后直接去除首部,交给上面的应用层就完成任务了。因此,它需要应用层控制报文的大小

TCP是面向字节流的,它把上面应用层交下来的数据看成无结构的字节流会发送,可以想象成流水形式的,发送方TCP会将数据放入“蓄水池”(缓存区),等到可以发送的时候就发送,不能发送就等着TCP会根据当前网络的拥塞状态来确定每个报文段的大小

2.了解http吗?

3.讲讲http和https的区别,https是基于什么协议的?

(1)HTTP 的不足

窃听风险: 通信使用明文(不加密),内容可能会被窃听;

冒充风险: 不验证通信方的身份,因此有可能遭遇伪装;

篡改风险: 无法证明报文的完整性,所以有可能已遭篡改;

(2)HTTPS 的缺点

HTTPS 协议多次握手,导致页面的加载时间延长近 50%;

HTTPS 连接缓存不如 HTTP 高效,会增加数据开销和功耗;

SSL 涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大;

(3)区别

端口不同:HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443;

资源消耗:HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 ssl 加密传输协议,需要消耗更多的 CPU 和内存资源

开销:HTTPS 协议需要到 CA 申请证书,一般免费证书很少,需要交费;

安全性:HTTP 的连接很简单,是无状态的;HTTPS 协议是由 TLS+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全

补充:HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。

4.讲一下OSI七层模型

5.讲一下三次握手

5.了解sizeof和strlen吗

首先strlen和sizeof之间没有任何关联。
sizeof是单目运算符,主要是用来计算变量、数组等在内存中所占空间的大小(单位是字节)。
strlen是库函数,是用来计算字符串的长度的,只能用于字符串,且需要引用的头文件。

6.sizeof(char*)结果是什么?

64位是8

7.c语言如何在C++文件里使用

extern"C"的用法

为了能够正确的在C++代码中调用C语言的代码:在程序中加上extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++;

哪些情况下使用extern “C”:

(1)C++代码中调用C语言代码;

(2)在C++中的头文件中使用;

(3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;

举个例子,C++中调用C代码:

#ifndef __MY_HANDLE_H__
#define __MY_HANDLE_H__

extern "C"{
    typedef unsigned int result_t;
    typedef void* my_handle_t;
    
    my_handle_t create_handle(const char* name);
    result_t operate_on_handle(my_handle_t handle);
    void close_handle(my_handle_t handle);
}
 
        @阿秀: 代码已成功复制到剪贴板
    

综上,总结出使用方法**,在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。**所以使用extern "C"全部都放在于cpp程序相关文件或其头文件中。

总结出如下形式:

(1)C++调用C函数:

//xx.h
extern int add(...)

//xx.c
int add(){
    
}

//xx.cpp
extern "C" {
    #include "xx.h"
}
 
        @阿秀: 代码已成功复制到剪贴板
    

(2)C调用C++函数

//xx.h
extern "C"{
    int add();
}
//xx.cpp
int add(){    
}
//xx.c
extern int add();
 

8.C++内存分布情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KArOANkP-1662347126751)(https://uploadfiles.nowcoder.com/images/20181217/308571_1545017390911_DF773F426C6422406BE9A58D11FC23B0)]

32bitCPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中03G是用户态空间,34G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:

各个段说明如下:

3G用户空间和1G内核空间

静态区域:

text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

data segment(数据段):存储程序中已初始化的全局变量和静态变量

bss segment:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0

动态区域:

heap(堆): 当进程未调用malloc时是没有堆段的,只有调用malloc时采用分配一个堆,并且在程序运行过程中可以动态增加堆大小(移动break指针),从低地址向高地址增长。分配小内存时使用该区域。 堆的起始地址由mm_struct结构体中的start_brk标识,结束地址由brk标识。

memory mapping segment(映射区):存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)

stack(栈):使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定。

得分点 代码区、未初始化数据区(BSS)、已初始化数据区(DATA)、栈区(Stack)、堆区(Heap)

标准回答 C++ 的内存分区主要有:代码区、未初始化数据区(BSS)、已初始化数据区(DATA)、栈区(Stack)、堆区(Heap)

  1. 代码区 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
  2. 未初始化数据区 加载的是可执行文件 BSS 段,位置可以分开也可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
  3. 已初始化数据区(全局初始化数据区/静态数据区) 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
  4. 栈区 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
  5. 堆区 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序,用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

9.C++map底层数据结构、unordered_map底层数据结构

map/set和multimap/multiset都是基于红黑树进行某些接口的进行二次改造形成的具有某些特定功能的容器,具有红黑树自动排序的特性。

unordered_map则是基于哈希表实现的,并不具备自动排序的特性

10.linux常见命令有哪些

太基础的面试官根本不会问啦。什么ls\mv\cd\cp\rm,玩过Linux都会吧,给出6个在面试中容易被问到的也是生产环境中经常用到的吧~

\1. find:查找文件

\2. grep:查找文件中的行,常用来在日志中找信息

\3. awk:相当强大的工具,用来统计日志信息

\4. sed:按行编辑文件,常用来处理配置文件

\5. netstat:查看网络信息

\6. ps:用来查看进程状态

这些命令都是在生产环境中使用非常频繁又非常重要的,每一个都相当强大,参数很多,有些还支持正则表达式

cd:切换当前目录

ls:查看当前文件与目录

grep:通常与管道命令一起使用,用于对一些命令的输出进行筛选加工

cp:复制文件或文件夹 mv:移动文件或文件夹

rm:删除文件或文件夹

ps:查看进程情况 kill:向进程发送信号

tar:对文件进行打包

cat:查看文件内容

top:查看操作系统的信息,如进程、CPU占用率、内存信息等(实时)

free:查看内存使用情况

pwd:显示当前工作目录

11.查看内存用的哪个命令?

top

free

12.翻转字符串(“hello world”如何变成“world hello”),讲思路,如何优化?

即给出时间复杂度O(n),空间复杂度O(1),当时想出来了但是没表达清楚,然后面试官以为我思路错了,我也以为我思路错了,,尴尬了五分钟。。

最后面试官给出左旋字符串的做法,我说我懂了,但是我没理解细节。

他接着问,实现左旋字符串需要几个指针,当时就乱了。

没有反问环节,掐点结束。。。

总体来说不难,但是答得不好。

剩下HR面了。

#面经##深信服##深信服面经#

字节跳动

自我介绍

深挖项目:

进程、线程、协程的区别

用户态线程是什么

所谓用户态线程就是把内核态的线程在用户态实现了一遍而已,目的是更轻量化(更少的内存占用、更少的隔离、更快的调度)和更高的可控性(可以自己控制调度器)。用户态所有东西内核态都「看得见」,只是对于内核而言「用户态线程」只是一堆内存数据而已。

线程并不是一个具体的名词,在不同语境下可以指代不同的东西,但都代表多个任务共享同一个 CPU。例如 Intel 的超线程技术可以让操作系统以为有两个核,在 CPU 层面通过共用原件来做到一个物理核执行两个逻辑核的任务;操作系统层面的线程就是所谓的「内核态线程」;「用户态线程」则多种多样,只要能满足在同一个内核线程上执行多个任务的都算,例如 coroutine、golang 的 goroutine、C# 的 Task。不要被名词所局限,他们其实是不同的东西。

sleep 之后整个进程阻塞是因为混用了用户态和内核态的线程,正确的使用姿势是用用户态线程的替代品,例如 C# 的 Task.Delay。这是使用用户态线程的一个常见问题,即不能混用阻塞的 syscall。

一个进程能有无数个线程,无数个协程吗

这个要分不同系统去看:

  • 如果是32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
  • 如果是64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。

顺便多说一句,过多的线程将会导致大量的时间浪费在线程切换上,给程序运行效率带来负面影响,无用线程要及时销毁。

为什么说线程轻量,进程哪里重量级吗

进程是重量级的,而线程是轻量级的,开销相比更小

进程切换分两步:

1.切换页目录以使用新的地址空间

2.切换内核栈和硬件上下文

对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。

切换的性能消耗:

1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

五大IO模型

https://blog.csdn.net/weixin_43705195/article/details/120099487

为什么用epoll

epoll的优点:epoll 是一种更加高效的 IO 复用技术

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

epoll 的使用步骤及原理如下:

1)调用epoll_create()会在内核中创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数。

在这个结构体中有 2 个比较重要的数据成员:一个是需要检测的文件描述符的信息 struct_root rbr (红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发送改变的文件描述符信息 (双向链表);

2)调用epoll_ctl() 用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除

3)调用epoll_wait() 可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。

epoll 的两种工作模式:

a)LT 模式(水平触发)LT(Level - Triggered)是缺省的工作方式,并且同时支持 Block 和 Nonblock Socket。 在这种做法中,内核检测到一个文件描述符就绪了,然后应用程序可以对这个就绪的 fd 进行 IO 操作。应用程序可以不立即处理该事件,如果不作任何操作,内核还是会继续通知。

b)ET 模式(边缘触发) ET(Edge - Triggered)是高速工作方式,只支持 Nonblock socket。 在这种模式下,epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件。必须要一次性将数据读取完,使用非阻塞I/O,读取到出现EAGAIN。但是,如果一直不对这个 fd 进行 IO 操作(从而导致它再次变成未就绪 ),内核不会发送更多的通知(only once)。

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

3)EPOLLONESHOT

一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket

我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件

(2)I/O 多路复用是一种使得程序能同时监听多个文件描述符的技术,从而提高程序的性能。 Linux 下实现 I/O 复用的系统调用主要有select、poll 和 epoll。

select、poll和epoll区别

1)调用函数

select和poll都是一个函数,epoll是一组函数

2)文件描述符数量

select通过线性表描述文件描述符集合,文件描述符有上限(与系统内存关系很大),32位机默认是1024个,64位机默认是2048。

poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目

epoll通过红黑树描述,最大可以打开文件的数目

3)将文件描述符从用户传给内核

select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝

epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上

4)内核判断就绪的文件描述符

select和poll通过线性遍历文件描述符集合,判断哪个文件描述符上有事件发生

epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。

epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list

5)应用程序索引就绪文件描述符

select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历

epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可

6)工作模式

select和poll都只能工作在相对低效的LT模式下

epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。

7)应用场景

当监测的fd数目较小,且全部fd都比较活跃,建议使用select或者poll

当监测的fd数目非常大,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能

条件编译:

#ifdef 标识符

程序段1

#else

程序段2

#endif

除了reactor还有别的模型吗

事件:I/O事件、信号及定时事件

(1)reactor模式中,主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元 ),将socket可读写事件放入请求队列,交给工作线程处理,即读写数据、接受新连接及处理客户请求均在工作线程中完成。通常由同步I/O实现(epoll_wait)。

(2)proactor模式中,主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑,如处理客户请求。通常由异步I/O实现(aio_read/aio_write)。

本服务器采用:同步I/O模拟Proactor模式

实现了http的哪些方法

GET和POST的区别
(1)get主要用来获取数据,post主要用来提交或修改数据。

(2)get的参数有长度限制,最长2048字节,而post没有限制。

(3)get是明文传输,可以直接通过url看到参数信息,post是放在请求体中,除非用工具才能看到。

(4)get的参数会附加在url中,以 " ?"分割url和传输数据,多个参数用 "&"连接, 而post会把参数放在http请求体中。

(5)get请求会保存在浏览器历史记录中,也可以保存在web服务器日志中。 (6)get请求会被浏览器主动缓存,而post不会,除非手动设置。

(7)get在浏览器回退时是无害的,而post会再次提交请求。

(8)get请求只能进行url编码,而post支持多种编码方式。

(9)get请求的参数数据类型只接受ASCII字符,而post没有限制。

(10)get是幂等的,而post不是幂等的。 幂等性:对同一URL的多个请求应该返回同样的结果。

如何处理http请求

http版本

time_wait和close_wait有什么区别

  • FIN_WAIT_2:
    • 半关闭状态。
    • 发送断开请求一方还有接收数据能力,但已经没有发送数据能力。
  • CLOSE_WAIT状态:
    • 被动关闭连接一方接收到FIN包会立即回应ACK包表示已接收到断开请求。
    • 被动关闭连接一方如果还有剩余数据要发送就会进入CLOSE_WAIT状态。
  • TIME_WAIT状态:
    • 又叫2MSL等待状态。
    • 如果客户端直接进入CLOSED状态,如果服务端没有接收到最后一次ACK包会在超时之后重新再发FIN包,此时因为客户端已经CLOSED,所以服务端就不会收到ACK而是收到RST。所以TIME_WAIT状态目的是防止最后一次握手数据没有到达对方而触发重传FIN准备的。
    • 在2MSL时间内,同一个socket不能再被使用,否则有可能会和旧连接数据混淆(如果新连接和旧连接的socket相同的话)。

socket是什么

在计算机通信领域,socket 被翻译为"套接字"”(套接字=主机+端口号),它是计算机之间进行通信的一种约定或一种方式。通过 socket这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据

数据库索引数据结构

得分点 B+树

标准答案 索引可选的底层数据机构包括: - 二叉树 - 红黑树 - hash - B-tree

但mysql索引的底层用的并不是二叉树和红黑树。因为二叉树和红黑树在某些场景下都会暴露出一些缺陷。

首先,二叉树在某些场景下会退化成链表,而链表的查找需要从头部开始遍历,而这就失去了加索引的意义。

不使用红黑树的原因是:红黑树作为底层数据结构在面对在些表数据动辄数百万数千万的场景时,会导致索引树的层数很高。索引从根节点开始查找,而如果我们需要查找的数据在底层的叶子节点上,那么树的高度是多少,就要进行多少次查找,数据存在磁盘上,访问需要进行磁盘IO,这会导致效率过低;

而B+树由B树和索引顺序访问方法演化而来,它是为磁盘或其他直接存取辅助设备设计的一种平衡查找树,在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点,各叶子节点通过指针进行链接。 B+树索引在数据库中的一个特点就是高扇出性,例如在InnoDB存储引擎中,每个页的大小为16KB。在数据库中,B+树的高度一般都在2~4层,这意味着查找某一键值最多只需要2到4次IO操作,这还不错。因为现在一般的磁盘每秒至少可以做100次IO操作,2~4次的IO操作意味着查询时间只需0.02~0.04秒。

数据库主从表相关?

数据库事务隔离级别

sql定义了四个隔离级别:

1.读取未提交:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读、不可重复读。

2.读取已提交:允许读取并发事物已经提交的数据,可以阻止脏读,但幻读和不可重复读仍有可能发生。

3.可重复读:对同一字段的多久读取结果都是一致的,除非数据本身是被事务自己修改。可以阻止脏读和不可重复读,但幻读仍有可能发生。

4.串行化:最高的隔离级别,完全符合ACID的隔离级别。所有事务依次逐个执行,这样事务间就不可能产生干扰。也就是说,可以防止脏读,不可重复读和幻读。

腾讯测试

自我介绍

问服务器相关内容,

介绍一下非阻塞IO与阻塞IO,

为什么选择epoll,而不是其他的?

你的项目是支持长连接还是短连接?

HTTP/1.1 的性能如何?

HTTP 协议是基于 TCP/IP,并且使用了「请求 - 应答」的通信模式,所以性能的关键就在这两点里。

1. 长连接

早期 HTTP/1.0 性能上的一个很大的问题,那就是每发起一个请求,都要新建一次 TCP 连接(三次握手),而且是串行请求,做了无谓的 TCP 连接建立和断开,增加了通信开销。

为了解决上述 TCP 连接问题,HTTP/1.1 提出了长连接的通信方式,也叫持久连接。这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。

持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vqTei0Ys-1662347126752)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/HTTP/16-%E7%9F%AD%E8%BF%9E%E6%8E%A5%E4%B8%8E%E9%95%BF%E8%BF%9E%E6%8E%A5.png)]

当然,如果某个 HTTP 长连接超过一定时间没有任何数据交互,服务端就会主动断开这个连接。

HTTP/1.1 相比 HTTP/1.0 提高了什么性能?

HTTP/1.1 相比 HTTP/1.0 性能上的改进:

  • 使用长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。
  • 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

但 HTTP/1.1 还是有性能瓶颈:

  • 请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩 Body 的部分;
  • 发送冗长的首部。每次互相发送相同的首部造成的浪费较多;
  • 服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端一直请求不到数据,也就是队头阻塞;
  • 没有请求优先级控制;
  • 请求只能从客户端开始,服务器只能被动响应。

你是如何处理HTTP的长连接的?

那个定时器是怎样起作用的?

你的服务器并发量是多少?

如何提高并发量?

你觉得你现在的瓶颈在哪里?

你在测试时有没有查看CPU效率?

测试时服务器崩溃是什么情况?

为什么建立连接需要三次握手,而挥手需要四次?

UDP如何实现可靠连接?

得分点

​ 将运输层TCP的可靠传输机制在应用层实现

参考答案

标准回答

​ UDP不是面向连接的协议,因此资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。如果想要使用UDP还要保证数据的可靠传输,就只能通过应用层来做文章。实现的方式可以参考TCP的可靠传输机制,差别就是将TCP传输层功能,确认机制、重传机制和窗口确认机制实现在了应用层。

加分回答

​ 在应用层实现可靠传输关键点有两个,从应用层角度考虑分别是:

  1. 提供超时重传机制,能避免数据报丢失的问题。
  2. 提供确认序列号,保证数据拼接时候的正确排序。

请求端:首先在UDP数据报定义一个首部,首部包含确认序列号和时间戳,时间戳是用来计算RTT(数据报传输的往返时间),计算出合适的RTO(重传的超时时间)。然后以等-停的方式发送数据报,即收到对端的确认之后才发送下一个的数据报。当时间超时,本端重传数据报,同时RTO扩大为原来的两倍,重新开始计时。

响应端:接受到一个数据报之后取下该数据报首部的时间戳和确认序列号,并添加本端的确认数据报首部之后发送给对端。根据此序列号对已收到的数据报进行排序并丢弃重复的数据报。

延伸阅读

​ 已经实现的可靠UDP:

  • RUDP 可靠数据报传输协议。

  • RTP 实时传输协议:为数据提供了具有实时特征的端对端传送服务;例如:组播或单播网络服务下的交互式视频、音频或模拟数据。

  • UDT:基于UDP的数据传输协议,是一种互联网传输协议。主要目的是支持高速广域网上的海量数据传输,引入了新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议,同时支持可靠的数据流传输和部分可靠的数据报服务。应用:高速数据传输,点到点技术(P2P),***穿透,多媒体数据传输。

进程、线程、协程区别?

那个信号是如何发送给进程的?

1.使用 Kill 向进程发送信号

使用 kill 命令向进程发送信号。例如,如果要向进程“a.out”发送 USR1 信号,请执行以下操作。

$ ps -C a.out PID TTY TIME CMD 3699 pts/1 00:00:00 a.out $ kill -s USR1 3699

注意:请查看之前发过的文章 KILL进程的 4 种方法 - kill、killall、pkill、xkill。

2.从另一个进程向一个进程发送信号

您可以使用 UNIX 系统调用 kill(来自 C 程序)将信号从一个进程发送到另一个进程。以下 C 代码片段显示了如何使用 kill 命令。

Kill 系统调用有两个参数:1)需要发送信号的进程的 PID(进程 ID)2)需要发送到进程的信号。Kill 命令成功时返回 0。

int send_signal (int pid) { int ret; ret = kill(pid,SIGHUP); printf(“ret : %d”,ret); }

3.从键盘向进程发送信号

当一个进程在终端上运行时,您可以使用某些特定的键组合从键盘向该进程发送信号。以下是几个例子。

SIGINT (Ctrl + C) - 你已经知道了。按 Ctrl + C 会终止正在运行的前台进程。这会将 SIGINT 发送到进程以杀死它。

您可以通过按 Ctrl + \ 或 Ctrl + Y 向进程发送 SIGQUIT 信号

进程又是如何接收到信号的?

在linux中,进程会视具体信号执行以下操作之一:
忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理。
捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux系统提供了signal()系统调用可用于注册信号的处理函数。

执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。

进程并不会主动删除信号,对于不需要的信号,忽略就可以了。

共享内存是如何实现的?

共享内存是连续的吗?

做一个题两数之和

问到的好多不会,总之凉凉,还得继续努力。

反问。

linux查询端口

-t – 显示 TCP 端口。-u – 显示 UDP 端口。-n – 显示数字地址而不是主机名。-l – 仅显示侦听端口。-p – 显示进程的 PID 和名称。仅当您以 root 或 sudo 用户身份运行命令时,才会显示此信息。

查询指定端口通过grep过滤:netstat -tnlp | grep :80

快手

时间比较久远,很多问题记不清了。

9月23一面

问项目相关(20来分钟)

面试官当场去看github代码

问了计网相关内容、epoll内容

做了三道题

删除链表中间节点1

删除链表中间节点2

判断是否平衡二叉树

10月11二面

问了项目相关

操作系统方面内容

算法题:

盛最多水的容器

想到用贪心,但是没写对

目前还没有消息

飞书提前批四轮技术面面经

1、一面

一面是个比我大不了几岁的小哥哥来面我,问的问题都很基础。

1、自我介绍+webserver介绍

2、const int* a, int* const a, int const* a 的区别

3、智能指针介绍下,auto_ptr现在还在用吗?

4、讲一下tcp,三次握手,能不能两次

5、tcp粘包拆包,怎么解

6、介绍socket和epoll,IO模型

7、epoll的优点,与select和poll的区别

8、什么场景下用select、poll、epoll

9、epoll怎么解决io效率问题的?

10、内核和用户空间之间消息传递方式知道几种

11、死锁产生的条件

两道力扣经典算法

12、最长上升子序列

13、右边第一个大的数

2、二面

1、自我介绍

2、vector 尾部添加元素,需要连续的内存空间吗?

3、C++ 程序到可执行文件的过程 (这题就是秀哥网站上的原题,并且讲解的很清楚)

4、编译原理,动态链接和静态链接有什么区别?一般什么情况用动态链接,什么情况用静态链接?

5、C++ 程序内存布局是怎么样的?堆和栈有什么区别?栈和堆各有什么优缺点?栈空间大小?

6、2 GB 内存的操作系统中,可以分配4 GB 的数组吗?(虚拟内存)

7、给出一个程序,看看能不能正常运行?空指针方面的

8、TCP了解吗?怎么保证可靠性的?按序到达如何做到的?

9、TCP通讯,服务端的程序挂掉了,客户端会怎么样?

10、数据库了解吗?MySQL呢?索引?主键?

11、操作系统中一个进程要删除正在被写入的文件,能不能删除成功?remove

两道算法题:最大岛屿数量、二叉树的中后序遍历

3、三面

1、C跟C++的区别?

2、智能指针有几种?

3、auto_ptr指针摒弃的原因?

4、其他三种智能指针?

5、C的设计模式应该有很多吧,都有哪些?

6、介绍下单例模式

7、用到的工具类?STL

8、stl种的sort内部实现

9、有哪些数据结构,能说多少说多少?是否了解红黑树?

10、hash冲突了解么,怎么解决。

11、hash函数有了解么?

12、操作系统用的进程和线程的区别?

13、线程安全了解么?

14、OSI->TCP/IP,为什么从OSI转向TCP/IP

15、HTTPS了解么,了解怎么建立连接的

两道算法

16、TOP K, 先写个快排,然后堆的思想优化

17、归并排

当问到我红黑树的那一刻,我简直太激动了!!!

因为我看了秀哥在星球里分享的两个面试利器,其中一个就是红黑树相关,我花了一周时间把秀哥给的资料好好研究了下,这下终于派上用场了!

三面结束后面试官很友好的跟我说欢迎我去飞书,当时我都以为自己稳了。

没想到半小时后HR通知我要进行交叉面,我勒个去,,,

4、交叉面

交叉面感觉是个大leader,给人的感觉很nice 1、 自我介绍

2、 socket服务端建立连接到结束用到了哪些系统调用

3、epoll是什么模式,为什么要IO多路复用

4、 epoll原理

5、 LT和ET模式的区别

6、IO多路复用中,一些开源的软件用到了IO多路复用(不会)

7、系统调用讲一下,具体细节

8、看你简历上写了一个redis客户端项目?对redis很了解吗?具体说说?(终于问我Redis了,泪目。。。)

9、内核态和用户态的区别,细节

10、SQL语句写一个,取第K大的行

11、毒药毒老鼠智力题(秀哥网站智力题原题)

这几轮面试都很快,一般都是面完一个小时给电话约下次一面试,面试体验也很好,有来有回的。

字节飞书测开暑期实习面经

  • 测试了解多少

黑盒测试和白盒测试分别是什么

  • 分别写出他们的测试用例
    - [ ] 百度输入一个链接,到页面展示会发生什么
    - [ ] DNS的工作原理
    - [ ] DNS劫持
    - [ ] 大白话描述DNS的作用
  • DNS域名解析协议,用来实现域名和ip地址的相互转换,ARP和RARP是实现ip地址和mac地址的转换
  • - [ ] HTTP的请求报文的形式
    - [ ] GET和POST的区别
    - [ ] POST还支持什么编码类型
    - [ ] GET保留的完整记录是什么
    - [ ] 端口的概念
    - [ ] MYSQL的端口号
    - [ ] 一个表,有姓名、学号和性别,查找出学号最小的10个学生(写出来)(排序)
    - [ ] 新增一个学生信息进去(写出来)
    - [ ] Linux的基本指令有哪些
    - [ ] 怎么在文件里查找特定单词(linux语句写出来)
    - [ ] Linux的硬链接和软链接
    - [ ] 析构函数和构造函数
    - [ ] 什么情况下会产生内存泄漏(除了那两种)
    - [ ] 申请内存的整个申请流程
    - [ ] 堆是怎么申请内存的
    - [ ] 写malloc申请内存语句(申请10个字节)
    - [ ] 怎么避免内存泄漏呢

算法- [ ] 数组找出乘积为n的两个数
- [ ] 不让用排序呢,说时间复杂度

(二面45min)

- [ ] 对测试的看法
- [ ] 测试和开发的区别

- [ ] HTTP请求方法除了GET和POST还了解什么
- [ ] 刚说出来的五种方法的应用场景
- [ ] HTTP状态码从1到5是什么意思

得分点 1xx、2xx、3xx、4xx、5xx。

标准回答 HTTP状态码有:1xx代表服务器端已经接受了请求。2xx代表请求已经被服务器端成功接收,最常见的有200、201状态码。3xx代表路径被服务器端重定向到了一个新的URL,最常见的有301、302状态码。4xx代表客户端的请求发生了错误,最常见的有401、404状态码。5xx代表服务器端的响应出现了错误。

加分回答 - 1xx:指定客户端相应的某些动作,代表请求已被接受,需要继续处理。由于 HTTP/1.0 协议中没有定义任何 1xx 状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应。 - 2xx:代表请求已成功被服务器接收、理解、并接受。这系列中最常见的有200、201状态码。 - 200(成功):服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。 - 201(已创建):请求成功并且服务器创建了新的资源。 - 202(已接受):服务器已接受请求,但尚未处理。 - 203(非授权信息):服务器已成功处理了请求,但返回的信息可能来自另一来源。 - 204(无内容):服务器成功处理了请求,但没有返回任何内容。 - 205(重置内容):服务器成功处理了请求,但没有返回任何内容。 - 206(部分内容):服务器成功处理了部分 GET 请求。 - 3xx:代表需要客户端采取进一步的操作才能完成请求,这些状态码用来重定向,后续的请求地址(重定向目标)在响应头Location字段中指明。这系列中最常见的有301、302状态码。 - 300(多种选择):针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。 - 301(永久移动):请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。 - 302(临时移动):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。 - 303(查看其他位置):请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。 - 304(未修改):自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。 - 305(使用代理):请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。 - 307(临时重定向):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。 - 4xx:表示请求错误。代表了客户端看起来可能发生了错误,妨碍了服务器的处理。常见有:401、404状态码。 - 400(错误请求):服务器不理解请求的语法。 - 401(未授权):请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。 - 403(禁止):服务器拒绝请求。 - 404(未找到):服务器找不到请求的网页。 - 405(方法禁用):禁用请求中指定的方法。 - 406(不接受):无法使用请求的内容特性响应请求的网页。 - 407(需要代理授权):此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。 - 408(请求超时):服务器等候请求时发生超时。 - 409(冲突):服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。 - 410(已删除):如果请求的资源已永久删除,服务器就会返回此响应。 - 411(需要有效长度):服务器不接受不含有效内容长度标头字段的请求。 - 412(未满足前提条件):服务器未满足请求者在请求中设置的其中一个前提条件。 - 413(请求实体过大):服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。 - 414(请求的 URI 过长):请求的 URI(通常为网址)过长,服务器无法处理。 - 415(不支持的媒体类型):请求的格式不受请求页面的支持。 - 416(请求范围不符合要求):如果页面无法提供请求的范围,则服务器会返回此状态代码。 - 417 (未满足期望值):服务器未满足"期望"请求标头字段的要求。 - 5xx:代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。常见有500、503状态码。 - 500(服务器内部错误):服务器遇到错误,无法完成请求。 - 501(尚未实施):服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。 - 502(错误网关):服务器作为网关或代理,从上游服务器收到无效响应。 - 503(服务不可用):服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。 - 504(网关超时):服务器作为网关或代理,但是没有及时从上游服务器收到请求。 - 505(HTTP 版本不受支持):服务器不支持请求中所用的 HTTP 协议版本。

- [ ] 500,502,504分别什么意思
- [ ] TCP和UDP的区别
- [ ] TCP和UDP的应用场景
- [ ] HTTP和HTTPS的区别
- [ ] HTTPS的加密解密过程
- [ ] 数据库的几种连接和区别
- [ ] 了解过索引么
- [ ] 数据库的索引类别
- [ ] 写出所知道的排序方法的时间复杂度和空间复杂度

算法- [ ] 两个表,一个personid一个address,不管有没有地址信息,输出所有personde的表中信息
- [ ] 写一个快排并实现

测试题- [ ] 怎么测试微信朋友圈的点赞功能

面完第二周也没收到感谢信,突然国际电商部门直接打电话来约面,岗位变成了后端开发,然后非常水又随意地面了二十几分钟给挂了,很神奇的操作。

字节跳动-幸福里-测开实习 一面面经

时间: 2020/8/1

方式:飞书

面试官是一个比较年轻的男性,比较亲切,感觉很好交流

大体流程:

\1. 自我介绍

\2. 项目 (之前做过个人博客)

​ 追问1:用了什么技术

​ 追问2:数据库怎么设计的

\3. 项目里提到了爬虫,问了一下爬虫

​ 追问1:爬了那些网站

​ 追问2:爬了什么数据

​ 追问3:怎么做的反爬

​ 追问4:用了几个cookie

​ 追问5:几个线程,怎么调度的

​ 追问6:你怎么初始化你的爬虫任务的

\4. 编程题

​ 面试官:擅长什么语言?

​ 回答:C++

​ 面试官:好(飞书打开内置编辑器,没见过,一开始不适应)

​ 题目:链表的倒数第N个节点

​ 反问:链表长度一定大于N吗

​ 面试官:当然不

​ 反问:如果长度小于N我应该怎么返回

​ 面试官:返回个头指针吧

​ (快慢指针解决,较为简单,说了下思路)

​ * 那个编辑器不支持格式化,有的东西也不给自动添加,写程序要有好习惯,要不然比较难受

​ 问题:

​ 指针和引用

​ 野指针

​ 虚函数

​ 深拷贝浅拷贝

​ C++内存管理

\5. 你怎么测你的这个题

\6. 你做过测试吗,接触过吗

​ 回答:没有

​ 追问:你研究方向不是这个,为什么做测试

​ 回答:为了学东西

​ 追问:那为什么学测试

​ (懵了,开始乱扯)

​ 面试官:这个职位跟你想的不一样,可能不是你理想的职位

​ 回答:是是是,这就是(赶紧解释)

​ 面试官:好吧,继续

\7. SQL

​ 一个学生表,一个家长表,查询平均成绩小于60分的学生的家长电话

​ 写错了,说了思路

​ 面试官比较亲切的肯定了思路,然后指出了错误的原因

\8. 网络

​ 问题:url输入到输出经历了什么

​ 回答:(流畅回答,提到get请求)

​ 追问:为什么是get

​ 回答:(开始蒙,没蒙对)

​ 追问:get和post有什么区别

​ 回答:(有点懵,说的不太好)

​ 追问:tcp了解吗(简单问题,忘了是啥了)

​ 追问:http和https的区别,还有吗,还有吗

\9. 逻辑题

​ 4个人过桥,时间分别为1,2,5,10,一次两个,只有一个手电筒,最短时间

10 Linux

​ 问题:词频统计(承认不会)

​ 问题:Linux命令说几个(一直说,越多越好,有逻辑一点)

​ 追问:你怎么删除目录的,文件呢,都是rm -rf吗,r是啥,f是啥

​ \11. 你能实习多久

\12. 你有什么想问的

总结:

​ \1. 问的还是比较简单的,题目都不难

​ \2. 不会就说不会,不要蒙,蒙不对的

​ \3. 代码写错没关系,别慌, 冷静分析,找bug也是一种能力(自我安慰…),养成好一点的编程习惯

​ \4. 别给自己挖坑,注意引导面试官,说一些自己会的方向,回答上一个问题给下一个留下伏笔(很关键)

​ \5. 注意观察面试官,比如我这个面试官,当我把问题答得差不多的时候,他就会说“OK”,然后点头,记录,如果状态不对就想想补充点什么东西

8、new / delete 与 malloc / free的异同

相同点

  • 都可用于内存的动态申请和释放

不同点

  • 前者是C++运算符,后者是C/C++语言标准库函数
  • new自动计算要分配的空间大小,malloc需要手工计算
  • new是类型安全的,malloc不是。例如:
int *p = new float[2]; //编译错误
int *p = (int*)malloc(2 * sizeof(double));//编译无错误
 
/ *
malloc 返回值的类型是void *,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型。    
malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
*/
    
  • new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后者均没有相关调用
  • 后者需要库文件支持,前者不用
  • new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象

#9、new和delete是如何实现的?

  • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针
  • delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存

#10、malloc和new的区别?

  • malloc和free是标准库函数,支持覆盖;new和delete是运算符,不重载。

  • malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。

  • malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

    update1:感谢微信好友“猿六学算法”指出错误,已修正!

#11、既然有了malloc/free,C++中为什么还需要new/delete呢?直接用malloc/free不好吗?

  • malloc/free和new/delete都是用来申请内存和回收内存的。
  • 在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free,所以new/delete是必不可少的。

#12、被free回收的内存是立即返还给操作系统吗?

不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

野指针和悬空指针

此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。 避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。

产生原因及解决办法:

野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。

悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空

static

  1. 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

  2. static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;

  3. 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员;

  4. static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function

指针和const的用法

  1. 当const修饰指针时,由于const的位置不同,它的修饰对象会有所不同。
  2. int * const p2中const修饰p2的值,所以理解为p2的值不可以改变,即p2只能指向固定的一个变量地址,但可以通过*p2读写这个变量的值。顶层指针表示指针本身是一个常量
  3. int const p1或者const int * p1两种情况中const修饰 p1,所以理解为* p1的值不可以改变,即不可以给*p1赋值改变p1指向变量的值,但可以通过给p赋值不同的地址改变这个指针指向。

new和malloc的区别

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

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

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

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

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

delete p、delete [] p、allocator都有什么作用?

1、 动态数组管理new一个数组时,[]中必须是一个整数,但是不一定是常量整数,普通数组必须是一个常量整数;

2、 new动态数组返回的并不是数组类型,而是一个元素类型的指针;

3、 delete[]时,数组中的元素按逆序的顺序进行销毁;

4、 new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。

delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。

malloc与free的实现原理?

1、 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、,munmap这些系统调用实现的;

2、 brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;

3、 malloc小于128k的内存,使用brk分配内存,将_edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。

4、 malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

类成员初始化方式?

  1. 赋值初始化,通过在函数体内进行赋值初始化;列表初始化,在冒号后使用初始化列表进行初始化。

这两种方式的主要区别在于:

对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。

列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。

介绍面向对象的三大特性,并且举例说明

三大特性:继承、封装和多态

(1)继承

让某种类型对象获得另一个类型对象的属性和方法。

它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

常见的继承有三种方式:

  1. 实现继承:指使用基类的属性和方法而无需额外编码的能力
  2. 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
  3. 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力(C++里好像不怎么用)

例如,将人定义为一个抽象类,拥有姓名、性别、年龄等公共属性,吃饭、睡觉、走路等公共方法,在定义一个具体的人时,就可以继承这个抽象类,既保留了公共属性和方法,也可以在此基础上扩展跳舞、唱歌等特有方法

(2)封装

数据和代码捆绑在一起,避免外界干扰和不确定性访问。

封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。

(3)多态

同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为**(重载实现编译时多态,虚函数实现运行时多态)**。

多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单一句话:允许将子类类型的指针赋值给父类类型的指针

实现多态有二种方式:覆盖(override),重载(overload)。

覆盖:是指子类重新定义父类的虚函数的做法。

重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。例如:基类是一个抽象对象——人,那教师、运动员也是人,而使用这个抽象对象既可以表示教师、也可以表示运动员。

初始化列表更快

对于用户定义类型:
1)如果使用类初始化列表,直接调用对应的构造函数即完成初始化
2)如果在构造函数中初始化,那么首先调用默认的构造函数,然后调用指定的构造函数

所以对于用户定义类型,使用列表初始化可以减少一次默认构造函数调用过程

你可能感兴趣的:(面试,c++)