后台开发面试题.md

面试题

一、C++

1.c/c++中static的作用

(1)在修饰变量时,static修饰的静态局部变量只执行初始化一次(在程序执行到该变量声明处时被首次初始化,即以后的函数调用不再进行初始化),而且延长了局部变量的生命周期,直到程序运行结束以后才释放,只有运行包含此静态局部变量时候才能访问它。

(2)static修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其他文件中访问,即便是extern外部声明也不可以,这样其他文件中就可以定义相同名字的变量,不会发生冲突。

(3)static修饰一个函数,则这个函数只能在本文件中调用,不能被其他文件调用,static修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存,初始化的时候自动化为0

(4)static修饰的类变量,可以通过类名.变量名直接引用,而不需要new一个类来,static修饰的类方法,可以通过类名.方法名直接引用,而不需要new出一个类来。被static修饰的类变量和类方法属于类的静态资源,是类实例之间共享的,类的静态成员属于整个类而不是某个对象,静态成员只存储一份供所有对象共用,所以在所有对象中都可以共享它,使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。不能通过类名来调用类的非静态成员函数,因为类的非静态成员必须必须在类实例化对象后才有内存空间,静态成员函数中不能引用使用非静态函数

2.c/c++中const的作用

(1)const修饰普通类型的变量其值不允许修改,const修饰指针变量有三种情况,第一种const修饰指针指向的内容,则内容为不可变量。第二种const修饰指针,则指针为不可变量,const修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量.

(2)const参数传递和函数返回值,值传递的const修饰传递,一般不需要const修饰,因为函数会自动产生临时变量复制实参值;当const参数为指针时,可以防止指针被意外篡改;自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此采取const对加引用传递的方法,对于一般的Int、double等内置类型,不采用引用的传递方式。对于const修饰内置类型的返回值,修饰与不修饰返回值作用一样;const修饰的自定义类型作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改;const修饰返回的指针或者引用,是否返回一个指向const的指针,取决与我们想让用户干什么。

(3)const修饰类成员函数,其目的是防止成员函数修改成员变量的值,如果我们不想修改一个调用对象成员变量的值,所有的成员函数都应当声明为const成员函数,如果有成员函数想修改对象某一个成员变量的值,可以用mutable关键字修饰这个成员变量。

3.const和static可以同时修饰一个函数吗?

​ const关键字不能与static关键字同时使用,因为static关键字修饰静态成员函数,静态成员函数不含有this指针,既不能实例化,const成员函数必须具体到某一实例。

4.malloc和new的区别

(1)malloc和free是C++/C语言的标准库函数,new和delete是C++的运算符,它们都可以申请和释放内存;

(2)对于非内部数据类型的对象而言,光用malloc和free无法满足动态对象的要求,对象的创建的同时要自动执行构造函数,对象的消亡之前要自动执行析构函数,由于malloc/free是库函数而不是运算符,不能执行构造函数和析构函数。

(3)new是从自由存储区上为对象动态分配内存空间,而malloc从堆上动态分配内存,自由存储区不仅可以是堆,还可以是静态存储区;

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

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

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

(7)new和delete可以被重载,malloc和free不可以

5.什么是多态?有什么作用?

​ 多态就是不同对象对同一行为会有不同的状态(举例:学生和成人都去买票时,学生会打折,成人不会),实现多态有两个条件:

(一)是虚函数重写,重写就是用来设置不同状态的。

(二)对象调用虚函数时必须是指针或者引用,用父类的指针指向子类的对象

​ 多态就是一个接口多种实现,多态是面向对象的三大特性之一,多态分为静态多态和动态多态,静态多态包含函数重载和泛型编程,静态多态是程序调用函数,编译器决定使用哪个可执行的代码块,动态多态是由继承机制以及虚函数实现的,通过指向子类的父类指针或者引用,访问子类中同名重写成员函数,多态的作用就是把不同子类对象都当做父类来看,可以屏蔽不同子类之间的差异,从而写出通用的代码,做出通用的编程,以适应需求的不断变化,有程序解耦合的作用。

6.什么是虚函数?

虚函数是带有virtual关键字的成员函数,子类有个和父类完全相同的虚函数,就称子类虚函数重写父类虚函数

7.重载、重写和隐藏

​ 重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

​ 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

​ 重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

8.多态的原理

​ 多态是用虚函数表实现的,有虚函数的类都会生成一个虚函数表,这个表在编译时生成,虚函数表是一个存储虚函数地址的数组,以NULL结尾,如果要生成子类虚函数表。要经过三个步骤,第一步将父类虚表内容拷贝到子类虚表上,第二步将子类重写的虚函数地址覆盖掉表中父类的虚函数地址,第三步如果子类有新增加的虚函数,按声明次序加到最后

9.多态如何调用

​ 满足多态的函数调用,程序运行起来后,根据对象中的虚函数表指针来找实际应该调用的函数,而不满足多态的函数在函数编译时就确定函数地址了

10.inline函数可以是虚函数吗?

​ 不可以,因为inline函数没有地址,无法将它存放到虚函数表中,放在虚函数表中的函数才是虚函数

11.静态成员可以是虚函数吗?

​ 不可以,因为静态成员函数没有this指针,使用 :: 成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表

12.构造函数可以是虚函数吗?

​ 不可以,因为对象中的虚函数指针是在对象构造的时候初始化的,从存储空间角度,虚函数对应一个指向虚函数表的指针,可是这个指向虚函数表的指针其实是存储在对象的内存空间的,问题出来了,如果构造函数是虚的,就需要通过虚函数表来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找虚函数表呢?所以构造函数不能是虚函数。

13.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

​ 可以,最好是将父类的析构函数设置为虚函数,因为这样可以避免内存泄漏的问题,如果是多态的,会先去调用子类的析构函数,然后再去调用父类的析构函数,不然只会去调用父类的析构函数,造成内存泄漏问题。

​ 析构函数的作用与构造函数正好相反,是在对象的生命期结束时,释放系统为对象所分配的空间,既要撤销一个对象,用对象指针来调用一个函数,有以下两种情况:

​ 1.如果是虚函数,会调用派生类中的版本(在有派生类的情况下)

​ 2.如果是非虚函数,会调用指针所指类型的实现版本

​ 析构函数也会遵循以上两种情况,因为析构函数也是函数嘛,不要把它看得太特殊,当对象出了作用域或是我们删除对象指针,析构函数就会被调用,当派生类对象出了作用域,派生类的析构函数会先调用,然后再调用它父类的析构函数,这样能保证分配给对象的内存得到正确释放,但是,如果我们删除一个指向派生类对象的基类指针,而基类析构函数又是非虚的话,那么就会先调用基类的析构函数

14.对象访问普通函数快还是虚函数快?

​ 如果是普通对象,是一样快,如果是指针对象或者是引用对象,调用普通函数更快一些,因为构成了多态,运行时调用虚函数要先到虚函数表中去查找,这样然后才拿到函数的地址,这样就不如直接可以拿到函数地址的普通函数快。

15.虚函数表是在什么阶段生成的?它存放在哪里?

​ 虚函数表是在编译阶段生成的,它一般存放在代码段,也就是常量区

16.虚函数表指针被编译器初始化的过程是怎么理解的?

​ 当类中声明了虚函数时,编译器会在类中生成一个虚函数表,VS编译器是存放在代码段,虚函数表实际上就是一个存放虚函数指针的指针数组,是由编译器自动生成并维护的,虚表是属于类的,不属于某个具体的对象,一个类中只需要一个虚表即可,同一个类中的所有对象使用同一个虚函数表,为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器会在每个对象的头添加一个指针,用来指向虚函数表,并且这个指针的值会自动被设置成指向类的虚函数表,每一个virtual函数的函数指针存放在虚表中,如果是单继承,先将父类的虚表添加到子类的虚表中,然后子类再添加自己新增的虚函数指针,但是在VS编译器中我们通常看不到新添加的虚函数指针,是编译器故意把它们隐藏起来,如果是多继承,在子类新添加的虚函数指针会存放在第一个继承父类的虚函数表中。

17.为什么要引入抽象类和纯虚函数?

​ 为了方便使用多态特性,在很多情况下由基类生成对象是很不合理的,纯虚函数在基类是没有定义的,要求在子类必须加以实现,这种包含了纯虚函数的基类被称为抽象类,不能被实例化,如果子类没有实现纯虚函数,那么它也是一个抽象类

18.虚函数和纯虚函数有什么区别?

​ 从基类的角度出发,如果一个类中声明了虚函数,这个函数是要在类中实现的,它的作用是为了能让这个函数在他的子类中被重写,实现动态多态。纯虚函数,只是一个接口,一个函数声明,并没有在声明他的类中实现,对于子类来说它可以不重写基类中的虚函数,但是他必须要将基类中的纯虚函数实现,虚函数既继承接口的同时也继承了基类的实现,纯虚函数关注的是接口的统一性,实现完全由子类来完成。

19.什么函数不能声明为虚函数

​ 1.普通函数(非成员函数)只能重载,不能被覆盖,不能被声明为虚函数,因此,编译器会在编译时绑定函数

​ 2.静态成员函数不能是虚函数,因为静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,它不归某个对象所有,所以它也没有动态绑定的必要性

​ 3.内联成员函数不能是虚函数,因为内联函数本身就是为了在代码中直接展开,减少函数调用花费的代价而设立的,而虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的,再说,inline函数在编译时被展开,虚函数在运行时才能动态的绑定函数

​ 4.构造函数之所以不能是虚函数,因为构造函数本来是为了明确初始化对象成员才产生的,然而虚函数主要是为了在不完全了解细节的情况下也能正确处理对象,另外虚函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用虚函数来完成你想完成的动作呢?

​ 5.友元函数,C++语言不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法,友元函数不属于类的成员函数,不能被继承,所以友元函数不能是虚函数

20.关键字const是什么含义

​ 1.欲阻止一个变量被改变,可以使用const关键字,在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了

​ 2.对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const

​ 3.在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值

​ 4.对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量

​ 5.对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为左值

21.C++中static有什么作用

​ 1. 隐藏。当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,故使用static在不同的文件中定义同名函数和同名变量,而不必担心命名冲突

​ 2. static的第二个作用是保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化,共有两种变量存储在静态存储区,全局变量和static变量

​ 3. static的第三个作用是默认初始化为0,其实全局变量也具备这一属性,因为全局变量也存储在静态数据区,在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量

18.在C++程序中调用被C编译器编译后的函数,为什么要加extern “C”声明

​ 函数和变量被C++编译后在符号库中的名字与C语言的不同,被extern “C”修饰的变量和函数是按照C语言方式编译和连接的,由于编译后的名字不同,C++程序不能直接调用C函数,C++提供了一个C连接交换指定符合extern “C”来解决这个问题

23.delete []arry 和delete arry的区别

​ delete []arry释放的是多个同一类型的地址空间

​ delete arry释放的是一个某种类型的地址空间

