计算机基础面经积累---持续更新

编程知识 更新时间:2023-05-03 03:13:07

1、gcc,g++,gdb常用命令
首先了解gcc,g++的区别。要先知道我们写的源代码是如何被编译器运行的。大概有四个阶段:
预处理:处理宏定义等宏命令,删除空格等,生成后缀为“.i”的文件  
编译:将预处理后的文件转换成汇编语言,生成后缀为“.s”的文件
汇编:由汇编生成的文件翻译为二进制目标文,生成后缀为“.o”的文件
连接:多个目标文件(二进制)结合库函数等综合成的能直接独立执行的执行文件——生成后缀为“.out”的文件(exe文件)。
gcc无法进行库文件的连接;而g++则能完整编译出可执行文件。前三个阶段g++也是调用gcc实现的。
简单来说gcc是C语言编译器,g++是C++语言编译器。但是事实上,二者都可以编译c或cpp文件。区别在于,对于 .c和.cpp文件,gcc分别当做c和cpp文件编译,g++则统一当做cpp文件编译。
gdb调试是一个功能强大的命令调试程序,也就是一个debug的工具。
安装gdb: sudo apt install gdb
生成可执行代码:gcc test.c -g -o test或者g++ -g test.cpp -o test(注意添加-g参数,才可以调试)
进入gdb调试:gdb ./test

**list:**查看源代码,默认显示10行,可以修改。后面可以加文件名:行号,显示指定文件的以那一行为中心的附近的代码。这个很重要,因为我们需要观察源代码来了解程序逻辑,知道在哪里设置断点等。

插入断点:b 行号(或者函数入口处) 显示断点:info break 删除断点:delete 断点号
知道如何使用断点很重要,这样我们才知道程序中变量的值是如何变化的。
watch 可以用来监视一个变量或者一段内存,当这个变量或者该内存处的值发生变化时,GDB 就会中断下来。被监视的某个变量或者某个内存地址会产生一个 watch point(观察点)。这用于我们想要观察一个变量是否改变,如果一句句地调试太慢了。
关于调试还有**setp[n]和next[n]**两个但不调试命令,二者都代表每隔n行就自动断点,但是前者遇到函数会进入函数体执行,后者则会把函数执行完出来,整个函数体当成是一行。
知道了断点之后,我们可以用run命令运行程序,在断点处会中断,我们进行查看相应的变量的操作,再用continue或者go继续向下执行。

接下来很重要的一部分就是如何查看变量的值、地址、寄存器的值等操作。
查看变量的值**:print/p**. p 变量名,就可以查看变量的值。p/a按十六进制显示,p/c按字符显示等。也可以用display来显示变量的值,display在每次断点都会自动显示,不需要每次都p了,更方便。

查看内存地址的值:x/<n/f/u> n代表往后显示几个地址的内容,f代表显示的格式,比如十六进制十进制浮点数等,如果址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。u代表指定的字节作为一个值,默认是4个字节。

查看寄存器的值**:info registers**查看所有寄存器的情况。也可以使用print命令来访问寄存器的情况,只需要在寄存器名字前加一个$符号就可以了。如:p $eip。寄存器中可能存放了下一条指令的地址、函数返回地址、堆栈地址等,很重要。

**set args [arguments]:**重新指定被调试程序的命令行参数。show args显示被调试程序的命令行参数。这对于程序参数的相关调试有用。
GDB调试多线程看面试场景题积累汇总那一篇文章。

5、死锁问题
死锁就是,两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。比如很多进程需要以独占方式占用资源,这样进程间互相等待,无限期陷入僵持。
原因有:资源不足、资源分配不当、进程执行顺序不当等。
死锁的四个必要条件,即只有这四个条件成立,才可能发生死锁,不是肯定的。
互斥:一个资源只能同时被一个进程使用;
占有等待:进程资源得不到满足等待时,不释放已有的资源
不可剥夺:进程资源不能被别的进程抢占;
循环等待:每个进程都在等待链中等待下一个进程所持有的资源

死锁解决办法:
死锁防止:破坏四个必要条件之一即可。比如采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行。实现简单,但是严重的减低了资源利用率。剥夺调度能够防止死锁,但是只适用于内存和处理器资源。给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
总结,死锁防止方法能够防止发生死锁,但必然会降低系统并发性,导致低效的资源利用率。

死锁避免:典型的就是银行家算法。本质就是,每次分配的时候,保证系统处于安全状态,如果这次分配导致不安全状态,则不分配。安全状态就是可以找到一个执行序列,满足每个进程对资源的最大需求,顺利完成执行序列。
银行家算法就是有最大需求矩阵,已分配矩阵,还需要矩阵,可用资源矩阵。分配资源时,若分配大于所需要或分配大于可用资源,不分配;若分配后导致进入不安全状态,不分配。

死锁检测:当且仅当资源分配图不可完全简化,就是死锁。
死锁解除:抢占资源、终止进程。

7、进程间通信方式
1.管道:
匿名管道:两个特点:信息单向传输,只能在父子兄弟进程之间使用(因为没有显式的管道文件,只能fork复制父进程的fd)。
例子:ps auxf | grep mysql 这个就是打印出所有进程信息,通过管道送入右边,再获取其中mysql进程。
命名管道FIFO:可以在不相关的进程间传递信息。使用方法:先需要通过mkfifo命令来创建,并且指定管道名字:mkfifo myPipe
echo “hello” > myPipe // 将数据写进管道 cat < myPipe // 读取管道里的数据。可以看出,管道这种通信方式效率低, 不适合进程间频繁地交换数据。当然,它的好处就是简单。

2、消息队列:
A进程要给B进程发送消息,A进程把数据放在对应的消息队列后就可以正常返回了,B进程需要的时候再去读取数据就可以了。效率更高。
消息队列是内核中消息链表,发送消息是消息体,固定大小的存储块,所以克服了字节流效率低的特点。消息队列生命周期内核持续,不是随进程的。
缺点:消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理 另一进程 读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程

3、共享内存
消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去, 大大提高了进程间通信的速度。
缺点是:多人写的话,会产生覆盖问题,读没问题。

4、信号量(按理说不严格属于进程通信)
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。控制信号量的方式有两种原子操作:一个是P操作,这个操作会把信号量减去1,另一个是V操作,这个操作会把信号量加上1。P操作是用在进入共享资源之前,V操作是用在离开共享资源之后,这两个操作是必须成对出现的。初始为0是同步信号量,初始为1是互斥信号量。

5、信号
信号一般用于一些异常情况下的进程间通信,是一种异步通信,它的数据结构一般就是一个数字。信号来源主要是键盘或者命令(比如kill)。crtrl+C就是产生 SIGINT 信号,表示终止该进程;资源管理器结束进程;kill -9 1050,表示给PID为1050的进程发送SIGKILL 信号,用来立即结束该进程

6、socket
socket就是常见的网络编程。跨网络与不同主机上的进程之间通信,就需要Socket通信。

8、程序、进程、线程、协程
进程是程序在某个数据集合上的一次运行活动,也是操作系统进行资源分配和保护的基本单位
所以程序是静态的,进程是动态的,有生命周期的。
进程的组成:PCB(进程描述符,进程控制管理信息比如阻塞挂起等,资源分配信息,CPU寄存器相关信息)、数据段、程序段。
进程三种基本状态:阻塞(等待某一事件,比如IO完成)、运行、就绪(缺少CPU,比如CPU时间用完)

线程:一个进程可以有很多线程,是轻量级的进程,不同的线程共享进程的资源(内存,IO等),没有自己的地址空间,但有自己的堆栈和局部变量。线程是处理器调度的基本单位,而进程是资源分配的基本单位。
比如浏览器是一个进程,其中的HTTP 请求线程、事件响应线程、渲染线程等等,所以线程并发程度比进程高,同时由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,需要较大的时空开销,而线程相对开销小从属于不同进程的线程间切换,它是会导致进程切换的,所以开销也大

缺点:一个线程崩溃会影响其他线程,所以健壮性不够

协程:协程是一个用户态的轻量级线程。线程是同步的,协程是异步的。
进程线程的痛点是涉及到线程阻塞状态和可运行状态之间的切换,同步锁,上下文切换。比如JDBC,数据库是最大性能瓶颈,因为是同步阻塞的,线程占用的CPU一直在空转。
协程切换:当出现IO阻塞的时候,协程调度切换时,将数据流立刻yield掉(主动让出),将寄存器上下文和栈保存到其它地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,且其他协程可以继续运行,不会使整个线程阻塞住;
第二是可以不加锁的访问全局变量,所以上下文的切换非常快,因为协程都是属于一个线程的,不存在同时写变量冲突。

临界资源:一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机等。访问临界资源的那段代码称为临界区

9.智能指针问题
智能指针是一个,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。
使用new和delete运算符进行动态内存的管理虽然可以提高程序的效率,但是也非常容易出问题:忘记释放内存,造成内存泄漏,在尚有指针引用内存的情况下就将其释放,产生引用非法内存的指针,程序发生异常后进入catch忘记释放内存。智能指针是借用RAII技术对普通指针进行封装。RAII技术,也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。使用存储在栈上的局部对象(类)来封装资源的分配和初始化,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放。 局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
C++11版本之后提供的智能指针包含在头文件中,分别是auto_ptr、shared_ptr、unique_ptr、weak_ptr
智能指针代码实现: 用两个类来实现智能指针的功能,一个是引用计数类,另一个则是指针类。

//  引用计数器类  用于存储指向同一对象的指针数
template<typename T>
class Counter
{
private:
	//  数据成员
	T *ptr;    //  对象指针
	int cnt;   //  引用计数器
 
	//  友元类声明
	template<typename T>
	friend class SmartPtr;
 
	//  成员函数
	//  构造函数
	Counter(T *p)   //  p为指向动态分配对象的指针
	{
		ptr = p;
		cnt = 1;
	}
	//  析构函数
	~Counter()
	{
		delete ptr;
	}
};
//  智能指针类  
template<typename T>
class SmartPtr
{
private:
	//  数据成员
	Counter<T> *ptr_cnt;  //  
 
public:
 
	//  成员函数
	//  普通构造函数  初始化计数类
	SmartPtr(T *p)
	{
		ptr_cnt = new Counter<T>(p);
	}
	//  拷贝构造函数 计数器加1
	SmartPtr(const SmartPtr &other)
	{
		ptr_cnt = other.ptr_cnt;
		ptr_cnt->cnt++;
	}
	//  赋值运算符重载函数
	SmartPtr &operator=(const SmartPtr &rhs)
	{
		ptr_cnt = rhs->ptr_cnt;
		rhs.ptr_cnt->cnt++; 增加右操作数的计数器
		ptr_cnt->cnt--; 左操作数计数器减1
		if (ptr_cnt->cnt == 0)
			delete ptr_cnt;
		return *this;
	}
	//  解引用运算符重载函数
	T &operator*()
	{
		return *(ptr_cnt->cnt);
	}
 
	//  析构函数
	~SmartPtr()
	{
		ptr_cnt->cnt--;
		if (ptr_cnt->cnt == 0)
			delete ptr_cnt;
		else
			cout << "还有" << ptr_cnt->cnt << "个指针指向基础对象" << endl;
	}
};

shared_ptr:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。
shared_ptr初始化:


std::shared_ptr<T> sp; //空shared_ptr,可以指向类型为T的对象
 
std::shared_ptr<int> sp(new int(5)); //指定类型,传入指针通过构造函数初始化
 
std::shared_ptr<int> sp = std::make_shared<int>(5); //使用make_shared函数初始化
 
//智能指针是一个模板类,不能将一个原始指针直接赋值给一个智能指针,因为一个是类,一个是指针
std::shared_ptr<int> sp = new int(1); //error

unique_ptr unique_ptr唯一拥有其所指的对象,在同一时刻只能有一个unique_ptr指向给定对象。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。
初始化:没有make_shared函数,只能通过new传入指针。

std::unique_ptr<T> up; //空unique_ptr,可以指向类型为T的对象,up会使用delete来释放它的指针
 
std::unique_ptr<int> up(new int(5)); //绑定动态对象

nique_ptr没有copy构造函数,不支持普通的拷贝和赋值操作;但却提供了一种移动机制来将指针的所有权从一个unique_ptr转移给另一个unique_ptr(使用std::move函数,也可以调用release或reset)

std::unique_ptr<int> upMove = std::move(up); //转移所有权

std::unique_ptr<int> up1(new int(5));
std::unique_ptr<int> up2(up1.release()); //up2被初始化为up1原来保存的指针,且up1置为空
std::unique_ptr<int> up3(new int(6));
up2.reset(up3.release()); //reset释放了up2原来指向的内存,指向up3原来保存的指针,且将up3置为空

unique_ptr适用范围比较广泛,它可返回函数内动态申请资源的所有权;可在容器中保存指针;支持动态数组的管理。

weak_ptr是一种弱引用指针,它是伴随shared_ptr而来的,不具有普通指针的行为 ,模板类中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。。它主要是解决了shared_ptr引用计数的问题:在循环引用时会导致内存泄漏的问题。
weak_ptr指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
use_count() 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired() 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)
lock() 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针.
循环引用:A调用B,B调用A,这样初始化时A,B的计数为1,赋值时又加1,析构减1,最后还是1,资源没有释放。

智能指针线程安全吗?
结论:同一个shared_ptr被多线程读是安全的;同一个shared_ptr被多线程写不安全;共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。
原因,shared_ptr其实由指向对象的指针和计数器组成,计数器加减操作是原子操作,所以这部分是线程安全的,但是指向对象的指针不是线程安全的。比如智能指针的赋值拷贝,首先拷贝指向对象的指针,再使引用次数加减操作,虽然引用次数加减是原子操作,但是指针拷贝和引用次数两步操作 并不是原子操作,线程不安全,需要手动加锁解锁。

10. 二叉搜索树、平衡二叉树、红黑树、B树、B+树
二叉搜索树:为了使查找的平均时间复杂度为logn,性质是左子树节点小于根,右子树节点大于根,同时左右子树也是二叉搜索树,是一个递归定义。特点是中序遍历的话是递增序列。
但是,如果本身是一个递增序列,那么构造的二叉搜索树就退化成链表了,查找复杂度变成O(n)了,所以才有平衡二叉搜索树。
AVL的特点是左右子树的高度差不超过1,左右子树也满足。把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间(为了保持平衡,插入删除元素都要调整二叉树)。
因此,才有了红黑树。
红黑树一些特点:节点是红色或黑色,根节点是黑色;红色节点的孩子是黑色节点;从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
虽然这么构造深意我也不理解,但是是为了从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的,但不是AVL限制那么严格。这么做原因是换来了保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。AVL调整旋转次数就不可预计了。
红黑树虽然减少了树的高度,但是当数据量很大时,树高度还是很高,不适合io级别的操作,更适合内存级别的应用。比如STL中哈希表unordered_map的底层实现就是红黑树,可以实现查找插入删除的复杂度都是logn级别。

那么,适用于io操作的是B树和B+树,适用于数据库的索引
B树就是一个多路的二叉搜索树,B树每个节点有键、数据、指针三部分。每个节点有m-1个元素和2-m个孩子节点,这样就大大减小树的高度,从而减小io操作次数。
B+树是B树的变种,B+树的非叶子结点只保存指针和键,数据保存在叶子结点。B+树在叶子结点添加了指向相邻叶子结点的指针。B+树由于非叶子节点没有数据域,所以能够携带更多的键,所以B+树的层数少,看起来更矮胖一点。那么查询时,B+树所进行的I/O次数更少;由于B+树在叶子结点增加了指向相邻叶子结点的指针,当进行区间查询时,只要沿着指针读取就可以,天然具备排序功能。而B树的索引字段大小相邻近的结点可能隔得很远。

