面试笔记

面试笔记

一、C++

1.基础知识

1.1 关键字

1.1.1 static
  • 隐藏:在编译多个文件的时候,指定函数变量的作用于为文件内部,未加static具有全局可见性;
  • 保持变量的内容持久;
  • 默认初始化为0;
  • C++的类成员变量会声明为static,不是对象的成员。
1.1.2 const
  • 顶层const和底层const
  • 对于内置类型,const相当于#define
  • 作用:指定变量不可被当前进程、线程修改。但可被其他线程修改。
  • int *const 和 int const *区别
    • int * const是常量指针,指针本身是一个常量,必须初始化,且不能赋值,即所放在指针中的地址就不能变。
    • int const* 和const int *:指针指向的内容是一个常量,可以先不用初始化,可以改变地址来改变指针解引用的值,但不能直接解引用指针来改变值。
  • const和#define的区别:
    • #define在预编译阶段起作用,而const在编译运行的阶段起作用;
    • #define只是简单的字符替换,而const有类型检查;
    • #define占用的是代码段空间,在对应地方展开,而const在程序运行过程只有一份备份,占用的是数据段空间;
    • const不能重定义,而#define可以采用#undef取消定义然后重定义;
    • const可以用于类成员变量的定义,而define不可以。
    • #define可以防止头文件的重复引用。
1.1.3 sizeof

​ 计算一个数据类型的大小

1
2
3
4
char buf[10] = "123456";
char buf1[] = "123456";
const char *p = "123456";
cout << sizeof(buf) << " " << sizeof(buf1) << " " << strlen(p) << " " << sizeof(p) << endl; //10 7 6 4
1.1.4 auto

​ 自动变量,由编译器自动分配释放,通常在栈上分配。

auto和decltype在使用上的不同:

​ decltype:从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。

​ 对于 decltype 所用的表达式来说,如果变量名加上一对括号,则得到的类型与不加上括号的时候可能不同。如果 decltype 使用的是一个不加括号的变量,那么得到的结果就是这个变量的类型。但是如果给这个变量加上一个或多层括号,那么编译器会把这个变量当作一个表达式看待,变量是一个可以作为左值的特殊表达式,所以这样的decltype就会返回引用类型。

1
2
3
int i;
decltype(i) // int类型
decltype((i)) // int& 类型

1.1.5 extern

​ 作用:1.和“C”一起,告诉编译器编译函数的时候按照C的规则去翻译相应函数名;2.声明函数或者全局变量的作用范围的关键字,其声明的可以在本模块或者其他模块中使用。

1.2 类

1.2.1 创建对象的步骤?
  • 开辟空间
  • 初始化对象信息
  • 返回对象的地址引用
1.2.2 类的初始化

​ 类的初始化顺序:只与定义有关,与初始化列表无关。若有构造函数,根据构造函数内的初始化顺序,此外const必须在构造函数内初始化,static必须在类外初始化。

1.2.3 析构函数可以是虚函数吗?构造函数可以是虚函数吗?

​ 析构函数可以是虚函数,且在大部分的时候应该是虚函数,这样在进行delete运算的时候能够确保析构函数能被正确执行,可以采用动态联编的方式选择对应的析构函数。

​ 构造函数不能为虚函数,因为在执行构造函数的时候,对象还是一片未定的空间,只有在构造完成后对象才成为一个类的实例。

1.2.4 C++的多态是怎么实现的?
  • 编译时的多态:函数的重载
  • 运行时的多态:虚函数
1.2.5 析构函数可以重载吗?

​ 由于析构函数不接受任何参数,因此不能被重载。

1.2.6 成员对象的初始化顺序

​ 成员对象的初始化顺序只和成员对象在类的声明顺序有关,和成员初始化列表的顺序无关。

1.2.7 临时对象立马析构
1
2
3
4
5
6
7
8
9
10
11
12
13
class A{
A(int x,int y):x(x),y(y){}
private:
int x;int y;
}
int main()
{
A a=A(3,2);//这里A(3,2)是一个临时对象,在完成这条复制语句后立马析构。
//实际等价操作:
A temp(3,2);
A a=temp;
释放temp
}
1.2.8 类的静态成员

​ 为实现类的多个对象之间的数据共享而引进的。且类的静态数据成员初始化只能在类外进行。类的静态成员初始化和类的构造函数无关。静态成员对象可以作为产生对象的计数器。

类的静态成员函数不能直接引用非静态数据成员。因为静态成员函数不属于任何一个对象,因此没有this指针。

1.2.9 使一个类不可以被继承
  • 采用单例模式类似的形式,将构造函数设置为私有,然后提供一个对外的方法。

  • 采用友元的性质。

    • 将A的构造函数和析构函数都声明为private的,但是将B作为A的友元类,这样B就可以访问A的构造函数和析构函数了,此时B能正常构造;
    • 为了使B的子类C不能被正常构造,可以让C直接调用A的构造函数,那么将B设置成虚拟继承自A;
    • 因为友元关系是不能被继承的,所以C调用A的构造函数时会报错
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <iostream>
    using namespace std;

    template<class T>
    class A
    {
    friend T;//注意这里不需要class关键字

    private : //将构造函数和7构函数都声明是私有的
    A(){}
    ~A(){}
    };

    class B:public virtual A<B> //这里一定需要使用虚继承,只有使用虚继承,它的子类才会直接调用A类的构造函数,这样才会报错,如果是普通继承,那么通过B类调用A类的构造函数时时不会报错的
    {};

    class C:public B
    {};

    int main()
    {
    B b;
    cout<<"success"<<endl;
    // C c;
    return 0;

    }
1.2.10 关于派生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
public:
void test1() { cout << "A test1" << endl; test2(); }
void test2() { cout << "A test2" << endl; }
};
class B : public A
{
public:
void test2() { cout << "B test2" << endl; }
};
int main()
{
B *b = new B;
b->A::test2(); // A test2
b->test1(); //A test1 /n A test2
b->test2(); // B test2
}
1.2.11 struct和class区别,class可以继承struct吗(可以)?

​ 区别:

  • class表现为行为,而struct常用于存储数据;

  • struct作为数据结构的实现体,它默认的数据访问控制是public的;而class作为对象的实现体,它默认的成员变量访问控制是private的

  • “class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。

  • 作为参数传递时,class变量以按址方式传递,而struct变量是以按值方式传递的。

struct可以继承class,同样class也可以继承struct。默认是public继承还是private继承取决于子类而不是基类

1
2
3
struct A{};
class B : A{}; //private继承
struct C : B{}; //public继承
1.2.12 空的class多大

​ 在c++中一个空的class是一个字节,因为 c++ 中规定不同的对象必须拥有不同的地址,如果为0会导致两个空类的地址一样。

1
2
class A {};
std::cout << "sizeof(A): " << sizeof(A) << std::endl;// sizeof(A): 1
1.2.13 浅拷贝带来的重析构的问题,使用智能指针可以避免吗?

​ 默认的构造函数可能会让两个指针指向同一片内存区域,当对象进行析构的时候,会将这一片内存区域析构两次,因此会出错。使用智能指针应该可以避免,因为内存泄露的问题本质在于析构函数多次释放堆内存,使用shared_ptr可以解决这个问题。

1.2.14 拷贝构造函数和赋值函数的区别?

​ 在看到“=”操作符为对象赋值的时候,

​ 如果是在对象定义时(Test B = (Test)c),此时调用拷贝构造函数;

​ 如果不是在对象定义赋值时(B = c),此时调用赋值函数。

 内存空间角度:

 1)拷贝构造函数的使用,是在建立对象时;当时对象没有占有内存,故不需要释放内存,不重新建立内存空间。

 2)赋值函数的使用,是在对象建立后;当时对象已经占有内存,故需要释放先前内存,然后重新获取内存空间。

​ 注意:当函数参数为对象的时候,实参传递给形参的实际上是实参对象的一个拷贝,系统自动通过拷贝构造函数实现;当返回值为一个对象的时候,也是如此。

1.2.15 构造函数中是否可以使用虚函数?

​ 可以使用(语法没有问题)但虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
using namespace std;

class Base
{
public:
Base()
{
Function();
}
virtual void Function()
{
cout << "Base::Fuction" << endl;
}
};

class A : public Base
{
public:
A(){}
virtual void Function()
{
cout << "A::Fuction" << endl;
}
};

int main()
{
Base *a=new A;
delete a; //输出:Base::Function
}

​ 虚函数在构造函数中不起作用。因为创建A对象的时候,先调用Base构造函数,此时派生类还没被完全创建,因此此时只是个基类对象,因此a绑定的是基类的Function();纯虚函数也不可以,这种虚函数的调用是不可传递到子类的。

1.3 函数

1.3.1 参数传递
  • 值传递:单向,形参不能改变实参,形参是实参的拷贝,形参在栈中;
  • 指针传递:本质是值传递,传递的是一个地址值,函数执行的时候在栈中创建并拷贝实参的地址值,然后访问内存单元,因而可以改变主函数实参指向的变量的值。
  • 引用传递:表示引用实参,即实参的一个别名,操作引用变量就是操作实参变量。在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
  • 选择方式:传递类对象参数的标准方式是按引用传递。若不修改值,数据对象是较大的结构,则使用const指针或const引用,传递数组采用指针。
1.3.2 函数重载与函数重写的区别?
  • 函数重载指的是在同一可访问区被声明的几个具有不同参数列(不同参数的类型、个数和顺序)的同名函数,根据参数列表确定调用哪一个函数。(重载不关心函数的返回类型)

  • 函数重写(覆盖,隐藏):派生类重新定义了函数,函数的返回值,函数名以及参数列表都需要和基类中被重写的一致,只有函数体是不同的,需要有virtual修饰。

  • 此外,C++重载是不支持函数返回类型的重载的。这是为了保持解析操作符或者函数调用的时候不依赖上下文,为了防止隐式类型转换冲突。

    1
    2
    3
    float sqrt(float);
    double sqrt(double);
    //这样调用的时候返回值可以通过类型转换,而不是调用的时候依赖上下文判断。
1.3.3 内联函数
  • 性质:内联函数在编译后不具备函数的性质,将函数体直接在编译时嵌入每一个调用处,类似宏替换,使用函数体替换调用处的函数名。此外,在类中的声明的成员函数自动转换为内联函数。内联函数不能有循环和递归。
  • 优点:有类型参数检测,更加安全;在程序运行时展开,进行参数传递。如果内联函数不符合标准,会自动转为普通函数。
  • 缺点:在函数调用处展开,会使代码变长,占用更多内存
1.3.4 宏定义函数
  • 优点:使用像函数,但不是函数,使用预处理器实现,没有参数压栈,代码生成等一系列操作,因此效率很高;
  • 缺点:1.仅仅是做预处理符号表中的简单替换,因此不能进行参数的有效性检测,也就不能享受C++编译器严格类型检查的好处,此外返回值也不能被强制转换为可转换的合适类型;2.宏定义不能访问类的成员变量;3.宏定义使用参数时是严格的替换策略,在展开代码中采用形参代替实参,因此容易产生二义性。
1.3.5 宏定义函数和内联函数的区别?
  • 内联函数在运行时可调试,宏定义不可以;
  • 编译器会对内联函数的参数类型做安全检查或自动类型转换,而宏定义不会;
  • 内联函数可以访问类的成员变量,而宏定义不行。

1.4 指针

1.4.1 智能指针
  • shared_ptr:维护一个计数器,采用make_shared进行分配和使用动态内存。会存在循环复用问题,采用weak_ptr可以解决。
  • unique_ptr:不支持普通的拷贝和赋值操作,但可以转移所有权。
  • auto_ptr:一种不安全的unique_ptr,C++11不建议使用。
  • weak_ptr:计数器不改变,一种弱指针,要用shared_ptr初始化,不能直接访问对象,必须调用lock判断指向的对象是否还存在,存在lock会返回一个shared_ptr。
  • 线程安全:智能指针本身不是线程安全的,它的引用计数是线程安全且无锁的,但对象的读写则不是线程安全。因为shared_ptr 有两个数据成员,读写操作不能原子化。
  • 注意:shared_ptr和unique_ptr必须采用直接初始化的形式。
  • shared_ptr不能代替普通指针,比如原子操作在多线程中消耗大。
1.4.2 指针和引用的区别?
  • 指针是一个对象,而引用只是对象的一个别名;
  • 指针可以赋值或者拷贝,引用不可以;
  • 指针可以不用初始化,但引用在使用的时候必须初始化。
1.4.3 delete p和delete[] p的区别?

​ 基本类型的对象没有析构函数,所以回收基本类型组成的数组空间用 delete 和 delete[] 都是应该可以的;但是对于类对象数组,只能用 delete[]。对于 new 的单个对象,只能用 delete 不能用 delete[] 回收空间。

​ new []返回的地址会后移4个字节,并用那4个存放数组的大小!而new不用后移这四个字节。delete[]根据那个4个字节的值,调用指定次数的析构函数 ,同样delete也不需要那四个字节。

1
2
delete []objects; // 正确的用法
delete objects; // 错误的用法

​ 后者相当于delete objects[0],漏掉了另外99 个对象
​ 严格应该这样说:后者相当于仅调用了objects[0]的析构函数,漏掉了调用另外99 个对象的析构函数,并且在调用之后释放内存时导致异常(如果存在析构函数的话),如果对象无析构函数该语句与delete []objects相同。

1.4.4 new、delete和malloc、free的区别?

​ malloc与free是C++/C的库函数,new/delete是C++的运算符,都可以用于申请动态内存和释放内存。

​ 对于非内置类型,光有malloc和free是无法满足动态对象的要求,对象在创建的时候同时要自动执行构造函数,对象在释放的时候要自动执行析构函数,由于malloc和free是库函数,不是运算符,因此不在编译器的控制权限之内,不能把构造函数和析构函数的任务强加给malloc和free。

1.4.5 malloc和mmap的底层实现?malloc分配的是什么?

https://blog.csdn.net/weixin_41033366/article/details/105185320

1598073194438

​ Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。我们用malloc进行内存分配就是从break往上进行的。 获得了break指针的位置也就得到了堆内存申请的起始地址 malloc实际上是将可用空间用一个空闲链表连接起来,若用户申请空间,就遍历该链表,找到第一个满足条件的空闲块,将其进行拆分,返回合适大小的空间给用户,将剩下的部分链接到链表中。当调用free释放空间时,会把这块空间连接到空闲链表上。到最后,该空闲链就会被切成很多的小内存块,一旦用户申请一块较大的空间,空闲链中的空间大小都无法满足需求,malloc会申请延时,对空闲链表进行检查,内存重新整理,把相邻的小片段合并成大的空闲块。

​ 当使用 malloc() 分配过大的空间,malloc 不再从堆中分配空间,而是使用 mmap() 这个系统调用从映射区寻找可用的内存空间。 1.当开辟的空间小于128k时,调用brk()函数,malloc的底层实现是系统调用函数brk(),其主要移动指针_enddata来开辟空间、 2.当开辟的空间大于128k时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空间来开辟。

进程的内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* 
0XFFFFFFFF----->____________________
| |
| kernel |
|__________________|
| |
| stack |
top of stack ------->|------------------|
| | |
| | |
| V |
| |
|__________________|
| |
| mmap |
|__________________|
| |
| ^ |
| | |
| | |
program break ------>|------------------|
| heap |
|__________________|
| |
| BSS |
|__________________|
| |
| data |
|__________________|
| |
| text |
0X00000000 ------>|__________________|
*/
1.4.6 什么是内存泄露?

​ 指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

  • 堆内存泄露:new/malloc在申请内存使用完毕后未释放。
  • 系统资源泄露:比如socket,BItmap等程序使用的系统分配的资源在使用完毕后没有使用相应的函数释放掉。
1.4.7 什么是野指针?

​ 指向内存被释放的内存或者没有访问权限的内存的指针。

造成的原因:

  • 指针变量没有初始化,乱指;
  • 指针p被delete或free后,没有置为NULL;
  • 指针操作超越了变量的作用范围。
1.4.8常见的动态内存泄露?
  • 1.对NULL指针解引用
  • 2.对动态开辟空间进行越界访问
  • 3.非动态开辟空间使用free进行释放
  • 4.在使用free释放一块动态开辟的空间只释放了部分
  • 5.一块空间多次释放
  • 6.忘记释放—->内存泄露
1.4.9 操作系统进程内存分配方式

​ 当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:

  • 1、检查要访问的虚拟地址是否合法

  • 2、查找/分配一个物理页

  • 3、填充物理页内容(读取磁盘,或者直接置0,或者啥也不干)

  • 4、建立映射关系(虚拟地址到物理地址)

​ 从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(大于128K,初始化为0)(不考虑共享内存)。

  • brk是将数据段(.data)的最高地址指针_edata往高地址推;

  • mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

1.4.10 malloc能分配最多多少内存?

​ 查阅博客后发现,一次malloc能申请到的内存和linux内核的参数有关系,“/proc/sys/vm/overcommit_memory”这个文件控制着最大虚拟内存的分配。其一共有3个值,0、1、2。默认值为0,0表示启发式分配,在该模式下,默认的检测是比较弱,可能会导致进程被“OOM-killed”。1表示总是超额分配,从不检测。2表示总是检测,从不超额分配。查看了下实验的两个机器,分配最大内存解决物理内存上限的系统,该值为0,也就是采用的是启发式超额分配;分配最大内存接近2^47的系统,该值为1,也就是从不检测,总是超额分配。 也就是说系统参数的差异,决定了一次malloc()能申请的最大内存空间是不一样的。

​ 之所以只能分配到2的47次方,是因为x86_64本身不是一个完全意义上的64位地址空间,其中48~63位必须和47位保持一致。因此最大内存空间只有47位,同时要给操作系统留出空间,因此理论上能分配到不足2^47。

1.4.11 malloc()成功返回后OS已经分配相应内存了吗?

对于申请较大内存没有。对于用户而言,所有的内存都是虚存,malloc返回的是虚拟的内存地址,实际上并没有真正分配这么多内存。但每次实际物理内存会增加4K,也就是1页,会增加页表项,会可能增加物理内存占用。

​ 对于申请比较小的内存确实分配了相应的内存。

1.5 move()和forward()

  • move():显式的将一个左值转换为对应的右值引用(绑定关系),eg. int &&rr3= std::move(rr1),其中rr1是一个左值。左值持久,右值短暂。
  • forward():

1.6 左值引用和右值引用

1.6.1 左值和右值的区分?
  • C++中可以取地址有名字的就是左值;反之不能取地址的没有名字的就是右值(将忘值或者纯右值)。
  • 左右值都属于引用类型,因此需要初始化,左值是具有变量值的别名,右值是不具有变量名的别名,左值通常不能绑定到右值(例外:const int& b=2是对的,不加const是错的),右值也通常不能绑定到任何左值(但采用std::move()可以将左值强制转换为右值)。
1.6.2 为什么要进行右值引用?

​ 当程序猿需要使用寄存器的值的时候是可以进行右值引用的,寄存器的刷新速度很快,没有右值引用的话就需要先将寄存器的值拷贝到内存中,再进行使用,这样很浪费空间,效率也不高。

1.7 union

1.7.1 什么是union?

​ 一种特殊的类,可以在里面定义多个不同的数据类型。这些数据共享内存,可以节省空间。

1.7.2 union和struct区别?
  • struct里面成员各自有自己的内存空间;
  • union中共享内存空间,大小为各成员中最长的长度。
1.7.3 union需要注意的?

​ 不能继承,也不能作为基类,所以无虚函数。类不可以放入union中,此外由于联合里面的内容共享内存,所以也不能够放引用和静态。

1.8 强制转换

  • static_cast:用于非多态类型转换(静态转换),任何标准转换都可以用它,但是不能用于两个不相关的类型转换。
  • dynamic_cast:用于将一个父类对象的指针转换为子类对象的指针或引用,只能用于有虚函数的类(必须有共有继承和虚函数)。(动态交换)
    • 向上转型:子类对象指针–>父类对象指针/引用(不需要转化)
    • 向下转型:父类对象指针–>子类对象指针/引用(用dynamic_cast转型是安全的)
  • const_cast:删除变量const的属性,方便赋值
  • reinterpret_cast:将一种类型转换为另一种不同的类型。

2.STL库

  • vector:先预先分配一块空间,然后不够以两倍扩容。clear()只会清空元素,所有占用的内存并不会释放,采用vector().swap(nums)可以使vector离开其自身的作用域,从而强制释放vector所占用的内存空间。

注意:对于序列式容器(如vector,deque,list等),删除当前的iterator会使后面所有元素的iterator都失效。这是因为vector,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。不过erase方法可以返回下一个有效的iterator。

1
2
3
4
5
6
7
8
9
vector<int> val = { 1,2,3,4,5,6 };  
vector<int>::iterator iter;
for (iter = val.begin(); iter != val.end(); )
{
if (3 == *iter)
iter = val.erase(iter); //返回下一个有效的迭代器,无需+1
else
++iter;
}

​ 对于关联容器(如map,set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前的iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入,删除一个结点不会对其他结点造成影响。

1
2
3
4
5
6
7
8
9
set<int> valset = { 1,2,3,4,5,6 };  
set<int>::iterator iter;
for (iter = valset.begin(); iter != valset.end(); )
{
if (3 == *iter)
valset.erase(iter++);
else
++iter;
}
  • list

  • deque

  • queue

  • stack

  • set、multiset

  • map、multimap:红黑树实现。元素有序

  • unordered_map:底层采用hash_table实现,实际上是一个带桶的vector。查找O(1),元素无序。内存占有率比map高,但查找效率更高。

  • string操作

    构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    String::String(const char* str)
    {
    if(str == nullptr)
    {
    m_data = new char[1];
    *m_data = '\0';
    }
    else
    {
    int len = strlen(str);
    m_data = new char[len+1];
    strcpy(m_data,str);
    }
    }

    拷贝构造函数:

    1
    2
    3
    4
    5
    6
    7
    String::String(const String &other)
    {
    int len = strlen(other.m_data);
    m_data = new char[len+1];
    strcpy(m_data,other.m_data);
    }
    }

    赋值构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    String &String::operator = (const String& other)
    {
    if(other == this)
    {
    return *this;
    }
    delete[] m_data;
    int len = strlen(other.m_data);
    m_data = new char[len+1];
    strcpy(m_data,other.m_data);
    return *this;
    }
  • sort():数据量大时采用Quick Sort,分段递归排序。一旦分段后的数据量小于某个阈值,为避免Quick Sort的递归调用带来过大的额外开销,就改用Insertion Sort(插入排序)。如果递归层次过深,还会改用Heap Sort。

    阈值一般为16,若元素规模小于等于16,采用插入排序;若大于16,则判断递归的调用深度是否超过限制,超过则采用堆排序,未超过采用快速排序。

3.线程库

3.1 thread

3.2 condition_variable

3.3 mutex

4.内存管理(堆栈管理)

4.1堆栈的区别?

  • 管理方式:堆是程序员管理,栈属于编译器管理;
  • 空间大小:堆的空间大,栈的空间小;
  • 是否产生碎片:堆会产生碎片,栈不会产生碎片
  • 生长方式不同:堆是向内存大的方向生长,栈是向内存小的方向生长
  • 分配方式:堆一般是动态分配的,栈是静态分配;
  • 分配效率:堆的分配效率低,栈的分配效率高。

4.2 内存管理中的碎片问题

内存碎片:回收内存时,将内存块放入free链表中。因内存越分越小,内存块小而多。当需要一块大内存时,尽管此时空闲内存的总和可能足够满足需求,但是过于零散,没有一个合适的内存块。

内部碎片是由于采用固定大小的内存分区,当一个进程不能完全使用分给它的固定内存区域时就产生了内部碎片,通常内部碎片难以完全避免;

外部碎片是由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。

​ 原因:分配内存时,不能将相邻内存合并

解决办法:

  • 小块内存单独分配(内存池),大块内存由操作系统分配(STL采用)

  • 伙伴算法(解决外部碎片问题,尽量分配连续的页面,简单)

    • 划分组:第1组1个块,第2组2个块,第三组4个块,…,第m组2^(m-1)块,其中快大小相等,一个组是一个链表;
    • 分配内存:申请的内存大小为n,则向上取整为2的幂次数,定位到响应组,到组中(链表上)找空闲块分配出去;若没有空闲块,则到上一组找,直到找到为止,并将剩余的内存放到下面适当的组中。
    • 合并内存:用完内存需要归还,根据实际内存块大小向上取整为2的幂次数,归入链表。
      1. 检测其伙伴块是否空闲
      2. 如果空闲就合并在一起,继续检测1
      3. 如果不空闲,直接归入链表
  • slab算法(以内存池为思想,解决内部碎片问题,解决小内存)

    https://www.cnblogs.com/cobbliu/articles/8535670.html

    1598341258164

5.C++编译成可执行程序的过程

过程如下:

(一)预处理阶段。首先我们会对我们编辑得到的源代码(即扩展名为.c/.cpp文件)通过预处理器进行预处理,这一部分是由我们的编译器帮我们完成的。预处理器做的事情就是将原始源文件中的所有预处理器指令替换为暗示指令的实际库代码。那么什么是预处理指令呢?实际上这些是我们在代码很常见的,如#include和#define指令。之后,生成的文件基本上被取代并获得扩展名为.i文件。

(二)编译器编译阶段。接下来是编译器的处理阶段,这个阶段是把高级语言翻译成低级语言的过程,也负责检查源代码的语法/语法。若发现无误,则会将文件转换为扩展名为.s的文件,即我们所知的汇编代码。

(三)目标文件转换阶段。得到汇编文件之后,要对其进行转换,即该过程是将汇编级语言转换为机器级语言(一般为二进制格式),此时生成的文件就是我们的目标文件,扩展名为.o或.obj。

(四)链接阶段。C++语言支持分离式编译(这里指的编译是指上面(一)至(三)阶段)机制的,该机制允许将程序分割为若干个文件,每个文件可独立编译。那么多个已编译的文件如何合并呢?答案就是在此阶段。该阶段通过链接器将一个或多个目标文件合并到一个可执行文件,即将扩展名为.obj / .o文件转换为扩展名为.exe文件。

  • 静态链接:主要进行符号解析和重定位。
    • 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
    • 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
  • 动态链接:
    • 在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
    • 在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。

注:我们平常所说的“编译”是一个整体,即包括预处理,编译和汇编三个阶段。它在这些步骤中基本上将高级语言转换为机器级语言,并生成单个二进制对象文件。若编译(但不链接)三个单独的文件,则将创建三个作为输出的目标文件。在运行程序前(未加载到内存),可执行程序已经分为code,data,bbs区(未初始化数据区),运行后增加了堆区和栈区。

pic

6.C++11的新特性有哪些?

  • 类型说明符:auto用于从初始化表达式中推断出变量的数据类型。因此,auto定义的变量必须有初始值。

  • 类型说明符:decltype:选择并返回操作数的数据类型。编译器会分析表达式并得到它的类型,但是不会去计算表达式的值。

    注意:如果decltype使用的是一个不加括号的变量,得到的就是该变量的类型。如果给变量加上了一层括号,编译器会把它当作一个表达式,得到的则是引用类型。

    1
    2
    3
    int i=10;
    decltype(i) a; //a的类型是int
    decltype((i)) b=i; //b的类型是&int,必须对其初始化,否则编译错误。
  • 空指针:nullptr:为了解决原来NULL的二义性,原来的NULL就是0;

  • 基于范围的for循环

1
2
3
4
vector<point> ps(10,x);
for (auto i : ps){
cout << i.x << " "<<i.y << endl;
}
  • 虚函数的override和final指示符:

    • override,表示派生类应当重写基类中的虚函数
    • final,表示派生类不应当重写这个虚函数
  • constexpr变量:将变量声明为constexpr类型以便由编译器来验证变量的值是否为一个常量表达式,必须在编译期间计算出它的值并且它的值不可以被改变

  • noexcept:声明一个函数不可以抛出任何异常使用关键字noexcept;

  • 匿名函数(Lambda函数)

    1
    2
    3
    4
    [] (int x, int y) { return x + y; } // 隐式返回类型
    [] (int& x) { ++x; } // 没有 return 语句 -> Lambda 函数的返回类型是 'void'
    [] () { ++global_x; } // 没有参数,仅访问某个全局变量
    [] { ++global_x; } // 与上一个相同,省略了 (操作符重载函数参数)

7.面向对象的三大特性和七大原则以及类图

  • 三大特性
    • 封装
    • 继承
    • 多态
  • 七大原则
    • 单一职责原则:一个类只做一件事
    • 开放封闭原则:可以扩展,不可修改。也就是对扩展开放,对修改封闭。
    • 里式替换原则:子类必须可以替换基类,子类对父类的方法尽量不要重写和重载。
    • 依赖倒置原则:高层模块不依赖于底层模块,二者都依赖于抽象。
    • 接口隔离原则:每个接口不存在子类用不到却必须实现的方法。
    • 迪米特法则:一个类对自己依赖的类知道的越少越好,即封装好通过public方法提供给外部。
    • 合成复用原则:尽量使用合成、聚合的方式,而不是使用继承。
  • 类图
    • 泛化关系(Generalization)(继承)
    • 实现关系(Realization)(实现一个接口)
    • 聚合关系(Aggregation)(表示整体由部分构成,但不是强依赖的,整体不在了部分还是会存在,比如电脑,鼠标,键盘,电脑不存在了,各自还存在)
    • 组合关系(Composition)(和聚合关系差不多,但整体和部分是强依赖的,比如公司和部门,公司不存在了,部门也就不存在了)
    • 关联关系(Association)(表示不同的对象之间有关联,这是一种静态关系,比如学生和学校就是关联关系,一个学校多个学生,一个学生属于一个学校)
    • 依赖关系(Dependency)(A类是B类的局部变量,B类方法的参数,或者A向B发送消息导致B发生变化)

二、数据结构

1.链表

1.1 有环链表

1.2 链表的插入和删除

1.3 链表的逆序

1.4 链表的深复制

2.二叉树

2.1完全二叉树的存储

2.2 哈夫曼树

2.3 满二叉树

2.4 树的前序遍历

2.5 树的中序遍历

2.6 树的后序遍历

2.7 树的层序遍历

2.8 树的深度和广度

2.9 红黑树

​ 红黑树的特性:

​ 1.每个节点只能是红色或者黑色。

​ 2.根节点必须是黑色。

​ 3.红色的节点,它的叶节点只能是黑色。

​ 4.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

​ 红黑树的使用有两种特点:1.key/value快速查找;2.RBtree的顺序,来查找一段。

​ epoll、内存管理、map、进程的调度都用到了红黑树。进程中有一个就绪队列,可以通过vruntime当做key组织成一个红黑树,比当前时间小的都拿出来进行执行。

2.10 字典树(tries树)

3.哈希表

3.1 哈希表怎么解决冲突?

​ 再hash法,链地址法,建立公共溢出区

4.图

4.1最小生成树算法

  • prim算法
  • 克鲁斯卡尔算法

4.2 最短路径算法

  • 迪杰斯特拉算法
  • Floyd算法

三、算法

1.常用的算法

单调栈,双指针,滑动窗口,动态规划,贪心法,分治法,减治法,回溯法,位运算,蛮力法;

2.内排序

2.1常见排序

​ 归并排序,快速排序,堆排序,冒泡排序,选择排序,直接插入排序,计数排序(基数排序)。

2.2 快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int patition(vector<int>& nums,int left,int right)
{
int i=left,j=right;
while(i<j)
{
while(i<j&&nums[i]<=nums[j]) j--; //右侧扫描
if(i<j)
{
int temp=nums[i];nums[i]=nums[j];nums[j]=temp;
i++;
}
while(i<j&&nums[i]<=nums[j]) i++;
if(i<j)
{
int temp=nums[i];nums[i]=nums[j];nums[j]=temp;
j--;
}
}
return i;
}
void quickSort(vector<int> &nums,int first, int end)
{
int mid;
if(first < end)
{
mid = partition(nums,first,end);
quickSort(nums,first,mid-1);
quickSort(nums,mid+1,end);
}
}

2.3 堆排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<iostream>
#include<vector>
using namespace std;

void print(vector<int> &nums)
{
for (int i = 0; i < nums.size(); i++)
{
cout << nums[i] << " ";
}
cout << endl;
}

void swap(vector<int> &nums, int i, int j)
{
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
void adjustHaep(vector<int> &nums, int i, int length) //调整当前节点i;
{
int temp = nums[i];
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) //从i的左孩子开始
{
if (k + 1 < length&&nums[k] < nums[k + 1]) //如果节点的左孩子小于右孩子,指向右孩子
k++;
if (nums[k] > temp) //如果孩子的值大于父节点,则将孩子节点的值给父节点(不用交换)
{
nums[i] = nums[k];
i = k;
}
else
break;
}
nums[i] = temp;
cout << " After adjust:";
print(nums);
}
void sort(vector<int> &nums)
{
for (int i = nums.size() / 2 - 1; i >= 0; i--) //从第一个非叶子节点下到上,从右到左调整堆的结构为大顶堆
{
adjustHaep(nums, i, nums.size());
}
for (int i = nums.size()-1; i > 0; i--) //交换堆顶元素和队尾元素,然后继续调整堆的结构。
{
swap(nums, 0, i);
adjustHaep(nums, 0, i);
}
}

int main()
{
vector<int> nums = { 1,3,5,7,9,2,4,6,8,0 };
sort(nums);
print(nums);
}

2.4 归并排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<iostream>
#include<vector>
using namespace std;

void print(vector<int> &nums)
{
for (int i = 0; i < nums.size(); i++)
{
cout << nums[i] << " ";
}
cout << endl;
}

void Merge(vector<int> &nums, vector<int> &help, int left, int mid, int right)
{
int i = left, j = mid + 1;
while (i <= mid && j <= right)
{
if (nums[i] <= nums[j]) help.push_back(nums[i++]);
else help.push_back(nums[j++]);
}
while (i <= mid)
{
help.push_back(nums[i++]);
}
while (j <= right)
{
help.push_back(nums[j++]);
}
}

void MergeSort(vector<int> &nums,int left,int right)
{
if (left == right) return;
else
{
int mid = (left + right) / 2;
MergeSort(nums, left, mid);
MergeSort(nums, mid + 1, right);
vector<int> help;
Merge(nums,help,left,mid,right); //将两个有序序列进行合并,将结果保存在help中;
for (int i = 0, j = left; i < help.size(); i++, j++)
nums[j] = help[i];
}
}

