在循环引用的场景下探讨 C++ 中 shard_ptr 和 weak_ptr 原理
- + -引言
C++ 中经常需要 new 一个对象,开辟一个内存空间,返回一个指针来操作这个内存。使用完毕之后,需要通过 delete 来释放内存空间。如果内存没有释放,那这块内存将无法再利用,导致内存泄漏。为降低人为疏忽,C++ 11 的新特性中引入了三种智能指针,来自动化地管理内存资源:
+引言
C++ 中经常需要 new 一个对象,开辟一个内存空间,返回一个指针来操作这个内存。使用完毕之后,需要通过 delete 来释放内存空间。如果内存没有释放,那这块内存将无法再利用,导致内存泄漏。为降低人为疏忽,C++ 11 的新特性中引入了三种智能指针,来自动化地管理内存资源:
unique_ptr: 管理的资源唯一的属于一个对象,但是支持将资源移动给其他 unique_ptr 对象。当拥有所有权的 unique_ptr 对象析构时,资源即被释放。
shared_ptr: 管理的资源被多个对象共享,内部采用引用计数跟踪所有者的个数。当最后一个所有者被析构时,资源即被释放。
weak_ptr: 与 shared_ptr 配合使用,虽然能访问资源但却不享有资源的所有权,不影响资源的引用计数。有可能资源已被释放,但 weak_ptr 仍然存在。因此每次访问资源时都需要判断资源是否有效。
本文主要在循环引用的场景下探讨 shard_ptr 和 weak_ptr 原理。
-循环引用
shared_ptr 通过引用计数的方式管理内存,当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他的 shared_ptr 指向相同的对象,当引用计数为 0 时,内存将被自动释放。
+循环引用
shared_ptr 通过引用计数的方式管理内存,当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他的 shared_ptr 指向相同的对象,当引用计数为 0 时,内存将被自动释放。
1 | auto p = make_shared<int>(10); // 创建一个名为 p 的 shared_ptr,指向一个取值为 10 的 int 型对象,这个数值 10 的引用计数为 1,只有 p |
当对 shared_ptr 赋予新值,或被销毁时,引用计数会递减。
1 | auto r = make_shared<int>(20); // 创建一个名为 r 的 shared_ptr,指向一个取值为 20 的 int 型对象,这个数值 20 的引用计数为 1,只有 r |
shared_ptr 原理
shared_ptr 实际上是对裸指针进行了一层封装,成员变量除了裸指针外,还有一个引用计数,它记录裸指针被引用的次数(有多少个 shared_ptr 指向这同一个裸指针),当引用计数为 0 时,自动释放裸指针指向的资源。影响引用次数的场景包括:构造、赋值、析构。基于三个最简单的场景,实现一个 demo 版 shared_ptr 如下(实现既不严谨也不安全,仅用于阐述原理):
+shared_ptr 实际上是对裸指针进行了一层封装,成员变量除了裸指针外,还有一个引用计数,它记录裸指针被引用的次数(有多少个 shared_ptr 指向这同一个裸指针),当引用计数为 0 时,自动释放裸指针指向的资源。影响引用次数的场景包括:构造、赋值、析构。基于三个最简单的场景,实现一个 demo 版 shared_ptr 如下(实现既不严谨也不安全,仅用于阐述原理):
1 |
|
在循环引用示例中,用到了 make_shared 函数:
@@ -282,43 +281,42 @@1 | auto son = make_shared<Son>(); // 调用构造函数,son.counter=1 |
当 main 函数执行完时,执行析构函数,此时由于 son.counter=1,father.couter=1,不满足 if 条件,不会实行 delete 命令完成资源释放,导致内存泄漏。
-weak_ptr 原理
为解决循环引用的问题,仅使用 shared_ptr 是无法实现的。堡垒无法从内部攻破的时候,需要借助外力,于是有了 weak_ptr,字面意思是弱指针。为啥叫弱呢?shared_ptr A 被赋值给 shared_ptr B 时,A 的引用计数加 1;shared_ptr A 被赋值给 weak_ptr C 时,A 的引用计数不变。引用力度不够强,不足以改变引用计数,所以就弱了(个人理解,有误请指正)。
+weak_ptr 原理
为解决循环引用的问题,仅使用 shared_ptr 是无法实现的。堡垒无法从内部攻破的时候,需要借助外力,于是有了 weak_ptr,字面意思是弱指针。为啥叫弱呢?shared_ptr A 被赋值给 shared_ptr B 时,A 的引用计数加 1;shared_ptr A 被赋值给 weak_ptr C 时,A 的引用计数不变。引用力度不够强,不足以改变引用计数,所以就弱了(个人理解,有误请指正)。
weak_ptr 在使用时,是与 shared_ptr 绑定的。基于 SharedPtr 实现来实现 demo 版的 WeakPtr,并解决循环引用的问题,全部代码如下:
1 |
|
代码执行结果如下:
-WeakPtr
+::WeakPtr(T*) [with T = Father] WeakPtr
::WeakPtr(T*) [with T = Father] Son::Son()
-SharedPtr
+::SharedPtr(T*) [with T = Son] SharedPtr
::SharedPtr(T*) [with T = Son] Father::Father()
-SharedPtr
-::SharedPtr(T*) [with T = Son] SharedPtr
-::SharedPtr(T*) [with T = Father] WeakPtr
-& WeakPtr ::operator=(SharedPtr &) [with T = Father] SharedPtr
+& SharedPtr ::operator=(const SharedPtr &) [with T = Son] SharedPtr
+::SharedPtr(T*) [with T = Son] SharedPtr
+::SharedPtr(T*) [with T = Father] WeakPtr
+& WeakPtr ::operator=(SharedPtr &) [with T = Father] SharedPtr
& SharedPtr ::operator=(const SharedPtr &) [with T = Son] son: 2
father: 1
-SharedPtr
+::~SharedPtr() [with T = Father] SharedPtr
::~SharedPtr() [with T = Father] Father::~Father()
-SharedPtr
-::~SharedPtr() [with T = Son] SharedPtr
+::~SharedPtr() [with T = Son] SharedPtr
+::~SharedPtr() [with T = Son] SharedPtr
::~SharedPtr() [with T = Son] Son::~Son()
-WeakPtr
+::~WeakPtr() [with T = Father] WeakPtr
::~WeakPtr() [with T = Father]
可以看到 Son 对象和 Father 对象均被析构,内存泄漏的问题得到解决。析构过程解读如下:
--SharedPtr
+::~SharedPtr() [with T = Father] # 析构 father,由于 father.couter=1,减 1 后执行 delete father_ SharedPtr
::~SharedPtr() [with T = Father] # 析构 father,由于 father.couter=1,减 1 后执行 delete father_ Father::~Father() # 析构 father_,执行~Father(),进一步析构成员变量
-SharedPtr
-::~SharedPtr() [with T = Son] # 析构 SharedPtr ,此时 son.couter 减 1,son.counter=1 SharedPtr
+::~SharedPtr() [with T = Son] # 析构 son,由于 son.counter=1,减 1 后执行 delete son_ SharedPtr
+::~SharedPtr() [with T = Son] # 析构 SharedPtr ,此时 son.couter 减 1,son.counter=1 SharedPtr
::~SharedPtr() [with T = Son] # 析构 son,由于 son.counter=1,减 1 后执行 delete son_ Son::~Son() # 析构 son_,执行~Son(),进一步析构成员变量
-WeakPtr
+::~WeakPtr() [with T = Father] # 析构 WeakPtr WeakPtr
::~WeakPtr() [with T = Father] # 析构 WeakPtr
总结
-
+
- 尽量使用智能指针管理资源申请与释放,减少人为 new 和 delete 误操作和考虑不周的问题。
- 使用 make_shared 来创建 shared_ptr,如果先 new 一个对象,再用这个对象的裸指针构造一个 shared_ptr 指针,可能出现问题。shared_ptr 会自动释放资源,如果再手动 delete,释放两次那就挂了。
总结
- 浅析 C++ 的默认构造函数与拷贝构造函数
-
+
-定义
C++ 是一种面向对象编程语言,在设计时,将具有共同特征的对象抽象成类,在使用时,通过将类实例化得到不同的对象。创建一个实例对象的时候,就需要用到构造函数。
创建一个实例有两种方法:
(1) 通过初始化成员变量的来构造实例对象,这时候调用普通构造函数。
(2) 通过复制已有的实例化对象来构造新的实例对象,这时候调用拷贝构造函数。
+定义
C++ 是一种面向对象编程语言,在设计时,将具有共同特征的对象抽象成类,在使用时,通过将类实例化得到不同的对象。创建一个实例对象的时候,就需要用到构造函数。
创建一个实例有两种方法:
(1) 通过初始化成员变量的来构造实例对象,这时候调用普通构造函数。
(2) 通过复制已有的实例化对象来构造新的实例对象,这时候调用拷贝构造函数。
构造函数的函数名与类名相同,没有返回值,可以通过不同的参数值实现重载。定义 Object 类如下:
1
2
3
4
5
6
7
8
class Object
{
public:
int d;
Object() { std::cout << "default constructor" << std::endl; } // 默认构造函数,调用时可不提供参数的构造函数,即 Object obj
Object(int k) { d = k; std::cout << "defined constructor" << std::endl; } // 普通构造函数
Object(const Object& obj) { d = obj.d; std::cout << "copy constructor" << std::endl; } // 拷贝构造函数,定义时需要以当前类对象的 const 引用作为入参,即 const Object& obj
};
定义 Object 对象如下:
1
2
3
Object obj1 = Object(); // default constructor
Object obj2 = Object(3); // defined constructor
Object obj3 = obj1; // copy constructor,注意不是重载 = 的赋值函数
-合成
当程序中没有显示定义默认和拷贝构造函数时,逻辑上编译器会自己定义默认和拷贝构造函数。编译器自己定义的构造函数可以分为无用的 (trivial) 和有用的(non-trivial)。编译器通过实现优化,仅在有限的几种情况下,编译器才会将定义的有用的构造函数真正合成出来。
-默认构造函数
C++ 默认构造函数有两个重要作用:
+合成
当程序中没有显示定义默认和拷贝构造函数时,逻辑上编译器会自己定义默认和拷贝构造函数。编译器自己定义的构造函数可以分为无用的 (trivial) 和有用的(non-trivial)。编译器通过实现优化,仅在有限的几种情况下,编译器才会将定义的有用的构造函数真正合成出来。
+默认构造函数
C++ 默认构造函数有两个重要作用:
一是提供成员变量初始化的方法,默认构造函数并不会申请成员变量所需的内存空间,也不会主动给成员变量赋初值,而只是提供了给成员变量赋初值的一种途径。
二是在 virtual 场景下,完成虚函数表指针和虚基类指针的设置,使得在运行时可以通过指针或引用访问到正确的函数实现,呈现出多态的特性。
鉴于此,C++ 编译器会在以下场景合成 non-trivial 的默认构造函数:
1、virtual 场景,即 定义了虚函数或虚继承 的情形,此时需要合成默认构造函数,以正确设置虚函数表指针和虚基类指针。
2、组合或继承场景下,被组合的类或被继承的类中显式定义了默认构造函数,需要在合成的默认构造函数中递归调用被组合类或被继承类的构造函数。
注:
非 virtual 场景,C++ 构造函数类似于 Python 的__init__方法,区别在于 C++ 编译器会在构造函数中自动插入父类的构造函数,而 Python 需要通过 super 显式调用父类的构造函数。
-拷贝构造函数
C 语言中的拷贝是 bitwise 拷贝(内存上存的是啥就拷贝啥),C++ 如果用 bitwise 拷贝,可能会导致程序出问题,例如:
+拷贝构造函数
C 语言中的拷贝是 bitwise 拷贝(内存上存的是啥就拷贝啥),C++ 如果用 bitwise 拷贝,可能会导致程序出问题,例如:
1、在 virtual 场景下,如果用派生类对象初始化父类对象(Base base_obj = derived_obj 对象模型会发生切割),bitwise 拷贝使得 base_obj 的 vptr 指向 Derived 的虚函数表,导致程序异常。
2、在非 virtual 场景下,如果类中有指针类型的成员对象,bitwise 拷贝使得两个对象的成员变量指向同一处内存(浅拷贝),析构时由于内存被重复释放导致程序崩溃,此时需要定义拷贝构造函数使得拷贝结果位于一处新内存上(深拷贝)。
因此,C++ 编译器会在以下场景合成 non-trivial 的拷贝构造函数:
1、virtual 场景,即 定义了虚函数或虚继承 的情形,此时需要合成拷贝构造函数,保证在复制过程(特别是用派生类对象初始化基类对象)中正确设置虚函数表指针和虚基类指针。
2、组合或继承场景下,被组合的类或被继承的类中显式定义了拷贝构造函数,需要在合成的默认构造函数中递归调用被组合类或被继承类的拷贝构造函数。
-应用
设计模式中有一种单例模式,意思就是在程序中有且仅有唯一的实例。要实现单例模式,需要解决两个问题:
+应用
设计模式中有一种单例模式,意思就是在程序中有且仅有唯一的实例。要实现单例模式,需要解决两个问题:
(1) 创建一个实例:解决存在性问题
(2) 禁止多个实例:解决唯一性问题
定义 SingleInstance 类如下:
@@ -293,7 +292,6 @@ 应用
default constructor
-
diff --git "a/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html" "b/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html"
index 26d4280..27abeb3 100644
--- "a/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html"
+++ "b/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html"
@@ -5,21 +5,21 @@
-
- C++ 对象模型:对象内存布局 - 千里之行始于足下
+
+ C++ 对象模型:对象内存布局 - 千里之行,始于足下
-
-
+
+
-
-
+
+
@@ -33,7 +33,7 @@
-
+
@@ -86,7 +86,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -257,25 +257,24 @@
- 介绍 C++ 对象在内存中的存储模型
-
+
-对象模型基础
C++ 在 C 的基础上引入了面向对象的程序设计思想,类的封装、继承和多态使得 C++ 功能更加强大。C++ 的类包含数据成员和成员函数:
+对象模型基础
C++ 在 C 的基础上引入了面向对象的程序设计思想,类的封装、继承和多态使得 C++ 功能更加强大。C++ 的类包含数据成员和成员函数:
数据成员:静态数据成员,非静态数据成员
成员函数:非静态成员函数、静态函数、虚函数
C++ 对象模型研究 C++ 类的数据成员和成员函数在内存中如何布局的问题。假设有个 C++ 类定义如下:
1
2
3
4
5
6
7
8
9
10
class Base
{
public:
int member; // 非静态数据成员
static int smember; // 静态数据成员
void func(); // 非静态成员函数
static void sfunc(); // 静态成员函数
virtual void vfunc(); // 虚函数
};
-简单对象模型
在简单对象模型中,类的每个实例化对象在内存中都会申请一块连续内存,在这块内存上保存着指向成员变量和函数的指针。这样做的好处是,每个对象的内存大小是固定的,不管是数据成员还是成员函数,都只对应一个指针,在访问时可以很快计算出指针偏移量。但这样做的弊端也很明显,当访问对象的数据成员时,需要先找到数据成员的指针,然后通过这个指针获取到真实的数据,多了一次寻址,效率较低。
-
+简单对象模型
在简单对象模型中,类的每个实例化对象在内存中都会申请一块连续内存,在这块内存上保存着指向成员变量和函数的指针。这样做的好处是,每个对象的内存大小是固定的,不管是数据成员还是成员函数,都只对应一个指针,在访问时可以很快计算出指针偏移量。但这样做的弊端也很明显,当访问对象的数据成员时,需要先找到数据成员的指针,然后通过这个指针获取到真实的数据,多了一次寻址,效率较低。
+
-表驱动对象模型
表格驱动对象模型对类中的数据成员和成员函数进行了区分,每个对象保留两个指针,一个指向数据成员表,其中存放数据成员的取值,一个指向成员函数表,其中存放指向成员函数的指针。这种做法的好处是对数据和函数进行分开管理,不好的地方在于访问成员函数时,需要先找到成员函数表,效率更低。
-
+表驱动对象模型
表格驱动对象模型对类中的数据成员和成员函数进行了区分,每个对象保留两个指针,一个指向数据成员表,其中存放数据成员的取值,一个指向成员函数表,其中存放指向成员函数的指针。这种做法的好处是对数据和函数进行分开管理,不好的地方在于访问成员函数时,需要先找到成员函数表,效率更低。
+
-C++ 对象模型
介绍 C++ 对象模型之前,先理解 C++ 中虚函数的概念。C++ 的多态是通过虚函数实现的。在基类中用 virtual 关键字定义虚函数,在派生类中重写虚函数,那么在运行时,传入基类指针时,会根据对象的实际类型来调用正确的函数,实现多态。
+C++ 对象模型
介绍 C++ 对象模型之前,先理解 C++ 中虚函数的概念。C++ 的多态是通过虚函数实现的。在基类中用 virtual 关键字定义虚函数,在派生类中重写虚函数,那么在运行时,传入基类指针时,会根据对象的实际类型来调用正确的函数,实现多态。
不妨试想一下,如果要自己设计 C++ 对象模型,应该怎么做?我是这样想的:C++ 的类实例化为不同的对象,这些对象的静态数据成员是共享的,非静态数据成员是互不影响的,成员函数对各个对象来说是相同的。那么非静态数据成员应该放在每个对象的内存空间中,以保证不同对象相互独立;静态数据成员和成员函数应该放在这个类的公共空间,不用每个对象都存一份,减少存储空间;而虚函数在继承中发挥独特作用,应该与其他成员函数分开。
揭开谜底,以 Base 类为例,C++ 对象模型是长这样的:
-
+
可以看出:
@@ -283,10 +282,10 @@ 普通继承场景下的对象模型 单一继承场景
以 Base 为基类,定义派生类 Derived 如下:
+普通继承场景下的对象模型
单一继承场景
以 Base 为基类,定义派生类 Derived 如下:
1
2
3
4
5
6
7
8
9
10
11
class Derived : public Base
{
public:
int member; // 非静态数据成员
static int smember_d; // 静态数据成员
void func(); // 非静态成员函数
static void sfunc_d(); // 静态成员函数
virtual void vfunc(); // 虚函数
virtual void vfunc_d(); // 虚函数
}
这种场景下,Derived 的对象模型如下:
-
+
可以看出:
@@ -294,28 +293,28 @@ 多重继承场景
单一继承场景下,派生类在基类的虚函数表上进行修改。多重继承场景,有多个基类的虚函数表。定义基类 Base1 和派生类 Derived 如下:
+多重继承场景
单一继承场景下,派生类在基类的虚函数表上进行修改。多重继承场景,有多个基类的虚函数表。定义基类 Base1 和派生类 Derived 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base1
{
public:
int member1; // 非静态数据成员
virtual void vfunc(); // 虚函数
virtual void vfunc1(); // 虚函数
virtual void vfunc2(); // 虚函数
};
class Derived : public Base, public Base1
{
public:
int member_d; // 非静态数据成员
static int smember_d; // 静态数据成员
void func(); // 非静态成员函数
static void sfunc_d(); // 静态成员函数
virtual void vfunc(); // 虚函数
virtual void vfunc1(); // 虚函数
virtual void vfunc_d(); // 虚函数
};
此时 Derived 的对象模型为:
-
+
可以看出:
- 继承多个父类时,派生类的对象模型中分别存储一个虚函数表的指针。如果派生类重写的基类同名虚函数,那就在虚函数表中进行替换;如果派生类新增了虚函数,那么在第一个父类的虚函数表中进行新增扩展。
-虚继承场景下的对象模型
使用普通继承的菱形继承
多重继承场景中,有一种特殊的情况叫菱形继承,它是这样操作的:B1 继承 B,B2 继承 B,D 继承 B1 和 B2,祖父类 B 被子类 D 重复继承,图示如下:
-
+虚继承场景下的对象模型
使用普通继承的菱形继承
多重继承场景中,有一种特殊的情况叫菱形继承,它是这样操作的:B1 继承 B,B2 继承 B,D 继承 B1 和 B2,祖父类 B 被子类 D 重复继承,图示如下:
+
按多重继承的方式推导 D 的对象模型是这样的:
-
+
可以看到,在菱形继承场景中,访问祖父类成员 B::b 时,出现了二义性。
为解决菱形继承中的问题,C++ 发明了虚继承。虚继承的对象模型与普通继承不同:普通继承是子类拷贝并修改父类的虚函数表;虚继承是子类和父类的虚函数表分开保存,将指向父类的虚函数表的指针也加入到子类的对象模型中。
-使用虚继承的单一继承
假定单一虚继承场景如下:
-
+使用虚继承的单一继承
假定单一虚继承场景如下:
+
此时派生类 Derived 的对象模型为:
-
+
可以看到:
@@ -323,18 +322,17 @@ 使用虚继承的菱形继承
假定使用虚继承的菱形继承场景如下:
-
+使用虚继承的菱形继承
假定使用虚继承的菱形继承场景如下:
+
结合多重继承与单一虚继承,推导派生类 Derived 的对象模型为:
-
+
可以看到:
- 多重继承的布局基本不变,虚基类的信息被追加到内存布局最后,并用 0x00000000 隔开。此时再访问 B::b 不会出现二义性。
附注:
推导时,从子类往祖父类逐步推进,子类与父类适用多重继承,父类与祖父类适用单一虚继承,每一步只决定派生类的数据成员的位置,例如 B::b 的布局应该由单一虚继承决定,如果在多重继承时决定,那推出来也是二义的。
-总结
对象模型揭示了 C++ 对象在内存中的存储布局,结合面向对象的三大特性(继承、封装和多态)来理解对象模型,印象会更深一点。
-
+总结
对象模型揭示了 C++ 对象在内存中的存储布局,结合面向对象的三大特性(继承、封装和多态)来理解对象模型,印象会更深一点。
diff --git "a/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" "b/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html"
index 7733d9b..820d8fb 100644
--- "a/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html"
+++ "b/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html"
@@ -5,25 +5,25 @@
-
- C++ 与 Python 的 OOP 比较 - 千里之行始于足下
+
+ C++ 与 Python 的 OOP 比较 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
@@ -76,7 +76,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -247,12 +247,11 @@
- C++ 和 Python 都是从 C 语言演变出来的面向对象(OOP)的编程语言,本文基于 OOP 三特性,比较 C++ 和 Python 的异同点
-
+
-1. OOP 三特性
封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。
-2. 封装
类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。
-数据
于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。
+1. OOP 三特性
封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。
+2. 封装
类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。
+数据
于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。
@@ -272,7 +271,7 @@ 数据
实例变量
-方法
既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。
+方法
既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。
@@ -293,29 +292,27 @@ 方法
C++ 中还有一种重要方法是 虚函数,使用虚函数可以实现 C++ 中的多态。
Python 中还有一种方法是静态方法。在 Python 中可以认为,实例方法传入了实例对象的指针,类方法传入了类的指针,而静态方法既不需要传入实例,也不需要传入类。
-3. 继承
子类继承父类,使子类拥有父类的数据和方法。
-单一继承
这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。
-多重继承
没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO。
-4. 多态
多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。
-C++ 多态
前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:
-
+3. 继承
子类继承父类,使子类拥有父类的数据和方法。
+单一继承
这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。
+多重继承
没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO。
+4. 多态
多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。
+C++ 多态
前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:
+
执行如下代码:
1
2
Base *ptr = new Derived();
ptr->method();
编译器看到 ptr 是 Base 类型,如果 method 不是虚函数,那么执行的应该是 Base::method。现在 method 是虚函数,执行期调用 Base::method 还是 Derived::method,要看赋给 ptr 的是 Base 对象地址还是 Derived 对象地址。上面的代码是创建了一个 Derived 对象,并把地址传给 Base 类指针,但是内存模型中的 vptr 指向的仍然是 Derived 类实现的虚函数,所以最后调用的是 Derived::method。
-Python 多态
相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。
+Python 多态
相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。
1
2
3
4
5
6
7
8
9
10
11
12
class Cat:
def say(cls):
print("miao miao")
class Dog:
def say(cls):
print("wang wang")
def func(kls):
kls.say()
这里 Cat 和 Dog 没有继承关系,say 也不是虚函数,调用 func(Cat)和 func(Dog)都能正确执行。要是再定义个 Person 类,只要定义了方法 say,就可以把 Person 传给 func 完成调用。
-5. 写在最后
虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:
+5. 写在最后
虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:
C++ 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
public:
int x = 1;
virtual void print() { std::cout << x << std::endl; }
};
class Derived : public Base
{
public:
int x = 2;
};
int main() {
Base *ptr = nullptr;
ptr = new Base();
ptr->print(); // 打印 1
ptr = new Derived();
ptr->print(); // 打印 1
}
Python 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base:
def __init__(self):
self.x = 1
def print(self):
print(self.x)
class Derived(Base):
def __init__(self):
self.x = 2
def func(Kls):
obj = Kls()
obj.print()
func(Base) // 打印1
func(Derived) // 打印2
-
-
diff --git "a/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html" "b/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
index 8146de8..c55e117 100644
--- "a/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
+++ "b/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
@@ -5,29 +5,29 @@
-
- Linux 从入门到熟练 - 千里之行始于足下
+
+ Linux 从入门到熟练 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
-
+
@@ -76,7 +76,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -247,16 +247,15 @@
- 常用的各类 Linux 操作命令
-
+
-一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
+一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
CLI 是用户与内核打交道的工具。用户日常使用操作系统,最频繁的场景就是文件操作和网络访问。故而基础篇笔记主要记录 CLI 基本操作、文件系统、网络配置、权限管理等相关知识点。
-二、CLI 基本操作
(一)查看目录
+二、CLI 基本操作
(一)查看目录
- ls:显示指定目录下的文件及属性信息
- pwd:显示当前工作目录的路径
-(二)查看内容
+(二)查看内容
- echo:输出字符串或变量值(变量前加 $ 符号)
- cat:输出文件内容
- head:查看文件头部内容
@@ -264,17 +263,17 @@ (三)开关机
+(三)开关机
- shutdown:按指定方式关闭系统
- poweroff:关机
- reboot:重启
-(四)归档压缩
+(四)归档压缩
- zip:打包成 zip 文件
- unzip:zip 文件解包
- tar:压缩和解压缩文件
-(五)文件和目录操作
+(五)文件和目录操作
- locate:基于数据库快速查找文件和目录,手动更新数据库使用 updatedb 命令
- find:根据条件在指定目录下查找文件和目录
- cp:复制文件或目录
@@ -285,7 +284,7 @@ (六)进程操作
+(六)进程操作
- &:命令 + 空格 +&,会将任务置于后台执行
- jobs:查看后台任务
- fg:将后台任务放到前台执行
@@ -293,11 +292,11 @@ (七)命令帮助
+(七)命令帮助
- history:查看历史命令记录
- man:查看命令的帮助手册,一般情况下命令加 -h 或–help 够用了
-(八)文本编辑
+(八)文本编辑
- grep:搜索文本,可使用关键字或正则表达式
- sed:编辑文本,可使用正则表达式或脚本
- cut:基于列处理文件内容
@@ -332,7 +331,7 @@ (九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
+(九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
通过管道和重定向操作符,可以控制标准数据流的来源和去向:
@@ -380,7 +379,7 @@ echo ‘hello world’ | grep ‘hello’
-三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
+三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
graph TB
A["/"] --> B[bin] & C[boot] & D[dev] & E[etc] & F[home] & G[lib] & H[mnt] & I[opt] & J[proc] & K[root] & L[sbin] & M[tmp] & N[usr] & O[var]
N[usr] --> N1[bin] & N2[lib] & N3[local]
@@ -390,7 +389,7 @@
/: 根目录
/bin: 存放二进制可执行文件,root 用户和普通用户都能使用
/boot: 存放 linux 启动时的引导程序
/dev: linux 的设备也是以文件形式组织,存放在此目录下
/etc: 存放 linux 的系统管理和配置文件
/home: 存放普通用户的家目录,类似于 windows 下 C 盘 Users 用户文件夹
/lib: 存放程序运行依赖的库文件和内核驱动模块
/mnt: 设备挂载目录
/opt: 三方软件或大型软件的安装目录
/proc: 虚拟文件系统,记录系统实时信息,只存在内存中,不占用磁盘空间
/root: root 用户的家目录
/sbin: 存放二进制文件,仅 root 用户有权限使用
/tmp: 临时目录,重启系统会自动删除
/usr: 不是 user 缩写,是 unix software resource 缩写,存放系统上安装的软件资源
/var: 存放系统中经常变动的文件,如缓存 cache、日志 log、邮件 mail 等
-(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
+(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
+四、权限管理
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
linux 的用户分为 root 用户、系统用户和普通用户,每个用户用唯一的 uid 标识,root 用户的 uid 为 0,系统用户的 uid 通常在 1~999,普通用户的 uid 通常 >=1000。系统用户主要用于执行系统程序,无法使用 shell 登录。
为了便于对用户进行管理,每个用户还有对应的用户组,包括主组和附加组,主组只能有一个,附加组可以有多个。默认情况下,创建用户时,会将用户归属于同名的主组。
linux 上用户和用户组的信息都保存在 /etc 目录下,相关命令和文件说明如下:
@@ -416,9 +415,9 @@ /etc/shadow:用户密码信息(已加密)
/etc/group:用户组信息
-(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
+(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
linux 通过 UGO 模型对文件进行权限控制,其中 U(user)表示文件所属用户,G(group)表示文件所属用户组,O(other)表示既不是所属用户也不是所属用户组的其他群体。
-
+
通过 ls -l 查看文件权限如上图所示,第一列权限信息包含 10 位字符,第三列表示文件所属用户(属主),第四列表示文件所属用户组(属组)。权限信息中,第 1 个字符表示文件类型(- 表示文件,d 表示文件夹,l 表示符号链接),后面 9 个字符每 3 个一组,分别描述属主(user)权限、属组(group)权限、其他(other)权限。权限位是 rwx 表示具有相应权限,权限位是 - 表示没有相应权限。除了以字符的方式,每组权限还可以用 3 个 bit 位来表示,r 在高位,w 在中位,x 在低位,于是 rwx=b111=4+2+1=7。在修改权限时,可以使用任一种表达方式。
在创建文件或文件夹时,系统会设定默认的权限,通过原始权限减去权限掩码的方式,可以得到默认权限。文件夹的原始权限是 777(rwxrwxrwx),文件的原始权限是 666(rw-rw-rw-)。普通用户的默认掩码是 002,root 用户的默认掩码是 022。因此,对普通用户来说,新建文件夹的权限是 775(rwxrwxr-x),新建文件的权限是 664(rw-rw-r–)。
@@ -429,7 +428,7 @@ (三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
+(三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
@@ -461,11 +460,11 @@ 五、网络配置 (一)IP- 子网掩码 - 网关 -DNS
IP(v4) 编码用来标识互联网中的机器地址,它包含 32 个 bit 位,由网络地址 + 主机地址两部分组成。
+五、网络配置
(一)IP- 子网掩码 - 网关 -DNS
IP(v4) 编码用来标识互联网中的机器地址,它包含 32 个 bit 位,由网络地址 + 主机地址两部分组成。
子网掩码用来将 IP 地址切分为网络地址和主机地址,它也由 32 个 bit 位构成,且高位为连续的 1。子网掩码与 IP 地址成对出现,子网掩码与 IP 地址进行按位与运算可得网络地址。例如 IP 地址 192.168.1.1,子网掩码 255.255.255.0,可以算出网络地址为 192.168.1.0,可分配的主机地址包括 1~254(255 转换成 bit 位全为 1,全 1 的主机地址保留为广播地址),这一网段可以记为 192.168.1.0/24。同一网段的机器通信使用 ARP 协议,将 IP 地址解析为 MAC 地址,再基于 MAC 通信。
不同网络的主机之间进行通信需要使用网关,比如常见的路由器。网络 A 中的主机 A1 要跟网络 B 中的主机 B1 通信,主机 A1 需要先将数据包发送到 A 的网关,再由 A 的网关转发到 B 的网关,然后 B 的网关把数据包发送到主机 B1。现实中,跨域网络通信可能经过很多次路由器转发。
在互联网冲浪的时候,敲在地址栏里的是一串字符组成的域名。访问一个域名,实际上访问的也是互联网的一台主机。域名和 IP 的映射关系,通过 DNS 服务器进行解析,DNS 协议可以将域名解析为 IP 地址。
-(二)相关命令和配置文件
+(二)相关命令和配置文件
- ifconfig:查看和设置网络设备,重启后会失效,永久配置需要修改配置文件
- hostname:查看和设置系统的主机名
- ping:测试本机与目标主机之间的网络连通性
@@ -478,7 +477,6 @@
-
- Docker 必知必会 - 千里之行始于足下
+
+ Docker 必知必会 - 千里之行,始于足下
-
-
+
+
-
-
+
+
@@ -36,11 +36,11 @@
-
+
-
+
@@ -89,7 +89,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -260,12 +260,11 @@
- Docker 基本原理以及常用命令
-
+
-一、基本概念
Docker 是一种轻量级的虚拟化技术,可以让开发者非常便捷地实现应用程序的打包、移植、启动等操作,在软件开发、交付和部署中,有非常广泛的应用。
+一、基本概念
Docker 是一种轻量级的虚拟化技术,可以让开发者非常便捷地实现应用程序的打包、移植、启动等操作,在软件开发、交付和部署中,有非常广泛的应用。
Docker 容器与传统虚拟机的架构对比如下:
-
+
传统 VM 使用 Hypervisor 通过对物理主机的硬件资源进行协调和管理,为每个 GuestOS 分配独立的资源,让每个 GuestOS 成为一个虚拟主机,不同的 GuestOS 中的应用程序互不影响。Docker 容器直接运行在物理主机的 HostOS 上,共用物理主机的硬件资源,Container Engine 负责实现容器之间的资源隔离,让每个容器的应用独立地运行。
可以看出,容器比虚拟机少了一层 GuestOS,容器占用资源更少,启动更快,但隔离程度不如虚拟机。容器和虚拟机简要对比如下:
@@ -314,16 +313,16 @@
Docker 中有三个常见的名词:镜像、容器和仓库。这里先简单介绍下概念,知道是什么就行,后面再详细说明。
-(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
-(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
+(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
+(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
容器实质上是运行在宿主机上的进程,Docker 是用特殊的技术将容器与宿主机上的其他进程隔离开来,使得容器内的应用看起来是运行在一个独立的环境中。
-(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
-二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
+(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
+二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
- 容器内屏蔽容器外的情况,使用 Linux 的 Namespace 机制
- 容器拥有独立的资源,使用 Linux 的 Cgroups 机制
-(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
+(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
@@ -368,30 +367,30 @@ 隔离系统时间
-(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
+(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
查看 cgroup 挂载的目录,可以看到 cgroup 挂在 sys/fs/cgroup 节点,该路径下还有很多子目录(又称子系统),如 cpu、memory 等,每个子系统对应一种可以被限制的资源类型。
-
+
以 cpu 为例,查看 cpu 子系统。其中有两个参数 cfs_period_us 和 cfs_quota_us 通常组合使用,用于限制进程在长度为 cfs_period_us 的时间内,只能被分配到总量为 cfs_quota_us 的 cpu 时间。还有一个 tasks 文件,其中存放的是受限制的进程编号。
-
-
+
+
cpu 子系统中有个 docker 子目录,docker 目录中的文件与 cpu 目录中的文件一样。当我们拉起一个容器,比如运行 redis 镜像,可以看到 docker 目录中又多了一层以容器 id 为名称的子目录。
-
-
+
+
综上两点,容器其实是一个启用了多种 Namespace 的进程,它能够使用的资源量收到 Cgroups 的限制。截至目前,我们使用 Namespace 和 Cgroups 为容器开辟了一个独立的运行环境,接下来我们再剖析一下容器里运行的镜像。
前面提到,容器镜像是一个文件系统,我们运行 ubuntu 的镜像,可以通过命令行查看根目录内容,也可以创建并执行脚本文件:
-
+
我们还可以把改动之后的文件系统打包成我们自己的镜像,下次直接执行:
-
+
可以看到,我们以 ubuntu 文件系统为基础,加了一层我们自己的修改,打包了一个新的镜像。容器镜像实际上就是以 Linux 的联合文件系统机制(UnionFS)实现分层文件系统。
-(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
+(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
Docker 分层镜像的示例图如下,除了最顶层(称为容器层)是可写的,其他层(称为镜像层)都是只读的。UnoinFS 将所有层都叠加起来,形成最后我们在容器中所看到的文件系统。
-
+
由于镜像层都是只读的,所有对文件的修改操作都发生在容器层中,Docker 采用了 Copy-On-Write(写时复制) 的机制,具体来说:
@@ -401,15 +400,15 @@ (四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
-三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
+(四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
+三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
1
docker command --help
Docker 命令主要涉及到镜像、容器、仓库的操作和使用,下图可以概览全貌:
-
+
以下列举一些常用的 docker 命令,各命令详细用法可以通过 help 查看:
-1. 镜像相关
+1. 镜像相关
-2. 容器相关
+2. 容器相关
-(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
+(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
Dockerfile 是由一行行指令组成的脚本文件,每一行命令都会生成一个镜像层,多行命令生成多个镜像层,叠加生成最终的镜像文件。Dockerfile 的常见指令及含义如图所示:
-
+
最后看个 Dockerfile 的小例子:
编写 Dockerfile 文件
-
+
生成镜像文件
-
+
查看并运行镜像文件
-
+
-四、参考资料
+四、参考资料
-
diff --git "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html" "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
index 3df135e..40a3d52 100644
--- "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
+++ "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
@@ -5,28 +5,28 @@
-
- LibFuzzer 使用说明 - 千里之行始于足下
+
+ LibFuzzer 使用说明 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
-
+
@@ -74,7 +74,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -245,8 +245,7 @@
- LibFuzzer 从 0 到 1,原理 + 安装 + 使用 + 优化,一篇讲完
-
+
按照 官方定义,LibFuzzer 是一个in-process(进程内的),coverage-guided(以覆盖率为引导的),evolutionary(进化的) 的 fuzz 引擎,是 LLVM 项目的一部分,主要用于对 C/C++ 程序进行 Fuzz 测试。LibFuzzer 三个特性的具体含义为:
@@ -264,13 +263,13 @@
LibFuzzer 与待测的 library 进行链接,通过向指定的 fuzzing 入口(即target 函数)发送测试数据,并跟踪被触达的代码区域,然后对输入的数据进行变异,以达到代码覆盖率最大的目的,其中代码覆盖率的信息由 LLVM 的 SanitizerCoverage 工具提供。
-一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
+一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
1
apt install clang
安装完毕之后,可以通过如下命令查看 clang 版本:
1
clang --version
-2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
+2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Values other than 0 and -1 are reserved for future use.
}
target 函数的名称、参数类型、返回值类型都不能改变。此外,官方文档中还有如下说明:
@@ -336,11 +335,11 @@ libFuzzer 编译链接。
关于 LibFuzzer 通过插桩统计代码覆盖率的具体实现,有兴趣可以参考Coverage Control in libFuzzer。
-3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
+3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
1
./fuzz-target -flag1=val1 -flag2=val2 ... path1 path2 ...
在不限制运行时长的情况下,Fuzz 将会一直执行下去,直到出现 crash(通常是因为触发 sanitizer 异常)才会终止。导致 crash 的输入将会作为能触发 bug 的 testcase 保存到磁盘上,并根据 crash 的类型,用不同的文件名前缀进行区分,比如 crash-XXX,leak-XXX,timeout-XXX 等。
-(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
+(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
浅析 C++ 的默认构造函数与拷贝构造函数
- + -定义
C++ 是一种面向对象编程语言,在设计时,将具有共同特征的对象抽象成类,在使用时,通过将类实例化得到不同的对象。创建一个实例对象的时候,就需要用到构造函数。
创建一个实例有两种方法:
(1) 通过初始化成员变量的来构造实例对象,这时候调用普通构造函数。
(2) 通过复制已有的实例化对象来构造新的实例对象,这时候调用拷贝构造函数。
定义
C++ 是一种面向对象编程语言,在设计时,将具有共同特征的对象抽象成类,在使用时,通过将类实例化得到不同的对象。创建一个实例对象的时候,就需要用到构造函数。
创建一个实例有两种方法:
(1) 通过初始化成员变量的来构造实例对象,这时候调用普通构造函数。
(2) 通过复制已有的实例化对象来构造新的实例对象,这时候调用拷贝构造函数。
构造函数的函数名与类名相同,没有返回值,可以通过不同的参数值实现重载。定义 Object 类如下:
1 | class Object |
定义 Object 对象如下:
1 | Object obj1 = Object(); // default constructor |
合成
当程序中没有显示定义默认和拷贝构造函数时,逻辑上编译器会自己定义默认和拷贝构造函数。编译器自己定义的构造函数可以分为无用的 (trivial) 和有用的(non-trivial)。编译器通过实现优化,仅在有限的几种情况下,编译器才会将定义的有用的构造函数真正合成出来。
-默认构造函数
C++ 默认构造函数有两个重要作用:
+合成
当程序中没有显示定义默认和拷贝构造函数时,逻辑上编译器会自己定义默认和拷贝构造函数。编译器自己定义的构造函数可以分为无用的 (trivial) 和有用的(non-trivial)。编译器通过实现优化,仅在有限的几种情况下,编译器才会将定义的有用的构造函数真正合成出来。
+默认构造函数
C++ 默认构造函数有两个重要作用:
一是提供成员变量初始化的方法,默认构造函数并不会申请成员变量所需的内存空间,也不会主动给成员变量赋初值,而只是提供了给成员变量赋初值的一种途径。
二是在 virtual 场景下,完成虚函数表指针和虚基类指针的设置,使得在运行时可以通过指针或引用访问到正确的函数实现,呈现出多态的特性。
鉴于此,C++ 编译器会在以下场景合成 non-trivial 的默认构造函数:
1、virtual 场景,即 定义了虚函数或虚继承 的情形,此时需要合成默认构造函数,以正确设置虚函数表指针和虚基类指针。
2、组合或继承场景下,被组合的类或被继承的类中显式定义了默认构造函数,需要在合成的默认构造函数中递归调用被组合类或被继承类的构造函数。
注:
非 virtual 场景,C++ 构造函数类似于 Python 的__init__方法,区别在于 C++ 编译器会在构造函数中自动插入父类的构造函数,而 Python 需要通过 super 显式调用父类的构造函数。
拷贝构造函数
C 语言中的拷贝是 bitwise 拷贝(内存上存的是啥就拷贝啥),C++ 如果用 bitwise 拷贝,可能会导致程序出问题,例如:
+拷贝构造函数
C 语言中的拷贝是 bitwise 拷贝(内存上存的是啥就拷贝啥),C++ 如果用 bitwise 拷贝,可能会导致程序出问题,例如:
1、在 virtual 场景下,如果用派生类对象初始化父类对象(Base base_obj = derived_obj 对象模型会发生切割),bitwise 拷贝使得 base_obj 的 vptr 指向 Derived 的虚函数表,导致程序异常。
2、在非 virtual 场景下,如果类中有指针类型的成员对象,bitwise 拷贝使得两个对象的成员变量指向同一处内存(浅拷贝),析构时由于内存被重复释放导致程序崩溃,此时需要定义拷贝构造函数使得拷贝结果位于一处新内存上(深拷贝)。
因此,C++ 编译器会在以下场景合成 non-trivial 的拷贝构造函数:
1、virtual 场景,即 定义了虚函数或虚继承 的情形,此时需要合成拷贝构造函数,保证在复制过程(特别是用派生类对象初始化基类对象)中正确设置虚函数表指针和虚基类指针。
2、组合或继承场景下,被组合的类或被继承的类中显式定义了拷贝构造函数,需要在合成的默认构造函数中递归调用被组合类或被继承类的拷贝构造函数。
-应用
设计模式中有一种单例模式,意思就是在程序中有且仅有唯一的实例。要实现单例模式,需要解决两个问题:
+应用
设计模式中有一种单例模式,意思就是在程序中有且仅有唯一的实例。要实现单例模式,需要解决两个问题:
(1) 创建一个实例:解决存在性问题
(2) 禁止多个实例:解决唯一性问题
定义 SingleInstance 类如下:
@@ -293,7 +292,6 @@应用
default constructor
-
diff --git "a/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html" "b/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html"
index 26d4280..27abeb3 100644
--- "a/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html"
+++ "b/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html"
@@ -5,21 +5,21 @@
-
- C++ 对象模型:对象内存布局 - 千里之行始于足下
+
+ C++ 对象模型:对象内存布局 - 千里之行,始于足下
-
-
+
+
-
-
+
+
@@ -33,7 +33,7 @@
-
+
@@ -86,7 +86,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
default constructor
- 介绍 C++ 对象在内存中的存储模型
-
+
-对象模型基础
C++ 在 C 的基础上引入了面向对象的程序设计思想,类的封装、继承和多态使得 C++ 功能更加强大。C++ 的类包含数据成员和成员函数:
+对象模型基础
C++ 在 C 的基础上引入了面向对象的程序设计思想,类的封装、继承和多态使得 C++ 功能更加强大。C++ 的类包含数据成员和成员函数:
数据成员:静态数据成员,非静态数据成员
成员函数:非静态成员函数、静态函数、虚函数
C++ 对象模型研究 C++ 类的数据成员和成员函数在内存中如何布局的问题。假设有个 C++ 类定义如下:
1
2
3
4
5
6
7
8
9
10
class Base
{
public:
int member; // 非静态数据成员
static int smember; // 静态数据成员
void func(); // 非静态成员函数
static void sfunc(); // 静态成员函数
virtual void vfunc(); // 虚函数
};
-简单对象模型
在简单对象模型中,类的每个实例化对象在内存中都会申请一块连续内存,在这块内存上保存着指向成员变量和函数的指针。这样做的好处是,每个对象的内存大小是固定的,不管是数据成员还是成员函数,都只对应一个指针,在访问时可以很快计算出指针偏移量。但这样做的弊端也很明显,当访问对象的数据成员时,需要先找到数据成员的指针,然后通过这个指针获取到真实的数据,多了一次寻址,效率较低。
-
+简单对象模型
在简单对象模型中,类的每个实例化对象在内存中都会申请一块连续内存,在这块内存上保存着指向成员变量和函数的指针。这样做的好处是,每个对象的内存大小是固定的,不管是数据成员还是成员函数,都只对应一个指针,在访问时可以很快计算出指针偏移量。但这样做的弊端也很明显,当访问对象的数据成员时,需要先找到数据成员的指针,然后通过这个指针获取到真实的数据,多了一次寻址,效率较低。
+
-表驱动对象模型
表格驱动对象模型对类中的数据成员和成员函数进行了区分,每个对象保留两个指针,一个指向数据成员表,其中存放数据成员的取值,一个指向成员函数表,其中存放指向成员函数的指针。这种做法的好处是对数据和函数进行分开管理,不好的地方在于访问成员函数时,需要先找到成员函数表,效率更低。
-
+表驱动对象模型
表格驱动对象模型对类中的数据成员和成员函数进行了区分,每个对象保留两个指针,一个指向数据成员表,其中存放数据成员的取值,一个指向成员函数表,其中存放指向成员函数的指针。这种做法的好处是对数据和函数进行分开管理,不好的地方在于访问成员函数时,需要先找到成员函数表,效率更低。
+
-C++ 对象模型
介绍 C++ 对象模型之前,先理解 C++ 中虚函数的概念。C++ 的多态是通过虚函数实现的。在基类中用 virtual 关键字定义虚函数,在派生类中重写虚函数,那么在运行时,传入基类指针时,会根据对象的实际类型来调用正确的函数,实现多态。
+C++ 对象模型
介绍 C++ 对象模型之前,先理解 C++ 中虚函数的概念。C++ 的多态是通过虚函数实现的。在基类中用 virtual 关键字定义虚函数,在派生类中重写虚函数,那么在运行时,传入基类指针时,会根据对象的实际类型来调用正确的函数,实现多态。
不妨试想一下,如果要自己设计 C++ 对象模型,应该怎么做?我是这样想的:C++ 的类实例化为不同的对象,这些对象的静态数据成员是共享的,非静态数据成员是互不影响的,成员函数对各个对象来说是相同的。那么非静态数据成员应该放在每个对象的内存空间中,以保证不同对象相互独立;静态数据成员和成员函数应该放在这个类的公共空间,不用每个对象都存一份,减少存储空间;而虚函数在继承中发挥独特作用,应该与其他成员函数分开。
揭开谜底,以 Base 类为例,C++ 对象模型是长这样的:
-
+
可以看出:
@@ -283,10 +282,10 @@ 普通继承场景下的对象模型 单一继承场景
以 Base 为基类,定义派生类 Derived 如下:
+普通继承场景下的对象模型
单一继承场景
以 Base 为基类,定义派生类 Derived 如下:
1
2
3
4
5
6
7
8
9
10
11
class Derived : public Base
{
public:
int member; // 非静态数据成员
static int smember_d; // 静态数据成员
void func(); // 非静态成员函数
static void sfunc_d(); // 静态成员函数
virtual void vfunc(); // 虚函数
virtual void vfunc_d(); // 虚函数
}
这种场景下,Derived 的对象模型如下:
-
+
可以看出:
@@ -294,28 +293,28 @@ 多重继承场景
单一继承场景下,派生类在基类的虚函数表上进行修改。多重继承场景,有多个基类的虚函数表。定义基类 Base1 和派生类 Derived 如下:
+多重继承场景
单一继承场景下,派生类在基类的虚函数表上进行修改。多重继承场景,有多个基类的虚函数表。定义基类 Base1 和派生类 Derived 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base1
{
public:
int member1; // 非静态数据成员
virtual void vfunc(); // 虚函数
virtual void vfunc1(); // 虚函数
virtual void vfunc2(); // 虚函数
};
class Derived : public Base, public Base1
{
public:
int member_d; // 非静态数据成员
static int smember_d; // 静态数据成员
void func(); // 非静态成员函数
static void sfunc_d(); // 静态成员函数
virtual void vfunc(); // 虚函数
virtual void vfunc1(); // 虚函数
virtual void vfunc_d(); // 虚函数
};
此时 Derived 的对象模型为:
-
+
可以看出:
- 继承多个父类时,派生类的对象模型中分别存储一个虚函数表的指针。如果派生类重写的基类同名虚函数,那就在虚函数表中进行替换;如果派生类新增了虚函数,那么在第一个父类的虚函数表中进行新增扩展。
-虚继承场景下的对象模型
使用普通继承的菱形继承
多重继承场景中,有一种特殊的情况叫菱形继承,它是这样操作的:B1 继承 B,B2 继承 B,D 继承 B1 和 B2,祖父类 B 被子类 D 重复继承,图示如下:
-
+虚继承场景下的对象模型
使用普通继承的菱形继承
多重继承场景中,有一种特殊的情况叫菱形继承,它是这样操作的:B1 继承 B,B2 继承 B,D 继承 B1 和 B2,祖父类 B 被子类 D 重复继承,图示如下:
+
按多重继承的方式推导 D 的对象模型是这样的:
-
+
可以看到,在菱形继承场景中,访问祖父类成员 B::b 时,出现了二义性。
为解决菱形继承中的问题,C++ 发明了虚继承。虚继承的对象模型与普通继承不同:普通继承是子类拷贝并修改父类的虚函数表;虚继承是子类和父类的虚函数表分开保存,将指向父类的虚函数表的指针也加入到子类的对象模型中。
-使用虚继承的单一继承
假定单一虚继承场景如下:
-
+使用虚继承的单一继承
假定单一虚继承场景如下:
+
此时派生类 Derived 的对象模型为:
-
+
可以看到:
@@ -323,18 +322,17 @@ 使用虚继承的菱形继承
假定使用虚继承的菱形继承场景如下:
-
+使用虚继承的菱形继承
假定使用虚继承的菱形继承场景如下:
+
结合多重继承与单一虚继承,推导派生类 Derived 的对象模型为:
-
+
可以看到:
- 多重继承的布局基本不变,虚基类的信息被追加到内存布局最后,并用 0x00000000 隔开。此时再访问 B::b 不会出现二义性。
附注:
推导时,从子类往祖父类逐步推进,子类与父类适用多重继承,父类与祖父类适用单一虚继承,每一步只决定派生类的数据成员的位置,例如 B::b 的布局应该由单一虚继承决定,如果在多重继承时决定,那推出来也是二义的。
-总结
对象模型揭示了 C++ 对象在内存中的存储布局,结合面向对象的三大特性(继承、封装和多态)来理解对象模型,印象会更深一点。
-
+总结
对象模型揭示了 C++ 对象在内存中的存储布局,结合面向对象的三大特性(继承、封装和多态)来理解对象模型,印象会更深一点。
diff --git "a/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" "b/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html"
index 7733d9b..820d8fb 100644
--- "a/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html"
+++ "b/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html"
@@ -5,25 +5,25 @@
-
- C++ 与 Python 的 OOP 比较 - 千里之行始于足下
+
+ C++ 与 Python 的 OOP 比较 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
@@ -76,7 +76,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -247,12 +247,11 @@
- C++ 和 Python 都是从 C 语言演变出来的面向对象(OOP)的编程语言,本文基于 OOP 三特性,比较 C++ 和 Python 的异同点
-
+
-1. OOP 三特性
封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。
-2. 封装
类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。
-数据
于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。
+1. OOP 三特性
封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。
+2. 封装
类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。
+数据
于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。
@@ -272,7 +271,7 @@ 数据
实例变量
-方法
既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。
+方法
既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。
@@ -293,29 +292,27 @@ 方法
C++ 中还有一种重要方法是 虚函数,使用虚函数可以实现 C++ 中的多态。
Python 中还有一种方法是静态方法。在 Python 中可以认为,实例方法传入了实例对象的指针,类方法传入了类的指针,而静态方法既不需要传入实例,也不需要传入类。
-3. 继承
子类继承父类,使子类拥有父类的数据和方法。
-单一继承
这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。
-多重继承
没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO。
-4. 多态
多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。
-C++ 多态
前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:
-
+3. 继承
子类继承父类,使子类拥有父类的数据和方法。
+单一继承
这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。
+多重继承
没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO。
+4. 多态
多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。
+C++ 多态
前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:
+
执行如下代码:
1
2
Base *ptr = new Derived();
ptr->method();
编译器看到 ptr 是 Base 类型,如果 method 不是虚函数,那么执行的应该是 Base::method。现在 method 是虚函数,执行期调用 Base::method 还是 Derived::method,要看赋给 ptr 的是 Base 对象地址还是 Derived 对象地址。上面的代码是创建了一个 Derived 对象,并把地址传给 Base 类指针,但是内存模型中的 vptr 指向的仍然是 Derived 类实现的虚函数,所以最后调用的是 Derived::method。
-Python 多态
相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。
+Python 多态
相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。
1
2
3
4
5
6
7
8
9
10
11
12
class Cat:
def say(cls):
print("miao miao")
class Dog:
def say(cls):
print("wang wang")
def func(kls):
kls.say()
这里 Cat 和 Dog 没有继承关系,say 也不是虚函数,调用 func(Cat)和 func(Dog)都能正确执行。要是再定义个 Person 类,只要定义了方法 say,就可以把 Person 传给 func 完成调用。
-5. 写在最后
虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:
+5. 写在最后
虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:
C++ 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
public:
int x = 1;
virtual void print() { std::cout << x << std::endl; }
};
class Derived : public Base
{
public:
int x = 2;
};
int main() {
Base *ptr = nullptr;
ptr = new Base();
ptr->print(); // 打印 1
ptr = new Derived();
ptr->print(); // 打印 1
}
Python 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base:
def __init__(self):
self.x = 1
def print(self):
print(self.x)
class Derived(Base):
def __init__(self):
self.x = 2
def func(Kls):
obj = Kls()
obj.print()
func(Base) // 打印1
func(Derived) // 打印2
-
-
diff --git "a/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html" "b/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
index 8146de8..c55e117 100644
--- "a/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
+++ "b/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
@@ -5,29 +5,29 @@
-
- Linux 从入门到熟练 - 千里之行始于足下
+
+ Linux 从入门到熟练 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
-
+
@@ -76,7 +76,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -247,16 +247,15 @@
- 常用的各类 Linux 操作命令
-
+
-一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
+一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
CLI 是用户与内核打交道的工具。用户日常使用操作系统,最频繁的场景就是文件操作和网络访问。故而基础篇笔记主要记录 CLI 基本操作、文件系统、网络配置、权限管理等相关知识点。
-二、CLI 基本操作
(一)查看目录
+二、CLI 基本操作
(一)查看目录
- ls:显示指定目录下的文件及属性信息
- pwd:显示当前工作目录的路径
-(二)查看内容
+(二)查看内容
- echo:输出字符串或变量值(变量前加 $ 符号)
- cat:输出文件内容
- head:查看文件头部内容
@@ -264,17 +263,17 @@ (三)开关机
+(三)开关机
- shutdown:按指定方式关闭系统
- poweroff:关机
- reboot:重启
-(四)归档压缩
+(四)归档压缩
- zip:打包成 zip 文件
- unzip:zip 文件解包
- tar:压缩和解压缩文件
-(五)文件和目录操作
+(五)文件和目录操作
- locate:基于数据库快速查找文件和目录,手动更新数据库使用 updatedb 命令
- find:根据条件在指定目录下查找文件和目录
- cp:复制文件或目录
@@ -285,7 +284,7 @@ (六)进程操作
+(六)进程操作
- &:命令 + 空格 +&,会将任务置于后台执行
- jobs:查看后台任务
- fg:将后台任务放到前台执行
@@ -293,11 +292,11 @@ (七)命令帮助
+(七)命令帮助
- history:查看历史命令记录
- man:查看命令的帮助手册,一般情况下命令加 -h 或–help 够用了
-(八)文本编辑
+(八)文本编辑
- grep:搜索文本,可使用关键字或正则表达式
- sed:编辑文本,可使用正则表达式或脚本
- cut:基于列处理文件内容
@@ -332,7 +331,7 @@ (九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
+(九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
通过管道和重定向操作符,可以控制标准数据流的来源和去向:
@@ -380,7 +379,7 @@ echo ‘hello world’ | grep ‘hello’
-三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
+三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
graph TB
A["/"] --> B[bin] & C[boot] & D[dev] & E[etc] & F[home] & G[lib] & H[mnt] & I[opt] & J[proc] & K[root] & L[sbin] & M[tmp] & N[usr] & O[var]
N[usr] --> N1[bin] & N2[lib] & N3[local]
@@ -390,7 +389,7 @@
/: 根目录
/bin: 存放二进制可执行文件,root 用户和普通用户都能使用
/boot: 存放 linux 启动时的引导程序
/dev: linux 的设备也是以文件形式组织,存放在此目录下
/etc: 存放 linux 的系统管理和配置文件
/home: 存放普通用户的家目录,类似于 windows 下 C 盘 Users 用户文件夹
/lib: 存放程序运行依赖的库文件和内核驱动模块
/mnt: 设备挂载目录
/opt: 三方软件或大型软件的安装目录
/proc: 虚拟文件系统,记录系统实时信息,只存在内存中,不占用磁盘空间
/root: root 用户的家目录
/sbin: 存放二进制文件,仅 root 用户有权限使用
/tmp: 临时目录,重启系统会自动删除
/usr: 不是 user 缩写,是 unix software resource 缩写,存放系统上安装的软件资源
/var: 存放系统中经常变动的文件,如缓存 cache、日志 log、邮件 mail 等
-(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
+(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
+四、权限管理
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
linux 的用户分为 root 用户、系统用户和普通用户,每个用户用唯一的 uid 标识,root 用户的 uid 为 0,系统用户的 uid 通常在 1~999,普通用户的 uid 通常 >=1000。系统用户主要用于执行系统程序,无法使用 shell 登录。
为了便于对用户进行管理,每个用户还有对应的用户组,包括主组和附加组,主组只能有一个,附加组可以有多个。默认情况下,创建用户时,会将用户归属于同名的主组。
linux 上用户和用户组的信息都保存在 /etc 目录下,相关命令和文件说明如下:
@@ -416,9 +415,9 @@ /etc/shadow:用户密码信息(已加密)
/etc/group:用户组信息
-(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
+(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
linux 通过 UGO 模型对文件进行权限控制,其中 U(user)表示文件所属用户,G(group)表示文件所属用户组,O(other)表示既不是所属用户也不是所属用户组的其他群体。
-
+
通过 ls -l 查看文件权限如上图所示,第一列权限信息包含 10 位字符,第三列表示文件所属用户(属主),第四列表示文件所属用户组(属组)。权限信息中,第 1 个字符表示文件类型(- 表示文件,d 表示文件夹,l 表示符号链接),后面 9 个字符每 3 个一组,分别描述属主(user)权限、属组(group)权限、其他(other)权限。权限位是 rwx 表示具有相应权限,权限位是 - 表示没有相应权限。除了以字符的方式,每组权限还可以用 3 个 bit 位来表示,r 在高位,w 在中位,x 在低位,于是 rwx=b111=4+2+1=7。在修改权限时,可以使用任一种表达方式。
在创建文件或文件夹时,系统会设定默认的权限,通过原始权限减去权限掩码的方式,可以得到默认权限。文件夹的原始权限是 777(rwxrwxrwx),文件的原始权限是 666(rw-rw-rw-)。普通用户的默认掩码是 002,root 用户的默认掩码是 022。因此,对普通用户来说,新建文件夹的权限是 775(rwxrwxr-x),新建文件的权限是 664(rw-rw-r–)。
@@ -429,7 +428,7 @@ (三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
+(三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
@@ -461,11 +460,11 @@ 五、网络配置 (一)IP- 子网掩码 - 网关 -DNS
IP(v4) 编码用来标识互联网中的机器地址,它包含 32 个 bit 位,由网络地址 + 主机地址两部分组成。
+五、网络配置
(一)IP- 子网掩码 - 网关 -DNS
IP(v4) 编码用来标识互联网中的机器地址,它包含 32 个 bit 位,由网络地址 + 主机地址两部分组成。
子网掩码用来将 IP 地址切分为网络地址和主机地址,它也由 32 个 bit 位构成,且高位为连续的 1。子网掩码与 IP 地址成对出现,子网掩码与 IP 地址进行按位与运算可得网络地址。例如 IP 地址 192.168.1.1,子网掩码 255.255.255.0,可以算出网络地址为 192.168.1.0,可分配的主机地址包括 1~254(255 转换成 bit 位全为 1,全 1 的主机地址保留为广播地址),这一网段可以记为 192.168.1.0/24。同一网段的机器通信使用 ARP 协议,将 IP 地址解析为 MAC 地址,再基于 MAC 通信。
不同网络的主机之间进行通信需要使用网关,比如常见的路由器。网络 A 中的主机 A1 要跟网络 B 中的主机 B1 通信,主机 A1 需要先将数据包发送到 A 的网关,再由 A 的网关转发到 B 的网关,然后 B 的网关把数据包发送到主机 B1。现实中,跨域网络通信可能经过很多次路由器转发。
在互联网冲浪的时候,敲在地址栏里的是一串字符组成的域名。访问一个域名,实际上访问的也是互联网的一台主机。域名和 IP 的映射关系,通过 DNS 服务器进行解析,DNS 协议可以将域名解析为 IP 地址。
-(二)相关命令和配置文件
+(二)相关命令和配置文件
- ifconfig:查看和设置网络设备,重启后会失效,永久配置需要修改配置文件
- hostname:查看和设置系统的主机名
- ping:测试本机与目标主机之间的网络连通性
@@ -478,7 +477,6 @@
-
- Docker 必知必会 - 千里之行始于足下
+
+ Docker 必知必会 - 千里之行,始于足下
-
-
+
+
-
-
+
+
@@ -36,11 +36,11 @@
-
+
-
+
@@ -89,7 +89,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -260,12 +260,11 @@
- Docker 基本原理以及常用命令
-
+
-一、基本概念
Docker 是一种轻量级的虚拟化技术,可以让开发者非常便捷地实现应用程序的打包、移植、启动等操作,在软件开发、交付和部署中,有非常广泛的应用。
+一、基本概念
Docker 是一种轻量级的虚拟化技术,可以让开发者非常便捷地实现应用程序的打包、移植、启动等操作,在软件开发、交付和部署中,有非常广泛的应用。
Docker 容器与传统虚拟机的架构对比如下:
-
+
传统 VM 使用 Hypervisor 通过对物理主机的硬件资源进行协调和管理,为每个 GuestOS 分配独立的资源,让每个 GuestOS 成为一个虚拟主机,不同的 GuestOS 中的应用程序互不影响。Docker 容器直接运行在物理主机的 HostOS 上,共用物理主机的硬件资源,Container Engine 负责实现容器之间的资源隔离,让每个容器的应用独立地运行。
可以看出,容器比虚拟机少了一层 GuestOS,容器占用资源更少,启动更快,但隔离程度不如虚拟机。容器和虚拟机简要对比如下:
@@ -314,16 +313,16 @@
Docker 中有三个常见的名词:镜像、容器和仓库。这里先简单介绍下概念,知道是什么就行,后面再详细说明。
-(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
-(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
+(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
+(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
容器实质上是运行在宿主机上的进程,Docker 是用特殊的技术将容器与宿主机上的其他进程隔离开来,使得容器内的应用看起来是运行在一个独立的环境中。
-(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
-二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
+(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
+二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
- 容器内屏蔽容器外的情况,使用 Linux 的 Namespace 机制
- 容器拥有独立的资源,使用 Linux 的 Cgroups 机制
-(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
+(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
@@ -368,30 +367,30 @@ 隔离系统时间
-(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
+(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
查看 cgroup 挂载的目录,可以看到 cgroup 挂在 sys/fs/cgroup 节点,该路径下还有很多子目录(又称子系统),如 cpu、memory 等,每个子系统对应一种可以被限制的资源类型。
-
+
以 cpu 为例,查看 cpu 子系统。其中有两个参数 cfs_period_us 和 cfs_quota_us 通常组合使用,用于限制进程在长度为 cfs_period_us 的时间内,只能被分配到总量为 cfs_quota_us 的 cpu 时间。还有一个 tasks 文件,其中存放的是受限制的进程编号。
-
-
+
+
cpu 子系统中有个 docker 子目录,docker 目录中的文件与 cpu 目录中的文件一样。当我们拉起一个容器,比如运行 redis 镜像,可以看到 docker 目录中又多了一层以容器 id 为名称的子目录。
-
-
+
+
综上两点,容器其实是一个启用了多种 Namespace 的进程,它能够使用的资源量收到 Cgroups 的限制。截至目前,我们使用 Namespace 和 Cgroups 为容器开辟了一个独立的运行环境,接下来我们再剖析一下容器里运行的镜像。
前面提到,容器镜像是一个文件系统,我们运行 ubuntu 的镜像,可以通过命令行查看根目录内容,也可以创建并执行脚本文件:
-
+
我们还可以把改动之后的文件系统打包成我们自己的镜像,下次直接执行:
-
+
可以看到,我们以 ubuntu 文件系统为基础,加了一层我们自己的修改,打包了一个新的镜像。容器镜像实际上就是以 Linux 的联合文件系统机制(UnionFS)实现分层文件系统。
-(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
+(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
Docker 分层镜像的示例图如下,除了最顶层(称为容器层)是可写的,其他层(称为镜像层)都是只读的。UnoinFS 将所有层都叠加起来,形成最后我们在容器中所看到的文件系统。
-
+
由于镜像层都是只读的,所有对文件的修改操作都发生在容器层中,Docker 采用了 Copy-On-Write(写时复制) 的机制,具体来说:
@@ -401,15 +400,15 @@ (四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
-三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
+(四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
+三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
1
docker command --help
Docker 命令主要涉及到镜像、容器、仓库的操作和使用,下图可以概览全貌:
-
+
以下列举一些常用的 docker 命令,各命令详细用法可以通过 help 查看:
-1. 镜像相关
+1. 镜像相关
-2. 容器相关
+2. 容器相关
-(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
+(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
Dockerfile 是由一行行指令组成的脚本文件,每一行命令都会生成一个镜像层,多行命令生成多个镜像层,叠加生成最终的镜像文件。Dockerfile 的常见指令及含义如图所示:
-
+
最后看个 Dockerfile 的小例子:
编写 Dockerfile 文件
-
+
生成镜像文件
-
+
查看并运行镜像文件
-
+
-四、参考资料
+四、参考资料
-
diff --git "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html" "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
index 3df135e..40a3d52 100644
--- "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
+++ "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
@@ -5,28 +5,28 @@
-
- LibFuzzer 使用说明 - 千里之行始于足下
+
+ LibFuzzer 使用说明 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
-
+
@@ -74,7 +74,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -245,8 +245,7 @@
- LibFuzzer 从 0 到 1,原理 + 安装 + 使用 + 优化,一篇讲完
-
+
按照 官方定义,LibFuzzer 是一个in-process(进程内的),coverage-guided(以覆盖率为引导的),evolutionary(进化的) 的 fuzz 引擎,是 LLVM 项目的一部分,主要用于对 C/C++ 程序进行 Fuzz 测试。LibFuzzer 三个特性的具体含义为:
@@ -264,13 +263,13 @@
LibFuzzer 与待测的 library 进行链接,通过向指定的 fuzzing 入口(即target 函数)发送测试数据,并跟踪被触达的代码区域,然后对输入的数据进行变异,以达到代码覆盖率最大的目的,其中代码覆盖率的信息由 LLVM 的 SanitizerCoverage 工具提供。
-一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
+一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
1
apt install clang
安装完毕之后,可以通过如下命令查看 clang 版本:
1
clang --version
-2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
+2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Values other than 0 and -1 are reserved for future use.
}
target 函数的名称、参数类型、返回值类型都不能改变。此外,官方文档中还有如下说明:
@@ -336,11 +335,11 @@ libFuzzer 编译链接。
关于 LibFuzzer 通过插桩统计代码覆盖率的具体实现,有兴趣可以参考Coverage Control in libFuzzer。
-3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
+3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
1
./fuzz-target -flag1=val1 -flag2=val2 ... path1 path2 ...
在不限制运行时长的情况下,Fuzz 将会一直执行下去,直到出现 crash(通常是因为触发 sanitizer 异常)才会终止。导致 crash 的输入将会作为能触发 bug 的 testcase 保存到磁盘上,并根据 crash 的类型,用不同的文件名前缀进行区分,比如 crash-XXX,leak-XXX,timeout-XXX 等。
-(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
+(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
介绍 C++ 对象在内存中的存储模型
- + -对象模型基础
C++ 在 C 的基础上引入了面向对象的程序设计思想,类的封装、继承和多态使得 C++ 功能更加强大。C++ 的类包含数据成员和成员函数:
+对象模型基础
C++ 在 C 的基础上引入了面向对象的程序设计思想,类的封装、继承和多态使得 C++ 功能更加强大。C++ 的类包含数据成员和成员函数:
数据成员:静态数据成员,非静态数据成员
成员函数:非静态成员函数、静态函数、虚函数
C++ 对象模型研究 C++ 类的数据成员和成员函数在内存中如何布局的问题。假设有个 C++ 类定义如下:
1 | class Base |
简单对象模型
在简单对象模型中,类的每个实例化对象在内存中都会申请一块连续内存,在这块内存上保存着指向成员变量和函数的指针。这样做的好处是,每个对象的内存大小是固定的,不管是数据成员还是成员函数,都只对应一个指针,在访问时可以很快计算出指针偏移量。但这样做的弊端也很明显,当访问对象的数据成员时,需要先找到数据成员的指针,然后通过这个指针获取到真实的数据,多了一次寻址,效率较低。
- +简单对象模型
在简单对象模型中,类的每个实例化对象在内存中都会申请一块连续内存,在这块内存上保存着指向成员变量和函数的指针。这样做的好处是,每个对象的内存大小是固定的,不管是数据成员还是成员函数,都只对应一个指针,在访问时可以很快计算出指针偏移量。但这样做的弊端也很明显,当访问对象的数据成员时,需要先找到数据成员的指针,然后通过这个指针获取到真实的数据,多了一次寻址,效率较低。
+ -表驱动对象模型
表格驱动对象模型对类中的数据成员和成员函数进行了区分,每个对象保留两个指针,一个指向数据成员表,其中存放数据成员的取值,一个指向成员函数表,其中存放指向成员函数的指针。这种做法的好处是对数据和函数进行分开管理,不好的地方在于访问成员函数时,需要先找到成员函数表,效率更低。
- +表驱动对象模型
表格驱动对象模型对类中的数据成员和成员函数进行了区分,每个对象保留两个指针,一个指向数据成员表,其中存放数据成员的取值,一个指向成员函数表,其中存放指向成员函数的指针。这种做法的好处是对数据和函数进行分开管理,不好的地方在于访问成员函数时,需要先找到成员函数表,效率更低。
+ -C++ 对象模型
介绍 C++ 对象模型之前,先理解 C++ 中虚函数的概念。C++ 的多态是通过虚函数实现的。在基类中用 virtual 关键字定义虚函数,在派生类中重写虚函数,那么在运行时,传入基类指针时,会根据对象的实际类型来调用正确的函数,实现多态。
+C++ 对象模型
介绍 C++ 对象模型之前,先理解 C++ 中虚函数的概念。C++ 的多态是通过虚函数实现的。在基类中用 virtual 关键字定义虚函数,在派生类中重写虚函数,那么在运行时,传入基类指针时,会根据对象的实际类型来调用正确的函数,实现多态。
不妨试想一下,如果要自己设计 C++ 对象模型,应该怎么做?我是这样想的:C++ 的类实例化为不同的对象,这些对象的静态数据成员是共享的,非静态数据成员是互不影响的,成员函数对各个对象来说是相同的。那么非静态数据成员应该放在每个对象的内存空间中,以保证不同对象相互独立;静态数据成员和成员函数应该放在这个类的公共空间,不用每个对象都存一份,减少存储空间;而虚函数在继承中发挥独特作用,应该与其他成员函数分开。
揭开谜底,以 Base 类为例,C++ 对象模型是长这样的:
- +可以看出:
-
@@ -283,10 +282,10 @@
- 继承多个父类时,派生类的对象模型中分别存储一个虚函数表的指针。如果派生类重写的基类同名虚函数,那就在虚函数表中进行替换;如果派生类新增了虚函数,那么在第一个父类的虚函数表中进行新增扩展。
- 多重继承的布局基本不变,虚基类的信息被追加到内存布局最后,并用 0x00000000 隔开。此时再访问 B::b 不会出现二义性。
附注:
推导时,从子类往祖父类逐步推进,子类与父类适用多重继承,父类与祖父类适用单一虚继承,每一步只决定派生类的数据成员的位置,例如 B::b 的布局应该由单一虚继承决定,如果在多重继承时决定,那推出来也是二义的。
普通继承场景下的对象模型 单一继承场景
以 Base 为基类,定义派生类 Derived 如下:
+普通继承场景下的对象模型
单一继承场景
以 Base 为基类,定义派生类 Derived 如下:
1 | class Derived : public Base |
这种场景下,Derived 的对象模型如下:
- +可以看出:
-
@@ -294,28 +293,28 @@
多重继承场景
单一继承场景下,派生类在基类的虚函数表上进行修改。多重继承场景,有多个基类的虚函数表。定义基类 Base1 和派生类 Derived 如下:
+多重继承场景
单一继承场景下,派生类在基类的虚函数表上进行修改。多重继承场景,有多个基类的虚函数表。定义基类 Base1 和派生类 Derived 如下:
1 | class Base1 |
此时 Derived 的对象模型为:
- +可以看出:
虚继承场景下的对象模型
使用普通继承的菱形继承
多重继承场景中,有一种特殊的情况叫菱形继承,它是这样操作的:B1 继承 B,B2 继承 B,D 继承 B1 和 B2,祖父类 B 被子类 D 重复继承,图示如下:
- +虚继承场景下的对象模型
使用普通继承的菱形继承
多重继承场景中,有一种特殊的情况叫菱形继承,它是这样操作的:B1 继承 B,B2 继承 B,D 继承 B1 和 B2,祖父类 B 被子类 D 重复继承,图示如下:
+按多重继承的方式推导 D 的对象模型是这样的:
- +可以看到,在菱形继承场景中,访问祖父类成员 B::b 时,出现了二义性。
为解决菱形继承中的问题,C++ 发明了虚继承。虚继承的对象模型与普通继承不同:普通继承是子类拷贝并修改父类的虚函数表;虚继承是子类和父类的虚函数表分开保存,将指向父类的虚函数表的指针也加入到子类的对象模型中。
-使用虚继承的单一继承
假定单一虚继承场景如下:
- +使用虚继承的单一继承
假定单一虚继承场景如下:
+此时派生类 Derived 的对象模型为:
- +可以看到:
-
@@ -323,18 +322,17 @@
使用虚继承的菱形继承
假定使用虚继承的菱形继承场景如下:
- +使用虚继承的菱形继承
假定使用虚继承的菱形继承场景如下:
+结合多重继承与单一虚继承,推导派生类 Derived 的对象模型为:
- +可以看到:
总结
对象模型揭示了 C++ 对象在内存中的存储布局,结合面向对象的三大特性(继承、封装和多态)来理解对象模型,印象会更深一点。
- +总结
对象模型揭示了 C++ 对象在内存中的存储布局,结合面向对象的三大特性(继承、封装和多态)来理解对象模型,印象会更深一点。
diff --git "a/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" "b/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" index 7733d9b..820d8fb 100644 --- "a/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" +++ "b/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" @@ -5,25 +5,25 @@ - -
- C++ 和 Python 都是从 C 语言演变出来的面向对象(OOP)的编程语言,本文基于 OOP 三特性,比较 C++ 和 Python 的异同点
-
+
-1. OOP 三特性
封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。
-2. 封装
类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。
-数据
于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。
+1. OOP 三特性
封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。
+2. 封装
类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。
+数据
于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。
@@ -272,7 +271,7 @@ 数据
实例变量
-方法
既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。
+方法
既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。
@@ -293,29 +292,27 @@ 方法
C++ 中还有一种重要方法是 虚函数,使用虚函数可以实现 C++ 中的多态。
Python 中还有一种方法是静态方法。在 Python 中可以认为,实例方法传入了实例对象的指针,类方法传入了类的指针,而静态方法既不需要传入实例,也不需要传入类。
-3. 继承
子类继承父类,使子类拥有父类的数据和方法。
-单一继承
这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。
-多重继承
没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO。
-4. 多态
多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。
-C++ 多态
前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:
-
+3. 继承
子类继承父类,使子类拥有父类的数据和方法。
+单一继承
这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。
+多重继承
没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO。
+4. 多态
多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。
+C++ 多态
前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:
+
执行如下代码:
1
2
Base *ptr = new Derived();
ptr->method();
编译器看到 ptr 是 Base 类型,如果 method 不是虚函数,那么执行的应该是 Base::method。现在 method 是虚函数,执行期调用 Base::method 还是 Derived::method,要看赋给 ptr 的是 Base 对象地址还是 Derived 对象地址。上面的代码是创建了一个 Derived 对象,并把地址传给 Base 类指针,但是内存模型中的 vptr 指向的仍然是 Derived 类实现的虚函数,所以最后调用的是 Derived::method。
-Python 多态
相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。
+Python 多态
相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。
1
2
3
4
5
6
7
8
9
10
11
12
class Cat:
def say(cls):
print("miao miao")
class Dog:
def say(cls):
print("wang wang")
def func(kls):
kls.say()
这里 Cat 和 Dog 没有继承关系,say 也不是虚函数,调用 func(Cat)和 func(Dog)都能正确执行。要是再定义个 Person 类,只要定义了方法 say,就可以把 Person 传给 func 完成调用。
-5. 写在最后
虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:
+5. 写在最后
虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:
C++ 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
public:
int x = 1;
virtual void print() { std::cout << x << std::endl; }
};
class Derived : public Base
{
public:
int x = 2;
};
int main() {
Base *ptr = nullptr;
ptr = new Base();
ptr->print(); // 打印 1
ptr = new Derived();
ptr->print(); // 打印 1
}
Python 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base:
def __init__(self):
self.x = 1
def print(self):
print(self.x)
class Derived(Base):
def __init__(self):
self.x = 2
def func(Kls):
obj = Kls()
obj.print()
func(Base) // 打印1
func(Derived) // 打印2
-
-
diff --git "a/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html" "b/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
index 8146de8..c55e117 100644
--- "a/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
+++ "b/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html"
@@ -5,29 +5,29 @@
-
- Linux 从入门到熟练 - 千里之行始于足下
+
+ Linux 从入门到熟练 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
-
+
@@ -76,7 +76,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -247,16 +247,15 @@
- 常用的各类 Linux 操作命令
-
+
-一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
+一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
CLI 是用户与内核打交道的工具。用户日常使用操作系统,最频繁的场景就是文件操作和网络访问。故而基础篇笔记主要记录 CLI 基本操作、文件系统、网络配置、权限管理等相关知识点。
-二、CLI 基本操作
(一)查看目录
+二、CLI 基本操作
(一)查看目录
- ls:显示指定目录下的文件及属性信息
- pwd:显示当前工作目录的路径
-(二)查看内容
+(二)查看内容
- echo:输出字符串或变量值(变量前加 $ 符号)
- cat:输出文件内容
- head:查看文件头部内容
@@ -264,17 +263,17 @@ (三)开关机
+(三)开关机
- shutdown:按指定方式关闭系统
- poweroff:关机
- reboot:重启
-(四)归档压缩
+(四)归档压缩
- zip:打包成 zip 文件
- unzip:zip 文件解包
- tar:压缩和解压缩文件
-(五)文件和目录操作
+(五)文件和目录操作
- locate:基于数据库快速查找文件和目录,手动更新数据库使用 updatedb 命令
- find:根据条件在指定目录下查找文件和目录
- cp:复制文件或目录
@@ -285,7 +284,7 @@ (六)进程操作
+(六)进程操作
- &:命令 + 空格 +&,会将任务置于后台执行
- jobs:查看后台任务
- fg:将后台任务放到前台执行
@@ -293,11 +292,11 @@ (七)命令帮助
+(七)命令帮助
- history:查看历史命令记录
- man:查看命令的帮助手册,一般情况下命令加 -h 或–help 够用了
-(八)文本编辑
+(八)文本编辑
- grep:搜索文本,可使用关键字或正则表达式
- sed:编辑文本,可使用正则表达式或脚本
- cut:基于列处理文件内容
@@ -332,7 +331,7 @@ (九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
+(九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
通过管道和重定向操作符,可以控制标准数据流的来源和去向:
@@ -380,7 +379,7 @@ echo ‘hello world’ | grep ‘hello’
-三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
+三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
graph TB
A["/"] --> B[bin] & C[boot] & D[dev] & E[etc] & F[home] & G[lib] & H[mnt] & I[opt] & J[proc] & K[root] & L[sbin] & M[tmp] & N[usr] & O[var]
N[usr] --> N1[bin] & N2[lib] & N3[local]
@@ -390,7 +389,7 @@
/: 根目录
/bin: 存放二进制可执行文件,root 用户和普通用户都能使用
/boot: 存放 linux 启动时的引导程序
/dev: linux 的设备也是以文件形式组织,存放在此目录下
/etc: 存放 linux 的系统管理和配置文件
/home: 存放普通用户的家目录,类似于 windows 下 C 盘 Users 用户文件夹
/lib: 存放程序运行依赖的库文件和内核驱动模块
/mnt: 设备挂载目录
/opt: 三方软件或大型软件的安装目录
/proc: 虚拟文件系统,记录系统实时信息,只存在内存中,不占用磁盘空间
/root: root 用户的家目录
/sbin: 存放二进制文件,仅 root 用户有权限使用
/tmp: 临时目录,重启系统会自动删除
/usr: 不是 user 缩写,是 unix software resource 缩写,存放系统上安装的软件资源
/var: 存放系统中经常变动的文件,如缓存 cache、日志 log、邮件 mail 等
-(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
+(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
+四、权限管理
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
linux 的用户分为 root 用户、系统用户和普通用户,每个用户用唯一的 uid 标识,root 用户的 uid 为 0,系统用户的 uid 通常在 1~999,普通用户的 uid 通常 >=1000。系统用户主要用于执行系统程序,无法使用 shell 登录。
为了便于对用户进行管理,每个用户还有对应的用户组,包括主组和附加组,主组只能有一个,附加组可以有多个。默认情况下,创建用户时,会将用户归属于同名的主组。
linux 上用户和用户组的信息都保存在 /etc 目录下,相关命令和文件说明如下:
@@ -416,9 +415,9 @@ /etc/shadow:用户密码信息(已加密)
/etc/group:用户组信息
-(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
+(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
linux 通过 UGO 模型对文件进行权限控制,其中 U(user)表示文件所属用户,G(group)表示文件所属用户组,O(other)表示既不是所属用户也不是所属用户组的其他群体。
-
+
通过 ls -l 查看文件权限如上图所示,第一列权限信息包含 10 位字符,第三列表示文件所属用户(属主),第四列表示文件所属用户组(属组)。权限信息中,第 1 个字符表示文件类型(- 表示文件,d 表示文件夹,l 表示符号链接),后面 9 个字符每 3 个一组,分别描述属主(user)权限、属组(group)权限、其他(other)权限。权限位是 rwx 表示具有相应权限,权限位是 - 表示没有相应权限。除了以字符的方式,每组权限还可以用 3 个 bit 位来表示,r 在高位,w 在中位,x 在低位,于是 rwx=b111=4+2+1=7。在修改权限时,可以使用任一种表达方式。
在创建文件或文件夹时,系统会设定默认的权限,通过原始权限减去权限掩码的方式,可以得到默认权限。文件夹的原始权限是 777(rwxrwxrwx),文件的原始权限是 666(rw-rw-rw-)。普通用户的默认掩码是 002,root 用户的默认掩码是 022。因此,对普通用户来说,新建文件夹的权限是 775(rwxrwxr-x),新建文件的权限是 664(rw-rw-r–)。
@@ -429,7 +428,7 @@ (三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
+(三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
@@ -461,11 +460,11 @@ 五、网络配置 (一)IP- 子网掩码 - 网关 -DNS
IP(v4) 编码用来标识互联网中的机器地址,它包含 32 个 bit 位,由网络地址 + 主机地址两部分组成。
+五、网络配置
(一)IP- 子网掩码 - 网关 -DNS
IP(v4) 编码用来标识互联网中的机器地址,它包含 32 个 bit 位,由网络地址 + 主机地址两部分组成。
子网掩码用来将 IP 地址切分为网络地址和主机地址,它也由 32 个 bit 位构成,且高位为连续的 1。子网掩码与 IP 地址成对出现,子网掩码与 IP 地址进行按位与运算可得网络地址。例如 IP 地址 192.168.1.1,子网掩码 255.255.255.0,可以算出网络地址为 192.168.1.0,可分配的主机地址包括 1~254(255 转换成 bit 位全为 1,全 1 的主机地址保留为广播地址),这一网段可以记为 192.168.1.0/24。同一网段的机器通信使用 ARP 协议,将 IP 地址解析为 MAC 地址,再基于 MAC 通信。
不同网络的主机之间进行通信需要使用网关,比如常见的路由器。网络 A 中的主机 A1 要跟网络 B 中的主机 B1 通信,主机 A1 需要先将数据包发送到 A 的网关,再由 A 的网关转发到 B 的网关,然后 B 的网关把数据包发送到主机 B1。现实中,跨域网络通信可能经过很多次路由器转发。
在互联网冲浪的时候,敲在地址栏里的是一串字符组成的域名。访问一个域名,实际上访问的也是互联网的一台主机。域名和 IP 的映射关系,通过 DNS 服务器进行解析,DNS 协议可以将域名解析为 IP 地址。
-(二)相关命令和配置文件
+(二)相关命令和配置文件
- ifconfig:查看和设置网络设备,重启后会失效,永久配置需要修改配置文件
- hostname:查看和设置系统的主机名
- ping:测试本机与目标主机之间的网络连通性
@@ -478,7 +477,6 @@
-
- Docker 必知必会 - 千里之行始于足下
+
+ Docker 必知必会 - 千里之行,始于足下
-
-
+
+
-
-
+
+
@@ -36,11 +36,11 @@
-
+
-
+
@@ -89,7 +89,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -260,12 +260,11 @@
- Docker 基本原理以及常用命令
-
+
-一、基本概念
Docker 是一种轻量级的虚拟化技术,可以让开发者非常便捷地实现应用程序的打包、移植、启动等操作,在软件开发、交付和部署中,有非常广泛的应用。
+一、基本概念
Docker 是一种轻量级的虚拟化技术,可以让开发者非常便捷地实现应用程序的打包、移植、启动等操作,在软件开发、交付和部署中,有非常广泛的应用。
Docker 容器与传统虚拟机的架构对比如下:
-
+
传统 VM 使用 Hypervisor 通过对物理主机的硬件资源进行协调和管理,为每个 GuestOS 分配独立的资源,让每个 GuestOS 成为一个虚拟主机,不同的 GuestOS 中的应用程序互不影响。Docker 容器直接运行在物理主机的 HostOS 上,共用物理主机的硬件资源,Container Engine 负责实现容器之间的资源隔离,让每个容器的应用独立地运行。
可以看出,容器比虚拟机少了一层 GuestOS,容器占用资源更少,启动更快,但隔离程度不如虚拟机。容器和虚拟机简要对比如下:
@@ -314,16 +313,16 @@
Docker 中有三个常见的名词:镜像、容器和仓库。这里先简单介绍下概念,知道是什么就行,后面再详细说明。
-(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
-(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
+(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
+(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
容器实质上是运行在宿主机上的进程,Docker 是用特殊的技术将容器与宿主机上的其他进程隔离开来,使得容器内的应用看起来是运行在一个独立的环境中。
-(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
-二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
+(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
+二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
- 容器内屏蔽容器外的情况,使用 Linux 的 Namespace 机制
- 容器拥有独立的资源,使用 Linux 的 Cgroups 机制
-(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
+(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
@@ -368,30 +367,30 @@ 隔离系统时间
-(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
+(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
查看 cgroup 挂载的目录,可以看到 cgroup 挂在 sys/fs/cgroup 节点,该路径下还有很多子目录(又称子系统),如 cpu、memory 等,每个子系统对应一种可以被限制的资源类型。
-
+
以 cpu 为例,查看 cpu 子系统。其中有两个参数 cfs_period_us 和 cfs_quota_us 通常组合使用,用于限制进程在长度为 cfs_period_us 的时间内,只能被分配到总量为 cfs_quota_us 的 cpu 时间。还有一个 tasks 文件,其中存放的是受限制的进程编号。
-
-
+
+
cpu 子系统中有个 docker 子目录,docker 目录中的文件与 cpu 目录中的文件一样。当我们拉起一个容器,比如运行 redis 镜像,可以看到 docker 目录中又多了一层以容器 id 为名称的子目录。
-
-
+
+
综上两点,容器其实是一个启用了多种 Namespace 的进程,它能够使用的资源量收到 Cgroups 的限制。截至目前,我们使用 Namespace 和 Cgroups 为容器开辟了一个独立的运行环境,接下来我们再剖析一下容器里运行的镜像。
前面提到,容器镜像是一个文件系统,我们运行 ubuntu 的镜像,可以通过命令行查看根目录内容,也可以创建并执行脚本文件:
-
+
我们还可以把改动之后的文件系统打包成我们自己的镜像,下次直接执行:
-
+
可以看到,我们以 ubuntu 文件系统为基础,加了一层我们自己的修改,打包了一个新的镜像。容器镜像实际上就是以 Linux 的联合文件系统机制(UnionFS)实现分层文件系统。
-(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
+(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
Docker 分层镜像的示例图如下,除了最顶层(称为容器层)是可写的,其他层(称为镜像层)都是只读的。UnoinFS 将所有层都叠加起来,形成最后我们在容器中所看到的文件系统。
-
+
由于镜像层都是只读的,所有对文件的修改操作都发生在容器层中,Docker 采用了 Copy-On-Write(写时复制) 的机制,具体来说:
@@ -401,15 +400,15 @@ (四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
-三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
+(四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
+三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
1
docker command --help
Docker 命令主要涉及到镜像、容器、仓库的操作和使用,下图可以概览全貌:
-
+
以下列举一些常用的 docker 命令,各命令详细用法可以通过 help 查看:
-1. 镜像相关
+1. 镜像相关
-2. 容器相关
+2. 容器相关
-(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
+(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
Dockerfile 是由一行行指令组成的脚本文件,每一行命令都会生成一个镜像层,多行命令生成多个镜像层,叠加生成最终的镜像文件。Dockerfile 的常见指令及含义如图所示:
-
+
最后看个 Dockerfile 的小例子:
编写 Dockerfile 文件
-
+
生成镜像文件
-
+
查看并运行镜像文件
-
+
-四、参考资料
+四、参考资料
-
diff --git "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html" "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
index 3df135e..40a3d52 100644
--- "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
+++ "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
@@ -5,28 +5,28 @@
-
- LibFuzzer 使用说明 - 千里之行始于足下
+
+ LibFuzzer 使用说明 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
-
+
@@ -74,7 +74,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -245,8 +245,7 @@
- LibFuzzer 从 0 到 1,原理 + 安装 + 使用 + 优化,一篇讲完
-
+
按照 官方定义,LibFuzzer 是一个in-process(进程内的),coverage-guided(以覆盖率为引导的),evolutionary(进化的) 的 fuzz 引擎,是 LLVM 项目的一部分,主要用于对 C/C++ 程序进行 Fuzz 测试。LibFuzzer 三个特性的具体含义为:
@@ -264,13 +263,13 @@
LibFuzzer 与待测的 library 进行链接,通过向指定的 fuzzing 入口(即target 函数)发送测试数据,并跟踪被触达的代码区域,然后对输入的数据进行变异,以达到代码覆盖率最大的目的,其中代码覆盖率的信息由 LLVM 的 SanitizerCoverage 工具提供。
-一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
+一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
1
apt install clang
安装完毕之后,可以通过如下命令查看 clang 版本:
1
clang --version
-2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
+2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Values other than 0 and -1 are reserved for future use.
}
target 函数的名称、参数类型、返回值类型都不能改变。此外,官方文档中还有如下说明:
@@ -336,11 +335,11 @@ libFuzzer 编译链接。
关于 LibFuzzer 通过插桩统计代码覆盖率的具体实现,有兴趣可以参考Coverage Control in libFuzzer。
-3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
+3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
1
./fuzz-target -flag1=val1 -flag2=val2 ... path1 path2 ...
在不限制运行时长的情况下,Fuzz 将会一直执行下去,直到出现 crash(通常是因为触发 sanitizer 异常)才会终止。导致 crash 的输入将会作为能触发 bug 的 testcase 保存到磁盘上,并根据 crash 的类型,用不同的文件名前缀进行区分,比如 crash-XXX,leak-XXX,timeout-XXX 等。
-(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
+(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
C++ 和 Python 都是从 C 语言演变出来的面向对象(OOP)的编程语言,本文基于 OOP 三特性,比较 C++ 和 Python 的异同点
- + -1. OOP 三特性
封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。
2. 封装
类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。
-数据
于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。
+1. OOP 三特性
封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。
2. 封装
类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。
+数据
于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。
实例变量 |
方法
既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。
+方法
既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。
C++ 中还有一种重要方法是 虚函数,使用虚函数可以实现 C++ 中的多态。
Python 中还有一种方法是静态方法。在 Python 中可以认为,实例方法传入了实例对象的指针,类方法传入了类的指针,而静态方法既不需要传入实例,也不需要传入类。
3. 继承
子类继承父类,使子类拥有父类的数据和方法。
-单一继承
这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。
-多重继承
没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO。
4. 多态
多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。
-C++ 多态
前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:
3. 继承
子类继承父类,使子类拥有父类的数据和方法。
+单一继承
这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。
+多重继承
没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO。
4. 多态
多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。
+C++ 多态
前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:
执行如下代码:
1 | Base *ptr = new Derived(); |
编译器看到 ptr 是 Base 类型,如果 method 不是虚函数,那么执行的应该是 Base::method。现在 method 是虚函数,执行期调用 Base::method 还是 Derived::method,要看赋给 ptr 的是 Base 对象地址还是 Derived 对象地址。上面的代码是创建了一个 Derived 对象,并把地址传给 Base 类指针,但是内存模型中的 vptr 指向的仍然是 Derived 类实现的虚函数,所以最后调用的是 Derived::method。
-Python 多态
相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。
+Python 多态
相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。
1 | class Cat: |
这里 Cat 和 Dog 没有继承关系,say 也不是虚函数,调用 func(Cat)和 func(Dog)都能正确执行。要是再定义个 Person 类,只要定义了方法 say,就可以把 Person 传给 func 完成调用。
-5. 写在最后
虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:
+5. 写在最后
虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:
C++ 示例
1 |
|
Python 示例
1 | class Base: |
- 常用的各类 Linux 操作命令
-
+
-一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
+一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
CLI 是用户与内核打交道的工具。用户日常使用操作系统,最频繁的场景就是文件操作和网络访问。故而基础篇笔记主要记录 CLI 基本操作、文件系统、网络配置、权限管理等相关知识点。
-二、CLI 基本操作
(一)查看目录
+二、CLI 基本操作
(一)查看目录
- ls:显示指定目录下的文件及属性信息
- pwd:显示当前工作目录的路径
-(二)查看内容
+(二)查看内容
- echo:输出字符串或变量值(变量前加 $ 符号)
- cat:输出文件内容
- head:查看文件头部内容
@@ -264,17 +263,17 @@ (三)开关机
+(三)开关机
- shutdown:按指定方式关闭系统
- poweroff:关机
- reboot:重启
-(四)归档压缩
+(四)归档压缩
- zip:打包成 zip 文件
- unzip:zip 文件解包
- tar:压缩和解压缩文件
-(五)文件和目录操作
+(五)文件和目录操作
- locate:基于数据库快速查找文件和目录,手动更新数据库使用 updatedb 命令
- find:根据条件在指定目录下查找文件和目录
- cp:复制文件或目录
@@ -285,7 +284,7 @@ (六)进程操作
+(六)进程操作
- &:命令 + 空格 +&,会将任务置于后台执行
- jobs:查看后台任务
- fg:将后台任务放到前台执行
@@ -293,11 +292,11 @@ (七)命令帮助
+(七)命令帮助
- history:查看历史命令记录
- man:查看命令的帮助手册,一般情况下命令加 -h 或–help 够用了
-(八)文本编辑
+(八)文本编辑
- grep:搜索文本,可使用关键字或正则表达式
- sed:编辑文本,可使用正则表达式或脚本
- cut:基于列处理文件内容
@@ -332,7 +331,7 @@ (九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
+(九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
通过管道和重定向操作符,可以控制标准数据流的来源和去向:
@@ -380,7 +379,7 @@ echo ‘hello world’ | grep ‘hello’
-三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
+三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
graph TB
A["/"] --> B[bin] & C[boot] & D[dev] & E[etc] & F[home] & G[lib] & H[mnt] & I[opt] & J[proc] & K[root] & L[sbin] & M[tmp] & N[usr] & O[var]
N[usr] --> N1[bin] & N2[lib] & N3[local]
@@ -390,7 +389,7 @@
/: 根目录
/bin: 存放二进制可执行文件,root 用户和普通用户都能使用
/boot: 存放 linux 启动时的引导程序
/dev: linux 的设备也是以文件形式组织,存放在此目录下
/etc: 存放 linux 的系统管理和配置文件
/home: 存放普通用户的家目录,类似于 windows 下 C 盘 Users 用户文件夹
/lib: 存放程序运行依赖的库文件和内核驱动模块
/mnt: 设备挂载目录
/opt: 三方软件或大型软件的安装目录
/proc: 虚拟文件系统,记录系统实时信息,只存在内存中,不占用磁盘空间
/root: root 用户的家目录
/sbin: 存放二进制文件,仅 root 用户有权限使用
/tmp: 临时目录,重启系统会自动删除
/usr: 不是 user 缩写,是 unix software resource 缩写,存放系统上安装的软件资源
/var: 存放系统中经常变动的文件,如缓存 cache、日志 log、邮件 mail 等
-(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
+(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
+四、权限管理
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
linux 的用户分为 root 用户、系统用户和普通用户,每个用户用唯一的 uid 标识,root 用户的 uid 为 0,系统用户的 uid 通常在 1~999,普通用户的 uid 通常 >=1000。系统用户主要用于执行系统程序,无法使用 shell 登录。
为了便于对用户进行管理,每个用户还有对应的用户组,包括主组和附加组,主组只能有一个,附加组可以有多个。默认情况下,创建用户时,会将用户归属于同名的主组。
linux 上用户和用户组的信息都保存在 /etc 目录下,相关命令和文件说明如下:
@@ -416,9 +415,9 @@ /etc/shadow:用户密码信息(已加密)
/etc/group:用户组信息
-(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
+(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
linux 通过 UGO 模型对文件进行权限控制,其中 U(user)表示文件所属用户,G(group)表示文件所属用户组,O(other)表示既不是所属用户也不是所属用户组的其他群体。
-
+
通过 ls -l 查看文件权限如上图所示,第一列权限信息包含 10 位字符,第三列表示文件所属用户(属主),第四列表示文件所属用户组(属组)。权限信息中,第 1 个字符表示文件类型(- 表示文件,d 表示文件夹,l 表示符号链接),后面 9 个字符每 3 个一组,分别描述属主(user)权限、属组(group)权限、其他(other)权限。权限位是 rwx 表示具有相应权限,权限位是 - 表示没有相应权限。除了以字符的方式,每组权限还可以用 3 个 bit 位来表示,r 在高位,w 在中位,x 在低位,于是 rwx=b111=4+2+1=7。在修改权限时,可以使用任一种表达方式。
在创建文件或文件夹时,系统会设定默认的权限,通过原始权限减去权限掩码的方式,可以得到默认权限。文件夹的原始权限是 777(rwxrwxrwx),文件的原始权限是 666(rw-rw-rw-)。普通用户的默认掩码是 002,root 用户的默认掩码是 022。因此,对普通用户来说,新建文件夹的权限是 775(rwxrwxr-x),新建文件的权限是 664(rw-rw-r–)。
@@ -429,7 +428,7 @@ (三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
+(三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
@@ -461,11 +460,11 @@ 五、网络配置 (一)IP- 子网掩码 - 网关 -DNS
IP(v4) 编码用来标识互联网中的机器地址,它包含 32 个 bit 位,由网络地址 + 主机地址两部分组成。
+五、网络配置
(一)IP- 子网掩码 - 网关 -DNS
IP(v4) 编码用来标识互联网中的机器地址,它包含 32 个 bit 位,由网络地址 + 主机地址两部分组成。
子网掩码用来将 IP 地址切分为网络地址和主机地址,它也由 32 个 bit 位构成,且高位为连续的 1。子网掩码与 IP 地址成对出现,子网掩码与 IP 地址进行按位与运算可得网络地址。例如 IP 地址 192.168.1.1,子网掩码 255.255.255.0,可以算出网络地址为 192.168.1.0,可分配的主机地址包括 1~254(255 转换成 bit 位全为 1,全 1 的主机地址保留为广播地址),这一网段可以记为 192.168.1.0/24。同一网段的机器通信使用 ARP 协议,将 IP 地址解析为 MAC 地址,再基于 MAC 通信。
不同网络的主机之间进行通信需要使用网关,比如常见的路由器。网络 A 中的主机 A1 要跟网络 B 中的主机 B1 通信,主机 A1 需要先将数据包发送到 A 的网关,再由 A 的网关转发到 B 的网关,然后 B 的网关把数据包发送到主机 B1。现实中,跨域网络通信可能经过很多次路由器转发。
在互联网冲浪的时候,敲在地址栏里的是一串字符组成的域名。访问一个域名,实际上访问的也是互联网的一台主机。域名和 IP 的映射关系,通过 DNS 服务器进行解析,DNS 协议可以将域名解析为 IP 地址。
-(二)相关命令和配置文件
+(二)相关命令和配置文件
- ifconfig:查看和设置网络设备,重启后会失效,永久配置需要修改配置文件
- hostname:查看和设置系统的主机名
- ping:测试本机与目标主机之间的网络连通性
@@ -478,7 +477,6 @@
-
- Docker 必知必会 - 千里之行始于足下
+
+ Docker 必知必会 - 千里之行,始于足下
-
-
+
+
-
-
+
+
@@ -36,11 +36,11 @@
-
+
-
+
@@ -89,7 +89,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -260,12 +260,11 @@
- Docker 基本原理以及常用命令
-
+
-一、基本概念
Docker 是一种轻量级的虚拟化技术,可以让开发者非常便捷地实现应用程序的打包、移植、启动等操作,在软件开发、交付和部署中,有非常广泛的应用。
+一、基本概念
Docker 是一种轻量级的虚拟化技术,可以让开发者非常便捷地实现应用程序的打包、移植、启动等操作,在软件开发、交付和部署中,有非常广泛的应用。
Docker 容器与传统虚拟机的架构对比如下:
-
+
传统 VM 使用 Hypervisor 通过对物理主机的硬件资源进行协调和管理,为每个 GuestOS 分配独立的资源,让每个 GuestOS 成为一个虚拟主机,不同的 GuestOS 中的应用程序互不影响。Docker 容器直接运行在物理主机的 HostOS 上,共用物理主机的硬件资源,Container Engine 负责实现容器之间的资源隔离,让每个容器的应用独立地运行。
可以看出,容器比虚拟机少了一层 GuestOS,容器占用资源更少,启动更快,但隔离程度不如虚拟机。容器和虚拟机简要对比如下:
@@ -314,16 +313,16 @@
Docker 中有三个常见的名词:镜像、容器和仓库。这里先简单介绍下概念,知道是什么就行,后面再详细说明。
-(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
-(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
+(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
+(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
容器实质上是运行在宿主机上的进程,Docker 是用特殊的技术将容器与宿主机上的其他进程隔离开来,使得容器内的应用看起来是运行在一个独立的环境中。
-(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
-二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
+(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
+二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
- 容器内屏蔽容器外的情况,使用 Linux 的 Namespace 机制
- 容器拥有独立的资源,使用 Linux 的 Cgroups 机制
-(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
+(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
@@ -368,30 +367,30 @@ 隔离系统时间
-(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
+(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
查看 cgroup 挂载的目录,可以看到 cgroup 挂在 sys/fs/cgroup 节点,该路径下还有很多子目录(又称子系统),如 cpu、memory 等,每个子系统对应一种可以被限制的资源类型。
-
+
以 cpu 为例,查看 cpu 子系统。其中有两个参数 cfs_period_us 和 cfs_quota_us 通常组合使用,用于限制进程在长度为 cfs_period_us 的时间内,只能被分配到总量为 cfs_quota_us 的 cpu 时间。还有一个 tasks 文件,其中存放的是受限制的进程编号。
-
-
+
+
cpu 子系统中有个 docker 子目录,docker 目录中的文件与 cpu 目录中的文件一样。当我们拉起一个容器,比如运行 redis 镜像,可以看到 docker 目录中又多了一层以容器 id 为名称的子目录。
-
-
+
+
综上两点,容器其实是一个启用了多种 Namespace 的进程,它能够使用的资源量收到 Cgroups 的限制。截至目前,我们使用 Namespace 和 Cgroups 为容器开辟了一个独立的运行环境,接下来我们再剖析一下容器里运行的镜像。
前面提到,容器镜像是一个文件系统,我们运行 ubuntu 的镜像,可以通过命令行查看根目录内容,也可以创建并执行脚本文件:
-
+
我们还可以把改动之后的文件系统打包成我们自己的镜像,下次直接执行:
-
+
可以看到,我们以 ubuntu 文件系统为基础,加了一层我们自己的修改,打包了一个新的镜像。容器镜像实际上就是以 Linux 的联合文件系统机制(UnionFS)实现分层文件系统。
-(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
+(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
Docker 分层镜像的示例图如下,除了最顶层(称为容器层)是可写的,其他层(称为镜像层)都是只读的。UnoinFS 将所有层都叠加起来,形成最后我们在容器中所看到的文件系统。
-
+
由于镜像层都是只读的,所有对文件的修改操作都发生在容器层中,Docker 采用了 Copy-On-Write(写时复制) 的机制,具体来说:
@@ -401,15 +400,15 @@ (四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
-三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
+(四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
+三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
1
docker command --help
Docker 命令主要涉及到镜像、容器、仓库的操作和使用,下图可以概览全貌:
-
+
以下列举一些常用的 docker 命令,各命令详细用法可以通过 help 查看:
-1. 镜像相关
+1. 镜像相关
-2. 容器相关
+2. 容器相关
-(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
+(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
Dockerfile 是由一行行指令组成的脚本文件,每一行命令都会生成一个镜像层,多行命令生成多个镜像层,叠加生成最终的镜像文件。Dockerfile 的常见指令及含义如图所示:
-
+
最后看个 Dockerfile 的小例子:
编写 Dockerfile 文件
-
+
生成镜像文件
-
+
查看并运行镜像文件
-
+
-四、参考资料
+四、参考资料
-
diff --git "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html" "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
index 3df135e..40a3d52 100644
--- "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
+++ "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html"
@@ -5,28 +5,28 @@
-
- LibFuzzer 使用说明 - 千里之行始于足下
+
+ LibFuzzer 使用说明 - 千里之行,始于足下
-
-
+
+
-
-
+
+
-
+
-
+
@@ -74,7 +74,7 @@
href="/"
aria-label=""
>
- 千里之行始于足下
+ 千里之行,始于足下
@@ -245,8 +245,7 @@
- LibFuzzer 从 0 到 1,原理 + 安装 + 使用 + 优化,一篇讲完
-
+
按照 官方定义,LibFuzzer 是一个in-process(进程内的),coverage-guided(以覆盖率为引导的),evolutionary(进化的) 的 fuzz 引擎,是 LLVM 项目的一部分,主要用于对 C/C++ 程序进行 Fuzz 测试。LibFuzzer 三个特性的具体含义为:
@@ -264,13 +263,13 @@
LibFuzzer 与待测的 library 进行链接,通过向指定的 fuzzing 入口(即target 函数)发送测试数据,并跟踪被触达的代码区域,然后对输入的数据进行变异,以达到代码覆盖率最大的目的,其中代码覆盖率的信息由 LLVM 的 SanitizerCoverage 工具提供。
-一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
+一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
1
apt install clang
安装完毕之后,可以通过如下命令查看 clang 版本:
1
clang --version
-2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
+2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Values other than 0 and -1 are reserved for future use.
}
target 函数的名称、参数类型、返回值类型都不能改变。此外,官方文档中还有如下说明:
@@ -336,11 +335,11 @@ libFuzzer 编译链接。
关于 LibFuzzer 通过插桩统计代码覆盖率的具体实现,有兴趣可以参考Coverage Control in libFuzzer。
-3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
+3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
1
./fuzz-target -flag1=val1 -flag2=val2 ... path1 path2 ...
在不限制运行时长的情况下,Fuzz 将会一直执行下去,直到出现 crash(通常是因为触发 sanitizer 异常)才会终止。导致 crash 的输入将会作为能触发 bug 的 testcase 保存到磁盘上,并根据 crash 的类型,用不同的文件名前缀进行区分,比如 crash-XXX,leak-XXX,timeout-XXX 等。
-(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
+(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
常用的各类 Linux 操作命令
- + -一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
+一、引言
操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。
CLI 是用户与内核打交道的工具。用户日常使用操作系统,最频繁的场景就是文件操作和网络访问。故而基础篇笔记主要记录 CLI 基本操作、文件系统、网络配置、权限管理等相关知识点。
-二、CLI 基本操作
(一)查看目录
-
+
- ls:显示指定目录下的文件及属性信息
- pwd:显示当前工作目录的路径
- echo:输出字符串或变量值(变量前加 $ 符号)
- cat:输出文件内容
- head:查看文件头部内容 @@ -264,17 +263,17 @@
- shutdown:按指定方式关闭系统
- poweroff:关机
- reboot:重启
- zip:打包成 zip 文件
- unzip:zip 文件解包
- tar:压缩和解压缩文件
- locate:基于数据库快速查找文件和目录,手动更新数据库使用 updatedb 命令
- find:根据条件在指定目录下查找文件和目录
- cp:复制文件或目录 @@ -285,7 +284,7 @@
- &:命令 + 空格 +&,会将任务置于后台执行
- jobs:查看后台任务
- fg:将后台任务放到前台执行 @@ -293,11 +292,11 @@
- history:查看历史命令记录
- man:查看命令的帮助手册,一般情况下命令加 -h 或–help 够用了
- grep:搜索文本,可使用关键字或正则表达式
- sed:编辑文本,可使用正则表达式或脚本
- cut:基于列处理文件内容 @@ -332,7 +331,7 @@
二、CLI 基本操作
(一)查看目录
(二)查看内容
-
+
(二)查看内容
(三)开关机
-
+
(三)开关机
(四)归档压缩
-
+
(四)归档压缩
(五)文件和目录操作
-
+
(五)文件和目录操作
(六)进程操作
-
+
(六)进程操作
(七)命令帮助
-
+
(七)命令帮助
(八)文本编辑
-
+
(八)文本编辑
(九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
+(九)管道和重定向
linux 的标准数据流包括输入流 stdin(0)、输出流 stdout(1)、错误流 stderr(2),默认情况下 stdin 接受键盘输入,stdout 和 stderr 将结果和错误输出到命令行终端。
通过管道和重定向操作符,可以控制标准数据流的来源和去向:
三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
+三、文件系统
(一)linux 文件目录结构
linux 下一切皆文件(普通文件、目录文件、设备文件、管道文件……),linux 文件系统不区分磁盘(windows 通常分 C 盘 D 盘,cmd 切换跨盘目录时,需要先切换盘符),整个 linux 系统目录是以 / 为根节点的单根树结构,遵循 FHS 标准规范:
graph TB A["/"] --> B[bin] & C[boot] & D[dev] & E[etc] & F[home] & G[lib] & H[mnt] & I[opt] & J[proc] & K[root] & L[sbin] & M[tmp] & N[usr] & O[var] N[usr] --> N1[bin] & N2[lib] & N3[local] @@ -390,7 +389,7 @@
/: 根目录
-
/bin: 存放二进制可执行文件,root 用户和普通用户都能使用
/boot: 存放 linux 启动时的引导程序
/dev: linux 的设备也是以文件形式组织,存放在此目录下
/etc: 存放 linux 的系统管理和配置文件
/home: 存放普通用户的家目录,类似于 windows 下 C 盘 Users 用户文件夹
/lib: 存放程序运行依赖的库文件和内核驱动模块
/mnt: 设备挂载目录
/opt: 三方软件或大型软件的安装目录
/proc: 虚拟文件系统,记录系统实时信息,只存在内存中,不占用磁盘空间
/root: root 用户的家目录
/sbin: 存放二进制文件,仅 root 用户有权限使用
/tmp: 临时目录,重启系统会自动删除
/usr: 不是 user 缩写,是 unix software resource 缩写,存放系统上安装的软件资源
/var: 存放系统中经常变动的文件,如缓存 cache、日志 log、邮件 mail 等(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
+(二)分区 - 格式化 - 挂载
linux 添加一个新的磁盘设备时,由于 linux 一切皆文件,需要将磁盘格式化为指定的文件系统,然后挂载到 linux 的系统目录中才能使用。相关的常用命令如下:
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
+四、权限管理
(一)用户和组
linux 是服务器的主流操作系统,服务器通常由很多人同时使用。为不同的用户分配各自的操作权限,对维护系统的安全和稳定来说是十分必要的。
linux 的用户分为 root 用户、系统用户和普通用户,每个用户用唯一的 uid 标识,root 用户的 uid 为 0,系统用户的 uid 通常在 1~999,普通用户的 uid 通常 >=1000。系统用户主要用于执行系统程序,无法使用 shell 登录。
为了便于对用户进行管理,每个用户还有对应的用户组,包括主组和附加组,主组只能有一个,附加组可以有多个。默认情况下,创建用户时,会将用户归属于同名的主组。
linux 上用户和用户组的信息都保存在 /etc 目录下,相关命令和文件说明如下:
@@ -416,9 +415,9 @@/etc/shadow:用户密码信息(已加密)
/etc/group:用户组信息 -(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
+(二)普通权限
linux 定义的普通权限有 r(读)w(写)x(执行)三种,对文件来说很容易理解,对文件夹来说,要浏览目录需要同时开启 r 和 x 权限,x 表示可以通过 cd 进入目录。
linux 通过 UGO 模型对文件进行权限控制,其中 U(user)表示文件所属用户,G(group)表示文件所属用户组,O(other)表示既不是所属用户也不是所属用户组的其他群体。
- +通过 ls -l 查看文件权限如上图所示,第一列权限信息包含 10 位字符,第三列表示文件所属用户(属主),第四列表示文件所属用户组(属组)。权限信息中,第 1 个字符表示文件类型(- 表示文件,d 表示文件夹,l 表示符号链接),后面 9 个字符每 3 个一组,分别描述属主(user)权限、属组(group)权限、其他(other)权限。权限位是 rwx 表示具有相应权限,权限位是 - 表示没有相应权限。除了以字符的方式,每组权限还可以用 3 个 bit 位来表示,r 在高位,w 在中位,x 在低位,于是 rwx=b111=4+2+1=7。在修改权限时,可以使用任一种表达方式。
在创建文件或文件夹时,系统会设定默认的权限,通过原始权限减去权限掩码的方式,可以得到默认权限。文件夹的原始权限是 777(rwxrwxrwx),文件的原始权限是 666(rw-rw-rw-)。普通用户的默认掩码是 002,root 用户的默认掩码是 022。因此,对普通用户来说,新建文件夹的权限是 775(rwxrwxr-x),新建文件的权限是 664(rw-rw-r–)。
@@ -429,7 +428,7 @@(三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
+(三)特殊权限
除了 rwx 三种普通权限外,还有三种特殊权限:
Docker 中有三个常见的名词:镜像、容器和仓库。这里先简单介绍下概念,知道是什么就行,后面再详细说明。
-(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
-(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
+(一)镜像(Image)
镜像是一个特殊的文件系统,提供容器运行时所需的环境和配置,例如程序、库、资源、配置等文件,以及环境变量、匿名卷、用户等配置参数。镜像是静态的,不包含任何动态数据,在镜像构建之后,其内容不会发生改变。
+(二)容器(Container)
容器和镜像的关系,类似于面向对象编程中对象和类的关系,容器是运行镜像后得到的实例,运行镜像就相当于类的实例化,多次运行镜像,可以得到多个容器。容器是动态的,可以对容器进行创建、删除、启动、停止、暂停等操作。
容器实质上是运行在宿主机上的进程,Docker 是用特殊的技术将容器与宿主机上的其他进程隔离开来,使得容器内的应用看起来是运行在一个独立的环境中。
-(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
-二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
+(三)仓库(Repository)
仓库类似 github,对镜像进行存储和分发。在任一宿主机上,都可以从仓库拉取指定镜像,也可以把自己打包好的镜像上传到仓库,供他人访问。默认的是官方仓库 Docker Hub,拥有众多官方镜像,国内访问需要配加速器,如阿里云的镜像仓。也可以自行搭建本地私有镜像仓。
+二、基础原理
前面提到,容器是宿主机用特殊机制隔离出来的进程。为了实现容器进程的互不干扰,这个机制需要解决两个基本问题:
- 容器内屏蔽容器外的情况,使用 Linux 的 Namespace 机制
- 容器拥有独立的资源,使用 Linux 的 Cgroups 机制
(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
+(一)Namespace
顾名思义,Namespace 就是命名空间。C++ 使用命名空间解决了类型、变量和函数的冲突问题。Docker 容器也具有自己的命名空间,通过命名空间对资源对象进行隔离,使得不同的容器进程号、用户、文件目录等相互屏蔽。Linux 支持的命名空间有以下几种:
(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
+(二)Cgroups
Linux Cgroups 的全称是 Linux Control Group,主要用于对共享资源进行隔离、限制、审计。通过 Cgroups 限制容器能够使用的资源上限,包括 cpu、内存、磁盘、网络带宽等,可以避免多个容器之间的资源竞争。Linux 一切皆文件,Cgroups 也是通过树状的文件系统来对资源进行限制。
查看 cgroup 挂载的目录,可以看到 cgroup 挂在 sys/fs/cgroup 节点,该路径下还有很多子目录(又称子系统),如 cpu、memory 等,每个子系统对应一种可以被限制的资源类型。
- +以 cpu 为例,查看 cpu 子系统。其中有两个参数 cfs_period_us 和 cfs_quota_us 通常组合使用,用于限制进程在长度为 cfs_period_us 的时间内,只能被分配到总量为 cfs_quota_us 的 cpu 时间。还有一个 tasks 文件,其中存放的是受限制的进程编号。
- - + +cpu 子系统中有个 docker 子目录,docker 目录中的文件与 cpu 目录中的文件一样。当我们拉起一个容器,比如运行 redis 镜像,可以看到 docker 目录中又多了一层以容器 id 为名称的子目录。
- -
+
+
综上两点,容器其实是一个启用了多种 Namespace 的进程,它能够使用的资源量收到 Cgroups 的限制。截至目前,我们使用 Namespace 和 Cgroups 为容器开辟了一个独立的运行环境,接下来我们再剖析一下容器里运行的镜像。
前面提到,容器镜像是一个文件系统,我们运行 ubuntu 的镜像,可以通过命令行查看根目录内容,也可以创建并执行脚本文件:
- +我们还可以把改动之后的文件系统打包成我们自己的镜像,下次直接执行:
- +可以看到,我们以 ubuntu 文件系统为基础,加了一层我们自己的修改,打包了一个新的镜像。容器镜像实际上就是以 Linux 的联合文件系统机制(UnionFS)实现分层文件系统。
-(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
+(三)UnionFS
UnionFS(联合文件系统),可以将不同目录挂载到同一个虚拟文件系统下。Docker 利用 UnionFS 的功能,在基础文件系统上以增量的方式,叠加一层又一层新的文件系统,通过联合挂载,最终得到一个完整的 Docker 镜像文件系统。
Docker 分层镜像的示例图如下,除了最顶层(称为容器层)是可写的,其他层(称为镜像层)都是只读的。UnoinFS 将所有层都叠加起来,形成最后我们在容器中所看到的文件系统。
- +由于镜像层都是只读的,所有对文件的修改操作都发生在容器层中,Docker 采用了 Copy-On-Write(写时复制) 的机制,具体来说:
-
@@ -401,15 +400,15 @@
编写 Dockerfile 文件
- +生成镜像文件
- +查看并运行镜像文件
-
+
(四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
-三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
+(四)Volumes
由前所述,启动容器后,在容器内执行的所有文件的修改,都保存在容器层中,在删除容器时,对应的修改也会被删除。当我们需要保存修改的结果时,就需要用到数据卷(Data Volume),换句话说,数据卷提供了一种数据持久化的方式,使得在容器中产生的修改能永久的保留到宿主机上。数据卷的使用也很简单,在启动容器时,将宿主机的目录映射到容器中即可。
+三、使用方法
(一)命令行
与 Linux 一样,Docker 主要通过命令行操作,Docker 提供了非常多的命令,可以通过如下命令查看说明文档:
1 | docker command --help |
Docker 命令主要涉及到镜像、容器、仓库的操作和使用,下图可以概览全貌:
- +以下列举一些常用的 docker 命令,各命令详细用法可以通过 help 查看:
-1. 镜像相关
-
+
1. 镜像相关
-2. 容器相关
-
+
2. 容器相关
-(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
+(二)镜像制作
制作 Docker 镜像有两种方法,一种是在容器中使用 commit 提交修改,另一种是使用 Dockfile 进行 build,后一种是主流做法。
Dockerfile 是由一行行指令组成的脚本文件,每一行命令都会生成一个镜像层,多行命令生成多个镜像层,叠加生成最终的镜像文件。Dockerfile 的常见指令及含义如图所示:
- +最后看个 Dockerfile 的小例子:
四、参考资料
-
+
四、参考资料
- diff --git "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html" "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html" index 3df135e..40a3d52 100644 --- "a/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html" +++ "b/2023/09/17/LibFuzzer\344\275\277\347\224\250\350\257\264\346\230\216/index.html" @@ -5,28 +5,28 @@ - -
- LibFuzzer 从 0 到 1,原理 + 安装 + 使用 + 优化,一篇讲完
-
+
按照 官方定义,LibFuzzer 是一个in-process(进程内的),coverage-guided(以覆盖率为引导的),evolutionary(进化的) 的 fuzz 引擎,是 LLVM 项目的一部分,主要用于对 C/C++ 程序进行 Fuzz 测试。LibFuzzer 三个特性的具体含义为:
@@ -264,13 +263,13 @@
LibFuzzer 与待测的 library 进行链接,通过向指定的 fuzzing 入口(即target 函数)发送测试数据,并跟踪被触达的代码区域,然后对输入的数据进行变异,以达到代码覆盖率最大的目的,其中代码覆盖率的信息由 LLVM 的 SanitizerCoverage 工具提供。
-一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
+一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
1
apt install clang
安装完毕之后,可以通过如下命令查看 clang 版本:
1
clang --version
-2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
+2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Values other than 0 and -1 are reserved for future use.
}
target 函数的名称、参数类型、返回值类型都不能改变。此外,官方文档中还有如下说明:
@@ -336,11 +335,11 @@ libFuzzer 编译链接。
关于 LibFuzzer 通过插桩统计代码覆盖率的具体实现,有兴趣可以参考Coverage Control in libFuzzer。
-3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
+3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
1
./fuzz-target -flag1=val1 -flag2=val2 ... path1 path2 ...
在不限制运行时长的情况下,Fuzz 将会一直执行下去,直到出现 crash(通常是因为触发 sanitizer 异常)才会终止。导致 crash 的输入将会作为能触发 bug 的 testcase 保存到磁盘上,并根据 crash 的类型,用不同的文件名前缀进行区分,比如 crash-XXX,leak-XXX,timeout-XXX 等。
-(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
+(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
LibFuzzer 从 0 到 1,原理 + 安装 + 使用 + 优化,一篇讲完
- +按照 官方定义,LibFuzzer 是一个in-process(进程内的),coverage-guided(以覆盖率为引导的),evolutionary(进化的) 的 fuzz 引擎,是 LLVM 项目的一部分,主要用于对 C/C++ 程序进行 Fuzz 测试。LibFuzzer 三个特性的具体含义为:
-
@@ -264,13 +263,13 @@
LibFuzzer 与待测的 library 进行链接,通过向指定的 fuzzing 入口(即target 函数)发送测试数据,并跟踪被触达的代码区域,然后对输入的数据进行变异,以达到代码覆盖率最大的目的,其中代码覆盖率的信息由 LLVM 的 SanitizerCoverage 工具提供。
-一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
+一、使用方法
1. 安装环境
Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:
1 | apt install clang |
安装完毕之后,可以通过如下命令查看 clang 版本:
1 | clang --version |
2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
+2. 构建 target
使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:
1 | // fuzz_target.cc |
target 函数的名称、参数类型、返回值类型都不能改变。此外,官方文档中还有如下说明:
@@ -336,11 +335,11 @@libFuzzer 编译链接。
关于 LibFuzzer 通过插桩统计代码覆盖率的具体实现,有兴趣可以参考Coverage Control in libFuzzer。
-3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
+3. 执行 fuzz
在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:
1 | ./fuzz-target -flag1=val1 -flag2=val2 ... path1 path2 ... |
在不限制运行时长的情况下,Fuzz 将会一直执行下去,直到出现 crash(通常是因为触发 sanitizer 异常)才会终止。导致 crash 的输入将会作为能触发 bug 的 testcase 保存到磁盘上,并根据 crash 的类型,用不同的文件名前缀进行区分,比如 crash-XXX,leak-XXX,timeout-XXX 等。
-(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:
+(1) 参数说明
常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式: