2022-04-13 - 程序员c++技术积累

lambda表达式常见用法 :
【最常见】1 [capture list] {function body} // 自动推断返回类型,并且无参数,
2 [capture list] (params list) {function body} // 自动推断返回类型,并有参数

捕获类型 :

值捕获
int a = 123;
auto f = [a] { cout << a << endl; };

引用捕获
int a = 123;
auto f = [&a] { cout << a << endl; };
隐式值捕获
int a = 123;
auto f = [=] { cout << a << endl; }; // 值捕获
隐式值捕获
int a = 123;
auto f = [=] { cout << a << endl; }; // 值捕获

int a = 123;
auto f = [&] { cout << a << endl; };    // 引用捕获

1,不用宏,用 constexpr
2, 包括返回值,该加const,要加const
3,类的声明,注意final,这样就不用写虚析构函数
4, 类默认值,注意用定义时初始化,比初始化列表更好

1,声明: 函数声明多用 inline __always_inline
2, 表达式: if判断多用 likely unlikely
3, 类型: size_t

1,私有成员变量 加后划线 私有成员函数,加前划线, 这样代码一清二楚。 参考litao的类 class TransponderRPC

2, 返回值的场景,区分获取成功和有值2种情况,用 std::pair ,这样返回值的意思清晰明确,调用者不容易用错,并且方便
以后扩展。 参考函数 DefaultAddressRepository::GetPeerComponentId(uint64_t componentId)

3,整数,不要写 uint32_t xx,要写 auto xx = 0U
char* 定义,可以写 static const auto objType = "obj"s;

迭代器(iterator)是一种智能指针,智能指针是一个类,具体来说是1个模板类

ARRAY是C++11提供的一种容器,VECTOR是STL提供的一种容器,区别在于前者不能扩容

弱符号:未初始化的全局变量是弱符号,或者__attribute__((weak)) 修饰的变量,函数是弱符号
效果: 同时存在强弱符号,则有限匹配强符号
【具体应用:库中定义弱符号,使得可以被用户定义的强符号替代。】

弱引用: attribute__((weak)) 或 attribute__((weakref)) 修饰函数声明
效果: 如果未找到该符号定义,则不会报错
【具体应用:将扩展功能的符号作为弱符号,定义放在so中,作为扩展模块,如果去掉扩展模块,也可以正常链接,如果不去掉,那么就自然加上了扩展模块】

linux的程序格式:ELF格式。
elf头包括魔数字,表明自己是ELF文件 (7f 45 4c 46),还有多少位,大小端,文件类型(可执行程序,共享SO文件,可重定位文件等)

还表明了 section 和 segment的偏移。
section是给链接视图,指导链接器链接的,segemnt是运行视图,是给链接器指导生成可执行程序内存布局的。
比如我们通常说的,代码段,数据段, BSS 他们在section中也有这个概念,同时在segment也有这个概念。 除了这几个 segemnt ,还有其他的某些segment,他们1个
segment会包含多个section。

ELF文件头中,第一个字段是魔数,这个数字是用来表明自己是ELF文件(固定是7f 45 4c 46),ELF的字长,字节序,以及ELF版本

静态链接:

静态链接与虚拟地址 : 静态链接就分配了虚拟地址,也就是确定各段的起始地址。

链接之后,怎么看分配到虚拟地址是什么 : 生成的elf文件,用objdump -h 看,可以看到链接后,就已经分配了虚拟地址()。由于ELF可执行可执行文件默认从0x8048000开始的,所以可以看到代码段的虚拟地址不是从0x0开始,而是从0x84000往后加一点开始的。
比如通过 objdump -d a.out,查看了funcli的地址:
0000000000400677 <_Z5func1i>:
400677: 55 push %rbp
400678: 48 89 e5 mov %rsp,%rbp
40067b: 48 83 ec 10 sub 0x4007ae,%edi
40068c: b8 00 00 00 00 mov $0x0,%eax
400691: e8 aa fe ff ff callq 400540 printf@plt

运行a.out,gdb上去看这个地址是不是确实这样的,而可以看到是这样的 :
Breakpoint 1, 0x000000000040067b in func1(int) ()
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040067b
breakpoint already hit 1 time
(gdb) bt

0 0x000000000040067b in func1(int) ()

1 0x00000000004006c7 in main ()

(gdb) disassemble
Dump of assembler code for function _Z5func1i:
0x0000000000400677 <+0>: push %rbp
0x0000000000400678 <+1>: mov %rsp,%rbp
=> 0x000000000040067b <+4>: sub 0x4007ae,%edi
0x000000000040068c <+21>: mov $0x0,%eax
0x0000000000400691 <+26>: callq 0x400540 printf@plt
0x0000000000400696 <+31>: nop
0x0000000000400697 <+32>: leaveq

链接中的重定位:重定位就发生在静态链接中,可以说是静态链接的核心。由于各段已经确定了起始偏移地址,所以各符号加上起始偏移地址就可以了。这里的关键是,如何知道哪些指令的地址要被调整呢,
就是记录在重定位表中的额。

中断:
linux内核的要求 :
1,保存上下文并恢复上下文
2,能够将中断处理分为紧急和不紧急的部分
3,能够在处理中断中,允许另一个中断到来
4,能够在处理中断的特殊临界区中,禁止中断

x86 :
    1, 中断(cpu外部产生)和异常(CPU内部产生,段错误就是一种异常)
    2,中断又分为可屏蔽中断(I/O设备产生的)和不可屏蔽中断(电源,总线,内存等)。
    3,中断和异常都是由0~255之间的一个数来标识,该数字也叫向量(vector)。 其中不可屏蔽中断和异常的向量是固定的,可屏蔽中断的向量是通过中断控制器的编程来控制的。