int main()
{
vector<int> nums = { 1,3,5,7,9,2,4,6,8,0 };
MergeSort(nums,0,nums.size()-1);
print(nums);
}

3.外排序

3.1 多路归并排序(败者树)

​ 将原文件分解成多个能够一次性装人内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行归并排序。

​ 划分的时候可以采用置换-选择排序。依次挑出n个升序列(列的长度可能不一致)。

​ 归并的时候可以采用两两归并排序,但这种方法要归并排序多次,耗时高,因此采用多路归并排序。

​ 多路归并排序采用败者树进行归并。败者树是一个完全二叉树,叶子节点为k个,代表k路归并,并且为k个归并序列的首值,非叶子节点有k-1个(不包括根节点),根节点代表了最小值。每次挑出一个最小值后,从那个队列进来一个新值,然后更新败者树,重新挑选新值。这样完成最终排序,相当于一共每个元素有两次IO操作,第一次进行划分,第二次进行归并。

4.递归

3.1递归的本质是一个栈

5.时间复杂度分析

1598252982642

1598253172605

四、设计模式

1.常见的设计模式

  • 创建型模式:单例模式,工厂模式
  • 结构型模式:适配器模式,装饰器模式
  • 行为性模式:观察者模式、模板模式

2.单例模式

懒汉模式(线程不安全):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton
{
private:
static Singleton* instance;
Singleton(){}
public:
static Singleton* getInstance()
{
if(instance==nullptr)
instance=new Singleton();
return instance;
}
}
Singleton* Singleton::instance=nullptr;

饿汉模式(线程安全):

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton
{
private:
static Singleton* instance;
Singleton(){}
public:
static Singleton* getInstance()
{
return instance;
}
}
Singleton* Singleton::instance=new Singleton();

懒汉模式(线程安全,加锁):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Singleton
{
private:
static Singleton* instance;
static mutex m;
Singleton(){}
public:
static Singleton* getInstance()
{
if(instance==nullptr)
{
unique_lock<mutex> lock(m);
if(instance==nullptr)
{
instance=new Singleton();
}
}
return instance;
}
}
Singleton* Singleton::instance=nullptr;
mutex Singleton::m;

3.观察者模式

​ 项目中出现的观察者模式:vue中的v-module。观察者模式通常被用来实时事件处理系统。

4.发布与订阅者模式

​ ROS中的topic。

5.两种模式的区别

  • 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,生产者与消费者不知道对方的存在,它们之间通过频道进行通信。
  • 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,生产者向频道发送一个消息之后,就不需要关心消费者何时去订阅这个消息,可以立即返回。

五、数据库(MySQL)

1.mysql特点

​ 插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取相分离。

2.mysql的框架

​ 连接层,服务层,数据存储引擎层,文件系统(存储层)

3.事务

3.1 ACID特性

  • 原子性:一个事务要么全都执行,要么全部不执行;
  • 一致性:一个事物提交前后数据库应该是保持一致的。
  • 隔离性:事务之间是互相不影响的,一个事务的执行不能被其他事务干扰。
  • 持久性:事务一旦提交对数据库的修改就是永久性的。

3.2 事务的安全性问题

  • 脏读:读取了已经更新但未提交的数据(可能撤销了更新),脏读是事务B修改了数据。
  • 不可重复读:在脏读后撤销操作再次读取,发现两次读的内容不一样,A读取了B已经提交的修改数据。
  • 幻读:针对的插入和删除,两次读取结果不一样,幻读是事务B新增了数据。

3.3 事务的隔离级别

  • 读未提交:三种问题都会出现(脏读、不可重复读、幻读)
  • 读已提交(Oracle):可以避免脏读,但不可避免另外两种。
  • 可重复读(MySQL):不可解决幻读,可解决另外两个。
  • 可序列化:可解决所有问题(相当于加锁等待),但性能低下。

4.存储引擎

4.1 InnoDB

​ 支持事务、主外键、行锁(适合高并发),不仅缓存索引,还缓存真实数据,对内存要求较高,而且内存大小对性能有决定性影响。表空间大,主要关注点为事务。

​ 实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ Next-Key Locking 防止幻影读。

4.2 MyISAM

​ 不支持主外键和事务,采用表锁(操作一条记录会锁住整个表),只缓存索引,不缓存真实数据,表空间小,关注点为性能。

5.索引

5.1索引的定义

​ 索引是帮助MYSQL高效获取数据的数据结构。提高查询效率,类比字典,排好序的快速查找数据结构。索引一般都很大,不可能全部存在内存中,因此会以索引文件存储在磁盘上。

5.2 索引的分类(方法)

  • 全文索引:只有MyISAM支持,为了解决针对文本的模糊查询效率较低的问题;
  • HASH索引:一次定位,针对‘=’和‘in’有极高的效率,但范围查询,排序等效率仍然不高;
  • BTree:MYSQL默认和最常用的索引;
  • RTree:在mysql中少,优势在于范围查找。

5.3 索引的分类

  • 单一索引
  • 主键索引
  • 唯一索引
  • 复合索引:在使用的时候将条件顺序按照索引的顺序,否则索引失效。遵循的是最左原则,创建复合索引的规则是先对最左边字段进行排序,在第一个完成排序的基础上再对第二个字段进行排序,第一个字段绝对有序,而第二个字段就是无序的。
  • 文本索引

5.4 索引的优势和劣势

  • 优势:查询快,排序快
  • 劣势:更新表速度慢,占用空间变大

5.5 B树和B+树

  • B树:非叶子节点也有数据,效率比B+树高
  • B+树(MySQL采用的):只有叶子节点有数据。B+树的优势是IO少,在设备内存一定的情况下,B+树更好;此外连续读取的时候只需要访问叶子节点。

5.6 聚簇索引和非聚簇索引

​ 除了主键索引其他都是非聚簇索引,在空间上不连续,不是主顺序。

​ 非主键索引的叶子节点存放的是主键的值,而主键索引的叶子节点存放的是整行的数据。因此非主键索引也被成为二级索引。

5.7mysql怎么建表和增加列?怎么建立索引?两种建立区别?怎么删除表?

  • 创建表,增加列

    1
    2
    3
    CREATE TABLE table_name (column_name column_type);

    alter table TABLE_NAME add column NEW_COLUMN_NAME varchar(45) not null ;##可选:after column_name/first(在指定列后插入列,在首列插入)
  • 删除

    • delete(删除数据)(可以回滚,和事务相关)
    • drop(删除表,包括表结构)(不可回滚,除非通过备份)
  • 建立索引

    • alter table
    1
    2
    3
    ALTER TABLE table_name ADD INDEX index_name (column_list)
    ALTER TABLE table_name ADD UNIQUE (column_list)
    ALTER TABLE table_name ADD PRIMARY KEY (column_list)
    • create index(不能创建主键索引)
    1
    2
    CREATE INDEX index_name ON table_name (column_list)
    CREATE UNIQUE INDEX index_name ON table_name (column_list)

5.8 char和varchar区别?

  • char:不可变长,最大存储(255字节)
  • varchar:自动可变长,且最大可存储长度更长(65535字节)

6.锁

6.1锁粒度

  • 表级锁:偏向MyISAM引擎,开销小,加锁快,粒度大,冲突高。
  • 行级锁:偏向InnoDB,开销大,加锁慢,会出现死锁,锁定粒度最小,发生冲突概率最低,并发度最高,支持事务。(无索引或索引失效行锁会升级为表锁),锁定一行(select…for update)。
  • 使用行级锁产生间隙锁:产生危害,采用范围条件加锁,没有这一项也会被锁住。

6.2 锁级别

  • 共享锁(读锁):当前用户锁后,其他用户可以查询但写会阻塞,当前用户也不可以写,也不能读其他表。
  • 排它锁(写锁):当前用户加锁后增删改查都可以,但其他用户所有操作被阻塞。
  • 意向锁:并不是真正加锁,只是意向上的。有意向读锁和写锁。通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。
  • 区别:读锁会阻塞写,写锁会把读和写都阻塞。当一个事务加了读锁后,其他事务也可加读锁,但不可加写锁,此外一个事务加了写锁后,其他事务不可以加锁。

6.3 乐观锁和悲观锁

  • 悲观锁:有共享锁和排它锁,在数据处理的整个过程中将数据处于锁定状态
  • 乐观锁:假定不会出现冲突,如果在提交更新的时候出现冲突,则返回错误信息让用户选择如何解决,一般实现方式就是记录数据版本。并未加锁,效率高,但掌握不好更新失败的概率会很高。

7.视图

7.1视图

​ 一个虚拟表,临时性,和普通表一样使用;动态生成,只保存了sql逻辑,不保存结果。

8.主键自增的好处