24.C++的浅拷贝和深拷贝

在使用一个对象对另一个对象初始化或赋值时,若对象包含指针成员变量,则需要手动的编写拷贝构造函数实现深拷贝,调用编译器内部默认的拷贝构造函数只能实现浅拷贝操作

​ 一、浅拷贝问题
(1)如果类中叧包含简单数据成员,没有指向堆的指针, 可以使用编译器提供的默认复制构造函数

(2)如果类中包含指向堆中数据的指针或引用,浅拷贝将出现 严重问题

​ ①浅拷贝直接复制两个对象间的指针成员,导致两个指针指向堆中同一坑内存区域

​ ② 一个对象的修改将导致另一个对象的修改

​ ③ 一个对象超出作用域,将导致内存释放,使得另一个对象的指针无效,对其访问将导致程序异常。
​ 二、深拷贝问题

拷贝的时候先开辟出和源对象大小一样的空间,当他遇到指针的时候,他会知道new出来一块新的内存,然后把原来指针指向的值拿过来,这样指针和源对象的指针就指向了不同的内存位置,并且里面的内容是一样的,这样就不会出现重复释放同一块内存的错误。

25.vector和list的区别

​ vector:连续存储的容器,动态数组,在堆上分配空间,底层实现是数组,两倍容量增长。

​ 适应场景:经常随机访问,且不经常对非尾节点进行插入删除

​ list:动态链表,在堆上分配空间,每插入一个元素都会分配空间,每删除一个元素都会释放空间,底层是双向链表

​ 适应场景:经常插入删除大量数据

​ 区别:

​ (1)vector底层实现是数组,list是双向链表

​ (2)vector支持随机访问,list不支持

​ (3)vetcor是顺序内存,list不是

​ (4)vector在中间节点进行插入删除会导致内存拷贝,list不会

​ (5)vector一次性分配好内存,不够时才进行2倍扩容,list每次插入新节点都会进行内存申请

​ (6)vector随机访问性能好,插入删除性能差,list随机访问性能差,插入删除性能好

26.C++的vector的释放内存的几种方式

​ vector与其他容器不同,其内存空间只会增长,不会减小

​ 待释放vector: vector myvector

​ 1.swap方法

​ vector().swap(myvector)

​ 2.clear + shrink_to_fit

​ myvector.clear();

​ myvector.shrink_to_fit();

27.内存泄漏

​ 当我们用new或者malloc申请了内存,但是没有用delete或者free及时的释放了内存,结果导致一直占据该内存,内存泄漏形象的比喻是“操作系统可提供给所有进程的存储空间被某个进程榨干”,最终结果是程序运行时间越长,占用存储空间越来越多,最终用尽全部存储空间,整个系统崩溃。

​ 程序结束后,会释放其申请的所有内存,这样是可以解决问题,但是你的程序还是有问题的,比如写了一个函数,申请了一块内存,但是没有释放,每调用一次你的函数就会白白浪费一些内存,如果你的程序不停的在运行,就会有很多内存被浪费,最后可能你的程序会因为用掉内存太多而被操作系统杀死

​ 解决方法:智能指针——一种类似指针的数据类型,将对象存储在智能指针中,可以不需要处理内存泄漏的问题,它会帮你调用对象的析构函数自动撤销对象(主要是智能指针自己的析构函数用了delete ptr,delete会自动调用指针对象的析构函数,前提该内存是在堆上,如果是在栈上就会出错),释放内存,你要做的就是在析构函数中释放掉数据成员的资源

28.C++的智能指针

​ 智能指针(smart pointer)其实不是一个指针。它就是用来帮助我们管理指针,维护其生命周期的类。因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

​ 智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象,当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏,C++11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用,该引用计数的内存在堆上分配,当新增一个时引用计数加1,当过期时引用计数减1,只有引用计数为0时,智能指针才会自动释放引用的内存资源,对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类,可以通过make_shared函数或者通过构造函数传入普通指针,并可以通过get函数获得普通指针

​ 三种智能指针,shared_ptr、unique_ptr、weak_ptr。

​ (1)unique_prt 只允许基础指针的一个所有者,可以移到新所有者(具有移动语义),但不会复制或共享(即我们无法得到指向同一个对象的两个unique_ptr),无法进行复制构造,无法进行复制赋值操作,既无法使两个unique_ptr指向同一个对象,但是可以进行移动构造和移动赋值操作(所有权转让),保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象,我们知道auto_ptr通过复制构造或者通过=赋值后,原来的auto_ptr对象就报废了.所有权转移到新的对象中去了.而通过shared_ptr可以让多个智能指针对象同时拥有某一块内存的访问权.但假如我们不希望多个内存块被多个智能指针对象共享,同时又不会像auto_ptr那样不知不觉的就让原来的auto_ptr对象失效,可咋整呢? 就是同时只能有一个智能指针对象指向某块内存,1.无法进行复制构造与赋值操作. 2.可以进行移动构造和移动赋值操作

​ (2)shared_ptr维护了一个指向control block的指针对象,来记录引用个数。采用引用计数的智能指针。 shared_ptr基于“引用计数”模型实现,多个shared_ptr可指向同一个动态对象,并维护了一个共享的引用计数器,记录了引用同一对象的shared_ptr实例的数量。当最后一个指向动态对象的shared_ptr销毁时,会自动销毁其所指对象(通过delete操作符)。使用计数机制来表明资源被几个指针共享,可以通过成员函数use_count()来查看资源的所有者个数,拷贝构造时候,计数器会加1,当我们调用release()时,当前指针会释放资源所有权,计数减1,当计数等于0时,资源会被释放,会有死锁问题,引入weak_ptr,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。

​ (3)weak_ptr用于避免shared_ptr相互指向产生的环形结构,造成的内存泄漏。weak_ptr count是弱引用个数;弱引用个数不影响shared count和对象本身,shared count为0时则直接销毁。“循环引用”简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用。导致引用计数失效。weak_ptr用于配合shared_ptr使用,并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数器。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。构造和析构不会引起引用的增加或减少,协助shared_ptr

29.编译过程

(1)预处理(Preprocessing)——将所有的“#define”删除,并且展开所有的宏定义,处理所有条件预编译指令,处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置,删除所有的注释,添加行号和文件名标识,保留所有的#pragma编译器指令

(2)编译(Complication)——预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件

(3)汇编(Assembly)——将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令

(4)链接(Linking)—— 链接的主要内容就是将各个模块之间相互引用的部分正确的衔接起来。它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向

30.VS为什么能进行断点单步调试,原理是啥

​ 调试断点依赖于父进程和子进程之间的通信,打断点实际上就是在被调试的程序中,改变断点附件程序的代码,这个断点使得被调试的程序暂时停止,然后发送信号给父进程也就是调式器进程,然后父进程能够得到子进程的变量和状态,达到调试的目的。

​ 硬件中断:CPU有一个单独的执行序列,会一条指令一条指令的顺序执行,要处理类似I/O或者硬件时钟这样的异步事件,CPU就要用到中断,硬件中断通常是一个专门的电信号,连接到一个特殊的响应电路上,这个电路会感知中断的到来,然后让CPU停止当前的执行流,保存当前的状态,然后跳转到一个预定义的地址处去执行,这个地址上有一个中断处理例程,当中断处理例程完成它的工作后,CPU就从之前停止的地方恢复执行

​ 软件中断:CPU支持特殊指令来模拟一个中断,当执行到这个指令之后,CPU将其当做一个中断,停止当前的正常的执行流,保存状态然后跳转到一个处理例程中执行,这种’陷阱’让许多现代操作系统得以有效的完成很多复杂的任务——任务调度、虚拟内存、内存保护、调试等。

31.STL组件中,有sort函数,它同了哪些排序算法

​ 纵观STL的container,关系型container例如map和set利用RB树自动排序,不需要用到sort,stack和queue和priority_queue都有特定的出入口,不允许用户进行排序,剩下的vector,list和deque,list的迭代器属于bidirectional-iterator,剩下的vector和deque适合用sort算法,数据量大的时候,STL是sort算法采用快速排序,分段递归排序,一旦分段后的数据量小于某个门槛,为避免quick sort的递归调用带来过大的额外负担,就改用插入排序,还会改用堆排序

32.C++的四种cast转换

​ C++中的四种类型转换是:static_cast,dynamic_cast,const_cast,reinterpret_cast

​ (1)const_cast

​ 用于将const变量转换为非const

​ (2)static_cast

​ 用于各种隐式转换,比如非const转const,void*转指针等,static_cast多用于多态向上转化,如果向下转能成功但是不安全,结果未知。

​ (3)dynamic_cast

​ 用于动态类型转换,只能用于含有虚函数的类,用于类层次间的向上和向下转化,只能转指针或引用,向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。

​ 向上转换:指的是子类向基类的转换

​ 向下转换:指的是基类向子类的转换

​ 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

​ (4)reinterpret_cast

​ 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用

​ (5)为什么不使用C的强制转换?

​ C的强制转换表面上看起来功能强大什么都能转,但是转化不过明确,不能进行错误检查,容易出错

33.介绍一下STL的allocator

​ STL的分配器用于封装STL容器在内存管理上的底层细节,在C++中,其内存配置和释放如下:

​ new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容

​ delete运算分两个阶段:(1)调用对象析构函数;(2)调用::operator delete释放内存

​ 为了精密分工,STL alloctor将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。

​ 同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器,第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

34.STL迭代器删除元素

​ 主要考察迭代器失效的问题

​ (1) 对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器。

​ (2) 对于关联容器map,set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。

​ (3) 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法可以使用。

35.STL基本组成

​ STL主要由容器迭代器、仿函数算法、分配器、配接器

​ 它们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数

36.STL的迭代器的作用,有指针为何还要迭代器

​ 1.迭代器

​ iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露对象的内部表示,或者这样说可能更容易理解,iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。

​ 由于iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。

​ 2.迭代器和指针的区别

​ 迭代器不是指针,是类模板,表现的像指针,它只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、–等,迭代器封装了指针,是一个“可遍历STL容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。

​ 迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。

​ 3.迭代器产生原因

​ iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

37.说一说隐式类型转换

​ 首先对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,其次对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。

38.说说了解的类型转换

(1)reinterpret_cast:可以用于任意类型的指针之间的转换,对转换的结果不做任何保证

(2)dynamic_cast:这种其实也是不被推荐使用的,更多使用static_cast,dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常

(3)const_cast:对于未定义const版本的成员函数,我们通常需要使用const_cast来去除const引用对象的const,完成函数调用。另外一种使用方式,结合static_cast,可以在非const版本的成员函数内添加const,调用完const版本的成员函数后,再使用const_cast去除const限定。

(4)static_cast:完成基础数据类型;同一个继承体系中类型的转换;任意类型与空指针类型void*之间的转换。

38.说说RTTI

