标签搜索

目 录CONTENT

文章目录

C++方向复习总结.md

小小城
2021-08-22 / 0 评论 / 1 点赞 / 35 阅读 / 81,437 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-04,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

@

一、基础知识

1.内存对齐???

进行内存对齐的原因????
a.平台原因(可移植原因):并不是所有的平台都支持任意地址处的任意数据的访问,某些硬件平台只支持在某特定地址处理特殊数据,否则会抛异常

b.性能原因:如果没有发生内存对齐,比如说栈,因尽可能的在自然边界上对齐,否则为了访问未对齐的内存可能需要两次访问内存;而对齐的内存只需要访问一次内存

内存对齐的规则
a.第一个成员在结构体偏移量为0的地址处。
b.其它成员要对齐到对齐数的整数倍的地址处
c.对齐数 = min(编译器默认的对齐数,该成员的大小),,VS的默认对齐数为8,gcc默认对齐数为4,可以使用#pragma pack(num)来修改VS中的默认对齐数。
d.结构体的总大小要对齐到最大对齐数(max(上一个变量的对齐数 ,min(编译器默认的对齐数,该成员的大小)))的整数倍的地址处
e.如果出现嵌套结构体的情况,嵌套结构体的大小对齐到自己的最大最齐数的地址处,整个结构体的大小要对齐到所有的对齐数当中最大的对齐数的整数倍的地址处。
f.联合体的大小等于联合体中最大成员所占字节数且该最大数据类型所对应的字节应为默认对其数的最小倍数
e. 任何枚举类型的大小都是4。
g. 位段的声明和结构(struct)类似,但它的成员是一个或多个位的字段,这些不同长度的字段实际储存在一个或多个整型变量中。在声明时,位段成员必须是整形或枚举类型(通常是无符号类型),且在成员名的后面是一个冒号和一个整数,整数规定了成员所占用的位数。位域不能是静态类型。不能使用&对位域做取地址运算,因此不存在位域的指针,编译器通常不支持位域的引用(reference)。
h. 空的不包含任何成员以及虚函数的结构体或者类大小为1。

2. static关键字的作用

1. 全局静态变量

  • 全局变量前加上关键字static,全局变量就定义成一个全局静态变量.放在静态存储区,在整个程序运行期间一直存在
  • 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
  • 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾

2. 局部静态变量

  • 局部变量之前加上关键字static,局部变量就成为一个局部静态变量。 内存中的位置:静态存储区
  • 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
  • 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变

3. 静态函数

  • 在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但 静态函数只是在声明他的文件当中可见,不能被其他文件所用。
  • 函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;

4. 类的静态成员

  • 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
  • 静态成员需要在类内声明类外实现

5. 类的静态函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。静态成员函数没有this指针,对静态成员的引用不需要用对象名。

  • 静态成员函数是不可以调用类的非静态的成员函数的
  • 非静态成员函数是可以调用类的静态成员函数 的
  • 静态成员函数是不可以调用类的非静态的成员变量的
  • 非静态成员函数是可以调用类的静态成员变量 的
  • 静态成员函数不可以被virtual关键字修饰

6. 让静态成员函数来访问非静态成员变量的方法?????

  • 在静态成员函数的参数列表当中增加一个对象的实例。
  • 放一个全局的类类型的对象。
  • 静态成员函数是不可以访问非静态的成员变量的,但是可以访问静态的成员变量。如果这个类是单例,我们可以在创建的时候把this指针赋值给类内的那个静态的类类型成员变量,然后就可以通过这个变量来进行访问非静态的成员变量
  • 在静态的成员函数的形参列表加一个void类型的指针,然后在内部强转即可

3. 请你来介绍一下STL的allocaotr

  1. STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
  • new运算分两个阶段
    (1)调用::operator new配置内存;
    (2)调用对象构造函数构造对象内容
  • delete运算分两个阶段
    (1)调用对象希构函数;
    (2)掉员工::operator delete释放内存
  • 为了精密分工,STL allocator将两个阶段操作区分开来:
    内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责
  • 同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128字节时,会使用第一级空间配置器;当分配的空间大小小于128字节时,将使用第二级空间配置器
  • 第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存

4.请你来说一说STL迭代器删除元素

主要考察的是迭代器失效的问题。
1.对于序列容器vector,deque来说使用erase(itertor)后,如果对迭代器++,后边的每个元素的迭代器都会失效,因为erase后,后边每个元素都会往前移动一个位置,erase会返回下一个有效的迭代器;只需要重新赋值,不用++
2.对于关联容器map set来说使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可
3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。

5. 请你说一说vector和list的区别,应用,越详细越好

1.概念:
1)Vector

  • 连续存储的容器,动态数组,在堆上分配空间
  • 底层实现:数组
  • 两倍容量增长(在 VS 下是 1.5倍,在 GCC 下是 2 倍。):vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
  • 性能:
  • 访问:O(1)
  • 插入:在最后插入(空间够):很快
  • 在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝
  • 在中间插入(空间够):内存拷贝
  • 在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
  • 删除:在最后删除:很快
  • 在中间删除:内存拷贝
  • 适用场景:经常随机访问,且不经常对非尾节点进行插入删除。

2.List

  • 动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
  • 底层:双向链表
  • 性能:
  • 访问:随机访问性能很差,只能快速访问头尾节点。
  • 插入:很快,一般是常数开销
  • 删除:很快,一般是常数开销
  • 适用场景:经常插入删除大量数据

2.区别:

  • vector底层实现是数组;list是双向 链表。
  • vector支持随机访问,list不支持。
  • vector是顺序内存,list不是。
  • vector在中间节点进行插入删除会导致内存拷贝,list不会。
  • vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
  • vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

3.应用

  • vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
  • list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list

6. 请你来回答一下include头文件的顺序以及双引号””和尖括号<>的区别?

  1. Include头文件的顺序:对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。
  2. 双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。

对于使用双引号包含的头文件,查找头文件路径的顺序为:

  • 当前头文件目录
  • 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
  • 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

对于使用尖括号包含的头文件,查找头文件的路径顺序为:

  • 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
  • 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

7. 请你回答一下malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?

  • Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,
  • 先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位
  • 当用户申请内存时,直接从堆区分配一块合适的空闲块。
  • Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;
  • 同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址
  • 当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配
  • 当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
  • Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

8. 请你说一下源码到可执行文件的过程

1)预编译

主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下

  • 删除所有的#define,展开所有的宏定义
  • 处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”
  • 处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
  • 删除所有的注释,“//”和“/**/”。
  • 保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
  • 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号

2)编译
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件

  • 词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
  • 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
  • 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
  • 优化:源代码级别的一个优化过程。
  • 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
  • 目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。

3)汇编
将汇编代码转变成机器可以执行的指令(机器码文件)。

  • 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。
  • 经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。

4)链接
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:

  • 静态链接
  • 函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
  • 空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
  • 更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
  • 运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
  • 动态链接
  • 动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
  • 共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本,符号表中保存同一个函数地址;
  • 更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
  • 性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

9. 大小端

1. 为什么会出现大小端??
计算机系统中内存是以字节为单位进行编址的,每个地址单元都唯一的对应着1个字节(8 bit)
这可以应对char类型数据的存储要求,因为char类型长度刚好是1个字节,但是有些类型的长度是超过1个字节的(字符串虽然是多字节的,但它本质是由一个个char类型组成的类似数组的结构而已),
比如C/C++中,short类型一般是2个字节,int类型一般4个字节等。因此这里就存在着一个如何安排多个字节数据中各字节存放顺序的问题。
正是因为不同的安排顺序导致了 大端存储模式和小端存储模式 的存在。

2. 什么是大小端??

  • 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放
  • 小端模式, 是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

假如有一个4字节的数据为 0x12 34 56 78(十进制:305419896,0x12为高字节,0x78为低字节),若将其存放于地址0x4000 8000中,则有:

在这里插入图片描述
大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中
小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中

3. 大小端的特点?

  • 大端模式优点:符号位在所表示的数据的内存的第一个字节中,便于快速判断数据的正负和大小
  • 小端模式优点
  • 内存的低地址处存放低字节,所以在强制转换数据时不需要调整字节的内容注解:比如把int的4字节强制转换成short的2字节时,就直接把int数据存储的前两个字节给short就行,因为其前两个字节刚好就是最低的两个字节,符合转换逻辑);
  • CPU做数值运算时从内存中依顺序依次从低位到高位取数据进行运算,直到最后刷新最高位的符号位,这样的运算方式会更高效

4.大小端判断?

// @Ret: 大端,返回true; 小端,返回false
bool IsBigEndian_1()
{
    int nNum = 0x12345678;
    char cLowAddressValue = *(char*)&nNum;

    // 低地址处是高字节,则为大端
    if ( cLowAddressValue == 0x12 )    
    	return true;

    return false; 
}
// @Ret: 大端,返回true; 小端,返回false
bool isBigEndian_2()
{
    union uendian
    {
       int nNum;
       char cLowAddressValue;
    };
    uendian u;
    u.nNum = 0x12345678;
    
    if ( u.cLowAddressValue == 0x12 )     
    	return true;

    return false;
}

5.大小端的转换?

#include<stdio.h>  
  
typedef unsigned int uint_32 ;  
typedef unsigned short uint_16 ;  
 
//16位
#define BSWAP_16(x) \
    (uint_16)((((uint_16)(x) & 0x00ff) << 8) | \
              (((uint_16)(x) & 0xff00) >> 8) \
             )
             
//32位               
#define BSWAP_32(x) \
    (uint_32)((((uint_32)(x) & 0xff000000) >> 24) | \
              (((uint_32)(x) & 0x00ff0000) >> 8) | \
              (((uint_32)(x) & 0x0000ff00) << 8) | \
              (((uint_32)(x) & 0x000000ff) << 24) \
             )  
 
//无符号整型16位  
uint_16 bswap_16(uint_16 x)  
{  
    return (((uint_16)(x) & 0x00ff) << 8) | \
           (((uint_16)(x) & 0xff00) >> 8) ;  
}  
 
//无符号整型32位
uint_32 bswap_32(uint_32 x)  
{  
    return (((uint_32)(x) & 0xff000000) >> 24) | \
           (((uint_32)(x) & 0x00ff0000) >> 8) | \
           (((uint_32)(x) & 0x0000ff00) << 8) | \
           (((uint_32)(x) & 0x000000ff) << 24) ;  
}  
 
int main(int argc,char *argv[])  
{  
    printf("------------带参宏-------------\n");  
    printf("%#x\n",BSWAP_16(0x1234)) ;  
    printf("%#x\n",BSWAP_32(0x12345678));  
    printf("------------函数调用-----------\n");  
    printf("%#x\n",bswap_16(0x1234)) ;  
    printf("%#x\n",bswap_32(0x12345678));  
      
    return 0 ;  
}  
输出结果:
------------带参宏-------------
0x3412
0x78563412
------------函数调用-----------
0x3412
0x78563412
#define ntohs(n)     // 16位数据类型网络字节顺序到主机字节顺序的转换  
#define htons(n)     // 16位数据类型主机字节顺序到网络字节顺序的转换  
#define ntohl(n)     // 32位数据类型网络字节顺序到主机字节顺序的转换  
#define htonl(n)     // 32位数据类型主机字节顺序到网络字节顺序的转换

10.函数重载、多态重写(覆盖)、同名隐藏(重定义)

1.函数重载:

  • 同一作用域
  • 函数名字相同,参数列表不同
  • 参数列表不同为,参数顺序,类型大小,参数的个数不同
  • 形参是否带const不构成重载。引用类型和指针类型(修饰内容的const指针)的形参带const构成重载。类中的函数使用const修饰构成函数重载(是否能够修改类当中的成员变量)
  • 与返回值类型、是否带缺省参数无关

2.多态重写(覆盖):

  • 两个函数分别在基类和派生类的作用域中
  • 函数的类型相同
  • 函数类型包括:返回值类型,参数名字,参数列表
  • 但是也有函数类型不同但是可以实现重写的例外:
  • 返回值不同的例外:
  • 斜变基类虚函数返回基类对象的指针或引用派生类的虚函数返回派生类的指针或引用
  • 函数名字不同的例外:
  • 子类与基类的析构函数如果都是虚函数,名字不同但是可以构成重写
  • 两个函数必须都是虚函数

3.同名隐藏(重定义):

  • 两个函数分别在基类和派生类的作用域中
  • 函数的名字相同
  • 基类和派生类的两个同名函数不构成重写,就是同名隐藏

11.const和宏的优缺点

1.区别:

  • 就起作用的阶段而言: #define是在编译的预处理阶段起作用,而const是在 编译、运行的时候起作用
  • 就起作用的方式而言: #define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误
  • 就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份。
  • 从代码调试的方便程度而言: const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了。

2.const优点

  • const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
  • 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
  • const可节省空间,避免不必要的内存分配,提高效率

3.const关键字:

  • 想阻止一个变量被改变,可以使用const,在定义该const变量时,需要先进行初始化,否则后面无法修改;
  • 对指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  • const int * a代表a所指向的空间当中的内容不能被修改,但是可以修改a的指向。
  • int* const a 代表可以修改a指向的空间当中的内容,但是不可以修改a的指向
  • 在一个函数声明中,const可以修饰形参表明它是一个输入参数,在函数内部不可以改变其值;
  • 对于类的成员函数,有时候必须指定其为const类型,表明其是一个常函数,不能修改类的成员变量,修饰的是this指针;
  • 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”
  • 注意const修饰的成员函数是构成重载的
  • const修饰的成员函数/对象只能调用const的对象/成员函数
  • 非const修饰的成员函数既可以调用const也可以调用非const成员
  • 每个成员变量只可以在初始化列表出现一次,注意引用类型的成员变量、const修饰的成员变量、自定义类型的成员变量(该自定义类型必须显示提供非默认的构造函数)必须在初始化列表处进行初始化
  • const 关键字定义变量的注意事项:
int main()
{
	const int a = 999;
	int* pa = (int*)&a;

	*pa = 666;
	
	//注意此处打印的值为999,666
	cout<<a<<endl;
	cout<<*pa<<endl;

	//产生这样的原因是编译器对const修饰变量的一种优化,即采用寄存器提高读取速度。
	//所以为了禁止这种现象的发生,可以使用volatite关键字修饰,保证内存的可见性
	return 0;
}

11.请你回答一下map和unordered_map优点和缺点

对于map,其底层是基于红黑树实现的,优点如下

  • 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
  • map的查找、删除、增加等一系列操作时间复杂度稳定,都为logn

缺点如下

  • 查找、删除、增加等操作平均时间复杂度较慢,与n相关

对于unordered_map来说,其底层是一个哈希表,优点如下

  • 查找、删除、添加的速度快,时间复杂度为常数级O©

缺点如下

  • 因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用率高
  • Unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O©,取决于哈希函数。极端情况下可能为O(n)

12. 指针和引用区别别???

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

int a=1;int *p=&a;
int a=1;int &b=a;

(2)引用不可以为空,当被创建的时候,必须初始化而指针可以是空值,可以在任何时候被初始化。

(4)指针可以有多级但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(5)指针的值可以为空但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(6)指针的值在初始化后可以改变,即指向其它的存储单元而引用在进行初始化后就不会再改变了。

(7)”sizeof引用”得到的是所指向的变量(对象)的大小而”sizeof指针”得到的是指针本身的大小;

(8) 指针和引用的自增(++)运算意义不一样;

(9) 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏; 因为虽然不存在局部变量的被动销毁的问题,但是在此种情况下,仍然存在一些问题。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的由new分配的空间就无法被释放,从而造成内存泄漏问题。

13.New/malloc的区别??

1. New/delete的原理????

  • New/delete(操作符)底层调用的是operator new/operator delete函数,而operator new/operator delete又调用的是malloc/free来进行申请/释放空间
  • malloc申请过程中如果成功则成功返回空间的地址,如果malloc申请失败则会尝试执行用户自定义的空间失败的措施(前提是用户提供了),最后还没有成功申请就会抛异常
  • new会调用operator new进行申请空间,完成空间的申请后还需要调用构造函数来完成对象的构造(new[N]底层是调用operator new[N],而operator new[N]会调用N次operator new)
  • 调用delete之前会先调用析构函数完成资源的清理,再调用operator delete完成空间的释放(或者调用N次operator delete)
  • 定位new表达式是在已经申请的空间上调用构造函数完成空间的的初始化(使用场景:内存池),new (空间地址) type (初始化列表);eg:申请空间:Test* test = (Test*)malloc(sizeof(Test));完成空间的构造:new (test) ();

2. malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是

  • malloc和free是函数,new和delete是操作符
  • malloc申请的空间不会初始化,new可以初始化
  • malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
  • malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  • malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  • 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

14. 为什么选择deque为stack的默认容器?

1. deque底层原理?????

  • deque的底层:首先有一个空间(map,可以理解为一个数组)专门存储一块块连续空间(存储用户数据的空间)的首地址的,使用的时候从中间开始使用(方便完成deque的性质:两端插入扩容,那边不够,就申请空间,把空间的首地址放到map的对应边的位置上)。
  • deque的迭代器:每个迭代器的内部都有4个指针:
  • cur指向空间中的用户正在使用的位置,
  • first指针指向用户正在使用的空间的首地址,
  • last指针指向用户正在使用的空间的末尾,
  • node指针指向map的对应空间的位置(如果没有该指针,那么迭代器遍历完一小块空间,就不知道下一片空间的地址了)begin:指向map的最左边的空间
  • end:指向map当中最右边的空间地址

2. 为什么选择deque为stack的默认容器?

  • deque的优点是插入/删除效率高,不需要搬移元素,就算扩容也不需要搬移元素,和list相比,空间连续的,空间利用率比较高
  • deque的缺点是:不适合遍历,因为在迭代器遍历是时,需要不断的检查是否走到一块空间的末尾,导致效率比较低
  • Stack/queue的底层容器vector/list都可以,选择deque作为底层的容器,是因为stack/queue的特性是两端操作,不需要遍历,而deque的特性是两端操作效率比vector/list高,而遍历效率比较低,这样stack/queue刚好和deque互补,且效率高。

15.模版、内联函数为什么不支持分离编译??

模版、内联函数不支持分离编译

  • 模版(以函数模版举例,a.h:模版的声明,a.c模版的代码,main.c实例化模版的代码)是把声明和定义放在不同的文件当中而模版是根据实例化的具体类型来生成对应类型的函数代码而在a.c文件当中,编译器在编译阶段并没有找到模版对应的具体实例,所以就并不会生成对应类型的代码, 而main.c当中对模版的实例化后链接阶段并在符号表中没有找到对应类型函数代码的地址,所以导致链接错误
  • 内联函数:同样如果把声明和定义分离开来,注意内联函数的特性,在编译阶段就把内联函数展开,那么在链接阶段就无法找到具体的函数地址,也会报链接错误

16.派生类的生成过程????

1. 继承:

  • 注意基类的private成员不论是什么方式继承,在子类都是不可见的(类内类外都不可以访问)
  • 如果想让基类的成员只能在子类当中访问,只需要把该成员设为procted即可
  • 基类的成员能否在子类当中进行访问 = min(基类的成员访问方式,继承方式);
  • 派生类对象可以赋值给基类的对象/引用/指针,也把这中方式叫切片,意味把子类当中基类的部分切下来赋值给基类
  • 基类对象正常是不可以赋值给子类对象
  • 基类的指针可以强制转换赋值给子类的指针
  • 如果本来是父类的指针指向派生类,再把这个父类指针强转成派生类指针,赋值给派生类是可以的
  • 如果本来是父类指针指向父类,再把父类的指针强转成派生类指针,赋值给派生类虽然可以,但是存在内存越界的问题
  • 友元关系不能继承
  • 静态成员真个继承体系当中只有一份
  • 继承是白箱操作,基类的 细节对派生类可见,在一定程度上破坏了封装性,而且一旦基类发生改变,对派生类的影响很淡,耦合性比较高
  • 组合是黑箱操作,且组合之间没有强的耦合性,所以优先使用组合

2.派生类的生成过程

  • 我们在前面说过三个必须在初始化列表进行初始化的变量:引用类型、const修饰、类类型变量(没有默认构造函数),
  • 这里同样,派生类当中的构造函数需要先在初始化列表处调用基类的构造函数来初始化派生类当中基类的那部分,再初始化派生类的成员。如果基类没有默认的构造函数,则必须在派生类的初始化类列表显示调用
  • 派生类的拷贝构造函数必须调用基类的拷贝构造函数来构造基类的部分
  • 派生类的赋值运算符重载必须调用基类的赋值运算符重载来完成基类的赋值
  • 派生类的析构函数会在调用完成后自动调用基类的析构完成基类部分数据的析构,保证派生类对象先清理派生类的成员再清理基类的成员
  • 派生类的调用顺序派生类的构造 -->基类的构造 -->派生类的行为 -->派生类析构 -->基类析构
  • 打印顺序基类的构造 -->派生类的构造 -->派生类的行为 -->派生类析构 -->基类析构

17.菱形继承的问题???

继承分为:单继承、多继承、菱形继承

1. 菱形继承带来的问题:数据冗余和二义性

2. 解决方法虚拟继承
假设类A(int a)、类B(int b)虚拟继承A,类C(int c)虚拟继承A,类D(int d)继承B C;D类的对象将共有的a放到对象模型的最下边,解决菱形继承带来的问题数据冗余和二义性

3. 那么B C类对象如何找到他呢?这里就引出虚基表指针
因为在虚拟继承下,B和C类当中会生成一个指针(虚基表指针),指向一张表(虚基表),虚基表的内容是当前位置到共有的a的偏移量,这样就可以找到BC当中的a了
在这里插入图片描述

4. 菱形继承的对象模型

  • 第一层:B对象当中的虚基表指针 + 成员
  • 第二层:C对象当中的虚基表指针 + 成员
  • 第三层:D对象的成员
  • 第四层:共有的变量
  • 注意sizeof的大小(32位):sizeof(A) == 4;sizeof(B) == 12;sizeof© == 12;sizeof(D) == 24

5. 那么为什么BC给要去找自己的a呢??
这就是我们前面说的切片问题(把派生类的对象赋值给基类),我们是把派生类当中基类的部分赋值给基类,如果不找到基类的部分就无法完成基类的赋值,就完成不了派生类的构造

18.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的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

19.多态?

1. 多态的概念:当不同的对象去完成某个行为时有不同的结果

2. 多态的构成条件????

  • 必须在继承体系下
  • 必须通过过基类的指针或引用调用虚函数
  • 被调用的函数必须是虚函数,并且派生类必须对该虚函数进行重写

3. 虚函数重写???

  • 派生类当中有一个和基类‘一样’的虚函数(函数名字、函数参数列表、返回值完全相同)
  • 特例:
  • 协变:基类和子类的虚函数返回值不同,基类返回基类对象的指针或引用,派生类返回派生类对象的指针或引用,
  • 析构函数的重写:如果基类的析构函数是虚函数(涉及资源管理必须是虚函数),那么派生类当中的析构函数无论是否加virtual,都构成重写,如果基类的析构函数不是虚函数并且派生类不重写,那么现在如果基类指针指向派生类的对象,那么在析构时只会释放基类的对象,而不会释放子类的对象,造成资源泄漏

4.派生类当中虚表的生成???

  • 一个包含虚函数的类当中至少有一个虚函数表指针_vfptr,因为要将虚函数的地址放到虚函数表当中
  • 将基类的虚表当中的内容拷贝一份到子类的虚表当中
  • 如果派生类当中重写了基类的某个虚函数,就用派生类当中自己的虚函数地址覆盖虚表当中相同偏移量处的基类虚函数地址
  • 派生类再将派生类自己的虚函数按照虚函数的声明次序依次放到自己的虚表末尾
  • 普通函数是不放进虚表当中的
  • 虚函数表本质上就是一个函数指针数组以nullptr结尾
  • 虚函数和虚函数表都存在代码段

5. 多态的原理????

  • 当基类的指针或引用去调用虚函数时,是在基类对象的虚表当中找虚函数的
  • 当派生类的指针或引用去调用虚函数时,是在派生类对象的虚表当中找虚函数的
  • 这样就构成多态,不同的对象调用时调用不同的函数完成不同的行为
  • 此时回想多态啊的两个主要条件:
  • 必须用基类的指针或引用来调用虚函数:完成的功能是:如果不用基类的指针或引用来调用,使用派生类当中的指针或引用来调用,那么当基类来调用虚函数时,就会把基类的指针或引用赋值给派生类,不安全
  • 派生类必须重写基类的虚函数,因为只有重写基类的虚函数后,派生类的虚表当中才有自己的虚函数地址,否则调用的还是基类的虚函数地址
  • 满足多态条件后的函数调用,不是在编译时确定的,而是在运行时在对象当中去找的

6. 多继承体系下,派生类对象的内存模型???

  • 继承的第一个类:虚表指针、第一个类的成员
  • 继承的第二个类:虚表指针、第二个类成员
  • 子类的非虚函数成员
  • 注意派生类将字自己的未重写的虚函数放到第一个继承类的虚表指针的末尾

7. 多态的注意事项???

  • 虚表是在编译期间生成的,在运行时去对象中调用的
  • 一个类的不同对象是共享同一张虚表的
  • 内联函数不可一是虚函数,因为内联函数没有函数地址,在编译阶段就被展开,而多态是在运行时去找函数地址的
  • 静态成员函数不可以是虚函数,因为静态成员函数没有this指针,并且静态成员函数可以通过类名的方式来访问 ,无法访问虚函数表,所以静态成员函数无法放进虚表当中
  • 构造函数不可以是虚函数,因为类对象当中的虚函数指针是在构造函数的初始化列表中进行填充的,而如果把构造函数设为虚函数,那么对象就会去虚表当中调用构造函数的虚函数地址,而如果要正常访问虚表就得需要知道虚表的地址内容,而这些刚好在初始化列表进行
  • 访问普通函数快还是虚函数块:如果普通对象来调用这两种函数,是一样快的,因为没有构成多态。如果是对象的指针或引用来调用这两种函数,则普通对象快,因为后者构成多态,在调用虚函数时需要去虚表当中找函数的地址
  • 虚表是在编译期间产生的,一般存在代码段
  • 析构函数是可以为虚函数的,设为虚函数防止内存泄漏问题
class A 
{
	public:
		~A()
		{
			cout<<"i am ~A"<<endl;
		}
};
class B : public A 
{
	public:
		~B()
		{
			cout<<"i am ~B"<<endl;
		}
};

int main() 
{
	A* pa = new B();
	//这样的调用如果析构函数不是虚函数,那么程序结束时只调用A的析构,那么
	//我们期望调用的是B的析构,从而导致内存泄漏问题;
	//如果析构函数设为虚函数,那么析构函数就构成多态,在程序结束时会调用
	//B的析构,避免内存泄漏
	return 0;
}
  • 注意如果虚函数带有缺省参数,子类重写的虚函数不可以重定义缺省参数的值,因为缺省参数是在编译期间确定的,而多态是在运行期间确定的

20.智能指针

c++中的smart pointer四个智能指针: shared_ptr,unique_ptr,weak_ptr,auto_ptr

1. 为什么要使用智能指针:
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。

2.RAII思想:
使用对象的生命周期来管理资源,在对象构造时管理资源,对象析构时释放资源(用户不需要显示释放资源,并且对象所需要的资源在其生命周期当中一直有效),缺陷是前拷贝

3. auto_ptr(c++98的方案,cpp11已经抛弃)
采用所有权转移模式

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题

4. unique_ptr(替换auto_ptr)
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。就是禁止拷贝、赋值

unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;//此时会报错!!

5. shared_ptr

  • shared_ptr采用引用计数实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放
  • 它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。
  • 除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
成员函数:

use_count 返回引用计数的个数

unique 返回是否是独占所有权( use_count 为 1)

swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的

6. weak_ptr

  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段
  • weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,
  • 它的构造和析构不会引起引用记数的增加或减少
  • weak_ptr是用来解决shared_ptr循环引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放
  • 它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
class B;
class A
{
	public:
	shared_ptr<B> pb_;
	~A()
	{
		cout<<"A delete\n";
	}
};
class B
{
	public:
	shared_ptr<A> pa_;
	~B()
	{
		cout<<"B delete\n";
	}
};
void fun()
{
	shared_ptr<B> pb(new B());
	shared_ptr<A> pa(new A());
	pb->pa_ = pa;
	pa->pb_ = pb;
	cout<<pb.use_count()<<endl;
	cout<<pa.use_count()<<endl;
}
int main()
{
	fun();
	return 0;
}
  • 可以看到fun函数中pa,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(AB的析构函数没有被调用),
  • 如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_;
  • 这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
  • 注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:
 shared_ptr p = pa->pb_.lock(); p->print();

21.c++11

C++11

22.右值引用

右值引用

23. 位图、布隆过滤器、海量数据

位图、布隆过滤器、海量数据

24.请你说一说内存溢出和内存泄漏

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

内存溢出原因

内存中加载的数据量过于庞大,如一次从数据库取出过多数据

集合类中有对对象的引用,使用完后未清空,使得不能回收

代码中存在死循环或循环产生过多重复的对象实体

使用的第三方软件中的BUG

启动参数内存值设定的过小

2、内存泄漏

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

内存泄漏的分类:

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

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

3、没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函

25.string的深浅拷贝问题

深浅拷贝

26.C++中的异常

1.概念:异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
  • try:try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块,try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码

2.异常的使用

  • 异常的抛出和捕获

异常的抛出和匹配原则

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码
  2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
  4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
  5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配可以抛出的派生类对象,使用基类捕获

在函数调用链中异常栈展开匹配原则

  1. 先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。

  2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。

  3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。

  4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行

  5. 有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理

3.异常安全问题

  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
  • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、操作句柄未关闭等)
  • C中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C经常使用RAII来解决以上问题
  • 总之一句话,程序中一旦进行异常处理,那么如果这个程序发生异常,执行流就会改变,所以一定要注意代码执行流的问题

4. 异常规范

  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
  2. 函数的后面接throw(),表示函数不抛异常。
  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常

5.异常的优缺点

C++异常的优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,具体看下面的详细解释
  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用
    异常。
  4. 很多测试框架都使用异常,这样能更好的使用单元测试等进行白盒的测试。
  5. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T&operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误

C++异常的缺点:

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
  2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。
  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func()throw();的方式规范化

27.std::sort详解

std::sort详解

28.STL空间配置器

STL空间配置器

二、操作系统

1. 请你来说一下fork函数

Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
  • 成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。
  • 子进程中,成功的fork( )调用会返回0
  • 父进程中fork( )返回子进程的pid
  • 如果出现错误,fork( )返回一个负值。
  • 最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。
  • 这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。
  • 早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中 。但从内核角度来说,逐页的复制方式是十分耗时的。
  • 现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制

2.请你说一下进程与线程的概念,以及为什么要有进程线程,其中有什么区别,他们各自又是怎么同步的

基本概念:

  • 进程是对运行时程序的封装,是系统进行资源分配的的基本单位,实现了操作系统的并发;
  • 线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间也就是同样的动态内存,映射文件,目标代码等等,打开的文件队列和其他内核资源)

区别:

  • 包含关系:一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量
  • 进程是资源分配的最小单位,线程是CPU调度的最小单位;
  • 系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。
  • 通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预
  • 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 进程适应于多核、多机分布;线程适用于多核

进程间通信的方式:
进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。
1.管道

  • 管道主要包括无名管道和命名管道:无名管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信
  • 普通管道PIPE:
    1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
    2)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
    3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中
  • 命名管道FIFO
    1)FIFO可以在无关的进程之间交换数据
    2)FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中

2. 系统IPC:

  • 消息队列
    消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标记。 (消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;
    特点
    1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
    2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
    3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

  • 信号量semaphore
    信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
    特点
    1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
    2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作
    3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
    4)支持信号量组

  • 信号signal
    信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

  • 共享内存(Shared Memory)

它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等
特点
1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取
2)因为多个进程可以同时操作,所以需要进行同步
3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

  • 套接字SOCKET:

socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信

线程间通信的方式:

  • 临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
  • 互斥量Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
  • 信号量Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
  • 事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作

3.请你说一说Linux虚拟地址空间

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

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

虚拟内存的好处:

  • 1.扩大地址空间
  • 2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改
  • 3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
  • 4.当进程通信时,可采用虚存共享的方式实现
  • 5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
  • 6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
  • 7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片