​ 每次进行插入操作的时候,ID都比之前的大,因此每次只需要在后面插入即可,不需要移动位置、分裂等操作,这样可以提高性能。

六、操作系统

1.进程与线程

1.1进程

1.1.1 .进程的通信方式
  • 信号量:用于进程之间互斥和同步,实质是计数器

  • 管道:半双工,亲缘关系(父子或兄弟)

  • 有名管道:一种文件类型,半双工,可无亲缘关系,以一种特殊文件形式存在于文件系统中。

  • 共享内存:最快的方式,进程直接对内存进行读取,常与信号量结合使用,方便同步。

  • 消息队列:有优先级和特定格式,独立于接收和发送线程

    相比于 FIFO,消息队列具有以下优点:

    • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
    • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
    • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
  • socket

补充:进程同步与进程通信的区别:

  • 进程同步:控制多个进程按一定顺序执行;
  • 进程通信:进程间传输信息。

进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。

1.1.2.进程的定义与特点

​ 进程是资源分配和拥有的基本单位。进程是一个实体,有自己地址空间,比如CODE、DATA、堆栈区;进程是一个正在执行中的程序。

1.1.3 .进程(作业)的调度算法
  • FCFS(先来先服务)
  • SJF(最短作业优先)
  • HRN(最高响应比优先):R=(W+T)/T=1+W/T (W为等待时间,即当前时间减去到达时间,T为执行时间)
1.1.4 .进程拥有的资源

​ 系统资源,打开的文件fd,地址空间

1.2线程

1.2.1.线程同步的方式?

C++:互斥量(mutex)、条件变量(condition_variable)、读写锁、条件变量

1.2.2.线程拥有的资源?
  • 共享资源:和其他线程共享进程拥有的全部资源,包括代码段,共有数据等,自己基本上不拥有系统资源
  • 独有的资源:程序计数器,一组寄存器和堆栈,线程的优先级等。
1.2.3.线程的状态?

​ 就绪、阻塞、运行

1.3进程与线程的区别?

​ 地址空间:多进程中每个进程有自己的地址空间,线程则是共享地址空间;

​ 定义:前者是资源分配和拥有的基本单位,而后者只是共享进程的资源;线程是处理器调度的基本单位,但进程不是;

​ 执行:线程不能够独立执行,必须依存在应用程序中;

​ 速度:线程的速度更快,通讯快,切换快,线程资源利用率好。

2.IO多路复用

参见blog:https://segmentfault.com/a/1190000003063859

2.1 一次IO操作流程

​ 一次IO,数据首先会被拷贝到内核的缓冲区,然后从操作系统内核的缓冲区拷贝到应用程序的地址空间。

  • 等待数据准备,进入内核的缓冲区;
  • 从内核的缓冲区将数据拷贝到进程中。

2.2 同步IO

  • 阻塞IO:在IO的两个阶段都被阻塞,需要等待数据准备,比如默认socket通信;
  • 非阻塞IO:当用户进行read操作的时候,内核数据没有准备好,会返回一个error,用户进程不会被阻塞,这样用户进程可以一直询问,一旦内核的数据准备好,并且收到了用户进程的system call,内核立马准备将数据拷贝到用户进程内存中。(特点:用户进程不断主动询问内核数据是否数据已经准备好)
  • IO多路复用:同时处理多个网络连接的IO,采用select、poll、epoll这几种方式不断轮询所负责的所有socket,当某个socket有数据到达的时候,通知用户进程。

2.3 异步IO(使用较少)

​ 用户在发出read后,立马可以做其他的事情,当kernel接收到异步read后,首先会立刻返回,所以用户进程不会被block,然后kernel会等待数据准备完成,然后将数据拷贝到用户内存。只有当这两个步骤都完成了,kernel会给用户进程发送一个signal,告诉它完成了read。

2.4 同步IO和异步IO的区别

​ 同步IO在进行真实的IO操作(两步)的时候进程会被阻塞(其中非阻塞IO会在kernel将数据从内核拷贝到内存的时候被阻塞了),因此前几种都属于同步IO,而异步IO值的是内核在数据准备好后,还自己完成了读写,当两个步骤都完成了之后才会告诉用户进程。

2.5 阻塞IO和非阻塞IO的区别

​ 阻塞IO会一直阻塞对应的进程直到操作完成,而非阻塞IO在内核还在准备数据的情况下回立即返回。

2.6 IO多路复用(事件驱动IO)

​ 特点:通过一种机制一个进程就能同时等待多个文件描述符(fd),而这些fd其中任意一个进入读就绪状态,select()函数就可以返回。

​ select、poll、epoll本质上都是同步IO,因为都需要在读写事件就绪后自己负责读写,也就是读写这个过程仍然是阻塞的。

2.6.1 select()和 poll()

​ select和poll都需要在返回后遍历文件描述符来获取已经就绪的socket,描述符越多效率越低,两者异同为:

  1. select有最大数量限制(linux:1024),poll没有,但是数量过大都会影响性能(遍历文件描述符)
  2. select使用3个位图表示三种文件描述符,poll使用一个pollfd的指针实现。
  3. select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
2.6.2 epoll()

​ epoll是select和poll的增强版,epoll通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知(不再需要遍历文件描述符,通过监听回调的机制,也是epoll的魅力)。

1
2
3
4
5
6
7
8
int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,并没有限制,只是一个参考。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//- epfd:是epoll_create()的返回值。
//- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
//- fd:是需要监听的fd(文件描述符)
//- epoll_event:是告诉内核需要监听什么事
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//等待epfd上的io事件,最多返回maxevents个事件。参数events用来从内核得到事件的集合.
  • 工作模式
    • LT(水平触发,默认):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
    • ET(边缘触发):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。效率相对更高。
2.6.3 应用场景
  • select:可移植性更好,几乎被所有主流平台所支持。select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。
  • poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
  • 只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。

3.内存管理

3.1内存管理的方式

​ 段页式管理(linux),且段比较少,尽量使用页。

3.2linux内存管理

​ linux采用的是请求调页的方式进行内存管理

3.3 虚拟内存、常驻内存、共享内存

  • VIRT:进程需要的虚拟内存的大小,包括进程使用的库,代码,数据,堆栈空间。

    VIRT = SWAP + RES;

  • RES:进程当前使用的内存大小,包括堆栈空间,不包括swap out的量,包括了其他进程的共享内存,只统计加载的库文件的大小。

    RES = CODE + DATA

  • SHARE: 除了自身进程的共享内存,也包括其他进程的共享内存,包含了整个共享库的大小。

计算某个进程所占用的物理内存:RES-SHR。

swap以及swap out/in:在linux的SWAP分区,当实际内存不够使用的时候让进程能够运行起来,比如系统刚刚启动的时候,加载的内存过大,但完成后大部分可以不再使用就可以out到swap分区。

4.死锁

4.1死锁产生的条件

  • 互斥条件
  • 部分分配
  • 不可剥夺条件
  • 环路条件

4.2死锁的解决办法

​ 死锁可以预防、避免、检测和解除。死锁预防直接打破四个产生的条件之一即可(但都不合适)。死锁避免采用银行家算法。死锁解除:可以杀掉所有进程或争夺临界变量的进程。

5.生产者消费者模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<iostream>
#include<queue>
#include<mutex>
#include<condition_variable>
#include<Windows.h>

using namespace std;

#define MAXSIZE 20
#define threadNum 2
condition_variable produce,consume;
mutex m;
queue<int> q;

void consumer()
{
while(true)
{
Sleep(500);
unique_lock<mutex> lock(m);
while(q.empty())
{
consume.wait(lock);
}
int temp=q.front();
q.pop();
cout<<"consume:"<<temp<< " size:"<<q.size()<endl;
produce.notify_all();
lock.unlock();
}
}

void producer(int i)
{
while(true)
{
Sleep(100);
unique_lock<mutex> lock(m);
while(q.size() == MAXSIZE)
{
produce.wait(lock);
}
q.push(i);
cout<<"produce: "<<i<<" size:"<<q.size()<<endl;
consume.notify_all();
lock.unlock():
}
}
int main()
{
thread p[threadNum], c[threadNum];
for (int i = 0; i < threadNum; i++)
{
p[i] = thread(producer, i);
c[i] = thread(consumer);
}
for (int i = 0; i < threadNum; i++)
{
p[i].join();
c[i].join();
}
system("pause");
return 0;
}

七、计算机网络

1.七层、五层、四层网络模型

五层协议:

  • 应用层:HTTP,FTP,TFTP,SMTP,DNS,Telnet
  • 传输层:TCP、UDP
  • 网络层:IP,ICMP,IGMP,RIP,ARP,RARP
  • 数据链路层:CSMA/CD
  • 物理层:IEEE802.2