​ 运行时类型检查,在C++层面主要体现在dynamic_cast和typeid,VS中虚函数表的-1位置存放了指向type_info的指针,对于存在虚函数的类型,typeid和dynamic_cast都会去查询type_info

39.C语言是怎么进行函数调用的?

​ 每一个函数调用都会分配函数栈,在栈内进行函数执行过程,调用前,先把返回地址压栈,然后把当前函数的esp指针压栈

40.C++如何处理返回值

​ 生成一个临时变量,把它的引用作为函数参数传入函数内

41.C++中拷贝赋值函数的形参能否进行值传递

​ 不能,如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数,如此循环,无法完成拷贝,栈也会满。

42.说一下strcpy和strlen

​ strcpy是字符串拷贝函数,从src逐字节拷贝到dest,直到遇到"/0"结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy函数.

​ strlen函数是计算字符串长度的函数,返回从开始到“\0”之间的字符个数。

42.什么是右值引用,跟左值又有什么区别?

​ 右值引用是C++11中引入的新特性,它实现了转移语义和精确传递,它的主要目的有两个方面:(1)消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。(2)能够更简洁明确的定于泛型函数。

​ 左值和右值的概念:

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

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

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

​ (1)左值可以寻址,而右值不可以

​ (2)左值可以被赋值,右值不可以被赋值,可以用来给左值赋值

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

43.C++类内可以定义引用数据成员吗?

​ 可以,必须通过成员函数初始化列表初始化

二、数据结构

1.map的底层数据结构是什么?

​ Hashmap底层数据结构是哈希表,线程不安全,效率高,LinkHashMap底层数据结构由链表和哈希表组成,由链表保证元素有序,由哈希表保证元素唯一,Hashtable底层数据结构是哈希表,线程安全,效率低,TreeMap底层数据结构是红黑树(是一种自平衡的非严格的平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,TreeMap根据比较的返回值是否是0来决定保证元素唯一性,保证元素的排序方式有两种,第一种自然排序,元素具备比较性,第二种比较器排序

2.map和unorder_map的比较

map优点有序性,内部是用红黑树实现的,红黑树具有自动排序的功能,缺点空间占用率高,对于那些有顺序要求的问题,用map会更高效一些。unorder_map内部用哈希表实现的,因此查找速度快一些执行效率高一些,但是unorder_map占用内存更高一些,map的查找、删除、增加等一系列操作时间复杂度稳定都为logN,缺点就是查找、删除、增加等操作平均时间复杂度较慢,unordered_map底层是哈希表,它的查找、删除、添加的速度快,时间复杂度为常数级O(1),取决于哈希函数,极端情况可能为O(n)

3.解决哈希冲突?

​ 当哈希表关键字集合很大时,关键字的值不同的元素可能会映射到哈希表的同一个地址上,这样的现象叫哈希冲突,解决方法如下:

(1)开放定址法:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。

(2)再哈希表:当发生哈希冲突时使用另一个哈希函数计算地址值,直到冲突不再发生,这种方法不易产生聚集,但是增加计算时间,同时需要准备许多哈希函数。

(3)链地址法:将所有哈希值相同的Key通过链表存储,Key按顺序插入到链表中。

(4)建立公共溢出区:采用一个溢出表存储产生冲突的关键字,如果公共溢出区还产生冲突,再采用处理冲突方法

4.红黑树和AVL(平衡二叉树)树的定义、特点以及区别

​ 平衡二叉树左右子树都是平衡二叉树,其左右子树高度之差的绝对值不超过1,红黑树是一种二叉查找树,但在每个节点增加一个存储位表示结点的颜色,可以是红或黑,通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于要求严格的平衡二叉树,它的旋转次数少,插入最多两次旋转,删除最多三次旋转,所以对于搜索,插入,删除操作比较多的情况下,通常使用红黑树。红黑树在查找,插入删除的性能都是O(logN)

5.map和set有什么区别,分别又是怎么实现的

​ map和set都是c++的关联容器,其底层实现都是红黑树,由于map和set所开放的各种操作接口,红黑树也都提供了,所以几乎所有的map和set的操作行为,都只是转调红黑树的操作行为。

​ map和set的区别在于:

​ (1)map中的元素是key-value键值对,关键字起到索引的作用,值则表示与索引相关联的数据,set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字

​ (2)set的迭代器是const,不允许修改元素的值,map允许修改value,但不允许修改key,其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置,所以STL中将set的迭代器设置成const,不允许修改迭代器的值,而map的迭代器则不允许修改key值,允许修改value值。

​ (3)map支持下标操作,set不支持下标操作,map可以用key做下标,map的下标运算符【】将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符【】在map应用中需要慎用,const_map不能用,只希望确定某一个关键字是否存在而不希望插入元素时不应该使用,mapped_type类型没有默认值也不应该使用,如果find能解决需要,尽可能用find

5.请介绍一下B+树

​ B+树是一种多路搜索树,主要为磁盘或其他存取辅助设备而设计的一种平衡查找树,在B+树中,每个节点可以有多个孩子,并且按照关键字大小有序排列,所有记录结点都是按照键值的大小顺序存放在同一层的叶结点中,相比于B树,具有以下特点:每个结点上的指针上限为2d而不是2d+1(d为结点的出度),内结点不存储data,只存储key,叶子结点不存储指针

6.排序算法及其复n杂度

​ O(n2):直接插入排序、直接选择排序、冒泡排序

​ O(nlogN):希尔排序、堆排序、快速排序、归并排序

​ 稳定:直接插入排序、冒泡排序、归并排序

​ 不稳定:直接选择排序、希尔排序、堆排序、快速排序

8.优先队列时间复杂度

​ 优先级队列用堆实现,只是需要构建初始堆,这个时间复杂度是O(n),插入和删除只是修改了堆顶和堆底,不需要所有的都排序,只是需要再次调整好堆,因此时间复杂度都是O(log2n),堆的维护时间复杂度就是O(n)

9.红黑树如果插入和删除的

​ 插入:

​ (1)如果父节点为黑色,直接插入不处理

​ (2)如果父节点为红色,叔叔节点为红色,则父节点和叔叔节点变为黑色,祖先节点变为红色,将节点操作转换为祖先节点

​ (3)如果当前节点为父亲节点的右节点,则以父亲节点为中心左旋操作

​ (4)如果当前节点为父亲节点的左节点,则父亲节点变为黑色,祖先节点变为红色,以祖先节点为中心右旋操作

​ 删除:

​ (1)先按照排序二叉树的方法,删除当前节点,如果需要转移即将转移到下一个节点

​ (2)当前节点,必定为这样的情况,没有左子树

​ (3)删除为红色节点,不需要处理,直接按照删除二叉树节点一样

​ (4)如果兄弟节点为黑色,兄弟节点的两个子节点为黑色,则将兄弟节点变为红色,将着色转移到父亲节点

​ (5)如果兄弟节点为红色,将兄弟节点设为黑色,父亲节点设为红色节点,对父亲节点进行左旋操作

​ (6)如果兄弟节点为黑色,左孩子为红色,右孩子为黑色,对兄弟节点进行右旋操作

​ (7)如果兄弟节点为黑色,右孩子为红色,则将父亲节点的颜色赋值给兄弟节点,将父亲节点设置为黑色,将兄弟节点的右孩子设为黑色,对父亲节点进行左旋

10.什么是堆,堆排序实现原理?

堆就是利用完全二叉树的结构来维护的一维数组。堆是一种完全二叉树(不是平衡二叉树,也不是二分搜索树),完全二叉树除了最底层,每一层都是满的,这使得堆可以利用数组来表示,i结点的父结点下标就为(i-1)/2,它的左右子结点下标分别为2i+1和2i+2,最大堆和最小堆,特点是父节点的值大于(小于)两个小节点的值。

​ 堆排序的基本思想是:以最大堆为例,先根据序列还原构造一棵完全二叉树,将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点,将其与末尾元素进行交换,此时末尾为最大值,然后将剩余n-1个元素重新构造成一个大顶堆,这样根节点会是n个元素中的第二大值,将其与n-1的元素互换,将剩下的n-2个元素继续建堆,如此反复执行,便能得到一个有序的升序序列了(反之,降序则建立小顶堆),建立最大堆时是从最后一个非叶子节点开始从下往上调整的

​ 总结堆排序的基本思路:

(1)将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

(2)将堆顶元素与末尾元素交换,将最大(小)元素“沉”到数组末端;

(3)重新调整结构,使其满足最大堆或最小堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序

10.请描述C/C++程序的内存分区

​ 1. 栈区(stack)——由编译器自动分配释放,存放函数参数值,局部变量的值等,其操作方式类似于数据结构中的栈

​ 2. 堆区(heap)——一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表

​ 3. 全局区(静态区static)——全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量的在相邻的另一块区域,程序结束后由系统释放

​ 4. 文字常量区——常量字符串就是放在这里的,程序结束后由系统释放

​ 5. 程序代码区——存放函数体的二进制代码

11.栈区和堆区的区别

​ 1. 堆和栈中的存储内容,栈存储局部变量、函数参数等,堆存储使用new、malloc申请的变量等

​ 2. 申请方式:栈内存由系统分配,堆内存由自己申请

​ 3.申请后系统的响应:

​ 栈——只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出

​ 堆——首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序

​ 4.申请大小的限制:Windows下栈的大小一般是2M,堆的容量较大

​ 5.申请效率的比较:栈由系统自动分配,速度较快,堆使用new、malloc等分配,较慢

10.了解哪些设计模式?

(1)单例模式:单例模式主要解决一个全局使用的类频繁的创建和销毁的问题,单例模式下可以确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,单例模式有三个要素,一是某个类只能有一个实例,二是它必须自行创建这个实例,三是它必须自行向整个系统提供这个实例

(2)工厂模式:工厂模式主要解决接口选择的问题,该模式下定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,使其创建过程延迟到子类进行,tensorflow的模型类就很多用到工厂模式

(3)观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新

(4)装饰器模式:对已经存在的某些类进行装饰,以此来扩展一些功能,从而动态的为一个对象增加新的功能,装饰器模式是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能,使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀

三、计算机网络

1.TCP和UDP的特点和区别?

​ TCP全双工,通过三次握手建立连接,保证数据无差错连接,,面向字节流,可能出现黏包问题,TCP首部开销20字节,UDP单双工,UDP数据包括目的端口号和源端口号,不需要建立连接,UDP是面向报文的不会出现黏包问题,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低,首部只有8个字节,TCP连接只能是点到点的,UDP支持一对一,一多,多对一和多对多通信

2.为什么TCP建立连接需要三次,而释放连接则需要四次?

​ 因为TCP是全双工模式,客户端请求关闭连接后,客户端向服务器的连接关闭(一二次挥手),服务端继续传输之前没有传完的数据给客户端,服务端向客户端的连接关闭(三四次挥手),所以TCP释放连接时服务器的ACK和FIN是分开发送的(中间隔着数据传输),而TCP建立连接时服务器的ACK和SYN是一起发送的(第二次握手),所以TCP建立连接需要三次,而释放连接则需要四次

3.为什么TCP连接时可以ACK和SYN一起发送,而释放时ACK和FIN分开发送呢?

​ 因为客户端请求释放时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客户端FIN请求(服务端发送ACK),然后数据传输,传输完成后,服务端再提出FIN请求(服务端发送FIN),而连接时则没有中间的数据传输,因此连接时可以ACK和SYN一起发送

4.四次挥手客户端释放最后为什么不直接关闭要进入等待计时器设置的2MSL等待状态

(1)保证客户端发送的ACK报文段能够到达服务器,从而保证TCP连接能够进行可靠的关闭,如果客户端发送ACK后立刻关闭,那么如果ACK丢失的话,服务端就会一直处于等待关闭确认的状态,超时后再发送关闭请求时,此时的客户端已经关闭,那么服务端就无法进行正常的关闭,确保有足够的时间让服务端收到ACK包,如果服务端没有收到ACK,服务端会重发FIN包,这样正好是2MSL

(2)避免新旧连接混淆,避免跟后边的其他连接混在一起,因为有些路由器会缓存IP数据包,服务端延迟收到的包,就有可能跟新连接的包混在一起,防止已失效的连接请求报文段会出现在本连接中,TIME-WAIT持续2MSL可使本连接持续的时间内所产生的所有报文段都从网络中消失,这样可使下次连接中不会出现旧的连接报文段

5.为什么等待的时间是2MSL?

​ 客户端发出ACK,等待ACK到达对方的超时时间MSL,等待FIN的超时重传也是MSL,所以如果2MSL时间内没有收到FIN,说明对方安全收到ACK。

6.TCP协议如何保证可靠传输

​ (1)校验和

​ 发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化,如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段

​ (2)确认应答+序列号

​ 接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传,当接收端主机接收到来自客户端主机的数据包之后,接收端主机会返回一个已收到消息的通知,这个通知消息就叫做确认应答信号ACK包,TCP给发送的每一个包(报文段)进行编号,接收方对数据包进行排序,把有序数据传送给应用层

​ (3)超时重传

​ 当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段,如果不能及时收到一个确认,将重发这个报文段

​ (4)流量控制

​ TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据,当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失,TCP使用的流量控制协议是可变大小的滑动窗口协议,接收方有即时窗口(滑动窗口),随ACK报文发送

​ (5)拥塞控制

​ 当网络拥塞时,减少数据的发送。发送方有拥塞窗口,发送数据前比对接收方发过来的即时窗口,取小

7.拥塞控制的四种方式

​ 拥塞:在某段时间,若对网络中某资源的需求超过了该资源所能提供的可用部分,网络中的性能就要变坏,整个网络的吞吐量将随输入负荷的增大而下降。

​ TCP有一个端对端通告的接收窗口(rwnd),窗口值的大小就代表能够发送出去的但还没有收到ACK(确认)的最大数据报文段,显然窗口越大那么数据发送的速度也就越快,但是也越有可能使得网络出现拥塞,如果窗口值为1,那么就简化为一个停等协议,每发送一个数据,都要等到对方的确认才能发送第二个数据包,显然数据传输效率低下,TCP的拥塞控制算法就是要在这两者之间权衡,选取最好的cwnd值,从而使得网络吞吐量最大化且不产生拥塞

(1)慢开始

​ 发送方维持一个叫做拥塞窗口cwnd(congestion wondow)的状态变量,拥塞窗口的大小取决于网络的拥塞程度,并且动态的在变化,发送方让自己的发送窗口等于拥塞窗口,如再考虑到接收方的接收能力,则发送窗口还可能小于拥塞窗口(取决于接收方缓冲空间的大小),发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去,但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。

​ 当主机开始发送数据时,如果立即送大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况,因此,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时,先把拥塞窗口cwnd设置为一个最大报文段MSS的数值,而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值,用这样的方法逐步增大发送方的拥塞窗口cwnd,可以使分组注入到网络的速率更加合理,为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量来控制慢启动算法的停止。慢开始过程结束,进入拥塞避免阶段。

(2)拥塞避免

​ 让拥塞窗口cwnd缓慢的增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢的多,无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送方窗口值的一半(但不能小于2),然后把拥塞窗口cwnd重新设置为1,执行慢开始算法,这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕

(3)快重传

​ 快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。接收方收到了M1和M2后都分别发出了确认,现在假定接收方没有收到M3但接着收到了M4,显然,接收方不能确认M4,因为M4是收到的失序报文段,根据可靠传输原理,接收方可以什么都不做,也可以在适当时机发送一次对M2的确认,但按照快重传算法的规定,接收方应及时发送对M2的重复确认,这样做可以让发送方及早知道报文段M3没有到达接收方,发送方接着发送了M5和M6,接收方收到这两个报文后,也还要再次发出M2的重复确认,这样,发送方共收到了接收方的四个对M2的确认,其中后三个都是重复确认,快重传算法还规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段M3,而不必继续等待M3设置的重传计时器到期,由于发送方尽早重传被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%,不难看出,快重传并非取消重传计时器,而是在某些情况下可更早的重传丢失的报文段。

(4)快恢复

​ 当发送方连续收到三个重复确认,执行乘法减小,ssthresh减半,由于发送方可能认为网络现状没有拥塞,因此与慢开始不同,把cwnd值设置为ssthresh减半之后的值,然后执行拥塞避免算法,线性增大cwnd

8.说一下GET和POST的区别

​ 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据),而对于POST,浏览器先发送header,服务器响应100continue,浏览器再发送data,服务器响应200 ok(返回数据)

