c++知识点总结(一)

本文深入探讨了C++的内存管理,包括内存分配方式、栈与堆的区别、常见的内存错误及处理策略,以及数组与指针的对比。详细介绍了malloc/free与new/delete的使用细节,强调了内存耗尽的解决方案。此外,文章还阐述了多进程与多线程的区别,排序算法的类别、复杂度和适用场景,以及http和https的区别。最后,讲解了哈希表的工作原理和优缺点,STL中的vector、list和deque的特性,以及hash_map与map的区别。此外,文章还涉及了IO多路复用机制的Select、Poll和Epoll模型,以及智能指针的种类和应用场景。
摘要由CSDN通过智能技术生成

https://blog.csdn.net/quzhongxin/article/details/48833345#1-静态编译与动态编译

c++内存管理

https://www.cnblogs.com/findumars/p/5929831.html

内存管理详解

内存分配方式
  • ,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • ,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
栈和堆的区别

主要的区别由以下几点:

1、管理方式不同;

2、空间大小不同;

一般来讲在32位系统下,堆内存可以达到4G的空间

3、能否产生碎片不同;

对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。

4、生长方向不同;

​ 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

5、分配方式不同;

​ 堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

6、分配效率不同;

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。

堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。

常见的内存错误以及处理方式

常见的内存错误及其对策如下:

  • 内存分配未成功,却使用了它。

    常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行

    检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

  • 内存分配虽然成功,但是尚未初始化就引用它

​ 犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。

  • 内存分配成功并且已经初始化,但操作越过了内存的边界。
  • 忘记了释放内存,造成内存泄露。

​ 含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错 误。终有一次程序突然死掉,系统出现提示:内存耗尽。

  • 释放了内存却继续使用它。

​ 有三种情况:

(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

(2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

(3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

数组和指针的对比
  • 数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

  • 指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

杜绝"野指针"

“野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。 “野指针”的成因主要有两种:

(1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

(2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

(3)指针操作超越了变量的作用域范围。

malloc/free与new/delete的辨识
  • malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

  • 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

  • 用new/delete来完成动态对象的内存管理。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

  • 既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?

    这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

  • 如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

内存耗尽怎么解决

如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。

(1)判断指针是否为NULL,如果是则马上用return语句终止本函数。

​ (2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。

​ (3)为new和malloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。

malloc/free的使用要点
函数malloc的原型如下:
void * malloc(size_t size);
用malloc申请一块长度为length的整数类型的内存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);

void * malloc(size_t size);

两个要素上:“类型转换”和“sizeof”

  • malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。
  • malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。

函数free的原型如下:

void free( void * memblock );

  • 为什么free函数不象malloc函数那样复杂呢?

​ 这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free§能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误

new/delete的使用要点

运算符new使用起来要比函数malloc简单得多,例如:

int *p1 = (int *)malloc(sizeof(int) * length);

int *p2 = new int[length];

这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。

多进程和多线程的区别

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

https://blog.csdn.net/linraise/article/details/12979473

![屏幕快照 2019-04-22 下午10.05.19](/Users/wangli/Downloads/typora文档里的图片/屏幕快照 2019-04-22 下午10.05.19.png)

1)需要频繁创建销毁的优先用线程。
实例:web服务器。来一个建立一个线程,断了就销毁线程。要是用进程,创建和销毁的代价是很难承受的。
2)需要进行大量计算的优先使用线程。
所谓大量计算,当然就是要消耗很多cpu,切换频繁了,这种情况先线程是最合适的。
实例:图像处理、算法处理
3)强相关的处理用线程,若相关的处理用进程。
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的server需要完成如下任务:消息收发和消息处理。消息收发和消息处理就是弱相关的任务,而消息处理里面可能又分为消息解码、业务处理,这两个任务相对来说相关性就要强多了。因此消息收发和消息处理可以分进程设计,消息解码和业务处理可以分线程设计。

4)可能扩展到多机分布的用进程,多核分布的用线程。

https://www.cnblogs.com/kaituorensheng/p/3603057.html

进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的(动态的)。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身;所有由你启动的进程都是用户进程。进程是操作系统进行资源分配的单位。

排序算法

十大经典算法的动图演示和代码实现https://www.cnblogs.com/onepixel/articles/7674659.html

算法分类

十种常见排序算法可以分为两大类:

  • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

849589-20190306165258970-1789860540

算法复杂度

849589-20180402133438219-1946132192

https://blog.csdn.net/u012428012/article/details/79083206

排序算法使用场景
  • 若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
  • 若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
  • 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况(逆序时,复杂度为O(n2))。这两种排序都是不稳定的。
若要求排序稳定,则可选用归并排序。通常可以将它和直接插入排序结合在一起使用:先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。

http和https的区别

https://blog.csdn.net/xionghuixionghui/article/details/68569282

HTTPS和HTTP的区别主要如下:

1、https协议需要到CA申请证书,一般免费证书较少,因而需要一定费用。

2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

SSL协议详解https://kb.cnblogs.com/page/162080/

SSL协议的三个特性

① 保密:在握手协议中定义了会话密钥后,所有的消息都被加密。

② 鉴别:可选的客户端认证,和强制的服务器端认证。

③ 完整性:传送的消息包括消息完整性检查(使用MAC)。

SSL的位置

SSL介于应用层和TCP层之间。应用层数据不再直接传递给传输层,而是传递给SSL层,SSL层对从应用层收到的数据进行加密,并增加自己的SSL头。

SSL的工作原理

握手协议(Handshake protocol)

记录协议(Record protocol)

警报协议(Alert protocol)

TCP的三次握手和四次挥手

http://www.cnblogs.com/Jessy/p/3535612.html

理解:窗口和滑动窗口、TCP的流量控制

TCP使用窗口机制进行流量控制

什么是窗口?

连接建立时,各端分配一块缓冲区用来存储接收的数据,并将缓冲区的尺寸发送给另一端

接收方发送的确认信息中包含了自己剩余的缓冲区尺寸

剩余缓冲区空间的数量叫做窗口

哈希表

哈希函数

哈希法又称散列法、杂凑法以及关键字地址计算法等。

基本思想:首先在元素的关键字K和元素的位置P之间建立一个建立一个对应关系f,使得P=f(K),其中f成为哈希函数。

创建哈希表时,把关键字K的元素直接存入地址为f(K)的单元;查找关键字K的元素时利用哈希函数计算出该元素的存储位置P=f(K).

哈希函数的构造方法
  • 数字分析法
  • 平方取中法
  • 分段叠加法
  • 除留余数法

哈希冲突

https://www.cnblogs.com/wuchaodzxx/p/7396599.html

解决哈希冲突的方法
  • 开放定址法
    • 线性探测再散列
    • 二次探测再散列
    • 伪随机探测再散列
  • 再哈希法
  • 链地址法
  • 建立公共溢出区
开放定址法

这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式:

Hi=(H(key)+di)% m i=1,2,…,n

其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种:

  1. 线性探测再散列:dii=1,2,3,…,m-1

    这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

  2. 二次探测再散列:di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )

​ 这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。

  1. 伪随机探测再散列:di=伪随机数序列。
再哈希法

这种方法是同时构造多个不同的哈希函数:

Hi=RH1(key) i=1,2,…,k

当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况

建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

优缺点
开放散列/拉链法(针对桶链结构)

优点:

对于记录总数频繁可变的情况,处理的比较好(也就是避免了动态调整的开销)

②**由于记录存储在结点中,而结点是动态分配,不会造成内存的浪费,所以尤其适合那种记录本身尺寸(size)很大的情况,因为此时指针的开销可以忽略不计了 **

删除记录时,比较方便,直接通过指针操作即可

缺点:

①存储的记录是随机分布在内存中的,这样在查询记录时,相比结构紧凑的数据类型(比如数组),哈希表的跳转访问会带来额外的时间开销