虚拟内存的代价:

  • 1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
  • 2.虚拟地址到物理地址的转换,增加了指令的执行时间。
  • 3.页面的换入换出需要磁盘I/O,这是很耗时的
  • 4.如果一页中只有一部分数据,会浪费内存。

4. 请你说一说操作系统中的缺页中断

  • malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常
  • 缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
    1、保护CPU现场
    2、分析中断原因
    3、转入缺页中断处理程序进行处理
    4、恢复CPU现场,继续执行
  • 但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:
    1、在指令执行期间产生和处理缺页中断信号
    2、一条指令在执行期间,可能产生多次缺页中断
    3、缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。

5. 请你回答一下fork和vfork的区别

vfork的基础知识:

  • 在实现写时复制之前,Unix的设计者们就一直很关注在fork后立刻执行exec所造成的地址空间的浪费。BSD的开发者们在3.0的BSD系统中引入了vfork(
    )系统调用
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
  • 除了 子进程必须要立刻执行一次对exec的系统调用,或者调用_exit( )退出 ,对vfork()的成功调用所产生的结果和fork( )是一样的。
  • vfork( )会挂起父进程直到子进程终止或者运行了一个新的可执行文件的映像
  • 通过这样的方式,vfork( )避免了地址空间的按页复制
  • 在这个过程中,父进程和子进程共享相同的地址空间和页表项。实际上vfork( )只完成了一件事:复制内部的内核数据结构。因此,子进程也就不能修改地址空间中的任何内存。
  • vfork( )是一个历史遗留产物,Linux本不应该实现它。需要注意的是,即使增加了写时复制,vfork( )也要比fork( )快,因为它没有进行页表项的复制。
  • 然而,写时复制的出现减少了对于替换fork( )争论。实际上,直到2.2.0内核,vfork( )只是一个封装过的fork( )。因为对vfork( )的需求要小于fork( ),所以vfork( )的这种实现方式是可行的。

写时复制

  • Linux采用了写时复制的方法,以减少fork时对父进程空间进程整体复制带来的开销。
  • 写时复制是一种采取了惰性优化方法来避免复制时的系统开销。它的前提很简单:
  • 如果有多个进程要读取它们自己的那部门资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。只要没有进程要去修改自己的“副本”,就存在着这样的幻觉:每个进程好像独占那个资源。从而就避免了复制带来的负担。
  • 如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。所以这就是名称的由来:在写入时进行复制。
  • 写时复制的主要好处在于:如果进程从来就不需要修改资源,则不需要进行复制。惰性算法的好处就在于它们尽量推迟代价高昂的操作,直到必要的时刻才会去执行。
  • 在使用虚拟内存的情况下,写时复制(Copy-On-Write)是以页为基础进行的。所以,只要进程不修改它全部的地址空间,那么就不必复制整个地址空间。
  • 在fork( )调用结束后,父进程和子进程都相信它们有一个自己的地址空间,但实际上它们共享父进程的原始页,接下来这些页又可以被其他的父进程或子进程共享。
  • 写时复制在内核中的实现非常简单。与内核页相关的数据结构可以被标记为只读和写时复制。如果有进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断的方式就是对该页进行一次透明复制。这时会清除页面的COW属性,表示着它不再被共享。
  • 现代的计算机系统结构中都在内存管理单元(MMU)提供了硬件级别的写时复制支持,所以实现是很容易的。
  • 在调用fork( )时,写时复制是有很大优势的。因为大量的fork之后都会跟着执行exec,那么复制整个父进程地址空间中的内容到子进程的地址空间完全是在浪费时间:如果子进程立刻执行一个新的二进制可执行文件的映像,它先前的地址空间就会被交换出去。写时复制可以对这种情况进行优化。

fork和vfork的区别

  • fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段
  • fork( )的父子进程的执行次序不确定vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。
  • vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁
  • vfork( )当需要改变共享数据段中变量的值,则拷贝父进程

6. 请你说一说并发(concurrency)和并行(parallelism)

  • 并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核cpu上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率。
  • 并行(parallelism):指严格物理意义上的同时运行, 比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu都是往多核方面发展。

7.请问单核机器上写多线程程序,是否需要考虑加锁,为什么?

  • 在单核机器上写多线程程序,仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。
  • 在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。

8. 请问线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的

  • 线程在切换的过程中需要保存 当前线程Id、线程状态、堆栈、寄存器状态等信息。
  • 其中寄存器主要包括SP PC EAX等寄存器,其主要功能如下:
    SP:堆栈指针,指向当前栈的栈顶地址
    PC:程序计数器,存储下一条将要执行的指令
    EAX:累加寄存器,用于加法乘法的缺省寄存器

9. 请你说一下虚拟内存置换的方式

比较常见的内存替换算法有:FIFO,LRU,LFU,LRU-K,2Q。
1、FIFO(先进先出淘汰算法)

  • 思想:最近刚访问的,将来访问的可能性比较大
  • 实现:使用一个队列,新加入的页面放入队尾,每次淘汰队首的页面,即最先进入的数据,最先被淘汰
  • 弊端:无法体现页面冷热信息

2、LFU(最不经常访问淘汰算法)

  • 思想:如果数据过去被访问多次,那么将来被访问的频率也更高。
  • 实现:每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。每次淘汰队尾数据块。
  • 开销:排序开销。
  • 弊端:缓存颠簸。

3、LRU(最近最少使用替换算法)

  • 思想:如果数据最近被访问过,那么将来被访问的几率也更高。
  • 实现:使用一个栈,新页面或者命中的页面则将该页面移动到栈底,每次替换栈顶的缓存页面。
  • 优点:LRU算法对热点数据命中率是很高的。
  • 缺陷:
    1)缓存颠簸
    2)缓存污染,突然大量偶发性的数据访问,会让内存中存放大量冷数据。

4、LRU-K(LRU-2、LRU-3)

  • 思想:最久未使用K次淘汰算法。
  • LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。
  • 实现:
    1)数据第一次被访问,加入到访问历史列表;
    2)如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
    3)当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;
    4)缓存数据队列中被再次访问后,重新排序;
    5)需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。
  • 针对问题:
    LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

5、2Q
类似LRU-2。使用一个FIFO队列和一个LRU队列。

  • 实现:
    1)新访问的数据插入到FIFO队列;
    2)如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;
    3)如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部;
    4)如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;
    5)LRU队列淘汰末尾的数据。
  • 针对问题:LRU的缓存污染
  • 弊端:
  • 当FIFO容量为2时,访问负载会退化为FIFO,用不到LRU

10.请你讲述一下互斥锁(mutex)机制,以及互斥锁和读写锁的区别

1、互斥锁和读写锁区别:

  • 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
  • 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
  • 互斥锁和读写锁的区别:
    1)读写锁区分读者和写者,而互斥锁不区分
    2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
  • 读写锁的接口
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
              const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 1.只有一个线程去占有写模式的读写锁
  • 2.可以有多个线程同时占有读模式的读写锁。(读模式之所以可以实现锁的共享,是因为内部维护了一份计数器,当有多个线程以读模式获取读写锁的时候,释放锁资源引用计数-1,获取 锁资源+1)
  • 3.当多个线程以读模式的方式获取读写锁,有一个线程以写模式获取读写锁:那么该写线程将会被阻塞,直到所有的读线程结束。
  • 4.当多个线程以读模式的方式获取读写锁,有一个线程以写模式获取读写锁,后面还有很多以读模式的方式获取读写锁:阻塞后面所有想要读的线程,保证以写模式的线程不会长时间阻塞
  • 5.读写锁适用于少量的写+大量的读操作。不能同时写但是可以同时读。当一个线程在写的时候,其他线程既不能读也不能写。一个线程在读的时候,其他线程可以读但是不可以写------》写是互斥的,读是共享的

2、Linux的4种锁机制:

  • 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
  • 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)适用于读取数据的频率远远大于写数据的频率的场合
  • 自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率但如果加锁时间过长,则会非常浪费CPU资源。
  • RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。

11. 请你来说一说用户态到内核态的转化原理

1)用户态切换到内核态的3种方式

  • 1、系统调用
    这是用户进程主动要求切换到内核态的一种方式,用户进程通过系统调用申请操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的ine 80h中断。
  • 2、异常
    当CPU在执行运行在用户态的程序时,发现了某些事件不可知的异常,这是会触发由当前运行进程切换到处理此。异常的内核相关程序中,也就到了内核态,比如缺页异常。
  • 3、外围设备的中断
    外围设备完成用户请求的操作之后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条将要执行的指令,转而去执行中断信号的处理程序, 如果先执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了有用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

2)切换操作
从出发方式看,可以在认为存在前述3种不同的类型,但是从最终实际完成由用户态到内核态的切换操作上来说,涉及的关键步骤是完全一样的,没有任何区别,都相当于执行了一个中断响应的过程,因为系统调用实际上最终是中断机制实现的,而异常和中断处理机制基本上是一样的,用户态切换到内核态的步骤主要包括
1、从当前进程的描述符中提取其内核栈的ss0及esp0信息。
2、使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个过程也完成了由用户栈找到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
3、将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。

12.请问GDB调试用过吗,什么是条件断点

1、GDB调试
GDB 是自由软件基金会(Free Software Foundation)的软件工具之一。它的作用是协助程序员找到代码中的错误。如果没有GDB的帮助,程序员要想跟踪代码的执行流程,唯一的办法就是添加大量的语句来产生特定的输出。但这一手段本身就可能会引入新的错误,从而也就无法对那些导致程序崩溃的错误代码进行分析。
GDB的出现减轻了开发人员的负担,他们可以在程序运行的时候单步跟踪自己的代码,或者通过断点暂时中止程序的执行。此外,他们还能够随时察看变量和内存的当前状态,并监视关键的数据结构是如何影响代码运行的。
2、条件断点
条件断点是当满足条件就中断程序运行,命令:break line-or-function if expr。
例如:(gdb)break 666 if testsize==100

13.理解文件描述符???

  • 进程的PCB当中有程序计数器、上下文环境信息,还有一个struct_file*这个变量,
  • struct_file这个结构体的当中也有一个变量叫file* array_fd的数组
  • 数组当中每个元素都是file*类型的指针,其指向的空间当中存放文件的相关信息,如文件大小、创建时间、所属者等,进而操作磁盘上的文件
  • 文件描述符就是file* array_fd数组的下标,就是通过这个下标来控制文件操作的。
  • 文件流指针FILE也是一个结构体,内部封装了文件描述符,并且提供了缓冲区,所以可以操作文件。

14.Ext2文件系统及文件的查找过程

Ext2文件系统:

  • Ext2文件系统当中,硬盘被分为多个块block,每个block当中有一个data block块,也被划分为一个一个的块,data block的作用是用来存放数据的,而我们怎么知道data block当中那个块使用过,那个块空闲呢?
  • 这里就需要用到block bitmap位图,用来标记每个块是否被使用。
  • 现在把数据已经放到硬盘当中了,那么我们还应该记录文件信息(如我们刚刚是谁往文件当中写的数据、什么时间修改的等文件信息),这些描述文件信息的叫inode,
  • 文件不只一个,inode就不只一个,所以就有inode table这个块,那么我们又怎么知道那个inode是空闲的哪个是被使用的,
  • 所以还需要一个位图inode bitmap位图来标记。
  • 最后还需要一个用来记录block和inode的总量信息。这就是ext2文件系统

查找文件的过程???
根据文件系统的定义,那么查找文件的过程就是根据文件名找到inode节点号再根据inode节点当中的信息找到存储数据的块把对应的块合并,就是整个文件的数据

15. 软连接和硬连接的区别???

  • 软连接文件有自己独立的inode节点号,但是操纵的确是同一个文件,即inode节点的内容是一样的,但是删除软连接文件是没有任何影响的,相当于快捷方式
  • 软链接有自己的文件属性及权限等
  • 可对不存在的文件或目录创建软链接
  • 软链接可交叉文件系统
  • 软链接可对文件或目录创建
  • 创建软链接时,链接计数 i_nlink 不会增加;
  • 删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。
ln -s 存在文件名 自定义文件名
  • 硬连接文件没有自己的inode节点号,和源文件一样的inode节点号,硬连接的删除是删除目录当中的记录,再把文件连接数-1,当连接数为0的时候就可以删除文件了。

  • 只能对已存在的文件进行创建

  • 不能交叉文件系统进行硬链接的创建;

  • 不能对目录进行创建,只可对文件创建

  • 删除一个硬链接文件并不影响其他有相同 inode 号的文件。

ln 存在文件名 自定义文件名

16.静态库和动态库创建???

Ar -tv[静态库名称];用来查看静态库的目录列表

生产静态库:ar -rc lib[name].a [name].o...;
使用:gcc [name].c -L[path] -l[name]

生产动态库:gcc -shared -fPIC [name].c  -o lib[name].so;
使用:gcc [name].c -L[path] -l[name]

17.创建共享内存流程???

  • a.在物理内存上开辟一段空间
  • b.每个进程通过页表结构将物理内存上的空间映射到自己虚拟地址空间的共享区
  • 每个进程间的通信是通过修改自己虚拟地址空间的共享区来完成的
 int shmget(key_t key, size_t size, int shmflg);创建共享内存操作句柄

key是共享内存的物理地址,size:是共享内存大小,shmflg:IPC_CREAT|IPC_EXCL
 void *shmat(int shmid, const void *shmaddr, int shmflg);将进程附加到创建
 //的共享内存上,shmid是共享内存操作句柄,shmaddr是指映射到进程虚拟地址
 //空间的那个地址上,一般为null,由操作系统分配,shmflg:0代表可读可写,
 //IPC_RDONLY只读,返回附加共享内存的具体地址,就可以拿这个地址进行读写操作了,注意
 //共享内存创建成功后,并且附加到当前进程上,是不用向文件一样去读取到,只需要拿到
 //shmat返回到映射地址,可以直接在地址上进行读写操作(就相当于你拿到了一个
 //char* 的数组)
int shmdt(const void *shmaddr);将共享内存和当前进程分离,
//shmaddr代表附加共享内存的具体地址,
int shmctl(int shmid, int cmd, struct shmid_ds *buf);将共享内存删除等操作,
//shmid为共享内存操作句柄,cmd为操作:IPC_STAT 查看共享内存的状态IPC_RMID删除共
//享内存,buf为出参,返回共享内存的状态信息等

18.管道和共享内存的特性

匿名管道的特性???

  • a.只适用于具有亲缘关系的进程之间
  • b.匿名管道提供流式服务,数据会被读走,需要重新写入
  • c.匿名管道是半双工的,数据的流向只能从一端就想另外一端
  • d.管道的生命周期随进程
  • e.管道的大小PIPE_SIZE是64K
  • f.PIPE_BUF的大小是4K,如果当前管道操作的数据大小是小于PIPE_BUF的,那么管道内部提供同步与互斥功能(原子的)
  • 管道的几种读写状态?????
  • 匿名管道(非阻塞):a.如果读端不关闭,调用写,write就会返回-1,表示当前资源不可用
  • b.如果读端全部关闭,在调用写,就会收到操作系统发给当先进程的SIGPIPE信号,导致当前进程终止
  • c.如果写端不关闭,调用读,read就会返回-1,表示当前资源不可用
  • d.如果写端全部关闭,调用读,read正常调用,返回读取的字节数

命名管道的特性???

  • 命名管道是具有标识符的,可适用于不同的进程之间的通信。其他特性适合匿名管道相同的

共享内存的特性??

  • a.共享内存是进程间最快的通信方式
  • b.共享内存不带同步与互斥的功能
  • c.共享内存写入数据是覆盖方式的,是不会被读走的
  • d共享内存的生命周期随内核

19.system V消息队列???

int msgget(key_t key, int msgflg); 创建消息队列的操作句柄,
key为消息队列的标识符,msgflg为IPC_CREAT
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
发送数据
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
					int msgflg); 接收数据
int msgctl(int msqid, int cmd, struct msqid_ds *buf);删除消息队列

System V消息队列 本质上是操作系统内核当中的优先级队列,进程通过访问每个节点或增加节点来进行通信。其生命周期随进程,带同步与互斥功能

20 .Sysyem V信号量???