9.输入www.baidu.com在浏览器的完整过程越详细越好

​ 1. 浏览器获取输入的域名www.baidu.com

​ 2. 浏览器向域名系统DNS请求解析www.baidu.com的IP地址

​ 3. DNS解析出百度服务器的IP地址

​ 4. 浏览器与服务器建立TCP连接(默认端口号80)

​ 5. 浏览器发出HTTP请求,请求百度首页

​ 6. 服务器通过HTTP请求把首页文件发给浏览器

​ 7. TCP连接释放

​ 8.浏览器解析首页文件,展示web界面

10.socket网络编程的步骤

​ 服务端:创建socket——>bind绑定ip和端口信息——>设置最大连接数,监听listen——>接收连接accept

​ 客户端:创建socket——>设置要连接的对方ip和端口信息——>连接服务器connect

11.地址解析协议ARP

​ 无论网络层使用什么协议,在实际网络的链路上传送数据帧时,最终必须使用硬件地址,所以需要一种方法来完成IP地址到MAC地址的映射,这就是地址解析协议ARP,每个主机都设有一个ARP高速缓存,存放本局域网上各主机和路由器的IP地址到MAC地址的映射表,称ARP表,使用ARP协议来动态维护此ARP表。

ARP工作在网络层,其工作原理:当主机A欲向本局域网上的某个主机B发送IP数据报时,就先在其ARP高速缓存中查看有无主机B的IP地址,如果有,就可查出其对应的硬件地址,再将此硬件地址写入MAC帧,然后通过局域网将该MAC帧发往此硬件地址,如果没有,就通过使用目的MAC地址为FF-FF-FF-FF-FF-FF的帧来封装并广播ARP请求分组,可以使同一个局域网里的所有主机收到ARP请求,当主机B收到该ARP请求后,就会向主机A发出响应ARP分组,分组中包含主机B的主机IP与MAC地址的映射关系,主机A收到后将此映射写入ARP缓存中,然后按查询到的硬件地址发送MAC帧

​ 注意:ARP是解决同一个局域网上的主机或路由器的IP地址和硬件地址的映射问题。如果所要找的主机和源主机不在同一个局域网上,那么就要通过ARP协议找到一个位于本局域网上的某个路由器的硬件地址,然后把分组发送给这个路由器,让这个路由器把分组转发给下一个网络,剩下的工作就由下一个网络来做,尽管ARP请求分组是广播发送的,但是ARP响应分组是普通的单播,即从一个源地址发送到一个目的地址

12.动态主机配置协议DHCP

​ 动态主机配置协议DHCP常用于给主机动态的分配IP地址,它提供了即插即用联网的机制,这种机制允许一台计算机加入新的网络和获取IP地址而不用手工参与,DHCP是应用层协议,它是基于UDP的

​ DHCP协议工作原理:它使用客户/服务器方式,需要IP地址的主机在启动时就向DHCP服务器广播发送广播报文,这时该主机就成为DHCP客户,本地网络上所有主机都能收到此广播报文,但只有DHCP服务器才回答此广播报文,DHCP服务器先在其数据库中查找该计算机的配置信息,若找到,则返回找到的信息,若找不到,则从服务器的IP地址池中取一个地址分配给该计算机,DHCP服务器的回答报文叫做提供报文。

13.网际控制报文协议ICMP

​ 为了提高IP数据报交付成功的机会,在网络层使用了网际控制报文协议ICMP来允许主机或路由器报告差错和异常情况,ICMP报文作为IP层数据报的数据,加上数据报的首部,组成IP数据报发送出去,ICMP协议是IP网络层协议,ICMP报文种类有两种,即ICMP差错报文和ICMP询问报文,ICMP差错报告报文用于目标主机或到目标主机路径上的路由器向源主机报告差错和异常情况,五种类型(1)终点不可达(2)源点控制(3)时间超时(4)参数问题(5)改变路由(重定向)。ICMP询问报文有四种类型(1)回送请求和回答报文(2)时间戳请求和回答报文(3)掩码地址请求和回答报文(4)路由器询问和通告报文

​ ICMP的两个常见的应用的分组网间探测PING(用来测试两个主机之间的连通性)和traceroute(Unix中的名字,Windows中是tracert,可以用来跟踪分组经过的路由)。其中PING使用了ICMP回送请求和回答报文,traceroute使用了ICMP时间超过报文

​ 注意:PING工作在应用层,它直接使用网络层的ICMP协议,而没有使用传输层的TCP和UDP协议,traceroute工作在网络层

12.TCP的nagle算法和延迟ack,还有CORK

​ nagel算法:防止网络中存在太多小包而造成网络拥塞

​ 延迟ack:减少ACK包的频繁发送

​ CORK:将多个包变成一个包发送,提高网络利用率,使载荷率更大

​ 不可以一起使用

15.搜索baidu,会用到计算机网络中的什么层?每层是干什么的

​ 浏览器中输入URL,浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS服务器发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。得到IP地址后,浏览器就要与服务器建立一个http连接。因此要用到http协议,http协议报文格式上面已经提到。http生成一个get请求报文,将该报文传给TCP层处理,所以还会用到TCP协议。如果采用https还会使用https协议先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。TCP的数据包然后会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要直到目的IP地址的物理地址,有需要ARP协议。

(1)DNS协议,http协议,https协议属于应用层