②如果所有的 key-value 对是可以提前预知,并之后不会发生变化时(即不允许插入和删除),可以人为创建一个不会产生冲突的完美哈希函数(perfect hash function),此时封闭散列的性能将远高于开放散列

③由于使用指针,记录不容易进行序列化(serialize)操作

封闭散列/开放定址法

优点:

①记录更容易进行**序列化(serialize)操作 **

如果记录总数可以预知,可以创建完美哈希函数,此时处理数据的效率是非常高的

缺点:

存储记录的数目不能超过桶数组的长度,如果超过就需要扩容,而扩容会导致某次操作的时间成本飙升,这在实时或者交互式应用中可能会是一个严重的缺陷

②使用探测序列,有可能其计算的时间成本过高,导致哈希表的处理性能降低 ③由于记录是存放在桶数组中的,而桶数组必然存在空槽,所以当记录本身尺寸(size)很大并且记录总数规模很大时,空槽占用的空间会导致明显的内存浪费

④删除记录时,比较麻烦。比如需要删除记录a,记录b是在a之后插入桶数组的,但是和记录a有冲突,是通过探测序列再次跳转找到的地址,所以如果直接删除a,a的位置变为空槽,而空槽是查询记录失败的终止条件,这样会导致记录b在a的位置重新插入数据前不可见,所以不能直接删除a,而是设置删除标记。这就需要额外的空间和操作。

STL

STL组件

  1. containers(容器):所谓容器,是指存放数据的地方,将数据以一定的方法组织存放。根据不同的组织方式,可以把容器分为顺序容器,如vector、deque、list,关联容器,如set、map。Container是一种class template。
  2. algorithm(算法):各种常用不常用的算法如sort、copy、search等等。algorithm是一种function template。
  3. iterator(迭代器):迭代器是算法和容器之前的胶合剂,算法作用于容器之上,但算法以迭代器作为形参。从实现上看,迭代器是一种将operator*,operator++,operator–,operator->等指针相关操作予以重载的class template。所以容器都带有自己的迭代器,因为只有容器设计者才知道如何遍历自己的元素。
  4. functors(仿函数):行为类似函数,可作为算法的某种策略。从实现的角度来看,仿函数是一种重载了operator()的class或class template,它常常是算法的一个输入,类似于一种策略。
  5. adapters(适配器):用来形容容器、迭代器或仿函数接口的东西,有时候上面那些组件的行为可能跟我们想要的约束不大一样,于是给它们包装一下,使它们遵守一定的行为。
  6. allocator(配置器):负责空间配置与管理。从实现的角度来看,配置器是一种实现了动态空间配置、管理、空间释放的class template。

STL空间配置器剖析

https://www.2cto.com/kf/201607/527069.html

  • 一级空间配置器:

    STL源码中的一级空间配置器命名为class __malloc_alloc_template ,它很简单,就是对malloc,free,realloc等系统分配函数的一层封装

  • 二级空间配置器:是由一个内存池自由链表配合实现的

    二级空间配置器是为频繁分配小内存而生的一种算法。其实就是消除一级空间配置器的外碎片问题

(vector实现代码和空间配置器实现内存池)https://blog.csdn.net/qq_37058442/article/details/77895688

STL内存管理:http://www.mamicode.com/info-detail-515765.html

STL中list, vector,map,set区别与比较

https://cloud.tencent.com/developer/article/1052125

List封装了链表,Vector封装了数组, list和vector得最主要的区别在于vector使用连续内存存储的,支持[]运算符,而list是以链表形式实现的,不支持[]

Vector对于随机访问的速度很快,但是对于插入尤其是在头部插入元素速度很慢,在尾部插入速度很快。List对于随机访问速度慢得多,因为可能要遍历整个链表才能做到,但是对于插入就快的多了,不需要拷贝和移动数据,只需要改变指针的指向就可以了。另外对于新添加的元素,Vector有一套算法,而List可以任意加入。

