[TOC]
答案:
- **链接方式不同:**静态链接是直接把静态库编译进目标文件。动态链接没有把动态库编译进目标文件,在程序运行的时候加载动态库,使用了地址无关代码的技术,装载时重定位,在链接时只做语法检查。程序环境需要指定动态库查找路径。
- **执行速度上: **相对来说静态库更快一点,动态库慢一点。
- **当库文件发生变更时:**如果接口发生变化,静态库和动态库都需要重新编译。但是如果接口没变,使用静态库的程序需要重新编译,使用动态库的程序只需要重新编译库文件就行。
下面是生成库和链接库的命令:
1.生成方式:
两者的第一步都需要生成目标文件,以.o结尾。
//-c表示只编译不链接 -o表示指定输出文件名称
g++ -c add.cc -o add.o
静态库生成方式:
//linux下.a文件 windows下.lib文件
ar rcs libapi.a add.o
动态库生成方式:
//shared表示生成动态库 -fPIC表示会生成与地址无关的代码,并且装载时会帮忙进行重定位
//linux下.so文件 windows下.dll文件
g++ -shared -fPIC -o libapi.so add.o
2.链接方式:
编译代码时使用静态库
g++ -static main.cc -o static_main -L./ -lapi -I./
编译代码时使用动态库
g++ main.cc -o dynamic_main -L./ -lapi -I./
程序运行的时候,系统的加载器根据导入表加载程序需要的动态库,把动态库加载到程序的地址空间中,然后解析所有的外部符号,把程序代码里对函数或变量的引用与 DLL 中的实际地址进行绑定。一般会延迟加载,在程序首次调用动态库里面的符号时才加载。
- 可以让声明与定义分离: 头文件一般包含函数、变量、类的声明。具体的实现细节一般放在源文件里面。
- **能代码复用:**头文件里面的函数、类和全局变量的声明在多个源文件中共用。用
#include
可以在多个源文件使用同一个头文件,避免重复声明,提高复用性。 - **能加快编译速度:**如果有源文件有改动的话,只有这个源文件需要重新编译,其他包含这个头文件的源文件不需要重新编译。可以减少编译时间,提高编译效率。
- **可以提供外部接口:**可以通过包含库的头文件,直接使用库的功能,不需要知道库的实现细节。
- **用头文件可以直接展开:**编译之前就把头文件的内容在源文件中展开,能检查未声明的符号。
cmake_minimum_required(VERSION 3.15) # 规定的最低版本 不写也行
project(cmake_study) # 起一个项目名
add_subdirectory(lesson1_1) # 添加子目录 会自动区寻找子目录下的CMakelists.txt
//lesson1_1目录下的内容 有一个add.cpp add.h main.cpp CMakelists.txt
//lesson1_1 CMakelists.txt内容
add_executable(lesson1_1 main.cpp add.cpp)
四个流程:预处理,编译,汇编,链接
1. 预处理
展开头文件,做宏替换,去掉注释。
2. 编译
编译的输出结果是汇编文件,输入是.i结尾的文件(预处理的结果) 输出是.s结尾的文件
3. 汇编
会变成机器码的二进制文件,如果一个函数只有声明是可以经过预处理,编译和汇编的到达现在这步二进制文件,在最后的链接阶段才会找函数的具体实现。
4. 链接
将多个源文件的二进制文件和库链接到一起。
在项目目录下新建build
目录,这个build
目录与顶层CMakeLists.txt
文件同层,然后cd
到build
下,cmake ..
表示将编译与build
同层的CMakeLists.txt
,然后将生成的文件都放在当前的build目录下。
cmake .. -DCMAKE_VERBOSE_MAKEFILE=ON
表示展示camke
过程。其中包括生成makefile
等过程。
在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分。每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
- 栈段,包括局部变量和函数调用的上下文等,从高地址向低地址增长;
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长;
- 堆段,包括动态分配的内存,从低地址开始向上增长;
- BSS 段,包括未初始化的静态变量和全局变量;
- 数据段,包括已初始化的静态常量和全局变量;
- 常量区:常量的存储位置,程序结束后由操作系统释放;
- 代码段,包括二进制可执行代码;
在这 6 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc()
或者 mmap()
,就可以分别在堆和文件映射段动态分配内存。
执行new
步骤:
- 调用
operator new()
分配内存,通常底层用malloc,得到void*, - 把void指针转成对应类型指针
- 调用对应类型构造函数
执行delete
步骤:
- 调用对应类型的析构函数
- 调用
operator delete
释放内存。
new
和delete
是C++关键字,operator new()
和operator delete()
是c++的函数,其内部会调用malloc
和free
,而且这两个函数可以重写。
- malloc 申请内存,有两种方式:
- 如果用户分配的内存小于 128 KB,通过 brk() 申请内存,释放时不会把内存归还给操作系统,会缓存在 malloc 的内存池中;
- 如果用户分配的内存大于 128 KB,通过 mmap() 申请内存,释放时会把内存归还给操作系统,内存释放
-
malloc() 会预分配更大的空间作为内存池。
-
malloc不是系统调用,brk和mmap是系统调用。
-
brk和mmap分配的都是虚拟内存,在第一次访问以分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;
- 可以,但不提倡。
- 如果是简单类型,比如int类型,malloc申请的本身没有构造函数,可以调用delete。
- new之后调用free可能会导致没有调用虚析构函数,导致内存泄漏。
- new自动计算所需内存大小,malloc需要手动计算
- new和delete是C++关键字,malloc和free是库函数
- new返回的是对象类型的指针,malloc返回的是void*
- new分配失败会抛出异常,malloc分配失败返回的是NULL
- malloc只分配内存,不调用构造。在堆上分配虚拟内存,不一定拥有物理内存。new会先申请内存,然后调用构造函数,构造函数会初始化成员变量,会分配物理内存。
malloc会多申请16字节,多出来的 16 字节就是保存了该内存块的信息。比如有内存块的大小。free 会对传入进来的内存地址向左偏移 16 字节,得到内存块的大小,就知道要释放多大的内存了。
内存泄漏定义:内存泄漏是指程序在动态分配内存后,不用的时候没释放,随着程序运行时间的增加,可能会耗尽系统资源。
内存泄漏场景:
new
和delete
malloc
和free
没有成对出现- 系统资源泄漏,比如socket和文件描述符没释放
- 基类的析构函数没定义成虚函数。
- 在释放对象数组时没有使用
delete []
而是使用了delete
- 智能指针循环引用
解决方法:通过智能指针可以规避大部分内存泄漏,linux环境可以使用工具valgrind
。
unique_ptr定义:
- 对动态分配的单⼀对象所有权的独占管理。确保只有⼀个
unique_ptr
可以拥有指定的内存资源。 - 默认情况下,
unique_ptr
大小等于原始指针的大小,一般为8字节。 unique_str
不允许拷贝,只允许移动。移动语义和右值引用可以对std::unique_ptr
的所有权转移。- 可以自定义删除器(但是这可能会产生大
unique_ptr
对象)
shared_ptr定义:
std::shared_ptr
允许多个智能指针共享同一块内存资源。内部使用引用计数管理,当计数为零时,释放资源。但可能存在循环引用的问题。shared_ptr
一般是16字节大小,比正常指针大一倍shared_ptr
其中包含一个原始指针和一个指向控制块的指针。控制块内包含引用计数,自定义删除器。shared_ptr
不能指向一个数组,因为默认的删除器调用delete,不是delete []。
shared_ptr
控制块的生成时机:
- 用
make_shared
创建shared_ptr
- 通过
unique_ptr
构造shared_ptr
- 向
shared_ptr
的构造函数中传入一个裸指针。
share_ptr
会重复释放堆对象,比如说两个控制块与同一个T object
关联,而且连个控制块的计数不一样,就会多次释放堆对象。
较深解释:
auto pt = new Test;
std::shared_ptr<Test> spt1(pt);
std::shared_ptr<Test> spt2(pt);
此时两个Control Blcok
中的引用计数都为1,在spt1
和spt2
都会进行释放,会释放两次。所以对于同一个对象,不管有多少个share_ptr
都应该对应同一个Control Block
。**解决方法:**用shared_ptr
构造shared_ptr
这样会调用shared_ptr
的拷贝构造,而且不会生成新的Control Block
。然后最好不要用裸指针指向堆对象,将new Test
的返回的地址(形成右值)放入shared_ptr
的构造函数中。可以避免后续用裸指针构造shared_ptr
的情况。
//用`spt1`构造`spt2` 用`shared_ptr`构造`shared_ptr`
std::shared_ptr<Test> stp2(spt1);
//在构造shared_ptr时 直接用堆区返回的右值地址构造,避免后续用裸指针构造shared_ptr
std::shared_ptr<Test> spt1(new Test);
**定义:**栈区有个个指针a,b指针分别指向堆区对象,这2个堆区对象里面也有指针互相指向。就会产生循环引用。释放时,堆区的两个对象计数不为0,不会释放。
**解决方法:**将其中一个指针改成weak_ptr
就可以了,不增加引用计数就不会影响堆内存释放。
**定义:**主要解决循环引用问题。weak_ptr
不能单独使用,通常从shared_ptr
创建。对于共享内存,如果没有shared_ptr
指向这片区域,就会被释放。但是对于weak_ptr
来说,如果weak_ptr
指向这片区域,仍然会被释放,weak_ptr
的存在并不能延长堆内存的生命周期。本质上来说,weak_ptr
没有堆内存空间的所有权,只有借用权。
较深解释:
auto spw = std::make_shared<Widget>();
std::weak_ptr<Widget> spw(spw);
spw = nullptr;
虽然weak_ptr
不能掌握堆对象的生死,但是weak_ptr
知道这个堆上的对象存不存在。
wpw.expired();//返回true表明资源已经释放
//weak_ptr不可以直接使用,需要先转成shared_ptr
std::shared_ptr<Widget> spw1 = wpw.lock();//如果wpw堆内存被释放,spw1则为空
std::shared_ptr<Widget> spw2(wpw);//如果wpw堆内存被释放,构造方式会抛异常
-
std::shared_ptr
不直接支持数组类型的内存管理,因为它默认使用delete
而不是delete[]
来释放内存。 -
可以自定义删除器让数组内存能够被正确释放
可以,反之不行。
指向对象的并发读写不安全,计数器是线程安全的,是原子操作。
-
好处可以减少代码重复(能稍微少写点,作用不大)
-
更安全,可以防止new对象和把指针赋给
shared_ptr
这两个操作被打断。 -
std::make_shared
与new
相比有效率上的提升。如果先new申请对象的空间,然后交给shared_ptr
管理的时候会再申请控制块的堆空间,会申请两次增加开销。如果用make_shared
会一次将空间都申请下来。
较深解释:
更安全:
processWidget(std::shared_ptr<Widget>(new Widget), computepriority());
这里有三个动作,如果先执行new Widget,然后执行computeriority(),然后再把内存空间的指针赋值给shared_ptr。这时如果computeriority()函数出现异常,那么会打断shared_ptr的构建。
上面存在异常风险的本质是因为在new Widget和将指针赋值给shared_ptr这两个操作被打断,所以将他这两步合并即可,用make_shared<Widget>()
效率提升解释
看上面这张图片,是shared_ptr
,它在堆区上有两个对象一个是T Object
和Control Block
。所以如果先new一个对象会先申请对象的空间,然后交给shared_ptr
管理的时候会再申请Control Block
的堆空间,会申请两次增加开销。如果用make_shared
会一次将空间都申请下来。但是对于unique_ptr
,只有一根指针,用不用make_unique
没有什么区别。
- 使用自定义删除器只能用new
- 不能通过{}初始化指向的对象 (因为{}不能完美转发)
auto spv1 = std::make_shared<std::vector<int>>(10, 20);//定义10个20的vector
auto spv2 = std::make_shared<std::vector<int>> ({10, 20});//错误 不能通过编译
- 如果类中重载了
operator new/delete
,使用make_shared
不会执行重载函数
这个问题出现的原因是内联函数的特性。内联函数是在编译时展开的,直接把代码粘贴到函数调用处,而不是在运行时通过函数调用机制调用的。所以,编译器需要在编译时就能看到内联函数的定义,而不只是声明。如果你在头文件中声明了一个内联函数,但是在实现文件中提供了定义,那么在包含这个头文件的其他源文件中,编译器只能看到声明,看不到定义。这就会导致“内联函数未定义引用”的错误。所以,对于内联函数,我们通常在头文件中同时提供声明和定义,确保编译器在编译任何包含这个头文件的源文件时都能看到内联函数的定义 。
static关键字作用:
- 修饰局部变量:在静态存储区分配内存;⾸次函数调⽤中初始化,之后的函数调⽤不再初始化;局部作⽤域内可⻅。
- 修饰全局变量:静态存储区(全局数据区)分配内存;整个⽂件内可⻅,⽂件外不可⻅
- 修饰函数:整个⽂件可见,⽂件外不可⻅,可以避免函数同名冲突
- 修饰成员变量:所有对象共享这个成员变量;需要在类外初始化;不需要对象实例化就可以访问
- 修饰成员函数:这个函数就不能访问⾮静态成员变量,也不能调⽤⾮静态成员函数,因为没有this指针;这个函数只能访问静态成员;不需要对象实例化就可以调用这个函数。
C和C++中的区别:
C只能修饰局部变量和全局变量、函数。C++还能修饰成员变量和成员函数
extern
用来声明变量或者函数,extern
是声明不是定义,不分配存储空间。
如果一个函数或者变量,想在其他文件中使用,有两种方式:
-
可以用
extern
在头文件中声明,然后引用头文件,其他文件再去包含这个头文件。 -
直接在使用的文件中
extern
。
在同一个文件中,如果一个全局变量在下面定义的,我在当前位置调用不了这个变量,也可以用extern
先声明一下,就能调用到下面的全局变量了。
让编译器这部分代码按C语⾔的⽅式进⾏编译,就是能让C++代码调⽤其他C语⾔代码,因为c++会对函数名修饰,c不会。。比如 C++ 程序需要调用由C 写的库或函数时,可能会因为c++得名称修饰导致链接错误。
**gcc:**用于编译c代码,链接c的标准库,也可以编译c++代码,但是如果c++代码使用c++标准库的内容就会报错:undefined reference
。
**g++:**用于编译c++代码,链接c++的标准库。
表⽰变量随时可能被改变,编译后程序读取时候直接从地址读⼊,避免编译器优化从寄存器中读。主要用于多线程环境中,变量被多个线程共享。
可以
**原因:**const表⽰程序内部只读不能改变,volatile表示程序外部条件变化下改变且编译器不会优化这个变量。
**定义:**RAll (Resource Acquisition ls Initialization)资源获取即初始化,使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存(heap)、网络套接字,互斥量,文件句柄等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
整个RAII有四个步骤:
-
设计一个类封装资源
-
在构造函数中初始化
-
在析构函数中进行销毁操作
-
用的时候定义一个该类的对象
**定义:**RTTI(Runtime Type Identification)是“运行时类型识别”的意思。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。C++的数据类型是在编译期就确定的,不能在运行时更改。因为多态,C++中的指针或引用本身的类型,可能与它实际代表的类型并不一致,我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息。
详细解释:
但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型的变量对应的类型。为什么会出现RTTI这一机制呢?这和C++语言本身有关系,C++是一门静态类型语言,其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用本身的类型,可能与它实际代表的类型并不一致,有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就有了运行时类型识别需求。和Java相比,C++要想获得运行时类型信息,只能通过RTTI机制,并且C++最终生成的代码是直接与机器相关的。
不能。
//非法结构体声明
struct Date
{
int day = 23,
month = 8,
year = 1983;
};
public继承:
- 基类的public成员 -> 子类的public成员
- 基类的protected成员 -> 子类的protected成员
- 基类的private成员只能通过基类的接口访问
protected继承
- 基类的public成员 -> 子类的protected成员
- 基类的protected成员 -> 子类的protected成员
- 基类的private成员只能通过基类的接口访问
private继承
- 基类的public成员 -> 子类的private成员
- 基类的protected成员 -> 子类的private成员
- 基类的private成员只能通过基类的接口访问
因为值传递会让拷贝构造的无限递归。
**在类的成员函数中是可以调用delete this的。delete的本质是调用一个或多个析构函数,然后释放内存。delete this会调用本对象的析构函数。**当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。
深入解释:
在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用他。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。
https://blog.csdn.net/qq_31597573/article/details/51438996
含有纯虚函数的类被称为抽象类,抽象类只能作为派生类的基类,不能定义对象。
菱形继承时最上面的基类会被继承两次,调用最上面基类的成员变量时会产生歧义,而且也会造成空间的浪费,需要用虚继承解决问题,让中间的两个类都虚继承最上面的基类。虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。这个被共享的基类就称为虚基类。
**虚继承底层实现原理:**通过虚表偏移的方式实现虚继承,子类只继承一次父类的父类。
static_cast
在编译期完成,使用数值类型,指针类型,继承关系的向上类型转换。
dynamic_cast
在运行期完成,适用于具有继承关系的类或指针类型之间的转换。动态转换会进行类型检查。在继承关系中向上向下转换均可,向上转型始终安全,向下转换有类型检查。
const_cast
作用是去掉表达式的常量属性。
reinterpret_cast
于进行各种类型之间的强制转换。它是一种非常危险的类型转换,因为它会改变数据的原本含义。
为什么内存对齐:
- 不是所有的硬件平台都能访问任意地址上的任意数据的,有些只能在某些地址处取某些特定类型的数,否则抛出硬件异常。
- 效率,内存对齐可以减少cpu开销,(避免一个数据被分割放在两个cpu cache上,这样读取这个数据需要读取两次将这个数据拼接而成。对齐之后可以从一个cache中直接读取到。不知道对不对) 为了访问未对齐的内存,处理器需要两次内存访问;而对齐的内存访问仅需要一次访问。
深入解释:
- 数据成员对齐规则,
struct
,class
的数据成员,第一个数据成员放在offset为0的地方,以后每个成员存储的起始位置都要从该成员大小或者成员的的子成员大小(只要该成员有子成员,比如所数组,结构体等)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。 - 结构体作为成员:如果一个结构A嵌套了其他结构体B,要从B最大元素整数倍地址开始存储(
struct a
有struct b b
里有char
,int
,double
等元素,那b应该从8的整数倍开始存储) - 收尾工作,结构体的总大小,也就是
sizeof
的结果,必须是其内部最大成员的整数倍,不够大要补齐。
//1.
struct xx
{
long long _x1;//8
char _x2;// 1->4char大小是1字节 但是下一个元素是int int要从4的整数倍开始 所以这里补三个字节 占4个字节
int _x3;//4
char _4[2];// 2->8 struct整体大小必须是最大元素整数倍 最大元素是8 所以补6 总共24
}
原因:编译器需要区分这个空类的不同实例,分配一个字节,使得空类的实例拥有独一无二的地址。
模板函数特例化:必须为所有模板参数提供实参。就是要把特例的参数全写出来。
类模板特例化:不必为所有模板参数提供实参。
在算法中我们可能会定义简单的中间变量或者设定算法的返回变量类型,这时候需要知道迭代器所指元素的类型是什么,所以需要用到traits
。
如果需要用到迭代器所指元素的类型,就直接在迭代器的定义中将迭代器所指元素类型起个value_type
别名即可。
template <class T>
struct MyIter {
typedef T value_type; // 内嵌型别声明
T* ptr;
MyIter(T* p = 0) : ptr(p) {}
T& operator*() const { return *ptr; }
};
这样可以直接使用迭代器所指类型。
MyIter::value_type
但是C++中的原生类型的指针类型比如int*
里面就没有写value_type
,此时就需要用traits
。
template <class Iterator> // 专门写一个iterator_traits类用来获取迭代器相关的各种类型
struct iterator_traits {
typedef typename Iterator::iterator_category iterator_category; //迭代器类型
typedef typename Iterator::value_type value_type; //迭代器指向的对象类型
typedef typename Iterator::difference_type difference_type; //容器元素间的间隔
typedef typename Iterator::pointer pointer; //迭代器指向的对象的指针
typedef typename Iterator::reference reference; //迭代器指向的对象的引用
};
//然后为原生指针写偏特化的版本就可以了
template <class T>
struct iterator_traits<T*> {
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
};
序列:array、vector、deque、list、forward_list、string
关联:map、set、multiset、multimap、unordered_map、unordered_set、 unordered_multiset、unordered_multimap
容器适配器:stack、queue、priority_queue
**定义:**底层是数组,连续的内存空间,支持随机访问。有三个迭代器start、finish、end_of_storage
,分别是起始字节位置,当前最后一个元素起始位置和内存空间的末尾位置。空间不够时,会扩容,申请1.5/2倍空间,把原来的数据拷贝到新的内存空间,释放原来的内存空间。
**vector扩容倍数:**微软是1.5倍,gcc是2倍。
vector迭代器失效情况:
-
erase删除位置之后的迭代器、指针、引⽤失效,因为元素前移了
-
insert,如果扩容了全部失效,没扩容就插入位置之后失效,因为后面元素后移了。
-
reserve
:仅仅设置capacity
这个参数resize
:容量变大,填充初始值;容量变小,不调整容量,只把前n个元素填充为初始值
**定义:**带头结点的双向循环链表,非连续的内存空间,不支持随机访问。
问题:如果list的数据域很小,小于前后向指针,导致空间占用很大。Redis中的解决方案是压缩列表。
deque
有一个中控器map,每个map指向一段连续的内存空间,每段连续空间有一个迭代器。deque
的start和finish一开始指向map的中间,让前后空间保持相同。
底层使用红黑树, 增删改查logn
红黑树:节点有红黑两种颜色,根节点是黑色的,叶子节点是null并且是黑色的,红色节点的子节点必须是黑色,从任意节点到叶子节点的路径都包括相同数量的黑色节点(黑子节点的数量称为黑高)
-
AVL平衡规则太过严格,每次操作几乎都涉及左旋右旋。
-
AVL适合读取查找型密集任务,红黑树适插入密集型任务。
底层用哈希表,用一个vector数组存储哈希值,并且使用拉链法、链表解决冲突
▪ 插入:vector、deque插入之后的位置失效,list、forward_list、map、set插入操作不失效
▪ 删除:vector、deque删除之后的位置失效,list、forward_list、map、set仅删除位置失效;递增当前iterator即可获取下⼀个位置
▪ 扩容:内存重新分配全部失效
▪ unordered迭代器意义不大,stack、queue、priority_queue没有迭代器
▪ 数据量很大使用快速排序
▪ 递归过程中,分段之后数据量很小,使用插入排序,数据大致有序时候为O(n),快排取元素存在不确定性,快排在数据本身有序的时候是最慢的O(n^2)
▪ 递归过程中,递归层次过深,使用堆排序处理,递归层数多浪费时间堆排序最好最坏都 是nlogn
std::move()
将左值转换为将亡值,所属右值。move
虽然叫移动语义,但是没有进行移动,只是转换成右值。纯右值也可以std::move()
。
完美转发:一个函数给另一个函数传参的时候,原参数是左值/右值,新函数还能保持左值/右值。
**左值:**指向特定内存的具名对象,可以在等号的左面,能够取地址,具名。前置自增自减是左值。
右值: 只能在等号右面,不能取地址,不具名。右值分为两种:纯右值和将亡值。
- 纯右值:纯右值有 字面量(例如10),返回非引用类型的函数调用,后置自增自减,算数、逻辑和比较表达式。例外char *p = "hello,world" `是例外它会被分配空间,可以取地址。
- 将亡值:
c++11
新引入的,与右值引用(移动语义)相关的值类型。将亡值将会触发移动构造或者移动赋值构造,然后进行资源转移。可以转移堆上的资源,不可以转移栈上的资源。
区分左值和右值的方法:看是否能取地址。比较典型的例子:不能对(x++)
取地址(因为返回的是临时变量),能对(++x)
取地址。
int geta() {
int a = 10;
return a;
}
int x = geta();
上面这段代码会产生两次拷贝,在调用geta()
结束后局部a会被销毁,所以会用一个临时变量接收a的值(此时产生一次拷贝),然后将临时变量拷贝给x(产生第二次拷贝)。
左值引用:主要目的是避免对象拷贝。
右值引用 :主要目的是 实现移动语义 通常是对堆上资源的转移,这样对象赋值时,避免重新分配资源。右值引用可以通过std::move()
指向左值,std::move()
会将左值变为右值。
左值引用和右值引用都是左值,因为是具名的。
深入解释
C++11表达式类型:
移动操作:
int &y = -1;//错误 左值引用不能接收右值
int &&y = -1;//正确 右值引用可以接收右值
右值引用仍然是左值,所以会出现完美转发
万能引用接收一个左值(比如为int类型),T会被推导为左值引用,然后和后面的两个引用号发生折叠,变成左值引用int & 万能引用接收一个右值,T会被推导为右值引用,然后和后面的两个引用发生折叠,变成右值引用。
template <tyepname T>
void func(T &¶m) //此为万能引用固定写法
{
return;
}
- 启动一个线程,需要明确等待它结束(与之汇合),还是让它独自运行(与之分离)。如果在thread对象销毁时还没决定好,
thread
的析构函数将调用terminate()
终止整个程序。 - **若等待线程完成:**调用
join()
,join()
只能调用一次,thread
对象曾经调用过join()
,线程就不可再汇合。 - **若让线程分离:**调用
detach()
,并且分离时线程还未结束运行,那它将继续运行,在thread对象销毁很久后依然运行,它只有最终从线程函数返回时才会结束运行。调用detach()
后,会令线程在后台运行,就不能直接和这个线程同通信了,也不能等待它结束,也不能获得与它关联的thread
对象,因而无法汇合线程,分离后的线程在后台运行,其归属权和控制权转移给C++运行库(runtime library
,又名运行库),能保证线程退出,与之关联的资源都会被正确回收。分离出去的线程成为守护线程,往往长时间运行,几乎在程序的整个生存期内,它们都一直运行,执行后台任务。
**lock_guard定义:**是RAII机制下的锁,locak_guard()
实现基本的功能-加锁。unique_lock()
是对lock_guard()
的扩展,允许在声明周期内再调用lock
和unlock
切换锁的状态。lock_guard
是不可移动的,即不能拷贝、赋值、移动,只能通过构造函数初始化和析构函数销毁。不能手动解锁。
**unique_lock定义:**也是RAII机制下的锁。unique_lock()
是对lock_guard()
的扩展,允许在声明周期内再调用lock
和unlock
切换锁的状态。unique_lock
是可移动的,可以拷贝、赋值、移动。unique_lock
提供了更多的控制锁的行为,比如锁超时、不锁定、条件变量等。unique_lock
支持手动解锁。
所以在条件变量condition_variable
传入锁的时候需要unique_lock
,因为过程中需要手动释放锁。
-
有
constexpr
关键字的表达式在编译器执行。 -
constexpr
修饰函数返回值,尽可能让其被当做⼀个常量,编译期间没有被计算出来,会被当成⼀个普通函数处理。
constexpr int func(int i){
return i + 1;
}
int main(){
int i = 2;
func(i); //普通函数
func(2); //编译期间就会被计算出来
}
final
和override
是在C++11中引入的。
final
:
- 禁止当前类进⼀步派生
- 指定某个虚函数不能在派生类中被覆盖。
class B final : public A {}
virtual void Func() final {}
override
:指定子类的一个虚函数复写基类的虚函数,保证该重写的虚函数与基类的虚函数具有相同的签名;如果基类没有声明这个虚函数,编译报错。解决问题:本意想重写父类中的虚函数,但是函数签名不一致,导致没有重写,如果使用了override
必须要重写,这样就可以在编译期就检测出问题。举例:virtual void Func() override {}
explicit
:修饰构造函数,只能显式构造,不能被隐式转换。
宏定义缺点:
- 会导致代码膨胀:宏定义是文件替换,需要对代码进行展开,会存在较多的冗余代码。
- 无法进行类型检查:不能编译前就检查好类型是否匹配,而只能在编译时才知道,所以不具备类型检查功能。
- 宏定义不能访问成员变量。
inline内联函数:
- 相当于把内联函数中的内容在调用内联函数处展开。
- 不用进行函数调用(栈帧开辟与回收,参数压栈),直接执行函数体。
- 不能包含循环、递归、switch等复杂操作。
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
- 内联函数可以访问成员变量。
- 内联函数相比于宏来说,在代码展开时,会做安全检查,类型检查和自动类型转换(同普通函数),宏定义不会。
- 内联函数不可控,就算标为内联函数,编译器觉得不行,它就不是,决定权在编译器。
-
虚函数可以是内联函数,内联函数是可以修饰虚函数的,但是当虚函数表现为多态时不能内联。
-
内联是在编译期建议编译内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现多态性时不可以内联。
答:模板函数不能是虚函数
原因:
- 首先模板函数并不是函数,需要特定的类型去实例化成为函数。定义一个函数模板,是不生成任何函数的,只有当调用它时,才会根据的类型去实例化成为特定的函数。
- 而virtual函数是要写入虚函数表的,是必须要存在的。因此,模板函数是不能声明为virtual的。你可能会想到纯虚函数,纯虚函数只是表明这个函数还未实现,但是已经在父类的虚表里存在了。
深入解释:
**模板函数不能是虚函数。**这是因为模板函数并不是一个具体的函数,而是需要特定的类型去实例化成为函数。只有当你用代码去调用它时,才会根据你的类型去实例化成为特定的函数。而虚函数是要写入虚函数表的,是必须要存在的。因此,模板函数是不能声明为virtual的。另外,模板函数的实例化是在编译时进行的,而虚函数的调用是在运行时进行的。因此,模板函数的虚函数表在编译时是未确定的,无法在运行时进行虚函数的动态绑定。
不可以,没有意义。
原因:
-
虚函数是为了实现运行期函数和对象(类的实例)的动态绑定,通过对象的指针或引用访问被指向的对象,只要有继承关系,被访问的对象的实际类型可以和指针或引用指向的类型不同。
-
如果没有对象,那么这种多态就没有意义,因为根本不存在需要在运行期确定对象类型的必要。
所以只从属于类而不和具体对象相关的静态成员函数作为虚函数是没有意义的,因此语言禁止这么做。
什么时候生成默认拷贝构造函数?https://blog.csdn.net/weixin_45031801/article/details/133993523
- 类成员变量也是一个类,该成员类有默认拷贝构造函数。
- 类继承自一个基类,该基类有默认拷贝构造函数。
如果不提供,会进行浅拷贝,按字节复制。
浅拷贝危害:
- 多个对象拥有堆上的相同资源
- 多个对象拥有相同文件句柄,socket
什么时候触发拷贝构造函数?
- 用一个对象去构造另一个对象。赋值:
A a(b); A a = b;
- 函数传参
- 函数返回值
**封装:**目的是隐藏细节,特性是控制访问权限,
**继承:**目的是继承父类,不选哟修改原有类的基础上扩展功能 ,特性是权限继承(public protected private ),这几个关键字的作用是基类成员在子类成员中的最高权限,权限不能超过继承时的关键字。
**多态:**静态多态,比如函数重载。动态多态通过虚函数重写实现,目的是一个接口多种形态,通过实现接口重用,增强可扩展性。
动态多态通过重写基类的虚函数实现,运行时确定。如果是基类就从基类的虚函数表中寻找函数。如果是子类就从子类的虚函数表中寻找函数。
虚函数表是虚函数地址的数组,指向的是代码段里的一个地址。
虚函数表的创建时机:
- 在编译器编译的时候生成的,发现
virtual
修饰的函数时。 - 在程序运行时,虚函数表存放在代码区,程序运行之前在只读数据段中。
虚函数表指针的创建时机:
- 在类对象构造函数中会创建虚函数表指针,指向虚函数表。虚函数表指针是每个对象实例都有的。一个类的虚函数表只有一个。浅拷贝的情况下两个对象会共享一个虚函数表指针,其中一个对象释放了,会造成另一个对象的虚函数表指针丢失,用深拷贝避免。
- 继承的情况下,虚函数表指针的赋值过程:
- 调用基类的构造函数,把基类的虚函数表地址赋值给vptr
- 调用子类的构造函数,把子类的虚函数表地址赋值给vptr
静态多态
通过函数重载实现,编译器确定。函数重载允许在同一作用域中声明多个功能类似的同名函数,这些函数的参数列表、参数个数、参数顺序或者参数类型不一样,注意不能通过返回值类型区别函数重载。函数重载是通过函数名修饰实现的。编译过程:预编译、编译(语法分析,同时进行符号汇总)、汇编(生成函数名到函数地址的映射,方便以后通过函数名找到函数定义位置从而执行函数)、链接。
动态多态
通过虚函数重写,运行时实现的。
- **函数指针:**是一个指向函数内存地址的变量,函数地址的概念表示非内联函数,其中一个用途是作为回调函数。
- **仿函数等同于函数对象,**主要作用是因为函数指针无法和STL的一些组件搭配,产生更灵活的变化。
- **lambda表达式:**可以方便的定义一个匿名函数,是一段匿名的内联代码块。lambda可以访问作用域内的动态变量,即可通过捕获列表访问一些上下文中的数据。让函数的定义在函数使用的地方,维护性和可读性很高。**lambda原理:**就是在编译的时候将它转化为一个函数对象,就是重载了()的类,根据
lambda
参数列表重载operator()
,lamada捕获的值相当于函数对象的成员变量。 function
:因为C++中有很多不同的函数对象,函数指针、仿函数、lambda和bind产生的函数对象,所以需要一个类型来描述这些函数对象。function
是一个抽象了函数参数以及函数返回值的类模板。function
是用来描述函数对象的类型是把任意一个函数包装成对象,该对象可以保存、传递和复制。赋值不同的function
对象可实现动态绑定,实现类似多态的效果。
//普通函数、类静态成员函数
void hello() {
cout << "hello, world!";
}
int main() {
function<void(int)> f_hello1 = hello;
f_hello(1);
function<void(int)> f_hello2 = &hello; //此处取地址和不取地址是一样的
f_hello2(1);
function<void(int)> f_hello3 = &StaticFunc::hello;//这是某个类的静态成员函数
f_hello3(2);
return 0;
}
//仿函数
class Hello {
public:
void operator() (int count) {
i += count;
cout << "Hello::hello mark:" << i << endl;
}
void operator() (int a, int b) {
cout << "Hello:hello mark: a+b= " << a + b << endl;
}
int i = 0;
};
int main() {
function<void(int)> f_hello4 = Hello();
f_hello4(4);
}
//成员函数
class CHello {
public:
void hello(int count) {
cout << "StaticFunc::hello mark" << count << endl;
}
}
int main() {
function<void(CHello *, int)> f_hello5 = &CHello::hello;
CHello c;
f_hello5(&c, 5);
}
//lambda表达式
int main() {
int i = 0;
auto f_hello6 = [&i](int count) -> void {
++i;
cout << " lambda hello mark:" << count << "i = " << i << endl;
}
f_hello6(6);
}
//bind是一个函数适配器 通过绑定函数以及函数参数的方式生成函数对象的模板函数,提供占位符,实现灵活绑定
void hello(int count) {
cout << "StaticFunc::hello mark" << count << endl;
}
class CHello {
public:
void hello(int count) {
cout << "StaticFunc::hello mark" << count << endl;
}
}
int main() {
auto f_hello7 = bind(&hello, 9);//这个地方写hello也行,会自动转成函数指针
f_hello7();
CHello c;
auto f_hello8 = bind(&CHello::hello, &c, 8);
f_hello8();
//可以通过占位符 保留不想绑定的参数
auto f_hello9 = bind(&CHello::hello, &c, placeholders::_1);
f_hello11(1000);
return 0;
}
//起始bind就是生成了一个函数对象 传入了函数和参数都作为类的成员变量,然后在()重载函数中调用传入的函数和变量
//举例
class BindHello {
public:
BindHello(function<void(int)> _fn, int _count) : fn(_fn), count(_count) {}
void operator()(){
fn(count);
}
private:
function<void(int)> fn;
int count;
};
int main() {
auto f_hello10 = BindHello(&hello, 10);
f_hello10();
}
如果一个基类的析构函数没有设置为虚函数。此时有一个基类指针指向一个派生类对象,通过基类指针删除派生类对象,只会调用基类的析构函数,编译器并不知到实际删除的是派生类对象,可能会导致内存泄漏。
io网络模型,当read=0时,write=-1&&errno=EPIPE。这两种的区别是,read是读端关闭,另外一个是写端关闭。io多路复用模型,EPOLLRDHUP读端关闭,EPOLLHUP读写段都关闭。
**重载规则:**同一个作用域内,可以有一组相同的函数名,不同参数列表(参数顺序不同也算重载)的函数,就叫重载。
**重载原理:**函数名修饰,两个同名函数编译后,为_z3addid和_z3adddi,以z开头,3是名字长度,add为函数名id为参数首字母,所以上面连个函数可以重载。
//下面两个函数算重载
void add(int a, double b);
void add(double a, int b);
c中:
- 不支持
class
关键字 - c中不能定义函数,仅能定义数据成员。
C++中:
struct
默认的数据访问控制是public的,class默认的访问控制是private的,struct可以添加private/public修饰符。- struct也可以继承,struct默认的继承访问权限是public。class默认的继承方式为private。
- class关键字可以用于定义模板参数,struct不能。
总结
c++支持struct主要是为了兼容c。struct更像是一个数据结构,class更像是一个对象。在c++中的struct加了访问控制权限
Linux系统由Linux系统内核和系统级应用程序组成。
- 内核提供系统最核心的功能:CPU调度,调度内存,调度文件系统,调度网络通讯,调度IO等
- 系统级应用程序:文件管理器,任务管理器,图片查看,音乐播放。
Linux的目录结构:Linux没有盘符的概念,只有一个根目录/,所有文件都在它下面
命令格式:command [-options] [parameter]
Linux命令:
su #登录root用户
exit #退出登录
ctrl+alt+F3 #转为命令行界面
ctrl+alt+F1 #图形界面
firewall-cmd --list-ports #查看防火墙开启端口
firewall-cmd --zone=public --add-port=7373/tcp --permanent #开启指定端口
firewall-cmd --reload #重新加载防火墙
tty #查看当前是那个终端
ps -aux #查看当前所有进程
cd #进入目录
pwd #打印当前工作目录
ls #查看目录和文件 不加参数默认当前目录 -a表示所有 -l表示列表形式 -h显示文件大小的单位 选项可以混合使用 -al -lh
echo $$ #查看当前终端进程的id
#命令最后加上&代表后台运行
vim命令行模式输入e可以刷新文件
getconf GNU_LIBPTHREAD_VERSION #查看当前pthread库版本
man + 系统调用名 #查看命令或者系统调用详细内容
ifconfig #查看网卡信息
netstat #查看网络相关信息的命令
unzip #解压zip文件 -d xxxxxx 表示解压到xxxx目录
sudo apt-get install libmysqlclient-de #安装mysql相关连接库 tinywebserver需要用到
mkdir [-p] 路径 #Make Directory 创建文件夹 -p表示自动创建不存在的父目录
touch 路径 #创建文件
cat 路径 #查看文件 cat显示文件的全部内容
more 路径 #查看文件内容 more支持翻页 空格翻页 q退出
cp [-r] 参数1 参数2
#复制文件或文件夹 -r用于文件夹的复制 表示递归 参数1表示要复制的文件 2表示目的地
mv 参数1 参数2
#移动文件或文件夹 参数1为被移动的文件或文件夹 参数2为目的地 不存在就改名
rm [-r -r] 参数1 参数2 ...参数N
#删除文件或文件夹 -r用于递归删除文件夹 -f用于强制删除 参数代表删除的文件 可以有多个参数 删除的文件可以有通配符 例如test*表示所有以test开头的文件
su - root #切换root用户 exit退出
which 命令 #查找所使用的一系列命令的程序文件存放在哪
find 起始路径 -name "被查找文件名" #按名字查找文件
find 起始路径 -size +|-n[kMG]
#按大小查找文件 +和-表示大于和小于 n表示大小数字 kMG表示大小单位 k表示kb M表示MB G表示GB
find / -size -10k #表示从根目录查找小于10kb的文件
grep [-n] 关键字 文件路径
#从文件中通过关键字过滤文件行 -n在结果中显示匹配行的行号 关键字表示过滤的关键字,带有空格或其他特殊符号,建议使用""将关键字包裹起来 文件路径表示要过滤的文件路径,可作为内容输入端口
wc [-c -m -l -w] 文件路径
#-c 统计bytes数量 -m 统计字符数量 -l统计行数 -w统计单词数量 参数是文件路径 可作为内容输入端口
| #管道符 将管道符左边命令的结果 作为管道符右边命令的输入 下面举例
cat test.txt | grep itheima
cat test.txt | wc -l
ls | grep test
cat test.txt | grep itcast | grep itheima
echo 输出的内容 #在命令行内输出指定内容,复杂内容可以用""包裹
`` #反引号 被``包裹的内容会作为命令去执行
> #将重定向左侧命令的结果 覆盖写入到符号右侧指定的文件中
>> #将重定向左侧命令的结果 追加写入到符号右侧的指定的文件中
tail [-f -num] Linux路径 #查看文件尾部的内容 -f表示持续跟踪 -num表示尾部多少行 默认10行
su [-] [用户名] #-是可选的,表示是否在切换用户后加载环境变量 参数:用户名,表示要切换的用户,省略表示切换到root, 切换用户后exit回到上一个用户,或者ctrl+d
sudo 命令 #sudo可以为普通的命令授权,临时以root身份执行,但不是所有用户都有sudo权力
为普通用户配置sudo认证
1.切换到root用户,执行visudo命令 自动打开/etc/sudoers
在文件的最后添加
jianglong ALL=(ALL) NOPASSWD: ALL
其中NOPASSWD: ALL表示使用sudo命令 无需输入密码
最后通过wq保存退出
Linux中关于权限的管控级别有两个级别
1.针对用户的权限控制
2.针对用户组的权限控制
用户组管理
以下命令需要root用户执行
groupadd #创建用户组
groupdel #删除用户组
用户管理
1.useradd [-g -d] 用户名 #-g指定用户的组 不指定用户的组会创建同名的组并自动加入 -d指定用户的HOME路径,默认/home/用户名
2.userdel [-r] 用户名 #-r删除用户的HOME目录,不使用-r,删除用户时,HOME目录保留
3.id [用户名] #查看用户所属组
4.usermod -aG 用户组 用户名 #及那个指定用户加入指定用户组
getent passwd #查看当前系统中有哪些用户
getent group #查看当前系统中有哪些用户组
ls -l
权限控制信息 所属用户 所属用户组
drwxrwxr-x 13 jianglong jianglong 4096 Jan 17 20:43 code
drwxrwxr-x 5 jianglong jianglong 4096 Jan 20 20:22 fastdfs
drwxrwxr-x 3 jianglong jianglong 4096 Dec 9 20:03 git_learn
drwxrwxr-x 3 jianglong jianglong 4096 Jan 20 19:36 heima_distributed_file_system
-rw-rw-r-- 1 jianglong jianglong 23 Jan 26 21:03 hello2.txt
-rw-rw-r-- 1 jianglong jianglong 21 Jan 26 21:02 hello.txt
drwxrwxr-x 2 jianglong jianglong 4096 Aug 21 15:29 java
drwxr-xr-x 13 jianglong jianglong 4096 Aug 8 15:47 neo4j-community-5.11.0
drwxrwxr-x 3 jianglong jianglong 4096 Jan 17 20:03 tools
权限控制信息
第1位:-表示文件 d表示文件夹 l表示软连接
第2-4位:表示所属用户权限
第5-7位:表示所属用户组权限
第8-10位:表示其他用户权限
r表示读权限
w表示写权限
x表示执行权限
chmod [-R] 权限 文件或文件夹 #修改文件、文件夹的权限信息(只有文件(夹)的所属用户或root用户可以修改) -R对文件夹内的全部内容应用同样的操作
chmod举例:
chmod u=rwx,g=rx,o=x hello.txt #将hello.txt文件权限修改为:rwxr-x--x,其中u表示user所属用户权限,g表示group组权限,o表示other其他用户权限 也可以使用权限的数字序号:权限可以用3位数字来代表,第一位表示用户权限,第二位表示用户组权限,第三位表示其他用户权限,其中r为4,w为2,x为1
举例:
chmod 751 hello.txt #将hello.txt文件权限修改为:rwxr-x--x
chown [-R] [用户][:][用户组] 文件或文件夹 #-R同chmod 对文件夹内全部内容应用相同规则 用户表示修改所属用户 用户组表示修改所属用户组 :用于分隔用户和用户组
chown root hello.txt #将hello.txt所属用户修改为root
chown :root hello.txt #将hello.txt所属用户组修改为root
chown root:itheima hello.txt #将hello.txt所属用户修改为root 用户组修改为itheima
chown -R root test #将test所属用户修改为root 并对文件夹下所有文件修改
快捷按键:
ctrl + d #退出或登出(退出某些特定程序的专属页面 例如python)
history #查看历史命令
ctrl + r #输入内容 去历史命令中内容 回车可以直接执行 左右键获得该命令
ctrl + a #光标跳到命令开头
ctrl + e #光标跳到命令结尾
ctrl + 左 #向左跳一个单词
ctrl + 右 #向右跳一个单词
ctrl + l 或者 clear #清空终端内容
yum:RPM包软件管理器,用于自动化安装配置Linux软件,并可以自动解决依赖问题
语法: yum [-y] [install | remove | search] 软件名称
选项: -y自动确认 无需手动确认安装或卸载过程
intall:安装
remove:卸载
search:搜索
apt:DEB包软件管理器,用于自动化安装配置Linux软件,并可以自动解决依赖问题
语法: apt [-y] [install | remove | search] 软件名称
选项: -y自动确认 无需手动确认安装或卸载过程
intall:安装
remove:卸载
search:搜索
systemctl命令
systemctl start | stop | status | enable | disable 服务名
start #启动
stop #关闭
status #查看状态
enable #开启开机自启
disable #关闭开机自启
系统内置服务:
NetworkManager #主网络服务
network #副网络服务
firewalld #防火墙服务
sshd, ssh服务(FinalShell远程登录Linux)
软链接:包含了到原文件的路径信息 硬链接:对原文件起了一个别名
在系统创建软链接,可以将文件、文件夹链接到其它位置 类似快捷方式
ln -s 参数1 参数2
-s选项表示创建软链接
参数1:被链接的文件或文件夹
参数2:要链接去的目的地
date命令
在命令行中查看系统的时间
date [-d] [+格式化字符串]
-d按照给定的字符串显示日期 一般用于日期计算
修改linux时区为东八区
rm -f /etc/localtime
sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
ping命令
ping [-c num] ip或主机名
选项:-c检查的次数 不使用-c选项 将无限次持续检查
参数:ip或主机名 被检查的服务器的ip地址或主机地址
wget命令
wget是非交互式的文件下载器 可以在命令行内下载网络文件
wget [-b] url
选项:-b可选 后台下载 会将日志写入到当前工作目录的wget-log文件
参数:url 下载链接
curl命令
curl可以发送http网络请求 可用于:下载文件、获取信息等
语法: curl [-O] url
选项:-O 用于下载文件 当url是下载链接时 可以使用此选项保存文件
参数:url 要发起请求的网络地址
查看进程
语法: ps [-e -f]
选项: -e 显示出全部的进程
选项: -f 以完全格式化的形式展示信息
查看系统资源占用
top命令查看CPU、内存使用情况,类似windows的任务管理器 默认每五秒刷新一次
语法:直接输入top即可 按q或者ctrl+c退出
top命令选项:
选项 功能
-p 只显示某个进程的信息
-d 设置刷新时间 默认5s
-c 显示产生进程的完整命令 默认是进程名
-n 指定刷新次数 比如top -n 3 刷新输出3次后退出
-b 以非交互非全屏模式运行 以批次的方式执行top 一般配合-n指定输出几次统计信息 将输出重定向到指定文件 比如top -b -n 3 > /tmp/top.tmp
-i 不显示闲置或无用的进程
-u 查找特定用户启动的进程
释放缓存:echo 1 > /proc/sys/vm/drop_caches
磁盘信息监控
使用df命令 查看硬盘的使用情况
df [-h]
使用iostat查看cpu 磁盘的相关信息
语法: iostat [-x][num1][num2]
选项: -x 显示更多信息
num1: 数字 刷新间隔 num2: 刷新几次
网络状态监控
sar命令查看网络的相关统计
语法:sar -n DEV num1 num2
选项:-n 查看网络 DEV表示查看网络接口
num1:刷新间隔 num2:查看次数
环境变量
在Linux系统中执行:env命令即可查看当前系统中记录的环境变量
环境变量是kv型结构
PATH=/home/jianglong/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/local/jdk/bin:/usr/local/jdk/jre/bin
PATH记录了系统执行任何命令的搜索路径,当执行任何命令,都会按照顺序从上述路径中搜索要执行的程序的本体
echo #PATH
自行设置环境变量:
Linux环境变量可以用户自行设置,其中分为:
1.临时设置 语法:export 变量名=变量值
2.永久生效
- 针对当前用户生效 配置在当前用户的: ~/.bashrc文件中
- 针对所有用户生效 配置在系统的: /etc/profile文件中
- 通过语法: source配置文件,进行立刻生效
压缩和解压
压缩格式:
1.zip:Linux Windows MacOS常用
2.7zip:Windows常用
3.rar:Windows常用
4.tar:Linux MacOS常用
5.gzip:Linux MacOS常用
tar命令
Linux和Mac系统常用常用有2中压缩格式 后缀名分别是 .tar .gz
.tar 称之为tarball 归档文件 即简单的将文件组装到一个.tar的文件内 并没有太多文件体积的减少 仅仅是简单的封装
.gz 也常见为.tar.gz gzip格式压缩文件 即使用gzip压缩算法将文件压缩到一个一个文件内 可极大减少压缩后的体积
针对这两种格式 使用tar命令均可以进行压缩和解压缩的操作
语法:tar [-c -v -x -f -z -C] 参数1 参数2 ... 参数N
-c 创建压缩文件 用于压缩模式
-v 显示压缩 解压过程 用于查看进度
-x 解压模式
-f 要创建的文件或要解压的文件 -f选项必须在所有选项中位置处于最后一个
-z gzip模式 不使用就是不同的tarball模式
-C 选择解压的目的地 用于解压模式
zip命令
使用zip命令 压缩文件为zip压缩包
语法: zip [-r] 参数1 参数2 ...参数N
-r 被压缩的包含文件夹的时候 需要使用-r选项
示例:
zip -r test.zip test itheima a.txt
unzip命令
使用unzip命令 解压zip压缩包
语法: unzip [-d] 参数
-d 指定解压去的位置
参数 被解压的zip压缩包文件
在服务器上搭建一些服务的时候,经常要用到screen命令。某些服务开启的时候需要占据一整个控制台,如果关闭了就会导致进程终止。这就成了类似单进程的效果。而screen命令就是为了能够在服务器上开启多个控制面板(screen),用以容纳不同的任务。
1、创建一个screen
screen -S ***
2、查看当前screen列表
screen -ls
#detached:相当于最小化窗口
#attached:相当于当前窗口
#dead:相当于死了的窗口
3、重新进入已经创建的screen
screen -r ***
4、退出screen窗口
输入一下快捷键:ctrl + a + d
5、kill screen窗口
在screen窗口内部时 :
输入一下快捷键:ctrl+d
在screen窗口外部时:
1.使用screen窗口的名字
screen -S session_name -X quit
2.回到screen窗口,再退出screen窗口
1.回到screen窗口
screen -r session_name
2.利用exit退出screen窗口,退出窗口后session窗口被自动kill
exit # 可能需要多次exit命令,因为screen中正在运行的程序可能也需要使用exit命令才退出(先使用exit命令退出screen中正在运行的程序,再使用exit命令退出screen)
tcpdump host 192.168.10.3 port 16379 -i 网卡名 -w /tmp/r1.cap
进程和线程的区别:
-
**所属关系:**一个进程拥有多个线程,所有线程共享进程的虚拟地址空间。线程是更轻量级的进程。
-
**本质区别:**进程是资源分配的基本单位,线程是cpu调度的基本单位。
-
**上下文切换:**进程切换包括cpu寄存器,程序计数器,虚拟地址空间(用户空间和内核空间(pcb))。线程切换包括cpu寄存器,程序计数器,栈空间,本地存储空间。
-
**健壮性:**进程之间的环境是隔离的,所以一个进程崩溃,不会影响其他的进程。线程之间的运行环境不是隔离的,访问共享变量需要加锁,而且一个线程崩溃,会导致整个进程崩溃。
-
**进程通信和线程通信:**线程间的通信开销更小一点,可以直接访问变量,或通过信号量和互斥锁。进程间的通信开销更大些,需要借助操作系统。
-
**使用场景:**在任务能够拆分,且加锁比较简单,会考虑多线程。使用多进程的情况:Redis的AOF日志重写。因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,修改数据时会发生「写时复制」,父子进程就有了独立的数据副本,就不用加锁来保证数据安全。
https://www.bilibili.com/read/cv8582207/?spm_id_from=333.999.0.0 程序喵大人
僵尸进程:子进程已终止,但父进程未读取其退出状态,导致占用进程表项资源,必须通过 wait()
函数清理。
孤儿进程:父进程已终止,孤儿进程由 init
进程接管,不会产生问题,操作系统会自动管理这些进程。
切换时机:
- 系统调用:这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。
- 异常:比如缺页异常。
- 外围设备中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU去执行与中断信号对应的处理程序比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
- 进程或者线程调度。
csdn: https://blog.csdn.net/weixin_63566550/article/details/131039726
函数栈空间分配
-
调用函数会在栈空间给函数分配一段空间,局部变量都在函数的占空间内。
-
当函数调用完毕后,函数的占空间会被释放,其中的局部变量也会被释放,
-
函数栈顶指针叫rsp,栈底指针叫rbp。
内存覆盖用于同一个进程内,内存交换用于不同进程间
内存覆盖:由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可以把用户空间分为一个固定区和若干个覆盖区。将经常活跃的部分放在固定区,其余部分按照调用关系分段,首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统将其调入覆盖区,替换覆盖区中原有的段。
内存交换:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)。
回答问题:
如果写文件时还没有调用fflush(与write等效),那么进程宕机,数据会丢失。如果调用了fflush,此时数据是在pagecache中的,操作系统不宕机就没事。
深入解释
stdio在用户态是有缓冲区的,作用是减少系统调用的,用stdio的库函数通过调用fflush可以将用户缓冲区的数写道内核里的page cache然后通过调用fsync可以将page cache中的数据刷到磁盘上。fputs,fopen等等是写在用户缓冲区的,需要fflush才能写到page cache中,而write是原生的系统调用,会直接写道page cache中。page cache的作用是减少磁盘io次数。
有两种磁盘io方式:缓存文件io(就是有pagecache的方式),还有一种直接文件io从用户缓冲区直接写入磁盘。
大文件使用直接文件io,因为大文件会填满page cacahe, 导致其他小文件读写的时候都跑到磁盘上去了,会降低page cache的命中率。
系统调用流程:
- 带着系统调用号的软中断,中断号是0x80,触发中断。
- 保存运行现场,比如cpu寄存器和堆栈指针,然后陷入内核态。
- 根据软中断的中断号在中断向量表中找到
system_call
,然后根据系统调用号在系统调用表中找到处理函数来区分不同的系统调用,然后内核线程执行代码,然后通过中断将函数的返回值返回,并从内核态回到用户态。
**中断:**中断分为硬中断和软中断。硬中断举例:网卡接收数据后由DMA写到ring_buffer中然后发硬中断给cpu。软中断举例:从用户态切换到内核态。中断包括:中断号(软中断都是0x80),中断处理程序和中断向量表。发起中断的时候需要携带中断号,然后可以根据中断号从中断向量表中找到中断处理程序,然后中断处理程序由内核线程来执行。
**系统调用:**系统调用是内核提供给用户空间的编程接口。
**系统调用的发生的时机:**用户态需要操作进程的外部资源(操作系统的公共资源,内核资源,硬件,这些资源只能由内核操作),此时需要由用户态转变为内核态。
**发生系统调用的流程:**应用程序调用库函数,库函数调用系统调用,由用户态转为内核态,在内核态操作具体的资源。
不一定,如果是阻塞IO,并且IO未就绪,会引起进程或线程的切换。非阻塞IO不会引起线程切换。
线程: 不同任务需要大量共享数据或者频繁通信的情况线程更好。
进程: 对于不同任务之间不需要进行大量交互, 上下文切换不频繁的就可以使用进程,比如守护进程, 不需要主任务进行交互, 同时可以防止多线程下线程崩溃导致的整个服务的崩溃。
因为操作系统中的进程是在虚拟内存中,通过映射的方式访问物理内存,这样会导致进程间是隔离的,所以需要进程间通信。 进程通信目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 资源共享:多个进程之间共享同样的资源。
- 进程间的同步/互斥
- 进程控制:有些进程希望完全控制另一个进程的执行,如 Debug 进程,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
**支撑通信的基石:**共享内核空间,共享内核页表。在fork()
子进程的时候,会初始化页表(用户页表),然后拷贝内核页表。每个进程的用户页表不一样,内核页表是一样的。不同进程使用同一份内核页表这样就实现了共享内核空间。
- **管道:**管道是在内核中维护的缓冲区,管道拥有文件的特质:读操作、写操作。可以按照操作文件的方式对管道进行操作。分为匿名管道和有名管道,匿名管道没有文件实体,一般用于父子进程,和兄弟进程间的通信。有名管道有文件实体, 但不存储数据,可以用于没有亲缘关系的进程间通信。
- **信号:**是在软件层次上对中断机制的一种模拟,目的是让进程知道已经发生了一个特定的事情,让进程执行信号处理程序。信号的特点是简单,不能携带大量信息,满足条件才发送,优先级比较高。比如 alarm 定时器到期将引起 SIGALRM 信号,Ctrl+C 给进程发中断信号。
- **共享内存:**共享内存允许两个或者多个进程共享物理内存的同一块区域。共享内存段是进程用户空间的一部分,所以无需内核介入。管道需要将用户缓冲区数据复制到内核缓冲区,所以共享内存更快。
- **消息队列:**可以让不同进程把格式化的数据流以消息队列形式发送给其他进程。管道的方式是无格式化字节流的。消息队列是内核管理的消息链表。
- 信号量 : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它一般和互斥量配合,实现进程对临界区的同步互斥。
- **socket:**可以用于不同主机之间的进程通信。
深入解释:
-
管道 : 也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式, 所有的 UNIX 系统都支持这种通信机制。管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体, 但不存储数据。可以按照操作文件的方式对管道进行操作。一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写 更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
-
创建匿名管道:
int pipe(int pipefd[2]);
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提 出了有名管道(FIFO),也叫命名管道、FIFO文件。匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘 关系)之间使用。 -
有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样 即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此 通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的 I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一 个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的 名称也由此而来:先入先出。有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:
- FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。
- 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
- FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。
通过命令创建有名管道 :
mkfifo
管道名 通过函数创建有名管道 :int mkfifo(const char *pathname, mode_t mode);
一旦使用mkfifo
创建了一个 FIFO,就可以使用 open 打开它,常见的文件 I/O 函数都可用于 fifo。如:close、read、write、unlink 等。FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是 从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。
-
-
信号 : 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也 称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号 可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程发送一个中断信号。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给 相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的 内存区域。
- 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
- 运行 kill 命令或调用 kill 函数。
使用信号的两个主要目的是:让进程知道已经发生了一个特定的事情,强迫进程执行它自己代码中的信号处理程序。
信号的特点: 简单 不能携带大量信息 满足某个特定条件才发送 优先级比较高
SIGINT
终止进程SIGQUIT
终止进程SIGKILL
终止进程 可以杀死任何进程SIGSEGV
终止进程并产生core文件SIGPIPE
终止进程信号产生但是没有被处理 (未决) - 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集) - SIGINT信号状态被存储在第二个标志位上 - 这个标志位的值为0, 说明信号不是未决状态 - 这个标志位的值为1, 说明信号处于未决状态
这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较 - 阻塞信号集默认不阻塞任何的信号 - 如果想要阻塞某些信号需要用户调用系统的API
在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了 - 如果没有阻塞,这个信号就被处理 - 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
-
共享内存 : 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种
IPC
机制无需内核介 入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC
技术的速度更快。- 调用
shmget()
创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。 - 使用
shmat()
来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。 - 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存, 程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
- 调用
shmdt()
来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存 了。这一步是可选的,并且在进程终止时会自动完成这一步。 - 调用
shmctl()
来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之 后内存段才会销毁。只有一个进程需要执行这一步。
- 调用
-
消息队列 : 消息队列是进程间通信的最主要方法之一,相比于其他方法而言,消息队列成功克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。在多进程之间需要协同处理某个任务时能够合理的进行进程间的同步以及数据交流。消息队列是 UNIX 下不同进程之间可实现共享资源的一种机制,UNIX允许不同进程将格式化的数据流以消息队列形式发送给任意进程。消息队列(Message Queue,简称MQ)是由内核管理的消息链接表,由消息队列标识符标识,标识符简称队列ID。消息队列提供了进程之间单向传送数据的方法,每个消息包含有一个正的长整型类型的数据段、一个非负的长度以及实际数据字节数(对应于长度),消息队列总字节数是有上限的,系统上消息队列总数也有上限。MQ传递的是消息,也就是进程间需要传递的数据,系统内核中有很多MQ,这些MQ采用链表实现并由系统内核维护,每个MQ用消息队列描述符(qid)来区分,每个MQ 的pid具有唯一性。如下图:在进程间通信时,一个进程A将消息加到由内核管理的MQ 末端,另一个进程B在MQ中获取消息(获取信息时不遵循先进先出的规则,也可以按照消息类型字段获取消息)
-
信号量 : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,实现进程、线程的对临界区的同步及互斥访问。
内存映射定义 : (Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
深入解释:
-
1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
答:不能 void * ptr = mmap(...); ptr++; 可以对其进行++操作 munmap(ptr, len); // 错误,要保存地址 munmap不能成功
-
2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样? 错误,返回MAP_FAILED open()函数中的权限建议和prot参数的权限保持一致。
-
3.如果文件偏移量为1000会怎样? 偏移量必须是4K的整数倍,返回
MAP_FAILED
-
4.
mmap
什么情况下会调用失败?- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
prot PROT_READ | PROT_WRITE
第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
-
5.可以open的时候
O_CREAT
一个新文件来创建映射区吗?- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
-
6.mmap后关闭文件描述符,对mmap映射有没有影响? 映射区还存在,创建映射区的fd被关闭,没有任何影响。
int fd = open("XXX"); mmap(,,,,fd,0); close(fd);
-
7.对ptr越界操作会怎样? 越界操作操作的是非法的内存,产生段错误
信号量,条件变量,互斥量;
- 条件变量:一个线程等待某个条件为真,将自己挂起;另一个线程使得条件成立,并通知等待的线程继续,条件变量要和互斥锁一起使用。
- 信号量:最主要的是可以指明可用资源的数量。
条件变量和信号量的最大区别就是,条件变量可以一次唤醒所有的线程,信号量一次只能唤醒一个线程。
**条件变量唤醒丢失:**如果生产者线程进行notify的时候,消费者线程还没有处于wait状态,会出现唤醒丢失,由于错过了唤醒信号,消费者可能会一直等待。加个flag记录状态既可以解决,condition_variable的wait的参数中加个lambda表达式,判断flag状态。
进程打开文件的时候,会将文件读到内存,此时内容已经和磁盘上的文件独立了,并且操作系统会将文件的inode信息关联到文件描述符,也就增加他的引用计数,文件系统中是按照目录项和inode存储文件的,删除文件的操作只是删除了文件的目录项,并没有删除inode,只有当inode的引用计数为0的时候,才会删除inode,但是进程没有结束,inode就不会被删除,因此进程可以写已经删除的文件。
是一种结构体,存储文件的属性信息,权限、类型、大小等,大多数存在磁盘,常用的会缓存到内存中,采用引用计数来存储文件
存储文件名和inode的映射,通过目录项可以访问到inode,并且增加inode的引用计数
多个目录下对应同一个inode文件
引用另一个文件的路径
可以的,因为tcp连接是使用4元组来进行确定的,因此只要更改其中一个既可以保证唯一性,因此如果有多张网卡,那么多个进程是可以监听同一个端口的
还有就是多个进程可以监听同一个套接字,这里建立完套接字、监听后,直接fork复制进程就可以实现,nginx中间件就是采用这样的架构实现的reactor模式,不够这里可能会出现惊群问题,当然linux操作系统已经处理掉了这个问题,但是nginx是自己进行处理的,因为操作系统来处理不能做到负载均衡
由于tcp中的窗口大小只有两个字节,窗口大小对于现如今的带宽来讲已经严重不足了,因此tcp的窗口所表示的含义已经不在单纯表示窗口大小了,这里在tcp建立的时候会双方会协商出一个窗口扩大因子shift.cnt
,然后真正的窗口大小计算公式如下
$$
windows_size = tcp_windows_size * 2 ^{shift.cnt}
$$
cookie和session原理:
- 用户输入用户名和密码给服务端,服务端会创建一个Session和会话结束时间,然后把Session id通过键值对的方式加入到Cookie中,再把会话结束时间对应设置为这个Cookie的有效期,然后返回给客户端。
- 浏览器拿到Cookie后会进行保存。后续请求带上Cookie,失效后浏览器会自己删除Cookie,得重新输入用户名和密码。
cookie和session的问题:
- 如果有大量用户访问服务器的时候,服务器需要存储大量Session。
- 多台服务器的情况下,用户可能下次访问的是其他服务器,需要将Session ID分享给其他服务器。
JWT原理:
用户第一次登录以后在服务端生成一个JWT
服务端不保存JWT
,只保存JWT
签名的密文,然后将浏览器发送给浏览器
浏览器以Cookie
或Storage
的形式存储,请求把JWT
发送给服务器。
JWT组成:
JWT
由三部分组成header.payload.signature
,最后一个签名是服务端算出来的,是做验证的。
主机向本地域名服务器的查询一般是递归查询,本地域名服务器向根域名服务器的查询一般是迭代查询。
递归查询
DNS客户端向本地DNS服务器发送查询请求,然后等待结果。本地DNS服务器会⾃⾏查询下⼀级的服务器,并将最终结果返回给DNS客户端。
迭代查询
本地DNS服务器向上层服务器发起查询,得到DNS服务器的地址,然后再⾃⾏向服务器发起查询请求,以此类推,直到获取完整的解析结果为⽌。
递归查询适合普通⽤户和客户端,⽽迭代查询适⽤于DNS服务器之间的通信。
URI,是统一资源标识符,用来唯一的标识一个资源。 URL,是统一资源定位器,它是一种具体的URI,URL既可以用来标识一个资源,还指明了如何定位到这个资源。
4个方面,重传机制,滑动窗口,流量控制,拥塞控制。
重传机制:解决数据丢失问题,通过序列号和确认应答机制。
滑动窗口:在没有应答的情况下,发送方可以发送多少数据。
- 事务方面:
- MyISAM不支持事务处理,所以它不能保证数据的一致性和完整性,也不支持ACID特性(原子性、一致性、隔离性、持久性)。
- InnoDB支持事务处理,使用ACID特性来保证数据的完整性和一致性。支持复杂的业务操作,比如回滚事务,确保数据的一致性。
- 锁:
- MyISAM采用表级锁,并发访问效率较低,容易发生锁冲突。
- InnoDB支持行级锁定,只锁定需要修改的行,这种锁定方式显著提高了并发性能,允许多个事务同时访问不同的行,减少了锁冲突的可能性。
- 外键支持:
- MyISAM不支持外键约束,所以不能通过外键实现关联查询和级联删除。
- InnoDB支持外键约束,允许通过外键建立表与表之间的关联,实现更复杂的业务逻辑。
- 数据恢复:
- 由于InnoDB有redo log和undo log,在数据库崩溃等情况下,可以根据日志文件进行恢复,保证了数据的可恢复性。
- MyISAM则没有事务日志,如果在没有备份的情况下发生数据丢失,可能难以恢复。
- 缓存机制:
- MyISAM仅仅缓存索引,不会缓存实际数据信息,而InnoDB有自己的缓存(buffer pool),不仅缓存索引,还缓存表数据。
- 查询性能:
- 在查询性能方面,MyISAM通常优于InnoDB,因为MyISAM可以直接定位到数据所在的内存地址,而InnoDB在查询过程中需要维护数据缓存,且查询过程需要先定位到行所在的数据块,然后再从数据块中定位到要查找的行。
zab协议:https://www.cnblogs.com/crazymakercircle/p/14339702.html
可以将zookeeper
当成一个数据库,类似redis
。多个zookeeper
节点之间可以同步数据。
zookeeper
的配置文件:
admin.serverPort
给管理台的端口
clientPort
给客户端分配的端口
dataDir=/tmp/zookeeper/data1
为当前zookeeper
节点存放数据的目录,以及自己的id
,
最下面三行为当前zookeeper
集群中的三个节点。
zxid:
为zookeeper
节点间同步的日志id
sid:
为节点id
zookeeper
也有任期的概念。
zookeeper节点的状态:
looking
:竞选状态following
:随从状态,同步leader
状态,参与投票observing
:观察状态,同步leader
状态,不参与投票leading
:领导者状态
分布式节点一致性分为:
- 强一致性:始终保持一致。
- 最终一致性:会出现短暂不一致,但最终一致。
- 弱一致性:进群节点会进行同步,但不保证成功,可以一直读取到旧的数据。
**分布式节点中如何保证强一致性:**通过加锁实现,不论读写都加锁。但是有开销。
zookeeper是最终一致性,但是也尽量保证强一致性。
**zookeeper通过两阶段提交来保证一致性:**与raft基本是一样的,在zk2第5步(不要在意图中的4和5的顺序)的commit是个异步的提交,不会阻塞直接就执行本地提交了,而且 这个commit并不是直接发送网络包了,而是放到一个队列上面,等待线程去发送。
zookeeper
的选举分为初始化选举和崩溃选举。
**zookeeper
初始化选举机制:**首先每个节点都会在本地维护一个投票箱(投票箱内包含了自己和其他节点的投票信息,改投时需要更新投票信息,然后进行广播),在节点启动以后会先投票给自己(vote=[zxid, sid]),然后给其他节点发送自己的投票情况。当一个节点收到一张其他节点的投票情况后,比较日志新旧,其次比较节点id号(大的优先当选),如果发现其他节点的投票情况日志旧或者节点id号小直接抛弃。如果发现优于自己,更改自己的投票情况,并把更改后的自己的投票情况发送给集群中的其他节点。假设有5个节点,前两个节点启动时选出不leader
因为启动的节点个数都没到3个,然后节点3启动后,由于sid大且获得超过半数的投票,当选leader
,节点4和节点5启动的时候,由于存在leader
,所以启动时就是follower
。
zookeeper
崩溃选举机制:
leader
故障后,follower
会得不到心跳,进入竞选状态。- 各节点投票,先投自己(zxid,sid),然后广播
- 接收到其他节点的投票信息,对比zxid和sid,如果本节点竞选失败,更新自己的投票结果,重新广播,如果本节点大,则不进行处理。
- 统计本地的投票信息,超过半数的节点,切换为
leader
并进行广播。
只有一个zookeeper是选举不出来leader的,所以在客户端进行连接的时候会直接断开。
zookeeper
的领导选举与raft
差别较大。
通过两阶段提交,见上面4.
**当给zookeeper从节点发送写请求时:**会把写请求转发给leader
,然后由leader
统一处理写请求。
但是当集群中的节点变多时,对于读肯定会性能提升,但是对于写会下降,所以为了解决写性能下降的问题,集群中会有watcher
观察者节点。
**观察者节点:**不会参加领导者选举,也不会参加两阶段提交过程,只负责同步数据。在leader
发送commit
时直接将它发送给观察者节点即可。
为了形成多数派和少数派。
假如集群有5个节点,那么能容忍宕机2个节点。当集群中有6个节点时,也只能容忍宕机2个节点,因为宕机3个不能形成多数派。综上容错性一样的情况下,5个成本更少。
集群中出现两个leader
。
不会出现脑裂现象。想选出一个leader
需要集群中的过半节点的投票。如果出现网络分区,最多只有一个网络分区能包含过半的节点或者所有网络分区都没有达到过半的节点数,那么就不会出现脑裂现象。
zookeeper
的数据模型是个树形结构,树中的节点可以存放数据也可以拥有子节点。有一个固定的根节点(/),查询节点只能用绝对路径,不能用相对路径。
节点类型:
- 持久节点:当创建该节点的客户端与服务端的会话关闭也不会删除节点,只能显示调用
delete
删除。 - 临时节点:当创建该节点的客户端与服务端的会话因超时或发生异常关闭时会删除节点,也可以显示调用
delete
删除。 - 有序节点:不算单独种类的节点,在前两种基础上增加了有序性质,创建节点时节点名追加一个递增数字作为后缀。
节点内容是二进制数组(byte data[]),不能是其他类型,存储节点的数据,节点访问权限信息,子节点数据。临时节点不能有子节点。还包括czxid(创建节点的事务id),mzxid(最后一个更新的事务id),version(版本号),dataLength(数据内容长度),numChildren(子节点个数)。
根据三个参数大小对比结果,选择数据同步方式:
- peerLastZxid:从节点(follower或者observer)最后处理的zxid。
- minCommittedLog:leader服务器队列中committedlog中的最小zxid
- maxCommittedLog: Leader服务器队列中committedlog中最大的zxid
四种数据同步方式:
- DIFF:直接差异化同步,当peerLastZxid介于minCommittedLog和maxCommittedLog之间时
- TRUNC+DIFF:先回滚再差异化同步 当从节点包含了一条leader没有的日志时,回滚到minCommittedLog和maxCommittedLog之间,再做差异化同步。
- TRUNC:仅回滚操作, 当peerLastZxid大于maxCommittedLog时,从节点仅回滚
- SNAP:全量同步 peerLastZxid小于minCommittedLog时
watch机制:
客户端可以通过在znode上设置watch,实现监听znode的变化。当发生事件:1.父节点的创建,修改,删除 2. 子节点的创建,删除。当发生这两个事件时会通知了设置了watch的客户端,只通知发生了事件,不告知事件内容(减轻服务器带宽和压力)。zookeeper最原本的设计是触发一次就移除了,现在默认触发多次。
watch机制实现原理:
原理:
- 每个客户端都在锁节点下创建临时顺序节点
- 当前不是最小顺序号,对前一个节点进行监听
- 前一个节点释放锁之后,会通知下一个拥有临时节点的客户端,去拿到锁。
分布式锁基本要求:
- 让最小顺序号的应用获取到锁,从而满足分布式锁每次只能一个占用锁。锁的释放,即删除应用在zookeeper上注册的节点,因为每个节点只被自己注册拥有,所以只有自己才能删除,这样就满足只有占用者才可以解锁
- zookeeper的序号分配是原子的,分配后即不会再改变,让最小序号者获取锁,所以获取锁是原子的。
- 因为注册的是临时节点,在会话期间内有效,所以不会产生死锁
- zookeeper注册节点的性能能满足几千,而且支持集群,能够满足大部分情况下的性能