​ 应用层是体系结构中的最高层。应用层确定进程之间通信的性质以满足用户的需要。这里的进程就是指正在运行的程序。应用层不仅要提供应用进程所需要的信息交换和远地操作,而且还要作为互相作用的应用进程的用户代理,来完成一些为进行语义上有意义的信息交换所必须的功能。应用层直接为用户的应用进程提供服务。

(2)TCP/UDP属于传输层

​ 传输层的任务就是负责不同主机中两个进程之间的通信。因特网的传输层可使用两种不同协议:即面向连接的传输控制协议TCP,和无连接的用户数据报协议UDP。面向连接的服务能够提供可靠的交付,但无连接服务则不保证提供可靠的交付,它只是“尽最大努力交付”。这两种服务方式都很有用,备有其优缺点。在分组交换网内的各个交换结点机都没有传输层。

(3)IP协议,ARP协议属于网络层

​ 网络层负责为分组交换网上的不同主机提供通信。在发送数据时,网络层将运输层产生的报文段或用户数据报封装成分组或包进行传送。在TCP/IP体系中,分组也叫作IP数据报,或简称为数据报。网络层的另一个任务就是要选择合适的路由,使源主机运输层所传下来的分组能够交付到目的主机。

(4)数据链路层

​ 当发送数据时,数据链路层的任务是将在网络层交下来的IP数据报组装成帧,在两个相邻结点间的链路上传送以帧为单位的数据。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制、以及流量控制信息等)。控制信息使接收端能够知道—个帧从哪个比特开始和到哪个比特结束。控制信息还使接收端能够检测到所收到的帧中有无差错。

(5)物理层

物理层的任务就是透明地传送比特流。在物理层上所传数据的单位是比特。传递信息所利用的一些物理媒体,如双绞线、同轴电缆、光缆等,并不在物理层之内而是在物理层的下面。因此也有人把物理媒体当做第0层。

16.HTTP和HTTPS的区别

​ HTTPS是安全的超文本传输协议,是安全版的HTTP协议,使用安全套接字层(SSL)进行信息交换

​ HTTP协议主要针对解决HTTP协议以下不足:

​ (1)通信使用明文(不加密),内容可能会被窃听,客户端和服务端都无法验证对方的身份,HTTPS是身披SSL(Secure Socket Layer)外壳的Http,运行于SSL上,SSL运行于TCP上,是添加了加密和认证的HTTP

​ (2)不验证通信方身份,因此可能遭遇伪装

​ (3)无法证明报文的完整性(即准确性),所以可能已遭篡改

​ HTTP+加密+认证+完整性保护=HTTPS

​ 主要区别:

​ (1)端口不同:HTTP和HTTPS使用不同的连接方式,用的端口也不一样,前者是80,后者是443

​ (2)资源消耗不同:和HTTP通信相比,HTTPS通信会由于加密处理消耗更多的CPU和内存资源

​ (3)开销:HTTPS通信需要证书,而证书一般需要向认证机构购买

17.为什么TCP连接需要三次握手,两次不可以么,为什么?

​ 为了防止已经失效的连接请求报文突然又传送到了服务端

​ 客户端发出的连接请求报文并未丢失,而是在某个网络节点长时间滞留了,以致延误连接释放以后的某个时间才到达Server,这时Server误以为这是Client发出的一个新的链接请求,于是就向客户端发送确认数据包,同意建立连接,若不采用“三次握手”,那么只要Server发出确认数据包,新的连接就建立了,由于Client此时并未发出建立连接的请求,所以其不会理睬Server的确认,也不与Server通信,而这时Server一直在等待Client的请求,这样Server就白白浪费了一定的资源,若采用“三次握手”,在这种情况下,由于Server端没有收到来自客户端的确认,则就会知道Client并没有要求建立请求,就不会建立连接

18.DNS和DNS劫持以及HTTP劫持

​ DNS又称域名系统,因特网上作为域名和IP地址相互映射的一个分布式数据库,可以使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串,通过主机名,最终得到该主机名对应的IP地址的过程叫做主机名解析,又称域名解析

​ DNS劫持又叫域名劫持,指攻击者利用其他攻击手段,篡改了某个域名的解析结果,使得指向该域名的IP变成了另一个IP,导致对相应网址的访问被劫持到另一个不可达的或者假冒的网址,从而实现非法窃取用户信息或者破坏正常网络服务的目的

​ HTTP劫持:在运营商的路由器节点上,设置协议检测,一旦发现是HTTP请求,而且是HTML类型请求,则拦截处理,后续做法往往分为2种,1种是类似DNS劫持返回302让用户浏览器跳转到另外的地址,还有1种是在服务器返回的HTML数据中插入js或dom节点(广告)

19.请描述IO多路复用机制

​ IO模型有4种:同步阻塞IO、同步非阻塞IO、异步阻塞IO、异步非阻塞IO;IO多路复用属于IO模型中的异步阻塞IO模型,在服务器高性能IO构建中常常用到。

​ 同步异步是表示服务端的,阻塞非阻塞表示用户端,所以可解释为什么IO多路复用(异步阻塞)常用于服务器端的原因

​ 文件描述符(FD,又叫文件句柄):描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等属性),具体来源:Linux内核将所有外部设备都看作一个文件来操作,对文件的操作都会调用内核提供的系统命令,返回一个FD(文件描述符)

​ 下面开始介绍IO多路复用:

​ (1)I/O多路复用技术通过把多个I/O的阻塞复用到同一个select、poll或epoll的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求,与传统的多线程/多进程模型相比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程

​ (2)select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

​ (3)I/O多路复用的主要应用场景如下:1.服务器需要同时处理多个处于监听状态或者多个连接状态的套接字;2.服务器需要同时处理多种网络协议的套接字;

​ (4)目前支持I/O多路复用的系统调用有select、poll、epoll

20.如果传输失败了怎么办

​ 如果此时ACK在网络中丢失,过了超时计时器后,那么Server端会重新发送SYN+ACK包,重传次数根据/proc/sys/net/ipv4/tcp_synack_retries来指定,默认是5次。如果重传指定次数到了后,仍然未收到ACK应答,那么一段时间后,Server自动关闭这个连接,但是Client认为这个连接已经建立,如果Client端向Server写数据,Server端将以RST包响应,方能感知到Server的错误。

21.延迟回应是什么?

​ 接收方在收到数据后,并不会立即回复ACK,而是延迟一定时间或者达到2*最大报文数据长度为止,不同操作系统实现并不一样,这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量,如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息,这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量

22.网络拥堵和堵塞以及流量控制?

​ 流量控制:如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失,为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制,流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面

​ 如何实现:由滑动窗口协议(连续ARQ协议)实现,滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制,主要的方式就是接收方返回的ACK中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。

​ 流量控制中的死锁:当发送者收到了一个窗口为0的应答,发送者便停止发送,等待接收者的下一个应答,但是如果这个窗口不为0的应答在传输过程丢失,发送者一直等待下去,而接收者以为发送者已经收到该应答,等待接收新数据,这样双方就相互等待,从而产生死锁。为了避免流量控制引发的死锁,TCP使用了持续计时器,每当发送者收到一个零窗口的应答后就启动该计时器,时间一到便主动发送报文询问接收者的窗口大小,若接收者仍然返回零窗口,则重置该计时器继续等待,若窗口不为0,则表示应答报文丢失了,此时重置发送窗口后开始发送,这样就避免了死锁的产生。

四、操作系统

1.进程和线程以及它们的区别

​ 进程:进程是系统进行资源的调度和分配的基本单位,实现操作系统的并发,每个进程都有一个自己的地址空间,至少有5种基本状态,它们是:初始态,就绪态,等待态,执行态,终止态。

​ 线程:线程是CPU调度和分配的基本单位,实现进程内部的并发,线程有就绪、阻塞和运行三种基本状态,计算机操作系统有两个重要的概念,并发和隔离,无非是为了尽量让硬件利用率高,线程是为了在系统层面做到并发,线程上下文切换效率比进程上下文切换会高很多,这样可以提高并发效率。计算机资源一般是共享的,隔离要能保证崩溃了的这些资源能够被回收,不影响其他代码使用。

​ 区别:

​ 根本区别:进程是资源分配的最小单位,线程是CPU调度和分配的最小单位

​ 包含关系:线程是进程的一部分,所以线程也被称为轻量级进程

​ 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间

​ 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间,而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源,同一进程内的线程共享本进程的资源如内存、I/O、CPU等,但是进程之间的资源是独立的

​ 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销,线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小

​ 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便

​ 所处环境:在操作系统中能同时运行多个进程,而在同一个进程中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

​ 健壮性方面:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮

​ 执行过程:每个独立的进程都有一个程序的入口,可以独立运行,但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

​ 并发:两者均可并发执行

2.进程之间的通信方式,线程之间的通信方式

​ 进程通信:

(1)管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,需要双方通信时,需要建立起两个管道,而且只能在具有亲缘关系的进程间使用,进程的亲缘关系通常是父子进程关系。通过内存缓冲区实现数据传输

(2)有名管道(FIFO):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。在磁盘上有对应的节点,但没有数据块,换言之,只是拥有一个名字和相应的访问权限,一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权,当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。

管道的实质是一个内核缓冲区,进程以先进先出的方式以缓冲区存取数据:管道一端的进程顺序的将进程数据写入缓冲区,另一端的进程则顺序的读取数据,该缓冲区可以看作一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后在缓冲区都不复存在了,当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写

(3)信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问,它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源,因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

(4)消息队列是一个消息的链表,是一系列保存在内核中消息的列表,用户进程可以向消息队列添加消息,也可以向消息队列读取消息,消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息,可以把消息看做是一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

(5)信号:信号用于通知接收进程某个事件已经发生。信号可以在任何时候发送给某一进程,而无须知道该进程的状态,如果该进程并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给它为止,如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程来通知用户空间发生了哪些系统事件,信号时间有两个来源:(1)硬件来源(2)软件来源

(6)共享内存:共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取操作读出,从而实现了进程间的通信,共享内存就是映射一段能被其他进程所访问的内存,和其他通信机制配合使用,如信号量,来实现进程间的同步和通信。消息队列和管道基本上都是4次拷贝,而共享内存(mmap, shmget)只有两次。

​ 4次:1,由用户空间缓冲区中将数据拷贝到内核空间缓冲区中
​ 2,内核空间缓冲区将数据拷贝到内存中
​ 3,内存将数据拷贝到到内核缓冲区
​ 4,内核空间缓冲区到用户空间缓冲区.
​ 2次: 1,用户空间到内存
​ 2,内存到用户空间。

(7)套接字:套接字用于不同设备及其间的进程通信

​ 线程通信:

(1)锁机制:包括互斥锁,读写锁、条件变量,互斥锁提供了以排他方式防止数据结构被并发修改的方法,读写锁允许多个线程同时读共享数据,而对写操作是互斥的,条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止,对条件的测试是在互斥锁的保护下进行的,条件变量始终与互斥锁一起使用

(2)信号量机制:包括无名线程信号量和命名线程信号量。