七层协议:

  • 应用层:FTP、HTTP
  • 表示层:Telnet
  • 会话层:SMTP,DNS
  • 传输层
  • 网络层
  • 数据链路层
  • 物理层

2.TCP/IP协议

https://juejin.im/post/6844903958624878606

2.1 三次握手

pic

2.2 四次挥手

pic

2.3 拥塞控制

  • 慢启动:发送方维持一个拥塞窗口(cwnd),单位为MSS(最大报文段长度,1046),指数增加,一直到慢开始的门限(ssthresh)。
  • 拥塞避免:
    • 加法增大:每经过一个RRT,拥塞窗口+1,ssthresh+1;
    • 乘法减少:一旦出现网络拥塞(比如丢包),将慢开始的门限(ssthresh)设置为原来的一半,然后将cwnd设置为1,重新执行慢开始算法(较低的起点,指数级增长)。
  • 快速重传和快速恢复:为了减少因为拥塞导致的数据包丢失带来的重传时间。
    • 快速重传的机制:接收方如果一个包丢失,则对后续的包继续发送针对该包的重传请求。当发送方接收到三个一样的确认(连续4个相同的ACK,标志数据段丢失),就知道该包出现错误,立刻开始重传该包。并且发送方开始执行快速恢复机制
    • 快速恢复:慢开始门限(ssthresh)减半,cwnd设置为慢开始门限减半后的数值,执行拥塞避免算法(高起点,线性增长)。

2.4 流量控制

​ TCP采用滑动窗口机制实现流量控制。有一个重传计时器(报文丢失)和一个持续计时器(针对0大小窗口)。在接收端的累计确认报文中会带有窗口大小。

https://www.bilibili.com/video/BV1Lb411G7J1?from=search&seid=9083482255869466301(十分详细)

1598433211610

2.5 为什么等待2MSL

​ 因为服务器端在选择断开连接时,会发送报文给客户端,然后客户端收到信息后要发送给服务器端我已经知道你要断开,但可能这个客户端发送的信息在网络过程中断掉了,服务器端没有收到,当服务器端在1个MSL没有收到后,会继续发给客户端我要断掉联系的请求,所以TCP的客户端需要等待2MSL以防止没有真正断掉。在linux中2MSL是2min。FIN是1min。

2.6 time_wait过多怎么解决?

​ 修改内核参数,包括开启SYN cookie,可防范少量SYN攻击;开启TCP连接中TIME-WAIT sockets的快速回收。开启重用,允许将TIME-WAIT sockets重新用于新的TCP连接。

2.7 有很多的close_wait怎么解决?

设置线程超时自动回收。出现close_wait基本上就是出bug了。

2.8 为什么要进行第三次握手?

​ 为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

​ 如果服务器端就直接创建了这个连接并返回包含SYN、ACK和Seq等内容的数据包给客户端,这个数据包因为网络传输的原因丢失了,丢失之后客户端就一直没有接收到服务器返回的数据包。客户端可能设置了一个超时时间,时间到了就关闭了连接创建的请求。再重新发出创建连接的请求,而服务器端是不知道的,如果没有第三次握手告诉服务器端客户端收的到服务器端传输的数据的话,

​ 还有一种情况是已经失效的客户端发出的请求信息,由于某种原因传输到了服务器端,服务器端以为是客户端发出的有效请求,接收后产生错误。

2.9 半连接队列和全连接队列

​ 服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列

​ 已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

2.10 TCP三次握手协商了哪些东西?

​ 滑动窗口大小,序列号。

2.11 网络字节序

​ 大端模式:最高有效位存在最低内存地址处,最低有效位存储在最高内存地址处;

​ 小端模式:最高有效位存在最高内存地址处,最低有效位存储在最低内存地址处;

​ TCP、UDP、IP协议规定网络字节序采用的是大端字节序

2.12 TCP半包粘包问题?

  • 产生原因:

    • 发送方原因:TCP 默认会使用 Nagle 算法。只有以下两个任一满足才会发送:已发送的数据都已经收到确认应答时;可以发送最大段长度(MSS)的数据时。
    • 接收方原因:TCP 接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP 将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收到的分组。
  • 解决办法:发送方关闭Nagle算法,接收方在应用层进行处理(不断从 TCP 缓冲区中读取数据,每次读完都需要判断是否是一个完整的数据包,根据数据的长度,或者转义符(\n))。

3.http协议(80端口)

3.1 返回码

  • ​ 1xx:请求已经被服务器接收,继续处理

  • ​ 2xx:成功,请求已经被服务器接收、理解并接收

  • ​ 3xx:重定向,需要进行后续操作才能完成这一操作

    • 301:永久性重定向
    • 302:临时性重定向
    • 303:和302功能相同,但要求客户端采用get方法获取资源。
  • ​ 4xx:请求含词法错误或无法正确被执行。

    • 403:拒绝提供服务;
    • 401:表示发送的请求需要认证信息,或者认证已经失败。
    • 404:无法找到指定位置的资源;
    • 400:请求出现语法错误。
  • ​ 5xx: 请求了正确的服务器,但服务器处理出错

    • 500:服务器正在执行请求时发生错误。
    • 503:服务器超负载或者正在停机维护,无法处理请求。
    • 504:网关超时。表示代理服务器不能及时从远程服务器获得应答。

3.2 输入一个网址的全过程

​ DHCP获取本机IP地址,子网掩码,DNS服务器的IP地址、网关的IP地址和子网掩码等,ARP获取网关MAC地址

  1. 浏览器查找对应的IP地址(递归进行DNS);
  2. 浏览器根据查询到的IP地址向web服务器发送http请求,建立TCP连接;
  3. 服务器收到请求并处理;
  4. 服务器返回响应;
  5. 浏览器对返回的结果进行解析,渲染显示;
  6. 页面显示完成后,浏览器发送异步请求请求其他数据。

3.3 http2.0/1.1/1.0区别

  • http1.0: 连接无法复用head of line blocking(队头阻塞)两个问题,队头阻塞是由于首个packet由于它的目的端口忙而被延迟转发,导致后面的packet被阻塞。
  • http1.1:默认为带流水线的持久连接,而且客户端可以不等数据回来就发送下一个请求。支持同时打开多个TCP连接、支持虚拟主机、新增状态码100、支持分块传输编码、新增缓存处理指令max-age。
  • http2.0: 多路复用二进制分帧服务器推送(主动推送客户端需要的资源,比如CSS,JS)、首部压缩(之前的头部太大,cookie太长)、httpsstream优先级(不同的重要性不同,可能还会存在依赖)。
    • 首部压缩采用的是HPACK编码,有四种方法,动态表(未出现的header),静态表(常用的61个header),静态Huffman,整数编码(整数有自己的编码方式)。

Chrome下最多只能有6个TCP连接。

3.4 get和post请求区别

  • get回退无害,post回退会再次提交请求;
  • get请求会被浏览器主动cache,post不会;
  • get请求参数会被完整保存到浏览器历史记录中,而post不会;
  • get请求URL中传送参数有长度限制,post参数无长度限制;
  • get直接暴露在URL上,post虽然在主体,也可能被抓包查看;
  • get只支持ASCII字符,post无限制。
  • get请求在URL上传递,post在请求主体中。

3.5 其他的http请求

  • options:使服务器传回该资源支持的所有方法;
  • head:和get一样向服务器发送指定资源的请求,但服务器不返回资源的文本部分。使用该选项,不必传输全部内容就可以获取关于该资源的信息。比如URL是否有效,以及资源更新的日期时间等。
  • put:向指定资源上传最新的内容。不带验证机制
  • delete:请求服务器删除request的URI所标识的资源;不带验证机制
  • trace:回显服务器收到的请求,主要用于调试诊断;
  • connect:http1.1中预留给能够将连接改为管道方式的代理服务器,通常用于SSL加密服务器的连接。

3.6 跨域的http请求是什么样的过程?

​ 跨域的http请求:在网络层,要是再一个内网可以直接发送,如果不是还要查询路由表寻找路由,或者默认网关,路由器收到数据后,再次为远程主机或网络查询路由,超过最大跳数就丢包。如果进入外网,会有NAT转换,改成路由器的公网IP。ARP协议获取MAC地址,将IP地址映射成MAC地址,可以将其划为网络层。BGP外部网关协议来控制路由的传播和选择最佳路由。数据链路层MAC寻址。

3.7 http的强缓存和协商缓存

3.7.1 区别:
  • 强缓存:直接从本地副本比对读取,不去请求服务器,返回的状态码是 200
  • 协商缓存:会去服务器比对,若没改变才直接读取本地缓存,返回的状态码是 304