Map,Set属于标准关联容器,使用了非常高效的平衡检索二叉树:红黑树,他的插入删除效率比其他序列容器高是因为不需要做内存拷贝和内存移动,而直接替换指向节点的指针即可。

Set和Vector的区别在于Set不包含重复的数据

Set和Map的区别在于Set只含有Key,而Map有一个Key和Key所对应的Value两个元素。

Map和Hash_Map的区别是Hash_Map使用了Hash算法来加快查找过程,但是需要更多的内存来存放这些Hash桶元素,因此可以算得上是采用空间来换取时间策略

vector

向量 相当于一个数组
在内存中分配一块连续的内存空间进行存储。支持不指定vector大小的存储。STL内部实现时,首先分配一个非常大的内存空间预备进行存储,即capacituy()函数返回的大小,当超过此分配的空间时再整体重新放分配一块内存存储,将数据拷贝到新的内存中。(内存长度每次以2倍的速率增长)

优点:(1) 不指定一块内存大小的数组的连续存储,即可以像数组一样操作,但可以对此数组
进行动态操作。通常体现在push_back() pop_back()
(2) 随机访问方便,即支持[ ]操作符和vector.at()
(3) 节省空间。
缺点:(1) 在内部进行插入删除操作效率低。
(2) 只能在vector的最后进行push和pop,不能在vector的头进行push和pop。
(3) 当动态添加的数据超过vector默认分配的大小时要进行整体的重新分配、拷贝与释

list

双向链表
每一个结点都包括一个信息块Info、一个前驱指针Pre、一个后驱指针Post。可以不分配必须的内存大小方便的进行添加和删除操作。使用的是非连续的内存空间进行存储
优点:(1) 不使用连续内存完成动态操作。
(2) 在内部方便的进行插入和删除操作
(3) 可在两端进行push、pop
缺点:(1) 不能进行内部的随机访问,即不支持[ ]操作符和vector.at()
(2) 相对于verctor占用内存多

deque

双端队列 double-end queue

deque是在功能上合并了vector和list。
优点:(1) 随机访问方便,即支持[* ]操作符和vector.at()
(2) 在内部方便的进行插入和删除操作
(3) 可在两端进行push、pop
缺点:(1) 占用内存多

使用区别:
1 如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
2 如果你需要大量的插入和删除,而不关心随即存取,则应使用list
3 如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque

hash_map

https://blog.csdn.net/txl199106/article/details/51074791

hash_map原理

基本原理是:使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。

(“直接定址”与“解决冲突”是哈希表的两大特点。)

hash_map使用

hash_map类在头文件hash_map中,和所有其它的C++标准库一样,头文件没有扩展名。如下声明:

#include <hash_map>  
using namespace std;  
using namespace stdext; 

hash_map是一个聚合类,它继承自_Hash类,包括一个vector,一个list和一个pair,其中vector用于保存桶,list用于进行冲突处理,pair用于保存key->value结构,简要地伪码如下:

class hash_map<class _Tkey, class _Tval>  
{  
private:  
    typedef pair<_Tkey, _Tval> hash_pair;  
    typedef list<hash_pair>    hash_list;  
    typedef vector<hash_list>  hash_table;  
};  
hash_map和map的区别
  • 构造函数。hash_map需要hash函数,等于函数;map只需要比较函数(小于函数).
  • 存储结构。hash_map采用hash表存储,map一般采用红黑树(RB Tree)实现。因此其memory数据结构是不一样的。
什么时候需要用hash_map,什么时候需要用map?

总体来说,hash_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小,hash还有hash函数的耗时。

如果考虑效率,特别是在元素达到一定数量级时,考虑考虑hash_map。但若对内存使用特别严格,希望程序尽可能少消耗内存,选择map。

权衡三个因素: 查找速度, 数据量, 内存使用

IO多路复用机制

https://cloud.tencent.com/developer/article/1005481

Select、Poll、Epoll模型

select、poll、epoll,都是IO多路复用的机制,可以监视多个描述符的读/写等事件,一旦某个描述符就绪(一般是读或者写事件发生了),就能够将发生的事件通知给关心的应用程序去处理该事件。