(3)信号机制:类似进程间的信号处理,线程间的通信目的主要用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

3.死锁产生的四个必要条件

(1)互斥条件:一个资源每次只能被一个进程使用。

(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3)不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。

(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

4.避免死锁的方法

​ 1.破坏请求与保持条件

​ 方法1:所有的进程在开始运行之前,必须一次性的申请在整个运行过程中所需要的全部资源

​ 缺点:因为某项资源不满足进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费,使进程经常发生饥饿现象

​ 方法2:该方法是第一种方法的改进,允许进程只获得运行初期需要的资源, 便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源,这样的话资源利用率会得到提高,也会减少进程的饥饿问题

​ 2.破坏不可剥夺条件

​ 当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请,这就意味着进程已占有的资源会被短暂的释放或者说是被抢占了,该方法实现起来比较复杂,且代价也比较大,释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延迟进程的周转周期,还会影响系统的吞吐量

​ 3. 破坏循环等待条件

可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源,这样虽然避免了循环等待,但是这种方法是比较低效的,资源的执行速度会变慢, 并且可能在没有必要的情况下拒绝资源的访问,比如说,进程c想要申请资源 1,如果资源1并没有被其他进程占有,此时将它分配给进程c是没有问题的,但是为了避免产生循环等待,该申请会被拒绝,这样就降低了资源的利用率
5.说一说用户态和内核态区别

​ 用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同,用户态拥有最低的特权级,内核态拥有较高的特权级,运行在用户态的程序不能直接访问操作操作系统内核数据结构和程序,内核态和用户态之间的转换方式主要包括:系统调用,异常和中断。

6.说一说多线程的同步,锁的机制

​ 同步的时候用一个互斥量,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁,对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁,如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用,在这种方式下,每次只有一个线程可以向前执行

7.C++的锁你知道几种

​ 锁包括互斥锁,条件变量,自旋锁和读写锁,生产者消费者问题利用互斥锁和条件变量可以很容易解决,条件变量这里替代信号量的作用

8.如何实现线程池

​ 线程池是一种多线程处理形式,处理时把任务提交到线程池,任务的执行由线程池管理。使用线程池的原因:(1)不同请求之间重复利用线程,无需频繁的创建和销毁线程,降低系统开销和控制线程数量上限,避免创建过多的线程耗尽进程内存空间。(2)同时减小线程上下文切换次数。

核心思想:线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,也就是这其中的每个线程都会执行提交上来的不同的任务的run,而不会消亡。

​ 1.设置一个生产者消费者队列,作为临界资源

​ 2.初始化N个线程,并让其运行起来,加锁去队列取任务运行

​ 3.当任务队列为空的时候,所有线程阻塞

​ 4.当生成者队列来了一个任务后,先对队列加锁,把任务挂在队列上,然后使用条件变量去通知阻塞中的一个线程

9.线程是否拥有资源

​ 线程自己持有的只有状态和计数器,同一个进程内的线程共享这个进程的资源 ,线程不占系统资源但是不代表不占用资源,线程就是一种系统的中断方式,由系统的时间片来确定中断运行的时间,如果有多个线程那么相当与多个程序同时运行,每一个程序都有要有自己的堆和栈,肯定要多占内存。而在线程在创建时cpu的工作模式模式在转换,所以开辟线程cpu占用率会升高或者满载,开辟之后线程与线程间切换也需要消耗cpu占有率的。

10.常见的进程调度算法?

(1)先来服务调度算法——先来先服务调度算法是一种最简单的调度算法,该算法既可用于作业调度,在进程调度中采用先来先服务算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理器,使之投入运行,该进程一直运行到完成或发生某事件而阻塞后才放弃处理机

(2)短进程优先调度算法——是从就绪队列选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度

(3)高优先权优先调度算法——从就绪队列中选择优先权最高的进程,分为1.非抢占式优先权算法——在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直运行下去,直至完成,或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程,这种调度算法主要用于批处理系统中。2.抢占式优先权调度算法——在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行,但在其执行期间,只要又出现另一个其优先权更高的进程,进程调度算法就立即停止当前进程的执行,重新将处理机分配给新的优先权最高的进程。

(4)基于时间片的轮转调度算法——1.时间片轮转法,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。2.多级反馈队列调度算法,应设置多个就绪队列,并为每个队列赋予不同的优先级,第一个队列优先级最高,依次次之,该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权越高的队列中,为每个进程所规定的执行时间片就越小;当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度,当轮到该进程执行时,如果能在该时间片内完成,便可准备撤离系统,如果在一个时间片内尚未完成,调度程序便将该进程转入第二队列的末尾;仅当第一队列空闲时候,调度程序才调度第二队列中的进程运行

11.物理地址和虚拟地址

(1)每个进程的4G内存空间只是虚拟内存空间,每次访问内存空间的某个地址,都需要把地址翻译为实际物理内存地址

(2)所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上

(3)进程要知道哪些内存地址上的数据在物理内存上,哪些不在,还有在物理内存上的哪里,需要用页表来记录

(4)页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址

(5)当进程访问某个虚拟地址,去看页表,如果发现对应的数据不在物理内存上,则缺页异常

(6)缺页异常的处理过程,就是把进程需要的数据从磁盘拷贝到物理内存中,如果内存已经满了,没有地方了,那就找一个页覆盖,当然如果被覆盖的页曾经被修改过,需要将此页写回磁盘

12.CPU是怎么执行指令的

​ 计算机每执行一条指令都可分为三个阶段进行,即取指令——分析指令——执行指令

​ 取指令:根据程序计数器PC中的值从程序存储器读出现行指令,送到指令寄存器

​ 分析指令:将指令寄存器中的指令操作码取出来后进行译码,分析其指令性质,如指令要求操作数,则寻找操作数地址

​ 执行指令:逐条指令的重复执行上述操作过程,直至遇到停机指令可循环等待指令

13.线上CPU爆高,请问如何找到问题所在

​ 1.top命令:Linux命令。可以查看实时的CPU使用情况,也可以查看最近一段时间的CPU使用情况

​ 2.ps命令:Linux命令。强大的进程状态监控命令,可以查看进程以及进程中线程的当前CPU使用情况,属于当前状态的采样数据

​ 3.pstack命令:Linux命令,可以查看某个进程的当前线程栈运行情况

14.为什么要字节对齐?

​ 1.有些特殊的CPU只能处理4倍开始的内存地址

​ 2.如果不是整数倍数读取会导致读取多次

​ 3.数据总线为读取数据提供了基础

15.阻塞和非阻塞的区别

​ (1)阻塞是指调用结果返回之前,当前线程被挂起。
​ (2)非阻塞是指在不能立刻得到结果之前,该调用不会阻塞当前线程。
​ (3)阻塞非阻塞着重在于服务端程序在等待结果时的状态

16.同步和异步区别

(1)同步是指客户端发出请求后,在没有得到想要结果前,一直阻塞
(2)异步是指客户端发出请求后,马上返回但是没有结果。等服务端运行结束后通过回调再通知客户端
(3)同步异步的区别着重在于客户端在等待结果时的状态

10.什么是协程?

协程是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程,更重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是用户态执行),这样的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源,协程的暂停完全由程序控制,线程的阻塞是由操作系统内核来进行切换,因此协程的开销远远小于线程的开销

11.请求页面置换策略有哪些方式,它们的区别是什么,各自有什么算法解决

​ 全局和局部

​ 全局:在整个内存空间置换

​ 1.工作集算法 2.缺页率置换算法

​ 局部:在本进程中进行置换

​ 1.最优算法 2.FIFO先进先出算法 3.LRU最近最久未使用 4.时钟算法

12.创建进程的步骤

​ 1.申请空的PCB

​ 2.为新进程分配资源

​ 3.初始化PCB

​ 4.将新进程插入就绪队列中

13.进程切换发生的原因,处理进程切换的步骤

​ 原因:(1)中断发生(2)更高优先级进程唤醒(3)进程消耗完了时间片(4)资源阻塞

​ 步骤:

​ (1)保存处理器的上下文

​ (2)用新状态和其它相关信息更新正在运行进程的PCB

​ (3)将原来的进程移到合适的队列中【就绪、阻塞】

​ (4)选择另一个执行的进程,更新被选中进程的PCB,将它加载进CPU

14.拥塞控制的方式,快重传的时机是什么

​ 1.慢开始

​ 2.拥塞避免

​ 3.快重传【收到3个失序分组确认】

​ 4.快恢复

15.分段机制和分页机制的区别

​ 1.分页机制会使用大小固定的内存块,而分段管理则使用了大小可变的块来管理内存

​ 2.分页使用固定大小的块更为适合管理物理内存,分段机制使用大小可变的块更适合处理复杂系统的逻辑分区

​ 3.段表存储在线性地址空间,而页表则保存在物理地址空间

16.进程的状态,各个状态之间的切换

​ 1.就绪:进程已处于准备好运行的状态,即进程已分配到除CPU外的所有必要资源后,只要再获得CPU,便可立即执行

​ 2.执行:进程已经获得CPU,程序正在执行状态

​ 3.阻塞:正在执行的进程由于发生某事件(如I/O请求,申请缓冲区失败等)暂时无法继续执行的状态

17.进程和线程

​ 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位

​ 线程是进程的一个实体,是CPU调度和分配的基本单位,它是比进程更小的能独立运行的基本单位

进程和线程的关系:

​ 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程,线程是操作系统可识别的最小执行和调度单位

​ 资源分配给进程,同一进程的所有线程共享进程的所有资源,同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储),但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量

​ 处理机分给线程,即真正在处理机上运行的是线程

​ 线程在执行过程中,需要协作同步,不同进程的线程间要利用消息通信的办法实现同步

进程和线程的区别:

​ 1.进程有自己的独立地址空间,线程没有

​ 2.进程是资源分配的最小单位,线程是CPU调度的最小单位

​ 3.进程和线程通信方式不同(线程之间的通信比较方便,同一进程下的线程共享数据(比如全局变量,静态变量),通过这些数据来通信不仅快捷而且方便,当然如何处理好这些访问的同步与互斥正是编写多线程程序的难点,而进程之间的通信只能通过进程通信的方式进行)

​ 4.进程上下文切换开销大,线程开销小

​ 5.一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程

​ 6.对进程操作一般开销比较大,对线程开销就小了

18.孤儿进程和僵尸进程有什么区别

​ (1)孤儿进程就是说一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程,孤儿进程将被init进程(进程ID为1的进程)所收养,并由init进程对它们完成状态收集工作,因为孤儿进程会被init进程收养,所以孤儿进程不会对系统造成伤害。

​ (2) 僵尸进程就是一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过wait()或waitpid()获取了子进程信息后才会释放,如果子进程退出,而父进程并没有调用wait()或waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程,僵尸进程通过ps命令显示出来的状态为Z。系统所能使用的进程号是有限的,如果产生大量僵尸进程,可能会因为没有可用的进程号而导致系统不能产生新的进程,从而被init进程所收养,这样init进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。

