1. 左值&右值&左值引用&& 右值引用
int a = 9
a就是左值,9就是右值
对于左值 可以多次使用,赋多个指
右值 就是作为值去给别人赋值
左值引用是用来引用左值的;右值引用时用来引用右值的
例如 :int&&c = 9
编译器在取右值引用的,会先用一个临时变量存储右值,然后再使用临时变量对这个 c 进行初始化,临时变量是有地址的
左值引用只能引用左值,右值引用只能引用右值(暂不考虑const)
对于使用 new 这种方式在堆上定义出的右值,我们想让这种就使用一次的内容可以不要再作为右值的时候被重复多次使用,每次用都开辟一次空间。那么,就可以采用将这个类实例强转为 右值引用类型,并在类中实现参数为右值引用的函数,就可以不重复在堆上开辟空间多次存放这些同一个内容的右值了
而所谓的 std::move()函数就是一个将变量强转为右值变量的墙砖函数罢了,因为若一个类的实例,默认他是一个左值,需要被强转为右值类型才可以使用右值的相关函数
3. 拷贝构造函数、移动构造函数
4. const 关键字,static关键字
1. 内存泄漏&&如何检测和避免 && 智能指针的使用:
(1)什么是内存泄漏:
内存泄漏是指在程序运行过程中,我们分配了某个大小的内存块,在使用完毕时没有及时释放。导致该内存块无法再次被程序使用,致使程序运行变慢或因内存耗尽而崩溃。而我们往往在程序中可以被程序员分配的内存空间是堆上的空间,因此,内存泄漏的全称应该是 堆内存泄漏。
(2)内存泄漏如何避免:
1. 尽量使用智能指针而非普通的指针来指向申请的内存空间
2. 对于类的析构函数,定义为虚函数
3. 对于指向数组的指针,delete时采用 delete p[]的方式释放
int * p = new int[5];
delete[] p;
为什么 用 delete[] ?
==》是因为 delete[]是与delete不同的函数,delete[]会调用析构函数很多次,而delete会调用析构函数仅一次;
同时会出现两个函数delete 和 delete[] 的原因是:
C++ 指针无法判断出指针指向的数据类型到底是一个 元素还是一组元素)
不可以对一块内存块 delete多次,但是可以对一个空指针delete多次
4. 如果非要使用new,malloc那么一定记得成对使用delete 和 free
(3)如何检测内存泄漏:
Linux下我一般使用的是Valgrind,它的作用是去追踪new malloc delete free 的操作,最后产生一个报告来警告提示使用者是否有内存的泄漏。
Valgrind的具体实现是:在程序运行的时候创造一个虚拟机,在虚拟机中,Valgrind会替换掉new malloc delete free 函数为自己的实现,而方便记录程序对内存的申请和释放,在程序运行结束后就可以提供相应的报告警告程序员
2. 智能指针
智能指针本身不能是动态分配的,是在栈上分配智能指针,让它指向堆上动态分配的对象,这样就能保证智能指针所管理的对象能够合理地被释放。(因此,智能指针在对应作用域结束时会被销毁,它管理的资源也会被释放掉。)
在C++11中常用两种指针:shared_ptr和unique_ptr和weak_ptr ,定义在头文件 <memory>中
(1)shared+ptr :
shared_ptr保证了当多个shared_ptr可以指向同一个内存空间,在不存在shared_ptr指向一块内存空间时内存空间只被释放一次:即,当最后一个指针指向某块内存空间时,会在这块指针指向其他位置后将该内存空间释放掉
【注意】:智能指针会在定义其作用域结束的时候释放掉当前所指的对象,调用一次析构函数release(),若此时智能指针所指的对象若无指针指向,那么释放掉这块内存空间。
使用方法是:
shared_ptr<int> sp = new int (2);
shared_ptr<int> sp2 = sp;
cout<< sp2.use_count(); //输出当前指针指向的内存块有多少指针指向
(2)shared_ptr的实现方法:
通过在类中定义 :
一个模板类型的指针;
一个计数器pcount用于标识有多少指针指向该内存块;
一个锁变量用于保证在进行pcount加减操作时其他的线程不会对pcount造成冲突影响
实现两个函数:AddCount() 和 Release() :
AddCount()所实现pcount通过加锁实现线程安全的加1操作,表明指向该块内存的指针数目+1;
Release()所实现的pcount通过加锁实现线程安全的减1操作,同时判断pcount值是否为0,若为0则释放该块内存
==》实现 构造函数,析构函数,拷贝函数,移动函数【?】 重载=运算符,
当给智能指针赋初值时 调用 AddCount()
给智能指针重新赋值时,调用 AddCount()表明指向新的内存指针数目+1 和 Release() 表明旧的内存块被指向的指针数-1 ;
当智能指针超出其作用域时,调用Release()释放指向的空间
(3)shared_ptr会造成循环引用错误:
一个典型的例子就是,对于双向链表,节点a的next指针指向节点B,节点B的pre指针指向节点A
若这两个指针都是shared_ptr类型,那么,节点A B超出作用于考虑释放时,就会出现:节点A想要释放,但是由于节点B的pre指向节点A,因此节点A无法释放,而节点B想要释放,节点A的next指针指向节点B,节点B无法释放,出现了循环,导致节点AB的空间无法释放,造成内存泄漏
==》如何解决:对于双向循环可以使用 weak_ptr 作为 pre和next 的指针类型:
(4)weak_ptr :
weak_ptr 指向一块内存不会对引用计数产生影响,weak_ptr不参与根据引用计数进行的内存释放(即,当shared_ptr打算对一块内存释放时,不会考虑是否存在shared_ptr指向该内存块)。
作用类似于一个普通的指针,但是与普通指针不同的是,weak_ptr可以通过调用lock()函数监测出指针指向的对象&内存是否被释放,从而避免非法访存
(5)auto_ptr :
这个指针时C++98采用的,现在的C++11标准已经弃用,这是auto_ptr采用的是copy语义,正常我们拷贝就拷贝,不会把原来的指针重新置为NULL,但是auto_ptr就是这么实现的,他的目标是想保障只有一个指针独享一块内存,但是实现得很糟糕,不符合人们认知,被丢弃掉了。
(6)unique_ptr :
保证同一时间内只有一个智能指针可以指向该对象。
实现方式是:unique_ptr禁用了拷贝构造和拷贝赋值构造,仅仅实现了移动构造和移动赋值构造,这也就使得它是独占式的( 即对于 = 运算符的重载和拷贝构造函数都标记为 delete )
==》非要实现赋值操作需要使用移动语义move() 函数:move()后源指针值为NULL
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 不允许
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 允许
##############################################
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
若手动实现了拷贝和复制,那么,多个指针指向同一块内存,在析构时,每一个unique_ptr都会析构指向的内存,就会出现同一块内存被多次释放致使程序崩溃。
3. 野指针 && 悬挂指针:
1. 什么是野指针:
正常的指针会指向一块明确的内存单元,而野指针会指向一块随机的、未知的内存单元
2. 野指针产生的原因:
(1)定义指针没有初始化
(2)释放指针指向对象后没有对指针进行初始化为 nullptr
当 : int *p = new int(3); 时新建了一个对象
delete p; 时只是释放了这个int类型的变量空间
【注意】此时的指针p 这个int* 型的变量并没有被释放,p中还有自己的初始值,值的内容就是已经被释放不存在的int变量的地址
因此,若不将变量p赋值为nullptr,那么,p就会变成一个野指针,若不处理直接使用户造成安全问题(指针p只有在程序运行结束才会被释放的哇,因为指针p是一个栈上的变量)
(3)超出指针作用域使用指针:
在函数中定义了一个指针,并把该指针作为返回值返回。由于函数调用结束,分配给函数的堆栈会被释放,而定义在函数内的指针指向的对象空间也会被释放。返回的指针指向的内容是一块未知的随机空间
==》因此不要返回函数中定义的堆栈空间上的变量的指针或引用
3. 野指针的危害:
(1)指向不可访问的空间,会直接造成对段错误
(2)指向可访问的随机空间,程序不会出错,但是又与对应逻辑错误会导致运行结果错误
(3)指向可访问的,正被其他进程使用的空间:此时对该内容进行修改会导致其他进程对这块数据的使用出错
3. 为什么C++中没有垃圾回收:
因为若实现垃圾回收需要有额外的空间开销和时间开销,例如:需要记录指向当空间的变量数和标记值对应的空间开销;当一个空间被释放的时候需要开新的线程执行free操作
1. 介绍一下进程线程的区别:
2. 死锁是什么,怎么解决:
4. 孤儿进程,僵尸进程 && 怎么解决
1. 零拷贝:
1. 介绍一下内核态、用户态
用户应用通过发行版(Ubuntu、CentOS)操作Linux内核,Linux内核与计算机硬件相交互
Linux内核通过各种管理系统例如 virtual file system 来使用计算机硬件的驱动,来控制计算机硬件。并且给上层的应用暴露出可供使用的接口
因为系统说白了也是一个应用,上层的MySQL啥的也是一个应用,是应用在运行过程中就会消耗内存空间,需要占用一定的CPU资源。如果不加区分,上层的应用如MySQL啥的抢占了内核的内存空间,就会导致我们系统崩溃了。
为了避免这种情况,于是将进程的地址空间划分为了2个区域:用户态和内核态
因为无论内核进程还是具体上层应用进程,都无法直接访问物理内存。是给这些进程分配虚拟的内存空间,映射到真实的内存空间。
应用或内核在访问虚拟内存空间时,就需要对应的虚拟地址,这个地址是一个无符号的整数。最大值取决于CPU地址总线和寄存器带宽。例如32位的系统,带宽是32,因此,他的地址最大值就是2^32。因此,虚拟地址的寻址空间就是0-2^32。
内存地址的每一个值代表一个存储单元,也就是一个字节。因此,虚拟寻址空间大小2^32字节就是4GB
操作系统是给每一个字节1Byte分配一个地址,一个字节有8bit
现在有2^32个地址,对应2^32Byte :
由于 1KB=2^10Byte 1MB=2^10KB 1GB=2^10MB (1 GB=2^30 B)
==》因此,2^32 B = 2^2 * 2^30 B = 4GB
我们把虚拟地址这4G的空间划分:用户空间占3G,内核空间占1G。
同时,也把操作划分为不同的等级:R0最高,R3最低
用户空间只能执行R3级别的命令,不能直接调用系统资源,必须使用内核提供的接口才能访问系统资源。内核空间中可以执行各种
用户应用需要执行普通的命令,在用户空间中执行,也需要调用系统资源,进程就需要调用内核接口,执行内核中的指令,当进程涉及的操作指令在内核运行时,就称之为内核态,在执行用户空间中的指令,就called用户态。一段程序很有可能就会出现进程在用户空间和内核空间中切换。
以IO访问为例(无论是访问的磁盘,还是访问的是网卡,都是与物理设备与内存间的交换数据):
用户区的缓冲区主要起到一个IO流的缓冲:不然来一个数据就写一个数据到内核去操作复杂效率低。
内核空间的缓存:作用是若一次用户的请求在内核缓存区中,那么可以直接读过来。
以数据写入到磁盘为例:数据准备好了房子用户态的缓冲区中,接着吧用户空间的缓冲区中数据拷贝到内核态空间中。然后再讲内核缓冲区等数据写入到磁盘中。
以数据从磁盘中读取数据为例:用户空间发起读请求,调用内核中的接口,调用在内核空间执行的指令,内核的指令等待磁盘中数据的到来,当数据到来后,将数据从磁盘读到内核区的缓冲区中,然后将数据从内核的缓冲区拷贝回用户的缓冲区。这些数据在用户的缓冲区中被处理。
可以看出,进程执行过程中,两个地方耗时比较多:一个是内核指令等待从磁盘或网卡中响应的数据到来,第二就是数据从内核到用户区的拷贝
1. 虚函数:
C++面试必备之虚函数:C++面试必备之虚函数 - 知乎
2. C++的内存管理:
【C++初阶】第七篇——C/C++的内存管理(C/C++动态内存分布+new和delete的用法和实现原理)_c 动态内存 new_呆呆兽学编程的博客-优快云博客
3. C++STL的容器的底层实现详解
C++STL的容器的底层实现详解_c++stl底层实现_qq_43313035的博客-优快云博客
# 布隆过滤器:
什么是内存泄漏?有什么危害?_内存泄漏会导致什么后果_赵鹏翔的博客-优快云博客
一致性哈希算法(consistent hashing) - 知乎 (zhihu.com)
面试必考 | 进程和线程的区别 - 知乎 (zhihu.com)
TCP与UDP的区别(超详细)_tcp协议和udp协议的区别_董HaoYu的博客-优快云博客
4. 启动计算机&&服务器的启动流程:
启动一台计算机的流程可以大致分为以下几个步骤:
1. 电源接通:将计算机的电源插头插入电源插座,打开电源开关,使计算机进入待机状态。
2. 自检(POST):当计算机电源接通后,计算机会进行自检,检查计算机硬件是否正常,包括主板、内存、硬盘、显卡等硬件设备。
3. BIOS启动:自检完成后,计算机会启动BIOS(Basic Input/Output System),BIOS是计算机的基本输入输出系统,它负责检测计算机硬件设备,并将它们初始化,以便操作系统能够使用它们。
4. 引导程序加载:BIOS会在计算机启动时读取硬盘上的引导程序(Boot Loader),引导程序会加载操作系统内核,并将控制权交给操作系统。
5. 操作系统启动:操作系统内核加载完成后,操作系统开始启动,进行初始化和配置,最终进入用户登录界面或桌面环境。
6. 用户登录:在操作系统启动后,用户需要输入用户名和密码进行登录,登录成功后,用户可以开始使用计算机。
5. 磁盘感知:
探知一个磁盘是否可用可以通过以下几种方式:
1. 硬件检测:使用硬件检测工具,如CrystalDiskInfo、HD Tune等,检测硬盘的健康状态,包括硬盘的温度、读写速度、坏道等信息,以判断硬盘是否可用。
2. 操作系统检测:在操作系统中打开磁盘管理工具,查看硬盘的状态和分区情况,如果硬盘状态为“健康”,并且分区正常,那么硬盘就是可用的。
3. 读写测试:使用读写测试工具,如CrystalDiskMark、ATTO Disk Benchmark等,对硬盘进行读写测试,以检测硬盘的性能和稳定性,如果测试结果正常,那么硬盘就是可用的。
需要注意的是,如果硬盘出现了故障或者损坏,那么以上方法可能无法检测出来,因此在使用硬盘时,要定期备份数据,以防数据丢失。
5‘。计算机如何检测硬件是否正常:
计算机检测硬件是否正常的方法主要有以下几种:
1. 自检(POST):计算机在启动时会进行自检,检测硬件是否正常。自检主要检测计算机的主板、内存、硬盘、显卡等硬件是否正常。如果自检过程中发现硬件故障,计算机会发出警报声并停止启动。
2. 硬件监控:计算机可以通过硬件监控软件来检测硬件是否正常。硬件监控软件可以监测CPU、内存、硬盘、显卡等硬件的温度、电压、风扇转速等参数,如果这些参数超出了正常范围,软件会发出警报。
3. 诊断工具:计算机可以使用诊断工具来检测硬件是否正常。诊断工具可以检测硬件的性能、稳定性和可靠性,并提供详细的测试报告和建议。
4. 操作系统自带的工具:操作系统自带的工具也可以用来检测硬件是否正常。例如,Windows操作系统自带的设备管理器可以检测硬件设备是否正常工作,而Linux操作系统自带的lspci命令可以列出计算机的硬件设备信息。
总之,计算机可以通过自检、硬件监控、诊断工具和操作系统自带的工具等方式来检测硬件是否正常。
6. 磁盘初始化的过程:
磁盘初始化是指在使用新的硬盘或者重新格式化硬盘时,对硬盘进行分区、格式化等操作的过程。具体步骤如下:
1. 连接硬盘:将硬盘连接到计算机的主板上,可以通过SATA、IDE等接口连接。
2. 进入BIOS:开机时按下相应的键进入BIOS设置界面,选择硬盘启动顺序,确保计算机能够从硬盘启动。
3. 分区:使用磁盘管理工具对硬盘进行分区,将硬盘划分为一个或多个分区,每个分区可以独立使用。
4. 格式化:对每个分区进行格式化,选择文件系统类型(如NTFS、FAT32等),确定分区大小、分区名称等。
5. 完成:完成分区和格式化后,就可以在操作系统中使用硬盘了。
需要注意的是,在进行磁盘初始化操作时,要谨慎操作,确保数据安全。如果硬盘中已经有重要数据,需要先备份数据再进行操作。
7. 在系统中打开文件的过程:
打开文件在系统中通常包括以下过程:
1. 应用程序向操作系统发出打开文件的请求。
2. 操作系统检查文件是否存在,并且应用程序是否有足够的权限来打开该文件。
3. 如果文件存在并且应用程序有足够的权限,操作系统会为该文件创建一个文件句柄,该句柄包含了文件的元数据和指向文件数据的指针。
4. 操作系统将文件句柄返回给应用程序,应用程序可以使用该句柄来读取或写入文件数据。
5. 如果应用程序需要读取文件数据,它可以使用文件句柄中的指针来访问文件数据。
6. 当应用程序完成对文件的操作时,它会向操作系统发出关闭文件的请求,操作系统会关闭文件句柄并释放相关的资源。
在不同的操作系统中,打开文件的过程可能会有所不同,但通常都包括上述步骤。
(一)select、poll、epoll 和区别:
1.介绍下select :
select是最早的IO多路复用的实现方案。通过对3个数组对文件描述符上可能发生的三种事件:读事件、写事件、异常事件进行监听。源码中,这三个数组中的每个bit位来标记一个文件描述符,且固定了数组大小:1024位,因此,select最多监听的文件描述符只有1024个;同时select中还有一个用于记录阻塞时间的变量,可以用于控制select的阻塞时长是一直等待阻塞还是完全不阻塞或者等候多少秒后未收到事件就不继续阻塞。
那么,select的IO多路复用流程是:首先将想要监听的文件描述符记录在fd_set中,对应的fd置为1,接着,执行select()函数,此时fd_set还处于用户空间,select()函数会将其拷贝至内核空间。在内核空间,操作系统通过轮训去查看是否有文件描述符出现了相应的事件。若出现,则将出现事件的个数返回,同时,将fd_set中发生事件的文件描述符置为1,并拷贝回用户空间。
通过这个过程,可以看出,select()虽然实现了IO多路复用,但是存在着以下的缺点:
首先是:fd_set需要从用户空间拷贝到内核空间,并从内核空间再次拷贝回去。这个拷贝的动作因涉及内核态与用户态的切换,因此相当耗时。其次,当select监听到时事件发生,他只会将有几个事件发生这个数值回传,至于具体是哪几个文件描述符发生了事件,还要将循环查找fd_set看哪个bit位被置为1。同时,由于fd_set从内核态获得到了事件信息,拷贝回用户态,就会将之前的用户态内fd_set覆盖,然而我们需要一次次调用select()来监听各个文件操作符的事件状态,因此,这相当于每次调用前我们都需要重新初始化一遍fd_set定义好哪些事件要被监听,操作复杂。最后,就是由于当时的源码实现,只能令select()监听1024个文件描述符,这个对于当下的高并发有些不够,因此也需要改进。
2. 介绍下poll :
poll 同样是IO多路复用的一种实现,他针对select()的几个不足做出了调整:他将select方法中的fd_set数据结构改变为结构体类型的数组,数组中的每个结构体用于标识一个文件描述符,包含的信息有该文件描述符的fd,要监听的事件信息,以及要被内核写入的真正监听到的事件。
poll的IO流程同样是:创建poll的数组fd_set,存放要监听的文件描述符信息,调用poll函数,将fd_set数组传入内核态,在内核态也是以链表的方式存放这些结构体。当内核轮训发现有事件发生,将发生事件的个数返回,并将fd_set从内核态拷贝回用户态。由于返回的依旧是发生事件的个数,因此,还是需要代码实现遍历fd_set才可以知道到底是哪个文件描述符发生了什么时间。
总结一下,与select相比,fd_set能监听的文件描述符个数不局限于1024了,但是,由于采用链表作文文件描述符的存放的数据结构,这导致当监听的内容过多时,会导致轮询链表时间越来越长,导致性能变差。同时,由于指定监听什么事件和真实监听到的事件用两个数组存储,因此,不会再有需要每次调用前重新初始化监听fd_set的操作了。然鹅,关键的fd_set从用户态到内核态的拷贝移动、返回值只告诉使用者发生事件的文件描述符个数,而不告诉具体哪些事件发生变化 这些导致的性能问题依旧没有解决。
3. 介绍下epoll :
epoll 同样是IO多路复用的一种实现,但epoll解决了刚刚说到的关于select 和 poll 的问题。
这是因为它的实现方式:首先,epoll不再将监听的文件描述符存放在用户空间,而是直接通过epoll_ctl(ADD)加入到内核空间,其次,在内核空间的文件描述符也不再以链表形式存放,而是被组织为了一颗红黑树。这样的组织形式使得即使监听的文件描述符个数增长也不会对性能有太大的波动。同时,epoll还在内核态维护了一个链表,用来存放监听到发生事件的文件描述符,这样就可以明确的告知调用者那些文件描述符发生了事件,而无需调用者再次通过代码遍历一遍全部的fd_set才能知道谁发生了改变。
具体来说,使用epoll的过程是:首先通过epoll_create()在内核区开辟空间存放epoll实例,将要监听的文件描述符通过epoll_ctl(ADD)作为一个节点,加入epoll实例组织的红黑树中。而后,通过调用epoll_wait()要求内核开始轮询监听红黑树上的文件描述符上是否有事件发生,若有事件发生,那么,会调用回调函数,将该事件(以epoll_event结构体形式)加入到epoll实例的rdlist链表中。当调用epoll_wait()时,只会访问这个rblist链表,若链表为空则按照epoll的参数阻塞等待,或者不阻塞等等。当有事件发生,epoll_wait()检测到rblist链表非空,则将该链表内容从内核态拷贝回用户态,供用户态的程序处理。
总结一下:相较于select\poll,epoll相当于把select/poll的功能拆分,通过epoll_ctl()函数对插入要监听的文件描述符,这个从用户态到内核态的过程只有一次;同时,通过调用epoll_wait()直接将已经被监听到事件的从内核态拷贝回用户态,相较于select\poll会将全部的文件描述符集合拷贝回用户态,一来减少了拷贝的内容,二来由于直接将那些文件描述符发生事件返回给调用者,也避免了用户态代码遍历fdset一遍才能知道到底谁发生了事件。
@@同时,调用epoll_wait()会有两种通知用户事件发生的方式:LT和ET
LT:只要FD中有数据可读,且调用了epoll_wait(),那就会发出信号告知有事件有数据,会重复通知,直到FD中的数据被读光
ET:FD中有数据可读,且调用了epoll_wait(),那就会发出信号告知有事件有数据,只会通知一次,直到有新数据到来
实现的方法就是,将发生时间的FD从rblist中截取下来,查看是ET还是LT模式,若是LT,那么会查看数据有没有读光,没有读光就会把这两个FD重新加回到rblist,若是ET模式,无论是否数据读完都不会将FD添加回去。因此采用ET模式应选择非阻塞读,把所有到达的数据一次行全读走。(注意,一定要用非阻塞,阻塞的话会导致没数据就在那里等待,出问题了嘛这不)
LT模式可能会出现惊群现象,由于数据存在而不断发出通知,多进程或多线程情况下,然而只会有一个进线程能获取到内容去处理,这种频繁的选取一个进线程会导致CPU繁忙,效率不高
ET模式 可以结合非阻塞IO,一次性读走处理FD上的全部内容
Linux惊群效应详解(最详细的了吧)_戴着眼镜看不清的博客-优快云博客 【有时间好好看一下】
同时,也想补充一下,为什么要用红黑树来组织监听的文件描述符?
1.为他妈的要用红黑树不用哈希表:
为什么epoll使用红黑树来管理文件描述符,而不是哈希表? - 知乎
4. 总结下 select\poll\epoll 三者的区别:
1.为他妈的要用红黑树不用哈希表:
为什么epoll使用红黑树来管理文件描述符,而不是哈希表? - 知乎
1. Ubuntu 和 centOS 和 Linux 关系:
1. 五种IO模型:
IO的读写主要就分为两个阶段:一是等待数据从硬件(如磁盘、网卡)上就绪,写入到内核缓冲区,二是将数据从内核缓冲区拷贝回用户缓冲区
不同的IO模型就是在这两个阶段上处理的差别:分别是 阻塞IO、非阻塞IO、IO多路复用、信号IO和异步IO。我们分别来说下
(1)阻塞IO:阻塞IO就是在这两个阶段都一直阻塞等待:用户态发出读数据请求,通过系统调用要求内核读取数据,但是数据没有到达内核缓冲区,于是就一直阻塞等待。终于数据到达了磁盘,数据从磁盘中拷贝回内核态,内核态拷贝回用户空间。在这一整个过程直到数据拷贝回用户空间,用户态的函数一直在阻塞等待。
(2)非阻塞等IO:
与阻塞等待不同的是,当用户态请求发出要调用内核态的函数接口,发现数据不在内核态的缓冲区中,那么,此时用户态的函数不等待,直接返回无数据。
之后不断向内核发出请求,如果内核态依旧缓冲区无数据,那就同样不阻塞等待直接返回用户态。直到数据从磁盘拷贝回内核态,当新一次的请求到来,内核态缓冲区中有数据,于是数据被读走,即,从内核态拷贝回用户态。
(3)IO多路复用:
IO多路复用则是用一个线程去同时监听多个这种读写操作,即同时监听多个文件描述符FD。当出现某个FD可读或可写的时候,就会收到通知,然后调用read()或write()来对这个发生事件的FD读或写。这样,就可以避免之前对每一个FD调用recv()时,若当前recv()的FD没有就绪,其他FD就绪也没有办法被处理到的情况。而当select等收到信号,此时去读取发生事件的FD一定无需阻塞,直接可以将数据从磁盘读取到内核缓冲区了。
一般实现IO多路复用有三种实现select poll epoll : select poll只能通知用户态有几个FD发生事件,但是不嫩准确告知是哪几个FD;epoll则是不仅可以返回有几个FD发生事件,还可以将发生事件的FD写回到用户态,方便用户态继续处理。
4. 信号驱动IO:
信号驱动IO则对要监听的FD绑定一个信号处理函数,要求内核去监听这个FD。若内核无数据存放在缓冲区,那么用户态进程不阻塞,执行其他操作。当数据从磁盘到达内核缓冲区,内核会递交SIGIO信号通知用户态进程去调用recv()从内核把数据拷贝到用户区
缺点:
有大量信号产生的时候,会将大量的信号从内核空间拷贝到用户空间。造成性能较差。
同时,有大量信号产生时,会将信号加入到队列中,SIGIO处理函数若处理不及时会导致信号队列溢出。
5.异步IO:
用户态不再调用recv,而是调用aio_read()告知内核要监听那一个FD。内核返回一个OK,用户态就去执行其他的操作了。而后内核等待数据从磁盘就绪,拷贝到内核缓冲区,内核再将数据拷贝到用户缓冲区,再 调用信号处理函数 通知用户态数据到来
缺点:由于用户态不阻塞,当高并发时,可能会出现用户态不断要求内核监越来越多的FD,导致内核使用很多的缓冲区来存放这些FD上的数据,可能会导致超出内核内存,导致崩溃。因此,要做好限流,限制并发访问的数量。
总结下:同步还是异步:看的是第二阶段,数据从内核拷贝到用户态的拷贝过程是用户态执行还是内核态执行。因此:阻塞IO、非阻塞IO、IO多路复用、信号驱动IO都是同步的;只有异步IO是异步的。
至于阻塞非阻塞,则是看第一阶段,用户态发出请求,是否一直等待内核态的响应:
1. 校招八股:C/C++开发工程师常见笔试、面试题目汇总【基础】 - 简书 (jianshu.com)
https://blog.youkuaiyun.com/liang13664759/article/details/1771246
Makefile由浅入深--教程、干货 - 知乎 (zhihu.com)
2. 介绍下 session cookie token :
为什么会出现 session cookie token ?
因为HTTP是无状态的,第一次请求验证了身份,下一次还是这个客户请求无法识别出这是已经验证身份的人,还需要再次验证,用户体验差
解决方案就是session配合 cookie ;或者 token 配合 cookie
Session与Token的异同?
Session和Token机制原理上差不多,都是用户身份验证的一种识别手段,它们都有过期时间的限制,但两者又有一些不同的地方。
1、Session是存放在服务器端的,可以保存在:内存、数据库、NoSQL中。它采用空间换时间的策略来进行身份识别,把用户信息存放到服务器端。这会导致几个问题:(1)若Session没有持久化落地存储,一旦服务器重启,Session数据会丢失。(2)同时,若一亿个用户的session全部存储到服务器a,内存压力过大难以承受,另一方面,由nginx负载均衡时,存储在服务器a的session可能被转发到服务器b,导致session发现不了,还需要再次验证。
2、Token是放在客户端存储的,采用了时间换空间策略,它也是无状态的,所以在分布式环境中应用广泛。每次用户带着自己的token去访问请求,和服务器进行验证,速度更快,由于携带的是token信息,类似uuid,内容也更少,减轻了服务器端的存储压力。
而cookie就像是一个摆渡鸟,带着session或token 的信息来回互换穿梭于客户端和服务器,这样,就可以避免用户在请求服务过程中不断手动验证信息。例如一个场景是:你在淘宝浏览,买东西,跳转到购物车要买,这之间的验证时,不需要再次登录。这是因为cookie携带着我们的token,这样我们就不用一次次自己校验,重新登录了。
3. 进程与线程:
进程是被加载到内存中的程序,其中包含代码和相关的数据,还有操作系统为之创建的相关的数据结构,其中有PCB(task_struct)、进程地址空间(mm_struct)和页表,我们可以通过PCB找到对应的mm_struct。
4. C++ 内存分布:
【C++初阶】第七篇——C/C++的内存管理(C/C++动态内存分布+new和delete的用法和实现原理)_c 动态内存 new_呆呆兽学编程的博客-优快云博客
申请空间的本质是:向内存所要空间得到物理地址,然后在特定的区域申请没有被使用的虚拟地址,建立映射关系,再返回虚拟地址即可。
程序地址空间 就是 进程地址空间 就是 虚拟地址空间
5. 之后粘到这里来(C++和操作系统不分家)
【操作系统】虚拟内存相关&分段分页&页面置换算法_chuanauc的博客-优快云博客
C++构造函数体内初始化与列表初始化的区别_c++在类中初始化变量于在构造函数中初始化变量的区别_西塔666的博客-优快云博客
https://www.cnblogs.com/fortunely/p/14554114.html
6. 一份简单面经:
@C++11有什么新的特性?
@C++的编译器有从源码到可执行文件过程中有哪些步骤?可以有哪些加速?比如说编译器加速等一些加速方法
@常见的stl背后的算法结构
@模板类模板函数的使用
@一个类它会自动生成构造函数,析构函数拷贝复制函数,这些都是怎么实现的?
@Map和unordermap的区别