3.7.2 强缓存
  • expires缓存字段(http1.0):表示资源的过期时间(格林尼治时间),当再次请求该资源的时候,大于时间戳就过期了,小于时间戳直接使用缓存资源。
  • cache-control(http1.1):表示该资源过了多少秒后失效。当客户端请求资源的时候,发现该资源还在有效时间内则使用该缓存,它不依赖客户端时间cache-control 主要有 max-ages-maxagepublicprivateno-cacheno-store 等值。(分为浏览器缓存和代理服务器缓存)no-cache代表协商缓存,no-store代表不采用缓存。
  • pragma:他的值有 no-cacheno-store,表示意思同 cache-control,优先级高于 cache-controlexpires
3.7.3 协商缓存
  • last-modified: 记录资源最后修改的时间。启用后,请求资源之后的响应头会增加一个 last-modified 字段。当再次请求该资源时,请求头中会带有 if-modified-since 字段,值是之前返回的 last-modified 的值。服务端会对比该字段和资源的最后修改时间,若一致则证明没有被修改,告知浏览器可直接使用缓存并返回 304;若不一致则直接返回修改后的资源,并修改 last-modified 为新的值。

    • 缺点:只要编辑了,不管内容是否真的有改变,都会以这最后修改的时间作为判断依据,当成新资源返回,从而导致了没必要的请求响应,而这正是缓存本来的作用即避免没必要的请求。

      时间的精确度只能到秒,如果在一秒内的修改是检测不到更新的,仍会告知浏览器使用旧的缓存。

  • etag:基于资源的内容编码生成一串唯一的标识字符串,只要内容不同,就会生成不同的 etag。启用 etag 之后,请求资源后的响应返回会增加一个 etag 字段。但每次生成标识字符串会增加服务器的开销。

3.7.4 访问刷新分析
  • 标签进入,输入URL进入:走强缓存路线。若强缓存过期,采用协商缓存,若协商缓存过期,直接返回最新的资源,并更新last-modified。
  • 按刷新按钮、F5刷新、网页右键“重新加载”:走协商缓存路线,将cache-control的max-age设置为0;
  • ctrl+F5强制刷新:不采用缓存策略,直接设置为no-cache,强制访问最新资源。

3.8 使用HTTP长链接有哪些优点?

  • 减少握手次数
  • 减少慢启动的影响(每次连接都要通过慢启动达到一个合适的收发速率)。
  • 缺点:TCP是字符流协议,顺序不能乱,在一个长链接上发送多个请求,就是串行的,串行就会会产生队头阻塞。

3.9 cookie和session?cookie怎么保存?session怎么跟踪用户?session机制?

​ 使用 Session 维护用户登录状态的过程如下:

  • 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
  • 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID;
  • 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
  • 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。
  • session ID应该是安全的,不易被获取的。此外还需要经常重新生成sessionID。

3.10 session跨域问题?

  • 采用token,由客户端来维持状态,信息存储在客户端内,具有平台无关性。Token实质上是服务端给客户端的一个字符串,上面包含着一些验证信息,相当于一个身份令牌,你拿着这个令牌就能得到他的服务。相比较于 Cookie,Token 更加的灵活,可以在任何地方生成,基于 Token 的权限系统是非常容易实现的。Token传输采用https,此外采用对称加密。
  • 采用redis服务器。将所有的session集中存储在一个redis的服务器中,当需要访问的时候都去这个服务器查询,为了防止宕机可以做成集群(但会增加一次网络开销)。

3.11 浏览器禁用cookie?

​ 此时无法采用cookie来保存用户信息,只能采用session。除此之外,不能将session ID存放到cookie中,而是采用URL重写技术,将session ID作为URL的参数进行传递。

4.Https(443端口)

4.1https建立连接流程

  1. 浏览器将自己支持的一套加密规则发送给网站。

  2. 服务器端从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。

  3. 客户端验证证书的合法性,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。 使用约定好的HASH计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。

  4. 服务器端使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。 使用密码加密一段握手消息,发送给浏览器。

  5. 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

    HASH算法是为了验证数据的完整性。浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据。

4.2 https如何做到安全连接

​ 对称加密和非对称加密。

5.DNS

采用的是UDP协议。

DNS解析方法分为本地解析和集中式域名解析:

  • 本地解析:在客户端维护一个静态文件
  • 集中式域名解析:在DNS服务器上递归解析。

6.ping

​ 采用ICMP协议,位于网络层,而端口号信息位于传输层,因此没有端口号;此外ICMP采用IP协议发送,因此是一种尽力而为的传输。

7.TCP与UDP优缺点和区别

  • TCP
    • 优点:可靠、稳定。
    • 缺点:慢、效率低、占用系统资源高,易被攻击(DOS、DDOS等)。
  • UDP
    • 优点:快,比TCP稍安全
    • 缺点:不可靠、 不稳定
  • TCP和UDP的区别:
    • TCP面向连接、UDP无连接;
    • TCP可靠传输、UDP不可靠
    • TCP面向字节流、UDP面向数据报
    • TCP保证数据顺序、UDP不保证

8.NAT是什么?底层怎么实现的?

​ NAT是网络地址转换协议,有助于减缓IP地址枯竭,有效避免来自网络外部的攻击,隐藏并保护网络内部计算机。(带宽分享,安全防护)

​ 实现方式(三种):

  • 静态转换:IP地址一对一
  • 动态转换:IP地址不确定
  • 端口多路复用:采用的最多,改变外出数据包的源端口并进行端口转换,即端口地址转换。

9.能介绍一下多播是怎么实现的吗?

  • 多播目前在局域网使用的多。ARP攻击就是多播。
  • 多播分为广播和组播。
    • 广播和多播好处:对发送端性能提升很大,对带宽使用更少。
    • MAC地址全1就是广播。

10.服务器的最大并发连接数是多少?

​ 2^48

11.如何实现高并发?

​ 处理一个请求的粒度特别小,不要太长

12.linux下的网络的高并发优化?

八、Linux

1.常用的操作命令

1.1查看端口(netstat)

netstat -tunlp |grep port:查看指定端口号进程情况,可以看到本地地址,外部地址,采用的协议,端口状态(listen),进程的PID。

t:TCP

u:UDP

n:直接使用IP地址,不通过DNS服务器

l:监控中的服务器socket

p:显示正在使用的Socket的程序识别码和程序名

1.2查看进程(top)

可以看到PID,进程所有者,进程优先级,虚拟内存,常驻内存,共享内存大小(kb),进程的状态(D:不可中断的睡眠状态,R运行,S睡眠,T跟踪/停止,Z僵尸进程),进程的使用CPU时间,进程使用的物理内存、进程的名称(命令行/命令名)

2.Linux进程地址空间的布局

​ 虚拟内存、分页、页表

3.linux内核架构

1598001918577

4.写时复制原理?

只有在修改(写)的时候,才会进程分离

1598002280675

5.linux中如何创建进程?进程创建后如何区分子进程?

​ LInux中使用fork()函数创建进程,创建完毕后,fork()执行一次有两个返回值。在父进程中,返回值是子进程号,在子进程中返回的是0。因此可以通过返回值确定当前进程是父进程还是子进程。

6.fork()创建的子进程继承了父进程哪些内容?

​ 使用fork()函数获得的子进程是父进程的一个复制品,它从父进程那里复制了整个进程的地址空间,包括进程的上下文,进程的堆栈信息,内存信息,打开的文件描述符,信号控制设定,进程的优先级,进程组号,当前工作目录,根目录,资源限制,控制终端等。子进程独有的只是它的进程号、资源使用和计时器等。

7.fork()创建的子进程继承了父进程打开的文件描述符,如何让这种继承不发生?

​ 使用clone()系统调用使用参数在复制时将父进程的资源有选择性的复制给子进程。

​ 使用vfork(),子进程和父进程共享地址空间,也就是说子进程对某个变量的修改会影响父进程。而且vfork()创建的子进程必须显式调用exit()函数来结束,否则子进程不能结束,这个和fork()不一样。

​ 用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因此它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。

8.静态库和动态库的区别?

​ 静态库在程序编译的时候直接被链接到目标代码中,在程序运行过程中不再需要静态库;动态库在程序编译的时候不会被链接,而是在运行程序的时候被载入,因此程序运行还需要动态库的存在。

9.linux的内核态和用户态?

​ 如果一个进程要在用户态使用内核态的功能(系统调用、异常、外围设备的中断),就需要进行系统调用从而陷入内核,由操作系统代为完成。

linux常见的系统调用:

  • 进程控制:fork();exit();wait();
  • 进程通信:pipe();shmget();mmap();
  • 文件操作:open();read();write();
  • 设备操作:ioctl();read();write();
  • 信息维护:getpid();alarm();sleep();
  • 安全:chmod();umask();chown();

九、项目

1.ROS项目

2.JavaWeb项目

2.1非对称加密算法?MD5

​ 非对称加密算法:RSA,ECC;

​ 对称加密算法:DES,AES

​ MD5:不可逆,hash。