​ 守护进程:守护进程是运行在后台的一种特殊进程,它是独立于控制终端的,并周期的执行某些任务

19.线程有哪两种?

(1)用户级线程

​ 对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在,在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源,应用程序通常先在一个线程中运行,该线程被称为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程,用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。

(2)内核级线程

​ 对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口,内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成,内核级线程的好处是,内核可以将不同线程更好的分配到不同的CPU,以实现真正的并行计算。

20.为什么进程上下文切换比线程上下文切换代价高?

​ 进程切换分两步

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

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

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

​ 切换的性能消耗:

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

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

19.进程同步

​ 进程同步的主要任务:是对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效的共享资源和相互合作,从而使程序的执行具有可再现性

​ 同步机制遵循的原则:

​ (1)空闲让进

​ (2)忙则等待(保证对临界区的互斥访问)

​ (3)有限等待(有限代表有限的时间,避免死等)

​ (4)让权等待(当进程不能进入自己的临界区时,应该释放处理机,以免陷入忙等状态)

22.进程的通信方式有哪些

​ 进程通信,是指进程之间的信息交换(信息量少则一个状态或数值,多者则是成千上万个字节),因此,对于用信号量进行的进程间的互斥和同步,由于其所交换的信息量少而被归结为低级通信

​ 所谓高级进程通信指:用户可以利用操作系统所提供的一组通信命令传送大量数据的一种通信方式,操作系统隐藏了进程通信的实现细节,或者说,通信过程对用户是透明的

​ 高级通信机制可归结为三大类:

​ (1)共享存储器系统(存储器中划分的共享存储区);实际操作中对应的是“剪贴板”(剪贴板实际上是系统维护管理的一块内存区域)的通信方式,比如举例如下:word进程按下ctrl+c,在ppt进程按下ctrl+v,即完成了word进程和ppt进程之间的通信,复制时将数据放入到剪贴板,粘贴时从剪贴板中取出数据,然后显示在ppt窗口上

​ (2)消息传递系统(进程间的数据交换以消息(message)为单位),当今最流行的微内核操作系统中,微内核与服务器之间的通信,无一例外的采用了消息传递机制,应用距离:邮槽(MailSlot)是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输,邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据

​ (3)管道通信系统(管道即:连接读写进程以实现他们之间通信的共享文件(pipe文件,类似先进先出的队列,由一个进程写,另一进程读)),实际操作中,管道分为:匿名管道、命名管道。匿名管道是一个未命名的、单向管道,通过父进程和一个子进程之间传输数据,匿名管道只能实现本地机器上两个进程之间的通信,而不能实现跨网络的通信,命名管道不仅可以在本机上实现两个进程间的通信,还可以跨网络实现两个进程间的通信

  • ​ 管道:管道是单向的、先进先出、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起,写进程在管道的尾端写入数据,读进程在管道的头端读出数据,数据读出后将管道中移走,在有数据写入管道前,进程将一直阻塞,同样的,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
  • 信号量:信号量是一个计时器,可以用来控制多个进程对共享资源的访问,它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源,因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列:是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现的,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享内存:共享内存允许两个或多个进程访问同一个逻辑内存,这一段内存可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信,如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程,共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的,它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
  • 套接字:套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。
23.进程的调度

调度种类

  • 高级调度:又称为作业调度,它决定把后备作业调入内存运行
  • 低级调度:又称为进程调度,它决定把就绪队列的某进程获得CPU
  • 中级调度:又称为在虚拟存储器中引入,在内、外存对换区进行进程对换

非抢占式调度与抢占式调度

  • 非抢占式:分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生进程调度某事件而阻塞时,才把处理机分配给另一个进程
  • 抢占式:操作系统将正在运行的进程强行暂停,由调度程序将CPU分配给其他就绪进程的调度方式

FIFO或FCFS先来先服务

  • 调度的顺序就是任务到达就绪队列的顺序
  • 公平、简单(FIFO队列)、非抢占、不适合交互式
  • 未考虑任务特性,平均等待时间可以缩短

Shortest Job First(SJF)

  • 最短的作业(CPU区间长度最小)最先调度
  • SJF可以保证最小的平均等待时间

Shortest Remaining Job First(SRJF)

  • SJF的可抢占版本,比SJF更有优势
  • SJF(SRJF):如何知道下一CPU区间大小?根据历史进行预测:指数平均法

优先权调度

  • 每个任务关联一个优先权,调度优先权最高的任务
  • 注意:优先权太低的任务一直就绪,得不到运行,出现“饥饿”现象

Round-Robin(RR)轮转调度算法

  • 设置一个时间片,按时间片来轮转调度(轮转算法)
  • 优点:定时有响应,等待时间较短;缺点:上下文切换次数较多
  • 时间片太大,响应时间太长;吞吐量变小,周转时间变长;当时间片过长时,退化为FCFS

多级队列调度

  • 按照一定的规则建立多个进程队列
  • 不同的队列有固定的优先级(高优先级有抢占权)
  • 不同的队列可以给不同的时间片和采用不同的调度方法
  • 存在问题1:没有区分I/O bound和CPU bound
  • 存在问题2:也存在一定程序的“饥饿”现象

多级反馈队列

  • 在多级队列的基础上,任务可以在队列之间移动,更细致的区分任务
  • 可以根据“享用”CPU时间多少来移动队列,阻止“饥饿”
  • 最通用的调度算法,多数OS都使用该方法或其变形,如UNIX、Windows等
24.epoll,select,poll之间的区别

​ (1)select==>时间复杂度O(n)

​ 它仅仅知道了,有I/O事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作,所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

​ (2)poll==>时间复杂度O(n)

​ poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,但是它没有最大连接数的限制,原因是它是基于链表来存储的。

​ (3)epoll==>时间复杂度O(1)

​ epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们,所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的,(复杂度降低到了O(1))

​ select:内核需要将消息传递到用户空间,都需要内核拷贝动作

​ poll:同上

​ epoll:epoll通过内核和用户空间共享一块内存来实现的

25.静态链接和动态链接有什么区别?

(1)静态链接就是在编译期间,由编译器和连接器将静态库集成到应用程序内,并制作成目标文件以及可以独立运作的可执行文件,静态库一般是一些外部函数与变量的集合

(2)静态库很方便,但是如果我们只是想用库中的某一个函数,却仍然把所有的内容都链接进去,一个更现代的方法是使用共享库,避免了在文件中静态库的大量重复。

(3)动态链接可以在首次载入的时候执行,也可以在程序开始执行的时候完成,这个是由动态链接器完成,比如标准C库(libc.so)通常就是动态链接的,这样所有的程序可以共享同一个库,而不用分别进行封装。

26.页面替换算法有哪些?

​ 在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中,此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。

​ (1)最佳算法

​ 所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率,这是一种理论上的算法,因为无法知道一个页面多长时间不再被访问

​ (2)先进先出

​ 选择换出的页面是最先进入的页面,该算法将那些经常被访问的页面也被换出,从而使缺页率升高。

​ (3)LRU

​ 虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况,LRU将最近最久未使用的页面换出,为了实现LRU,需要在内存中维护一个所有页表的链表,当一个页面被访问时,将这个页面移到链表表头,这样就能保证链表表尾的页面是最近最久未访问的,因为每次访问都需要更新链表,因此这种方式实现的LRU代价很高。

​ (4)时钟算法

​ 时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面,它将整个环形链表的每一个页面做一个标记,如果标记是0,那么暂时就不会被替换,然后时钟算法遍历整个环,遇到标记为1的就替换,否则将标记为0的标记为1.

26.硬链接和软链接有什么区别?

(1)硬链接就是在目录下创建一个条目,记录着文件名与inode编号,这个inode就是源文件的inode,删除任意一个条目,文件还是存在,只要引用数量不为0,但是硬链接有限制,它不能跨越文件系统,也不能对目录进行链接

(2)符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为windows的快捷方式,当源文件被删除了,链接文件就打不开了,因为记录的是路径,所以可以为目录建立符号链接。

27.Linux的4种锁机制

(1)互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象,当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。

(2)读写锁:rwlock,分为读锁和写锁,处于读操作时,可以允许多个线程同时获得读操作,但是同一时刻只能有一个线程可以获得写锁,其它获取写锁失败的线程会进入睡眠状态,直到写锁释放时被唤醒。注意:写锁会阻塞其他读写锁,当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。

(3)自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象,但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放,这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率,但如果加锁时间过长,则会非常浪费CPU资源。

(4)RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改,修改完成后,再将老数据update成新的数据,使用RCU时,读者几乎不需要同步开销,即不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了,而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作,在有大量读操作,少量写操作的情况下效率非常高。

28.什么是大端和小端以及如何判断大端小端

​ 大端是指低字节存储在高地址,小端是指低字节存储在低地址,我们可以根据联合体来判断该系统是大端还是小端,因为联合体变量总是从低地址存储

//判断系统是大端还是小端:通过联合体,因为联合体的所有成员都从低地址开始存放
int func()
{
    union test
    {
        int i;
        char c;
    };
    test t;
    t.i = 1;
    //如果是大端,则t.c为0x00,则t.c != 1,返回0
    //如果是小端,则t.c为0x01,则t.c==1,返回1
    return (t.c == 1);
}
29.进程的五种基本状态

(1)创建状态:进程正在被创建

(2)就绪状态:进程被加入到就绪队列中等待CPU调度运行

(3)执行状态:进程正在被运行

(4)等待阻塞状态:进程因为某种原因,比如等待I/O,等待设备,而暂时不能运行。

(5)终止状态:进程运行完毕

​ 当多个进程竞争内存资源时,会造成内存资源紧张,并且,如果此时没有就绪进程,处理机会空闲,I/O速度比处理机速度慢得多,可能出现全部进程阻塞等待I/O

​ 针对以上问题。提出了两种解决方法:

(1)交换技术:换出一部分进程到外存,腾出内存空间

(2)虚拟存储技术:每个进程只能装入一部分程序和数据

​ 在交换技术上,将内存暂时不能运行的进程,或者暂时不用的数据和程序,换出到外存,来腾出足够的内存空间,把已经具备运行条件的进程,或进程所需的数据和程序换入到内存。

​ 从而出现了进程的挂起状态:进程被交换到外存,进程状态就成为了挂起状态

30.什么是活动阻塞,静止阻塞,活动就绪,静止就绪

(1)活动阻塞:进程在内存,但是由于某种原因被阻塞了

(2)静止阻塞:进程在外存,同时被某种原因阻塞了

(3)活动就绪:进程在内存,处于就绪状态,只要给CPU和调度就可以直接运行

(4)静止就绪:进程在外存,处于就绪状态,只要调度到内存,给CPU和调度就可以运行。

31.说一说内存溢出和内存泄漏

​ 1.内存溢出

​ 指程序申请内存时,没有足够的内存供申请者使用,内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。