5种IO模型:

[1] blocking IO - 阻塞IO
[2] nonblocking IO - 非阻塞IO
[3] IO multiplexing - IO多路复用
[4] signal driven IO - 信号驱动IO
[5] asynchronous IO - 异步IO

其中前面4种IO都可以归类为synchronous IO - 同步IO.

IO-同步、异步、阻塞、非阻塞

下面以network IO中的read读操作为切入点,来讲述同步(synchronous) IO和异步(asynchronous) IO、阻塞(blocking) IO和非阻塞(non-blocking)IO的异同。一般情况下,一次网络IO读操作会涉及两个系统对象:(1) 用户进程(线程)Process;(2)内核对象kernel,两个处理阶段:

[1] Waiting for the data to be ready - 等待数据准备好
[2] Copying the data from the kernel to the process - 将数据从内核空间的buffer拷贝到用户空间进程的buffer
  • 同步IO 之 Blocking IO
    • 用户进程process在Blocking IO读recvfrom操作的两个阶段都是等待的。在数据没准备好的时候,process原地等待kernel准备数据。kernel准备好数据后,process继续等待kernel将数据copy到自己的buffer。在kernel完成数据的copy后process才会从recvfrom系统调用中返回。
  • 同步IO 之 NonBlocking IO
    • process在NonBlocking IO读recvfrom操作的第一个阶段是不会block等待的,如果kernel数据还没准备好,那么recvfrom会立刻返回一个EWOULDBLOCK错误。当kernel准备好数据后,进入处理的第二阶段的时候,process会等待kernel将数据copy到自己的buffer,在kernel完成数据的copy后process才会从recvfrom系统调用中返回。
  • 同步IO 之 IO multiplexing
    • IO多路复用,就是我们熟知的select、poll、epoll模型。
    • 在IO多路复用的时候,process在两个处理阶段都是block住等待的。初看好像IO多路复用没什么用,其实select、poll、epoll的优势在于可以以较少的代价来同时监听处理多个IO
  • 异步IO
    • 异步IO要求process在recvfrom操作的两个处理阶段上都不能等待,也就是process调用recvfrom后立刻返回,kernel自行去准备好数据并将数据从kernel的buffer中copy到process的buffer在通知process读操作完成了,然后process在去处理。
    • linux的网络IO中是不存在异步IO的,linux的网络IO处理的第二阶段总是阻塞等待数据copy完成的。真正意义上的网络异步IO是Windows下的IOCP(IO完成端口)模型。
Linux的socket 事件wakeup callback机制

linux(2.6+)内核的事件wakeup callback机制,这是IO多路复用机制存在的本质。

Linux通过socket睡眠队列来管理所有等待socket的某个事件的process,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的process,通知process相关事件发生。通常情况,socket的事件发生的时候,其会顺序遍历socket睡眠队列上的每个process节点,调用每个process节点挂载的callback函数。在遍历的过程中,如果遇到某个节点是排他的,那么就终止遍历,总体上会涉及两大逻辑:(1)睡眠等待逻辑;(2)唤醒逻辑。

1)睡眠等待逻辑:涉及select、poll、epoll_wait的阻塞等待逻辑

[1]select、poll、epoll_wait陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前process构建一个wait_entry节点,然后插入到监控socket的sleep_list
[2]进入循环的schedule直到关心的事件发生了
[3]关心的事件发生后,将当前process的wait_entry节点从socket的sleep_list中删除。

2)唤醒逻辑:

[1]socket的事件发生了,然后socket顺序遍历其睡眠队列,依次调用每个wait_entry节点的callback函数
[2]直到完成队列的遍历或遇到某个wait_entry节点是排他的才停止。
[3]一般情况下callback包含两个逻辑:1.wait_entry自定义的私有逻辑;2.唤醒的公共逻辑,主要用于将该wait_entry的process放入CPU的就绪队列,让CPU随后可以调度其执行。
Select模型