256个向量的分配如下 :
    从0~31的向量对应于异常和非屏蔽中断
    从32~47的向量(即由I/O设备引起的中断)分配给屏蔽中断
    剩余的从48~255的向量用来标识软中断。Linux只用了其中的一个(即128或0x80向量)用来实现系统调用。
    中断向量的值,就是用于在中断向量表的偏移。

中断向量从哪里获取
    外部中断的是到时要从中断控制器获得,内部CPU产生的,直接内部就有了,不需要再去获取,软中断的话就是指令里面直接自带的。
    
    
为什么要围绕中断向量设计,包括填写中断向量表:
    因为CPU设计成拿到向量,就会自动去找向量表,比较表中的权限,以及拿出处理程序的地址,去执行后续的动作。
    
    
中断向量表的位置到底是不是从0开始 - 2个阶段:   
    实地址模式下,内存的0开始的1K字节作为中断向量表。
    保护模式下, 在保护模式下,中断描述符表在内存的位置不再限于从地址0开始的地方,而是可以放在内存的任何地方。为此,CPU中增设了一个中断描述符表寄存器IDTR。

中断向量表 :
    存放了处理函数的地址信息;描述了进入后的特权级,CPU会自动与当前特权级比较,不同则会引起堆栈的更换,比如从用户堆栈切换到内核堆栈;描述了是否要关闭中断(会屏蔽可屏蔽中断)


上半部与下半部的设计到底为了啥
    为了不会长时间关中断,导致丢中断。




ARM上的NMI:
    ARM是类似的,ARM上有IRQ和FIQ,其中FIQ就相当于X86上的NMI,即不可屏蔽中断。而IRQ的话每次一旦进去,就硬件自动关闭中断;但是ARM更加灵活,在某些架构上,是可以通过设置CPSR寄存器屏蔽的。




从中断屏蔽 --到---> 中断上半部下半部:
    硬件产生的中断可被称为硬中断(hardirp),执行中断指令(int)产生的中断为软中断。

    Linux下硬中断硬件上设计为可以嵌套的,但硬件设计是硬中断也是设置为可屏蔽的(比如x86中的IF位),就是可以关闭嵌套,而一般是设计为一旦进入硬件中断 ,就开启屏蔽,不允许嵌套!
    为什么这样设计呢,因为这样嵌套可能导致栈溢出。所以LINUX内核设计为不可嵌套。
    
    所以要执行的尽量的快,不然长时间关中断,会导致丢中断(并不是说一关就会丢,虽然有一定缓存,但是是有上限的)。所以就引入了上半部下半部的概念。      
        
软中断,是下半部的一种,还有tasklet和工作队列。

【软中断】
    【软中断 不等于 硬中断下半部,另外还有tasklet和工作队列,都是下半部的形式】
    【软中断还有种很重要的用法,系统调用】
    【软中断不能嵌套,不能屏蔽(会打断硬中断?软中断?),但是可以在不同处理器上同时执行,类型相同也可以】

    说明了如果有其他软中断触发,执行到此处由于先前的软中断已经在处理,则其他软中断会返回。所以,软中断不能被另外一个软中断抢占!唯一可以抢占软中断的是中断处理程序,所以软中断允许响应中断。虽然不能在本处理器上抢占,但是其他的软中断甚至同类型可以再其他处理器上同时执行。由于这点,所以对临界区需要加锁保护。
    软中断留给对时间要求最严格的下半部使用。目前只有网络,内核定时器和 tasklet 建立在软中断上。