​ 内存溢出的原因:

​ 内存中加载的数据量过于庞大,如一次从数据库取出过多数据,集合类中有对对象的引用,使用完后未清空,使得不能回收,代码中存在死循环或循环产生过多重复的对象实体,使用的第三方软件中的BUG,启动参数内存值设定的过小。

​ 2.内存泄漏

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

​ 内存泄漏的分类:

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

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

(3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确被释放,因此造成内存泄漏。

32.操作系统中的程序的内存结构

​ 一个程序本质上都是由BSS段、data段、text段三个组成的,可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。

​ 数据区(静态区)包含BSS段(未初始化)和已初始化数据区

​ BSS(Block Started by Symbol):通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域,BSS段属于静态分配,程序结束后静态变量资源由系统自动释放,BSS特点是可读可写,在程序执行之前BSS段会自动清0,所以,未初始化的全局变量在程序执行之前已经成0了。UNIX下可使用size命令查看可执行文件的段大小信息,如size a.out

​ 数据段:存放程序中已初始化的全局变量的一块内存区域,数据段也属于静态内存分配

​ 代码段:存放程序执行代码的一块内存区域,这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读,在代码段中,也有可能包含一些只读的常数变量。

​ text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的,BSS段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中,其原因是内核在程序开始运行前将它们设置为0,需要存放在程序文件中的只有正文段和初始化数据段。

​ data段(已经初始化的数据)则为数据分配空间,数据保存在目标文件中。

​ 数据段包含经过初始化的全局变量以及它们的值,BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面,当这个内存进入程序的地址空间后全部清零,包含数据段和BSS段的整个区段此时通常称为数据区。

​ 可执行程序在运行时又多出两个区域:栈区和堆区

(1)栈区

​ 由编译器自动释放,存放函数的参数值、局部变量等,每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中,然后这个被调用的函数再为它的自动变量和临时变量在栈上分配空间,每调用一个函数一个新的栈就会被使用,栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。

(2)堆区

​ 由于动态分配内存,位于BSS和栈中间的地址区域,由程序员申请分配和释放,堆是从低地址位向高地址位增长,采用链式存储结构,频繁的malloc/free造成内存空间的不连续,产生碎片,当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间,因此堆的效率比栈要低的多。

33.说一下操作系统中的缺页中断

​ malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存,当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。

​ 缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中,每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。

​ 缺页本身也是一种中断,与一般的中断一样,需要经过4个处理步骤

(1)保护CPU现场

(2)分析中断原因

(3)转入缺页中断处理程序进行处理

(4)恢复CPU现场,继续执行

​ 但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:

(1)在指令执行期间产生和处理缺页中断信号

(2)一条指令在执行期间,可能产生多次缺页中断

(3)缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。

五、数据库

1.SQL标准的四种隔离级别

(1)READ UNCOMMITED(未提交读)

事务中的修改,即使没有提交,对其他事务也都是可见的,事务1修改了数据A后未提交,事务2可以读到被事务1修改的数据A,但是事务1可能最终不提交,那么事务2读到的就是错误的数据(脏数据)

(2)READ COMMITED(提交了可读)

大多数数据库系统的默认隔离级别都是READ COMMITED(但是MySQL不是,MySQL默认是可重复读),一旦事务开始时,只能看到已经提交的事务所做的修改,换句话说,一个事务从开始到提交之前,所做的任何修改对其他事务都是不可见的,这个级别有时候也叫做不可重复的,因为两次执行同样的查询,可能会得到不一样的结果。事务1读取了数据A后未提交,事务2修改了数据A且提交,然后事务1再去读数据A会发现前后两次读结果不同,这就是不可重复读(因为重复读结果不一样)

(3)REPEATABLE READ(可重复读)

可重复读解决了脏读问题,该级别保证了在同一个事务中多次读取同样的记录的结果是一致的,但是理论上,可重复读隔离级别还是无法解决另一个幻读的问题,所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,Innodb和Xtradb存储引擎通过多版并发控制解决了幻读,可重复读是MySQL默认的事务隔离级别,事务1读取了数据A后未提交,事务2修改了数据A且提交,然后事务1再去读数据A会发现前后两次读结果相同,这就是可重复读,就是在开始读取数据(事务开启)时,不再允许修改操作——MySQL默认级别

(4)SERIALIZABLE(可串行化)

可串行化是最高的隔离级别,它通过强制事务串行,避免了前面说的幻读问题,简单是来说,可串行化会在读的每一行数据上都加锁,所以可能导致大量的超时和锁征用问题,实际应用中也很少用到这个隔离级别,只有在非常确保数据的一致性而且可以接受没有并发的情况,才考虑用该级别,每一行数据加锁,导致所有事务都必须串行执行,但是代价很大

2.Mysql的两种引擎的区别

(1)Innodb引擎

​ Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别,该引擎提供了行级锁和外键约束,它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统,MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引,但是该引擎不支持FULLTEXT类型的索引,而且它没有保存表的行数,当SELECT COUNT(*) FROM TABLE时需要扫描全表,当需要使用数据库事务时,该引擎当然是首选。由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提高效率,但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不难确定要扫描的范围,Innodb表同样会锁全表。

(2)MyISAM引擎

​ MyISAM引擎是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会更低一些,不过和Innodb不同,MyISAM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描,如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyISAM也是很好的选择

​ 区别:

​ (1)MyISAM是非事务安全的,而Innodb是事务安全的

​ (2)MyISAM锁的粒度是表级别的,而Innodb支持行级锁

​ (3)MyISAM支持全文类型索引,而Innodb不支持全文索引

​ (4)MyISAM相对简单,效率上要优于Innodb,小型应用可以考虑使用MyISAM

​ (5)MyISAM表保存文件形式,跨平台使用更加方便

​ 应用场景:

(1)MyISAM管理非事务表,提高高速存储和检索以及全文搜索能力,如果在应用中执行大量select操作,应该选择MyISAM

(2)Innodb用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择Innodb

3.使用mysql索引都有哪些原则

​ 1.对于查询频率高的字段创建索引

​ 2.对排序、分组、联合查询频率高的字段创建索引

​ 3.索引的数目不宜过多

​ 原因:a、每创建一个索引都会占用相应的物理空间

​ b、过多的索引会导致insert、updata、delete语句的执行效率降低

​ 4.若在实际中,需要将多个列设置索引时,可以采用多列索引

​ 5.选择唯一性索引

​ 6.尽量使用数据量少的索引

​ 7.尽量使用前缀来索引

​ 8.删除不再使用或者很少使用的索引

4.从Innodb的索引结构分析,为什么索引的Key长度不能太长

​ key太长会导致一个页当中存放的key的数目变少,间接导致索引树的页数目变多,索引层次增加,从而影响整体查询变更的效率

5.数据库事务有哪些

​ 原子性:所有操作要么全部成功,要么全部失败

​ 一致性:例如转账,一个事务执行前和执行后必须一致

​ 隔离性:防止脏读,重复读问题

​ 持久性:永久性提交数据库

6.SQL优化有哪些方法?

​ 通过建立索引对查询进行优化,避免全表扫描,DB在执行一条sql语句时候,默认的方式是根据搜索条件进行全表扫描,遇到匹配条件的就加入搜索结果集合,如果我们对某一字段增加索引,查询时就会先去索引列表中一次定位到特定值的行数,大大减少遍历匹配的行数,所以能明显增加查询的速度

7.什么是索引?

​ 索引其实就是一种单独的、物理的数据结构,索引文件包含两部分信息,一是数据库每条记录的索引关键字的值,二是其对应的记录号,只要给出索引关键字,就可以在索引文件中查到相应的记录号,然后在数据库中将记录指针迅速移动到对应的记录上,数据库索引可以是:顺序索引,B+树索引,hash索引。索引越多,更新的数据的速度越慢,当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。

8.索引是不是越多越好?

(1)首先数据量小的表不需要建立索引,因为小的表即使建立索引也不会有大的用处,还会增加额外的索引开销
(2)不经常引用的列不要建立索引,因为不常用,即使建立了索引也没有多大意义
(3)经常频繁更新的列不要建立索引,因为肯定会影响插入或更新的效率
(4)索引并不是一劳永逸的,用的时间长了需要进行整理或者重建

9.数据库事务的一致性

​ 事务是由一系列对数据库中数据进行访问与更新的操作所组成的一个程序执行逻辑单元,事务是DBMS中最基础的耽误,事务不可分割,4个基本特征:原子性,一致性,隔离性,持久性

​ 数据库事务的隔离级别有4种,由低到高分别为

​ (1)Read uncommitted——读未提交,就是一个事务可以读取另一个未提交事务的数据。

​ (2)Read committed——读提交,就是一个事务要等另一个事务提交后才能读取数据。

​ (3)Repeatable read——重复读,就是在开始读取数据(事务开启)时,不再允许修改操作

​ (4)Serializable——序列化,Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。而且,在事务的并发操作中可能会出现脏读,不可重复读,幻读。

六、Linux

1.Linux操作系统重要部分

​ Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序,内核、shell、和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统

​ 1.Linux内核是操作系统的核心,具有很多基本功能,如虚拟内存,多任务,共享库,需求加载,可执行程序和TCP/IP网络功能,Linux内核的模块分为以下几个部分:存储管理、CPU和进程管理、文件系统、设备管理和驱动、网络通信、系统的初始化和系统调用等

​ 2.Linux shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口,它接收用户输入的命令并把它送入内核去执行,是一个命令解释器,另外,shell编程语言具有普遍编程语言的很大特点,用这种编程语言写的shell程序与其他应用程序具有相同的效果

​ 3.Linux文件系统是文件存放在磁盘等存储设备上的组织方式,Linux系统能支持多种目前流行的文件系统,如EXT2、EXT3、FAT、FAT32

​ 4.Linux应用程序,标准的Linux系统一般都有一套称为应用程序的程序集,它包括文本编辑器、编程语言、XWindow、办公套件、Internet工具和数据库等

2.说一说Linux虚拟地址空间

​ 为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。

​ 虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存,所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上,事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.tetx和.data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常来拷贝数据,还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。请求分页系统,请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存和外存的信息置换。

​ 虚拟内存的好处:

(1)扩大地址空间.

(2)内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方,虚拟内存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。

(3)公平内存分配,采用了虚拟内存之后,每个进程都相当于有同样大小的虚拟内存空间。

(4)当进程通信时,可采用虚拟内存共享的方式实现。

(5)当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存。

(6)虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中,当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用,在内存中可以保留多个进程,系统并发度提高。

(7)在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片。

虚拟内存的代价:

(1)虚拟内存的管理需要建立很多数据结构,这些数据结构要占用额外的内存。

(2)虚拟地址到物理地址的转换,增加了指令的执行时间。

(3)页面的换入换出需要磁盘I/O,这是很耗时的。

(4)如果一页中只有一部分数据,会浪费内存。

你可能感兴趣的:(后台开发面试题.md)