11.深拷贝和浅拷贝
浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化;深拷贝是将对象及值复制过来,指向两个独立的地方,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝。
进一步思考,在对对象进行浅拷贝时,如果两个对象都析构函数的话,就会导致多次析构造成崩溃。
同时,在传参时,值传递就是深拷贝,比如vector a作为形参,那么函数里的a变化,不会导致函数外面的a变化,就会发现你的a没变化,因为函数退出时,局部的a已经销毁了。而且深拷贝复制对象时还浪费时间空间,开销很大。如果传vector& a,就是浅拷贝了,只复制了地址。

13、计算机系统开机的过程
第一阶段:BIOS,基本输入输出系统,控制硬件的一段代码,存放在ROM中,无法更改的,断电不消失。其中主要包含了自检程序、系统自动装载程序、IO驱动程序中断服务等。
1.1 BIOS程序首先检查,计算机硬件能否满足运行的基本条件,这叫做**”硬件自检”;**
1.2 **启动顺序:BIOS知道下一阶段的启动程序”具体存放在哪一个设备

第二阶段:主引导记录
BIOS按照”启动顺序”,把控制权转交给排在第一位的储存设备(U盘,硬盘等),然后读取启动设备的主引导记录,存放在最前面的512字节。它主要包含分区表,它的主要作用是,告诉计算机到硬盘的哪一个位置去找操作系统

第三阶段:**硬盘启动,**计算机的控制权就要转交给硬盘的某个分区

第四阶段,操作系统
控制权转交给操作系统后,操作系统的内核首先被载入内存。以Linux系统为例,内核加载成功后,第一个运行的程序是/sbin/init。它根据配置文件产生init进程。这是Linux启动后的第一个进程,pid进程编号为1,其他进程都是它的后代。
然后,i
nit线程加载系统的各个模块
,比如窗口程序和网络程序,直至执行/bin/login程序,跳出登录界面,等待用户输入用户名和密码。

  1. Linux系统的目录结构及目录的主要功能
    我们可以用ls /命令来查看linux的目录。ls就是显示目录下的文件名。/就是根目录,显示根目录下的文件,目录也是文件。

bin: cd bin;ls就可以看到bin目录下的文件。普通用户可以使用的必须的命令的存放目录。比如ls,pwd等等。是二进制文件。
sbin:超级root用户可以使用的系统管理的必须的命令的存放目录。普通用户无法执行。普通用户用$标识,root用#标识。
boot: 引导程序,内核等存放的目录。
dev :是 Device 的缩写, 该目录下存放的是 Linux 的外部设备,在 Linux 中访问设备的方式和访问文件的方式是相同的。设备文件可以使用mknod命令来创建。想要Linux系统支持某个设备,只要:相应的硬件设备,支持硬件的驱动模块,以及相应的设备文件。
/home:用户的主目录,在 Linux 中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的,保存了用户自己的配置文件,定制文件,文档,数据等。
/lost+found:看名字就知道是系统崩溃非法关机等文恢复的位置。一般是空的。
/mnt:系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了。
/proc: 是 Processes(进程) 的缩写,/proc 是一种伪文件系统,存储的是当前内核运行状态的一系列特殊文件,这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息
这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令,使别人无法ping你的机器。
/tmp:tmp 是 temporary(临时) 的缩写这个目录是用来存放一些临时文件的。实际上是内存中的,当关机重启后tmp清空了。
/var: 是 variable(变量) 的缩写,这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件、缓冲文件等。
/etc: 是 Etcetera(等等) 的缩写,这个目录用来存放所有的系统管理所需要的配置文件。例如,要配置系统开机的时候启动那些程序,配置某个程序启动的时候显示什么样的风格等等。通常这些配置文件都集中存放在/etc目录中,所以想要配置什么东西的话,可以在/etc下面寻找我们可能需要修改的文件。
/lib:lib 是 Library(库) 的缩写这个目录里存放着系统最基本的动态连接共享库,其作用类似于 Windows 里的 DLL 文件(system32目录)。几乎所有的应用程序都需要用到这些共享库。按理说,这里存放的文件应该是/bin目录下程序所需要的库文件的存放地。
/media:linux 系统会自动识别一些设备,例如U盘、光驱等等,当识别后,Linux 会把识别的设备挂载到这个目录下。比如插入U盘会在里面建一个disk目录,就能通过disk来访问u盘。
/opt:opt 是 optional(可选) 的缩写,这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。
/usr这个目录中包含了命令库文件和在通常操作中不会修改的文件。这个目录对于系统来说也是一个非常重要的目录,其地位类似Windows上面的”Program Files”目录
/usr/local这个目录存放的内容,一般都是我们后来自己安装的软件的默认路径
/usr/bin一般存放的只是对用户和系统来说“不是必需的”程序(二进制文件)。
/usr/sbin一般存放用于系统管理的系统管理的不是必需的程序(二进制文件)。

16. XSS攻击是什么?如何避免?
XSS攻击全称跨站脚本攻击(前端注入),注入攻击的本质,是把用户输入的数据当做前端代码执行(比如SQL注入)
。XSS拼接的是网页的HTML代码,一般而言我们是可以拼接出合适的HTML代码去执行恶意的JS语句。在渲染DOM树的时候,执行了不可预期的JS脚本,从而发生了安全问题。(信息泄露、未授权操作、弹窗关不掉等)
常见的 XSS 攻击有三种:反射型XSS攻击、DOM-based 型XXS攻击以及存储型XSS攻击。
反射型 XSS 一般是攻击者通过特定手法(如电子邮件),诱使用户去访问一个包含恶意代码的 URL,当受害者点击这些专门设计的链接的时候,恶意代码会直接在受害者主机上的浏览器执行。反射型XSS通常出现在网站的搜索栏、用户登录口等地方,常用来窃取客户端 Cookies 或进行钓鱼欺骗
例子:张三做了一个超链接发给阿伟,超链接地址为:http://www.xxx?content= 当阿伟点击这个链接的时候(假设他已经登录xxx),浏览器就会直接打开bbb,并且把xxx中的cookie信息发送到bbb。而bbb就是张三的非法网站,此时他已经得到了cookie信息。

存储型XSS也叫持久型XSS,主要将XSS代码提交存储在服务器端(数据库,内存,文件系统等),下次请求目标页面时不用再提交XSS代码。当目标用户访问该页面获取数据时,XSS代码会从服务器解析之后加载出来,返回到浏览器做正常的HTML和JS解析执行,XSS攻击就发生了。存储型 XSS 一般出现在网站留言、评论、博客日志等交互处,恶意脚本存储到客户端或者服务端的数据库中。
例子:张三在网站发布了文章,其中包含恶意代码:。
这样,只要你打开文章,就会丢失cookie信息,危害更大。

基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,是纯粹发生前端的攻击。属于前端 JavaScript 自身的安全漏洞。常见于类似JSON转换、翻译等工具区。

防御:1.对用户向服务器提交的信息(URL、关键字、HTTP头、POST数据等)进行检查,仅接受规定长度、适当格式、预期内容,其余的一律过滤
2.对输入内容的特定字符进行编码,例如表示 html标记的 < > 等符号。
3. 对重要的 cookie设置 httpOnly, 防止客户端通过document.cookie读取 cookie.
4. 确定接收到的内容被规范化,仅包含最小最安全的tag(不含JavaScript),去掉对任何远程内容的引用(尤其是样式表和JavaScript)

17.Cmake怎么用?为什么要用?
为什么要CMAKE工具呢?
我们如果直接把可执行代码给别人运行,可能由于平台不一样无法运行,怎么办呢?就要用make工具,实际上make工具有很多种,而生成的makefile文件形式也千差万别,所以,CMAKE出现,允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件。所以编译流程是先写 CMakeList.txt ,再cmake 路径生成makefile文件,再make一下生成可执行文件、动态库静态库等文件。实现写一次就可以运行在任何系统上。
CMakeList.txt 主要内容有

cmake_minimum_required(VERSION 3.0)  //最小版本
project(sylar) //工程名字

set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_FLAGS "$ENV{CXXFLAGS} -rdynamic -O3 -fPIC -ggdb -std=c++11 -Wall -Wno-deprecated -Werror -Wno-unused-function -Wno-builtin-macro-redefined -Wno-deprecated-declarations") #C++标准。比如-fPIC代表生成动态库,-O3优化级别,-ggdb可以用gdb调试

set(LIB_SRC
      sylar/log.cc
     ) # 设置要编译的源文件

add_library(sylar SHARED ${LIB_SRC})  #编译成动态库 把log源文件编译成动态库,这样就可以给test的main函数用
#add_library(sylar_static STATIC ${LIB_SRC})
#SET_TARGET_PROPERTIES (sylar_static PROPERTIES OUTPUT_NAME "sylar")

add_executable(test tests/test.cc)  #编译成可执行文件,将test源文件编译成可执行文件
add_dependencies(test sylar)



SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) #设置可执行文件的存储路径,一般就在bin目录下
SET(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib) #设置动态库文件的存储路径,一般在lib文件下

总结一下,就是设置要编译的源文件、要生成的动态库静态库,可执行文件,设置生成文件的存储位置等。

18.堆和栈的区别
首先,从内存管理角度看。在C++中,内存分为几个区:内核区域、栈、堆、全局/静态存储区(初始化和未初始化)、常量存储区。堆和栈相向生长。一般堆空间大一点,栈空间小
堆的内存分配是程序员控制的(malloc,new),申请时,系统遍历一个记录空闲区域地址的链表,找到第一个符合条件的,所以分配不连续,会产生碎片;栈是系统自动分配的,是连续的。
栈一般存储返回地址、局部变量、参数等。堆的话程序员自己控制。
形象一点,栈就是饭店点菜,不用烧不用洗碗,高效方便但是自由度小;堆就是自己烧,麻烦效率低还有可能内存泄漏,但是自由度大。

19. 请你说说 C++11、C++14、C++17、C++20 都有什么新特性

C++11:
auto自动类型推倒(在以前的版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。)
auto 的一个典型应用场景是用来定义 stl 的迭代器。不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,书写起来很麻烦;auto 用于泛型编程,不希望指明具体类型的时候,比如泛型编程中。
还有一个decltype和auto很像,也是自动类型推倒。
decltype(exp) varname [= value] 括号代表可省略,区别是,它根据exp表达式来推倒类型,表达式可以是简单的也可以是函数等。因为auto必须初始化,但decltype不用。auto不能用于类的非静态成员变量(也是因为没初始化),但decltype可以。

final关键字,使得类不能被继承,函数不能被重写 override提示虚函数要重写
lambda表达式 匿名函数,简单地理解就是没有名称的函数
使用using定义别名(替代typedef)。typedef被重定义的类型并不是一个新的类型,仅仅只是原有的类型取了一个新的名字
tuple tuple 最大的特点是:实例化的对象可以存储任意数量、任意类型的数据。
C++11 long long超长整形
C++11 shared_ptr、unique_ptr、weak_ptr智能指针 具体看前面

C++11右值引用右值引用只不过是一种新的 C++ 语法,真正理解起来有难度的是基于右值引用引申出的 2 种 C++ 编程技巧,分别为移动语义和完美转发
在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。
1 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。举个例子:
int a = 5; 5 = a; //错误,5 不能为左值.
C++ 中的左值也可以当做右值使用,例如:
int b = 10; // b 是一个左值
a = b; // a、b 都是左值,只不过将 b 可以当做右值使用

2 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。
简单说,左值就是变量,右值就是字面常量。

引用,使用 “&” 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:
int num = 10;
int &b = num; //正确
int &c = 10; //错误
为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。
int && a = 10;
a = 100;
cout << a << endl; (但是这种常量右值引用是没有意义的,引用一个不可修改的常量,因为完全可以交给常量左值引用完成)
非常量右值引用才可以实现移动语义和完美转发
移动语义
解决的问题:当拷贝对象时,如果对象成员有指针的话,是深拷贝的方式(浅拷贝的话,多次析构的问题),深拷贝会将指针指向的内存资源一起拷贝一份,如果临时对象中的指针成员申请了大量的堆空间,那么效率很低。
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
实现方式:手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式(引用就是浅拷贝),同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。
我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。

完美转发:完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)
总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值(不然的话只看成左值);其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。
之前的版本,const 左值引用既可以接收左值,也可以接收右值,但考虑到其 const 属性,除非被调用函数的参数也是 const 属性,否则将无法直接传递。

std::future
我们想要从线程中返回异步任务结果(即任务A的执⾏必须依赖于任务B的返回值),一般需要依靠全局变量;从安全角度看,有些不妥;为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。
简单地说,std::future 可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。
std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)
Provider 可以是函数或者类
std::async 函数,本文后面会介绍 std::async() 函数。
std::promise::get_future,get_future 为 promise 类的成员函数
std::packaged_task::get_future,此时 get_future为 packaged_task 的成员函数
std::future 一般由 std::async, std::promise::get_future, std::packaged_task::get_future 创建,不过也提供了构造函数。td::future 的拷贝构造函数是被禁用的,只提供了默认的构造函数和 move 构造函数(移动构造函数,利用右值作为参数)
std::future::valid()
检查当前的 std::future 对象是否有效,即释放与某个共享状态相关联。一个有效的 std::future 对象只能通过 std::async(), std::future::get_future 或者 std::packaged_task::get_future 来初始化。另外由 std::future 默认构造函数创建的 std::future 对象是无效(invalid)的,当然通过 std::future 的 move 赋值后该 std::future 对象也可以变为 valid。
std::future::wait()
等待与当前std::future 对象相关联的共享状态的标志变为 ready.
如果共享状态的标志不是 ready(此时 Provider 没有在共享状态上设置值(或者异常)),调用该函数会被阻塞当前线程,直到共享状态的标志变为 ready。一旦共享状态的标志变为 ready,wait() 函数返回,当前线程被解除阻塞,但是 wait() 并不读取共享状态的值或者异常。(这就是和std::future::get()区别 )
std::future::wait_for() 可以设置一个时间段 rel_time
std::shared_future 与 std::future 类似,但是 std::shared_future 可以拷贝、多个 std::shared_future 可以共享某个共享状态的最终结果
参数是⼀个future,⽤这个future等待⼀个int型的产品:std::future& fut
⼦线程中使⽤get()⽅法等待⼀个未来的future,返回⼀个result。⽣产者使⽤async⽅法做⽣产⼯作并返回⼀个future,消费者使⽤future中的get()⽅法可以获取产品。

C++14,17 就不详细介绍了。 C++20多了协程。

20 计算机存储系统层次

介绍几种存储介质
ram,随机存取,特点是断电后数据消失。分为sram和dram,前者更快,用双稳态电路,造价高,后者电容存储电荷表示0,1,电容会放电,所以需要刷新。
rom,只读存储,断电不消失,做外存。后来各种升级,flash等。
寄存器最快,用的ram或者dff等,不太了解;
高速缓存基本用sram,主存一般用dram。高速缓存的存在是解决CPU速度和内存速度的速度差异问题。利用时空局部性。而磁盘缓存是减少内存访问IO的次数,提高效率。
先到一级缓存中找,找不到再到二级缓存中找,如果还找不到就只有到内存中找了,再不行再到辅存找。

21. 程序装入和链接几种方式
预处理,编译,链接,装入,运行。
绝对装入:只适合单道程序环境。编译后产生绝对的物理地址;
可重定位装入:程序的地址以0开始计算,根据内存起始地址,真实物理地址为内存起始地址加程序中的地址;
动态重定位:可重定位装入不允许程序运行时在内存中移动位置(有点像多态的意思),实际上程序运行时会移动,比如对换技术。所以这种装入内存时依旧是逻辑地址,真正执行时再转换成物理地址,需要重定位寄存器的支持。

静态链接:在装入之前就变成一个完整的模块,已经可以执行了。需要修改相对地址,外部调用符号改成相对地址。
装入时动态链接:外部模块是分开单独存储的。在装入时,发生一个外部模块的调用,才链接。优点是方便修改更新,不用重新打开模块。而且容易实现模块的共享,不然程序很大占空间。

运行时动态链接:执行时才链接,不仅加快装入过程,还节省内存空间。比如错误处理模块,不出错时就不进入内存。

22 内存分配方式
主要分为连续分配和离散分配
连续分配分为:单一连续,固定分区,动态分区,动态可重定位分区。缺点是产生碎片,紧凑时需要代价;
离散分配(虽然也会有内部碎片):分页,分段,段页结合(最好的)

动态分区分配有一个空闲分区表和空闲链表。分配算法有首次适应、最佳适应、哈希等。动态可重定位分区就是多了一个紧凑的功能,收拾碎片。

页面大小一般在1KB-8KB。太小导致一个进程占用页面太多,页表太大,占用内存,页面换进换出效率低。太大导致业内碎片正大。

这种地址为 :页号+页内偏移地址。
地址转换的过程:硬件完成。页表在内存中**,每个进程都有**,存的页号到块号的映射。页表寄存器存页表始址和页表长度。先判断页号是否超过页表长度。若没有,则利用页表寄存器,通过页表始址+页表项长度*页号找到页表项,得到块号,物理地址=块号+页内地址。
可见,CPU存取一个数据需要两次访问内存,第一次访问页表合成物理地址,第二次访问这个物理地址。比较慢。所以,出现了快表。相当于放在高速缓存中的页表,无则添加,有则直接利用快表给出块号。也是利用了程序的时空局部性。

由于现代计算机的逻辑地址空间很大(2的32到64次方),这样页表就很大,甚至可以到2MB。可以利用两级页表。这样就变成外层页号+外层页内地址+页内地址。其实是一种时间换空间的方法。

而段式管理不是为了内存利用率,而是为了方便用户编程。方便共享。信号保护等。

23 介绍虚拟内存
通过对换技术,允许部分程序放入内存就可执行程序,执行中,不需要的换出,需要的换入。过程对用户是透明的,所以用户感觉内存很大,虚拟内存一般受可寻址的大小和外存大小限制。虚拟内存可提高系统资源利用率,方便用户编程

由于利用局部性原理,所以CPU利用率和内存利用率都更高;
虚拟内存的页表结构

当访问一个不在内存的逻辑地址时,产生缺页中断/缺段中断;OS将阻塞该进程,启动磁盘I/O,如果内存有空闲,装入该页,没有空闲启用置换算法,装入所需的页/段后,将阻塞进程置为就绪态。
虚拟地址和物理地址转换过程前面已经说过了。

虚拟内存管理考虑的算法问题。
读取策略:某一页何时调入内存(请求调入/预调入)。预调入:考虑到启动磁盘的寻道和延迟开销,在调入所需页的同时,也调入若干可能马上访问的页面(通常是所需页的后面几页)
置换策略:
OPT最佳置换:淘汰那些永不再使用或者下次访问距当前时间最长的页面最佳,但不可实现,用于衡量其它置换算法性能。因为不知道未来的情况。
LRU:最近最久未使用算法,淘汰在最近一段时间内最近未使用的一页。以过去预测将来。实现时注意时间戳。
FIFO:先入先出算法。淘汰驻留内存时间最久的一页。Belady异常现象:通常,帧越多则缺页次数越少,但FIFO算法中,有时**,帧越多反而缺页率越高**
简单时钟算法 某进程的所有页面(或整个内存的页框)排成一循环缓冲链;某页被装入或访问时,其使用位U置1 置换时顺序查找循环链,U=0,置换该页,U=1,将U置0,替换指针前移,下次置换时从替换处开始寻找。

抖动:
当系统并发度 ( 多道程序度 ) 过高时,缺页频繁,用 于调页的时间比进程实际运行的时间还多, CPU 利用率急剧下降,此时发生了 抖动
解决:抖动时,挂起 一 些进程,释放它们的帧
预防:L=S 准则:调整并发度,使得 产生缺页的平均时间 ( 即缺页中断之间的平均时间 ) 等于处理 一 次缺页中断的平均时间 ,此时 CPU 利用率最高。

24 前置++和后置++区别和效率
最初始的就是,前置是先自增,再赋值;后置是先赋值再自增。更深入一点的区别是:
++a表示取a的地址,增加它的内容,然后把值放在寄存器中;
a++表示取a的地址,把它的值装入寄存器,然后增加内存中的a的值;
int a=0;
b=++a; //b=1
b=a++; //b=0;
再深入:
对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符.,理由是 前置自增 (++i) 通常要比后置自增 (i++) 效率更高
后置++编译器会生成一个临时空间进行存储,最后返回的也是临时空间的值,前置++不会生成临时对象,直接在原对象上++。

a++是线程安全的吗?
不是,从汇编角度看,他不是原子操作,因为要先找到a的地址,再把它装入寄存器,再把地址中的值加1.一共三个步骤,如果多线程的话,可能线程A还没来得及加1到内存,线程2就开始读,就是脏读。

int a=b是线程安全的吗?
也不是,因为对应两个语句,将b的值放入寄存器,再把寄存器的值放到a的内存。如果执行一半就被另一个线程占用CPU,就会出问题。

解决办法, 设置std::atomic类型。std::atomic value; value=99. 这样就可以避免在多线程编程时加锁,不但麻烦,而且效率还低。 atomic类型变量有一个特点,不可拷贝只能赋值。因为源码里拷贝构造函数delete了。(为什么这么做?可能是在没有原子指令的平台上,需要通过互斥体即不能复制提供原子性,所以。拷贝赋值时不能保证原子性)

25 C++三大特性
封装、继承、多态
封装就是把数据和代码组合在一起形成类,避免不确定访问和外界干扰。类可以设置访问限制,把公共的数据和方法用public访问,私有的private.
在类的内部(定义类的代码内部),⽆论成员被声明为 public、protected 还是 private,都是可以互相访问的,
没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访
问 private、protected 属性的成员。
⽆论共有继承、私有和保护继承,私有成员不能被“派⽣类”访问,基类中的共有和保护成员能被“派⽣类”访问。对于私
有和保护继承,基类中的所有成员不能被“派⽣类对象”访问。

继承:它可以使⽤现有类的所有功能,并在⽆需新编写原来的类的情况下对这些功能进⾏扩展。代码复用。
(理解虚拟继承和多重继承的问题)

多态:即向不同对象发送同⼀消息,不同的对象在接收时会产⽣不同的⾏为(重载实现编译时多态,虚函数实现运⾏时多态)
主要使用虚函数,用于当父类对象指针指向不同子类对象,表现的是虚函数的不同的行为。
基类是⼀个抽象对象——⼈,那学⽣、运动员也是⼈,⽽使⽤这个抽象对象既可以表示学⽣、也可以表示运动员。
虚函数依赖虚函数表⼯作,表来保存虚函数地址,当我们⽤基类指针指向派⽣类时,虚表指针指向派⽣类的虚函数

26 类模板和模板特化
类模板
template
class A {}; // 类模板是能接受任意类型,A后面不需要(不能)任何处理
模板偏特化(局部特化) 可以接受任意指针类型
// A
template
class A<T *> {}; // 类模板A的偏特化版本,在A后指出特化的范围

指定接受int类型
template<>
class A {} // 类模板A的全特化版本(已经是类模板的一个实例了),在A后直接指出明确类型int

全特化优先级最高。

27 强制类型转换的几种方法
static_cast:没有运⾏时类型检查来保证转换的安全性进⾏上⾏转换(基类指针=派生类指针)是安全的
进⾏下⾏转换(把基类的指针或引⽤转换为派⽣类表示),由于没有动态类型检查,所以是不安全的。
使⽤:

  1. ⽤于基本数据类型之间的转换,如把int转换成char。
  2. 把任何类型的表达式转换成void类型

dynamic_cast:主要用于“安全地向下转型”。在进⾏下⾏转换时,dynamic_cast具有类型检查(信息在虚函数中)的功能,⽐static_cast更安全。
转换后必须是类的指针、引⽤或者void*,基类要有虚函数,因为dynamic_cast需要从类的虚函数表表中获得类类型信息。
dynamic本身只能⽤于存在虚函数的⽗⼦关系的强制类型转换;对于指针,转换失败则返回nullptr,对于引⽤,转
换失败会抛出异常。

reinterpret_cast
可以将整型转换为指针,也可以把指针转换为数组;可以在不同类型的指针间进行任意转换,其实就是重新解释了内存取值,指针是一样的,但是指针的内容怎么解释(取几个字节),这个转换了一下。错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。它的使用价值:用来辅助哈希函数,把void当成Int,对整数的操作显然要对地址操作更方便。
const_cast 常量指针转换为⾮常量指针,并且仍然指向原来的对象。常量引⽤被转换为⾮常量引⽤,并且仍然指向原来的对
象。去掉类型的const或volatile属性。
去掉const属性:const_cast<int
> (&num),常用,因为不能把一个const变量直接赋给一个非const变量,必须要转换。
但是,如果把常量A的指针改成非常量指针,然后赋给一个非常量B,修改B值,常量A的值并没有变。但是它们地址是相同的。很奇怪。如果我们不想修改const变量的值,那我们又为什么要去const呢?原因是,我们可能调用了一个参数不是const的函数,而我们要传进去的实际参数确实const的,但是我们知道这个函数是不会对参数做修改的。于是我们就需要使用const_cast去除const限定,以便函数能够接受这个实际参数。

28 std::function实现回调机制的了解
什么是回调函数:如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调:解耦导致:高效、灵活现代C++的封装性导致模块之间一定的独立性。但是模块之间又需要互相协作,所以有了回调
直观一点:假设A是核心团队(A提供一个funA函数供B调用,它可以看成库函数,还有一个sell函数必须等funA返回结果才可以执行),B是另一个团队。
如果funA需要很长时间才能执行完成,如果不用回调,那么,A必须等B执行完才能执行接下来的sell(),浪费时间。
回调的话,就是funA(sell),sell回调函数虽然是A定义的,但是是B调用的,调用完直接把结果返回给A,A不用等。这就是高效
**灵活:**如果直接把sell函数写到funA函数里面不就万事大吉了吗,但是,funA不知道到底后来要做什么,只有调用者自己知道。所以如果不同人调用,只需要传入不同的函数指针就可以了。

思考:为什么用?
但是把函数指针应用于回调函数就体现了一种解决问题的策略,一种设计系统的思想。在解释这种思想前我想先说明一下,回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。回调函数本身是一种破坏系统结构的设计思路
就是说回调函数的本质就是“只有我们B才知道做些什么,但是我们并不清楚什么时候去做这些,只有其它模块A才知道,因此我们必须把我们知道的封装成回调函数告诉其它模块”

函数指针可以是普通函数,也可是类的静态函数。
也可是类的非静态函数。

为什么要用?
在C语言的时代,我们可以使用函数指针来把一个函数作为参数传递,这样我们就可以实现回调函数的机制。到了C++11以后在标准库里引入了std::function模板类,这个模板概括了函数指针的概念
std::function<int(int a, int b)> plusFunc;
它可以表示任何一个返回值为int,形参列表为int a, int b这样的函数指针。
int puls(int a, int b)

{
return a + b;
}
// 函数名就代表着该函数的地址,也就是指向该函数的指针

plusFunc = plus;

29. epoll使用
IO多路复用是什么?
先介绍三种IO模型:阻塞IO,非阻塞IO,IO多路复用。
阻塞IO:所有套接口都是阻塞的。比如read调用必须等到缓冲区有内容才返回,不然一直阻塞在这里;
非阻塞IO:当我们把一个套接口设置为非阻塞时,就是在告诉内核,当请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。不会一直阻塞。
多路复用:此模型用到select poll epoll,这两个函数也会使进程阻塞,select先阻塞,有活动套接字才返回,但是和阻塞I/O不同的是,这两个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写(就是监听多个socket)。select被调用后,进程会被阻塞,内核监视所有select负责的socket,当有任何一个socket的数据准备好了,select就会返回套接字可读,我们就可以调用recvfrom处理数据。
正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。

为什么epoll最好?
select: 1 由于只知道有几个fd准备好了,所以需要采用轮询的方式找到准备好的fd。所以会随着连接数的增加,性能变低。 2 调用时需要将fdset用用户态拷贝到内核态,遍历时再拷贝回用户空间,判断哪个fd就绪了。 3 fdset只能监视1024个。

poll:主要用链表代替fdset结构,监视数量不受限制了。新增水平触发,这次没处理,下次继续通知。

epoll:空间换时间。没有 fd 个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过 epoll_ctl 注册 fd,一旦 fd 就绪就会通过 callback 回调机制来激活对应 fd,进行相关的 io 操作。
三个函数:
int epoll_create(int size);创建一个 epoll 的句柄(红黑树结构),参数 size 并非限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。它就会占用一个 fd 值,在 linux 中查看/proc/进程id/fd/,能够看到这个 fd,所以 epoll 使用完后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
事件注册函数,将需要监听的事件和需要监听的 fd 交给 epoll 对象。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型。

要深刻理解epoll,首先得了解epoll的三大关键要素:mmap、红黑树、链表。
epoll是通过内核与用户空间mmap同一块内存实现的。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。
红黑树将存储epoll所监听的套接字。上面mmap出来的内存如何保存epoll所监听的套接字,必然也得有一套数据结构,epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。
当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。成功时返回就绪的文件描述符的个数

关于ET、LT两种工作模式:默认情况下,epoll采用 LT模式工作,这时可以处理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。
当一个新的事件到来时,ET模式下当然可以从 epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字没有新的事件再次到来时,在 ET模式下是无法再次从 epoll_wait调用中获取这个事件的;而 LT模式则相反,只要一个事件对应的套接字缓冲区还有数据,就总能从 epoll_wait中获取这个事件。因此,在 LT模式下开发基于 epoll的应用要简单一些,不太容易出错,而在 ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。默认情况下,Nginx是通过 ET模式使用 epoll的。

30 QPS,TPS,吞吐量
TPS就是每秒钟处理的事务数。用户一次请求到收到服务器响应。比如双十一一秒完成多少订单。
QPS:每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。QPS主要针对查询服务性能指标,不能描述增删改等操作,不建议用QPS描述系统整体性能。所以在最小并发数和最大并发数之间,一定有一个最合适的并发数值,在该并发数下,QPS能够达到最大。但是,这个并发并非是一个最佳的并发,因为当QPS到达最大时的并发,可能已经造成用户的等待时间变得超过了其最优值,所以对于一个系统,其最佳的并发数,一定需要结合QPS,用户的等待时间来综合确定。
假设一个查询功能需要调用N个查询接口
则QPS = N * TPS
吞吐量是指系统在单位时间内处理请求的数量。表现了一个系统的承压能力。