cmake
cmakelist.txt
1, 继承父目录该文件的一切变量
2,里面的执行是一条一条顺序执行的
3,查找外部的头文件和so,使用者需要 find_package(xx ,比如 find_package(msgc, 这样camke就会自动去对应指定目录找并执行 Findxx.cmake 。 Findxx.cmake 是模块提供方创建的,写法就是把头
文件的路径,查找好并设置成名为 xx_INCLUDE_DIR 或者 xx_INCLUDE_DIRS 的变量,把so的路径查找好并设置为 LIBXML2_LIBRARIES 方便使用者用这个变量找到: 比如下列写法 :

一些语法:
add_library(vos-ext-objects OBJECT ./vos_move2ext.cpp) 并不是生成一个 vos-ext-object.so ,而只是生成1个打包,因为后面指定的是 OBJECT 
        
LINK_LIBRARIES -(添加需要链接的库文件路径,注意这里是全路径)  对比  TARGET_LINK_LIBRARIES - (这个是设置要链接的库文件的名称)
        

高级用法(变量范围)
1,父目录的变量自动继承到子目录

2,cache变量(set时指定cache)的范围时整个工程

3,  include和find_package (类似于引用头文件,自动在使用include与find_pacpage的本文件继承头文件中的变量,并会传递这个include与find_package后面的子目录(find_package没验证,应该也是一样的)):
include(Findtest) # 从 CMAKE_MODULE_PATH 包含的路径中搜索 Findtest.cmake 文件 (前面还有一句  : list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake_modules))
类似的, find_package 和 include类似,也会自动继承找到的Findxx.cmake中的变量。

内核源码目录:
init : 内核的初始化代码
fs : 虚拟文件系统((VFS))的代码
【kernel】 : 内核中最核心的部分,包括进程的调度(sched.c) ,以及进程的创建和撤销(fork.c和exit.c);平台相关的另外一部分核心代码再 arch//kernel目录下。
【mm】 : 与体系无关的内存管理代码,与体系有关的内存管理代码位于arch/
/mm下

arch:是architecture的缩写。所有与体系相关的代码都在这个目录

【drivers】: 驱动代码。这个目录是内核中最庞大的一个目录,显卡,网卡,各种总线等等的驱动程序都可以在这里找到
【firmware】 :注意不是防火墙,而是估计让计算机读取和理解从设备发来的信号的代码。举例来说,一个摄像头管理它自己的硬件,但计算机必须了解摄像头给计算机发送的信号。
固件和驱动的区别在于,固件不是运行在OS运行的处理器上,而是运行在外设中的处理器上。
【block】 : 块设备驱动,还有一部分位于drivers目录

【lib】 : 此目录包含了核心的库代码,实现了一个标准C库的通用子集,包括字符串和内存操作的函数,以及sprintf和atoi系列函数。
【ipc】: IPC(进程间通信)。它包含了共享内存,信号量以及其他形式的IPC代码

srcutiry: 包含了不同的linux安全
crypto: 内核本身所用的加密API,实现了常用的加密和散列算法,还有一些压缩衣和CRC校验算法。

select的内核实现 (参考自http://gityuan.com/2019/01/05/linux-poll-select/) 写的好,可以多看看他的博客 )
核心处理 : 将自身线程加入文件描述符的等待队列; 然后将线程休眠;等待文件描述符(可读,可写,异常)时,???将等待队列的线程唤醒。
select中第一个参数的目的:
在*Nix系统中,文件描述符只是系统表的索引,而fd_set结构包含与这些索引对应的位掩码。将描述符添加到fd_set时,将启用相应的位。select()需要知道描述符的最高值,这样它就可以循环遍历这些位,并知道在哪个位停止。
在Windows上,套接字由内核对象的句柄表示,而不是由索引表示。fd_set结构包含一个套接字句柄数组和数组中套接字数量的计数器。这样,select()就可以循环遍历数组,这就是为什么在Windows上忽略select()的第一个参数的原因。

C++ 类几个编译器自动生成的函数,以及不自动生成的场景 :
(默认的移动构造干啥呢,它咋知道怎么移动呢,比如将原来传入的临时对象的某个值释放什么的,答案是,默认的
移动构造仅仅调用所有对象的移动构造函数。。。)

构造函数并不都是,你定义1个,编译器会把其他的都干掉,具体情况如下 :
3个构造函数之间的影响(重点是这4个构造函数间的互相影响的规则,因为实际的应用中,大部分都会定义构造函数):
定义无参构造,编译器不会删除默认的无参构造, 不会删除默认的移动构造函数
定义拷贝构造,编译器会删除默认的无参构造,会删除默认的移动构造函数
定义移动构造,编译器会删除默认的无参构造,会删除默认的移动构造函数
【如果自己定义其它类型的构造函数,则上面所有的默认构造函数都不会生成!】

定义拷贝赋值或者移动赋值运算符,都会删除上面的拷贝构造,应该还有移动构造(未验证),但都不会删除默认构造

智能指针:
了解下这个 https://cloud.tencent.com/developer/article/1688444

【【auto_ptr : 关键,不可共享且可转移】】

【初始化要注意】
初始化不能用隐式转换,目的是为了让开发清晰的知道自己在处理智能指针 :

std::auto_ptr ptr1(new ClassA); // 正确
std::auto_ptr ptr1 = new ClassA; // 错误,编译器不让编译过,因为开发可能没意识到自己在处理的是智能指针

或者
std::auto_ptr ptr ;
ptr = std::auto_ptr (new ClassA); // 正确,写的很长,明显知道自己写的是只能指针
ptr = new ClassA(); // 错误,编译器不让编译过,因为开发可能没意识到自己在处理的是智能指针

【智能指针间赋值操作符要注意 - 直接就发生所有权转移了】

【判空要注意】
判空不能直接 if(ptr == nullptr) ,需要 if (ptr.get() == nullptr)

常见STL的一些底层实现 :

STL的迭代器是什么 - 不是指针,是类模板,具体用时,就是一个对象

vector :初始会有一片连续空间;扩容会扩大2倍,这会把原来的拷贝过来,再把原来的释放。 看下这个 https://segmentfault.com/a/1190000040103598

vector迭代器失效的情况:
push_back ,导致失效是因为可能导致扩容
insert, 导致失效的原因是要移动后面的元素,以及可能扩容
erase, 导致后面的元素要移动

vector的remove:
1, remove不会删除元素,不会导致size变小
2,remove的核心原理是: 每一位有3个步骤:1,检查是否是否是remove的值,满足则标记空洞,但并不会改值用下一个来填充(简称-记坑等后人)(为啥要交给后人来改,因为不一定有下一位,下一位也可能自身也是坑) ;
2,不是的话,就看前面是否有空洞,有则填充最前面的空洞(不一定是紧邻的前一个空洞);填充则把填充的标记转移到自身。 (简称-填坑要填最前面,填完自己也记坑 | 或者自己也是坑,记坑等后人)。
- 从上面第一点,如果是remove最后一位,则最后一位没有变化,因为此时只是标记了空洞,下一位已经没有了,无法在扫描下一位时,来填充这最后一位。
举例 :

include

include

include

using namespace std;

int main()
{
vector vc;

for (int i = 0; i <= 9; ++i) {
vc.push_back(i);
}

vc[3] = vc[5] = vc[9] = 99;

for (auto itr : vc) {
    cout << itr << endl;
}
cout << "after remove" << endl;

remove(vc.begin(), vc.end(), 99);
for (auto itr : vc) {
    cout << itr << endl;
}

return 0;

}

输出结果是:
0
1
2
99
4
99
6
7
8
99
after remove
0
1
2
4
6
7
8
7
8
99

map和set: 核心是红黑树(红黑树是一种更加平衡的二叉查找树)

operator new 的好处 :
1,频繁创建的对象,可以避免每次申请内存
2,即使是非频繁创建,栈上构造对象,可以避免走入C库的查找堆中可用空间的处理,速度更快

OS - 多核调度
2级调度的形式 - 先进入公共队列,再根据每个CPU核的负载情况,分发到每个CPU核
负载追踪 - 只采用2级队列,会导致一直在某个CPU核,不均衡,所以需要收集每个CPU核的负载(历史上每个任务的运行时间)
负载均衡的触发 - 消耗低的CPU核心进行触发,拉取其它CPU核心的任务到本地队列
负载均衡的层级 - 先在低级触发 (逻辑CPU域 -> 物理CPU域 ->NUMA域(多核CPU拥有本地内存)) - 减少跨级的高切换开销

STL的容器:

分类:
容器的类型:
顺序容器(vector, list, deque), 排列位置与其值无关。
关联容器(map, set, multimap, multiset), 排列位置与其值有关。
容器适配器(stack, queue, priority_queue)

set : 当只是想知道一个值是否存在时,set是最有用的。

性能:
检查为空:
使用empty()而不是size()来检查容器是否为空,因为有些容器,比如list,它的size需要遍历,而empty不需要

普通互斥锁的替代方案 : 读写锁 -> std::shared_mutex -> std::atomic
顺序容器vector:通过 resize 提前预留好空间 -> 可以保证不变化 -> 可以不加锁
关联容器map:不要同时修改同一个key的value就写

vector和list插入时,用emplace_back替代push_back, 前者只会使用构造函数,后者会使用构造函数和移动构造函数2个

插入:
map的插入,insert 相比 [] :
1,insert效率更高
2, insert更安全,如果[]赋值的一步失败了,则会处于中间状态 (赋值这一步干的啥 ? )
3,如果没有默认构造函数,则只能用insert (试验下 ? )
应用上的区别是,insert遇到相同key,则不会更新值,[]会。

时钟周期是震荡周期,是最基本的时间。
指令周期:执行一条指令需要划分为3个阶段:取指(去PC寄存器指向的地址种获取指令地址并
拿到指令),译指,运行指令,并将pc寄存器++;第一个和最后一个阶段都需要若干个时钟周期
综合来说:1个指令周期 - 若干时钟周期(取指) + 若干时钟周期(执行指令)

前2个阶段都是有控制器完成的,也就是CU,
而最后1个阶段是由算术逻辑单元ALU完成的,也就是ALU。

调度机制,分为三个层级:
长期调度:比如一个程序尝试运行,操作系统是否应该立即创建对应进程,并将进程状态改为就绪状态呢?
长期调度像阀门,用于限制系统中被短期调度管理的进程数量 -> 是为了减小短期调度的开销
由于进程可以分为 计算密集型和I/O密集型,所以长期调度会根据CPU, I/O利用率情况,选取合适的计算密集型或I/O密集型进程,交给短期调度 -> 为了有效控制系统中的资源利用情况,避免出现
激烈的资源竞争或者某项资源利用率过低的情况

      也就是长期调度,负责将进程状态从新生->就绪

短期调度:负责进程在 就绪,运行,阻塞状态间的切换。

中期调度 : 实际是换页机制的一部分。 它是从内存的角度考虑,当系统中的内存不够,将处理就绪或者阻塞这2中状态的进程设置为挂起就绪状态和挂起阻塞状态,并将挂起进程的内存也换入磁盘。当内存够时,又
被替换为就绪或者阻塞状态。

DMA使用的2个主要限制 (为什么写文件,写日志没用DMA):
1, 需要外设支持DMA协议
2,需要批量搬运数据,如果是少量,零碎的搬运,那么由于DMA协议,那么在总线上传输的数据更大了(DMA协议也会占用总线资源),这种cost可能导致更加劣化
3,(DMA搬运的数据不要求一定是连续的,这一点不是约束)

RDMA + CBDMA

LINUX内核的存储软件栈 (从上到下):
VFS 虚拟文件系统(包括页缓存机制等)
多种文件系统(比如ext4文件系统, ext3)
I/O调度器 (可能存在多个对存储设备的访问请求,以一定的顺序将请求发送给设备驱动)
设备驱动
tips : 上面的页缓存和I/O调度器的策略是否有可以优化的部分 ?

文件数据:文件的内容
文件元数据:存储文件数据的支撑性信息,包括文件模式,文件所有者,大小,文件访问时间等。

inode : 1,存放文件元数据 2,存放指向存储块的多级指针。
inode和文件是一一对应的。那么有多少类型的文件呢 :
linux 支持的文件类型 :常规文件 目录文件 符号链接文件 FIFO文件(管道) 套接字文件 字符设备文件 块设备文件

注意,一个文件的文件名不是这个文件的元数据,并没有存放在这个文件的inode中。 这引入了2种文件 :
而是存放在目录文件这个文件的inode中。 目录文件的核心就是 :存储文件名和inode的映射关系。
从上面目录就可以引入 硬链接 的概念 : 硬链接就是1个或2个目录文件下,又增加了一个新的文件名和原始inode的映射关系。 也就是说有2个映射关系(2个文件名),同时对应1个inode。
软链接 :和硬链接不同,它实在的增加了1个inode,也就是增加了1个文件,这个文件的内容就是原始文件的路径。当然,这还在其所在的目录,增加了1个映射()映射到这个新加的文件)

文件描述符: 该整数值是个索引值,是进程级文件描述符表的下标。该表中每一项本质是对inode的一层封装(实际不是直接存储inode,而是存了指向inode的指针,并且该指针是放在了全局的inode表,这里只是存储了指向全局inode表的指针),
打开文件返回文件描述符的本质:就是针对inode,在数组中分配一个文件描述符结构,并把数组下标,也就是整数fd,返回给进程。

  • 中间的全局文件描述符表,存储了指向inode的指针,以及偏移值(【当使用dup,或dup2重定向时,2个进程级文件描述符表,也就是2个fd,都会指向同一个中间的全局文件描述符表项,由于指向同一个中间表项,
    就可以共享同样的偏移值】)。

【ARM总线】
按照总线在soc系统的位置:
AMBA总线:片内总线,片内总线负责连接cpu芯片内部的各个模块。
pci/pcie总线:片间总线,负责连接外设。任何尝试生产的设备只要符合PCI规范,都能通过PCI插槽和CPU通信

有些总线访问使用自选锁的目的是,总线必须先占有,再使用,而自选锁来判断占有,效率更高。

【ARM函数调用】
进入子函数:
1, 压栈压了啥:压了 fp和lr。 fp是调试用的,用来推栈,有些编译器可能不加。 lr如果函数不会更进一步调用,那么lr可能也不会压栈。SP没有压栈的说法,随着子函数进入和退出先减多少,再加多少就行。

2, 父函数做了啥 : bl指令,把返回地址放在lr寄存器中,注意,这里不是压栈,只是让到了lr寄存器中。(当然还有保存入参到三个寄存器中)
子函数做了啥 : 负责压栈,负责压栈,负责压栈,就是上面说的fp和lr。只有子函数知道自己是否调用了更进一步的子函数,所以只要是否要保存lr,而且fp也是由子函数压栈,理论上由父函数压栈,只要约定好,都是可以的,但是ARM规定,如果fp压栈,必须由子函数来做。 (当然还有去除r0,r1,r2, 就是去除参数)

3,哪些是必须要压栈的:没有哪个寄存器是必须压栈的,fp是调试用,有些编译器就是省略了这个,比如选项"-fomit-frame-pointer"还专门用来忽略fp; lr的话,要看被调用的函数A,是否进一步调用了函数B,如果没调用,也不会对lr压栈; pc和lr,编译器不会压栈,除非指定 -mapcs-frame

子函数退出:
1,先把压栈的回复到寄存器,这里面就包括把lr寄存器恢复了。
2,执行指令 bl lr指定,跳回去。

ARM (压入返回地址):
调用者:
bl max
被调用者:
push {r11, lr} /* 序幕开始:保存帧指针和返回地址到堆栈*/

x86 (压入返回地址): 调用者call指令一条就自动完成了压栈:
调用者:
call 80483ed

【伙伴系统(大于2个页面)】
作用:避免外部碎片。核心优点:最大的特点是性能强大。2个伙伴块,地址只有1位不同,且差的这一位从块的大小可以算出。

2个内存块要是伙伴系统的一定要满足3个条件:地址连续,大小相同,是从同一个块中出来的。
缺点: 一个很小的块往往会阻碍一个大块的合并;申请的页不是2的幂次方,就会有浪费。

【slub分配器(小于2个页面走这里)】
作用:处理很小的内存申请,避免内部碎片。它总的效果就是维护了一堆小内存的池子,申释放都不会直接交给伙伴系统,而是把该大小内存放到池子中,以便以后申请再交还回去。

【SO的分布】
1,so的代码段和数据段是自己一块的。 2,so的堆,和整个进程的堆在一起的,so的栈,处于某个线程中。

【 缺页异常的场景 】 1,虚拟内存和物理内存有映射关系了,但是物理内存也没分配 - malloc 2,虚拟内存和物理内存都没有映射关系,比如内存不够操作系统的换页到磁盘,以及MMAP。

string_view :
1,优势,效率高: 它去掉前缀,后缀,或者算substr时,效率非常高: 内部只保存了字符起始和长度的位置,这几个操作只是挪动了指针而已
2, 劣势:1, 生命期易出错:它只是挪动指针,数据本地还在原来的string,或者char *,要注意生命期要考虑数据本体
2,一定要注意 data() 函数,它不像string的data()会自动带'\0'。 这完全看string_view 是谁的view,一般这个"谁"都是有结尾的"\0"的,包括常量字符串与string
可以转化为string:
string_view a = "hello";
string b = {a.begin(), a.end()};
可以转化为char * :data()

【maps文件】
从maps文件看,so被加进去后,自己的代码段和数据段自己靠在一起,但是堆是放在公共[heap]区,栈是放在对应线程的栈区。栈区每个线程都有1个,最下面的[stack]只是默认线程的栈区。

换页对象:代码段, 堆区栈区, 文件buf。 它们的换出策略都不同。代码段由于是只读,所以直接擦除,堆区栈区是换到swap区(就是块flash);文件buf,则回写到flash中文件。三种都可以释放物理内存。
注意: 上面说的堆区和栈区换出并不是所有linxu都支持,必须开启swap机制才会换。其它两项则都支持。

具体换哪一页呢: LRU算法: 核心操作就是每次访问某个物理页,就把物理页挪到队列尾部,而需要换出时,就把队列头部的换出即可。它的原理是局部性原理。

【写时拷贝(加载动态链接库以及进程fork2种常见会用到)和内存去重特性(OS自身不断扫描相同部分)】

【TLB】
TLB加速的是什么 - 加速的是多级页表的查询,只用查询一次TLB缓存即可。
TLB缓存的是什么 - 缓存的是多级页表的核心内容 - 虚拟页号->物理页号。 拿到物理页号后,还是要加上页内偏移(12位)
切换进程时,需要刷新切换TLB。
为啥不是切换线程时刷新呢 - 因为线程共享统一地址空间,所以同一个进程内不同线程,其虚拟物理页号,对应的是同样的物理页号。
【启示 - 所以虽然调度的单位是线程,但是同一个进程内的线程调度,少了TLB的刷新开销,不同进程的线程调度,增加了TLB的刷新开销。】

【大页内存】
大页的主要大小有2MB和1G。默认是4KB
大页的优化点:常见的是减少TLB MISS,还有1个优化点是: 减少缺页中断(因为一次缺页中断会加入一个物理页,现在一次性可以加入2M的物理页,原来4KB的物理页需要加载很多次才能把2M加完)。
大页的优化代价 : 会造成一定的内存消耗,比如原来一个物理页里面可能有2K是真正使用的,那么4KB的物理页面的情况下,加载后浪费了2KB,如果改为2M,浪费了很多。
具体使用 - linux原生手段:挂载hugetlbfs特殊文件系统到某个目录,然后通过mmap映射这个文件系统来使用共享内存, OS的mmap发现对端是这个,就可以对页表定制化处理,进行大页处理。
华为RTOS的话,是直接将exe目录下的所有可执行文件加载时,都使用大页。

【微架构级性能优化的cache - 有个印象就行】
1,TLB缓存 - 把虚拟地址和物理地址的映射,存放在TLB缓存里面,这样就可以不走几级页表,一级级访问来确认对应的物理地址。大小通常是1个页表(4K)或以上。
【优化思路 - 把紧密相关的大块处理,放在同样的文件中,使得其代码段相邻】 手段 - 大页机制,以及减少SO或者SO组合优化(SO加载到大页位置;多个SO代码组合到一起,以及消除SO跳转)
2,cache miss - 程序的局部性原理,读取指令时,那么认为随后一段也可能会执行,所以把他们加入cache中。
【优化思路 - 把紧密相关的代码行,放在临近的代码上,使得其代码段相邻,这样也可以使得其可能加入cache】
3,分支预测 - CPU内部处理,临近的汇编指令级别。CPU指令的流水线机制,使得自动把代码段相邻的下面几条指令加入流水线,
帮助编译器排布汇编指令 - if-else的likely和unlikely;函数重排(根据函数调用关系,将函数再代码端根据调用栈顺序进行重排);将多进程改为多线程(进程切换会导致TLB刷新 - )
perf的branch miss就是描述这个;

cache miss和TLB miss关系 : cache一般有l1 l2 l3,他们是接收物理地址作为输入,然后在cache中找内容并返回。所以:先有TLB miss,才会有 cache miss。

(注意,有些CPU l1 cache是接收虚拟地址作为输入的,这种情况下l1 cache的cache miss是发生在 TLB miss前)

管道 :
消息没有类型是字节流;发送和接收端各1个进程

消息队列 :
消息有类型,所以接收端可以选择类型接受或者全接受; 发送和接收端可以同时有多个进程。

相同点 :
发送端写满了都会阻塞,接收端无数据了也会阻塞。 内核都为其在内部维护了一段缓冲区。

【硬中断 https://www.itread01.com/content/1540961643.html 】
【x86】
NMI在x86構架上已經存在很長的時間(8086就已經有了)。一般的中斷INTR會受到IF的影響,當IF=1時,INTR被遮蔽,而NMI不受到IF設定的影響,CPU都會響應。

【ARM】
Arm處理器上有沒有NMI呢?這個需要分情況來說。眾所周知,現代的armCPU有Cortex-A/R/M序列處理器,Cortex-A為應用處理器,Cortex-R為實時處理器,Cortex-M為MCU處理器。
Cortex-M處理器有NMI。因為Cortex-M處理器用在MCU上,有的MCU用在非常關鍵的場景,比如火災監測,它需要不管當時MCU在處理什麼任務,
一旦監測到火災,必須立即報警和響應(噴水?)因此Cortex-M構架的CPU都有由NVIC支援的NMI,這個NMI是沒有任何辦法去mask的,即使在Cortex-M因為關鍵的錯誤進入了hard fault,甚至是lock up的狀態,NMI也可以被響應。不管怎麼設定PRIMASK,FAULTMASK都不能遮蔽NMI 。

Cortex-A和Cortex-R處理器沒有獨立的NMI。 在Cortex-R上對FIQ有特殊的處理,可以通過配置(非软件手段,CFGNMFI輸入訊號)將FIQ變成真正意義上的NMI(在ARM官网也提到ARMV7部分实现可以这样做,什么是ARMV7呢,cortex-A,cortex-R,cortex-M就是该架构的三种子实现)。
在Cortex-A和Cortex-R處理器中,有兩個中斷訊號,IRQ, FIQ, FIQ代表fast interrupt. FIQ在 arm的處理器裡有很長的歷史,早在armv4構架的arm7處理器已經存在,在armv8 64位構架之前還有與之對應的FIQ處理器模式。

【ARM处理器7种工作模式】
---------- 用户模式 ------------
用户模式 用户模式不能切换到别的模式
------以下全是特权模式 (特权体现在:1,可以访问ARM内部寄存器以及外设 2,特权模式可以切换到用户模式,用户模式无法切换到特权模式) --------
系统模式
---------如下5种又称为异常模式 (异常模式和系统模式的区别不只是名称上,核心在于如下几种异常模式有些特殊的寄存器是自己模式私有)-----------
IRQ模式
FIQ模式
管理模式 CPU上电后默认模式 + 软中断
中止模式 访问没有权限的内存地址,就会进入该模式
未定义指令终止模式 软件仿真是,如果

模式的更换 -
通过修改CPSR寄存器的[4:0]即可实现更改处理器模式

7种异常, 本质上7个异常向量地址 (对应上面5种异常模式):
Highest Reset
Data Abort (段错误或者非对齐访问都是这里)
FIQ
IRQ
Prefetch Abort
Lowest Undefined instruction and SWI。

优先级体现在:1,同时发生高优先级先被响应(也是要注意中断:中断中的fiq和data abort同时发生,虽然会先响应data abort,但是会穿插着执行fiq,把fiq执行完,再接着执行data abort。搜索“arm exception priority”即可看到arm官网描述)
2,嵌套(但是注意中断的情况有点特别,简单说,所有的异常都会导致irq无法嵌套,但是fiq除了复位和fiq,其它都可以打断: Disable interrupts. IRQs are disabled when any exception occurs. FIQs are disabled when an FIQ occurs and on reset. )

中断上半部下半部:
最大的不同是,上半部不可中断,下半部可以中断。习惯性的放在上半部的是不可被其它中断打断的任务。上半部的执行上下文应该就是中断的异常向量表走下来的上下文,由于
ARM关闭了中断(软件应该也要关闭中断(否则如果当前是irq,可能还是会被fiq打断));下半部的实现有,软中断,tasklet,工作队列。前2者的相同点是不能调用可能睡眠阻塞的函数;工作队列可以。

FIQ之所以fast體現在以下方面:

  1. FIQ和IRQ同時發生時,FIQ有更高的優先順序,先被處理。

  2. 在處理IRQ時如果FIQ沒有被mask, FIQ可以搶佔IRQ異常處理過程。

  3. 在發生IRQ異常,做IRQ異常處理過程中,CPU HW自動mask IRQ, 但是不mask FIQ.

但是FIQ不是真正意義上的NMI,因為它可以被軟體mask。通過設定CPSR.FIQ bit就可以(引入安全擴充套件和虛擬化擴充套件之外情況有點複雜,暫時不表)。
注意,linux-arm的情况下,linux并没有设置fiq

【区别ARM的软中断和LINUX的软中断】
swi :software irq,是ARM的一个软件中断指令,产生swi异常; - 我们常说的系统调用,通过软中断进入系统模式,就是指的这个。
softirq : linux kernel自己造的软中断,和硬件无关。 - 我们常说的中断下半部的软中断,就是指的这个,而不是arm的软中断模式。

softirq也有优先级,但是同一个cpu上,softirq不会强占,优先级仅仅体现在同时发生时,先处理高优先级的。

我们这样来理解为什么有softirq这个东西,什么时候有:

  • softirq 的触发时机:系统调用即将返回,硬件中断即将处理完,那些被softirq的被标记的(通常是硬件中断去设置的标记,因为常常这样标记,就表明还有工作要做,这样就是所谓的"下半部“),就会被处理。而
    由于这里已经不再屏蔽硬件中断,所以系统的响应性能提高了。
  • 原文:Whenever a system call is about to return to userspace, or a hardware interrupt handler exits, any 'software interrupts' which are marked pending (usually by hardware interrupts) are run (kernel/softirq.c).

那为啥又有tasklets:

  • 主要是因为3点softirq不具备的优点: 1, 本CPU不会被其它下半部机制抢占(softirq,tasklet,work queue),也就是不用担心重入问题。 2,其它CPU不会运行同一个tasklets,不用担心并发问题。 3,可以在运行时
    动态指定新的tasklets(softirq编译时写好了就不能增加了)。

拷贝构造函数于赋值 (区别的核心不是有没有=号,而是是否先创建了一个对象):
下面的例子中,都有=号,但是就不一样 :
A a; 默认构造
A b; 默认构造
b = a; 赋值函数

A a; 默认函数
A b = a; 拷贝构造函数函数

c++string的实现 epoll 真正安全的单例 https://cloud.tencent.com/developer/article/1606879 ? 产品的cmake 工程
如何避免多余的复制,左值右值操作符
异常的研究: http://baiy.cn/doc/cpp/inside_exception.htm
字面值常量 ?
怎样避免类被继承
C++中 构造函数(constructor): X() 拷贝构造函数(copy constructor):X(const X&) 拷贝赋值操作符(copy assignment):operator=(const X&)
移动构造函数(move constructor):X(X&&) C++11以后提供 移动赋值操作符(move assignment):operator=(X&&) C++11以后提供 析构函数(destructor):~X()
有什么讲究 ?

constexptr 函数

constexptr 函数常见用法 ,2个要求满足即可,且相比inline函数,没有空间消耗代价,更好:
1,返回类型,输入参数类型是 int ,char 等字面值类型。
2, 输入参数必须是常量,或者回溯调用栈,上层的上层的上层。。是常量

inline 函数

inline函数 :
原理:会扩大函数的大小,用空间换时间。
注意: 不能超过10行,不能包含while for ,switch等,编译器会视为比较复杂,改为普通函数代替。

如何禁止1个类被继承

移动构造函数,移动赋值函数是为了解决这种场景:

1,需要用1个对象a,初始化另1个对象b,或者直接用a给b赋值。
2,a在上面初始化完后,就立马丢弃不用了 - 比如函数return a等。 这种对象都是临时对象,我们给它一个定义,叫做“右值”。

( 那么上面的构造函数,或者赋值函数,就是要传入1个马上不用的对象,或者传入1个“右值”,更进一步,传入一个它的引用,就叫“右值引用”。)

所以我们希望这样设计: 设置1类特殊构造函数和赋值函数,它直接把临时对象的指针,或者资源改为自己的,能直接改,而不是复制,是由于这些资源来自临时对象的,不用考虑随后改为自己的,
临时对象还有业务的作用,导致临时对象用不了了。 作为收尾,要注意,析构函数要作区分,就是区分资源已经被改给别的对象了,常见的就是判断指针为nullptr。

基于以上思路,所以我们约定,1,定义这类构造函数和赋值函数,他们的特点是有 && 。 2,析构函数作点区分处理。 3,随后当return 临时对象时,或手动调用std::move转为右值引用的场景,典型是std::move并
给另一个对象赋值时,编译器就可以调用这些构造函数。当然,你不定义的话,编译器就没犯法了。

所以 && ,也就是右值引用的核心是: 和函数的调用者形成一种约定,使得调用者知道调用该函数(移动构造函数,移动赋值函数 ,普通函数等)后,该变量不再有效,所以函数设计者指定入参/&& ,调用者就必须指定 std::move,引导调用者明白的意识
到,传入后值就无效了,达到性能的提升。 注意,如果入参是右值引用,和普通引用一样,都不会导致创建2个参数。

【特别典型的应用场景是】 (试验时编译参数是 g++ main.cpp -fno-elide-constructors ,这样可以避免优化,看出效果),返回值是临时的:这种需要:
1,定义移动构造和移动赋值函数 - 这样会使得这个对象的构造更快速。 使用时,也需要使用std::move来标注希望调用移动构造/移动赋值。
2,普通函数入参直接定义右值引用 - 这样某些情况下,可以避免对象内部的大量拷贝。 (注意并不是说入参的临时对象会构造个对象,右值引用本身和左值引用一样都是引用,入参不会有新对象了,
而是说,用了右值引用,里面可以放心把入参直接掏空,函数返回后,外界不会再用入参变量了)
3,函数返回值声明为右值引用 objectType&& funcXX()。这样外界还要结合调用移动构造函数或者移动运算符才有意义 - 优化的核心,还是在于移动构造函数或者移动预算符

虚函数不允许使用缺省参数

重载是一个类内部的,不涉及父类和子类, 父类和子类间的是隐藏和重写(就是正经的多态特性),他们的区别如下:

【 子类是否有virtual不影响下面两条结论 】
父类函数无virtual 函数名相同 (参数和返回值任意组和)参数相同/不同/返回类型相同/不同 - 父类的函数被隐藏了,父类函数是调用不到的

父类函数有virtual 函数名相同 参数相同 - 正经的覆盖,即多态的特性,同时返回值必须相同,不然编译不过。
参数不同 返回值相同/不同 - 父类的函数被隐藏了,父类函数是调用不到的

构造函数与异常机制:

构造函数与异常机制

构造函数中如果发生了异常,则不会调用析构函数。这种做法要注意,容易出现内存泄露问题。

析构函数与异常机制. 析构函数中一旦抛出异常,就会直接导致程序退出 【异常机制使得一旦捕获不到,一层层往上退,但析构函数特殊,没有上层,一旦往上退,就会就会退到上层,上层直接导致进程退出。】

所以有2个做法:
a, 不要在析构函数中写try catch
b, 一定要写,则catch一定要捕获完成,不要捕获不完整。

反之,构造函数中没有返回值,可以用异常机制。

0611:

gic:
中断,就是让CPU停下来,响应CPU中断引脚对应的外部设备的请求。最开始每个外设有1个CPU的中断引脚,后面设备逐渐增多,但是CPU的中断
引脚无法不断增多,并且,中断同时出现,还要响应更重要的。 于是ARM上引入了 GIC,即通用中断控制器。

中断的处理中,需要考虑的主要是:
送中断信号:
CPU当前没有处理中断,GIC同时来了多个中断,那么送哪个中断信号到CPU中断引脚呢:
送优先级最高的中断
1,选择最高优先级的中断:
GIC输入端有多个中断信号,此时要决定送哪个到CPU中断引脚,这时就涉及到中断的优先级。

2,选择好后,确定是否立刻送:
a,如果是正在处理的中断,和要送的中断,是同一个中断,必须等到之前的中断处理完,才会再处理这个中断 (是丢弃了这个中断吗)

上半部: 中断处理中,会线关闭中断,执行完成后,再通知GIC打开中断,且这个中断已处理完毕,GIC就可以继续响应该中断(这之前GIC是直接丢了这个中断,还是丢了所有中断?)

上半部和下半部,我们平时注册的中断处理,属于哪个,比如 request_irq()

0609:

分段与分页是2种并列的机制,不是互相配合的机制。

虚拟内存的页机制,怎么解决物理内存不足的问题:
1,换页:比如物理内存低到一定阈值,就会触发换页,把最近不常用的页换出,写到磁盘。当然,后续再访问时再把页调进来。
2,按需分配:申请了内存(这里的本质是占用了虚拟内存,分配了虚拟页) 不会立刻映射到物理内存,只有访问时才映射。

物理内存分配:
伙伴系统分配器 :分配的最小单位是物理页。
SLAB分配器:但是有很多申请的内存大小通常是机制或几百个字节,远小于1个物理页,就出现了SLAB分配器。

碎片分为外部碎片和内部碎片。外部碎片是动态产生的,也就是有很多不相邻的小内存块,他们无法被分配;内部碎片只是内存块内部占不满,被分配出来。

0513:
vector的全部删除- clear
1,clear只释放每个元素,不释放vector自身的空间,为了效率还会预留。 也就是说,clear后,size()会为0,但是 capacity() 不变。
// 那么自然情况下,这个vector什么时候释放呐 ,是vector退出了其作用域时,比如一个函数里面有个vector,退出这个函数,那么这个vector的空间会释放。
如果我要想提前释放有办法吗,可以通过下面的swap函数来做 :
void clearVec(vector& vec)
{
vector
tmp;
tmp.swap(vec);
}

2,上面clear说道会释放每个元素,但是要注意,如果是指针,不会释放,比如vector中放入的是 对象,clear时会释放,但如果放入的是指向对象的指针,则不会释放。

vector的单个或者范围删除 - erase
相比clear的不同点
1,erase可以清除一个或一段范围的内容
2,会返回删除元素的下一个元素的迭代器
相比clear的相同点:1,减少size,不会减少 capacity 。 2,如果不是指针,erase会释放元素,如果是指针,不会释放

你可能感兴趣的:(2022-04-13 - 程序员c++技术积累)