二值信号量实现进程间的互斥

  • 通过01来对资源进行控制;1代表资源可用,0代表资源不可用
  • 当一个进程要访问一个资源等时候,先进行对信号量的预操作(-1);如果减完后小于0,代表之后资源是<0,就说明当前资源不可用,就把当前进程放到PCB等待队列当中;
  • 如果减完之后==0,就代表当前资源可用,该进程就可以使用该资源,后把资源计数(信号量)减一,代表当前资源已经有进程使用,不能再被其他进程使用了,因为二值信号量只有0和1两种取值

通过计数信号量实现进程间同步的

  • 计数器的值大于0,就代表可用资源的个数,计数器的值小于0的值就代表PCB等待队列当中等待访问资源的进程数量
  • 当一个进程访问使用一个资源,就对资源计数器-1,当一个进程释放一个资源不再使用某个资源就对资源计数器+1
  • 当资源计数器的值小于0,就代表PCB等待队列当中还有进程需要访问资源,那么当有其他进程释放资源的时候,就因该通知唤醒PCB等待队列当中等待资源的进程,告知其中的进程现在有资源可以使用
  • 当资源计数器大于0,就代表PCB等待队列当中没有进程等待使用资源,就不用去PCB当中唤醒。

21. 信号详解

1.几种常见的信号???

  • 11号信号是SIGSEGV,是段错误信号。
  • Ctrl + c是发2号信号SIGINT。
  • Ctrl + z是发20号信号SIGTSTP,
  • ctrl + \是发3号信号SIGQUIT

2.几种常见的函数???

  • raise函数相当于是自己给自己发信号,
  • abort函数是自杀信号(肯定会成功),
  • kill函数是给指定进程或进程组发信号,
  • alarm函数是在s秒后给当前进程发送SIGALRM信号,该信号的默认动作是终止当前进程

3.信号的4中产生方式????

  • 1.键盘键入信号
  • 2系统调用函数发送信号
  • 3.软件条件产生信号SIGPIPE/alarm函数的默认处理动作
  • 4.硬件异常产生信号,都会有操作系统给当前进程发送相应信号

4.信号的注意事项???

  • a.所有信号的产生后,都会有进程的管理者操作系统来执行处理函数,并且操作系统执行信号处理函数的时机是在其合适的时候,
  • 那么哪些不是被立即处理的信号是记录在进程地址空间当中的unsigend int 类型的位图当中。
  • 一个进程在没有收到信号的时候就知道自己对合法信号的默认处理方式,而操作系统向进程发送信号实际上是向进程写信号,修改进程PCB当中信号位图对应的比特位
  • b.进程是可以阻塞某个信号的,被阻塞的信号将被保存在未决状态,直到解除对信号对阻塞才可以执行抵达动作

5. 忽略信号和阻塞信号的区别???
忽略信号是在进程把信号抵达之后的一种处理方式,而阻塞是信号还没有成功执行的(已经收到但是被阻塞不可以执行信号处理函数,直到解除阻塞)

6.信号处理函数???

  • 信号的默认处理动作有:SIG_DFL(执行默认处理动作)SIG_IGN(执行忽略动作),Handler(执行用户自定义的信号处理动作)

7. 信号的注册???

  • 进程PCB的Task_struct当中有一个struct sigpending pending的结构体变量,而struct sigpending当中有一个双向链表,和sigset_t signal,而 sigset_t当中有一个unisigned long类型的数组,用的就是每一位的位图,
  • 当:1.非可靠信号注册时,先去看位图当中是否被设置过,如果为1,就代表发信号已经被注册过,就不再往sigqueue当中添加该信号的节点,而如果为0,就代表该信号未注册过,就往sigqueue当中添加该信号的节点
  • 2.当可靠信号注册的时候,先去看位图当中是否被设置过,如果为1,就代表发信号已经被注册过,仍然往sigqueue当中添加该信号的节点,而如果为0,就代表该信号未注册过,就往sigqueue当中添加该信号的节点

8. 信号的注销???

  • 1.非可靠信号的注销:先去判断位图大各种该信号的状态,如果为1,就直接把该比特位置为0,把该信号的节点从sigqueue当中去掉。
  • 2.可靠信号的注消,先去看sigqueue当中是否还有和当前信号一样的节点信息,a.如果有,就把该信号的一个节点删除,并不更改位图信息b.如果没有,就把该节点信息删除,并把该信号的比特位置为0;

9.信号的回收是????
当进程发现pending位图当中对应的比特位为1,系统就会由用户态进入内核态,如果信号处理函数是默认处理方式,就不去用户态,而如果信号处理函数是用户自定义的,就会切入用户态执行信号处理函数,执行完信号处理函数后,在调用系统调用sigreturn回到内核态,检查是否有其他信号要处理,没有就直接回到用户态(do_signal)。

10.用户自定义的信号捕捉处理流程???

  • 1.前提:tash_struct中有一个sighand_struct结构体的指针,其指向的空间是sighand_struct类型,其内部有一个action数组数组的每一个元素都是struct k_sigaction类型,其内部都是每个信号的处理逻辑,而struct k_sigaction内部有一个sigaction sa变量,而sa当中还有一个sa_handler变量这就是存储每个信号处理函数的地址的
  • 2.当调用signal时,是直接把sa_handler的值改变,达到处理用户自定义的处理函数
  • .3.当调用sigaction函数时是直接把action数组当中的内容给替换掉。
  • 4.signal函数底层是调用sigaction函数

11.信号的阻塞???

  • a.在task_struct结构体当中除了保存信号注册时的pending位图外,还保存了一个block位图,用来记录信号的阻塞信息.
  • b.信号的阻塞并不是信号不能被注册,二者是互不干扰的,都有响应的位图
  • c.每次操作系统调用do_signal函数时,发现收到某个信号,但是要处理这个信号之前需要判断block当中的位图是否为1,如果为1则不处理该信号,注意该信号的处理节点还在sigqueue当中,一旦操作系统接触该信号的阻塞,就会执行该信号的逻辑。如果block当中的位图为0,则直接处理该信号

12.信号阻塞的操作方式???

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how代表操作:SIG_BLOCK设某个信号为阻塞 SIG_NOBLOCK 设某个信号为非阻塞
SIG_SETMASH,设置某个信号的mask。Set为要设置的信号位图,
oldset为原来的信号位图
int sigemptyset(sigset_t *set);//将信号的位图清空即全0,
没有阻塞任何信号相当于非阻塞,
int sigfillset(sigset_t *set);/ /将信号的位图填满即全1,
相当于阻塞所有的信号(9、19号信号除外)
int sigaddset(sigset_t *set, int signum);(向位图当中添加一个信号)

int sigdelset(sigset_t *set, int signum);从位图当中删除一个信号

int sigismember(const sigset_t *set, int signum);检测某个信号是否在位图当中

22.线程相关函数

1.创建线程?????

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
				void *(*start_routine) (void *), void *arg);创建线程
Pthread_create函数的第二个 是pthread_attr_t类型的指针,
是可以访问线程的一些属性,如线程是否分离、线程栈大小、线程栈的位置、调度优先级等,
如pthread_attr_getstacksize()用来获取线程栈的大小

2. 线程等待???

int pthread_join(pthread_t thread, void **retval);
线程join的原因???
线程的默认属性是不自动释放线程资源,需要其他线程来回收它的资源,
如果不回收,就会造成资源泄漏(tid。信号屏蔽字、线程栈、errno、寄存器),
所以需要线程等待由其他线程回收,或者调用detach由操作系统来回收资源。

3.线程分离???

int pthread_detach(pthread_t thread);操作系统来回收线程资源

4.互斥锁???

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
互斥锁底层是一个互斥量(和二值信号量类似),也是一个计数器。
0表示资源不可用,1表示资源可用

5. 线程加锁原理???

  • mutex锁底层就是一个互斥量,1代表当前资源可用,就可以成功进行加锁,在对资源计数器-1置为0,表示当前资源已经有线程在使用,对其他线程是不可用。
  • 0代表当前资源不可用,线程不能够成功加锁,就会进入阻塞等待资源的状态(lock锁)。
  • 当有线程释放锁对时候,对锁的资源计数+1置为1,表示当前资源已经可以被其他线程使用。
  • trylock加锁是不阻塞等待资源,如果没获取到锁就直接返回。timedlock加锁是超过加锁时间还没有成功加锁就不会阻塞等待,会报错返回

6. 线程间同步???

int pthread_cond_wait(pthread_cond_t *restrict cond,
			             pthread_mutex_t *restrict mutex);

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

7.线程间同步的原因????

  • 保证各个线程访问资源的合理性;如果当前线程有资源可以获取就直接获取资源;没有资源的时候,就进入等待状态,等待另外一个线程来产生资源,然后唤醒等待线程
  • 使用条件变量来保证线程同步;同步:一个使线程等待的接口,一个使线程唤醒的接口,一个PCB等待队列;注意什么时候要使用线程同步要用户自己进行判断
  • 线程同步是避免轮训方式获取资源耗费CPU的情况;如多个线程去抢占使用资源,多个线程去生产资源,如果一个线程抢到资源就使用该资源,没抢到资源的线程就调用wait接口被放入到PCB等待对列当中,直到有其他线程产生资源,再去调用signal函数唤醒PCB等待队列当中的线程去抢占资源。

8.wait接口为什么还需要加互斥锁????

  • 同步只保证了生产者和消费者之间的同步,没有保证消费者之间或者生产者之间的互斥同步:一个线程消费资源,当消费线程没有获取到资源就会通知生产线程并进入PCB等待队列,一个线程生产资源,当生产线程生产一个资源就去通知消费线程进行消费。互斥:可能会有两个消费线程同时访问到了一个资源,造成线程安全问题,同理两个生产线程同时访问到一个资源也会造成线程安全问题)。增加互斥锁保证消费者或生产者之间每次只能有一个线程进行访问。

9.wait步骤详解????

  • 1.将该线程PCB放到PCB等待队列当中: 假设不将线程的PCB放到PCB等待队列当中,消费线程没有获取到资源就把释放锁,那么生产线程生产一个资源后可能不知道什么时候可以再次访问资源,消费线程就无法收到生产线程的signal唤醒
  • 2.对互斥锁解锁:如果不解锁而进入PCB等待队列等待,那么其他线程就无法获取到锁,该线程就一直在PCB等待队列当中等待资源,而其他线程因为锁没有被释放而无法获取到锁资源,从而导致程序阻塞
  • 3.当被从PCB等待队列当中唤醒的时候,首先竞争互斥锁资源(是因为线程的上下文信息、程序计数器的作用,所以可以继续抢锁),对互斥锁资源进行加锁操作
  • 3.1成功竞争到锁资源:跳出while轮训,执行下面的代码,访问资源.
  • 3.2没有成功竞争到锁资源:还在wait内部的逻辑当中,卡在抢锁的逻辑(但是已经从PCB等待队列当中移除),当其他线程释放锁后,该线程是有可能抢到锁的,那么就从wait返回,如果没有while轮询就会直接执行下面的逻辑,而不管条件是否满足,所以应该使用while轮询

10.注意???

  • signal是至少唤醒一个PCB等待队列当中的线程。broadcast唤醒全部PCB等待队列当中的线程
  • 多种角色需要多个等待队列,即多个条件变量
  • 如果 生产者或者消费者有多个,在进行使用条件变量的时候需要使用while轮寻来判断。
  • 不同角色的线程应该放到不同的PCB等待队列当中,如果放在同一个PCB队列中,可能会导致消费者线程唤醒的还是消费者,生产者线程唤醒的还是生产者线程,最后会导致程序阻塞在wait处

23. POSIX信号量???

  • POSIX信号量完成进程间/线程间的同步与互斥;1个资源计数器 + 等待队列 + 唤醒等待操作(通过自身内部提供的计数器来判断资源是否可以访问,不用用户手动判断)
Sem_init( sem,pshared,value),pshared表示适用于进程间还是线程间,
value = 1代表资源的个数为1,就可以实现互斥,当value > 1代表资源
个数大于1,就可以使心啊同步功能
  • POSIX 信号量不需要互斥锁的原因是其内部有一个资源计数器,直接可以判断资源是否可用。
int sem_post(sem_t *sem);

int sem_wait(sem_t *sem);

int sem_trywait(sem_t *sem);

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • POSIX 信号量保证同步是资源计数器,Sem_wait对内部的信号量-1,sem_post对资源计数器+1。互斥是二值信号量(保证资源数量不大于1),实现互斥。
  • sem实现生产者消费者模型的时候注意实现互斥的sem_wait(Lock)要在实现同步的sem_wait(Product)内部, 因为函数sem_wait内部是没有sem_post的功能,即在实现生产者消费者模型的时候,如果生产者Product已经把队列生产满了,那么就会阻塞在sem_wait(Product)当中(其没有释放锁的功能—只是那资源个数为1的时候模拟一个锁而已),如果把锁sem_wait(Lock)放到sem_wait(Product)的外部,其他线程sem_wait(Conuse)就无法获得到锁,就会导致程序阻塞。而pthread_cond_wait可以是因为其内部有释放锁的功能(给了其他线程抢锁的机会)。

24.请你说一说有了进程,为什么还要有线程?

线程产生的原因

  • 进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:
    (1).进程在同一时间只能干一件事
    (2).进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。

因此,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性。和进程相比,线程的优势如下:

  • 从资源上来讲,线程是一种非常"节俭"的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。
  • 从切换效率上来讲,运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。
  • 从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进城下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。

除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:

  • 1、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  • 2、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。

25.请你说一说死锁发生的条件以及如何解决死锁

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。死锁发生的四个必要条件如下:

  • 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;
  • 请求和保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源
  • 不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
  • 环路等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链

死锁预防的基本思想是 只要确保死锁发生的四个必要条件中至少有一个不成立,就能预防死锁的发生,具体方法包括:

  • 打破互斥条件:允许进程同时访问某些资源。但是,有些资源是不能被多个进程所共享的,这是由资源本身属性所决定的,因此,这种办法通常并无实用价值。
  • 打破占有并等待条件:可以实行资源预先分配策略(进程在运行前一次性向系统申请它所需要的全部资源,若所需全部资源得不到满足,则不分配任何资源,此进程暂不运行;只有当系统能满足当前进程所需的全部资源时,才一次性将所申请资源全部分配给该线程)或者只允许进程在没有占用资源时才可以申请资源(一个进程可申请一些资源并使用它们,但是在当前进程申请更多资源之前,它必须全部释放当前所占有的资源)。但是这种策略也存在一些缺点:在很多情况下,无法预知一个进程执行前所需的全部资源,因为进程是动态执行的,不可预知的;同时,会降低资源利用率,导致降低了进程的并发性。
  • 打破非抢占条件:允许进程强行从占有者哪里夺取某些资源。也就是说,但一个进程占有了一部分资源,在其申请新的资源且得不到满足时,它必须释放所有占有的资源以便让其它线程使用。这种预防死锁的方式实现起来困难,会降低系统性能。
  • 打破循环等待条件:实行资源有序分配策略。对所有资源排序编号,所有进程对资源的请求必须严格按资源序号递增的顺序提出,即只有占用了小号资源才能申请大号资源,这样就不回产生环路,预防死锁的发生。
  • 资源一次性分配,从而剥夺请求和保持条件
  • 可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件
  • 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件

26.请你来说一说协程

1、概念:
协程,又称微线程,纤程,英文名Coroutine。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

例如:

def A() :
print '1'
print '2'
print '3'
def B() :
print 'x'
print 'y'
print 'z'
由协程运行结果可能是12x3yz。在执行A的过程中,可以随时中断,
去执行B,B也可能在执行过程中中断再去执行A。但协程的特点在于是一个线程执行。

2)协程和线程区别

  • 那和多线程比,协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
  • 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

3)其他

  • 在协程上利用多核CPU呢——多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
  • Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。

27.请你说一下僵尸进程

1)正常进程
正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息,直到父进程通过wait / waitpid来取时才释放。保存信息包括:

1进程号the process ID

2退出状态the termination status of the process

3运行时间the amount of CPU time taken by the process等

2)孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

3)僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

僵尸进程是一个进程必然会经过的过程:这是每个子进程在结束时都要经过的阶段。

如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。

如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

危害:

如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

外部消灭:

通过kill发送SIGTERM或者SIGKILL信号消灭产生僵尸进程的进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源

内部解决:

1、子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。

2、fork两次,原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

28. wait函数和exec函数族

wait函数???

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);//pid为-1代表等待任意进程,大于0代表等待pid的进程
Option:0代表阻塞等待,代表非轮训等待
返回值:成功返回pid,失败返回-1,如果设置了WNOHANG,子进程未结束返回0
Wait/waitpid:如果子进程还在运行当中,那么父进程就会阻塞等待
进程退出状态status;正常退出:用的是status的当中低2个字节的高一字节来记录推出状态信息;异常退出:用的是status的当中低2个字节的低一字节当中的第7位记录退出信号
WIFEXITED(status)用来判断进程是正常退出还是异常退出
WEXITSTATUS(status)用来判断正常退出情况下的退出状态码

exec函数族???

exec函数族的调用返回值只有一个,即调用失败返回-1,成功是没有返回值的,
因为程序的数据代码已经被替换,返回也不知道返回给谁
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
                  ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
                char *const envp[]);

29.请介绍一下操作系统中的中断

中断是指CPU对系统发生的某个事件做出的一种反应,CPU暂停正在执行的程序,保存现场后自动去执行相应的处理程序,处理完该事件后再返回中断处继续执行原来的程序。中断一般三类,一种是由CPU外部引起的,如I/O中断、时钟中断,一种是来自CPU内部事件或程序执行中引起的中断,例如程序非法操作,地址越界、浮点溢出),最后一种是在程序中使用了系统调用引起的。而中断处理一般分为中断响应和中断处理两个步骤,中断响应由硬件实施,中断处理主要由软件实施。

30. 乐观锁、悲观锁

乐观锁、悲观锁

31.创建进程发生的事情

Linux创建一个进程,大致经历的过程如下:

  • 初始化进程描述符
  • 申请相应的内存区域
  • 设置进程状态、加入调度队列等等
  • 为了完整的描述一个进程,操作系统设计了非常复杂的数据结构、也申请了大量的内存空间。但是得益于写时复制技术,这些初始化操作,并没有明显的降低进程的创建速度。
  • 写时复制技术:当新进程(子进程)被创建时,Linux内核并不会立马将父进程的内容复制给子进程,而仅仅当进程空间的内容发生变化时,才执行复制操作。写时复制技术允许父子进程读取相同的物理页,只要两者有一个试图更改页内容,内核就会把这个页的内容拷贝到新的物理页,并把这块页分给正在写的进程。
  • Linux中有三种系统调用可以创建进程 clone()、fork()、vfork()
  • clone(): 最基础的创建进程的系统调用,可以指明子进程的基础属性(由各种FLAG标识)、堆栈等等。
  • fork(): 通过clone()实现,它的堆栈指向的是父进程的堆栈,因此父子进程共享同一个用户态堆栈。fork的子进程需要完全copy父进程的内存空间,但是得益于写时复制技术,这个过程其实挺快。
  • vfork(): 也是基于clone()来实现的,是历史上对fork()的优化,因为fork()需要copy父进程的内存空间,并且fork()后常常执行execve()将另一个程序加载进来,在写时复制技术之前,这种不必要的copy是代价是比较高昂的。因此vfork()实现时,会指明flag告诉clone()共享父进程的虚拟内存空间,以加快进程的创建过程。
  • 上下文切换
  • 概念:进程创建好之后,内核必须有能力挂起正在CPU运行的进程,并切换其他进程到CPU上执行。这种过程被称作为进程切换、任务切换或者上下文切换。
  • 这个过程包括硬件上下文切换和软件上下文切换。
  • 硬件上下文切换:主要通过汇编指令far jmp操作,将一个进程的描述符指针,替换为另一个进程描述符指针,并改变eip、cs、esp等寄存器,从而改变程序的执行流。
  • 软件上下文切换:内存地址的切换,切换页全局目录,安装新的地址空间。
    内核态堆栈的切换。
  • 进程切换发生在schedule()函数中,内核提供了一个 need_resched的标志,来表明是否需要重新执行一次调度。当某个进程被抢占或者更高优先级的进程进入可执行状态时,内核都会设置这个标志。那什么时候,内核会检查这个标志,来重新调度程序呢?那就是从内核态切换成用户态,或者从中断返回时。

执行系统调用时,会经历用户态与内核态的切换以及中断返回。也就是说,每一次执行系统调用,比如fork、read、write等,都可能触发内核调度新进程。

32.银行家算法

银行家算法:https://blog.csdn.net/wolfGuiDao/article/details/107513009

33.操作系统中进程调度策略有哪几种?

  • FIFO(先来先服务,队列实现,非抢占的):先请求CPU的进程先分配到CPU
  • SJF(最短作业优先调度算法):平均等待时间最短,但难以知道下一个CPU区间长度优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:老化
  • 时间片轮转调度算法(可抢占的):队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。
  • 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。
  • 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。

三、计算机网络

1. 请你说说select,epoll的区别,原理,性能,限制都说一说

1)IO多路复用

  • IO复用模型在阻塞IO模型上多了一个select函数,select函数有一个参数是文件描述符集合,意思就是对这些的文件描述符进行循环监听,当某个文件描述符就绪的时候,就对这个文件描述符进行处理
  • 这种IO模型是属于阻塞的IO。但是由于它可以对多个文件描述符进行阻塞监听,所以它的效率比阻塞IO模型高效
  • IO多路复用就是我们说的select,poll,epoll。
  • select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
  • 当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程
  • 所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
  • I/O多路复用和阻塞I/O其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
  • 所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大
  • select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
  • 在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

2)select

  • select:是最初解决IO阻塞问题的方法。用 结构体fd_set来告诉内核监听多个文件描述符,该结构体被称为描述符集。由数组来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理

select函数

int select(int nfds, fd_set *readfds, fd_set *writefds,
							fd_set *exceptfds, struct timeval *timeout);
  • nfds代表集合当中最大文件描述符fd_set类型:使用结构体中对应的比特位来表示要监视得的文件描述符(就相当于定义了一个位图)fd_set *readfds,监听读事件fd_set *writefds,监听写事件;fd_set *exceptfds, 监听异常事
  • Timeout:NULL表示阻塞等待,0表示非阻塞,直接返回,非0表示等待那么多秒。
  • 返回值:执行成功返回描述符状态准备就绪的个数就绪的文件描述符对应的比特位还在,没有就绪的文件描述符符对应的比特位将会被清0),返回0代表在timeout那么多时间内文件描述符的状态没有发生符改变。小于0代表错误,在errno当中

设置文件描述符接口

int fcntl(int fd, int cmd, ... /* arg */ );
//fcntl函数的第二个参数取值;
//F_GETFL:获取当前文件描述符的状态,
//F_SETFL:把文件描述符的一些属性设置到文件描述符当中(就是位图)
void FD_CLR(int fd, fd_set *set);      int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);		void FD_ZERO(fd_set *set);
  • FD_CLR:清理文件描述符词组set当中相关的比特位
  • FD_ISSET 判断文件描述符词组set当中相关的比特位是否为真
  • FD_SET设置文件描述符词组set当中相关的比特位
  • FD_ZERO清理文件描述符词组当中相全部的比特位

select存在的问题

  • 1. 内置数组的形式使得select的最大文件数受限与FD_SIZE
  • 2. 每次都需要手动把文件描述符设置进fd_set当中
  • 3. 每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态
  • 4. 轮寻排查当文件描述符个数很多时,效率很低(因为select的返回值是已经就绪的文件描述符,而没有就绪的文件描出符对应的比特位将会被清0,我们需要遍历临时存储select监控集当中的文件描述符变量temp并用FD_ISSET函数来判断每个描述符是否在select的返回值当中,在的就是已经就绪的文件描述符,我们就得知具体是那个文件描述符就绪,就可以进行通信了,不在的文件描述符就说明还没准备就绪);

3)poll

  • poll:通过一个可变长度的数组解决了select文件描述符受限的问题。数组中元素是结构体,该结构体保存描述符的信息,每增加一个文件描述符就向数组中加入一个结构体,结构体只需要拷贝一次到内核态。poll解决了select重复初始化的问题。轮寻排查的问题未解决

poll 函数

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds代表监控集文件描述符的数组集合,nfds代表监控文件描述符的个数

struct pollfd

struct pollfd {
              int   fd;         /* file descriptor */
              short events;     /* requested events */
              short revents;    /* returned events */
             };
  • struct pollfd结构体当中有文件描述fd + 监听事件集合events + 返回事件集合revents三部分组成;events和revents的常用取值:POLLIN数据可读、 POLLOUT 数据可写、POLLERR错误
  • 创建一个pollfd结构体变量的数组(所以没有限制文件描述符的数量),每个结构体的成员fd存放待监视的文件描述符,events存放监听的事件,revents代表发回的事件集合,然后调用poll,再循环判断数组每个元素当中的revents是否和event监听的事件是否一致,如果一致就说明该文件描述符已经就绪,可以进行通信

poll存在的问题

  • 1. 每次都需要手动把文件描述符设置进struct pollfd结构体当中
  • 3. 每次调用poll前都要重将fd从用户态拷贝到内核态,每次调用poll后,都需要将fd从内核态拷贝到用户态
  • 3. 轮寻排查当文件描述符个数很多时,效率很低(仍然需要轮训遍历)

4)epoll

  • epoll:轮询排查所有文件描述符的效率不高,使服务器并发能力受限。因此,epoll采用只返回状态发生变化的文件描述符,便解决了轮询的瓶颈。
  • epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式
    1. LT模式
    LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
    2. ET模式
    ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
  • ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。 epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死
    3、LT模式与ET模式的区别如下:
  • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
    4.Epoll函数???
int epoll_create(int size);创建epoll句柄
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//Epoll_ctl函数:epfd:为epoll句柄,op:表示动作(EPOLL_CTL_ADD将新的fd
//注册到epfd当中 op:EPOLL_CTL_MOD修改epfd监听的事件类型 
//EPOLL_CTL_DEL从epfd当中删除一个fd),fd:文件描述符,
//struct epoll_event* event:当中有一个events变量表示要监听的事件,
//还有一个epoll_data_t类型的 data数据,epoll_data_t中主要有一个文件描述符fd。
 typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

  struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };
  int epoll_wait(int epfd, struct epoll_event *events,
                               int maxevents, int timeout);
 //Epoll_wait函数:struct epoll_event* events内核会把就绪的文件描述符放到
 //events数组当中,注意内核只负责拷贝不负责开空间;
 //返回值:成功返回就绪的文件描述符的数量

5.原理:

  • 当进程调用epoll_creat函数的时候,操作系统就会在就会在内核创建一个eventpoll结构体,这个结构体当中主要有两个变量和epoll的使用相关,struct rb_root rbr(他是红黑树的根节点,存储着所有添加到epoll当中的监控事件)和struct list_head rdlist(这个变量是双向链表的头节点,其中存储着所有满足就绪事件的集合,将来通过epoll_wait函数来返回)这两个变量。
  • 调用一次epoll_ctl函数向内核添加一个监控事件,这些事件将会被放在红黑树上,而添加到epoll当中到所有事件都将和设备建立回调关系,当事件发生时将会调用这个回调方法(ep_poll_callback)将该节点的事件添加到双向链表rd_list当中
  • 在epoll当中,对于每个事件都会建立一个epitem结构体,其中存放红黑树、双向链表的信息
  • 调用epoll_wait检查是否有事件发生的时候,只需要检查eventpoll结构体当中的双向链表rd_list当中是否有epitem信息即可
  • 如果rd_list不为空则把双向链表拷贝到用户态并返回时间的数量
    6.epoll相对select的优点???
  • 接口方便:不需要每次设置监控的文件描述符
  • 数据拷贝轻量;每次在合适的时候调用epoll_ctl函数向内核拷贝
  • 为避免循环遍历直接采用回调机制把就绪的事件直接添加到双向链表当中,epoll_wait函数返回就知道哪些文件描述符就绪,不用遍历(epoll_ctl当中已经有文件描述符fd参数了,在struct epoll_event *event当中还有一个填充fd,所以当调用epoll_wait函数返回就绪文件描述符的时候,就不需要遍历原来的文件描述符就可以知道那个文件描述符就绪)
  • 没有数量限制

2. 请你说一下TCP怎么保证可靠性,并且简述一下TCP建立连接和断开连接的过程

TCP保证可靠性:

  • (1)序列号、确认应答、超时重传
    数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会说明了它下一次需要接收的数据序列号。如果发送发迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方在等待一定时间后会进行重传。这个时间一般是2*RTT(报文段往返时间)+一个偏差值。
  • (2)窗口控制与高速重发控制/快速重传(重复确认应答)
    TCP会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定要等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
    使用窗口控制,如果数据段1001-2000丢失,后面数据每次传输,确认应答都会不停地发送序号为1001的应答,表示我要接收1001开始的数据,发送端如果收到3次相同应答,就会立刻进行重发; 但还有种情况有可能是数据都收到了,但是有的应答丢失了,这种情况不会进行重发,因为发送端知道,如果是数据段丢失,接收端不会放过它的,会疯狂向它提醒…
  • (3)拥塞控制
    如果把窗口定的很大,发送端连续发送大量的数据,可能会造成网络的拥堵(大家都在用网,你在这狂发,吞吐量就那么大,当然会堵),甚至造成网络的瘫痪。所以TCP在为了防止这种情况而进行了拥塞控制。
  • (4)慢启动:
    定义拥塞窗口,一开始将该窗口大小设为1,之后每次收到确认应答(经过一个rtt),将拥塞窗口大小*2。
  • (5)拥塞避免
    **设置慢启动阈值,一般开始都设为65536。拥塞避免是指当拥塞窗口大小达到这个阈值,拥塞窗口的值不再指数上升,而是加法增加(每次确认应答/每个rtt,拥塞窗口大小+1),以此来避免拥塞。**将报文段的超时重传看做拥塞,则一旦发生超时重传,我们需要先将阈值设为当前窗口大小的一半,并且将窗口大小设为初值1,然后重新进入慢启动过程
  • (6)快速重传
    在遇到3次重复确认应答(高速重发控制)时,代表收到了3个报文段,但是这之前的1个段丢失了,便对它进行立即重传。然后,先将阈值设为当前窗口大小的一半,然后将拥塞窗口大小设为慢启动阈值+3的大小。

这样可以达到:在TCP通信时,网络吞吐量呈现逐渐的上升,并且随着拥堵来降低吞吐量,再进入慢慢上升的过程,网络不会轻易的发生瘫痪。

TCP建立连接和断开连接的过程:

在这里插入图片描述

  • 三次握手:
  1. Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
  2. Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
  3. Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给ServerServer检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
  • 四次挥手

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

1.数据传输结束后,客户端的应用进程发出连接释放报文段FIN(FIN = 1),序列号为u(seq = u),并停止发送数据,客户端进入FIN_WAIT_1状态,此时客户端依然可以接收服务器发送来的数据。

2.服务端发送ACK确认报文(ACK = 1),序列号为v(seq = v),确认报文u(ack = u + 1),进入CLOSE-WAIT状态,继续传送数据。客户端收到上述报文进入FIN-WAIT2状态,继续接收服务端传输的数据

3.当服务器没有数据要发送时,数据传输完毕后,发送FIN报文(FIN = 1,ACK = 1),序列号为w(seq = w),确认报文u(ack = u + 1),进入LAST-ACK状态,等待最后一个ACK。

4.客户端发送ACK确认报文(ACK = 1),序列号为u+1(seq = u + 1),确认报文w(ack = w + 1),进入TIME-WAIT状态,等待2MSL(最长报文段寿命),客户端进入CLOSED状态服务端收到后上述报文后进入CLOSED状态。

3. 请回答一下HTTP和HTTPS的区别,以及HTTPS有什么缺点?

HTTP和HTTPS的区别

4.请你说一说TCP拥塞控制?以及达到什么情况的时候开始减慢增长的速度?

拥塞控制是防止过多的数据注入网络,使得网络中的路由器或者链路过载。流量控制是点对点的通信量控制,而拥塞控制是全局的网络流量整体性的控制。发送双方都有一个拥塞窗口——cwnd

  • 1、慢开始
    最开始发送方的拥塞窗口为1,由小到大逐渐增大发送窗口和拥塞窗口。每经过一个传输轮次,拥塞窗口cwnd加倍。当cwnd超过慢开始门限,则使用拥塞避免算法,避免cwnd增长过大。

  • 2、拥塞避免
    每经过一个往返时间RTT,cwnd就增长1。
    在慢开始和拥塞避免的过程中,一旦发现网络拥塞,就把慢开始门限设为当前值的一半,并且重新设置cwnd为1,重新慢启动。(乘法减小,加法增大)

  • 3、快重传
    接收方每次收到一个失序的报文段后就立即发出重复确认,发送方只要连续收到三个重复确认就立即重传(尽早重传未被确认的报文段)。

  • 4、快恢复
    当发送方连续收到了三个重复确认,就乘法减半(慢开始门限减半),将当前的cwnd设置为慢开始门限,并且采用拥塞避免算法(连续收到了三个重复请求,说明当前网络可能没有拥塞)。

采用快恢复算法时,慢开始只在建立连接和网络超时才使用。

达到什么情况的时候开始减慢增长的速度?

采用慢开始和拥塞避免算法的时候

  1. 一旦cwnd>慢开始门限,就采用拥塞避免算法,减慢增长速度
  2. 一旦出现丢包的情况,就重新进行慢开始,减慢增长速度
    采用快恢复和快重传算法的时候
  3. 一旦cwnd>慢开始门限,就采用拥塞避免算法,减慢增长速度
  4. 一旦发送方连续收到了三个重复确认,就采用拥塞避免算法,减慢增长速度

5.TCP和UDP的区别和各自适用的场景

1)TCP和UDP区别

  • 1) 连接
    TCP是面向连接的传输层协议,即传输数据之前必须先建立好连接。
    UDP无连接。
  • 2) 服务对象
    TCP是点对点的两点间服务,即一条TCP连接只能有两个端点;
    UDP支持一对一,一对多,多对一,多对多的交互通信。
  • 3) 可靠性
    TCP是可靠交付:无差错,不丢失,不重复,按序到达。
    UDP是尽最大努力交付,不保证可靠交付。
  • 4)拥塞控制,流量控制
    TCP有拥塞控制和流量控制保证数据传输的安全性。
    UDP没有拥塞控制,网络拥塞不会影响源主机的发送效率。
  • 5) 报文长度
    TCP是动态报文长度,面向字节流,即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的。
    UDP面向报文,不合并,不拆分,保留上面传下来报文的边界。
  • 6) 首部开销
    TCP首部开销大,首部20个字节。
    UDP首部开销小,8字节。(源端口,目的端口,数据长度,校验和)
  • 7)UDP没有真正意义上的发送缓冲区
    调用sendto函数会直接把数据交给内核,有内核把数据交给网络层UDP由接收缓冲区,但是无法保证数据的顺序是否和原来的一致,并且如果接收缓冲区满了,还有数据发送就会被直接丢弃
  • 8).UDP的socket是全双工
    UDP首部有16位的数据长度,所以UDP传送数据的最大长度为64K,如果超过64K就需要手动分包、多次发送、手动拼装

2)TCP和UDP适用场景

从特点上我们已经知道,TCP 是可靠的但传输速度慢,UDP 是不可靠的但传输速度快。因此在选用具体协议通信时,应该根据通信数据的要求而决定。

若通信数据完整性需让位与通信实时性,则应该选用TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)。

6.进行三次握手、四次挥手及timewait的原因

1)三次握手原因:

  • 三次握手是为了防止,客户端的请求报文在网络滞留,客户端超时重传了请求报文,服务端建立连接,传输数据,释放连接之后,服务器又收到了客户端滞留的请求报文,建立连接一直等待客户端发送数据。
  • 服务器对客户端的请求进行回应(第二次握手)后,就会理所当然的认为连接已建立,而如果客户端并没有收到服务器的回应呢?此时,客户端仍认为连接未建立,服务器会对已建立的连接保存必要的资源,如果大量的这种情况,服务器会崩溃。
  • 因为TCP为保证可靠性,对传输对数据进行序列号,数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会说明了它下一次需要接收的数据序列号,所以三次握手就是相互确认序号的,两次握手只能确认一方的序列号

2)为什么TCP协议终止链接要四次?

  • 当客户端确认发送完数据且知道服务器已经接收完了,想要关闭发送数据口(当然确认信号还是可以发),就会发FIN给服务器。
  • 服务器收到客户端发送的FIN,表示收到了,就会发送ACK回复,这样关闭客户端到服务端的通信。
  • 但这时候服务器可能还在发送数据,没有想要关闭数据口的意思,所以服务器的FIN与ACK不是同时发送的,而是等到服务器数据发送完了,才会发送FIN给客户端。
  • 客户端收到服务器发来的FIN,知道服务器的数据也发送完了,回复ACK, 客户端等待2MSL以后,没有收到服务器传来的任何消息,知道服务器已经收到自己的ACK了,客户端就关闭链接,服务器也关闭链接了。

3)2MSL意义:

  • 保证最后一次握手报文能到达服务端,可以进行超时重传
  • 2MSL后,双向连接产生的所有报文都会消失,不会影响下一次连接(msl是报文最大的生命周期)
  • 如果没有TIME_WAIT状态,主动请求关闭链接的一方就会直接进入关闭状态,如果因为网络原因最后一个ACK发生了丢包,服务端就会不断的请求FIN,等待最后一个ACK,链接并没有成功关闭,并且如果此时打开一个新的链接,那么服务器端就会把SYN请求当成ACK,因而发生请求码错误,服务端就会发送RET重置链接。而TIME_WAIT的作用就是让主动请求的一方进入TIME_WAIT状态后等待2MSL时间关闭链接,等待这段时间是为了让客户端收到服务器端的FIN后可以有充分的时间回复ACK,让网络中延迟的FIN/ACK失效

7. 请你来说一下GET和POST的区别

1、概括

  • 对于GET方法用来向服务器获取请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
  • 而对于POST用来想服务器提交数据,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200
    ok(返回数据)

2、区别:
1、get参数通过url传递,post放在request body中。
2、get请求在url中传递的参数是有长度限制的,而post没有。
3、get比post更不安全,因为参数直接暴露在url中,所以不能用来传递敏感信息。
4、get请求只能进行url编码,而post支持多种编码方式。
5、get请求会浏览器主动cache,而post支持多种编码方式。
6、get请求参数会被完整保留在浏览历史记录里,而post中的参数不会被保留。
7、GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
8、GET产生一个TCP数据包;POST产生两个TCP数据包。

8.listen函数的第二个参数理解???

  • 代表全链接队列的大小(真正链接队列的大小是listen第二个参数 + 1)
  • 操作系统在**调用listen时维护了两个队列,半链接队列(存放SYN_SEND或SYN_RECV状态请求的文件描述符)和全链接队列(用来保存处于ESTABLISHED状态的文件描述符,即成功建立连接的文件描述符的集合,等待accept的调用取走)。**
  • 所以listen的作用就是等待三次握手的完成,在accept时三次握手已经完成

9.五层协议???

  • 应用层:负责程序之间数据的沟通。程序员就工作在这一次,负责数据的接收和发送,约定数据的格式等。典型协议:Http、FTP、DNS、SMTP协议,url的组成
  • 传输层:负责数据端与端之间的传输,TCP、UDP。0~1023是知名端口,1024~65535为操作系统可分配的端口,ssh:22,Http:80,Https:443,ftp:21,telnet:23,
  • 网络层:负责路由选择和地址管理,判断数据要发送给那个主机,IP、路由器设备
  • 数据链路层:负责相邻设备之间数据帧的传输,标识数据帧冲哪里来到哪里去。以太网协议,交换机设备。
  • 物理层:负责光电信号的传输,以太网协议、交换机设备

10.搜索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层。

11.请你说说TCP/IP数据链路层的交互过程

  • 网络层等到数据链层用mac地址作为通信目标,数据包到达网络等准备往数据链层发送的时候,首先会去自己的arp缓存表(存着ip-mac对应关系)去查找改目标ip的mac地址,
  • 如果查到了,就讲目标ip的mac地址封装到链路层数据包的包头。
  • 如果缓存中没有找到,会发起一个广播:who is ip XXX tell ip XXX,所有收到的广播的机器看这个ip是不是自己的,如果是自己的,则以单拨的形式将自己的mac地址回复给请求的机器

12.请你来说一下socket编程中服务器端和客户端主要用到哪些函数

1)基于TCP的socket:

1、服务器端程序:

1创建一个socket,用函数socket()

2绑定IP地址、端口等信息到socket上,用函数bind()

3设置允许的最大连接数,用函数listen()

4接收客户端上来的连接,用函数accept()

5收发数据,用函数send()和recv(),或者read()和write()

6关闭网络连接

2、客户端程序:

1创建一个socket,用函数socket()

2设置要连接的对方的IP地址和端口等属性

3连接服务器,用函数connect()

4收发数据,用函数send()和recv(),或read()和write()

5关闭网络连接

2)基于UDP的socket:

1、服务器端流程

1建立套接字文件描述符,使用函数socket(),生成套接字文件描述符。

2设置服务器地址和侦听端口,初始化要绑定的网络地址结构。

3绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定。

4接收客户端的数据,使用recvfrom()函数接收客户端的网络数据。

5向客户端发送数据,使用sendto()函数向服务器主机发送数据。

6关闭套接字,使用close()函数释放资源。UDP协议的客户端流程

2、客户端流程

1建立套接字文件描述符,socket()。

2设置服务器地址和端口,struct sockaddr。

3向服务器发送数据,sendto()。

4接收服务器的数据,recvfrom()。

5关闭套接字,close()。

13.请你来介绍一下udp的connect函数

除非套接字已连接,否则异步错误是不会反悔到UDP套接字的。我们确实可以给UDP套接字调用connect,然而这样做的结果却与TCP连接不同的是没有三路握手过程。 内核只是检查是否存在立即可知的错误,记录对端的IP地址和端口号,然后立即返回调用进程。

对于已连接UDP套接字,与默认的未连接UDP套接字相比,发生了三个变化。
其实 一旦UDP套接字调用了connect系统调用,那么这个UDP上的连接就变成一对一的连接,但是通过这个UDP连接传输数据的性质还是不变的,仍然是不可靠的UDP连接。一旦变成一对一的连接,在调用系统调用发送和接受数据时也就可以使用TCP那一套系统调用了。

  • 1、我们再也不能给输出操作指定目的IP地址和端口号。也就是说,我们不使用sendto,而改用write或send。写到已连接UDP套接字上的任何内容都自动发送到由connect指定的协议地址。可以给已连接的UDP套接字调用sendto,但是不能指定目的地址。sendto的第五个参数必须为空指针,第六个参数应该为0.
  • 2、不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在一个已连接UDP套接字上,由内核为输入操作返回的数据报只有那些来自connect指定协议地址的数据报。这样就限制一个已连接UDP套接字能且仅能与一个对端交换数据报。
  • 3、由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接的UDP套接字不接收任何异步错误。来自任何其他IP地址或断开的数据报不投递给这个已连接套接字,因为它们要么源IP地址要么源UDP端口不与该套接字connect到的协议地址相匹配。
  • 4、UDP客户进程或服务器进程只在使用自己的UDP套接字与确定的唯一对端进行通信时,才可以调用connect。调用connect的通常是UDP客户,不过有些网络应用中的UDP服务器会与单个客户长时间通信TFTP,这种情况下,客户和服务器都可能调用connect。

14.请你说一下阻塞,非阻塞,同步,异步

  • 阻塞和非阻塞:调用者在事件没有发生的时候,一直在等待事件发生,不能去处理别的任务这是阻塞。调用者在事件没有发生的时候,可以去处理别的任务这是非阻塞。
  • 同步和异步:调用者必须循环自去查看事件有没有发生,这种情况是同步。调用者不用自己去查看事件有没有发生,而是等待着注册在事件上的回调函数通知自己,这种情况是异步

15.请你来介绍一下5种IO模型(等待 + 拷贝数据)

  • 1.阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。举个例子,一个人拿着鱼竿去钓鱼,他一直阻塞等待鱼上钩,中间不干任何事
  • 2.非阻塞IO:非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。 举个例子,一个人拿着鱼竿去钓鱼,他一边等待鱼上钩,一边看报
  • 3.信号驱动IO:信号驱动IO:linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号。然后处理IO事件。举个例子,一个人拿着鱼竿去钓鱼,他在鱼竿上挂一个铃铛,然后他去干自己的事,如果听到铃声就去钓鱼
  • 4 .IO复用/多路转接IO:linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数。举个例子,一个人去钓鱼,但是他一次拿多个鱼竿,自己轮训遍历所有鱼竿看是否有鱼上钩,中间不干任何事
  • 5.异步IO:linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。举个例子,一个人想去钓鱼,但是他不自己去,而是雇佣其他人去钓鱼,如果有鱼上钩就打电话给自己自己去钓鱼

16.IP地址不够的解决方式???

  • 1.动态分配IP地址,只给接入网络的设备分配IP 。
  • 2.NAT技术:路由器有两个接口,LAN接口代表是子网IP,WAN接口是WAN口IP,我们的所有主机都是链接在LAN接口下的,多个主机就组成了一个局域网,每个路由器局域网下的IP是不可以重复的,但是路由器之间的局域网内是可以出现重复的,每次处于局域网内的主机要访问到公网进行通信,路由器将LAN接口下的ip替换成WAN口IP,但是WAN口IP不一定能直接访问到外网,可能需要多次替换,这样就减少IP地址的使用数量
  • 3.IPV6技术

17.路由:???

路由的过程是“一跳一跳”问路的过程,“一跳”就是源Mac地址发往目的Mac地址间的数据帧传输。IP数据包也一样,跟据路由器当中维护的路由表选择转发路径,如果拿目的ip和路由表当中每个IP对应的子网掩码做 & 运算,如果和对应的IP相等,就从该IP直接发给主机,反之&完如果没有一个相等的IP,就把IP数据包从缺省路由条目IP发出去,IP数据包就交给下一个路由器进行发送。

18.以太网是什么??

以太网不是一种网络,而是一种技术标准,即包含了数据链路层的内容,也包含了物理层的内容,如:规定了网络的拓扑结构、传输速率等。以太网是当前使用最广泛的局域网技术,和其并列的还有令牌环网技术、无线LAN等

19.Mac地址的作用??

Mac地址是用来识别数据链路层中相连的节点。是真正的物理设备上 的地址(IP地址只是提供了从哪里来,最后到哪里去,而Mac地址的作用:就是根据IP地址通过arp解析协议把IP地址映射到Mac地址,达到链路上的下一个节点后,再映射层IP地址,经过路由,看是否可以直接发给目的主机)

20.什么是MTU???

  • MTU:1500字节称为以太网当中最大的传输单元,不同的网络类型有不同的MTU。是对链路层的限制。
  • MTU对IP数据包长度限制???IP会因为MTU的限制,把较大的IP数据包进行分包处理,到达对端的IP层后重组返回给传输层。一旦丢包就会重组失败
  • MTU对UDP数据包长度限制???UDP的数据一旦超过1472(1500(MTU) - 20(TCP首部) - 8(UDP首部)),那么数据在IP层就会被分包,就大大提高数据丢包的概率
  • MTU对TCP数据包长度限制???TCP的数据包长度还是受制与MTU,TCP的单个数据包的最大消息长度称为MMS,TCP在三次握手的过程中就完成MMS 的协商,根据自己所能接受的MMS之后,选择双发较小的MMS,(理性想情况下,MMS刚好是IP最大不分片的长度),所以还是受制于MTU。

21.浏览器输入url后发生的事???

在输入一个网址后,发生的事情分为六步:DNS域名解析,TCP连接,HTTP请求,接收响应结果,浏览器解析HTML,浏览器布局渲染。

  1. 查找域名的IP地址

我们在浏览器中输入一个网址(URL),首先,浏览器会根据输入的网址找到对应的IP地址。那么,怎样找到对应的IP地址呢?接下来我们就来看一下。

(1)URL的格式

Url的组成:[协议方案名:HTTP]: //[user]:[passwd]@[服务器地址]:[端口]/[带层次的文件路径]?[查询字符串:query string]#片段标识符

一个URL包括协议,网络地址,资源路径;

协议,最常用的比如HTTP(超文本传输协议),FTP(文件传输协议);

网络地址,可以是域名或IP地址,包括端口号,如果没有端口号,默认为80;

资源路径,可以是多种多样的。

(2)DNS域名解析

浏览器发现输入的网址不是IP地址,便向操作系统发送请求IP地址,操作系统启动DNS域名解析协议,接下来就开始DNS查询了。

第一步:先在各种缓存信息中查找

浏览器缓存——浏览器会缓存DNS一段时间,但是操作系统不会告诉浏览器缓存多长时间,这个缓存时间完全由浏览器自己决定。

系统缓存——如果在浏览器中没有找到,浏览器会做一个系统调用,获得系统缓存中的记录。

路由器缓存——接着会将请求发给路由器,路由器一般也有自己的DNS缓存。

如果在缓存信息中都没有查找到,则转第二步。

第二步:DNS服务器查找

全球所有的DNS服务器组成了一个DNS域名解析系统,在这个系统中,包含了全球所有的主机和IP地址的映射。所以,先在和它直接相连的DNS服务器中查找,一般情况下,在这个DNS服务器中都可以找到,但是也不排除特殊情况。

如果在和本地相连的服务器上没有找到想要的IP地址,则进行递归查找。本地服务器请求比他高一级的服务器或者根服务器,根服务器查询自己的数据库,如果知道对应的IP地址,则返回信息给本地服务器,本地服务器再将信息返回给浏览器;如果没有直接找到对应的IP地址,则告诉本地服务器应该在另外的哪一个服务器上询问,然后将询问到的信息返回给浏览器。(在根服务器上一定可以找到对应的IP地址)。

  1. TCP连接

在知道对应的IP地址后,接下来我们就可以进行TCP连接请求了。TCP向服务器端发送SYN连接请求,经过TCP三次连接成功后,浏览器就和服务器端建立好连接了,就可以相互发送数据了。

  1. 浏览器发起web服务器 HTTP请求

根据HTTP协议的要求,组织一个HTTP数据包,HTTP请求的报头有请求行和报文,请求行包括三部分,请求方法,URL(服务器上的资源),版本。报文有一些其他信息,比如请求正文的有效载荷长度(Content_Length),缓存信息(Cache_Control),Cookie等。

  1. HTTP响应

在通过HTTP请求服务后,服务器会向浏览器返回一个应答信息——HTTP响应。HTTP响应的报头包括三部分——版本,状态码,状态码描述。

  1. 浏览器跟踪重定向地址

现在浏览器知道了真正要访问的目标服务器在哪里,便向此目标服务器发送和第三步相同的报文,请求响应。

  1. 服务器处理请求

服务器接收到获取请求,然后处理并返回一个响应。

这表面上看起来是一个顺向的任务,但其实这中间发生了很多有意思的东西:

Web 服务器软件
web服务器软件(像IIS和阿帕奇)接收到HTTP请求,然后确定执行什么请求处理来处理它。请求处理就是一个能够读懂请求并且能生成HTML来进行响应的程序(像ASP.NET,PHP,RUBY…)。
请求处理
请求处理阅读请求及它的参数和cookies。它会读取也可能更新一些数据,并讲数据存储在服务器上。然后,需求处理会生成一个HTML响应。

  1. 浏览器解析HTML

就像我们平常请求网页一样,浏览器会一个一个的响应出用户请求的页面,这个页面里面有表格,有图片,有文字,也可能有视频等等。

浏览器按顺序解析html文件,构建DOM树,在解析到外部的css和js文件时,向服务器发起请求下载资源,若是下载css文件,则解析器会在下载的同时继续解析后面的html来构建DOM树,则在下载js文件和执行它时,解析器会停止对html的解析。

  1. 浏览器布局渲染

布局:通过计算得到每个渲染对象在可视区域中的具体位置信息(大小和位置),这是一个递归的过程。
绘制:将计算好的每个像素点信息绘制在屏幕。

  1. TCP断开连接

在完成所有的工作后,客户端就要发送断开连接请求了,TCP释放连接需要四次挥手。

22.tcp的粘包问题???

粘包问题:因为对每个数据进行编号,传输层是一个一个报文按照序号放在缓冲区当中,应用层是一串连续的字节数据。所以必须明确包的边界(定长的包,每次读取固定大小,变长包可以在包的头部记录包的长度或者约定特殊标记)

23.几个常用协议或技术???

  • ARP协议:ARP协议是介于数据链路层和网络层的协议,来完成IP地址和Mac地址之间的映射关系
  • DNS协议:DNS是一套完整的域名映射到IP的系统,应用层协议
  • ICMP协议:是网络层的协议,IP本身不提供可靠的传输,如果测试网络畅通的时候丢包了,我们就无法得知网络的情况,所以就需要ICMP(主要功能是确认IP数据包是否成功到达目标地址)来完成这个功能。
  • ping命令:ping[域名],用来验证网络的畅通性,同时统计响应时间和ttl(IP包中的生命周期),ping命令会先发一个ICMP Echo Requse给对端,对端收到后也会回一个ICMP Echo Reply。,所以ping命令是基于ICMP协议工作在网络层,端口在传输层

24.NAT(用来解决ipv4地址数量不够到问题)技术详解????

NAT技术:
NAT技术可以实现路由器把私有IP转化为全局IP到作用。NAT路由器当中都有一个地址转换表,当私有IP第一次访问的时候就会被记录在这张表当中,形成映射关系(简单来说NAT技术就是搞代理的,你自己的圈子有限但是你有想要更大圈子的东西,这时候就需要一个代理来帮你完成这一功能)。

NAPT技术???
NAPT技术就是在NAT技术的基础上加一个端口信息,用来区分多个请求发往同一个服务器,然后服务器返回的IP都相同的情况,所以加一个端口用来区分

NAPT技术的缺陷???
NAPT技术的缺陷:由于该技术依赖于这张转换表,无法从服务器外部向服务器内部建立链接,并且这张表的构造和销毁都很费时

注意NAT技术和代理服务器的区别????
注意NAT技术和代理服务器的区别:

  • 从应用上讲:NAT技术主要是来解决IP数量不足的问题;而代理服务器是通过代理服务器进行翻墙,比如游戏加速器;
  • 从底层是现实上将:NAT技术是工作在网络层,完成私有IP和全局IP的转换;代理服务工作在应用层;
  • 从工作范围上讲:NAT技术工作在局域网的出口处;而代理服务器既可以工作在局域网内,也可以工作在局域网外。
  • 从部署位置上讲:NAT技术一般部署在路由器上;代理服务器则是一个软件程序部署在服务器上

25.Https的请求过程

  • 1) 客户端向服务器发起HTTPS请求,连接到服务器的443端口;
  • 2) 服务器端有一个密钥对,即公钥(即数字证书)和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人;
  • 3) 服务器将自己的公钥发送给客户端;
  • 4) 客户端收到服务器端的公钥之后,检查其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续,如果公钥合格,则客户端会生成一个客户端密钥,然后用服务器的公钥对客户端密钥进行非对称加密成密文,至此,HTTPS中的第一次HTTP请求结束;
  • 5) 客户端发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器
  • 6) 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文;
  • 7) 然后服务器将加密后的密文发送给客户端;
  • 8) 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束,整个HTTPS传输完成。
https的请求过程
1.浏览器向服务器发起请求,请求携带了浏览器支持的加密算法和哈希算法
2.服务器收到请求,选择浏览器支持的加密算法和哈希算法
3.服务器将数字证书返回给浏览器
4.浏览器进入数字证书的认证环节
5.浏览器生成一个随机数R,并使用网站公钥对R进行加密,浏览器将加密后的R传送给服务器
6.服务器通过自己的私钥解密得到R
7服务器以R为密钥将使用了对称加密算法的加密网页内容传输给浏览器
8.浏览器以R为密钥使用之前约定好的解密算法获取网页内容

四、数据结构

1.请你来说一说红黑树和AVL树的定义,特点,以及二者区别

平衡二叉树(AVL树):

平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。一句话表述为:以树中所有结点为根的树的左右子树高度之差的绝对值不超过1。将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

注意AVL树双旋的平衡因子的更新???
右左双旋:pSubRL == 1 ==> Parent->bf = -1;pSubRL == -1 ==> SubR = 1
左右双旋:pSubLR==1 ==> SubL->bf =-1;pSubLR ==-1==>Parent->bf=1 

红黑树:
红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。

性质:

  1. 红黑树性质1:根节点是黑色
  2. 红黑树性质2:如果一个节点为红色,那么其孩子节点都是黑色,即红黑树当中不可能出现两个连续的红色节点
  3. 红黑树性质3:从一个节点开始,到所有叶子节点的路径上黑色节点到个数是相等的,即每条路径中黑色节点的个数是相同的
  4. 红黑树性质4:每个节点不是黑色就是红色
  5. 红黑树性质5:每个叶子节点的两个左右空指针都是黑色的点;

红黑树保证最长路径中节点的个数不会超过最短路径中节点的2倍(最坏情况下才是两倍关系,路径要么全黑要么一红一黑)

红黑树的默认颜色红色黑色都可以,因为如果默认颜色为黑色,那么在插完一个节点后,在插其孩子节点就一定会破坏红黑树的性质(性质3),说明就需要调整,而如果默认颜色为红色,也会破坏红黑树的性质(性质2),所以红色或黑色都可以。我采用默认颜色为红色

红黑树的旋转规则(p为g的左孩子)??

规定cur为当前节点 p为cur的父亲节点 g为cur的祖父节点 u为cur的叔叔节点。
a.第一种情况:cur为红,p为红,g为黑,u存在且为红
将p和u变为黑色,g变为红色,再将cur放到g节点向上调整

b.第二种情况:cur为红,p为红,g为黑,u存在为黑或不存在(cur在p的左侧)
将p改为黑,g改为红,以g节点进行右单旋

c.第三种情况:cur为红,p为红,g为黑,u存在为黑或不存在(cur在p的右侧)
以p节点进行左单旋,转变为情况2

p为g的右孩子刚好相反。

区别:
AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。

2、平衡二叉树(AVL树):

红黑树是在AVL树的基础上提出来的。

平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。

AVL树中所有结点为根的树的左右子树高度之差的绝对值不超过1。
将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

红黑树较AVL树的优点:
AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。
所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括map底层实现都是使用的红黑树。

3.请你介绍一下B+树

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

4.请你说一说Top(K)问题

  • 1、直接全部排序(只适用于内存够的情况)
    当数据量较小的情况下,内存中可以容纳所有数据。则最简单也是最容易想到的方法是将数据全部排序,然后取排序后的数据中的前K个。
    这种方法对数据量比较敏感,当数据量较大的情况下,内存不能完全容纳全部数据,这种方法便不适应了。即使内存能够满足要求,该方法将全部数据都排序了,而题目只要求找出top K个数据,所以该方法并不十分高效,不建议使用。
  • 2、快速排序的变形 (只使用于内存够的情况)
    这是一个基于快速排序的变形,因为第一种方法中说到将所有元素都排序并不十分高效,只需要找出前K个最大的就行。
    这种方法类似于快速排序,首先选择一个划分元,将比这个划分元大的元素放到它的前面,比划分元小的元素放到它的后面,此时完成了一趟排序。如果此时这个划分元的序号index刚好等于K,那么这个划分元以及它左边的数,刚好就是前K个最大的元素;如果index > K,那么前K大的数据在index的左边,那么就继续递归的从index-1个数中进行一趟排序;如果index < K,那么再从划分元的右边继续进行排序,直到找到序号index刚好等于K为止。再将前K个数进行排序后,返回Top K个元素。这种方法就避免了对除了Top K个元素以外的数据进行排序所带来的不必要的开销。
  • 3、最小堆法

这是一种局部淘汰法。先读取前K个数,建立一个最小堆。然后将剩余的所有数字依次与最小堆的堆顶进行比较,如果小于或等于堆顶数据,则继续比较下一个;否则,删除堆顶元素,并将新数据插入堆中,重新调整最小堆。当遍历完全部数据后,最小堆中的数据即为最大的K个数。

  • 4、分治法
    将全部数据分成N份,前提是每份的数据都可以读到内存中进行处理,找到每份数据中最大的K个数。此时剩下NK个数据,如果内存不能容纳NK个数据,则再继续分治处理,分成M份,找出每份数据中最大的K个数,如果M*K个数仍然不能读到内存中,则继续分治处理。直到剩余的数可以读入内存中,那么可以对这些数使用快速排序的变形或者归并排序进行处理。
  • 5、Hash法
    如果这些数据中有很多重复的数据,可以先通过hash法,把重复的数去掉。这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间。处理后的数据如果能够读入内存,则可以直接排序;否则可以使用分治法或者最小堆法来处理数据。

5.请问加密方法都有哪些

1、单向加密
单向加密又称为不可逆加密算法,其密钥是由加密散列函数生成的。单向散列函数一般用于产生消息摘要,密钥加密等,常见的有:

MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文;
SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值。其变种由SHA192,SHA256,SHA384等;
CRC-32,主要用于提供校验功能;
算法特征:

  • 输入一样,输出必然相同;
  • 雪崩效应,输入的微小改变,将会引起结果的巨大变化;
  • 定长输出,无论原始数据多大,结果大小都是相同的;
  • 不可逆,无法根据特征码还原原来的数据;

2、对称加密
采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。

特点:

  • 1、加密方和解密方使用同一个密钥;
  • 2、加密解密的速度比较快,适合数据比较长时的使用;
  • 3、密钥传输的过程不安全,且容易被破解,密钥管理也比较麻烦;

优点:对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。

缺点:对称加密算法的缺点是在数据传送前,发送方和接收方必须商定好秘钥,然后使双方都能保存好秘钥。其次如果一方的秘钥被泄露,那么加密信息也就不安全了。另外,每对用户每次使用对称加密算法时,都需要使用其他人不知道的唯一秘钥,这会使得收、发双方所拥有的钥匙数量巨大,密钥管理成为双方的负担。

3、非对称加密
非对称密钥加密也称为公钥加密,由一对公钥和私钥组成。公钥是从私钥提取出来的。可以用公钥加密,再用私钥解密,这种情形一般用于公钥加密,当然也可以用私钥加密,用公钥解密。常用于数字签名,因此非对称加密的主要功能就是加密和数字签名。
特征:

  • 1)秘钥对,公钥(public key)和私钥(secret key)
  • 2)主要功能:加密和签名
    发送方用对方的公钥加密,可以保证数据的机密性(公钥加密)。
    发送方用自己的私钥加密,可以实现身份验证(数字签名)。
  • 3)公钥加密算法很少用来加密数据,速度太慢,通常用来实现身份验证。

常用的非对称加密算法
RSA:由 RSA公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的;既可以实现加密,又可以实现签名。
DSA(Digital Signature Algorithm):数字签名算法,是一种标准的 DSS(数字签名标准)。
ECC(Elliptic Curves Cryptography):椭圆曲线密码编码。

6.请你说一说洗牌算法

1、Fisher-Yates Shuffle算法
最早提出这个洗牌方法的是 Ronald A. Fisher 和 Frank Yates,即 Fisher–Yates Shuffle,其基本思想就是从原始数组中随机取一个之前没取过的数字到新的数组中,具体如下:

  • 1)初始化原始数组和新数组,原始数组长度为n(已知)。
  • 2)从还没处理的数组(假如还剩k个)中,随机产生一个[0, k)之间的数字p(假设数组从0开始)。
  • 3)从剩下的k个数中把第p个数取出。
  • 4)重复步骤2和3直到数字全部取完。
  • 5)从步骤3取出的数字序列便是一个打乱了的数列。
    时间复杂度为O(n*n),空间复杂度为O(n)。

2)Knuth-Durstenfeld Shuffle
Knuth 和 Durstenfeld 在Fisher 等人的基础上对算法进行了改进,在原始数组上对数字进行交互,省去了额外O(n)的空间。该算法的基本思想和 Fisher 类似,每次从未处理的数据中随机取出一个数字,然后把该数字放在数组的尾部,即数组尾部存放的是已经处理过的数字。
算法步骤为:

  • 建立一个数组大小为 n 的数组 arr,分别存放 1 到 n 的数值;
  • 2. 生成一个从 0 到 n - 1 的随机数 x;
  • 3. 输出 arr 下标为 x 的数值,即为第一个随机数;
  • 4. 将 arr 的尾元素和下标为 x 的元素互换;
  • 5. 同2,生成一个从 0 到 n - 2 的随机数 x;
  • 6. 输出 arr 下标为 x 的数值,为第二个随机数;
  • 7. 将 arr 的倒数第二个元素和下标为 x 的元素互换;
    ……
    如上,直到输出m 个数为止
    时间复杂度为O(n),空间复杂度为O(1),缺点必须知道数组长度n。