31 为什么有了epoll,非阻塞IO还需要reactor
技术角度,没有反应堆也能达到很高的并发,但是从编程角度,不行。即,某一瞬间,服务器共有10万个并发连接,此时,一次IO复用接口的调用返回了100个活跃的连接等待处理。先根据这100个连接找出其对应的对象,这并不难,epoll的返回连接数据结构里就有这样的指针可以用。接着,循环的处理每一个连接,找出这个对象此刻的上下文状态,再使用read、write这样的网络IO获取此次的操作内容,结合上下文状态查询此时应当选择哪个业务方法处理,调用相应方法完成操作后,若请求结束,则删除对象及其上下文。我们的主程序需要关注各种不同类型的请求,在不同状态下,对于不同的请求命令选择不同的业务处理方法。这会导致随着请求类型的增加,请求状态的增加,请求命令的增加,主程序复杂度快速膨胀,导致维护越来越困难。
反应堆模式可以在软件工程层面,将事件驱动框架分离出具体业务,将不同类型请求之间用OO的思想分离。通常,反应堆不仅使用IO复用处理网络事件驱动,还会实现定时器来处理时间事件的驱动(请求的超时处理或者定时任务的处理)
https://blog.csdn/fedorafrog/article/details/113849305具体内容看这篇文章

32. linux交换分区是什么?
是磁盘上的一块区域,可以是一个分区或一个文件,或者是他们组合。当系统物理内存吃紧时,Linux会将内存中不常访问的数据保存到swap上,这样系统就有更多的物理内存为各个进程服务。
比如:一些软件只在启动时需要很大内存,运行时不需要,就可以将这些弄到交换分区;
比如一些休眠功能,就是把内存中的数据保存到 swap 分区上,等下次系统启动的时候,再将数据加载到内存中,这样可以加快系统的启动速度。
比如:在某些情况下,物理内存有限,但又想运行耗内存的程序怎么办?这时可以通过配置足够的 swap 空间来达到目标,虽然慢一点,但至少可以运行。

33.linux普通用户和超级用户
普通用户显示符号位**$**
超级用户显示符号位**#**
普通用户进入超级用户:
输入su root,回车,再输入登陆密码
超级用户切换到普通用户:
su zx (zx是我的普通用户名)

34. vim编辑器使用
1、输入vim 文件名就可以进入文件,不存在的话会新建一个文件;
2、刚进入时是命令模式,vim编辑器会将按键解释成命令,键入i可进入插入模式(a,o也可以进入插入模式)可以编辑代码了,返回命令模式用esc键。
3、x:删除当前光标所在位置的字符 dd:删除当前光标所在行 y复制,p粘贴 可使用/(斜线键)来查找文本
4、命令模式下输入冒号:就进入底行模式。wq 保存并退出vim(最常用)q! 强制退出vim(不保存)set nu 显示行号

切换用户主目录 cd ~ 一般用户的主目录就是home/zx,切换父目录 cd … cd 绝对路径 .代表当前目录

查看当前路径:pwd

如何查看文件内容:cat 文件名 vim 文件名

ls -a显示所有文件包含隐藏文件 ls -l显示文件及其属性(rwx,拥有者,时间戳等)

Linux的touch命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。
touch 文件名 ——创建一个文件
mkdir 目录名 —— 创建一个目录
mkdir -p 目录名1/目录名2/目录名3 ——创建多层级目录
cp 原路径 目标路径 一般复制目录用cp -r 目录名 位置保证目录所有文件都被复制了。
mv 原路径 目标路径

ps 命令是最常用的监控进程的命令,通过此命令可以查看系统中所有运行进程的详细信息。(用户,PID,CPU和内存使用率,运行时间,状态等)
ps aux --sort -%mem按照内存使用率降序排列进程。
还可以通过一个管道给grep查看是否存在特定的进程。

rm 指令 :移除【删除】文件或目录 -r :递归删除整个文件夹
-f : 强制删除不提示 rm -rf要慎用

rmdir只能删除空文件夹

文件查看 head -n tail -n cat ; 长文件用less两边翻页 more可往下翻页

tail命令是实时显示日志文件的最常用解决方案 加上-f参数实时监控日志

ln -s target source 建立软连接,可以用ls -l查看这个文件夹的属性。
A 是 B 的硬链接(A 和 B 都是文件名),则 A 的目录项中的 inode 节点号与 B 的目录项中的 inode 节点号相同,即一个 inode 节点对应两个不同的文件名,两个文件名指向同一个文件,A 和 B 对文件系统来说是完全平等的。删除其中任何一个都不会影响另外一个的访问。
硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。
ln oldfile newfile建立硬链接。

echo命令将输入回显到屏幕上 -e参数表示转义字符会处理比如/n换行。
[root@localhost ~]# read name
Michael Zhang
[root@localhost ~]# echo “My name is $name”
My name is Michael Zhang

通配符
*表示匹配任意长度的任意字符 ?表示任意的一个字符 []表示括号内的匹配。

wc命令的功能为统计指定文件中的字节数、字数、行数, 并将统计结果显示输出。wc -lcw 文件名
-c, --bytes: 统计字节数。
-1,–lines: 统计行数。
-w,–words: 统计字数。

grep命令一般用来筛选数据,用来筛选我们需要的数据
grep [参数] [过滤的规则] [路径] 参数 -i : 忽略大小写 -n : 显示过滤出来的文本在文件内的行号 -v反向查找等等
标准输出 | grep [参数] [过滤规则] 用到了管道
grep -n “root” /etc/passwd 要求过滤出/etc/passwd中包含的root的行及其行号

linux进程几种状态:用ps top可以查看。S栏就是状态的意思。S 列可以看到 R、D、S、I 、Z 几个状态
R 是 Running,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
D是Disk Sleep 的缩写,也就是不可中断状态睡眠,一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。获得资源后才能运行。
Z 是 Zombie 的缩写,它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源
S 是 InterrupTIble Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
:I 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上

如何让一个任务在后台执行:&加在一个命令的最后,可以把这个命令放在后台执行
jobs用于查看当前终端后台运行的任务
ctrl+Z也可以将一个正在前台执行的命令暂停,并且放到后台(不过在后台是暂停stop的)
fg命令 将后台中的命令调至前台继续运行 如果后台中有多个命令,可以先用jobs查看jobnun,然后用 fg %jobnum 将选中的命令调出。
bg命令 将一个在后台暂停的命令,变成在后台继续执行,比如crtl+Z之后的那个进程。
kill命令:结束进程 通过jobs命令查看jobnum,然后执行 kill %jobnum 或者通过ps命令查看进程号PID,然后执行 kill %PID 如果前台进程的话直接crtl+C就可以终止了。

linux搜索文件最强大命令:find
find 指定目录 指定条件 指定动作
比如使用find命令搜索在根目录下的所有interfaces文件所在位置,命令格式为”find / -name ‘interfaces’
还有别的locate whereis等。

查看使用过的命令 history
history 5查看近5条命令
删除序号为 534 的历史命令 history -d 534
history -c清空历史

命令who的功能较简单,仅显示用户登录名、终端标志、和登录日期和时间 w显示更多用户正在执行的程序、占用CPU时间、系统的运行时间和平均负载
命名who am i 最简单,仅显示当前用户正使用的终端和登录时间,例如:
[francis@localhost ~]$ who am i
francis pts/2 2010-04-19 10:29

df 是检查Linux安装程序上可用分区空间的最常用的命令之一。可以使用“df -TH”以直观易读的格式打印分区类型和分区大小。此命令将显示每个部分的总可用空间、已用空间和可用空间。
du查看目录大小,df查看磁盘使用情况。du是通过搜索文件来计算每个文件的大小然后累加,du能看到的文件只是一些当前存在 。文件被删除不是立马就消失了,当所有程序都不用时,才会根据OS的规则释放掉已经删除的文件。

查看Linux服务器网络连接的方法:1、在Linux服务器终端可使用ifconfig命令显示所有网络接口的详细情况;2、在Linux服务器终端可使用ping命令检查网络上某台主机是否正常工作;3、在Linux服务器终端可使用netstat命令显示网络连接、路由表和网络接口的相关信息。
环境变量
env命令可以显示当前操作系统所有的环境变量 使用 echo 命令查看单个环境变量,例如:echo $PATH

$ export TEST="Test..."  # 增加一个环境变量 TEST
$ env|grep TEST  # 此命令有输入,证明环境变量 TEST 已经存在了
TEST=Test...
unset  TEST  # 删除环境变量 TEST
$ env|grep TEST  # 此命令没有输出,证明环境变量 TEST 已经删除

按照变量的生存周期划分,Linux 变量可分为两类:
永久的:需要修改配置文件,变量永久生效。
临时的:使用 export 命令声明即可,变量在关闭 shell 时失效。
在 Linux 中设置环境变量有三种方法:
所有用户永久添加环境变量: vi /etc/profile,在 /etc/profile 文件中添加变量。source /etc/profile # 激活后,环境变量才可永久生效
当前用户永久添加环境变量: vi ~/.bash_profile,在用户目录下的 ~/.bash_profile 文件中添加变量。
临时添加环境变量 PATH: 可通过 export 命令,如运行命令 export PATH=/usr/local/cuda/lib64:$PATH,将 /usr/local/cuda/lib64 目录临时添加到环境变量中。查看是否已经设置好,可用命令 export 查看。

37.同源策略和跨域问题
同源策略是浏览器安全功能,阻止一个域的js脚本和另一个域的内容进行交互,用于隔离潜在恶意文件,防止恶意网站窃取用户数据(不同源的网页不能共享cookie,获取dom,ajax请求)
当协议(http,https)域名(cc)端口号有一个不同就是跨域;
几乎任何时候安全性和便捷性都是负相关的,所以浏览器做了平衡,比如img,script,style等允许跨域引用,但是引用并不能读取资源的内容。

常见的几种磁盘调度算法
读写一个磁盘块的时间的影响因素有:
旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
实际的数据传输时间
其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短
1.先来先服务。寻道时间长
2.最短寻道时间优先,优先调度与当前磁头所在磁道距离最近的磁道。两端容易出现饥饿现象。
3、电梯调度算法。电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。
抖动现象理解
刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)
为进程分配的物理块太少,会使进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率 为了研究为应该为每个进程分配多少个物理块,Denning 提出了进程工作集” 的概念

为什么栈比堆快?
1.分配方式看,栈是后入先出,有严格顺序,堆需要在空闲链表找一段合适的空间需要时间的;2、栈有CPU支持的命令push pop
经常是和寄存器相关的,把寄存器值入栈。;3 访问来看,堆需要两次访问内存,一次指针一次数据,栈只要一次。

常见内存分配内存错误
内存分配未成功就使用,概率较小;
内存未初始化就使用,比如局部变量必须初始化,堆上的变量,全局变量会自动初始化0,不过最好还是自己初始化;
下标越界,经常出现的;
忘记释放内存(内存泄漏):经常出现,比如忘记free或者delete,delete[],比如基类析构函数没用虚函数;
多次释放内存:程序中的对象调用关系过于复杂;比如返回值是局部对象的引用或指针,但是这个局部对象返回就销毁了,是野指针,使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”(内存没了,指针变量依然存在);比如禁用了拷贝构造函数,在对象有指针成员时,默认的拷贝是位拷贝,把指针复制一遍,浅拷贝,那么析构的时候就会多次释放同一内存。
ASCII、Unicode和UTF-8编码的区别?
ASCII 只有127个字符,表示英文字母的大小写、数字和一些符号,但由于其他语言用ASCII 编码表示字节不够,一个字节表示一个字符。
由于每个国家的语言都有属于自己的编码格式,在多语言编辑文本中会出现乱码,这样Unicode应运而生,Unicode就是将这些语言统一到一套编码格式中,通常两个字节表示一个字符,而ASCII是一个字节表示一个字符,这样如果你编译的文本是全英文的,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。
为了解决上述问题,又出现了把Unicode编码转化为“可变长编码”UTF-8编码,UTF-8编码将Unicode字符按数字大小编码为1-6个字节,英文字母被编码成一个字节,常用汉字被编码成三个字节,如果你编译的文本是纯英文的,那么用UTF-8就会非常节省空间,并且ASCII码也是UTF-8的一部分

原子操作的是如何实现的
CAS吗?
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行。当一个处理器修改了缓存,就会设置缓存行无效,其他的就无法修改了。这和CAS类似,CAS就是乐观锁,先尝试修改,如果发现已经被修改了,放弃修改。只不过原子操作实现是操作系统的,而且基于缓存,所以更快一些。

页面置换算法
最佳置换算法(OPT,Optimal) :每次选择淘汰的页面将是以后永不使用,或者在最长时间内不再被访问的页面,这样可以保证最低的缺页率。但是理想情况,无法实现。
先进先出置换算法(FIFO) :每次选择淘汰的页面是最早进入内存的页面 实现方法:把调入内存的页面根据调入的先后顺序排成一个队列。FIFO的性能较差,因为较早调入的页往往是经常被访问的页,这些页在FIFO算法下被反复调入和调出,并且有Belady现象。所谓Belady现象是指:采用FIFO算法时,如果对—个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多但缺页率反而提高的异常现象
最近最久未使用置换算法(LRU,least recently used) :每次淘汰的页面是最近最久未使用的页面 实现方法:赋予每个页面对应的页表项中,用访问字段记录该页面自.上次被访问以来所经历的时间t(该算法的实现需要专门的硬件支持,虽然算法性能好,但是实现困难,开销大)。当需要淘汰一个页面时,选择现有页面中t值最大的,即最近最久未使用的页面。
LRU性能较好,但需要寄存器和栈的硬件支持。
(注意对于MySQL 和 Linux 操作系统是通过改进 LRU 算法来避免「预读失效和缓存污染」而导致缓存命中率下降的问题。
即活跃LRU和非活跃LRU,并且非活跃到活跃条件严格一点,不仅仅要活跃一次
注意手写LRU算法的实现。)
分析:为什么用哈希表+双向链表?
首先要分析需求,主要是put,get操作。LRU缓存:要求添加元素时判断是否存在,存在的话要找到它移动到头部,找不到就添加到头部;获取元素也要判断是否存在,存在的话就返回元素并移动到头部,不在的话添加元素到头部。
所以两大核心操作就是:判断元素是否存在,以及移动元素。判断元素的存在并找到位置很适合用_unordered_map。
移动元素适合用链表
,所以综合一下就是 unordered_map<int, DoubleList*> memory; 为什么是双向链表呢?因为涉及到删除中间某个元素,如果是单链表还需要从头找到前一个元素,线性复杂度,双向链表只要prev指针就能找到。
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

struct DoubleList {
int key, val;
DoubleList* pre, * next;
DoubleList(int _key,int _val):key(_key),val(_val),pre(nullptr),next(nullptr){ }
};

class LRU {
private:
	int capacity;
	DoubleList* head, * tail;
	unordered_map<int, DoubleList*> memory;
public:
	LRU(int _capacity) {
		this->capacity = _capacity;
		head = new DoubleList(-1, -1);
		tail = new DoubleList(-1, -1);
		head->next = tail;
		tail->pre = head;
	}
	~LRU(){
		if (head != nullptr) {
			delete head;
			head = nullptr;
		}
		if (tail != nullptr) {
			delete tail;
			tail = nullptr;
		}
		for (auto& a : memory) {
			if (a.second != nullptr) {
				delete a.second;
				a.second = nullptr;
			}
		}
	}
	void set(int _key, int _val) {
		if (memory.find(_key) != memory.end()) {
			DoubleList* node = memory[_key];
			removeNode(node);
            node->val = _val;
			pushNode(node);
			return ;
		}
		if (memory.size() == this->capacity) {// 这里很重要,也很爱错,千万记得更新memory
			int topKey = head->next->key;//取得key值,方便在后面删除
			removeNode(head->next);//移除头部的下一个
			memory.erase(topKey);//在memory中删除当前头部的值
		}
		DoubleList* node = new DoubleList(_key, _val);//新增node
		pushNode(node);//放在尾部
		memory[_key] = node;//记得在memory中添加进去
	}
	int get(int _key) {
		if (memory.find(_key) != memory.end()) {
			DoubleList* node = memory[_key];
			removeNode(node);
			pushNode(node);
			return node->val;
		}
		return - 1;
	}

	void removeNode(DoubleList* node) {
		node->pre->next = node->next;
		node->next->pre = node->pre;
	}
	void pushNode(DoubleList* node) {
		tail->pre->next = node;
		node->pre = tail->pre;
		node->next = tail;
		tail->pre = node;
	}
};

时钟置换算法是一种性能和开销较均衡的算法,又称CLOCK算法,或最近未用算法(NRU,Not Recently Used)为每个页面设置一个访问位,再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位置为1。当需要淘汰-一个页面时,只需检查页的访问位。如果是0,就选择该页换出;如果是1,则将它置为0,暂不换出,继续检查下一个页面。
在其他条件都相同时,应优先淘汰没有修改过的页面,避免I/O操作。这就是改进型的时钟置换算法的思想。

LFU算法:这是redis用的页面替换算法,防止LRU出现缓存污染。根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。 LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。

38.对虚拟内存的理解
单片机是没有操作系统的,所以每次写完代码,都需要借助工具把程序烧录进去,这样程序才能跑起来。并且单片机的 CPU 是直接操作内存的**「物理地址」。要想在内存中同时运行两个程序是不可能的。如果第一个程序在 2000 的位置写入一个新的值,将会擦掉第二个程序存放在相同位置上的所有内容,所以同时运行两个程序是根本行不通的,这两个程序会立刻崩溃。
如何解决?
我们可以把进程所使用的地址「隔离」开来,即让操作系统为
每个进程分配独立的一套「虚拟地址」,人人都有,大家自己玩自己的地址就行(虚拟地址),互不干涉。但是有个前提每个进程都不能访问物理地址,至于虚拟地址最终怎么落到物理内存里,对进程来说是透明的,操作系统已经把这些都安排的明明白白了。操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。主要有两种方式,分别是内存分段和内存分页
分段管理
程序是由若干个逻辑分段组成的,如可由
代码分段、数据分段、堆段、栈段**组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。
分段机制下的虚拟地址由两部分组成,段选择因子和段内偏移量。
段选择因子里面有段号,可以通过它寻找段表中对应的段基址,然后加上偏移量就是物理地址了。
缺点:
内存碎片和内存交换效率低的问题。内存分段管理可以做到段根据实际需求分配内存,所以有多少需求就分配多大的段,所以不会出现内部内存碎片。但是会出现外部碎片,很多小块的内存分散,无法被利用。解决「外部内存碎片」的问题就是内存交换。这个内存交换空间,在 Linux 系统里,也就是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,用于内存与硬盘的空间交换。因为硬盘的访问速度要比内存慢太多了,每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。所以,如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。(swap分区的作用就是在物理内存不够时,将一些不用的程序移到swap分区中,要用时再进入内存,有可能不是原来的物理内存位置了,因为有内存交换紧凑碎片)

分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB。会出现页内碎片。
虚拟地址与物理地址之间通过页表来映射,CPU将地址解释为页号和偏移量,页表保存页号和物理块号的映射。
缺点:内部碎片;因为操作系统是可以同时运行非常多的进程的,那这不就意味着页表会非常的庞大。在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表。
这 4MB 大小的页表,看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。
那么,100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。(可以用多级页表解决)
多级页表对导致映射过程变复杂,时间开销大。所以有TLB快表。把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer)。有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。
linux虚拟内存空间分布

文件映射段,包括动态库、共享内存等。
代码段下面还有一段内存空间的(灰色部分),这一块区域是「保留区」,之所以要有保留区这是因为在大多数的系统里,我们认为比较小数值的地址不是一个合法地址,例如,我们通常在 C 的代码里会将无效的指针赋值为 NULL。因此,这里会出现一段不可访问的内存保留区,防止程序因为出现 bug,导致读或写了一些小内存地址的数据,而使得程序跑飞。
堆:注意和数据结构中堆不一样,联系malloc原理。如果分配空间小于128k用brk方法,就是堆顶指针移动,比如你需要10K,可能分配不止10K,是100多k,其余的会被放到内存池。因为堆组织方式类似于链表,频繁malloc/free产生内存碎片。释放资源也是回到内存池
如果空间大于128k,就会调用mmap()系统调用,会从内存映射区直接取一段空间,避免产生内存碎片。但是会有状态切换以及缺页中断(malloc使用时才会分配物理内存)。释放资源被操作系统回收,这也是需要时间的。
所以二者结合使用
ulimit -a #查看栈大小,一般8M
ulimit -s 32768 # 设置当前栈的大小为32M

总结虚拟内存作用:
1、最主要的就是为了实现多进程。由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的,解决了多进程之间地址冲突的问题。
2、在多进程时,内存空间可能不够用,就需要虚拟内存的换入换出技术,swap。将硬盘空间当成内存,让用户感觉内存很大。
3、段中有优先级等,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性

39. 在4GB物理内存的机器上申请8GB内存空间会怎样
在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
在 64位 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;

40 如何避免预读失效和缓存污染的问题?
传统的 LRU 算法存在这两个问题:
「预读失效」导致缓存命中率下降(操作系统读磁盘时会多读一些没用的数据)
「缓存污染」导致缓存命中率下降(批量读数据可能会把热点数据挤出去)
Redis 的缓存淘汰算法则是通过实现 LFU 算法来避免「缓存污染」而导致缓存命中率下降的问题(Redis 没有预读机制)。
MySQL 和 Linux 操作系统是通过改进 LRU 算法来避免「预读失效和缓存污染」而导致缓存命中率下降的问题。

预读失效:由于空间局部性,如果想要0-4KB的数据,很可能把后面的8-16KB都读到缓存中,如果使用传统的 LRU 算法,就会把「预读页」放到 LRU 链表头部,而当内存空间不够的时候,还需要把末尾的页淘汰掉。
如果这些「预读页」如果一直不会被访问到,就会出现一个很奇怪的问题,不会被访问的预读页却占用了 LRU 链表前排的位置,而末尾淘汰的页,可能是热点数据,这样就大大降低了缓存命中率 。
我们不能因为害怕预读失效,而将预读机制去掉,大部分情况下,空间局部性原理还是成立的。

要避免预读失效带来影响,最好就是让预读页停留在内存里的时间要尽可能的短,让真正被访问的页才移动到 LRU 链表的头部,从而保证真正被读取的热数据留在内存里的时间尽可能长。
Linux 操作系统实现两个了 LRU 链表:活跃 LRU 链表(active_list)和非活跃 LRU 链表(inactive_list);
MySQL 的 Innodb 存储引擎是在一个 LRU 链表上划分来 2 个区域:young 区域 和 old 区域。
这两个改进方式,设计思想都是类似的,都是将数据分为了冷数据和热数据,然后分别进行 LRU 算法。不再像传统的 LRU 算法那样,所有数据都只用一个 LRU 算法管理。
有了这两个 LRU 链表后,预读页就只需要加入到 inactive list 区域的头部,当页被真正访问的时候,才将页插入 active list 的头部。如果预读的页一直没有被访问,就会从 inactive list 移除,这样就不会影响 active list 中的热点数据。

但是如果还是使用「只要数据被访问一次,就将数据加入到活跃 LRU 链表头部(或者 young 区域)」这种方式的话,那么还存在缓存污染的问题。
当我们在批量读取数据的时候,由于数据被访问了一次,这些大量数据都会被加入到「活跃 LRU 链表」里,然后之前缓存在活跃 LRU 链表(或者 young 区域)里的热点数据全部都被淘汰了,如果这些大量的数据在很长一段时间都不会被访问的话,那么整个活跃 LRU 链表(或者 young 区域)就被污染了。

当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 I/O,MySQL 性能就会急剧下降。

前面的 LRU 算法只要数据被访问一次,就将数据加入活跃 LRU 链表(或者 young 区域),**这种 LRU 算法进入活跃 LRU 链表的门槛太低了!**只要我们提高进入到活跃 LRU 链表(或者 young 区域)的门槛,就能有效地保证活跃 LRU 链表(或者 young 区域)里的热点数据不会被轻易替换掉。Linux 操作系统:在内存页被访问第二次的时候,才将页从 inactive list 升级到 active list 里。MySQL Innodb:在内存页被访问第二次的时候,并不会马上将该页从 old 区域升级到 young 区域,因为还要进行停留在 old 区域的时间判断:

41.count(1),count( ),count(字段),select(1),select ( )效率比较
首先说一下select(1),select ( )。
select(1)主要用于查询表中是否有符合条件的记录(比如select 1 from seckill where id = 1001;),select 1一般用来当作条件使用,比如**exists( select 1 from 表名)**等;select 1配合count()、sum()函数。select 1的效率比select 列名和select
快,因为不用查字典表。(为什么这么说,参数是 1,不是字段,所以不需要读取记录中的字段值。参数 1 很明显并不是 NULL,因此 server 层每从 InnoDB 读取到一条记录,就将 count 变量加 1。)
而select * from … 是返回所有行的所有列,性能很差,不推荐用。性能差的原因有:1 不需要的列会增加数据传输时间和网络开销(需要解析更多的对象、字段、权限、属性等相关内容,一些大的文本字段开销很大传输)2.大字段还会增加IO操作;3 失去MySQL优化器
“覆盖索引”策略优化的可能性。如果用户使用select *,获取了不需要的数据,则首先通过辅助索引过滤数据,然后再通过聚集索引获取所有的列,这就多了一次b+树查询,速度必然会慢很多。而聚集索引很可能数据在磁盘(外存)中(取决于buffer pool**的大小和命中率),这种情况下,一个是内存读,一个是磁盘读,速度差异就很显著了,几乎是数量级的差异。
(覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。)

count(1),count( *),count(字段)

在通过 count 函数统计有多少个记录时,MySQL 的 server 层会维护一个名叫 count 的变量。

server 层会循环向 InnoDB 读取一条记录,如果 count 函数指定的参数不为 NULL,那么就会将变量 count 加 1,直到符合查询的全部记录被读完,就退出循环。最后将 count 变量的值发送给客户端。
count(1),count( *),InnoDB 循环遍历聚簇索引(主键索引)或二级索引,将读取到的记录返回给 server 层,但是不会读取记录中的任何字段的值,所以效率高一点。
count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。
使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。

使用 MyISAM 引擎时,执行 count 函数只需要 O(1 )复杂度,这是因为每张 MyISAM 的数据表都有一个 meta 信息有存储了row_count值,由表级锁保证一致性,所以直接读取 row_count 值就是 count 函数的执行结果。
而 InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM一样,只维护一个 row_count 变量。

如何优化 count(*)
就可以使用 show table status 或者 explain 命令来表进行估算,不会真正查表;
或者每新插入一个记录就将计数表中的计数字段 + 1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。

static关键字的作用
最主要的是隐藏和对象共享某一数据功能。
作用在局部变量,使其存储在静态区(包括全局,静态),和程序生命一样长;作用在全局变量主要是其他文件不能引用它,防止不同文件重名的干扰;作用在成员变量,成为类的一部分,公共部分比如总数,只能在类外初始化不能在构造函数初始化。作用在成员函数主要是为了访问静态成员变量,只能访问它,因为没有this指针。

静态链接和动态链接
程序的几个步骤就是预处理,编译,汇编,链接,装入,运行。链接主要完成符号解析和重定位功能,如果在编译汇编时就完成链接,就是静态链接,如果这一步只添加参数信息,推迟到运行时链接,就是动态链接。

举个例子 test.c main.c源文件,静态链接方法
gcc -c test.c main.c -o test.o main.o
ar -rv  test.lib test.o main.o 
gcc  test.lib  -o test.exe
动态链接方法
gcc  test.c -shared -o test.dll
gcc main.c test.dll -o test.exe

静态链接使每个进程都包含完整的库代码副本,比较占空间,且一个库修改就要重新编译整个代码。优点就是发布方便,可独立运行,不需要动态库依赖,前期编译速度快一点。动态链接相反,耦合度小,适合大项目。

变量声明和定义
变量声明不分配内存,可声明多次(extern int a),定义分配内存只有一次(int a,int a=1)

#ifdef 标识符
程序1
#else
程序2
#endif
条件编译

#idndef aaa
#define aaa 2
##endif 解决双重define嵌套问题

C++中类成员的访问权限****三种继承方式
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

一般成员变量放在私有部分,隐藏起来不让外部看到,成员函数一般是公有的,因为你定义成保护或私有,类外是无法访问这个函数的,没什么意义。成员函数一般是给外人使用,操作成员变量。
保护权限和私有权限在继承时会有区别,比如父亲的车可以作为保护成员,可以让儿子也使用,而父亲的私房钱应作为私有成员,儿子不能使用。
继承方式: 公共继承、保护继承、私有继承
继承语法:class 子类 : 继承方式 父类 默认的继承方式(如果缺省,默认为private继承)
父类私有的部分不管是哪种继承方式,子类都无法访问(除非父类在public或protected中定义了访问私有内容的函数你才有可能访问到)
保护继承的特点是基类的所有公有成员和保护成员都作为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然为私有的。

友元:类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend. 函数的定义在类外。

class Box
{
   double width;
public:
   double length;
   friend void printWidth( Box box );
   void setWidth( double wid );
};

子类对父类成员的访问权限跟如何继承没有任何关系,“子类可以访问父类的public和protected成员,不可以访问父类的private成员”——这句话对任何一种继承都是成立的。
(2)继承修饰符影响着谁可以知道“继承”这件事。public继承大家都知道,有点像“法定继承人”,因此,任何代码都可以把子类的引用(或指针)直接转换为父类。也因为这个原因,public继承常用来表达设计中所谓的“is-a”关系。private继承则有点像“私生子”,除了子类自己,没有人知道这层关系,也因此,除了子类自己的代码之外,没有其它人知道自己还有个父亲,于是也就没有其它人可以做相应的类型转换。为此,私有继承常用于表达非“is-a”的关系,这种情况下子类只是借用父类的某些实现细节。protected继承则有点特殊,外界同样不知道这层关系,但家族内部的子孙们可以知道,有点像“自家知道就行了,不许外扬”的意思,于是子孙们是可以做这种向上转型,其它代码则不可以。因为这种特殊性,protected继承在实际中用得很少。
(3)还需要补充一点,由于“继承关系”的可见性受到了影响,那么继承来的财产的可见性也必然受到影响。比如一个成员变量或成员函数,在父类中本来是public的,被某个子类protected继承之后,对子类来讲,这个成员就相当于protected成员了——继承是继承到了,但权限变了。具体的规则教材上都会讲的。

**函数声明(函数原型)、函数调用、函数定义:**函数声明一般放在头文件中,没有函数体的,规定了参数和返回类型,参数名可以忽略,不开辟空间,目的是告诉编译器这是一个函数,在函数调用时编译器帮你检查函数写没写对。函数定义有函数体。函数调用不用写返回类型和参数类型。
多态的实现有哪几种?
多态分为静态多态和动态多态。其中,静态多态是通过重载和模板技术实现的,在编译期间确定;动态多态是通过虚函数和继承关系实现的,执行动态绑定,在运行期间确定。

重载和重写的区别:

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

#include<bits/stdc++.h>
using namespace std;
class A
{
	void fun() {};
	void fun(int i) {};
	void fun(int i, int j) {};
	void fun(double i){};
};

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

#include<bits/stdc++.h>

using namespace std;

class A
{
public:
	virtual	void fun()
	{
		cout << "A";
	}
};
class B :public A
{
public:
	virtual void fun()
	{
		cout << "B";
	}
};
int main(void)
{
	A* a = new B();  基类的指针指向派生类的对象,指向的是派生类中基类的部分。
	所以只能操作派生类中从基类中继承过来的数据和基类自身的数据。C++的多态性可以解决基类指针不能操作派生类的数据成员的问题。
	a->fun();//输出B  
}

为什么需要基类的指针指向派生类的对象,不用派生类自己的指针指向自己的对象?
因为可以用基类指针指向其不同派生类对象,所以才能实现多态和虚函数更为重要的是,当我们想要用某个数据结构去存储不同对象时,比如我们想实现一个所有动物对象“走”得数组,遍历数组时向我们展示出不同动物对象走的情况,该怎么做呢?因为我们的数组只能实现同一类型数据存储,而我们又想展示出不同类型对象的“走”,所以很显然我们只需在数组中存储基类指针,然后把数组中的每个基类指针与相应的类对象进行动态绑定即可(书面来说就是接口重用,提高代码可重用性,维护性扩充性)

vector<Animal*> walk;
	walk.emplace_back(new Bird());
	walk.emplace_back(new Reptile());
	walk.emplace_back(new Human());
	for (const auto &c : walk)
		c->walk();

动态绑定是如何实现的?
就是问动态多态怎么实现?当编译器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中并且在对象中增加一个指针vptr,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。
实现的必要条件就是:必须要有继承,有虚函数,有基类指针指向派生类对象
虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存一个指向该类虚函数表的指针vptr,每个对象的vptr的存放地址都不同,但都指向同一虚函数表。在gcc编译器的实现中虚函数表vtable存放在可执行文件的只读数据段.rodata中。

纯虚函数有什么作用?如何实现?
定义纯虚函数是为了实现一个接口,起到规范的作用,想要继承这个类就必须覆盖该函数。所有人用同一个接口。
实现方式是在虚函数声明的结尾加上= 0即可。 virtual void fun()=0;

C++禁止使用拷贝构造函数和赋值运算符方法
要么将拷贝构造函数和赋值运算符声明为private并不实现。或者在后面加=delete。 nocopyable

原则:
1、构造函数不能为虚函数;
2、析构函数需要是虚函数;

析构函数为什么要是虚函数: 防止内存泄露
因为我们可以用基类的指针指向派生类对象,我们希望调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。但是,如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。

构造函数不能为虚函数:不能也没必要
不能:虚函数有一个指向虚函数表的指针,这个虚函数表存储在对象的内存空间中,而构造函数就是实例化对象的,对象都没有,哪来的虚函数表。虚函数的调用依赖于虚函数表,而指向虚函数表的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。
没必要:虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的。*

内存泄漏的场景有哪些?
1、没有成对使用new/delete ,malloc/free, new []/delete [] 比如在构造函数new申请内存,但是析构函数没有释放内存。
2、基类析构函数不是虚函数,导致派生类析构时不调用。为了实现动态绑定,基类指针指向派生类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象中的部分数据。
3. 如果类中有从堆中动态分配的指针变量,则类中必须定义拷贝构造函数。为什么呢?因为默认的是位拷贝(浅拷贝)。导致两个对象指向同一个内存地址,两次释放就会出问题。

拷贝构造函数注意点 A(const A &a);
拷贝构造函数必须是当前类的引用
如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头**,陷入死循环**。
只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。
拷贝构造函数是const 引用
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。
另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。
没有返回类型

Student::Student(const Student &stu){
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout<<"Copy constructor was called."<<endl;
}
**//重载=运算符
Student & Student::operator=(const Student &stu){
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout<<"operator=() was called."<<endl;
   
    return *this;
}**

重载赋值运算符是返回引用。 赋值运算符写的时候还要注意就是要先删除之前动态申请的空间,重新申请。还要判断是不是本身,是本身直接返回*this 。

拷贝构造函数的应用场景:
将其它对象作为实参 A a(b);
Student stu4 = stu1; //在创建对象的同时赋值
stu5=display(stu5); //函数形参,返回值调用
(返回值优化(Return value optimization,缩写为RVO)是C++的一项编译优化技术。它最大的好处是在于: 可以省略函数返回过程中复制构造函数的多余调用,解决 “C++ 中长久以来为人们所诟病的临时对象的效率问题”。)
Student stu5; //调用普通构造函数Student()
stu5 = stu1; //调用operator=()

内存分配方式有几种?
1、栈分配,比如函数的局部变量,函数结束自动销毁,分配效率很高,但是栈容量有限。
2、堆分配(new,malloc)释放需要程序员自己控制;
3.自由存储区分配,和堆比较像,但不是一个空间,比定位new运算符就是指定一个地址分配,这个地址属于自由存储区。
4、常量存储区,无法修改;
5.全局、静态存储区,初始化和未初始化BSS,C++已经不区分初始化和未初始化了,自动初始化为0。

如何构造一个类,使得只能在堆上或只能在栈上分配内存?
//A a;//栈上创建
A* p = new A;//堆上创建
在C++中,创建类的对象有两种方法,一种是静态建立,A a; 另一种是动态建立,调用new 操作符。
只能在堆上分配内存:将析构函数声明为private;
编译器管理了对象的整个生命周期,编译器为对象分配空间的时候,只要是非静态的函数都会检查,包括析构函数,但是此时析构函数不可访问,编译器无法调用类的析构函数来释放内存,那么编译器将无法在栈上为对象分配内存。

只能在栈上生成对象:将new和delete重载为private。

private:
    void* operator new(size_t)
    {};
    void operator delete(void*)
    {};

如何让类只能创建一个对象?
单例模式。 构造函数,拷贝赋值都私有,创建一个静态的成员,静态的函数返回这个成员。

class singleton
{
public:
 static singleton* getInstance()
 {
  return &_single;
 }
private:
 //构造函数私有
 singleton(){};
 //防拷贝
 singleton(const singleton& s) = delete;
 singleton& operator=(const singleton& s) = delete;
 static singleton _single;
};

//静态成员的初始化
singleton singleton::_single;
// 在程序入口之前就完成单例对象的初始化

结构体内存对齐规则
一、结构体对齐规则首先要看有没有用**#pragma pack宏声明**,这个宏可以改变对齐规则,有宏定义的情况下结构体的自身宽度就是宏上规定的数值大小,所有内存都按照这个宽度去布局(这样说其实不太严谨,后面会提到),#pragma pack 参数只能是 ‘1’, ‘2’, ‘4’, ‘8’, or ‘16’。不过如果最大的类型都没有pack定义的大,就根据最大的类型优化。
二、在没有#pragma pack这个宏的声明下,遵循下面三个原则:
1、第一个成员的首地址为0.
2、每个成员的首地址是自身大小的整数倍
3、结构体的总大小,为其成员中所含最大类型的整数倍。

乐观锁、悲观锁的概念实现及应用场景
什么是悲观锁?使用场景是什么?
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。

什么是乐观锁?使用场景是什么?
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
在 Java 中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。
版本号:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
CAS 涉及到三个操作数:
V :要更新的变量值(Var)
E :预期值(Expected)
N :拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。

乐观锁的问题:
ABA问题:一开始是1,中间改成6,后来有改成1,这时是可以更新的,但是确实被修改过
循环开销大:CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。可以用pause指令来优化,不用循环。
只能保证一个共享变量的原子操作

说一下常用的应用层协议

struct可以定义函数吗?
在C++中,class和struct是同样的东西
区别仅仅在于class中的成员函数和变量如果不指定访问类型的话,缺省是private的,而struct中的成员函数和变量如果不知定访问类型,缺省是public的而已
其他的都是一样的了,所以struct可以有任何函数(构造、析构也包括在内)

关于右值引用和转移构造
左值:变量,可以长时间保存,可以存在于=左边的值,可以取地址;
右值:临时值,不能存在于=左边的值,不可以取地址。常数、表达式、函数返回值。

int &t = a;         // a为左值,所以可以赋给左值引用
// int &t1 = 3;     // 错误 3为一个临时值,为右值,不能赋给左值引用
// int &&t = a;     // 错误 a为左值,不能赋给右值引用
int &&t = 3;        // 可以
int &&t = -a;       // 可以

,C++11提供了更为优雅的转换函数std::move,std::move(a)无论a是左值还是右值都将转换为右值。就是将传入值强制转换为值原类型的右值引用。

常量右值引用没有用途。非常量右值引用可以用于移动语义,完美转发。
当右值引用作为构造函数参数时,这就是所谓的移动构造函数,也就是所谓的移动语义。

在返回对象时,需要拷贝构造函数,生成一个临时对象,再把临时对象拷贝给接收的变量,有一次拷贝是多余的。当堆内存很大时,多余的深拷贝以及其对象的堆内存析构耗时就会变的很可观,那么是否有一种方式,可以让函数中的返回的临时对象空间是否可以不析构,而可以重用呢?
基于上述原因,因此c++11提供了移动构造来解决上述问题。移动构造也是基于右值引用来实现的。

成员初始化列表的概念,为什么用它会快一些?
初始化列表只用了一次自定义构造函数,而函数体内赋值是先调用默认构造函数,再调用一次赋值函数。
若类的数据成员是静态的(const)和引用类型,必需用初始化列表。静态(const)的数据成员只能初始化而不能赋值,同样引用类型也是只可以被初始化,那么只有用初始化列表。

C++函数调用的压栈过程
当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;

写C++代码时有一类错误是 coredump ,很常见,你遇到过吗?怎么调试这个错误?
生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。

RAII与RTTI
RAII:(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的技术。
利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
主要表现为:使用智能指针,智能指针其实是模板类,然后你使用一个智能指针对象来管理你的资源,如果这个智能指针对象是栈对象,那么它超出作用域后,自动销毁时,也会释放掉它管理的资源,这样就可以避免你忘记释放资源而导致的资源泄露。

RTTI: (Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。
主要表现为:typeid和dynamic_cast操作符
typeid操作符:返回指针和引用所指的实际类型;
dynamic_cast操作符:将基类类型的指针或引用安全地转换为其派生类类型的指针或引用。
而运行期识别的信息存放在什么地方呢?
答案:存放在一个特定的对象中,这个对象的指针就放在虚函数表的其中一项。
(具体一点,如果类有虚函数,就会生成虚函数表,这是类共享的,每个对象都会有一个vptr指向这个虚函数表。当基类生成子类,子类也会继承这个虚函数表,如果子类重写了虚函数,就会替换该虚函数的地址,我们知道成员函数并不在类对象的内存布局中,是单独的。当基类指针指向派生类的时候,实际上既可以获得派生类的虚表指针,通过虚表指针,基类既可以去调用派生类中的成员函数。如此不同的派生类的虚函数不同,调用的时候实现的效果也就不同,也就符合了多态的定义,面对同一消息的不同应答。)

继承和组合的特点
继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的缺点有以下几点:
①:父类的内部细节对子类是可见的。隐藏性不够
②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。
③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。has-a关系

组合的优点:
①:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。隐藏性

②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。

③:当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。

组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义

函数指针?
数指针的声明方法
int (pf)(const int&, const int&); (1)
上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个const int&参数的函数。注意
pf两边的括号是必须的,否则上面的定义就变成了:
int *pf(const int&, const int&); (2)
而这声明了一个函数pf,其返回类型为int *, 带有两个const int&参数。

为什么需要内存对齐?

1.不同硬件平台不一定支持访问任意内存地址数据,使用内存对齐可以保证每次访问都从块内存地址头部开始存取

2.提高cpu内存访问速度,内存是分块的,如两字节一块,四字节一块,考虑这种情况:一个四字节变量存在一个四字节地址的后三位和下一个四字节地址的前一位,这样cpu从内存中取数据便需要访问两个内存并将他们组合起来,降低cpu性能
用空间换时间
结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个准则:

 1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

 2) 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

 3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。

union的大小取决于它所有的成员中,占用空间最大的一个成员的大小。

有指定时其对齐的规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐,并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。#pragma pack(2) //指定按2字节对齐。

你知道printf函数的实现原理是什么吗?
下面给出printf(“%d,%d”,a,b);(其中a、b都是int型的)的汇编代码

.section
.data
string out = “%d,%d”
push b
push a
push $out
call printf

你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了

i++,++i的区别以及效率
在单独存在的时候没区别。都是加1.
但是在运算时。
我们通过汇编代码,就可以很清楚地看到两者的不同了。
a=i++,先将i赋值给了a,然后才进行了+1操作,把加1结果赋值给了i。
b=++i,先进行加1操作,然后将加1结果先赋值给i,在赋值给了b。
效率没有区别,执行顺序不一样而已。
但是,在自定义类型时,就不一样了。i++是先用临时对象保存原来的对象,然后对原对象自增,再返回临时对象,不能作为左值;++i是直接对于原对象进行自增,然后返回原对象的引用,可以作为左值。
由于要生成临时对象,i++需要调用两次拷贝构造函数与析构函数(将原对象赋给临时对象一次,临时对象以值传递方式返回一次);
++i由于不用生成临时变量,且以引用方式返回,故没有构造与析构的开销,效率更高。
(一般编译器会有NRV优化,所以临时对象的拷贝可以优化,只有一次)

C++左值引用和右值引用
C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题,通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)
(关于移动语义和完美转发间上面)

左值就是有名字的,可以取地址的,放在等号左边的。比如int a=1; a就是左值;

右值就是没有名字的,不能取地址的,比如常量值,函数返回值,表达式等。

左值引用就是左值的引用类型,比如马上初始化。比如int a=1; int &b=a
int &&d=10,就是右值引用,必须右值初始化。
左值常引用可以左值,右值初始化;

通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。

如何在共享内存上使用STL标准库?
假设进程A在共享内存中放入了数个容器,进程B如何找到这些容器呢?
一个方法就是进程A把容器放在共享内存中的确定地址上(fixed offsets),则进程B可以从该已知地址上获取容器。另外一个改进点的办法是,进程A先在共享内存某块确定地址上放置一个map容器,然后进程A再创建其他容器,然后给其取个名字和地址一并保存到这个map容器里。
进程B知道如何获取该保存了地址映射的map容器,然后同样再根据名字取得其他容器的地址。

死锁之生产者消费者和哲学家进餐问题
五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。
下面是一种错误的解法,如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其它哲学家吃完并释放自己手中的筷子,导致死锁。
为了防止死锁的发生,可以设置两个条件:
必须同时拿起左右两根筷子
只有在两个邻居都没有进餐的情况下才允许进餐。

读写问题:不能同时读(可能出现覆盖数据),不能读写同时(可能数据不一致),可以同时读。
解决办法:最粗暴的,读写都加锁。并不满足题目读进程与读进程可以同时访问共享数据的要求!

引入count变量,用来记录当前有几个读进程在访问共享数据。

回收线程的几种方法
等待线程结束: int pthread_join(pthread_t tid, void retval);**
主线程调用,等待子线程退出并回收其资源,类似于进程中wait/waitpid回收僵尸进程,调用pthread_join的线程会被阻塞。
tid:创建线程时通过指针得到tid值。
retval:指向返回值的指针。

结束线程:*pthread_exit(void retval);
子线程执行,用来结束当前线程并通过retval传递返回值,该返回值可通过pthread_join获得。
retval:同上。

分离线程:int pthread_detach(pthread_t tid);
主线程、子线程均可调用。主线程中pthread_detach(tid),子线程中pthread_detach(pthread_self()),调用后和主线程分离,子线程结束时自己立即回收资源。

int status = pthread_create(&tid, NULL, ThreadFunc, NULL)
明确线程有joinable,unjoinable两种状态。如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多)。只有当你调用了pthread_join之后这些资源才会被释放。若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit时
自动会被释放。而create创建的默认是joinable,所以需要join函数来回收资源,不然就内存泄露了。

pthread_detach用到这一个函数的目的是主线程在运行的时候可能希望不阻塞能够继续运行,让子线程自己运行完毕后自己释放资源,即异步结束,调用join()可以看成是同步结束。这一个函数既可以在主线程中写也可以在子线程中写,在子线程中一般会调用pthread_self()函数,主线程中则需要知道要detach的子线程ID。这个函数会让线程变成unjoinable状态。

线程终止的几种方法
1、代码执行完自然终止;
2、使用pthread_exit() 或者 return;
二者的区别:return既可以用于普通函数也可以用于终止线程,但是exit()只能用于终止线程;
return若在主线程中,会终止其他子线程,exit()不会影响子线程,另外exit()还会调用线程清理程序,而return做不到,所以最好用exit()终止。

3、线程执行过程中,接收到其它线程发送的“终止执行”的信号,然后终止执行。
也就是pthread_cancel(id);向目标线程发送 Cancel 信号,至于目标线程是否接收该信号,何时响应该信号,全由目标线程决定。类比于进程的kill函数,但是不同的是,调用函数时,并非一定能取消掉该线程,因为这个函数需要线程进到内核时才会被杀掉,所以线程如果一直运行于用户空间,就没有契机进入内核也就无法取消(例while(1){}空循环),此时可以使用pthread_testcancel();进入内核。

进程终止的几种方法
五种正常终止:main函数正常return ,或者exit()函数。调用_exit或_Exit函数(其目的是为进程提供一种无须运行终止处理程序或信号处理程序而终止的方法)。进程的最后一个线程在其启动历程中执行return语句或者pthread_exit().

异常终止:收到信号。比如调用abort。它产生SIGABRT信号,来自自身;ctrl+c (SIGINT信号)来自内核,最后一个线程收到pthread_cancel()信号,来自其他线程。

关于pthread_create()
四个参数,分别是线程id,属性(NULL一般),要执行的函数,传入函数的参数(有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。)并且参数和函数返回值都是void*

如何后台运行程序一系列命令
(&,nohup,bg,fg,jobs -l,crtl+z)
&加在一个命令的最后,可以让这个命令放在后台执行

python test.py &

nohup加在一个命令的前面,不挂断的运行程序,退出终端不会影响程序的运行(终端退出会给bash进程发一个sighup信号,bash给其他进程发,如果没有设置处理sighup的信号处理函数,默认退出。)
nohup python test.py

nohup python test.py &
后台不挂断地运行程序,并且将输出到终端的内容输出到nohup.out

jobs命令
功能:查看当前终端后台运行的程序
jobs-l 可以显示当前终端所有任务的PID,jobs的状态可以是running,stopped,terminated,+号表示当前任务,-号表示后一个任务

查看当前的所有进程
ps -aux

关闭当前后台运行的命令
kill命令:结束进程
(1)通过jobs查看jobnum,然后执行 kill %jobnum
(2)通过ps命令查看进程号PID。然后执行 kill %PID

以及:crtl+z是将进程挂起到后台,停止执行,stopped状态。而&是弄到后台直接运行。

bg %num 是选择一个停止的后台进程继续运行;
fg %num是选择一个后台进程搞到前台运行,比如想要查看运行进度时。

关于快表
我的理解,页表是页号和块号的映射,快表相当于页表的一部分副本缓存。页表在内存中,快表在高速缓存中。用来加速地址变换的过程。
流程是:根据逻辑地址算出页号和偏移。如果快表命中有页号,直接得到块号,块号+偏移直接得到数据,一次访问内存;未命中,访问页表得到块号,再组合得到数据,两次访问内存,并把该页号加入到快表中,应该也有一个淘汰策略。

在执行malloc申请内存的时候,操作系统是怎么做的?
malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。
方式一:申请小于128KB,通过 brk() 系统调用从堆分配内存,将「堆顶」指针向高地址移动,获得新的内存空间.free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用;
方式二:大于128KB,通过 mmap() 系统调用在文件映射区域分配内存;free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。
malloc() 分配的是虚拟内存。
如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。
malloc() 在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。malloc(1) 实际上预分配 132K 字节的内存

为什么不全部使用 mmap 来分配内存?
频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
既然 brk 那么牛逼,为什么不全部使用 brk 来分配?
随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。

中断和异常
中断由硬件产生比如键盘,传给CPU,CPU再通知内核,执行中断处理程序。(保存现场,还需要返回)
异常由软件程序CPU产生,比如缺页异常,除数为0异常,通知内核处理异常。

僵尸进程,孤儿进程,以及解决方案
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。所以危害不大。
僵尸进程就是父进程没有设置子进程的waitpid函数,子进程终止后就变成僵尸进程,因为它的资源没有释放(进程id,中止状态,CPU运行时间等等)
如果父进程是一个服务器进程,一直循环着不退出,那子进程就会一直保持着僵尸状态
解决僵尸进程方法:
1、可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。会由内核init进程回收子进程信息;
2、父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。如果父进程很忙可以用signal注册信号处理函数(处理sigchild信号),在信号处理函数调用wait/waitpid等待子进程退出。
3、两次fork。第一次fork子进程,子进程再fork一个孙进程(真正的任务进程)后退出。孙进程由于子进程退出是孤儿进程,所以会由Init接管。

关系型数据库和非关系型数据库
SQL:关系型数据库指的是使用关系模型(二维表格模型)来组织数据的数据库。(mysql,sqlserver,sqllite,oracle)
关系数据库的优点:
容易理解,符合正常思维方式;都是用表格形式,格式统一,方便复杂查询
完整性约束和事务机制可以很好防止数据冗余,数据不一致的问题。
可以做一些子句的联系多个表的复杂查询支持;
数据存盘,不会丢失。

非关系型数据库又被称为 NoSQL(Not Only SQL ),意为不仅仅是 SQL。通常指数据以对象的形式存储在数据库中,而对象之间的关系通过每个对象自身的属性来决定,常用于存储非结构化的数据
常见的NOSQL数据库:
键值数据库:Redis、Memcached、Riak
列族数据库:Bigtable**、HBase**、Cassandra
文档数据库:MongoDB、CouchDB、MarkLogic
图形数据库:Neo4j、InfoGrid
随着互联网企业的不断发展,数据日益增多,因此关系型数据库面对海量的数据会存在很多的不足。
比如:
面对海量用户连接,无法满足同时连接,响应速度太慢,无法容纳新的数据类型,扩展能力差(不过也有sql集群,主从复制原理:主节点开启binlog记录所有修改事件,从节点开启IO线程假装客户端请求数据,主节点开启dump线程把数据传到从节点relay日志,从节点执行一遍,但是没有redis集群那么完善)
非关系优点:
存储信息的类型很多,不像关系型只有规定那些。比如redis就可以存地理数据GEO类型;
非常快,比如redis是基于内存的,另外就是没有SQL层的解析,没有事务机制的消耗;

缺点:
学习成本高,没有SQL语言支持,比如redis需要lua语言来写脚本;
没有事务,不是很安全;
功能不够完善,复杂查询 不容易实现;
存在内存的话还需要采用一些方式来存盘,如果不是always,会有数据丢失的风险。

mysql数据库的组成部分?一条查询语句的执行过程?
MySQL 的架构共分为两层:Server 层和存储引擎层,
Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等。)都在 Server 层实现。
存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB,从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。

查询一条语句的过程:
1、连接器连接服务器:mysql -h $ip -u $user -p。连接就是先建立TCP连接再看用户密码是否正确;
2、查询缓存,命中直接返回客户端。对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。所以,MySQL 8.0 版本直接将查询缓存删掉了。
3、分析器解析sql语句。词法分析分析关键字,语法分析看是否符合语法;
4、预处理+优化+真正执行。预处理包括检查 SQL 查询语句中的表或者字段是否存在;将 select * 中的 * 符号,扩展为表上的所有列。优化器主要负责将 SQL 查询语句的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。要想知道优化器选择了哪个索引,我们可以在查询语句最前面加个 explain 命令,这样就会输出这条 SQL 语句的执行计划。

为什么要用索引?索引几种形式区别?聚簇索引非聚簇索引?主键索引辅助索引?B,B+树区别?索引失效情况?索引的底层原理(小林coding)
就像书中的目录,就是充当索引的角色,方便我们快速查找书中的内容,所以索引是以空间换时间的设计思想
那换到数据库中,索引的定义就是帮助存储引擎快速获取数据的一种数据结构,形象的说就是索引是数据的目录。可以大大加快数据的检索速度,这也是创建索引的最主要的原因。(当然也有一些其他功能比如唯一索引保证完整性,加速表的连接等)
索引有很多类别:要看从什么角度划分:
按「数据结构」分类:B+tree索引、Hash索引、Full-text索引,B树索引。
按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。

聚簇索引和非聚簇索引
要明确,Mysql的MyISAM和InnoDB,使用的分别是非聚簇索引和聚簇索引。但是二者都是使用B+树作为数据结构的。区别在于,聚集索引的叶子结点数据域存储的是那一行数据本身,非聚集索引叶子结点的数据域存储的是那行数据的地址
1、由于聚簇索引存储的是数据本身,而非聚簇索引只存储指针,因此聚簇索引会占用更多的空间。
如果对非主键稿聚簇索引,那么还需要维护一个辅助索引,如果主键非常长,那么辅助索引将会非常大,很低效。
2、聚簇索引在增删改的时候比非聚簇索引慢很多,因为插入新数据时需要检测主键是否重复,这需要遍历主索引的所有叶节点,而非聚簇索引的叶节点保存的是数据地址,占用空间少,因此分布集中,查询的时候I/O更少,聚簇索引的主索引中存储的是数据本身,数据占用空间大,分布范围更大,可能占用好多的扇区,因此需要更多次I/O才能遍历完成。
综上,聚簇索引适用于主键,查询快,只要一次就行,而非聚簇索引还要根据指针去找;
但是,聚簇索引占用空间大一点,在非主键用聚簇索引还需要维护辅助索引;
最后,聚簇索引对于更新表影响大一点,维护成本更高(因为是数据的移动,I/O更多),非聚簇索引对于更新表影响小一点。

哈希索引:字面意思,就是哈希表的结构,键是列值,值是该行数据的物理地址。但是由于哈希表是单个查询很快,对于范围查询没有办法,而范围查询是很常见的,所以一般数据库不用哈希索引的结构。
全文索引:这也是一个比较独特的索引。它主要用于text的数据类型,比如如果使用普通索引,那么匹配文本前几个字符还是可行的,但是想要匹配文本中间的几个单词,那么就要使用LIKE %word%来匹配,这样需要很长的时间来处理,响应时间会大大增加,这种情况,就可使用时FULLTEXT索引了,在生成FULLTEXT索引时,会为文本生成一份单词的清单,在索引时及根据这个单词的清单来索引。(适用场景有限

组合索引:也就是多个列组成索引。注意组合索引的列不允许有空值。并且符合最左前缀原则,也就是如果组合A,B,C. 那么A,AB,ABC是符合条件的组合,而B,C等就不是。

B树实际上相当于一个平衡搜索多叉树,具体可以自己学习,我只说重点部分。B树每个节点有键、数据、指针三部分。每个节点有2到M个孩子。并且,所有叶子结点都在同一层上,所以说是平衡,这也是为了减少搜索时间(logn)。
B+树作为数据库引擎MyISAM和InnoDB使用的索引结构。它和B树的区别在于以下几点:
1、B+树只有叶子结点带有数据域,其余的只有键和指针。
2、B+树每个节点有M个孩子和M个键。(或者M和M+1,这不重要)
3、B+树在叶子结点添加了指向相邻叶子结点的指针。

为什么B+树比B树更适合作为索引呢?
B+树由于非叶子节点没有数据域,所以能够携带更多的键,所以B+树的层数少,看起来更矮胖一点。那么查询时,B+树所进行的I/O次数更少,因为途中经过每一层,我们都需要进行一次I/O读取一个结点。
由于B+树在叶子结点增加了指向相邻叶子结点的指针,当进行区间查询时,只要沿着指针读取就可以,天然具备排序功能。
最后,谈一下索引什么时候该用,什么时候不该用?
适用场景:
1、经常用于查询或排序的列,比如where, order by中的列需要索引,因为B+索引的查询和排序相对于扫描整张表而言是快的。就像用二叉搜索树找某个数字是log(n)复杂度,而普通数据是O(n)复杂度;
2、主键自动会创建索引;
3、在经常用在连接的列上,这些列主要是外键,可以加快连接速度。
4、用于聚合函数的列可以建立索引,为啥,因为B+树的全文搜索很快,只要在根据叶子结点链表搜下去就可以;

不适用场景:
1、经常增删改的列,因为维护索引结构需要时间和空间代价;
2、不怎么查询的列,因为,索引目的就是为了加速查询,如果不查询,索引就没用,而且维护需要代价;
3、数据量少的列。如果在测试数据库里只有几百条数据记录,它们往往在执行完第一条查询命令之后就被全部加载到内存里,这将使后续的查询命令都执行得非常快–不管有没有使用索引。索引是减少了I/O次数,即在磁盘查询的次数,所以快,在内存中,数据的查询很快,索引意义不大。
4、对于text等很大的数据,不要建立索引,因为聚簇索引中数据太大了,占用空间很大。除非text数据经常用到模糊查询,可以尝试建立全文索引。

总之,索引是在查询性能和修改性能的均衡,因为索引利于查询,不利于修改。

索引优化的方法?
前缀索引优化:使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。但是不能让order by。
覆盖索引优化:建立一个包含查询字段的联合索引。从二级索引中查询得到记录,而不需要通过主键索引查询获得,可以避免回表的操作
主键索引递增:每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。没有碎片。
防止索引失效。
索引失效场景:为什么这些场景会失效
左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。
对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。
联合索引没有正确使用需要遵循最左匹配原则
在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。是因为 OR 的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。

mysql行数据是怎么存储的?
(MySQL 的 NULL 值会占用空间吗?
MySQL 怎么知道 varchar(n) 实际占用数据的大小?
varchar(n) 中 n 最大取值为多少?
行溢出后,MySQL 是怎么处理的?)

创建一个表后,会在 /var/lib/mysql/ 目录里面创建三个文件:db.opt,用来存储当前数据库的默认字符集和字符校验规则。t_order.frm ,t_order 的表结构会保存在这个文件;t_order.ibd,t_order 的表数据会保存在这个文件。
表空间由段(segment)、区(extent)、页(page)、行(row)组成。,InnoDB 的数据是按「页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。默认每个页的大小为 16KB。

行格式:分别是 Redundant、Compact、Dynamic和 Compressed 行格式。现在用Dynamic,下面介绍Compact,因为后面都是根据compact改进的。

记录的额外信息包含 3 个部分:变长字段长度列表、NULL 值列表、记录头信息。
varchar(n) 和 char(n) 的区别是什么,相信大家都非常清楚,char 是定长的,varchar 是变长的,变长字段实际存储的数据的长度(大小)不固定的。所以,在存储数据的时候,也要把数据占用的大小存起来,存到「变长字段长度列表」里面。
要注意的是存放是逆序的,这样可以使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。同样的道理, NULL 值列表的信息也需要逆序存放。
因此,如果可以确定字符长度的,就用char,避免长度列表占用空间。
表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。
当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。
所以在设计数据库表的时候,通常都是建议将字段设置为 NOT NULL,这样可以至少节省 1 字节的空间(NULL 值列表至少占用 1 字节空间)。

记录头信息中包含的内容很多:
delete_mask:标识这条数据是否删除,所以删除一条数据不是马上删除的;
next_record:下一条记录的位置。从这里可以知道,记录与记录之间是通过链表组织的。在前面我也提到了,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。
record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录

记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer,我们来看下这三个字段是什么。

row_id
如果我们建表的时候指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。
trx_id
事务id,表示这个数据是由哪个事务生成的。 trx_id是必需的,占用 6 个字节。
roll_pointer
这条记录上一个版本的指针。roll_pointer 是必需的,占用 7 个字节。实现 MVCC 机制(具体怎么实现?
varchar(n) 中 n 最大取值为多少?
MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节。
如果ascii字符集,一个字符占一个字节。在数据库表只有一个 varchar(n) 字段且字符集是 ascii 的情况下,varchar(n) 中 n 最大值 = 65535 - 2 - 1 = 65532。

行溢出后,MySQL 是怎么处理的?
MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB,也就是 16384字节,而一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的**「溢出页」**中。真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。

事务机制,MVCC机制怎么实现?四种隔离级别?
ACID特性:原子性(一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节)undo log回滚日志实现
一致性(事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态)其他三个满足,这个自然满足
隔离性(多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的)MVCC机制实现
持久性(事务完成后落盘) redo log实现
这次将重点介绍事务的隔离性,这也是面试时最常问的知识的点。

四种隔离级别以及会出现的问题
读未提交(脏读)读已提交(不可重复读),可重复读(幻读MySQL InnoDB 引擎的默认隔离级别),串行
脏读就是读了其他事务修改的未提交的数据,因为可能回滚,所以这个数据是过期的;
不可重复读就是:A事务前后两次读取数据,中间可能有其他事务读取并修改数据,这样A前后读取的数据不一致了;
幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况。

MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。(虽然MVCC就算中间B插入一条数据,A查不到,但是如果A更新这个记录,这个记录的trx_id 就是A的,就能看到了,因此会出现幻读)
针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。(但是如果事务开启后,没有立即用当前读,可能前面select就插入了)
所以最好的办法就是开启事务后立即用select … for update,防止其他事务插入数据。
所以select … for update必须用在事务中,是为了在高并发场景下,对于金钱库存等要求准确性的记录,防止事务中间被其他事务烦扰比如插入数据更新数据。它会加排它锁,阻塞其他线程。

这四种隔离级别具体是如何实现的呢?
对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

那 Read View 到底是个什么东西?
Read View 有四个重要的字段:
m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。
min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
creator_trx_id :指的是创建该 Read View 的事务的事务 id。

那么,记录是否可见取决于:
如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,说明是在read view前提交的,记录可见;
如果大于max_trx_id说明创建read view时事务还没有启动,不可见;
如果在他们之间,如果rx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

我们还需要了解聚簇索引记录中的两个隐藏列。

trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
oll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

**可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。**比如A的活跃列表是51,B是51,52。然后A修改了数据,此时数据记录的trx_id是51,undo日志里连着50原来的数据,就算A提交修改,因为一直用的是启动时的read view,所以认为51是活跃列表中的,所以不能读,顺着undo找到50读。

**读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。**所以当另一事务提交,活跃事务列表就不包含51了。

数据页角度看索引
每一个索引节点就是一个数据页,非叶子节点只存储了页目录,也就是包含的键的范围,在叶子结点找到目标页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。

mysql中的锁机制
按照锁的功能主要是写锁和读锁
按照锁机制分为乐观锁和悲观锁。乐观锁: 对于出现更新丢失的可能性比较乐观,先认为不会出现更新丢失,在最后更新数据时进行比较。用在并发量不大的场景,性能较高。可以用CAS实现(会有ABA问题),版本号机制实现。
悲观锁就是互斥锁独占锁,;可通过select…for update实现。

在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。
全局锁主要用于全库的逻辑备份,一个操作可能涉及几个表。flush tables with read lock
释放全局锁 unlock tables
但是全局锁性能太差,InnoDB 存储引擎默认的事务隔离级别正是可重复读,因此可以采用这种方式来备份数据库。但是,对于 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法。备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 –single-transaction 参数的时候,就会在备份数据库之前先开启事务

//表级别的共享锁,也就是读锁;
lock tables t_student read;
//表级别的独占锁,也就是写锁;
lock tables t_stuent write;
不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁

元数据锁(MDL)
我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL,MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
对一张表进行 CRUD 操作时,加的是 MDL 读锁;
对一张表做结构变更操作的时候,加的是 MDL 写锁
当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。
MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。所以长事务时看看是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。

意向锁
意向锁的目的是为了快速判断表里是否有记录被加锁。在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」。如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。
AUTO-INC 锁
用于主键字段自增,执行完插入语句后就会立即释放。。在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。一样也是在插入数据的时候,会为被 AUTO_INCREMENT 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。

InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
普通的 select 语句是不会对记录加锁的,因为它属于快照读。如果要在查询时对记录加行锁,可以使用下面这两个方式,这种查询会加锁的语句称为锁定读。
//对读取的记录加共享锁
select … lock in share mode;
//对读取的记录加独占锁
select … for update;
上面这两条语句必须在一个事务中,因为当事务提交了,锁就会被释放
行级锁的类型主要有三类:
Record Lock,记录锁,也就是仅仅把一条记录锁上;
Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身

Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的。也就是最常用的那两个锁了。

Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。
Next-Key Lock 假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。

插入意向锁
一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。
加锁规则
select是不会加锁的通过MVCC机制实现,update,delete等更新操作会加锁,select…for update会加锁
两个原则
加锁的基本单位是next-key lock
查找过程中访问到的对象才会加锁
两个优化
索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁
索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁
在线上在执行 update、delete、select … for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。所以这也是为什么要索引?索引不仅会通过B+树结构加快查询速度。而且对于加锁的操作来说,通过索引只会锁住一行,其他线程可以修改表,全表扫描的话会锁住整个表。

mysql的死锁
务A先获取到id=1的行锁,然后事务B获取到id=2的行锁;
接着事务A要获取id=2的行锁,发现被事务B持有,阻塞;
事务B要获取id=1的行锁,发现被事务A持有,阻塞;
两个事务进入死锁状态。
当出现死锁后,有两种处理策略:死锁解除(互斥、占有且等待、不可强占用、循环等待
直接进入等待,直到连接超时,超时时间可通过innodb_lock_wait_timeout设置。
发起死锁检测,发现死锁后主动回滚死锁中的一个事务,让其他事务正常执行。将参数innodb_deadlock_detect设置为on,表示开启死锁检测。

mysql中的buffer bool

mysql中undo log ,redo log ,binlog作用
因此,undo log 两大作用:
实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
redo log
实现
事务的持久性
,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。
解释一下:
我们知道mysql在内存里有缓存池的,开启时会申请一段空间给缓存池,所以开始虚拟空间用的很多但是物理空间很小。当有数据更新,会先更新缓存池,然后找个时间落盘,但是内存不稳定,崩溃之后更新丢失了。所以有了redo log.
WAL技术,就是MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。
InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,在事务提交时,只要先将 redo log 持久化到磁盘即可。写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。
(注意一点,不是每次提交事务都会将 redo log 持久化到磁盘,效率太低了,redo log也是有buffer的,会有合适的时机,所以完全可靠和高速度是不可兼得的)
redo log文件写满了怎么办:redo log是环形的,当buffer pool脏页落盘后,redo log那一部分就没用了,所以InnoDB 用 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置。

为什么需要 binlog ?
前面介绍的 undo log 和 redo log 这两个日志都是 Innodb 存储引擎生成的。
MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。
binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。
最开始 MySQL 里并没有 InnoDB 引擎,MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。
而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用 redo log 来实现 crash-safe 能力。
binlog主要用于主从复制的,redo log因为边写边擦除没有全量日志,不能用来做备份。主从复制原理:首先主库开启binlog,所有更新操作会写到binlog;从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应;从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

文件系统知识汇总
首先,一个开胃的问题:硬链接和软连接区别
ln -s 原文件名 链接文件名
硬链接其实是给文件起了一个别名,也就是新建了一个目录项,只不过目录项中的索引节点指针是同一个。硬链接会让连接数加1,防止误删文件。但是它不能跨文件系统(因为不同系统就算索引节点一样,块号变了,找不到)
软连接其实是新增了一个索引节点也就是新增了一个文件,只不过这个文件的内容是原来文件的地址(索引节点的地址),相当于快捷方式,删除链接文件对源文件没有影响。可以跨文件系统。但是源文件路径改变,软连接就找不到了。

可能对于上面不太懂,学了文件系统就懂了。
Linux 文件系统会为每个文件分配两个数据结构:索引节点(index node)和目录项(directory entry),它们主要用来记录文件的元信息和目录层次结构。
索引节点,也就是 inode,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间。
目录项,也就是 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存
(目录是文件的一种,保存在磁盘,目录项是为了加快查询速度,会把目录或者文件缓存在内存)
磁盘读写的最小单位是扇区,扇区的大小只有 512B 大小,很明显,如果每次读写都以这么小为单位,那这读写的效率会非常低。所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为 4KB,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。

文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)。VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。

磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的 /proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据。
网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录

文件系统的作用:组成就是索引节点,目录项,磁盘物理块。为不同的文件系统组织统一接口。我们只要open,write,read,close。就可以。系统会为你分配一个fd文件描述符指定唯一的文件(索引节点以及指向的物理块).然后你写都是字节流形式,文件系统会为你自动分配物理块写,中间的差异文件系统屏蔽了。
为存储持久化,读取相关数据提供统一的接口。屏蔽字节流到数据块的转换细节

文件的存储方式
不同的存储方式,有各自的特点,重点是要分析它们的存储效率和读写性能
连续存储方式:需要在索引节点为每个文件定义文件起始位置和文件长度,文件紧密排列。这种方式读写效率很高,因为只要一次磁盘寻道时间就可以了。
但是有两个致命缺点:容易形成碎片,要想在磁盘移动紧凑空间不现实太慢了;第二就是文件长度固定不容易扩展,要想文件加点东西就要移动,很慢。反正就是文件长度不能增加减少,显然不符合现实需要的。

非连续存储:
非连续空间存放方式分为**「链表方式」和「索引方式」。**
链表有隐式链表和显式链表之分。隐式链表就是平常看到的,每个数据块有一个指针指向下一个数据块,索引节点存储第一块」和「最后一块」的位置。缺点是只能通过链表顺序访问,没办法直接访问了,而且指针会销毁磁盘空间,损坏的话就相当于丢失了。
显式就是在内存里保存一个文件分配表FAT,该表在整个磁盘仅设置一张,每个表项中存放链接指针,指向下一个数据块号,以一个不属于有效磁盘编号的特殊标记(如 -1 )结束。在内存中,因而不仅显著地提高了检索速度,而且大大减少了访问磁盘的次数。但也正是整个表都存放在内存中的关系,它的主要的缺点是不适用于大磁盘。

索引的实现是为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表,说白了就像书的目录一样,要找哪个章节的内容,看目录查就可以。另外,文件头需要包含指向「索引数据块」的指针,这样就可以通过文件头知道索引数据块的位置,再通过索引数据块里的索引信息找到对应的数据块。
这种方式优点:可扩展,没有碎片(对于连续分配)支持随机访问(对于链表)唯一的缺点是索引数据块也有空间消耗,但是可以忽略。如果文件很大一个块都装不下,就会有二级索引,三级索引出现。

空闲空间管理
前面这些都是对已经分配的空间进行管理。那么如果我要保存一个数据块,我应该放在硬盘上的哪个位置呢?难道需要将所有的块扫描一遍,找个空的地方随便放吗?
**空闲表法 :对于连续分配适用。一般不用

空闲链表法:不支持随机访问,指针消耗
位图法:利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。当值为 0 时,表示对应的盘块空闲,值为 1 时,表示对应的盘块已分配。

键盘敲入A 字母时,操作系统期间发生了什么吗
那要想知道这个发生的过程,我们得先了解了解「操作系统是如何管理多种多样的的输入输出设备」的。

为了屏蔽设备之间的差异,每个设备都有一个叫设备控制器(Device Control) 的组件,比如硬盘有硬盘控制器、显示器有视频控制器等。设备控制器里有芯片,它可执行自己的逻辑,也有自己的寄存器(数据,状态,命令),用来与 CPU 进行通信。
另外, 输入输出设备可分为两大类 :块设备(Block Device)和字符设备(Character Device)。
块设备,把数据存储在固定大小的块中,每个块有自己的地址,硬盘、USB 是常见的块设备。
字符设备,以字符为单位发送或接收一个字符流,字符设备是不可寻址的,也没有任何寻道操作,鼠标是常见的字符设备。
块设备通常传输的数据量会非常大,于是控制器设立了一个可读写的数据缓冲区。

IO控制方式:设备如何告诉CPU数据准备好了,轮询肯定不行;中断可以;对于频繁IO的设备经常中断影响CPU性能,DMA控制器方式,DMA完成把数据放到对应内存,再告诉CPU。
设备驱动程序
虽然设备控制器屏蔽了设备的众多细节,但每种设备的控制器的寄存器、缓冲区等使用模式都是不同的,所以为了
屏蔽「设备控制器」的差异,引入了设备驱动程序。

1、键盘控制器先把数据缓存在数据寄存器中,然后通过IO总线发送中断请求;
2、CPU 收到中断请求后,操作系统会保存被中断进程的 CPU 上下文,然后调用键盘的中断处理程序
(键盘的中断处理程序是在键盘驱动程序初始化时注册的,那键盘中断处理函数的功能就是从键盘控制器的寄存器的缓冲区读取扫描码,再根据扫描码找到用户在键盘输入的字符,如果输入的字符是显示字符,那就会把扫描码翻译成对应显示字符的 ASCII 码,把 ASCII 码放到「读缓冲区队列」,接下来就是要把显示字符显示屏幕了)

memcpy,strcpy,strncpy区别和效率
strcpy和memcpy区别。

  1. 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
    从参数也可以看出来,char* strcpy(char* dst,char* src) void* memcpy(void* dst,void* src,ssizet size);void*可以代表任何类型;
  2. 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度,如果长度超出了原来的数据长度,就会发生内存越界。

效率:看网上的代码,二者是一样的,但是源码实现不一样。strcpy是按字符一个字节一个字节复制的,而memcpy最低也是按照机器字长word复制的(4或8字节),而且对于处于内存对齐的位置可以优化成按页复制,所以更快。
strncpy就是更安全,因为可以防止内存溢出。

内存泄漏,内存越界怎么检查?
内存安全问题:内存重复释放,越界访问等。
C++中内存泄漏(主要指堆内存,当然也有socket等其他资源)很常见,因为没有垃圾回收需要自己处理,而malloc允许自己控制内存,即使你是经验老到的程序员,一些STL和其他库中涉及很多内存操作,很难防范。
1.阅读代码检查。要知道编写规范,比如哪些场景会出现内存泄漏(析构函数不用虚函数,忘记free,free[],构造函数抛出异常,浅复制一个含有指针的对象造成内存重复释放,用智能指针代替原始指针,free后要指针设为NULL

怎么解决:
1、可以借助工具比如valgrind定位内存泄漏,但是这个运行内存较大,不适合在虚拟机跑,当然也有其他的软件;内存泄漏其实不算内存安全问题;
2. 内存越界如果马上崩溃的话比较好查,写个backtrace模块,捕捉越界信号打印堆栈,有的工具tengine就是利用这个原理;
3. 如果越界不立刻崩溃的话就不好查(比如缓冲区溢出),可以在一些内存操作比如memcpy,malloc函数hook一下,测试的时候加一层日志。

更多推荐

计算机基础面经积累---持续更新

本文发布于:2023-04-30 08:21:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/12b0f95e74a98065dddca3ab96294dbb.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:计算机   基础

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 113589文章数
  • 28826阅读数
  • 0评论数