select为每个socket引入一个poll逻辑,该poll逻辑用于收集socket发生的事件,对于可读事件来说,简单伪码如下:

poll()
{
    //其他逻辑
    if (recieve queque is not empty)
    {
        sk_event |= POLL_IN}
   //其他逻辑
}

select的函数原型:5个参数,后面4个参数都是in/out类型(值可能会被修改返回)

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

当用户process调用select的时候:

  1. select会将需要监控的readfds集合拷贝到内核空间(假设监控的仅仅是socket可读)
  2. 遍历自己监控的socket sk,挨个调用sk的poll逻辑以便检查该sk是否有可读事件
  3. 遍历完所有的sk后,如果没有任何一个sk可读,那么select会调用schedule_timeout进入schedule循环,使得process进入睡眠
  4. 如果在timeout时间内某个sk上有数据可读了,或者等待timeout了,则调用select的process会被唤醒,接下来select就是遍历监控的sk集合,挨个收集可读事件并返回给用户了。

select存在两个问题:

  1. 被监控的fds需要从用户空间拷贝到内核空间

    为了减少数据拷贝带来的性能损坏,内核对被监控的fds集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)。

  2. 被监控的fds集合中,只要有一个有数据可读,整个socket集合就会被遍历一次调用sk的poll函数收集可读事件

​ 由于当初的需求是朴素,仅仅关心是否有数据可读这样一个事件,当事件通知来的时候,由于数据的到来是异步的,我们不知道事件来的时候,有多少个被监控的socket有数据可读了,于是,只能挨个遍历每个socket来收集可读事件。

有三个问题需要解决:

(1)被监控的fds集合限制为1024,1024太小了,我们希望能够有个比较大的可监控fds集

(2)fds集合需要从用户空间拷贝到内核空间的问题,我们希望不需要拷贝 

(3)当被监控的fds中某些有数据可读的时候,我们希望通知更加精细一点,就是我们希望能够从通知中得到有可读事件的fds列表,而不是需要遍历整个fds来收集。


Poll模型

elect遗留的三个问题中,问题(1)是用法限制问题,问题(2)和(3)则是性能问题。poll和select非常相似,poll并没着手解决性能问题,poll只是解决了select的问题(1)fds集合大小1024限制问题。

poll虽然解决了fds集合大小1024的限制问题,但是,它并没改变大量描述符数组被整体复制于用户态和内核态的地址空间之间,以及个别描述符就绪触发整体描述符集合的遍历的低效问题。poll随着监控的socket集合的增加性能线性下降,poll不适合用于大并发场景

poll的函数原型:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
Epoll模型
在计算机行业中,有两种解决问题的思想:
[1] 计算机科学领域的任何问题, 都可以通过添加一个中间层来解决
[2] 变集中(中央)处理为分散(分布式)处理
fds集合拷贝问题的解决

epoll引入了epoll_ctl系统调用,将高频调用的epoll_wait和低频的epoll_ctl隔离开。同时,epoll_ctl通过(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)三个操作来分散对需要监控的fds集合的修改,做到了有变化才变更,将select或poll高频、大块内存拷贝(集中处理)变成epoll_ctl的低频、小块内存的拷贝(分散处理),避免了大量的内存拷贝

同时,对于高频epoll_wait的可读就绪的fd集合返回的拷贝问题,epoll通过内核与用户空间mmap(内存映射)同一块内存来解决。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。

按需遍历就绪的fds集合

epoll引入了一个中间层,一个双向链表(ready_list),一个单独的睡眠队列(single_epoll_wait_list),并且,与select或poll不同的是,epoll的process不需要同时插入到多路复用的socket集合的所有睡眠队列中,相反process只是插入到中间层的epoll的单独睡眠队列中,process睡眠在epoll的单独队列上,等待事件的发生。同时,引入一个中间的wait_entry_sk,它与某个socket sk密切相关,wait_entry_sk睡眠在sk的睡眠队列上,其callback函数逻辑是将当前sk排入到epoll的ready_list中,并唤醒epoll的single_epoll_wait_list。而single_epoll_wait_list上睡眠的process的回调函数就明朗了:遍历ready_list上的所有sk,挨个调用sk的poll函数收集事件,然后唤醒process从epoll_wait返回。