// 得到一个在闭区间 [min, max] 内的随机整数
int randInt(int min, int max);
​
// 第一种写法
void shuffle(int[] arr) {
    int n = arr.length();
    /******** 区别只有这两行 ********/
    for (int i = 0 ; i < n; i++) {
        // 从 i 到最后随机选一个元素
        int rand = randInt(i, n - 1);
        /*************************/
        swap(arr[i], arr[rand]);
    }
}
​
// 第二种写法
    for (int i = 0 ; i < n - 1; i++)
        int rand = randInt(i, n - 1);
​
// 第三种写法
    for (int i = n - 1 ; i >= 0; i--)
        int rand = randInt(0, i);
​
// 第四种写法
    for (int i = n - 1 ; i > 0; i--)
        int rand = randInt(0, i);

7.哈希函数的设计方法???

a.直接定制法,简单方便,分布均匀,缺点是需要事先知道关键知道分布情况;Hash(key) = A * key + B;
b.除留余数法:设哈希地址数位n,则取一个小于等于n的素数作为除数
c.平方取中法:适合不知道关键址的情况
d.折叠法:将关键字分割成位数相等的几部分,将这几部分叠加求和
e.随机数法:
f.数学分析法

8.哈希冲突的解决方法:开散列和闭散列

闭散列

  • 概念:如果发生哈希冲突并且哈希表并没有存满,就直接找下一个空位置,放元素
  • 找下一个空位置的方式:线性探测(从当前位置开始依次往后探测)和二次探测(避免线性探测造成元素堆积问题,跳着找空位置)
  • 哈希表当中的元素是不可以随意删除的,如果直接删除可能会影响下一个元素的查找(4和44),需要使用标记来进行标志
  • 哈希表的扩容时机为哈希表的载荷因子大于等于0.7
  • 哈希表的扩容不可以使用vector的默认扩容方式来进行,因为vector的扩容方式为,申请新空间,拷贝元素,释放旧空间,而如果直接使用vector的扩容方式,那么下次就无法通过哈希函数来查找元素(capacity已经发生改变),所以只能一个一个的插入,重新计算哈希地址

开散列(链地址法)

  • 概念:根据关键码计算哈希地址,具有相同哈希地址的key归于同一个集合(桶)。每个桶当中的元素通过链表链接起来的。所以一旦发生哈希冲突,就会把发生哈希冲突的元素放到对应桶的链表的头部
  • 开散列的扩容,极端情况下会出现一个桶当中的链特别长,影响哈希表的性能,最好的情况下是每个桶当中放一个元素,此时再去插入元素,每次都会发生冲突,所以扩容时机是当元素个数刚好等于桶当个数时就可以进行扩容

9. 二叉树的非递归遍历

非递归遍历

10.十种排序

排序

排序2

11.B树

B树详解

12.B+树

B+树和B*树详解

13.LRU

LRU

14.UnionFind

并查集

15.LFU

LFU详解:https://blog.csdn.net/wolfGuiDao/article/details/107801398

五、数据库

1. 请你聊一聊数据库事物的一致性

事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。事务是DBMS中最基础的单位,事务不可分割。事务具有4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID

  • 1)原子性(Atomicity)

原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,[删删删]因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

  • 2)一致性(Consistency)

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态

拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

  • 3)隔离性(Isolation)

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离

即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。这指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。

  • 4)持久性(Durability)

持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作

例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

不同的隔离级别:

  • Read Uncommitted(读取未提交):最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。
  • Read Committed(读取提交内容)只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。
  • Repeated Read(可重复读):在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。
  • Serialization(可串行化)事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题

2. 请你说说索引是什么,多加索引一定会好吗

1、索引

数据库索引是为了增加查询速度而对表字段附加的一种标识,是对数据库表中一列或多列的值进行排序的一种结构。
DB在执行一条Sql语句的时候,默认的方式是根据搜索条件进行全表扫描,遇到匹配条件的就加入搜索结果集合。如果我们对某一字段增加索引,查询时就会先去索引列表中一次定位到特定值的行数,大大减少遍历匹配的行数,所以能明显增加查询的速度。

优点:

  • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  • 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
  • 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
  • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

缺点:

  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
  • 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
  • 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。

2、添加索引原则

  • 在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
  • 只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。定义为text、image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。
  • 当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。

3.请你说一下MySQL引擎和区别

1、MySQL引擎
MySQL中的数据用各种不同的技术存储在文件(或者内存)中。这些技术中的每一种技术都使用不同的存储机制、索引技巧、锁定水平并且最终提供广泛的不同的功能和能力。通过选择不同的技术,你能够获得额外的速度或者功能,从而改善你的应用的整体功能。

数据库引擎是用于存储、处理和保护数据的核心服务。利用数据库引擎可控制访问权限并快速处理事务,从而满足企业内大多数需要处理大量数据的应用程序的要求。使用数据库引擎创建用于联机事务处理或联机分析处理数据的关系数据库。这包括创建用于存储数据的表和用于查看、管理和保护数据安全的数据库对象(如索引、视图和存储过程)。

MySQL存储引擎主要有: MyIsam、InnoDB、Memory、Blackhole、CSV、Performance_Schema、Archive、Federated、Mrg_Myisam。
但是最常用的是InnoDB和Mylsam。

2、InnoDB

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

适用场景

  • 经常更新的表,适合处理多重并发的更新请求。
  • 支持事务。
  • 可以从灾难中恢复(通过bin-log日志等)。
  • 外键约束。只有他支持外键。
  • 支持自动增加列属性auto_increment。

索引结构

  • InnoDB也是B+Treee索引结构。
  • Innodb的索引文件本身就是数据文件,即B+Tree的数据域存储的就是实际的数据,这种索引就是聚集索引。这个索引的key就是数据表的主键,因此InnoDB表数据文件本身就是主索引。
  • InnoDB的辅助索引数据域存储的也是相应记录主键的值而不是地址,所以当以辅助索引查找时,会先根据辅助索引找到主键,再根据主键索引找到实际的数据。所以Innodb不建议使用过长的主键,否则会使辅助索引变得过大。建议使用自增的字段作为主键,这样B+Tree的每一个结点都会被顺序的填满,而不会频繁的分裂调整,会有效的提升插入数据的效率。

3、Mylsam
MyIASM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT或UPDATE数据时即写操作需要锁定整个表,效率便会低一些。MyIsam 存储引擎独立于操作系统,也就是可以在windows上使用,也可以比较简单的将数据转移到linux操作系统上去。

适用场景:

  • 不支持事务的设计,但是并不代表着有事务操作的项目不能用MyIsam存储引擎,可以在service层进行根据自己的业务需求进行相应的控制。
  • 不支持外键的表设计。
  • 查询速度很快,如果数据库insert和update的操作比较多的话比较适用。
  • 整天对表进行加锁的场景。
  • MyISAM极度强调快速读取操作。
  • MyIASM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyIASM也是很好的选择。

缺点:就是不能在表损坏后主动恢复数据。

索引结构:

  • MyISAM索引结构:MyISAM索引用的B+ tree来储存数据,MyISAM索引的指针指向的是键值的地址,地址存储的是数据。
  • B+Tree的数据域存储的内容为实际数据的地址,也就是说它的索引和实际的数据是分开的,只不过是用索引指向了实际的数据,这种索引就是所谓的非聚集索引

3、InnoDB和Mylsam的区别:

  • 1)事务:MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持,提供事务支持已经外部键等高级数据库功能。

  • 2)性能:MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快。

  • 3)行数保存:InnoDB 中不保存表的具体行数,也就是说,执行select count() fromtable时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含where条件时,两种表的操作是一样的。

  • 4)索引存储:对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。MyISAM支持全文索引(FULLTEXT)、压缩索引,InnoDB不支持。

  • MyISAM的索引和数据是分开的,并且索引是有压缩的,内存使用率就对应提高了不少。能加载更多索引,而Innodb是索引和数据是紧密捆绑的,没有使用压缩从而会造成Innodb比MyISAM体积庞大不小

  • InnoDB存储引擎被完全与MySQL服务器整合,InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB存储它的表&索引在一个表空间中,表空间可以包含数个文件(或原始磁盘分区)。这与MyISAM表不同,比如在MyISAM表中每个表被存在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上。

  • 5)服务器数据备份:InnoDB必须导出SQL来备份,LOAD TABLE FROM
    MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

  • MyISAM应对错误编码导致的数据恢复速度快。MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。InnoDB是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了。

  • 6)锁的支持:MyISAM只支持表锁。InnoDB支持表锁、行锁,大幅度提高了多用户并发操作的新能。但是InnoDB的行锁,只是在WHERE的主键是有效的,非主键的WHERE都会锁全表的。

3.请你回答一下mongodb和redis的区别

  • 内存管理机制上:Redis 数据全部存在内存,定期写入磁盘,当内存不够时,可以选择指定的 LRU 算法删除数据。MongoDB数据存在内存,由 linux系统 mmap 实现,当内存不够时,只将热点数据放入内存,其他数据存在磁盘。
  • 支持的数据结构上:Redis 支持的数据结构丰富,包括hash、set、list等。
    MongoDB 数据结构比较单一,但是支持丰富的数据表达,索引,最类似关系型数据库,支持的查询语言非常丰富

4.请你说一下mysql引擎以及其区别

  • 在Mysql数据库中,常用的引擎为Innodb和MyIASM
  • 其中Innodb是一个事务型的存储引擎,有行级锁定和外键约束,提供了对数据库ACID事物的支持,实现了SQL标准的四种隔离级别,即读未提交,读已提交,可重复读以及串行,其涉及目标就是处理大数据容量的数据库系统。
  • 而MyIASM引擎是Mysql默认的引擎,不提供数据库事务的支持,也不支持行级锁和外键,因此当写操作时需要锁定整个表,效率较低。不过其保存了表的行数,当金星select count(*)form table时,可直接读取已经保存的值,不需要进行全表扫描。因此当表的读操作远多于写操作,并且不需要事务支持时,可以优先选择MyIASM

5.请你来说一说Redis的定时机制怎么实现的

Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:文件事件(服务器对套接字操作的抽象)和时间事件(服务器对定时操作的抽象)。Redis的定时机制就是借助时间事件实现的

一个时间事件主要由以下三个属性组成:id:时间事件标识号;when:记录时间事件的到达时间;timeProc:时间事件处理器当时间事件到达时,服务器就会调用相应的处理器来处理时间。一个时间事件根据时间事件处理器的返回值来判断是定时事件还是周期性事件

6.请你来说一说Redis是单线程的,但是为什么这么高效呢?

虽然Redis文件事件处理器以单线程方式运行,但是通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程运行的模块进行对接,这保持了Redis内部单线程设计的简单性。

7.请问Redis的数据类型有哪些,底层怎么实现?

1)字符串:整数值、embstr编码的简单动态字符串、简单动态字符串(SDS)
2)列表:压缩列表、双端链表
3)哈希:压缩列表、字典
4)集合:整数集合、字典
5)有序集合:压缩列表、跳跃表和字典

8.请问Redis的rehash怎么做的,为什么要渐进rehash,渐进rehash又是怎么实现的?

因为redis是单线程,当K很多时,如果一次性将键值对全部rehash,庞大的计算量会影响服务器性能,甚至可能会导致服务器在一段时间内停止服务。不可能一步完成整个rehash操作,所以redis是分多次、渐进式的rehash。渐进性哈希分为两种:

1)操作redis时,额外做一步rehash
对redis做读取、插入、删除等操作时,会把位于table[dict->rehashidx]位置的链表移动到新的dictht中,然后把rehashidx做加一操作,移动到后面一个槽位。

2)后台定时任务调用rehash
后台定时任务rehash调用链,同时可以通过server.hz控制rehash调用频率

9.请你来说一下Redis和memcached的区别

1)数据类型 :redis数据类型丰富,支持set liset等类型;memcache支持简单数据类型,需要客户端自己处理复杂对象
2)持久性:redis支持数据落地持久化存储;memcache不支持数据持久存储。)
3)分布式存储:redis支持master-slave复制模式;memcache可以使用一致性hash做分布式。
4)value大小不同:memcache是一个内存缓存,key的长度小于250字符,单个item存储要小于1M,不适合虚拟机使用
5)数据一致性不同:redis使用的是单线程模型,保证了数据按顺序提交;memcache需要使用cas保证数据一致性。CAS(Check and Set)是一个确保并发一致性的机制,属于“乐观锁”范畴;原理很简单:拿版本号,操作,对比版本号,如果一致就操作,不一致就放弃任何操作
6)cpu利用:redis单线程模型只能使用一个cpu,可以开启多个redis进程

六、设计模式

1.请问你用过哪些设计模式,介绍一下单例模式的多线程安全问题

常见的设计模式如下:

  • 单例模式:单例模式主要解决一个全局使用的类频繁的创建和销毁的问题。单例模式下可以确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。单例模式有三个要素:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
  • 工厂模式:工厂模式主要解决接口选择的问题。该模式下定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,使其创建过程延迟到子类进行。
  • 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

单例模式的多线程安全问题

在单例模式的实现中,如果不采取任何措施,在多线程下是不安全的,可能会同时创建多个实例。因此,为了保证单例模式在多线程下的线程安全,一般采用下面几种方式实现单例模式:

1)饿汉式:基于class loader机制避免多线程的同步问题,不过,instance在类装载时就实例化,可能会产生垃圾对象。

2)懒汉式:通过双重锁机制实现线程安全。

懒汉式:
class singleton   //实现单例模式的类  
  {  
  private:  
      singleton(){}  //私有的构造函数  
      static singleton* Instance;  
        
  public:  
      static singleton* GetInstance()  
      {  
          if (Instance == NULL) //判断是否第一次调用  
          {   
              Lock(); //表示上锁的函数  
              if (Instance == NULL)  
              {  
                  Instance = new singleton();  
              }  
              UnLock() //解锁函数  
          }             
          return Instance;  
      }  
  };  

饿汉式:
class singleton   //实现单例模式的类  
  {  
  private:  
      singleton() {}  //私有的构造函数  
      static singleton Instance;  
  
  public:  
      static singleton* GetInstance()  
      {  
                    return &Instance;  
      }  
  }; 

2.请你说一说OOP的设计模式的五项原则

  • 1、单一职责原则

单一职责有2个含义,一个是避免相同的职责分散到不同的类中,另一个是避免一个类承担太多职责。减少类的耦合,提高类的复用性。

  • 2、接口隔离原则

表明客户端不应该被强迫实现一些他们不会使用的接口,应该把胖接口中额方法分组,然后用多个接口代替它,每个接口服务于一个子模块。简单说,就是使用多个专门的接口比使用单个接口好很多。
该原则观点如下:
1)一个类对另外一个类的依赖性应当是建立在最小的接口上
2)客户端程序不应该依赖它不需要的接口方法。

  • 3、开放-封闭原则

open模块的行为必须是开放的、支持扩展的,而不是僵化的。
closed在对模块的功能进行扩展时,不应该影响或大规模影响已有的程序模块。一句话概括:一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。
核心思想就是对抽象编程,而不对具体编程。

  • 4、替换原则

子类型必须能够替换掉他们的父类型、并出现在父类能够出现的任何地方。
主要针对继承的设计原则
1)父类的方法都要在子类中实现或者重写,并且派生类只实现其抽象类中生命的方法,而不应当给出多余的,方法定义或实现。
2)在客户端程序中只应该使用父类对象而不应当直接使用子类对象,这样可以实现运行期间绑定。

  • 5、依赖倒置原则

上层模块不应该依赖于下层模块,他们共同依赖于一个抽象,即:父类不能依赖子类,他们都要依赖抽象类。抽象不能依赖于具体,具体应该要依赖于抽象

七、算法

算法模块

其中有各种例题及讲解

八、项目

项目模块

1

评论区