ET vs LT

  • Edge Triggered (ET) 边沿触发

.socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件

.socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件

仅在缓冲区状态变化时触发事件,比如数据缓冲去从无到有的时候(不可读-可读)

  • Level Triggered (LT) 水平触发

.socket接收缓冲区不为空,有数据可读,则读事件一直触发

.socket发送缓冲区不满可以继续写入数据,则写事件一直触发

符合思维习惯,epoll_wait返回的事件就是socket的状态

智能指针(Boost库提供)

https://cloud.tencent.com/developer/article/1187781

​ 智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。

​ 它的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。

​ 智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。

auto_ptr

auto_ptr是现在标准库里面一个轻量级的智能指针的实现,存在于头文件 memory中,之所以说它是轻量级,是因为它只有一个成员变量(拥有对象的指针),相关的调用开销也非常小。主要是通过将指针通过对象管理,通过对象实例的生命周期来实现对被管理指针的自动释放。

auto_ptr的使用很简单,通过构造函数拥有一个动态分配对象的所有权,然后就可以被当作对象指针来使用,当auto_ptr对象被销毁的时候,它也会自动销毁自己拥有所有权的对象(嗯,标准的RAAI做法),release可以用来手动放弃所有权,reset可用于手动销毁内部对象。

auto_ptr是一个相当容易被误用并且在实际中常常被误用的类。原因是由于它的对象所有权占用的特性和它非平凡的拷贝行为

auto_ptr 拷贝构造函数是会修改引用参数的,因此不能使用在STL中,STL容器要求拷贝构造函数中对源对象保持不变,auto_ptr会将源对象的指针拥有转移到自身,导致源对象为空(不再指向被创建的对象)

auto_ptr的几点注意事项:

1、auto_ptr不能共享所有权

2、auto_ptr不能指向数组

3、auto_ptr不能作为容器的成员

4、不能通过复制操作来初始化auto_ptr

std::auto_ptr p(new int(42)); //OK

std::atuo_ptrp = new int(42);//Error

这是因为auto_ptr的构造函数被定义了explicit

5、不要把auto_ptr放入容器

shared_ptr

shared_ptr就是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。

**一个shared_ptr对象除了包括一个所拥有对象的指针(px)外,还必须包括一个引用计数代理对象(shared_count)的指针(pn)。**而这个引用计数代理对象包括一个真正的多态的引用计数对象(sp_counted_base)的指针(_pi),真正的引用计数对象在使用VC编译器的情况下包括一个虚表,一个虚表指针,和两个计数器。

shared_ptr是可以拷贝和赋值的,拷贝行为也是等价的,并且可以被比较,这意味这它可被放入标准库的一般容器(vector,list)和关联容器中(map)。

weak_ptr

weak_ptr是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用记数的增加或减少。没有重载*和->但可以使用lock获得一个可用的shared_ptr对象

weak_ptr的一个重要用途是通过lock获得this指针的shared_ptr,使对象自己能够生产shared_ptr来管理自己,但助手类enable_shared_from_this的shared_from_this会返回this的shared_ptr,只需要让想被shared_ptr管理的类从它继承即可

boost::weak_ptr是boost提供的一个弱引用的智能指针

通过boost::weak_ptr来打破循环引用

由于弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用。

scoped_ptr

头文件: "boost/scoped_ptr.hpp"

boost::scoped_ptr 用于确保动态分配的对象能够被正确地删除scoped_ptr 有着与std::auto_ptr类似的特性,而最大的区别在于它不能转让所有权auto_ptr可以。

scoped_ptr永远不能被复制或被赋值scoped_ptr 拥有它所指向的资源的所有权,并永远不会放弃这个所有权。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值