Table of Contents

1 收藏夹

1.2 博客

  1. google test
  2. cmake
  3. 强类型语言与弱类型语言,静态类型与动态语言
  4. _attribute_((packed))详解
  5. c++的NRV(named return value)优化问题

1.2.2 网络

  1. 如何正确关闭tcp连接
  2. TODO TODO TODO TODO TODO TODO 网络底层知识总结 http://www.52im.net/thread-3247-1-1.html

1.2.3 分布式

  1. protobuf

2 emacs/spacemacs

2.1 用法

2.2 配置

2.2.1 值得推荐的工具:

2.2.2 c-c++ 补全配置:

  1. TODO 1. 配置lsp

    lsp在master分支上没有,只在develop分支上有,具体怎么配置还不熟悉

    dotspacemacs-configuration-layers
    '(...
      lsp
      ...)
    
    1. 安装ccls

    https://github.com/MaskRay/ccls/wiki/Build

    brew update
    brew install ccls
    

2.2.3 使得能在org中执行c,c++代码

2.3 Spacemacs Rocks第二季

2.3.2 第一天:准备开始

  1. 交换ctrl键和caps lock键
  2. 基本操作

    ;; 移动 C-f 为前移一个字符, f 代表 forward。 C-b 为后移一个字符, b 代表 backward。 C-p 为上移至前一行, p 代表 previous。 C-n 为上移至下一行, n 代表 next。 C-a 为移至行首, a 代表 ahead。 C-e 为移至行尾, e 代表 end。

    ;; 文件操作 C-x C-f 为打开目标文件, f 代表 find/file C-x C-s 为保存当前缓冲区(Buffer), s 代表 save ;; 帮助信息 C-h k 寻找快捷键的帮助信息 C-h v 寻找变量的帮助信息 C-h f 寻找函数的帮助信息 ;; 创建链接 C-c C-l 执行代码 C-x C-e(执行时光标必须处于要执行函数/代码的末尾) 如下面的lisp代码: (+ 2 2)[执行前光标必须位于这个位置] ;; mode C-h m 显示当前buffer的所有major mode和minor mode ;; gtd C-c C-t(odo) 当前标签变为TODO,再按一次变为DONE,再按一次取消

2.3.3 第二天:高级自定义

  1. 70:45分开始:org-mode Agenda的使用

    ;; 设置默认 Org Agenda 文件目录 (setq org-agenda-files '("~/org"))

    你只需将你的 *.org 文件放入上面所指定的文件夹中就可以开始使用 Agenda 模式了。

    C-c C-s 选择想要开始的时间 C-c C-d 选择想要结束的时间 C-c a 可以打开 Agenda 模式菜单并选择不同的可视方式( r )(需要保存org文件才会更新agenda)

2.3.4 第十一天: Spacemacs 简介及安装

SPC s s 搜索 SPC h SPC 弹出一个信息窗口, 可以从窗口中选择具体的 layer 或者其他信息进行查看.

  1. 换monokai主题(46:00)

    主题预览:https://themegallery.robdor.com 出现的问题:https://emacs-china.org/t/spacemacs/885 , 解决方法:在addition-packets中加上monokai-theme

    dotspacemacs-additional-packages '(youdao-dictionary monokai-theme)
    
  2. 插入结构块

2.3.5 第十二天: 创建你的第一个 Spacemacs Layer

  1. 更换状态栏样式(09:27)
    ;; (setq powerline-default-separator 'alternate)
    ;; 更换状态栏格式
    (setq powerline-default-separator 'bar)
    

2.3.6 agenda 使用

3 reading

3.1 c++ primer(5th) pdf

3.1.1 初始化问题

  1. 类内初始化

    c++11标准规定,可以为数据成员提供类内初始值,这个初始值可以放在花括号(列表初始化)里,也可以 放在等于号右边,但是 不能使用圆括号

  2. 初始化要注意的地方
    1. 使用拷贝初始化时,只能提供一个初始值
    2. 类内初始值只能由拷贝初始化或使用花括号初始化
    3. 如果以提供初始元素值列表的形式初始化,只能用花括号,不能使用圆括号
    4. 如果初始化时使用了花括号形式但是不能用于列表初始化,那就只能考虑用这些值来匹配构造函数 也就是说花括号形式不等于列表初始化,如:

      vector<string> v{10, "hi"};    //v有10个值为“hi”的元素
      

3.1.2 左值引用与右值引用

  • 左值引用 46页 引用类型一般必须与其所引用对象的类型一致,但是有两个例外:
    1. 允许用任意表达式初始化常量引用(非常量对象,字面值,一般表达式)

      2020-11-20_21-26-23_screenshot.png

    2. TODO
  • 右值引用 471页

3.1.3 extern

如果要在多个文件中使用同一个函数或变量,则定义只能出现在一个文件中:

  声明 定义
变量 extern int a; [extern] int a = 1;
函数 extern void foo(); extern void foo() {}
const 变量 extern const int a; extern const int a = 1;
     

3.1.4 引用与指针

考虑代码:

class A {
public:
  int aa;
};

int main() {
  auto a = new A();
  auto &b = *a;  
  auto *c = a;
  return a->aa;
}

汇编代码:

main:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $32, %rsp
        movl    $4, %edi
        call    operator new(unsigned long)
        movl    $0, (%rax)
        movq    %rax, -8(%rbp)
        movq    -8(%rbp), %rax
        movq    %rax, -16(%rbp)
        movq    -8(%rbp), %rax
        movq    %rax, -24(%rbp)
        movq    -8(%rbp), %rax
        movl    (%rax), %eax
        leave
        ret

可以看到,引用b和指针c的汇编代码是一样的,所以引用和指针的底层实现并没有什么区别, 只是编译器对引用多了很多检查: 1.必须指定初始化值 2.初始化后无法改变 我的理解是这样可以更早暴露代码中的问题。

3.1.5 const注意点页

3.1.6 constexpr(58页)

  • 常量表达式是指值不会改变并且在编译过程就能计算出结果的表达式
  • 一个变量是不是常量表达式由他的类型和初始值共同决定
  • c++11标准允许将变量声明为constexpr类型以便编译器检查变量是否为一个常量表达式
  • 只有字面值类型(算术类型,引用和指针, TODO)能声明为constexpr类型
  • 一个constexpr指针的初始值必须是nullptr或0,或者是存储于固定地址的对象
  • constexpr只对指针有效,对指针所指对象无效 如:constexpr int *q = nullptr //q是指向整数的常量指针

3.1.7 auto类型说明符(61页)

  • 使用引用其实是使用引用的对象,特别是当引用被用作初始值时,编译器以引用对象的类型作为auto的类型:

    int i = 0, &r = i;
    auto a = r; //a的类型是int, 而不是int &
    
  • 一般auto会忽略掉顶层const,保留下底层const

    const int ci = i, &cr = ci;
    auto c = cr;  // c是一个整数(不带顶层const)
    auto e = &ci  // e是一个指向整数常量的指针(保留底层const)
    
  • 如果希望推断出的auto类型是一个顶层const,需要明确指出

    const auto f = ci;  // 和c不同, f是const int
    

3.1.8 decltype类型说明符(62页)

  • c++11标准引入了第二种类型说明符,他的作用使编译器分析表达式并是返回表达式的类型, 但不实际计算表达式的值, 需要指出的是,引用从来都作为其所指对象的同义词出现,只有 用在decltype处是一个 例外
  • decltype与auto的区别
    1. decltype保留顶层const和引用,auto则不保留
    2. decltype与表达式形式密切相关, decltype((variable))的结果永远是引用, 而decltype(*p)也是引用类型

3.1.9 内置数组与vector的区别

  1. 内直数组不能拷贝和赋值

    int a[] = {0 , 1, 2};
    int a2[] = a;  // 错误:不允许使用一个数组初始化另一个
    a2 = a;        // 错误:不允许使用一个数组赋值给另一个
    
  2. 内置的下标运算符所用的索引值不是无符号类型,而vector是

    int ia[] = {0, 2, 4 ,6 , 8};
    int *p = &ia[2];
    int k = p[-2];   //可以用负数作为下标
    

等价于*(it++) , 输出当前值并将it向前移动一个元素

3.1.10 sizeof运算符(139页)

#include <vector>
#include <iostream>
using namespace std;
int main() {
  vector<int> v(100, 0);
  cout << sizeof(v) << endl;
  return 0;
}

3.1.11 隐式类型转换 (141页)

2020-11-21_11-00-05_screenshot.png

3.1.12 运算符优先级表(147页)

3.1.13 函数重载(208页)

同一作用域 函数名相同但形参列表不同(类型或个数或顺序)

  1. 几种视为相同的函数声明

    void A(int &) void A(int &i)

    typedef phone telno int lookup(phone&) int lookup(telno&) 3.顶层const int lookup(phone) int lookup(const phone)

3.1.14 值初始化(88页)和默认初始化

3.1.15 类的静态成员

  • 类的静态成员函数不能声明成const的,因为不带this指针
  • 不能在类内部初始化静态成员,但可以初始化 const static
  • 静态数据成员可以是类本身,而飞静态数据成员则只能声明成它所属类的指针或引用

3.1.16 智能指针

  1. sharedptr初始化
    • 如果不初始化一个智能指针,它就会被初始化为一个空指针
    • 接受指针参数的智能指针的构造函数是explicit的,所以我们不能将一个内置指针隐式转换为一个智能指针
  2. 注意点(陷阱)
    • 不要混合使用普通指针和智能指针
    • 不要用get初始化另一个智能指针或为智能指针赋值
    • 不使用相同的内置指针初始化(或reset)多个智能指针
    • 不delete get()返回的指针
    • 当智能指针销毁后,get()返回的该智能指针的普通指针就无效了
    • 如果智能指针管理的资源不是new分配的内存,记住传递一个自定义的删除器

3.1.17 TODO allocator

3.1.18 13章 拷贝控制

  1. 拷贝构造函数
    • 和默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数
    • 拷贝构造函数的第一个参数必须是一个引用类型,而且这个参数几乎总是const的
    • 拷贝构造函数通常不应该是explicit的,因为它会被隐式使用
    • 每个成员的类型决定了它如何拷贝,对类类型的成员会使用其拷贝构造函数,内置类型直接拷贝
  2. 拷贝初始化和直接初始化
    • 拷贝初始化使用等号,直接初始化不使用等号
    • 直接初始化实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数, 拷贝初始化要求编译器将=右侧的运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型 转换
  3. 拷贝构造函数和拷贝赋值运算符怎么区别
    • 拷贝构造函数是在初始化时调用,而赋值运算符是在初始化后的赋值时调用

      // 拷贝构造函数
      string s = "emacs";
      
      // 拷贝赋值运算符
      string s;
      s = "emacs";
      
  4. 析构函数
    • 认识到析构函数自身并不直接销毁成员是非常重要的,成员是在析构函数体之后隐含的析构阶段中 被销毁的
  5. 三五法则
    1. 需要析构函数的类也需要拷贝和赋值操作

      因为需要析构函数说明需要自定义资源管理,那么拷贝和赋值也都需要,比如析构函数销毁指针所指 资源,如果用合成的拷贝和赋值操作的话,会使两个对象的该指针指向同一地址,引发错误

    2. 需要拷贝操作的类也需要赋值操作,反之亦然

      作为例子,考虑一个类为每个对象分配一个唯一的序号,这个时候就需要自定义拷贝和赋值操作而 不能用合成的,拷贝和赋值实现的效果其实是相同的,所以一个需要自定义的话,另一个也需要, 但是上面这个例子其实不需要自定义析构函数,因为没有资源需要管理。

    3. 一般来说,如果一个类定义了任何一个拷贝操作,他就应该定义所有五个操作

      因为如果必须自定义拷贝构造函数,拷贝赋值运算符和析构函数,那么这个类通常会涉及资源的管理问题, 而拷贝成员必须拷贝这些资源。这会导致额外的开销,所以一般要定义移动构造函数和移动赋值运算符来 避免这种开销

  6. 移动构造函数
    • 除了完成资源的移动,移动构造函数还必须保证源对象处于析构安全状态,也就是说析构它是无害的, 而且也必须是可以被析构的(还是存在的,可以进行操作)
    • 由于移动操作通常不分配任何资源,所以通常不抛出任何异常
    • 只有当一个类没有定义任何直接版本的拷贝控制成员,且类的每个非static数据成员都可以 移动时,编译器才会为它合成移动构造函数或移动赋值运算符
    • 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员 默认地被定义为删除的
  7. TODO 移动迭代器
  8. 右值引用和成员函数

    区分移动和拷贝的重载函数通常有一个版本接受一个const T&, 而另一个版本接受一个T&&, 因为一般 来说,当我们希望移动数据时实参不能是const的,因为这样才能保证移动源可以被改变成析构安全的。

  9. TODO 函数重载与const, &, &&

3.1.19 14章 重载运算和类型转换

  1. 重载运算符
    • 重载运算符实质是具有特殊名字的函数,由关键字operator和要定义的运算符共同组成。一元运算符有一个 参数,二元运算符有两个参数:运算符左侧对象传递给第一个参数,右侧传给第二个。
    • 除了operator()之外,其他重载运算符都不能有默认实参
    • 一个运算符函数,要么是类的成员,要么至少含有一个类类型的参数(因为内置类型的运算符已经预定了)
    • 当一个重载运算符是成员函数时,this绑定到左侧运算对象,所以显式参数比运算对象少一个
    • 不能被重载的运算符(:: .* . ?:)
    • 通常情况下不应该被重载的运算符(, & && ||), 因为重载本质是函数,无法保留求值顺序/短路求值属性
    1. 重载运算符是否定义为成员函数的准则:

      2020-11-06_11-06-01_screenshot.png

      • 输入运算符必须处理输入可能失败的情况,而输出符不需要(496页)
      • 如果定义了算术运算符和相应的复合赋值运算符,通常应使用后者实现前者
      • 赋值运算符必须是类的成员,复合赋值运算符通常情况下(难道不是必须吗?TODO,不是成员的话 不能访问类并对它赋值吧?)也是如此(500页)
      • 下标运算符必须是成员函数,下标运算符通常需要两个版本:一个返回普通引用,一个返回const引用
      • 递增递减运算符应该同时定义前置和后置版本,且通常被定义为类的成员,前置返回引用,后置返回值。 后置版本需要接受一个额外的不被使用的int形参来和前置区分
      • ->运算符必须是类的成员,解引用运算符通常也是类的成员
  2. 函数调用运算符
    • 函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,它们应该在参数数量或 类型上有所区别
    • 定义了调用运算符的类叫函数对象,这些对象的行为就像函数一样,函数对象常常作为泛型算法的实参
  3. TODO lambda(508页)
  4. 可调用对象与function

    c++有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的 类。他们的调用形式可以是相同的,但是却有着不同的类型,所以在很多地方受到约束。比如下面的例子, 我们想定义一个简单的计算器:

    int add(int i, int j) {return i+j;}
    auto mod = [](int i, int j) {return i%j;}
    
    map<string, int(*)(int,int)> binops;
    // 正确
    binops.insert({"+", add});
    //错误, mod不是函数指针
    binops.insert({"%", mod});
    

    c++11新标准库function类型解决了这个问题:

    map<string, function<int(int,int)>> binops;
    
    binops.insert("+", add);
    binops.insert("%", mod);
    

    当然,如果add函数被重载的话,上面的方法就行不通了,但是可以用函数指针来解决

  5. 重载,类型转换与运算符
    1. 类型转换运算符

      转换构造函数和类型转换运算符共同定义了类类型转换,与标准类型转换相对,这种转换也叫用户 定义的类型转换。 类型转换函数的一般形式为:

      operator type() const
      

      一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常 应该是const的,因为它不应该改变待转换对象的内容。 为了防止隐式类型转换引发意想不到的结果,c++11标准引入了显示的类型转换运算符:

      explicit operator int() const {};
      

      和显示构造函数一样,编译器不会将一个显式的类型转换运算符用于隐式类型转换,除了下边这个例外:

      2020-11-07_10-58-13_screenshot.png 正因为这个例外,operator bool一般定义为explicit的

    2. TODO 类型转换的二义性

      通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是 算术类型的转换

    3. TODO 函数匹配与重载运算符

3.1.20 15章 面向对象程序设计

  • 面向对象设计将类通过继承联系在一起构成一种层次关系,其中在层次关系根部的叫基类,其他类 则直接或间接从基类继承而来,这些继承的类称为派生类。
  • 派生类通过类派生列表来指出它继承自哪些类,派生列表形式为:
class A : public B, protected C, private D {

};
  • 基类如果希望派生类重新定义(覆盖)其成语函数,必须用virtual将该函数声明为虚函数,如果派生类 想要对这个函数重新定义自己的版本的话,派生类必须在其内部对该虚函数进行声明,否则,派生类将 直接继承该基类对该函数的定义(隐式保留虚函数属性)。
  • 虚函数和普通函数的最大区别是:当我们使用指针或引用调用虚函数时,虚函数需要在运行时才能选择函数的 版本,这种行为也叫动态绑定。如果成语函数不是虚函数,或者调用虚函数时使用的不是指针或引用,那么 函数的类型绑定将在编译器完成。
  • 所有虚函数都都必须有定义,不管我们会不会使用这个虚函数,这点也和普通函数不同,这是因为编译器 也不知道到底会使用哪个函数
  1. 类型转换与继承
    • 静态类型和动态类型 静态类型:声明时指定的类型,编译时确定 动态类型:内存中实际对象的类型,运行时才能确定 如果一个表达式不是引用也不是指针,那它的静态和动态类型永远一致 引用或指针的静态类型与动态类型不同这一事实正是c++语言支持多态性的根本所在
    • 智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基 类的智能指针内
    • 要理解在具有继承关系的类之间的类型转换,三点非常重要:

    2020-11-07_14-41-13_screenshot.png

  2. 虚函数
    • 覆盖
      1. 一个派生类的函数要覆盖某个继承来的虚函数,那么它的形参类型必须与被它覆盖的基类函数 完全一致
      2. c++11可以使用override关键字来说明派生类的虚函数,这使得程序员的意图更加清晰。因为 如果程序员本来想覆盖父类虚函数但是参数写错了,就会导致程序偏离意图。这时如果加上override 编译器就会报错。
    • 虚函数默认实参 如果虚函数使用了默认实参,则基类和派生类中定义的默认实参最好一致,因为如果 函数调用使用了默认实参,则实参由本次调用的静态类型决定,也就是说如果静态类型是基类的, 就算调用动态绑定到了继承类定义的函数,实参值还是基类的。
    • 可以使用作用域运算符来回避虚函数机制 派生类重新定义继承来的虚函数时,可能需要调用基类的虚函数版本,这就需要回避虚函数机制,不然 会导致派生类虚函数自己调用自己而无限循环:

      double undisconted = baseP->Quote::net_price(42);
      
  3. 抽象基类
    • 在函数体的位置写 =0 就可以将虚函数说明为纯虚函数,我们不能在类的内部为一个纯虚函数提供 结构体
    • 含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。不能直接创建一个抽象基类的对象 如下面的例子:

      // Type your code here, or load an example.
      #include <memory>
      using namespace std;
      struct A{
        //A(A &fk) { a = fk.a;}
        virtual int foo() = 0;
        int a;   
      };
      int A::foo() {return 0;}
      
      class B : public A {
      
      };
      int main() {
        B  aa;
        return 0;
      }
      

      虽然A的foo已经在类外定义好了,但是倒数第三行的B aa这个代码还是报错: error: cannot declare variable 'aa' to be of abstract type 'B'

  4. 访问控制
    • 每个类都负责控制自己的成员的访问权限:

        public protected private
      用户 ☑️
      派生类 ☑️ ☑️  
      友元 ☑️ ☑️ ☑️
    • 派生类通过派生访问说明符来控制它的成员访问权限,派生访问说明符对于派生类的成员(及友元) 能否访问其直接基类的成员没有任何影响,其目的只是控制该派生类的用户(包括派生类的派生类) 对该派生类的访问权限,其规则是:

      1. 如果继承是公有的,则派生类成员的访问权限直接继承基类的权限说明
      2. 如果是protected继承,则基类中的public成员都会被声明成protected
      3. 如果是private继承,则所有成员都变成private的

      如:

      #include <memory>
      using namespace std;
      class B {
      private:
        int pri_mem;
      protected:
        int prot_mem;
      };
      
      class S : private B {
        friend void c(S&);
        friend void c(B&);
        int j;
      
        int f1() const {return prot_mem;} //不报错
      };
      
      class G : public S {
        int g() {return prot_mem;}  //报错
        int g1() {return pri_mem;}  //报错,
      };
      
      void c(S &s) {
        s.j = s.prot_mem = 0; //不报错
      }
      
      int main() {
      
        return 0;
      }
      

      虽然S的派生访问符是private,但是c函数和f1函数能访问B的成员;而g和g1函数报错, 这个例子证明的上面的说法

    • 派生类向基类转换的可访问性 如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的
    • 友元关系不能继承
    • 派生类可以为那些它可以访问的名字通过using声明来改变可访问性:

      class Base {
      public:
        int pub;
      protected:
        int pro;
      private:
        int pri;
      };
      
      class D : private Base {
        using Base::pub;
        using Base::pro;
        // 错,D不能访问pri,所以不能用using
        // using Base::pri;
      }
      
  5. 继承中的类作用域
    • 派生类的作用域是嵌套在其基类的作用域之内的。
    • 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,即使静态类型和动态类型 不一致,也就是说,如果一个对、引用、指针象的静态类型是基类的类型,那么它就不能访问 基类没有定义的成员,就是继承类定义了这个成员
    • 派生类的成员将隐藏基类的成员,即使基类成员函数的参数列表和派生类不一样,因为名字查找优先于 类型检查,这也说明了为什么基类和派生类的虚函数必须有相同的形参列表,因为如果不这样,基类 的虚函数就会被 隐藏 而不是 覆盖
    • TODO 隐藏、覆盖、重写 虚函数也可以被重写
  6. TODO(深入对象模型重点理解) 构造函数与拷贝控制的继承

3.1.21 16章 模版与泛型编程

泛型编程与面向对象编程一样,也能处理在编写程序时不知道类型的情况。 不同之处在于:OOP能处理类型在程序运行之前都是未知的情况; 而泛型编程中,在编译时就能获知类型了。 而模版是c++中泛型编程的基础。

  1. 函数模版
    • 定义模版 模版定义以关键字template开始,后跟一个用 <> 包围起来的模版参数列表, 参数列表中的参数分为 类型模版参数非类型模版参数每个 类型参数前必须使用关键字 class或typename, class和typename没有什么区别,只是c++早期只有class关键字,其实typename 更加直观:

      //类型模版参数
      template <typename T>
      int compare(const T &v1, const T &v2)
      {
        if (v1 < v2) return -1;
        if (v2 < v1) return 1;
        return 0;
      }
      
      //非类型模版参数
      /* 非参数类型可以是一个整数,或者是一个指向对象或函数类型的指针或引用
       * 而且,绑定到整数的必须是常量表达式,绑定到指针或引用类型的必须具有
       * 静态生存期
      **/
      template<unsigned N, unsigned M>
      int compare(const char (&p1)[N], const char (&p2)[M])
      {
        return strcmp(p1, p2);
      }
      

      从上面代码可以说明编写泛型代码的两个重要原则:

      1. 模版中的函数参数是const的引用,这样保证函数可以用于不能拷贝的类型
      2. 函数体条件判断仅使用 < 比较运算符

      也就是说,模版程序应该尽可能减少对实参的要求

    • 实例化模版与头文件 编译器不会直接为模版定义生成代码,只有当我们调用一个函数模版时,编译器才用函数实参 来为我们推断模版实参并生成一个模版的实例代码,这个过程叫实例化。 通常,当我们调用一个函数时,编译器只需要掌握函数的声明;当我们使用一个类类型的对象时 ,类定义必须是可用的,但成员函数的定义不必已经出现;这是因为编译器编译过程中,只需要 知道对象的名字就可以了(具体定义由连接器负责),函数只要声明了就可以知道它的名字,而类 必须要有了定义才能知道它的成员的名字。 模版则不同,为了生成一个实例化的版本代码,编译器需要掌握函数模版或类模版成员函数的定义。 这些区别也导致了与头文件相关的一些不同:
      1. 我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中
      2. 与非模版代码不同,模版的头文件通常即包含声明也包含定义(因为编译器需要)
  2. 类模版
    • 定义 类模版是用来指导编译器生成类的。一个类模版的每个实例都形成一个独立的类 与函数模版不同,编译器不能为类模版推断模版参数类型 定义在类模版内的成员函数被隐式声明为内联函数
    • 实例化 一个实例化了的类模版,其成员只有在使用时才被实例化。 在一个类模版的作用域内,我们可以直接使用模版名而不必指定模版实参
    • TODO 模版与友元
    • 模版类作用域运算符 当编译器遇到类似T::mem这样的代码时,它不会知道meme是一个类型成员还是一个static数据成员 ,直到实例化才会直到。比如编译器遇到如下形式的语句:

      T::size_type * p;
      

      它需要知道我们是正在定义一个名为p的变量还是将一个名为sizetype的static数据成员与名 为p的变量相乘。默认情况下,c++假定通过作用域运算符访问的名字不是类型,所以如果我们希望 使用一个模版类型参数的类型成员,必须用typename关键字显示告诉编译器该名称是一个类型:

      typename T::size_type * p;
      

      这样才表示我们正在定义一个名为p的T::sizetype * 类型的变量

  3. 成员模版

    一个类(无论是普通类还是类模版)可以包含本身是模版的成员函数。这种成员被称为成员模版。 成员模版不能是虚函数 ,类模版和成员模版各自有各自独立的模版参数。 当我们在类模版外定义一个成员模版时,必须同时为类模版和成员模版提供模版参数列表。 类模版的参数列表在前,成员模版的参数列表在后。

    template <typename T>  //类的类型参数
    template <typename It> //构造函数的类型参数
    
  4. 控制实例化

    当两个或多个独立编译的源文件使用了相同的模版,并提供了相同的模版参数时,每个文件中就都会有该 模版的实例。在大系统中,这种额外开销可能非常严重。在c++11新标准中,我们可以通过显式实例化 来避免这种开销:

    extern template declaration;  // 实例化声明
    template declaration;         // 实例化定义
    

    对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义

    一个类模版的实例化定义会实例化该模版的所有成员,包括内联的成员函数。因为当编译器遇到一个 实例化定义时,它不知道程序你用哪些成员函数。 所以,我们用来显式实例化一个类模版的类型, 必须能用于模版的所有成员

  5. 效率与灵活性

    通过编译时绑定删除器,uniqueptr避免了间接调用删除器的运行时开销。通过在运行时绑定删除器, sharedptr使用户重载删除器更方便,就是说模版可以在效率和灵活性之间做trade off。

  6. 模版实参推断

    有些特殊情况类型推导并不那么顺利:

    • 模版类型参数的类型转换 能应用于函数模版的类型转换只有两种:
      1. const转换:将一个非const对象的引用或指针传递给一个const的引用形参
      2. (TODO:类型转换整理)数组或函数指针转换:

        2020-11-14_09-43-36_screenshot.png

    • 使用相同模版参数类型

      long lng;
      compare(lng, 1024);
      

      compare接受两个const T&参数,所以上面的调用使错误的, 因为推断出来的long和int不是 同一类型,这种情况可以通过显示指定参数类型解决:

      long lng;
      compare<long>(lng, 1024);
      

      上面的例子实例化为compare(long,long)

    • 函数模版中可以有 用普通类型定义的参数

      2020-11-14_09-52-56_screenshot.png

    • 显式实参

      2020-11-14_10-07-55_screenshot.png

      2020-11-14_10-08-14_screenshot.png

    • 尾置返回类型 当我们希望用户确定返回类型时:

      2020-11-14_10-21-00_screenshot.png 由于编译器在遇到参数列表之前,beg是不存在的,所以我们必须使用尾置类型来定义此函数:

      2020-11-14_10-23-17_screenshot.png

    • 用于进行类型转换的标准库模版类

      2020-11-14_10-26-27_screenshot.png

    • 函数指针类型推导 当参数是一个函数模版实例的地址时,程序上下文必须满足:对每个模版参数都能唯一确定 其类型或值
    • 引用的推断

      1. 绑定到常量时引用的汇编层面
        • const左值引用

          int main() {
            const int & a = 10;
            return 0;
          }
          
            main:
                    pushq   %rbp
                    movq    %rsp, %rbp
                    movl    $10, %eax
                    movl    %eax, -12(%rbp)
                    leaq    -12(%rbp), %rax
                    movq    %rax, -8(%rbp)
                    movq    -8(%rbp), %rax
                    movl    0, %eax
                    popq    %rbp
                    ret
          
          rbp-----> -----------
          
                        a
          rbp-8---> -----------
                        10
          rbp-12--> -----------
          
        • 右值引用

          int main() {
            int && a = 10;
            a = 11;
            return 0;
          }
          
          main:
                  pushq   %rbp
                  movq    %rsp, %rbp
                  movl    $10, %eax
                  movl    %eax, -12(%rbp)
                  leaq    -12(%rbp), %rax
                  movq    %rax, -8(%rbp)
                  movq    -8(%rbp), %rax
                  movl    $11, (%rax)
                  movl    $0, %eax
                  popq    %rbp
                  ret
          

      可以看到,绑定到常量时,无论左值引用和右值引用,编译出的代码都一样,编译器会把常量放到 引用所在定义域的一个位置。 TODO : 为什么左值可以绑定到常量?

  7. 重载与模版
    1. 函数匹配规则(209页)遇到模版时(615页)

3.2 STL源码剖析 pdf

3.2.1 3 迭代器与traits编程技法

  1. 迭代器分类

    2020-11-28_17-55-49_screenshot.png

    2020-11-28_17-57-29_screenshot.png

  2. traits机制

    因为c++不是强类型语言,没有typeof之类的操作来获取类的类型,即使有个RTTI的 typeid也只能获得类型的别名,而不能用来声明变量。 针对上面的问题,SGI通过利用function template的参数类型推导来间接获取 泛型的类型。

    2020-11-28_18-35-18_screenshot.png

3.2.2 4 序列化容器

queue,stack,priorityqueue没有迭代器

3.2.3 5 关联式容器

  1. set
    1. 不能通过set的迭代器改变set的元素,因为set元素的值关系到排序规则. set<T>::iterator被定义为RB tree的constiterator
    2. 操作之前的所有迭代器在操作完后依然有效(除了erase删除的)
  2. hashtable
    1. SGI的hashtable迭代器为forwarditeratortag迭代器,所以只能单向遍历 也没有逆向迭代器(rbegin()等)
    2. hashtable重载因子为1,并且用链地址法来解决冲突

3.2.4 6 算法

  1. numeric数值算法
    #include <numeric>
    #include <vector>
    #include <functional>
    #include <iostream>
    #include <iterator>
    using namespace std;
    
    int main()
    {
      int ia[5] = { 1, 2, 3, 4, 5};
      vector<int> iv(ia, ia+5);
      cout << accumulate(iv.begin(), iv.end(), 0) << endl;
      cout << accumulate(iv.begin(), iv.end(), 0, minus<int>()) << endl;
    
      ostream_iterator<int> oit(cout, " ");
    
      partial_sum(iv.begin(), iv.end(), oit);
      cout << endl;
      partial_sum(iv.begin(), iv.end(), oit, minus<int>());
      cout << endl;
    
      adjacent_difference(iv.begin(), iv.end(), oit);
      cout << endl;
      adjacent_difference(iv.begin(), iv.end(), oit, plus<int>());
      cout << endl;
    
      //  cout << power(10, 3) << endl;
      //  cout << power(10, 3, plus<int>()) << endl;
    
    }
    
    
  2. 基本算法
    #include <algorithm>
    #include <vector>
    #include <functional>
    #include <iostream>
    #include <iterator>
    #include <string>
    using namespace std;
    
    int main()
    {
      int ia[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
      vector<int> iv1(ia, ia+5);
      iv1.push_back(-1);
      vector<int> iv2(ia, ia+9);
    
      ostream_iterator<int> oit(cout, " ");
    
      // 判断两个区间的第一个匹配点,返回一个由两个迭代器组成的pair
      // 其中第一个迭代器指向第一个区间的不匹配点,
      //    第二个迭代器指向第二个区间的不匹配点
      cout << *(mismatch(iv1.begin(), iv1.end(), iv2.begin())).first << endl;
      cout << *(mismatch(iv1.begin(), iv1.end(), iv2.begin())).second << endl;
    
      // equel
      // 如果俩个序列在[first, last) 区间内相等,equal返回true
      cout << equal(iv1.begin(), iv1.end(), iv2.begin()) << endl;
      cout << equal(iv2.begin(), iv2.end(), iv1.begin()) << endl;
      cout << equal(iv1.begin(), iv1.end(), &ia[3]) << endl;
      cout << equal(iv1.begin(), iv1.end(), &ia[3], less<int>()) << endl;
    
      fill(iv1.begin(), iv1.end(), 9);
      for (auto e : iv1) cout << e << ' ';
      cout << endl;
    
      fill_n(iv1.begin(), 3, 7);
      for (auto e : iv1) cout << e << ' ';
      cout << endl;
    
      auto it1 = iv1.begin();
      auto it2 = it1;
      advance(it2, 3);
      iter_swap(it1, it2);
      for (auto e : iv1) cout << e << ' ';
      cout << endl;
      for (auto e : iv2) cout << e << ' ';
      cout << endl;
    
      swap(*iv1.begin(), *iv2.begin());
      for (auto e : iv1) cout << e << ' ';
      cout << endl;
      for (auto e : iv2) cout << e << ' ';
      cout << endl;
    
      string s1[] = { "abc", "def", "gh"};
      string s2[] = { "bc", "adc", "jungle"};
      // 以字典序对两个序列进行排序
      cout << lexicographical_compare(s1, s1+2, s2, s2+2) << endl;
      cout << lexicographical_compare(s1, s1+2, s2, s2+2, greater<string>()) << endl;
    }
    
    

3.2.5 7 仿函数

TODO 457页:两个问题还没搞清楚

3.3 Effective Modern C++

3.3.1 绪论

  1. 左值与右值

    检查能否取得表达式的地址。如果能取到,那么该表达式基本可以断定是左值; 如果不可以,则通常为右值。

    根据上面的定义,表达式的型别与它是左值还是右值没有关系:

    class A{
      int x = 7;
    };
    
    void foo(A &&a) {
      A &&b = A(); //b是左值, 意思是虽然b引用的对象没有名字,
      //是个右值,但是b是有名字的,所以他是个
      //左值
      //同样的道理,a也是个左值
      A c;
      b = c;
      //foo(b);
    }
    
    

3.3.2 类型推导

  1. 1 了解模版型别推导

    一个函数模版的声明和调用大致如下:

    template<typename T>
    void f(ParamType param);
    
    f(expr);
    

    在编译期,编译器会通过expr推导两个型别:T和PrarmType。 这两个型别往往是不一样的,因为ParamType常常会包含一些修饰词, 如const或引用符。实际情况是,T的推导结果,不仅依赖于expr的 型别,还依赖于ParamType的形式。要分为三种不同情况来讨论:

    1. 1. ParamType是个指针或引用,但不是万能引用

      推导规则如下:

      1. 若expr具有引用型别,先将引用部分忽略
      2. 然后,对expr的型别和ParamType的型别执行模式匹配,来决定T的型别

      例如,

      我们的模式如下:
      template<typename T>
      void f(T &param);
      
      template<typename T>
      void f1(const T& param);
      
      template<typename T>
      void f2(T *param);
      
      又声明了下列变量:
      int x = 27;
      const int cx = x;
      const int &rx = x;
      const int *px = &x;
      
      那么下面的调用中,推导结果如下:
      f(x);      // T是int, param是int&
      f(cx);     // T是const int, param是const int&
      f(rx);     // T是const int, param是const int&
      
      f1(x);      // T是int, param是const int&
      f1(cx);     // T是int, param是const int&
      f1(rx);     // T是int, param是const int&
      
      f2(&x);     // T是int, param是int*
      f2(px);     // T是const int, param是const int*
      
    2. 2. ParamType是个万能引用

      推导规则如下:

      1. 如果expr是个左值,T和ParamType都会被推导为左值引用。 这是T被推导为引用的唯一情形,而且尽管语法是右值引用的, 但结果却是左值引用。
      2. 如果expr是个右值,则应用“情况1”中的规则

      例子:

      template<typename T>
      void f(T &&param);      //param现在是个万能引用
      
      int x = 27;
      const int cx = x;
      const int &rx = x;
      
      f(x);                  // x是个左值,所以T为int&,param为int&
      f(cx);                 // cx是个左值,所以T为const int&,param也为const int&
      f(rx);                 // rx是个左值,所以T为const int&,param也为const int&
      f(27);                 // 27是个右值,所以T为int,param为int&&
      
    3. 3. ParamType既非指针也非引用

      这种情况下,param会是个全新的对象,而且是expr的副本,所以推导规则为:

      1. 如果expr具有引用型别,则忽略其引用部分
      2. 忽略expr的引用性之后,若expr是个const对象,也忽略它,这是因为 现在是值传递,不像情形1是个引用传递。所以expr是const,并不表 示param也要是一个const。他们是独立的。 如果expr是个volatile对象,也忽略它。
      template<typename T>
      void f(T param);
      
      int x = 27;
      const int cx = x;
      const int &rx = x;
      const char *const ptr = "Fun with pointers";
      
      f(x);               // T和param的类型都是int
      f(cx);              // T和param的型别仍都是int
      f(rx);              // 同上
      f(ptr);             // T和param为const char *,
      
      1. 有两个特殊情况需要考虑:数组或函数型别的实参会退化成对应的指针, 除非它们被用来初始化引用。 TODO : 例子p22
  2. 2 理解auto型别推导
    1. 一般情况下,auto型别推导和模版型别推导是一模一样的,但是 auto型别推导会假定用大括号括起来的初始化表达式代表一个 std::initializerlist,但模板型别推导不会当某变量采用auto来声明时,auto就扮演了T的角色,而变量的型别 修饰词扮演了ParamType的角色。
    2. 在参数返回值或lambda式的形参中使用auto,意思是使用模板型别推导 而不是auto。
  3. 3 理解decltype
  4. 14 TODO throw 的汇编层实现

3.4 深度探索C++对象模型

3.4.1 第一章 关于对象

  1. C++相比C的布局及存取时间上的主要额外负担

    主要包括两个点:

    1. virtual function机制
    2. virtual class

    此外还有一些

    1. 多重继承下的额外负担 发生在“一个derived class和其第二或后继的base class之间的转换”
  2. C++对象模型

    Stroustrup当初设计的(目前仍然是主流)的C++对象模型可以看作是 简单对象模型表格驱动对象模型 的结合: 在此模型中,Nonstatic data members被配置于每一个class object之内, static data members,static及nonstatic function members则 被存放在个别的class object之外。

    virtual function 则以两个步骤支持:

    1. 每一个class产生出一堆指向virtual functions的指针,放在表格之中。 这个表格被成为virtual table( vtbl )。
    2. 每一个class object被安插一个指针,指向相关的virtual table。通常 这个指针被称为 vptr 。vptr的设定和重置都由每个class的constructor、 destructor和copy assignment运算符自动完成。每一个class所关联 的typeinfo object(用来支持runtime type identification, RTTI)也经由virtual table被指出来,通常放在表格的第一个slot。

    virtual base class 则由和virtual function异曲同工的 btal*和*bptr 支持。

  3. C++对象大小
    • 非静态数据成员的大小总和
    • 加上内存对齐
    • 加上为了支持virtual而产生的额外负担

3.4.2 第二章 构造函数语意学

  1. (编译器合成默认构造函数时机)nontrivail default construct

    简单理解就是:下面的情况都进入了part of c++的领域,而不再是part of c,所以需要 合成默认构造函数

    1. 带有default construct的member class object 什么时候带有默认构造函数?答案是 当用户自定义了一个默认构造函数或编译器会为他合成一个的时候。例如下面代码:

      class Foo {
      public:
        Foo();
      };
      class Bar {
      public:
        Foo foo;
        char *str;
      };
      
      void foo_bar() {
        Bar bar;
      }
      

      其汇编代码为:

      Bar::Bar() [base object constructor]:
              pushq   %rbp
              movq    %rsp, %rbp
              subq    $16, %rsp
              movq    %rdi, -8(%rbp)
              movq    -8(%rbp), %rax
              movq    %rax, %rdi
              call    Foo::Foo() [complete object constructor]
              nop
              leave
              ret
      foo_bar():
              pushq   %rbp
              movq    %rsp, %rbp
              subq    $16, %rsp
              leaq    -16(%rbp), %rax
              movq    %rax, %rdi
              call    Bar::Bar() [complete object constructor]
              nop
              leave
              ret
      

      上面的例子中,由于Foo定义了一个默认构造函数,所以编译器为Bar函数也 定义了一个默认构造函数. 如果Foo没有定义默认构造函数的话:

      class Foo {
      public:
      };
      class Bar {
      public:
        Foo foo;
        char *str;
      };
      
      void foo_bar() {
        Bar bar;
      
      

      那么编译器也就不会为Bar合成一个默认构造函数:

      foo_bar():
              pushq   %rbp
              movq    %rsp, %rbp
              nop
              popq    %rbp
              ret
      

      当用户自定义了其他的construct就不会没有default constrcut,如下面 的代码会报错没有默认构造函数:

      class Foo {
      public:
        Foo(int);
      };
      class Bar {
      public:
        Foo foo;
        char *str;
      };
      
      void foo_bar() {
        Bar bar;
      
      
    2. 带有default constrcut的Base Class
    3. 带有一个virtual function的Class
    4. 带有一个virtual base class的Class

    如果用户自定义了默认构造函数,那么编译器会根据他的需要扩张这个构造函数

  2. 拷贝构造函数
    1. 拷贝函数被调用的三种情况:
      1. 显示初始化

        X x;
        X xx = x;
        
      2. 函数参数传递

        extern void foo(X x);
        
        void bar() {
          X xx;
          foo(xx);
        }
        
      3. 参数返回值

        X foo_bar()
        {
          X xx;
          return xx;
        }
        
    2. defult memberwise initialization和bitwise copy

      当一个类对象a以相同class的另一个对象b作为初值初始化时,用的是 default memberwise initialisztion手法完成的: 把每一个基本内置类型和复合类型的data member,从b拷贝一份到a; 但是对于类成员对象,则会递归对这些类对象实施memberwise initialization.

      所以memberwise initialization只会对类中的成员进行操作, 也就是说不会考虑 到vptr和vbptr.

      而bitwise是把一个类的内存对象的整个模型(包括vptr和vbptr)考虑进去了。

      也就是说,一个类的memberwise initialization不一定是bitwise的, 特别是加入了多态性之后,肯定就不是了。

      下面的理解有问题,看上面的

      当一个类对象a以相同class的另一个对象b作为初值初始化时,用的是 default memberwise initialisztion手法完成的: 把每一个基本内置类型和复合类型的data member,从b拷贝一份到a; 但是对于类成员对象,则会递归对这些类对象实施memberwise initialization.

      而bitwise copy就是可以理解为:当类中的所有成员的memberwise initialization 最终都表现为直接复制其基本内置类型和复合类型成员(也叫默认复制语意)。 以下面的例子来理解: 首先看非bitwise copy:

      class str {
      public:
        str(const str&);
      private:
      };
      
      class Word {
      public:
        Word( const str&);
      private:
        int cnt;
        str a;
      };
      
      void foo_bar(const Word &wd) {
        Word w = wd;
      
      

      其汇编为:

      Word::Word(Word const&):
              pushq   %rbp
              movq    %rsp, %rbp
              subq    $16, %rsp
              movq    %rdi, -8(%rbp)
              movq    %rsi, -16(%rbp)
              movq    -16(%rbp), %rax
              movl    (%rax), %edx
              movq    -8(%rbp), %rax
              movl    %edx, (%rax)
              movq    -8(%rbp), %rax
              addq    $4, %rax
              movq    -16(%rbp), %rdx
              addq    $4, %rdx
              movq    %rdx, %rsi
              movq    %rax, %rdi
              call    str::str(str const&)
              nop
              leave
              ret
      foo_bar(Word const&):
              pushq   %rbp
              movq    %rsp, %rbp
              subq    $32, %rsp
              movq    %rdi, -24(%rbp)
              movq    -24(%rbp), %rdx
              leaq    -8(%rbp), %rax
              movq    %rdx, %rsi
              movq    %rax, %rdi
              call    Word::Word(Word const&)
              nop
              leave
              ret
      

      可以看到,str自定义了一个拷贝构造函数,所以str和含有str的Word都不再有默认 复制语意.所以编译器会为Word合成一个拷贝构造函数。

      而bitwise copy的话:

      class str {
      public:
        // str(const str&);
      private:
      };
      
      class Word {
      public:
        Word( const str&);
      private:
        int cnt;
        str a;
      };
      
      void foo_bar(const Word &wd) {
        Word w = wd;
      
      

      重点注意到被注释掉的代码,也就是说str并没有自定义的copy,所以可以说str 符合默认复制语意。 其汇编为:

      foo_bar(Word const&):
              pushq   %rbp
              movq    %rsp, %rbp
              movq    %rdi, -24(%rbp)
              movq    -24(%rbp), %rax
              movq    (%rax), %rax
              movq    %rax, -8(%rbp)
              nop
              popq    %rbp
              ret
      
    3. 不展现bitwise copy semantics的情况:
      1. 当class内含有一个member object,而后者的class声明中有一个拷贝构造函数时 (不管是显式声明还是编译器合成)
      2. 当class继承自一个base class而后者存在一个拷贝构造函数时 (不管是显式声明还是编译器合成)
      3. 当class声明了一个或多个virtual functions时
      4. 当class派生自一个继承链,其中一个或多个为virtual base calss时
  3. NRV优化

    即named return value优化:

    X bar(){
      X xx;
      处理xx
      return xx;
    }
    
    void
    bar(X &__result)
    {
      __result.X::X();
      直接处理__result
      return ;
    }
    
  4. TODO 什么时候必须使用成员初始化列表(p.75)

    todo 为什么虚拟继承需要到执行期才能确定去哪? 感觉可以用虚函数做类比,因为其动态类型是执行期才 确定的,而虚拟继承和虚函数一样,其偏移或者地址是知道动态类型 才能确定的。

3.7 apue pdf

3.7.2 charpter3

  1. lseek

    在比较lseek的返回值时应该谨慎,不要测试是否小于0,而要测试是否等于-1

  2. 内核用3种数据结构来表示打开的文件
    1. 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,与每个文件描述符关联的是: a. 文件描述符标志 b. 指向一个文件表项的指针
    2. 内核为所有打开文件维持一张全局文件表。每个文件表项包含: a. 文件状态标志(读,写,添写,同步,非阻塞等) b. 当前文件偏移量 c. 指向该文件v节点表项的指针
    3. 每个打开的文件(或设备)都有一个v节点结构,包含了: a. 文件类型 b. 对此文件进行各种操作的函数的指针
  3. dup和dup2的区别
    • dup返回的文件描述符,一定是当前可用文件描述符中的最小值, dup2可以指定
  4. fcntl
    #include <fcntl.h>
    int fcntl(int fd, int cmd, .../* int arg */ );
    出错返回-1
    

    fcntl函数有5种功能

    1. 复制一个已有的描述符
    2. 获取/设置文件描述符标志
    3. 获取/设置文件状态标志
    4. 获取/设置异步io所有权
    5. 获取/设置记录锁
  5. /dev/fd的作用

    /dev/fd文件主要由shell使用,能用处理其他路径名同样的方式处理标准输入和输出

3.7.3 charpter4

3.7.4 charpter5 标准io库

3.7.5 chapter6 系统数据文件和信息

  1. unix系统中有大量与系统有关的数据文件,他们都有一套例程来访问这些系统数据文件

    如图:

    2020-11-06_15-55-43_screenshot.png

  2. 系统标示uname

    uname返回与主机和操作系统相关的信息

    TODO
    
  3. 时间和日期例程
    1. 有一套函数来获取时间,他们的关系如图:

      2020-11-06_17-33-21_screenshot.png

      其中的设计的数据结构有:

      time_t // 从1970年1月1日以来的秒数
      
      struct timeval {
        time_t       tv_sec;   /* seconds since Jan. 1, 1970 */
        suseconds_t  tv_usec;  /* and microseconds */
      };
      
      struct timespec {
        time_t  tv_sec;     /* seconds */
        long    tv_nsec;    /* and nanoseconds */
      };
      
      struct tm {
        int tm_sec;     /* seconds (0 - 60) */
        int tm_min;     /* minutes (0 - 59) */
        int tm_hour;    /* hours (0 - 23) */
        int tm_mday;    /* day of month (1 - 31) */
        int tm_mon;     /* month of year (0 - 11) */
        int tm_year;    /* year - 1900 */
        int tm_wday;    /* day of week (Sunday = 0) */
        int tm_yday;    /* day of year (0 - 365) */
        int tm_isdst;   /* is summer time in effect? */
        char *tm_zone;  /* abbreviation of timezone name */
        long tm_gmtoff; /* offset from UTC in seconds */
      }
      

      一般获取时间的流程是:用相关函数获取前三个结构体后,通常要调用相关函数转换为 第四个结构struct tm, 最后还可以把tm格式化输出

  4. 进程环境

3.7.6 chapter7 进程环境

  1. 进程启动
    • 启动例程 C程序总是从main函数开始执行,当内核执行C程序时(exec函数),在调用main函数前先调用一个 特殊的启动例程。该例程从内核取得命令行参数和环境变量值,为调用main函数做好准备。 连接器将该启动例程指定为C程序的起始地址。
  2. 进程终止
    • 有8种方法来使进程终止,其中五种为正常终止:

      1. 从main返回
      2. 调用exit
      3. 调用exit或Exit
      4. 最后一个线程从其启动例程返回
      5. 从最后一个线程调用pthreadexit

      另外三种为异常终止:

      1. 调用abort
      2. 接到一个信号
      3. 最后一个线程对取消请求做出响应
    • exit与exit/Exit

      1. exit总是执行一个标准I/O库的清理关闭操作:对所有打开流调用fclose函数。而exit和Exit 则直接进入内核
      2. exit和Exit是ISO C说明的,而exit是POSIX.1说明的,所以他们的头文件不一样
      #include <stdlib.h>
      
      void
      exit(int status);
      
      void
      _Exit(int status);
      
      
      
      #include <unistd.h>
      
      void
      _exit(int status);
      
    • return与exit main函数返回一个整型值与用该值调用exit是等价的。于是main函数中exit(0)等价于return(0), 所以一般在main中使用return而不是exit。但是某些情况也需要exit:

      2020-11-07_15-37-53_screenshot.png

    • atexit 进程可以通过atexit函数来注册终止处理函数 main调用exit后,exit会自动调用调用进程 登记的终止处理函数:

      NAME
      atexit -- register a function to be called on exit
      
      SYNOPSIS
      #include <stdlib.h>
      
      int
      atexit(void (*function)(void));
      
      int
      atexit_b(void (^function)(void));
      

      有一个要注意的点是:atexit注册的顺序和exit调用的顺序相反,如:

      #include "apue.h"
      
      static void my_exit1(void);
      static void my_exit2(void);
      
      int
      main()
      {
        if (atexit(my_exit1) != 0)
          err_sys("can't register myexit1 ");
        if (atexit(my_exit2) != 0)
          err_sys("can't register myexit2 ");
        if (atexit(my_exit2) != 0)
          err_sys("can't register myexit2 ");
      
        printf("main is done\n");
        return 0;
      }
      
      static void my_exit1() 
      {
        printf("my_exit1\n") ;
      }
      
      static void my_exit2() 
      {
        printf("my_exit2\n") ;
      }
      
      
    • 一个C程序的启动和终止过程可以表示为下图:

      2020-11-07_16-15-54_screenshot.png 可以看到,exit会首先调用各种终止处理程序,然后关闭所有打开流。而且,所有的程序都是从 exec开始的,也就是说,内核使得程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一 方法是显示或隐式的(通过exit)调用exit或Exit。当然,进程也可以非自愿地由一个信号 使其终止

  3. 进程环境表
    • 每个程序都接收到一张环境表,全局变量environ指向了这张表:

    2020-11-07_18-01-40_screenshot.png 与环境表相关的函数有如下:

    #include <stdlib.h>
    
    char *
    getenv(const char *name);
    
    int
    setenv(const char *name, const char *value, int overwrite);
    
    int
    putenv(char *string);
    
    int
    unsetenv(const char *name);
    
    

    2020-11-07_19-06-37_screenshot.png

    这些函数的执行细节如下:

    2020-11-07_19-07-55_screenshot.png

    2020-11-07_19-09-47_screenshot.png

  4. TODO 内存布局

    典型的进程内存布局如图:

    2020-11-07_18-05-21_screenshot.png

    可以用size命令来查看:

  5. TODO 共享库
  6. 内存分配

    ISO C说明了3个用于存储空间动态分配的函数:

    #include <stdlib.h>
    
    void *
    calloc(size_t count, size_t size);
    
    void
    free(void *ptr);
    
    void *
    malloc(size_t size);
    
    void *
    realloc(void *ptr, size_t size);
    
    void *
    reallocf(void *ptr, size_t size);
    
    void *
    valloc(size_t size);
    
    void *
    aligned_alloc(size_t alignment, size_t size);
    

    还有很多替代品:

    2020-11-07_18-25-23_screenshot.png

  7. TODO setjmp和longjmp
  8. 资源限制相关函数
    • 每个进程都有一组资源限制,相关函数和数据结构有:
    #include <sys/resource.h>
    
    int
    getrlimit(int resource, struct rlimit *rlp);
    
    int
    setrlimit(int resource, const struct rlimit *rlp);
    
    struct rlimit {
      rlim_t  rlim_cur;       /* current (soft) limit */
      rlim_t  rlim_max;       /* hard limit */
    };
    

    其规则是:cur不能高于max;max不能小于cur;普通用户只用往少了调.

    • 资源限制会继承到子进程

3.7.7 chapter8 进程控制

  1. fork
    #include "apue.h"
    
    int globvar = 6;
    char buf[] = "a write to stdout\n";
    
    int
    main()
    {
      int var;
      pid_t pid;
    
      var = 88;
      if (write(STDOUT_FILENO, buf, sizeof(buf)) != sizeof(buf))
        err_sys("write error");
      printf("before fork\n");
    
      if ((pid = fork()) < 0)
        err_sys("fork error");
      else if (pid == 0) {
        buf[0] = 'b';
        globvar++;
        var++;
      } else {
        sleep(2);
      }
    
      printf("pid = %ld, globvar = %d, var = %d\n", (long)getpid(), globvar, var);
      printf("%s\n", buf);
      sleep(2);
      return 0;
    }
    
    ~/l/a/a/m/chapter8 ❯❯❯ ./a.out                                          14:43:18
    a write to stdout
    before fork
    pid = 68796, globvar = 7, var = 89
    pid = 68795, globvar = 6, var = 88
    
    ~/l/a/a/m/chapter8 ❯❯❯ ./a.out > temp.out                               14:43:24
    ~/l/a/a/m/chapter8 ❯❯❯ cat temp.out                                     14:43:49
    a write to stdout
    before fork
    pid = 68799, globvar = 7, var = 89
    before fork
    pid = 68798, globvar = 6, var = 88
    
    # 俩个结果不同是因为重定向标准输出后变为全缓冲了,所以子进程
    # 该io缓冲中还有before fork
    
    • fork中,父进程的很多属性由子进程继承:

      2020-11-08_15-07-06_screenshot.png 但也有很多区别:

      2020-11-08_15-08-09_screenshot.png

  2. exit和终止状态

    chapter7说到终止进程的方法有8种,5种正常终止和3种异常终止。不管进程以8种中的哪种终止, 最后都会执行到内核中的同一段代码,这段代码为进程关闭所有的打开描述符,释放它使用的内存。 不仅如此,任何一种终止方法都实现了把终止进程的终止状态通知给父进程机制的机制。在正常终止 的情况下,main函数中return 语句返回的那个status叫退出状态,退出状态会传递给exit函数, 最后由exit函数返回。父进程可以使用wait或waitpid函数取得这个状态。posix.1还规定了一些 宏来查看终止状态:

    2020-11-08_18-10-08_screenshot.png

  3. wait和waitpid
    #include <sys/wait.h>
    
    pid_t
    wait(int *stat_loc);
    
    pid_t
    wait3(int *stat_loc, int options, struct rusage *rusage);
    
    pid_t
    wait4(pid_t pid, int *stat_loc, int options, struct rusage *rusage);
    
    pid_t
    waitpid(pid_t pid, int *stat_loc, int options);
    
    成功返回进程ID,出错返回0或-1
    

    waitpid与wait的主要不同是:

    2020-11-08_18-08-02_screenshot.png wait3和wait4比上面两个函数多了一个功能:允许内核返回由终止进程及其所有子进程使用的资源情况

  4. TODO 孤儿进程和僵尸进程

    如果想fork一个子进程,但是又不想调用wait来等待它终止,那么可以调用两次fork来达到这种效果(193页)

  5. exec函数
    #include <unistd.h>
    
    extern char **environ;
    
    int
    execl(const char *path, const char *arg0, ... /*, (char *)0 */);
    
    int
    execle(const char *path, const char *arg0, ...
           /*, (char *)0, char *const envp[] */);
    
    int
    execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
    
    int
    execv(const char *path, char *const argv[]);
    
    int
    execvp(const char *file, char *const argv[]);
    
    int
    execvP(const char *file, const char *search_path, char *const argv[]);
    

    各个函数之间的关系如下:

    2020-11-08_19-42-53_screenshot.png

    • 注意事项:
      1. file参数:
        • 如果file包含/,就将其视为路径名
        • 否则就按PATH环境变量指定的目录找名为file的可执行文件
      2. 助记:
        • 字母p(ath)表示取filename作为参数,并且用PATH环境变量寻找可执行文件。
        • 字母l(ist)表示该函数取一个参数表,与互斥
        • 字母(arg)v表示取一个argv[]矢量
        • 字母e(nvp)表示该函数取envp[]数组,而不使用当前环境
    • 调用exec并不创建新进程,所以前后的进程ID并没有改变,只是用磁盘上的一个新程序替换了当前进程 的正文段,数据段,堆栈段。新进程从调用进程继承了下列属性:

      2020-11-08_18-59-31_screenshot.png 对打开文件的处理与每个描述符的执行时关闭标志值有关。

  6. TODO 更改用户ID和更改组ID
  7. TODO 解释器与system
  8. TODO 进程时间(程序8-31)

3.7.8 TODO chapter9 进程关系 PASS

3.7.9 chapter10 信号

  1. 概念
    • 中断 操作系统中一个非常重要的概念就是中断,中断可以让程序执行异步操作。异步和同步相对,同步一般 意味着需要等待,所以异步可以带来很多好处。中断是指计算机在执行期间,系统内发生任何非寻常的 或非预期的急需处理事件,使得CPU暂时中断当前正在执行的程序而转去执行相应的事件处理程序,待 处理完毕后又返回原来被中断处继续执行或调度新的进程执行的过程。引起中断发生的事件被称为中断 源。中断源向CPU发出的请求中断处理信号称为中断请求,而CPU收到中断请求后转到相应的事件处理 程序称为中断响应。中断分为:
      1. 硬中断 通过硬件产生相应的中断请求,称为硬中断。
      2. 软中断
    • 软中断 软中断的概念主要来源于UNIX系统。软中断是对应于硬中断而言的。它是在通信进程之间通过模拟硬中断 而实现的一种通信方式。中断源发出软中断信号后,CPU或者接收进程在“适当的时机”进行中断处理或者 完成软中断信号所对应的功能。这里“适当的时机”,表示接收软中断信号的进程须等到该接收进程得到 处理器之后才能进行如果该接收进程是占据处理器的,那么,该接收进程在接收到软中断信号后将立即 转去执行该软中断信号所对应的功能。
    • 信号
      1. 本质 信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个 中断请求可以说是一样的。
      2. 来源
        • 硬件来源 比如我们按下了键盘或者其它硬件故障
        • 软件来源 最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数, 软件来源还包括一些非法运算等操作。
      3. 分类
        • 按可靠性:可靠和不可靠
        • 按实时性:实时和非实时
  2. 信号产生条件
    1. 按下某些终端键
    2. 硬件异常产生信号:除0、无效的内存引用
    3. kill系统调用
    4. kill命令行命令
    5. 当检测到某种软件条件已经发生
  3. 处理信号的三种方式
    1. 忽略 有两种不能忽略SIGKILL SIGSTOP
    2. 捕捉信号 收到信号时,调用预先设定好的用户函数
    3. 执行系统默认动作

      2020-11-09_20-20-58_screenshot.png

  4. signal函数

    signal函数表示为sig信号设置信号处理函数

    #include <signal.h>
    
    void (*signal(int sig, void (*func)(int)))(int);
    
    or in the equivalent but easier to read typedef'd version:
    
    typedef void (*sig_t) (int);
    
    sig_t
    signal(int sig, sig_t func);
    

    参数sig:信号名 func:新的信号调用函数, 可以是自定义的用户空间函数, 也可以是SIGIGN(ignore),SIGDFL(default) 返回值:旧的信号处理函数,或SIGERR表示出错

  5. fork和exec时信号的继承
    • fork 当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程 的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。
    • exec 当调用exec运行新程序时,除非 调用 exec的进程忽略该信号,否则所有信号都被设置为它们 的默认动作
  6. 可靠信号与信号阻塞

    早期的unix中,信号是不可靠的,这里的不可靠指的是:

    1. 信号可能会丢失,因为没有保存(阻塞)信号的能力
    2. 进程每次接到信号对其进行处理时,随机该信号动作的动作重置为默认值

    4.2BSD对信号进行了改动,提供了可靠的信号机制:

    1. 可靠信号
      • 产生信号 当造成信号的事件发生时,为进程产生一个信号
      • 递送信号 当内核在进程表中以某种形式设置标志表明产生信号时,称为递送了信号
      • 阻塞信号的递送 进程可以设置信号为阻塞的,这种情况下,信号的递送被阻塞,
      • 未决pending 产生信号开始,到递送信号完成之前;或者是信号被进程阻塞(而且进程对该信号的动作是系统默认 动作或捕捉该信号),则该进程为未决状态
      • 多次递送信号 如果在进程解除对某个信号的阻塞之前,信号递送了多次,则称信号进行了排队。除非支持posix实时 扩展,否则大多数uinx并不对信号排队,而是只递送这种信号一次
  7. 被信号中断的系统调用

    早期unix系统的一个特性是:如果进程执行一个 低速系统调用 而阻塞期间捕捉到一个 信号,则该系统调用就被中断而不再继续执行。该系统调用返回出错并且errno设置为 EINTR。TODO: 非低速系统调用自动重启吗? 其中低速系统调用是指可能会使进程永远阻塞的一类系统调用,包括以下几类:

    2020-11-10_15-42-21_screenshot.png 为了帮助应用程序,使得其不必处理被中断的系统调用,4.2BSD开始引入的中断系统调用的自动重启, 下面为几种实现所提供的与信号有关的函数与语义:

    2020-11-10_15-49-33_screenshot.png

  8. TODO 可重入函数
  9. kill和raise
    #include <signal.h>
    
    /* 将信号发送给进程或进程组 */
    int
    kill(pid_t pid, int sig);
    
    
    
    /* 进程向自己发送信号 */
    #include <signal.h>
    
    int
    raise(int sig);
    

    kill的pid参数有如下几种情况:

    1. pid > 0 将信号发送给进程id为pid的进程
    2. pid == 0 发送给该进程所在进程组的所有进程,发送进程必须有权限
    3. pid < 0 发送给进程中ID为-pid的所有进程,调用进程必须有权限
    4. pid == -1 发送给调用进程有权限发送信号的所有进程

    注:上面所说的所有进程,不包括系统进程集(内核进程和init进程(pid为1))

  10. alarm和pause
    #include <unistd.h>
    
    /* 设置一个定时器,当超时时产生一个SIGALRM信号 */
    unsigned
    alarm(unsigned seconds);
    
    
    /* 使调用进程挂起直到捕捉到一个信号 */
    #include <unistd.h>
    
    int
    pause(void);
    
  11. 信号集

    用来表示信号的数据结构为信号集(sigset), posix定义了下列五个处理信号集的函数:

    #include <signal.h>
    
    int
    sigaddset(sigset_t *set, int signo);
    
    int
    sigdelset(sigset_t *set, int signo);
    
    int
    sigemptyset(sigset_t *set);
    
    int
    sigfillset(sigset_t *set);
    
    int
    sigismember(const sigset_t *set, int signo);
    
  12. sigpending

    sigpending返回当前未决的信号

    #include <signal.h>
    
    int
    sigpending(sigset_t *set);
    

    10-15.c

  13. sigaction函数

    sigaction函数的功能是检查或修改(或检查并修改)与指定信号相关联的处理动作。

    #include <signal.h>
    
    /* 复制自macos 下的man,与书上有区别 */
    struct  sigaction {
      union __sigaction_u __sigaction_u;  /* signal handler */
      /* 如果sa_handler包含自定义的捕捉函数,则sa_mask字段说明了一个信号集
       * 在调用捕捉函数之前阻塞这些信号,调用完后再恢复
       */
      sigset_t sa_mask;               /* signal mask to apply */
      /* 对信号sig进行处理的各种选项,具体见代码下的图 */
      int     sa_flags;               /* see signal options below */
    };
    
    union __sigaction_u {
      void    (*__sa_handler)(int);
      void    (*__sa_sigaction)(int, siginfo_t *,
                                void *);
    };
    
    #define sa_handler      __sigaction_u.__sa_handler
    #define sa_sigaction    __sigaction_u.__sa_sigaction
    
    /* 要修改的动作存在act,上一个动作返回到oact */
    int
    sigaction(int sig, const struct sigaction *restrict act,
              struct sigaction *restrict oact);
    

    2020-11-11_14-34-39_screenshot.png

  14. TODO sigsetjmp和siglongjmp
  15. sigsuspend

    当我们希望保护代码临界区不被某些信号中断,可以用sigsuspend函数

    #include <signal.h>
    
    int
    sigsuspend(const sigset_t *sigmask);
    

    它的作用是在执行完临界区代码后原子地恢复信号屏蔽字,然后使得进程休眠。 这样就可以防止sigprocmask函数恢复屏蔽字和调用pause接受进程之间的空档

  16. TODO abort
    #include <stdlib.h>
    
    void
    abort(void);
    
  17. TODO system
  18. TODO sleep

    sleep函数是通过alarm和信号实现的

  19. sigqueue

    可以对信号实现排队

  20. 小结

    信号用于大多数复制的应用程序中,理解进行信号处理的原因和方式对于高级unix编程 极其重要

3.7.10 TODO chapter11 线程

  1. 线程的优点

    典型的unix进程在某一时刻只能做一件事情。有了线程后,程序设计就可以把程序设计成 在某一时刻能够做不止一件事,每个线程处理各自独立的任务。这中方法有很多好处:

    1. 可以简化处理异步事件的代码。
    2. 多个线程可以自动地访问相同的存储地址空间和文件描述符,不像进程那么复杂
    3. 有些问题可以分解从而提高整个程序的吞吐量
    4. 可以改善交互程序的响应时间

    单cpu也能得到多线程编程模型的好处

  2. 创建线程

    每个线程都有一个线程ID pthreadt,线程创建相关函数有:

    #include <pthread.h>
    
    /* 创建新线程 */
    int
    pthread_create(pthread_t *thread,       //保存新线程的id
                   const pthread_attr_t *attr,  // 定制各种不同的线程属性
                   void *(*start_routine)(void *), //新线程从start_rtn函数开始运行
                   void *arg); //start_rtn参数
    
    // 判断线程id是否相等, 不能直接==,因为pthread_t可能是结构体(具体实现)
    int pthread_equal(pthread_t tid1, pthread_t tid2);
    
    // 获得自身的线程ID
    pthread_t
    pthread_self(void);
    

    新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但线程的 挂起信号集被清除

  3. 终止线程

    下列方式会终止整个进程:

    1. 进程中任意线程调用了exit、exit或Exit
    2. 线程收到信号,并且动作是终止进程

    下列三个方法能在不终止整个进程的情况下停止单个线程:

    1. 线程可以简单地从启动例程返回(return),返回值是线程的退出码

      #include <pthread.h>
      
      void
      pthread_exit(void *value_ptr);
      

      进程的其他线程通过pthreadjoin等待线程结束并访问valueptr

      #include <pthread.h>
      
      int
      pthread_join(pthread_t thread, void **value_ptr);
      
    2. 线程可以被同一进程的其他线程取消:

      #include <pthread.h>
      
      int
      pthread_cancel(pthread_t thread);
      
    3. 线程调用pthreadexit
  4. 分离线程
    #include <pthread.h>
    
    int
    pthread_detach(pthread_t thread);
    

    默认情况下,线程的终止状态会保存知道对线程调用pthreadjoin, 调用pthreaddetach后, 线程会被分离,线程的底层存储资源可以在线程终止时立即回收。分离后不能再对该线程调用 pthreadjoin.

  5. TODO 线程清理处理程序
  6. 线程同步
    1. 自旋锁
      • 概念 自旋锁是通过在获取锁前一直处于忙等状态的一种锁。
      • 适用情况
        1. 自旋锁适用于锁被持有的时间短,而且线程并不希望在重新调度上化太多成本
        2. 自旋锁用在非抢占式内核中是很有用的:除了提供互斥机制,他们还会阻塞中断
        3. 自旋锁在用户层并不一定非常有用,因为获取自旋锁的线程可能会被取消调度而 进入休眠状态,那么阻塞在锁上的其他线程自旋的时间就可能会比预期的更长
      • 实现 一般可以通过使用 测试并设置 指令实现
    2. 互斥量

      互斥量保证同一时间只有一个线程访问数据。如果释放互斥量时有一个以上的线程阻塞,那么所有 该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程可以对互斥量加锁

      #include <pthread.h>
      
      int
      pthread_mutex_init(pthread_mutex_t *mutex,
                         const pthread_mutexattr_t *attr);
      
      int
      pthread_mutex_destroy(pthread_mutex_t *mutex);
      
      int
      pthread_mutex_lock(pthread_mutex_t *mutex);
      
      int
      pthread_mutex_trylock(pthread_mutex_t *mutex);
      
      int
      pthread_mutex_unlock(pthread_mutex_t *mutex);
      
    3. 读写锁
    4. 条件变量
    5. 屏障
  7. TODO 线程控制

3.7.11 TODO chapter12

3.7.12 守护进程(daemon)

守护进程是生存期长的一种进程。他们常常在系统引导装入时启动,仅在系统关闭时才终止。

  1. 编程规则(创建过程)
    1. 调用umask将文件模式创建屏蔽字设置为一个固定值(通常是0)。 因为继承来的屏蔽字可能被设置为拒绝某些权限
    2. 调用fork,然后使父进程exit。 理由:
      • 当守护进程由一条简单的shell命令启动,父进程exit会让shell认为这条命令已经 执行完毕。
      • fork保证子进程不是一个进程组的组长(继承了父进程的进程组ID,但是获得了新的进程ID), 为第三步做准备
    3. 调用setsid创建一个新的会话。 作用:
      • 成为新会话的首进程
      • 成为一个新进程组的组长进程
      • 没有控制终端
    4. 将当前工作目录改为根目录 因为父进程的当前工作目录可能在一个挂载的文件系统中,如果不改变,那么这个文件系统在 守护进程运行过程中都不能被卸载
    5. 关闭不再需要的文件描述符。 和父进程脱离联系
    6. 将0,1,2文件描述符重定向到/dev/null
  2. 出错(日志)记录

    守护进程存在的一个问题是如何处理出错信息,首先守护进程肯定不能把日志直接输出控制台,然后, 我们也不希望每个守护进程一个日志文件,这样太难管理。为此,大多数守护进程都使用syslog设施:

    2020-11-14_15-15-52_screenshot.png 可以看到,有三种途径产生日志消息:

    2020-11-14_15-24-57_screenshot.png

  3. 单例守护进程

    使用文件和记录锁

  4. 守护进程惯例

    2020-11-14_15-53-21_screenshot.png

3.8 tlpi

3.8.1 第四章

一旦应用程序需要访问文件系统或设备的专有功能时,可以选择瑞士军刀般的 ioctl()系统 调用(4.8 节),该调用为通用 I/O 模型之外的专有特性提供了访问接口。

3.8.2 5

所有系统调用都是以原子操作方式执行的。

3.8.3 ipc

信号(signal),用来表示事件的发生。 管道(亦即 shell 用户所熟悉的“|”操作符)和 FIFO,用于在进程间传递数据。 套接字,供同一台主机或是联网的不同主机上所运行的进程之间传递数据。 文件锁定,为防止其他进程读取或更新文件内容,允许某进程对文件的部分区域加以锁定。 消息队列,用于在进程间交换消息(数据包)。 信号量(semaphore),用来同步进程动作。 共享内存,允许两个及两个以上进程共享一块内存。当某进程改变了共享内存的内容时,其他所有进程会立即了解到这一变化。

3.9 TODO 内核架构 pdf

3.9.1 进程管理与调度

  1. taskstruct
    struct task_struct {
       - TASK_RUNNING: 可运行状态。该状态确保进程可以立即运行,而无需等待外部条件
       - TASK_INTERRRUPTIBLE:针对等待某事件或其他资源的睡眠进程设置的。在内核发送信号给
         进程表明事件已经发生时,进程状态变为TASK_RUNNING。
       - TASK_UNINTERRRUPTIBLE:因内核指示而停用的进程,不能由外部信号唤醒,只能由内核亲自唤醒
       - TASK_STOPPED :特意停止运行,如调试
       - TASK_TRACED:区分停止的进程中被调试的进程和常规的进程
       - EXIT_ZOMEIE 僵尸进程
       - EXIT_DEAD wait系统调用已经发出,在进程完全从系统中移除之前的状态,只在多个进程对同一进程
         发出wait调用时才有意义
      volatile long state;  /* -1 unrunnable, 0 runnable, >0 stopped */
      void *stack;
      atomic_t usage;
      unsigned int flags; /* per process flags, defined below */
      unsigned int ptrace;
    
      int lock_depth;   /* BKL lock depth */
    
    #ifdef CONFIG_SMP
    #ifdef __ARCH_WANT_UNLOCKED_CTXSW
      int oncpu;
    #endif
    #endif
    
      int prio, static_prio, normal_prio;
      struct list_head run_list;
      const struct sched_class *sched_class;
      struct sched_entity se;
    
    #ifdef CONFIG_PREEMPT_NOTIFIERS
      /* list of struct preempt_notifier: */
      struct hlist_head preempt_notifiers;
    #endif
    
      unsigned short ioprio;
      /*
       * fpu_counter contains the number of consecutive context switches
       * that the FPU is used. If this is over a threshold, the lazy fpu
       * saving becomes unlazy to save the trap. This is an unsigned char
       * so that after 256 times the counter wraps and the behavior turns
       * lazy again; this to deal with bursty apps that only use FPU for
       * a short time
       */
      unsigned char fpu_counter;
      s8 oomkilladj; /* OOM kill score adjustment (bit shift). */
    #ifdef CONFIG_BLK_DEV_IO_TRACE
      unsigned int btrace_seq;
    #endif
    
      unsigned int policy;
      cpumask_t cpus_allowed;
      unsigned int time_slice;
    
    #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
      struct sched_info sched_info;
    #endif
    
      struct list_head tasks;
      /*
       * ptrace_list/ptrace_children forms the list of my children
       * that were stolen by a ptracer.
       */
      struct list_head ptrace_children;
      struct list_head ptrace_list;
    
      struct mm_struct *mm, *active_mm;
    
    /* task state */
      struct linux_binfmt *binfmt;
      int exit_state;
      int exit_code, exit_signal;
      int pdeath_signal;  /*  The signal sent when the parent dies  */
      /* ??? */
      unsigned int personality;
      unsigned did_exec:1;
      pid_t pid;
      pid_t tgid;
    
    #ifdef CONFIG_CC_STACKPROTECTOR
      /* Canary value for the -fstack-protector gcc feature */
      unsigned long stack_canary;
    #endif
      /* 
       * pointers to (original) parent process, youngest child, younger sibling,
       * older sibling, respectively.  (p->father can be replaced with 
       * p->parent->pid)
       */
      struct task_struct *real_parent; /* real parent process (when being debugged) */
      struct task_struct *parent; /* parent process */
      /*
       * children/sibling forms the list of my children plus the
       * tasks I'm ptracing.
       */
      struct list_head children;  /* list of my children */
      struct list_head sibling; /* linkage in my parent's children list */
      struct task_struct *group_leader; /* threadgroup leader */
    
      /* PID/PID hash table linkage. */
      struct pid_link pids[PIDTYPE_MAX];
      struct list_head thread_group;
    
      struct completion *vfork_done;    /* for vfork() */
      int __user *set_child_tid;    /* CLONE_CHILD_SETTID */
      int __user *clear_child_tid;    /* CLONE_CHILD_CLEARTID */
    
      unsigned int rt_priority;
      cputime_t utime, stime, utimescaled, stimescaled;
      cputime_t gtime;
      cputime_t prev_utime, prev_stime;
      unsigned long nvcsw, nivcsw; /* context switch counts */
      struct timespec start_time;     /* monotonic time */
      struct timespec real_start_time;  /* boot based time */
    /* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
      unsigned long min_flt, maj_flt;
    
        cputime_t it_prof_expires, it_virt_expires;
      unsigned long long it_sched_expires;
      struct list_head cpu_timers[3];
    
    /* process credentials */
      uid_t uid,euid,suid,fsuid;
      gid_t gid,egid,sgid,fsgid;
      struct group_info *group_info;
      kernel_cap_t   cap_effective, cap_inheritable, cap_permitted;
      unsigned keep_capabilities:1;
      struct user_struct *user;
    #ifdef CONFIG_KEYS
      struct key *request_key_auth; /* assumed request_key authority */
      struct key *thread_keyring; /* keyring private to this thread */
      unsigned char jit_keyring;  /* default keyring to attach requested keys to */
    #endif
      char comm[TASK_COMM_LEN]; /* executable name excluding path
                 - access with [gs]et_task_comm (which lock
                   it with task_lock())
                 - initialized normally by flush_old_exec */
    /* file system info */
      int link_count, total_link_count;
    #ifdef CONFIG_SYSVIPC
    /* ipc stuff */
      struct sysv_sem sysvsem;
    #endif
    /* CPU-specific state of this task */
      struct thread_struct thread;
    /* filesystem information */
      struct fs_struct *fs;
    /* open file information */
      struct files_struct *files;
    /* namespaces */
      struct nsproxy *nsproxy;
    /* signal handlers */
      struct signal_struct *signal;
      struct sighand_struct *sighand;
    
      sigset_t blocked, real_blocked;
      sigset_t saved_sigmask;   /* To be restored with TIF_RESTORE_SIGMASK */
      struct sigpending pending;
    
      unsigned long sas_ss_sp;
      size_t sas_ss_size;
      int (*notifier)(void *priv);
      void *notifier_data;
      sigset_t *notifier_mask;
    #ifdef CONFIG_SECURITY
      void *security;
    #endif
      struct audit_context *audit_context;
      seccomp_t seccomp;
    
    /* Thread group tracking */
        u32 parent_exec_id;
        u32 self_exec_id;
    /* Protection of (de-)allocation: mm, files, fs, tty, keyrings */
      spinlock_t alloc_lock;
    
      /* Protection of the PI data structures: */
      spinlock_t pi_lock;
    
    #ifdef CONFIG_RT_MUTEXES
      /* PI waiters blocked on a rt_mutex held by this task */
      struct plist_head pi_waiters;
      /* Deadlock detection and priority inheritance handling */
      struct rt_mutex_waiter *pi_blocked_on;
    #endif
    
    #ifdef CONFIG_DEBUG_MUTEXES
      /* mutex deadlock detection */
      struct mutex_waiter *blocked_on;
    #endif
    #ifdef CONFIG_TRACE_IRQFLAGS
      unsigned int irq_events;
      int hardirqs_enabled;
      unsigned long hardirq_enable_ip;
      unsigned int hardirq_enable_event;
      unsigned long hardirq_disable_ip;
      unsigned int hardirq_disable_event;
      int softirqs_enabled;
      unsigned long softirq_disable_ip;
      unsigned int softirq_disable_event;
      unsigned long softirq_enable_ip;
      unsigned int softirq_enable_event;
      int hardirq_context;
      int softirq_context;
    #endif
    #ifdef CONFIG_LOCKDEP
    # define MAX_LOCK_DEPTH 30UL
      u64 curr_chain_key;
      int lockdep_depth;
      struct held_lock held_locks[MAX_LOCK_DEPTH];
      unsigned int lockdep_recursion;
    #endif
    
    /* journalling filesystem info */
      void *journal_info;
    
    /* stacked block device info */
      struct bio *bio_list, **bio_tail;
    
    /* VM state */
      struct reclaim_state *reclaim_state;
    
      struct backing_dev_info *backing_dev_info;
    
      struct io_context *io_context;
    
      unsigned long ptrace_message;
      siginfo_t *last_siginfo; /* For ptrace use.  */
    #ifdef CONFIG_TASK_XACCT
    /* i/o counters(bytes read/written, #syscalls */
      u64 rchar, wchar, syscr, syscw;
    #endif
      struct task_io_accounting ioac;
    #if defined(CONFIG_TASK_XACCT)
      u64 acct_rss_mem1;  /* accumulated rss usage */
      u64 acct_vm_mem1; /* accumulated virtual memory usage */
      cputime_t acct_stimexpd;/* stime since last update */
    #endif
    #ifdef CONFIG_NUMA
        struct mempolicy *mempolicy;
      short il_next;
    #endif
    #ifdef CONFIG_CPUSETS
      nodemask_t mems_allowed;
      int cpuset_mems_generation;
      int cpuset_mem_spread_rotor;
    #endif
    #ifdef CONFIG_CGROUPS
      /* Control Group info protected by css_set_lock */
      struct css_set *cgroups;
      /* cg_list protected by css_set_lock and tsk->alloc_lock */
      struct list_head cg_list;
    #endif
    #ifdef CONFIG_FUTEX
      struct robust_list_head __user *robust_list;
    #ifdef CONFIG_COMPAT
      struct compat_robust_list_head __user *compat_robust_list;
    #endif
      struct list_head pi_state_list;
      struct futex_pi_state *pi_state_cache;
    #endif
      atomic_t fs_excl; /* holding fs exclusive resources */
      struct rcu_head rcu;
    
      /*
       * cache last used pipe for splice
       */
      struct pipe_inode_info *splice_pipe;
    #ifdef  CONFIG_TASK_DELAY_ACCT
      struct task_delay_info *delays;
    #endif
    #ifdef CONFIG_FAULT_INJECTION
      int make_it_fail;
    #endif
      struct prop_local_single dirties;
    }
    
  2. 命名空间
    1. 概念

      传统上,在linux以及其他衍生的unix变体中,很多资源是全局管理的。比如内核必须管理一个全局pid 列表来唯一标识一个进程。这种数据管理方式在有些情况下会有一些限制。如果提供云计算服务的供应商 打算向用户提供linux计算机的root权限,这种管理方式一般需要使用kvm或vmware提供的虚拟化环境, 但是这些方法的资源分配做的不是非常好。 而命名空间提供了一种不同的解决方案,所需的资源较少,它 只使用一个内核在一台物理机上运作,前面那种全局管理的资源都使用命名空间抽象隔离起来。 本质上,命名空间建立了系统的不同视图:


      1. 2020-11-04_21-28-51_screenshot.png

        如上图所示,系统上有3个命名空间,一个父命名空间和两个子命名空间,每个空间都有各自的1,2,3号 进程,而子命名空间在父命名空间也有其进程的映射,如左边的字命名空间的1号映射为父进程空间中的4号

    2. 创建

      有两种方法可以创建新的命名空间

      1. fork或clone
      2. unshare系统调用
    3. 实现

      命名空间的实现需要两个部分:

      1. 每个子系统的命名空间结构
      2. 将给定进程关联到所属各个命名空间的机制

      1. 2020-11-04_21-57-00_screenshot.png

  3. 进程ID
    1. 进程ID类型
      • PID
      • 线程组ID(TGID)
      • 进程组ID(GID) 简化了向一个组所有成员发送信号
      • 会话ID(SID)
    2. 支持命名空间的PID
      • 全局ID 内核本身和初始命名空间中的唯一ID号,每个ID都有一个唯一的全局ID
      • 局部ID 某个特定命名空间的ID,不同的命名空间可能不唯一
    3. 相关数据结构(图)

      2020-11-05_21-18-08_screenshot.png

      2020-11-05_21-24-54_screenshot.png

      本质上,这两张图就为了完成两件事情:

      1. 给出局部数字ID和对应的命名空间,找到对应的taskstruct 这是通过pidhash出发来完成的: pidhash->struct upid->(containerof机制)struct pid->taskstruct
      2. 给出taskstruct和命名空间,找upid taskstruct->struct pid->numbers[ns.level]->upid
    4. 创建一个pid的过程

      从建立进程的命名空间开始,一直到初始的全局命名空间,内核会为此间的每个命名空间分别创建一个 局部PID(struct upid), 并把upid全映射到pidhash中

  4. 进程管理相关系统调用
    1. 进程复制
      1. 进程复制主要的系统调用是fork和clone,vfork在fork使用了COW后速度不再有优势
        • COW(copy-on-write) 该技术利用了下述事实:进程通常只使用了内存页的一小部分。在调用fork时,一般来说内核需要 对父进程的每个内存页都为子进程创建一个副本。这有样有两个缺点:

          1. 使用了大量的内存
          2. 复制操作耗费了很长的时间

          COW机制使得内核可以尽可能延迟内存页的复制,更重要的是,在很多情况下根本不需要复制,这 节省了大量的内存和时间

        • fork, 创建子进程, 该子进程是父进程的完整副本

          asmlinkage int sys_fork(struct pt_regs regs)
          {
            return do_fork(SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
          }
          
        • clone 产生线程,可以对父子进程的共享、复制进行精确控制

          asmlinkage int sys_clone(struct pt_regs regs)
          {
            unsigned long clone_flags;
            unsigned long newsp;
            int __user *parent_tidptr, *child_tidptr;
          
            clone_flags = regs.ebx;
            newsp = regs.ecx;
            parent_tidptr = (int __user *)regs.edx;
            child_tidptr = (int __user *)regs.edi;
            if (!newsp)
              newsp = regs.esp;
            return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr);
          }
          
        • 从上面代码中看到,clone和fork的区别主要在于:
          1. clone的标志不再是硬编码的,而是通过各种寄存器把参数传递到系统调用,
          2. clone不再复制父进程的栈地址,而是可以指定新的栈地址。在创建线程时可能需要这样做,
      2. dofork
        • fork和clone处理完准备工作后都调用了dofork函数:
        /*
         *  Ok, this is the main fork-routine.
         *
         * It copies the process, and if successful kick-starts
         * it and waits for it to finish using the VM if required.
         */
        long do_fork(
                     // 控制复制过程中的一些属性,最低字节指定了在子进程终止时
                     // 发给父进程的信号
                     unsigned long clone_flags,
                     // 用户状态下栈的起始地址,必须保证父子进程栈地址一样
                     unsigned long stack_start,
                     // 调用参数
                     struct pt_regs *regs,
                     // 用户状态下栈的大小,通常为0
                     unsigned long stack_size,
                     int __user *parent_tidptr,
                     int __user *child_tidptr)
        
        1. 实现

          dofork流程图如下:

          1. dofork流程图

            2020-11-06_19-51-04_screenshot.png 其中,主要工作由copyprocess完成,copyprocess执行生成新进程的实际工作,同时根据指定的 标志重用父进程的数据,copyprocess的主要流程图如下

            1. copyprocess流程图

              2020-11-06_19-57-34_screenshot.png

              • 首先检查标志,特别是捕获一些没有意义的标志组合,比如CLONENEWNS和CLONEFS就不 能组合在一起;而有些标志是必须结合在一起用的,比如CLONETHREAD和CLONESIGHAND、 CLONEVM,因为创建线程必须激活信号共享,而且创建线程时也必须与父进程共享地址空间。
              • 在检查完标志集后,调用duptaskstruct来建立父进程taskstruct的副本。父子进程 的taskstruct实例只有一个成员不同:子进程分配了新的内核栈,即taskstruct->stack:

                union thread_union {
                  struct thread_info thread_info;
                  unsigned long stack[THREAD_SIZE/sizeof(long)];
                };
                

                内核栈大小默认为8K,也可配置成4K,threadinfo保存了特定与体系结构的汇编代码需要 访问的那部分进程数据:

                struct thread_info {
                  struct task_struct  *task;    /* main task structure */
                  struct exec_domain  *exec_domain; /* execution domain */
                  /*
                   * 我们只关注其中两个,因为其他都是硬件相关的:
                   * 1. 如果进程有待决信号则置位TIF_SIGPENDING
                   * 2. TIF_NEED_RESCHED表示该进程应该或想要调度器选择另一个进程替换本进程执行
                   */
                  __u32     flags;    /* low level flags */
                  /* 线程同步标志 */
                  __u32     status;   /* thread synchronous flags */
                  /* 当前cpu,翻译的有问题? */
                  __u32     cpu;    /* current CPU */
                  /* 实现内核抢占 */
                  int       preempt_count;  /* 0 => preemptable, <0 => BUG */
                
                  /* 进程可以使用的虚拟地址上限 */
                  mm_segment_t    addr_limit; 
                  /* 用于实现信号机制 */
                  struct restart_block    restart_block;
                };
                

                threadinfo布局图

              2020-11-06_20-44-21_screenshot.png

              • 构建完taskstruct后,内核会检查当前用户(taskstruct->user)已经创建的进程数 是否超出了限制,若超出,则放弃创建进程
              • 确定可以创建进程后,初始化一些子进程taskstruct的一些数据
              • 接下来会调用schedfork。本质上,该函数会初始化一些统计字段, 并均衡下调度器负载,并把子进程设置为TASKRUNNING(TODO:why?)
              • 再下来就是根据标志来复制或者共享父进程的资源: ❑ copysemundo uses the System V semaphores of the parent process if COPYSYSVSEM is set (see Chapter 5). ❑ copyfiles uses the file descriptors of the parent process if CLONEFILES is set. Otherwise, a new files structure is generated (see Chapter 8) that contains the same information as the parent process. This information can be modified independently of the original structure. ❑ copyfs uses the filesystem context (taskstruct->fs) of the parent process if CLONEFS is set. This is an fsstruct type structure that holds, for example, the root directory and the current working directory of the process (see Chapter 8 for detailed information). ❑ copysighand uses the signal handlers of the parent process (taskstruct->sighand) if CLONESIGHAND or CLONETHREAD is set. Chapter 5 discusses the struct sighandstruct structure used in more detail. ❑ copysignal uses the non-handler-specific part of signal handling (taskstruct->signal, see Chapter 5) together with the parent process if CLONETHREAD is set. ❑ copymm causes the parent process and child process to share the same address space if COPYMM is set. In this case, both processes use the same instance of mmstruct (see Chapter 4) to which taskstruct->mm points. If copymm is not set, it does not mean that the entire address space of the parent process is copied. The kernel does, in fact, create a copy of the page tables but does not copy the actual contents of the pages. This is done using the COW mechanism only if one of the two processes writes to one of the pages. ❑ copynamespaces has special call semantics. It is used to set up namespaces for the child process. Recall that several CLONENEWxyz flags control which namespaces are shared with the parent. However, the semantics are opposite to all other flags: If CLONENEWxyz is not specified, then the specific namespace is shared with the parent. Otherwise, a new namespace is generated. copynamespace is a dispatcher that executes a copy routine for each possible namespace. The individual copy routines, however, are not too interesting because they essentially copy data or make already existing instances shared by means of reference counter management, so I will not discuss their implementation in detail. ❑ copythread is — in contrast to all other copy operations discussed here — an architecture- specific function that copies the thread-specific data of a process.
              • 最后,填好子进程与父进程不同的各个成员,包括taskstruct包含的各个链表元素 cputimers,待决信号列表pending

              copyprocess执行完毕后,dofork还必须执行一些收尾工作,如前面的dofork流程图

              • TODO:
    2. 内核线程

      内核线程是直接由内核本身启动的进程。它们并行于内核的执行,通常称为守护进程。

      1. 分类

        内核线程基本可以分为两类

        1. 启动后自动按特定周期间隔运行,检测特定资源的使用情况,超出或低于时采取行动
        2. 只有内核请求时才执行
      2. 主要任务

        内核线程主要用于执行以下任务:

        • 周期性地将修改的内存页与页来源块设备同步
        • 将很少使用的内存页写入交换区
        • 管理延时动作
        • 实现文件系统的事务日志
      3. 创建
        • 创建内核线程的传统方法是kernelthread函数
        long kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
        {
          struct pt_regs regs;
        
          memset(&regs, 0, sizeof(regs));
        
          regs.regs[4] = (unsigned long) arg;
          regs.regs[5] = (unsigned long) fn;
          regs.cp0_epc = (unsigned long) kernel_thread_helper;
          regs.cp0_status = read_c0_status();
        #if defined(CONFIG_CPU_R3000) || defined(CONFIG_CPU_TX39XX)
          regs.cp0_status = (regs.cp0_status & ~(ST0_KUP | ST0_IEP | ST0_IEC)) |
            ((regs.cp0_status & (ST0_KUC | ST0_IEC)) << 2);
        #else
          regs.cp0_status |= ST0_EXL;
        #endif
        
          /* Ok, create the new process.. */
          return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
        
        

        其中,fn函数负责帮助内核调用daemonize以转换为守护进程。步骤如下:

        1. 该函数从内核线程释放其父进程(用户进程)的所有资源(内存上下文,文件描述符),因为 不然的话这些资源会锁定到线程结束(通常时运行到关机)。而线程又不需要这些资源
        2. daemonize阻塞信号的接受
        3. 将init作为守护线程的父进程
        • 更现代的方法是辅助函数kthreadcreate,kthreadrun和kthreadcreatecpu(可指定cpu)

          struct task_struct *kthread_create(int (*threadfn)(void *data),
                                             void *data,
                                             const char namefmt[],
                                             ...)
          {
            struct kthread_create_info create;
          
            create.threadfn = threadfn;
            create.data = data;
            init_completion(&create.started);
            init_completion(&create.done);
          
            spin_lock(&kthread_create_lock);
            list_add_tail(&create.list, &kthread_create_list);
            wake_up_process(kthreadd_task);
            spin_unlock(&kthread_create_lock);
          
            wait_for_completion(&create.done);
          
            if (!IS_ERR(create.result)) {
              va_list args;
              va_start(args, namefmt);
              vsnprintf(create.result->comm, sizeof(create.result->comm),
                        namefmt, args);
              va_end(args);
            }
            return create.result;
          }
          
    3. 启动新程序
      • execve

      linux提供execve系统调用来通过新代码替换现存程序启动新程序,该系统调用的 入口是sysexecve函数,该函数很快将工作委托给doexecve例程

      int do_execve(char * filename,
                    char __user *__user *argv,
                    char __user *__user *envp,
                    struct pt_regs * regs)
      

      其代码流程图如下:

      2020-11-07_20-20-55_screenshot.png

      1. 首先,内核找到要执行文件的inode并生成一个文件描述符来打开此文件;
      2. 接下来bprminit处理一些管理任务,包括:
        • mmalloc生成新的mmstruct地址空间
        • initnewcontext初始化mmstruct
        • _bprmmminit建立初始的栈
      3. 然后,preparebinprm用于提供一些linuxbinprm结构中父进程相关的值(特别是有效UID和GID)。 其中,linuxbinprm结构保存的是新进程的各个参数
      4. 接下来就是复制环境和参数数组的内容
      5. 最后,searchbinaryhandler在doexecve结束时根据所要执行的文件查找适当的二进制格式, 如果找到合适的格式,则相应的二进制处理程序负责将新程序的数据加载到旧的地址空间中(TODO 那mmstruct有什么用),二进制格式用下面的结构体描述:

        /*
         * This structure defines the functions that are used to load the binary formats that
         * linux accepts.
         */
          struct linux_binfmt {
            struct list_head lh;
            struct module *module;
            // 加载普通程序
            int (*load_binary)(struct linux_binprm *, struct  pt_regs * regs);
            // 加载共享库,即动态库
            int (*load_shlib)(struct file *);
            // 输出内存转储
            int (*core_dump)(long signr, struct pt_regs *regs, struct file *file, unsigned long limit);
            unsigned long min_coredump; /* minimal dump size */
            int hasvdso;
          };
        
    4. TODO 退出程序
  5. 调度器
    1. 概览
      • 调度器主要完成两件事
        1. 调度策略
        2. 切换上下文
      • 就绪队列 调度器每次调度时,会挑选具有最高等待时间的进程,把CPU提供给该进程。调度器记录了每个进程已经 等待的时长并用红黑树从大到小排序,由于可运行进程是排队的,该结构称之为就绪队列:

      2020-11-08_22-13-35_screenshot.png

      • 虚拟时钟 就绪队列中还有个虚拟时钟的概念,它表示的是就绪队列中的每个进程*平均*推进(分配到)的时间的度量
    2. 数据结构
      • 调度器系统概观 调度器使用一系列数据结构来完成器调度任务,他的各组件关系如下:

      2020-11-08_22-31-56_screenshot.png 可以看到,图中主要由两个调度器组成:

      1. 主调度器 主要用于进程打算睡眠或出于其他原因放弃CPU
      2. 周期性调度器 以固定的频率周期性运行,检测是否有必要进行进程切换
      • 调度器类 调度器通过查询调度器类来选择进程运行,调度器类用于判断接下来运行哪个进程,而且不同的类 具有不同的调度策略,每个进程都属于一个调度器类
      • taskstruct中有关成员

        struct task_struct {
        ...
        /*
         * static_prio: 静态优先级进程启动时分配,进程运行期间保持恒定,除非用nice/sched_setscheduler
         * 系统调用修改
         * normal_prio: 普通优先级。基于进程的静态优先级和调度策略计算而来,所以即使实时进程和普通进程
         * 的静态优先级一样,他们的普通优先级页不同
         * prio: 动态优先级。调度器直接用的优先级,由于某些情况内核需要暂时提高进程的优先级,因此需要这
         * 第三个成员来表示
         */
          int prio, static_prio, normal_prio;
          /* 实时进程的优先级, 0-99, 值越大优先级越高 */
          unsigned int rt_priority; 
          struct list_head run_list;
          /* 所属调度器类 */
          const struct sched_class *sched_class;
          /* 调度实体 */
          struct sched_entity se; 
          /* 调度策略 */
          unsigned int policy;
          cpumask_t cpus_allowed;
          unsigned int time_slice;
        ...
        }
        
        1. 优先级

          • 用户空间 进程在用户空间表示为-20到+19(包含),值越低,优先级越高
          • 内核表示 内核使用0到139来表示内部优先级,nice值映射到范围100到139,0到99专用于实时进程, 也是值越低优先级越高:

            2020-11-08_23-44-58_screenshot.png 三个优先级中,静态优先级由用户设置好,其他两个则由内核计算得来,计算的时机是

            1. 新建进程用wakeupnewtask唤醒时
            2. 调用nice系统调用改变静态优先级时

            计算过程如下表:

          2020-11-09_00-04-11_screenshot.png

        2. 调度策略 linux支持5种调度策略,
          • 映射到完全公平调度器类:SCHEDNORMAL􏲹SCHEDBATCH􏰓SCHEDIDLE
          • 映射到实时调度器类:SCHEDRR􏰓SCHEDFIFO
        3. 调度器类 调度的实际工作主要由具体的调度器类来执行,调度器类可以提供以下操作:

          struct sched_class {
            const struct sched_class *next;
            /* 向就绪队列添加一个进程,当进程从睡眠状态变为可执行状态时发生该操作 */
            void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup);
            /* enqueue_task的逆向操作, */
            void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep);
            /* 进程调用sched_yield主动放弃处理器时,内核会调用此函数 */
            void (*yield_task) (struct rq *rq);
            /* 用新唤醒的进程来抢占当前进程,如wake_up_new_task唤醒新进程时会调用 */
            void (*check_preempt_curr) (struct rq *rq, struct task_struct *p);
            /* 选择下一个要运行的进程,需要执行上下文切换 */
            struct task_struct * (*pick_next_task) (struct rq *rq);
            void (*put_prev_task) (struct rq *rq, struct task_struct *p);
            void (*set_curr_task) (struct rq *rq);
            void (*task_tick) (struct rq *rq, struct task_struct *p);
            /* 建立fork与调度器直接的关联 */
            void (*task_new) (struct rq *rq, struct task_struct *p);
          };
          
      • 就绪队列 内核为每个CPU维护一个就绪队列runqueues,这是调度器最重要的数据结构,每个进程都出现在某一个队列中。

        static DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
        

        这个就绪队列并不直接管理进程,而是把特定于调度器类的子就绪队列嵌入它的结构体中:

        /*
         * This is the main, per-CPU runqueue data structure.
         *
         * Locking rule: those places that want to lock multiple runqueues
         * (such as the load balancing or the thread migration code), lock
         * acquire operations must be ordered by ascending &runqueue.
         */
        struct rq {
          /* runqueue lock: */
          spinlock_t lock;
        
          /*
           * nr_running and cpu_load should be in the same cacheline because
           * remote CPUs use both these fields when doing load calculation.
           * 队列上可运行的进程数目
           */
          unsigned long nr_running;
        #define CPU_LOAD_IDX_MAX 5
          /* 该队列的cpu之前的负荷状态 */
          unsigned long cpu_load[CPU_LOAD_IDX_MAX];
          struct load_weight load;
          struct cfs_rq cfs;
        #ifdef CONFIG_FAIR_GROUP_SCHED
          /* list of leaf cfs_rq on this cpu: */
          struct list_head leaf_cfs_rq_list;
        #endif
          struct rt_rq rt;
        
          /*
           * This is part of a global counter where only the total sum
           * over all CPUs matters. A task can increase this counter on
           * one CPU and if it got migrated afterwards it may decrease
           * it on another CPU. Always updated under the runqueue lock:
           */
          unsigned long nr_uninterruptible;
        
          struct task_struct *curr, *idle;
          /* 该就绪队列自身的时钟,每次调用周期性调度器都会更新clock */
          u64 clock, prev_clock_raw;
        ...
        }
        
      • 调度实体 之前一直说的是调度器调度的是进程,其实这是一种简化的说法,其实每个进程的taskstruct中 都嵌入了一个schedentity结构, 所以调度器调度的不是进程,而是这个被称为调度实体的更一般 化的概念,其定义如下:

        struct sched_entity {
          struct load_weight load; /* 用于负载均衡*/
          struct rb_node run_node;
          unsigned int on_rq;
          u64 exec_start;
          u64 sum_exec_runtime;
          u64 vruntime;
          u64 prev_sum_exec_runtime;
        ...
        }
        
      • 负荷权重 进程的重要性不是直接由优先级(nice)指定的,而是要由优先级和进程调度策略计算得来的 taskstruct->se.load的负荷权重。具体计算概念是:进程没降低一个nice值,就多获得10%的CPU 时间,美升高一个nice值就放弃10%的CPU时间。具体对应关系为:

        2020-11-09_09-34-21_screenshot.png

    3. 两个调度器的调度过程
      • 周期性调度器 周期性调度器在schedulertick中实现。如果系统正在活动中,内核会按照频率HZ自动调用该函数 它主要完成两个任务:

        1. 管理内核中与整个系统和各个进程的调度相关的统计量

        void scheduler_tick(void)
        {
          1. 管理内核中与整个系统和各个进程的调度相关的统计量
          int cpu = smp_processor_id();
          struct rq *rq = cpu_rq(cpu);
          struct task_struct *curr = rq->curr;
        
        ...
            /*更新当前cpu strut rq的时钟时间戳*/
          __update_rq_clock(rq)
          /* 更新cpu load[]数组 */
            update_cpu_load(rq);
        
        
         2. 激活负责当前进程的调度器类的周期性调度方法scheduler_tick
        /*
         * task_tick在当前进程需要被重新调度时,设置该进程的
         * TIF_NEED_RESCHED标志,内核会在返回用户态前检查该
         * 标志,如果该标志置位了,调用主调度器的schedule()函数
         */
         if (curr != rq->idle)
           curr->sched_class->task_tick(rq, curr);
        }
        
      • 主调度器 内核中有许多地方会调用主调度器函数schedule():

        1. 从系统调用返回用户态前链接
        2. 进程主动放弃cpu使用权

        他的主要流程为:

        asmlinkage void __sched schedule(void)
        {
          struct task_struct *prev, *next;
          struct rq *rq;
          int cpu;
         need_resched:
          cpu = smp_processor_id();
          rq = cpu_rq(cpu);
          prev = rq->curr;
        ...
         __update_rq_clock(rq);
         clear_tsk_need_resched(prev);
        ...
         TODO  /* TASK_INTERRUPTIBLE 的进程为什么能是cur进程 */
         if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
           if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
             unlikely(signal_pending(prev)))) {
             prev->state = TASK_RUNNING;
           } else {
             deactivate_task(rq, prev, 1);
           }
           switch_count = &prev->nvcsw;
         }
        
        ...
          prev->sched_class->put_prev_task(rq, prev);
          next = pick_next_task(rq, prev);
        ...
            if (likely(prev != next)) {
              rq->nr_switches++;
              rq->curr = next;
              ++*switch_count;
        
              context_switch(rq, prev, next); /* unlocks the rq */
            }
        }
        

        当需要切换进程时,调用contextswitch():

        /*
         * context_switch - switch to the new MM and the new
         * thread's register state.
         */
        static inline void
        context_switch(struct rq *rq, struct task_struct *prev,
                 struct task_struct *next)
        {
          struct mm_struct *mm, *oldmm;
        
          prepare_task_switch(rq, prev, next);
          mm = next->mm;
          oldmm = prev->active_mm;
          /*
           * For paravirt, this is coupled with an exit in switch_to to
           * combine the page table reload and the switch backend into
           * one hypercall.
           */
          arch_enter_lazy_cpu_mode();
        
          if (unlikely(!mm)) {
            /* 内核线程没有用户空间内存上下文,会借用prev的地址空间, */
            next->active_mm = oldmm;
            atomic_inc(&oldmm->mm_count);
            /* 惰性 TLB */
            enter_lazy_tlb(oldmm, next);
          } else
            1./* 更换内存管理上下文 */
            switch_mm(oldmm, mm, next);
        
          if (unlikely(!prev->mm)) {
            prev->active_mm = NULL;
            rq->prev_mm = oldmm;
          }
          /*
           * Since the runqueue lock will be released by the next
           * task (which is an invalid locking op but in the case
           * of the scheduler it's an obvious special-case), so we
           * do an early lockdep release here:
           */
        #ifndef __ARCH_WANT_UNLOCKED_CTXSW
          spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
        #endif
        
          2./* 切换处理器寄存器内容和内核栈 */
          /* Here we just switch the register state and the stack. */
          switch_to(prev, next, prev);
        
          barrier();
          3./*
           * this_rq must be evaluated again because prev may have moved
           * CPUs since it called schedule(), thus the 'rq' on its stack
           * frame will be invalid.
           */
          finish_task_switch(this_rq(), prev);
        }
        

        其中,switchto比较复杂,如下图:

        2020-11-09_14-14-47_screenshot.png 如图中右上角所示,在每个switchto调用之前,next和prev指针位于 当前进程的内核栈 上, prev指向当前运行的进程,而next指向将要运行的下一个进程,如图中第一个A所示,但是,经过 几次切换然后控制权再返回至switchto之后,也就是第二个A的状态,如果不做特殊处理, 那么prev和next应该和第一个图一样,next=B,prev=A,而实际上上一个运行的进程是图中的 C。所以,switchto做了特殊处理,使得prev=C,而不是A,如图右上角所示。

    4. 完全公平调度CFS
      • 数据结构

        struct cfs_rq {
          /* 当前队列所有进程的负荷值 */
          struct load_weight load;
          /* 可运行进程数 */
          unsigned long nr_running;
        
          /* 队列上所有进程的最小虚拟运行时间 */
          u64 min_vruntime;
        
          /* 按时间排序进程的红黑树 */
          struct rb_root tasks_timeline;
          struct rb_node *rb_leftmost;
        
          /* 当前执行进程的可调度实体 */
          struct sched_entity *curr
        }
        
      • 调度算法
        1. 虚拟时钟(时间) CFS并没有直接保存虚拟时钟,而是每次调度器运行时都计算一次,相关函数为updatecurr

          static void update_curr(struct cfs_rq *cfs_rq)
          {
            /* 当前运行进程*/
            struct sched_entity *curr = cfs_rq->curr;
            /* 当前cpu等待队列中获取当前实际时钟,每次调度都会更新 */
            u64 now = rq_of(cfs_rq)->clock;
            unsigned long delta_exec;
          
            /* 无事可做,退出 */
            if (unlikely(!curr))
              return;
          
            /*
             * Get the amount of time the current task was running
             * since the last time we changed load (this cannot
             * overflow on 32 bits):
             */
            /* exec_start为进程这轮调度运行的开始时间 */
            delta_exec = (unsigned long)(now - curr->exec_start);
          
            /* 更新当前进程在CPU上执行花费的物理时间和虚拟时间 */
            __update_curr(cfs_rq, curr, delta_exec);
            /* 更新进程运行开始时间 */
            curr->exec_start = now;
          
            if (entity_is_task(curr)) {
              struct task_struct *curtask = task_of(curr);
          
              cpuacct_charge(curtask, delta_exec);
            }
          }
          
          /*
           * Update the current task's runtime statistics. Skip current tasks that
           * are not in our scheduling class.
           */
          static inline void
          __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
                  unsigned long delta_exec)
          {
            unsigned long delta_exec_weighted;
            u64 vruntime;
          
            schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
          
            /* 进程物理时钟 */
            curr->sum_exec_runtime += delta_exec;
            schedstat_add(cfs_rq, exec_clock, delta_exec);
            delta_exec_weighted = delta_exec;
            /* 公式: delta_exec_weighted = delta_exec * NICE_0_LOAD / curr->load.weight*/
            if (unlikely(curr->load.weight != NICE_0_LOAD)) {
              delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
                        &curr->load);
            }
            /* 虚拟时钟*/
            curr->vruntime += delta_exec_weighted;
          
            /*
             * maintain cfs_rq->min_vruntime to be a monotonic increasing
             * value tracking the leftmost vruntime in the tree.
             */
            /* 如果等待树上最左边有进程在等待,取当前进程与这个最左等待进程的虚拟时间的最小值 */
            if (first_fair(cfs_rq)) {
              vruntime = min_vruntime(curr->vruntime,
                  __pick_next_entity(cfs_rq)->vruntime);
            } else
              /* 树上没有进程等待的话,最小值就直接等于当前进程的虚拟时间 */
              vruntime = curr->vruntime;
          
            /* 每个队列的min_vruntime只有被所有进程超出时才更新,确保min_vruntime单调增加 */
            cfs_rq->min_vruntime =
              max_vruntime(cfs_rq->min_vruntime, vruntime);
          }
          

          代码流程图为:

          2020-11-10_09-40-30_screenshot.png

          • 关键思想 虚拟时间在完全公平调度器中的关键点是,红黑树的排序过程按照下面的键来排序:

            static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se)
            {
              return se->vruntime - cfs_rq->min_vruntime;
            }
            

            键值越小,排序位置就越靠左,因此会被更快地调度,所以进程在树上的移动规则为:

            1. 进程运行时,它的vruntime一定会增加,所以在树中会向右移动 而且优先级值越小(重要性越高)的进程vruntime增加越慢,因此向右移动的速度也越慢 被调度的机会就要大于次要进程,也就是能分配更多的时间
            2. 如果进程进入睡眠,那么它就会从就绪队列中脱离出来,其vruntime保持不变。因为每个队列的 minvruntime会单调增加为等待队列中进程虚拟时间的最小值,当进程再次醒来进入等待队列 的时候,导致进程虚拟时间vruntime小于等待队列minvruntime,那么它在红黑树中的位置 会更靠左,因为其键值变得更小了。由此也可以看出,进程的vruntime是有可能小于队列的 minvruntime的.
        2. 延迟跟踪 几个重要的时间控制参数变量:

          • 进程每次调度的保证最小运行时间 sysctlschedmingranularity /proc/sys/kernel/schedmingranularityns间接控制,默认为4毫秒.
          • 延迟数 schednrlatency 控制一个周期中处理的最大活动进程数目,如果超出这个限制,那么延迟周期也成比例的线性扩展, 可以通过sysctlschedmingranularity间接控制
          • sysctlschedlatency : 保证每个可运行进程都至少运行一次的时间间隔,可以通过/proc/sys/kernel/schedlatencyns 控制默认为20毫秒。每次前两个之一改变,都会从新计算:

            _schedperoid确定延迟周期的长度,通常来说就是sysschedlatency,但如果延迟数超了的话, 改值就会线性扩展:sysctlschedlatency * nrrunning / schednrlatency。 一个延迟周期 会根据进程的 相对 权重在进程间分配: 物理时间:

            2020-11-11_19-11-39_screenshot.png 虚拟时间:

          2020-11-11_20-56-12_screenshot.png

          可以看到,每个进程在一个延迟周期中分配到的虚拟时间是一样的.

        3. 加入队列

          static void
          place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
          {
            u64 vruntime;
          
            vruntime = cfs_rq->min_vruntime;
          
            if (sched_feat(TREE_AVG)) {
              struct sched_entity *last = __pick_last_entity(cfs_rq);
              if (last) {
                vruntime += last->vruntime;
                vruntime >>= 1;
              }
            } else if (sched_feat(APPROX_AVG) && cfs_rq->nr_running)
              vruntime += sched_vslice(cfs_rq)/2;
          
            /*
             * The 'current' period is already promised to the current tasks,
             * however the extra weight of the new task will slow them down a
             * little, place the new task so that it fits in the slot that
             * stays open at the end.
             */
            /* initial表示se是新进程,如果se是新进程,se_vruntime加一个时间段,表示
            从下一个时间周期才开始调度这个新进程*/
            if (initial && sched_feat(START_DEBIT))
              vruntime += sched_vslice_add(cfs_rq, se);
          
            if (!initial) {
              /*如果se不是新进程,那么为了补偿它的睡眠,给他的vruntime-20ms,使得它能更快(马上?)被调度*/
              /* sleeps upto a single latency don't count. */
              if (sched_feat(NEW_FAIR_SLEEPERS) && entity_is_task(se))
                vruntime -= sysctl_sched_latency;
          
              /* ensure we never gain time by being placed backwards. */
              vruntime = max_vruntime(se->vruntime, vruntime);
            }
          
            se->vruntime = vruntime;
          }
          
        4. 选择下一个进程

          static struct task_struct *pick_next_task_fair(struct rq *rq)
          {
            struct cfs_rq *cfs_rq = &rq->cfs;
            struct sched_entity *se;
          
            if (unlikely(!cfs_rq->nr_running))
              return NULL;
          
            do {
              se = pick_next_entity(cfs_rq);
              cfs_rq = group_cfs_rq(se);
            } while (cfs_rq);
          
            return task_of(se);
          }
          
          static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
          {
            struct sched_entity *se = NULL;
          
            if (first_fair(cfs_rq)) {
              se = __pick_next_entity(cfs_rq);
              /* 将当前执行进程退队,并检查是不是超过该时间周期可以运行的时间 */
              set_next_entity(cfs_rq, se);
            }
          
            return se;
          }
          
        5. 处理周期调度调度器
        6. 唤醒抢占
        7. 实时调度器类
        8. TODO 主调度器和周期调度器的整个流程

3.9.2 内存管理

  1. 概述

    内存管理是内核中最复杂同时也是最重要的一部分,涵盖了许多领域:

    1. 内存中物理内存的管理
    2. 分配大块内存的伙伴系统
    3. 分配较小块内存的slab、slub和slob分配器
    4. 分配非连续内存块的vmalloc机制
    5. 进程的地址空间

    按管理内存方法的不同,可以将计算机分为两类:

    1. UMA计算机(一致内存访问 uniform memory access),内存以连续的方式组织起来, 特点是每个处理器访问每个内存区 都是同样快
    2. NUMA计算机。每个CPU都有本地内存,可支持快速的访问。每个CPU也可以访问其他CPU的本地内存, 但是速度会慢一些

    2020-11-12_18-37-39_screenshot.png

    两种计算机都可以有不同的内存布局:

    2020-11-12_18-39-18_screenshot.png 简单描述就是:UMA内存模型除了可以是一个NUMA的内存模型(一般内存有比较大的洞时配置)以外,还可以 配置为平坦内存模型

    我们主要集中于理解UMA的平坦内存模型,因为一般的计算机都是这种模型

    • 分配阶 内存区中页的数分配目的度量,阶0为20 = 1, 阶1的分配包括21 = 2…
  2. 内存组织
    1. 概述
      • NUMA linux把内存分为有层次的几个概念。
        1. 首先,内存被分为多个节点,每个节点关联到一个处理器,在,内核中表示为pgdatat
        2. 节点又被分为多个内存域
        3. 内存域关联了一个struct page数组,用来组织该内存域中的物理内存页(页帧)

      2020-11-12_19-11-35_screenshot.png

      • UMA UMA可以看作是只有一个节点的NUMA(不是平坦内存模型时应该看作多个节点)
    2. 数据结构
      1. 节点

        typedef struct pglist_data {
          /* 节点中各个内存域的数据结构 */
          struct zone node_zones[MAX_NR_ZONES];
          /* 备用节点及其内存域的列表 */
          struct zonelist node_zonelists[MAX_ZONELISTS];
          /* 节点内存域数目 */
          int nr_zones;
        #ifdef CONFIG_FLAT_NODE_MEM_MAP
          /* page实例数组的指针,用于描述所有物理内存页 */
          struct page *node_mem_map;
        #endif
          /* 自举分配器的实例 */
          struct bootmem_data *bdata;
        #ifdef CONFIG_MEMORY_HOTPLUG
          /*
           * Must be held any time you expect node_start_pfn, node_present_pages
           * or node_spanned_pages stay constant.  Holding this will also
           * guarantee that any pfn_valid() stays that way.
           *
           * Nests above zone->lock and zone->size_seqlock.
           */
          spinlock_t node_size_lock;
        #endif
          /* 第一个页帧的逻辑编号,全局唯一 */
          unsigned long node_start_pfn;
          unsigned long node_present_pages; /* total number of physical pages */
          unsigned long node_spanned_pages; /* total size of physical page
                                               range, including holes */
          /* 节点全局ID */
          int node_id;
          /* 交换守护进程的等待队列 */
          wait_queue_head_t kswapd_wait;
          /* 负责该节点的交互守护进程 */
          struct task_struct *kswapd;
          int kswapd_max_order;
        } pg_data_t;
        
      2. 内存域

        struct zone {
          /* Fields commonly accessed by the page allocator */
          unsigned long   pages_min, pages_low, pages_high; /* 水印,用于页换出 */
          /*
           * We don't know if the memory that we're going to allocate will be freeable
           * or/and it will be released eventually, so to avoid totally wasting several
           * GB of ram we must reserve some of the lower zone memory (otherwise we risk
           * to run OOM on the lower zones despite there's tons of freeable ram
           * on the higher zones). This array is recalculated at runtime if the
           * sysctl_lowmem_reserve_ratio sysctl changes.
           */
          unsigned long   lowmem_reserve[MAX_NR_ZONES]; /* 用于无论如何都不能失败的关键内存分配 */
        
        #ifdef CONFIG_NUMA
          int node;
          /*
           * zone reclaim becomes active if more unmapped pages exist.
           */
          unsigned long   min_unmapped_pages;
          unsigned long   min_slab_pages;
          struct per_cpu_pageset  *pageset[NR_CPUS];
        #else
          struct per_cpu_pageset  pageset[NR_CPUS]; /* 每个CPU的冷热页帧列表 */
        #endif
          /*
           * free areas of different sizes
           */
          spinlock_t    lock;
        #ifdef CONFIG_MEMORY_HOTPLUG
          /* see spanned/present_pages for more description */
          seqlock_t   span_seqlock;
        #endif
          struct free_area  free_area[MAX_ORDER]; /* 用于实现伙伴系统 */
        
        #ifndef CONFIG_SPARSEMEM
          /*
           * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
           * In SPARSEMEM, this map is stored in struct mem_section
           */
          unsigned long   *pageblock_flags;
        #endif /* CONFIG_SPARSEMEM */
        
        
          ZONE_PADDING(_pad1_)
        
          /* Fields commonly accessed by the page reclaim scanner */
          spinlock_t    lru_lock; 
          struct list_head  active_list;  /* 活动页 */
          struct list_head  inactive_list; /* 不活动页 */
          unsigned long   nr_scan_active;
          unsigned long   nr_scan_inactive;
          unsigned long   pages_scanned;     /* since last reclaim */
          unsigned long   flags;       /* zone flags, see below */
        
          /* Zone statistics */
          atomic_long_t   vm_stat[NR_VM_ZONE_STAT_ITEMS]; /* 统计信息 */
        
          /*
           * prev_priority holds the scanning priority for this zone.  It is
           * defined as the scanning priority at which we achieved our reclaim
           * target at the previous try_to_free_pages() or balance_pgdat()
           * invokation.
           *
           * We use prev_priority as a measure of how much stress page reclaim is
           * under - it drives the swappiness decision: whether to unmap mapped
           * pages.
           *
           * Access to both this field is quite racy even on uniprocessor.  But
           * it is expected to average out OK.
           */
          int prev_priority; /* 扫描优先级 */
        
        
          ZONE_PADDING(_pad2_)
          /* Rarely used or read-mostly fields */
        
          /*
           * wait_table   -- the array holding the hash table
           * wait_table_hash_nr_entries -- the size of the hash table array
           * wait_table_bits  -- wait_table_size == (1 << wait_table_bits)
           *
           * The purpose of all these is to keep track of the people
           * waiting for a page to become available and make them
           * runnable again when possible. The trouble is that this
           * consumes a lot of space, especially when so few things
           * wait on pages at a given time. So instead of using
           * per-page waitqueues, we use a waitqueue hash table.
           *
           * The bucket discipline is to sleep on the same queue when
           * colliding and wake all in that wait queue when removing.
           * When something wakes, it must check to be sure its page is
           * truly available, a la thundering herd. The cost of a
           * collision is great, but given the expected load of the
           * table, they should be so rare as to be outweighed by the
           * benefits from the saved space.
           *
           * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the
           * primary users of these fields, and in mm/page_alloc.c
           * free_area_init_core() performs the initialization of them.
           */
          /* 实现一个等待队列,供等待某一页变为可用的进程使用 */
          wait_queue_head_t * wait_table;
          unsigned long   wait_table_hash_nr_entries;
          unsigned long   wait_table_bits;
        
          /*
           * Discontig memory support fields.
           */
          struct pglist_data  *zone_pgdat; /* 内存域所在节点 */
          /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
          unsigned long   zone_start_pfn;  /* 内存域中第一个页帧的索引 */
        
          /*
           * zone_start_pfn, spanned_pages and present_pages are all
           * protected by span_seqlock.  It is a seqlock because it has
           * to be read outside of zone->lock, and it is done in the main
           * allocator path.  But, it is written quite infrequently.
           *
           * The lock is declared along with zone->lock because it is
           * frequently read in proximity to zone->lock.  It's good to
           * give them a chance of being in the same cacheline.
           */
          unsigned long   spanned_pages;  /* total size, including holes */
          unsigned long   present_pages;  /* amount of memory (excluding holes) */
        
          /*
           * rarely used fields:
           */
          const char    *name;
        } ____cacheline_internodealigned_in_smp;
        

        该结构被访问的非常频繁,而且可能同时被多个处理器访问,所以结构体中有两个自旋锁。 为了确保每个自旋锁都在自己的缓存行中,该结构由ZONEPADDING分隔为几个部分,

      3. TODO 水印的计算
      4. 冷热页 内核说的页是热的,意味着页已经加载到CPU高速缓存。其数据结构如下:

        struct zone {
        ...
        struct per_cpu_pageset pageset[NR_CPUS];
        ...
        };
        
        
        struct per_cpu_pageset {
          struct per_cpu_pages pcp[2]; /* 0对于热页,1对应冷页 */
        } ____cacheline_aligned_in_smp;
        
        
        
        struct per_cpu_pages {
          int count;    /* number of pages in the list */
          int high;   /* high watermark, emptying needed */
          int batch;    /* chunk size for buddy add/remove */
          struct list_head list;  /* the list of pages */
        }
        

        2020-11-12_20-21-59_screenshot.png

      5. 页帧 页帧代表系统内存的最小单位,对于内存中的每个页都会创建struct page的一个实例。所以 该结构应该尽可能小以节约内存

        struct page {
          unsigned long flags;    /* Atomic flags, some possibly
                   * updated asynchronously */
          atomic_t _count;    /* Usage count, see below. */
          union {
            atomic_t _mapcount; /* Count of ptes mapped in mms,
                   * to show when page is mapped
                   * & limit reverse map searches.
                   */
            unsigned int inuse; /* SLUB: Nr of objects */
          };
          union {
              struct {
            unsigned long private;    /* Mapping-private opaque data:
                     * usually used for buffer_heads
                     * if PagePrivate set; used for
                     * swp_entry_t if PageSwapCache;
                     * indicates order in the buddy
                     * system if PG_buddy is set.
                     */
            struct address_space *mapping;  /* If low bit clear, points to
                     * inode address_space, or NULL.
                     * If page mapped as anonymous
                     * memory, low bit is set, and
                     * it points to anon_vma object:
                     * see PAGE_MAPPING_ANON below.
                     */
              };
        #if NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS
              spinlock_t ptl;
        #endif
              struct kmem_cache *slab;  /* SLUB: Pointer to slab */
              struct page *first_page;  /* Compound tail pages */
          };
          union {
            pgoff_t index;    /* Our offset within mapping. */
            void *freelist;   /* SLUB: freelist req. slab lock */
          };
          struct list_head lru;   /* Pageout list, eg. active_list
                   * protected by zone->lru_lock !
                   */
          /*
           * On machines where all RAM is mapped into kernel address space,
           * we can simply calculate the virtual address. On machines with
           * highmem some memory is mapped into kernel virtual memory
           * dynamically, so we need a place to store that address.
           * Note that this field could be 16 bits on x86 ... ;)
           *
           * Architectures with slow multiplication can define
           * WANT_PAGE_VIRTUAL in asm/page.h
           */
        #if defined(WANT_PAGE_VIRTUAL)
          void *virtual;      /* Kernel virtual address (NULL if
                     not kmapped, ie. highmem) */
        #endif /* WANT_PAGE_VIRTUAL */
        };
        
  3. TODO 页表
  4. 初始化内存管理

    linux内存的初始化过程,首先会执行一些特定于处理器的操作,这些操作主要用于确定系统中内存的总数量, 及其在各个节点和内存域之间的分配情况。之后是关键的pgdatat数据结构的初始化

    1. 建立数据结构

      对于数据结构的初始化从全局启动例程startkernel开始的,它在加载内核并激活各个子系统之后执行。 其代码流程如下:

      2020-11-13_17-48-56_screenshot.png

      • setuparch:负责初始化自举分配器
      • setpercpuareas: 初始化静态per-cpu变量
      • buildallzonelists: 建立节点和内存域的数据结构 主要是建立节点的备用列表,用于在节点本身没有空闲空间时按先后顺序在备用列表中分配:

        2020-11-13_18-20-30_screenshot.png 其中,

      2020-11-13_18-22-03_screenshot.png

      • meminit:停用bootmem分配器并迁移到实际的内存管理函数
      • kmemcacheinit:初始化内核内部用于小块内存区的分配器
      • setpercpupageset:

        1. 内核的内存布局 我们只考虑默认情况,即内核被装载到物理内存中的一个固定位置。当然这也是可以配置的。 内核占据的内存大概分为几个段:

          2020-11-13_18-43-14_screenshot.png 下面是linux在I-32下的布局:

          2020-11-13_18-44-09_screenshot.png 这里内核从第一兆字节开始, AMD64下则会从第二兆字节开始

        2. 在内核已经载入内存在内存管理方面后会执行一些特定于系统的步骤:

          2020-11-13_18-47-39_screenshot.png

          • machinespecificmemorysetup: 创建一个包括系统占用内存区和空闲内存区的的列表,会用到BIOS提供的信息
          • parseearlyparam: 分析命令行,用于手工划分内存区(因为BIOS提供的值可能不正确)
          • setupmemory:
            1. 确定每个结点可用的物理内存数目
            2. 初始化bootmem分配器
            3. 分配各种内存区
          • paginginit: 初始化内核页表并启动内存分页。

            值的注意的是,低端内存中的所有页帧都直接映射到PAGEOFFSET之上的虚拟内存区。 这使得内核无需通过页表就可以直接寻址相当一部分内存

          • zonesizesinit: 初始化系统中所有结点的pgdatt实例

            AMD64下有所改变,但做的事都差不多,主要的区别就是AMD64的initmemorymapping函数 可以把所有物理内存直接映射到内核虚拟内存区,而不仅限于低端内存:

          2020-11-13_19-16-10_screenshot.png

        3. 分页机制初始化(64位) paginginit负责建立只能用于内核的也表,用户空间无法访问 我们主要关注AMD64的虚拟内存和分页:

          • 虚拟地址到物理地址空间的映射方式:

            2020-11-13_20-22-45_screenshot.png 因为现在AMD64处理器物理地址限制在48位,所以linux的虚拟地址映射分了非规范区,这个区域 是不会访问到的,只有上半部(内核空间)和下半部(用户空间)能访问。 如下图:

          2020-11-13_20-25-22_screenshot.png

        4. bootmem分配器 bootmem分配器用于在启动阶段早期分配内存,因为只在初始化时使用,不难想到,它的需求集中 在简单性方面,而不是性能和通用性,所以该分配器使用一个位图来管理页,一个位表示一个物理页。 分配算法采用最先适配算法(first-best)。

          • 数据结构:
          typedef struct bootmem_data {
            /* 系统中第一页的编号 */
            unsigned long node_boot_start;
            /* ZONE_NORMAL的结束页 */
            unsigned long node_low_pfn;
            /* 位图指针 */
            void *node_bootmem_map;
            unsigned long last_offset;
            unsigned long last_pos;
            unsigned long last_success; /* Previous allocation point.  To speed
                                         * up searching */
            struct list_head list;
          } bootmem_data_t;
          

          所有的分配器(UMA系统只有一个)都保存在一个全局bdatalist中

          • 初始化(AMD64)
            1. bootmembootmapbitmap利用BIOS在e820映射的信息计算bootmem位图所需页的数目
            2. initbootmem将该信息填充到bootmem数据结构中
          • API(TODO)
        5. 释放初始化数据 许多内核代码快和数据表只在系统初始化阶段需要,所以内核提供了两个属性(init和initcall) 用于标记初始化函数和数据。它们被编译器安排在一个特别的段中(.init.data, .init.text), 在启动过程刚好结束时会调用freeinitmem函数来释放这些区域,并将其返回伙伴系统。紧接其后 init作为系统中第一个进程启动.
  5. 物理内存的管理(伙伴系统)

    内核初始完成后,内存管理交给伙伴系统。它结合了分配器的两个关键特征:速度和效率

    1. 数据结构

      管理伙伴系统的数据存在于内存域结构体中:

      struct zone{
        ...
        /* 不同阶的内存区域, MAX_ORDER一般为11 */
        struct free_area free_area[MAX_ORDER];
        ...
      };
      
      struct free_area {
        struct list_head free_list[MIGRATE_TYPES];
        /* 空闲内存块的数目 */
        unsigned long nr_free;
      };
      

      可以图示如下:

      2020-11-13_21-51-32_screenshot.png 可以看到,伙伴系统相当简介,所以管理工作也非常少,这也是它的一个主要优点.

      就像前面说的,内存域有备用列表,所以伙伴系统也通过备用列表连接起来了:

      2020-11-13_21-55-49_screenshot.png

    2. 避免碎片

      在系统长期运行后,物理内存会产生很多的碎片:

      2020-11-13_22-16-14_screenshot.png 如上图,左边是有碎片的情况,内核才会涉及内存碎片(用户态在分配巨型页的情况下也会涉及), 内核会有内存碎片问题的原因:

      1. 有些底层指令(如lcr3)需要用到物理地址,所以直接映射不需要经过页表就能直接算出物理地址,
      2. 确实需要连续的物理内存

      所以内核加入了两种机制来尽量防止碎片:

      1. 将内存域内部的空闲内存按能不能移动分类 具体来说,内核将已分配的页分为下面3种不同类型:

        • 不可移动页
        • 可回收页 不能移动,但可以删除来重新装其他内容。映射自文件的数据属于这种类型。 kswapd守护进程会周期性释放此类内存
        • 可移动页 可以随意移动的页。 属于用户空间的应用程序的页属于该类别

        正如我们看到的,在伙伴系统中:

        struct free_area {
            struct list_head free_list[MIGRATE_TYPES];
            /* 空闲内存块的数目 */
            unsigned long nr_free;
          };
        

        空闲列表分为了MIGRSTETYPES的列表,MIGRATETYPES也就是上面说的页的分类类型数 与内存域之间的备用列表一样,这些不同类型内存间也有备用列表:

        2020-11-14_19-47-15_screenshot.png 当然,如果各迁移类型链表中如果没有一块较大的连续内存的话,这种分类没有任何好处,因此在可用 内存太少时内核会关闭该特性。

        分类后的一个问题是:既然分类了,那分配内存时必须指定要分配哪种类型的内存。这是通过分配掩码 来指定的。如果停用了分类特性,那所有页都是不可移动的。

        值得注意的是,内核并未在一开始划分内存为可移动性不同的区。

      2. TODO : 虚拟内存域 ZONEMOVABLE 好像与第一种有点冲突?可以同时激活吗?
    3. 初始化内存域和结点数据结构

      我们已经知道,体系结构特定初始化步骤前四步已经在启动过程中建立以下信息:

      • 各个内存域的页帧边界,maxzonepfn
      • 各个结点页帧的分配情况,earlynodemap

      现在需要做最后一步来完成内存域和结点数据结构的初始化: 该步骤由freeareainitnodes完成

      2020-11-14_21-25-12_screenshot.png

      • calculatenodetotalpages:计算结点中页的总数
      • allocnodememmap负责初始化每个物理页对应的struct page实例
      • freeareainitcore:初始化zone结构各个表头,并将所有成员初始化为0,所以这里也会把zone中 的nrfree设置为0。直到停用bootmem分配器,伙伴系统生效,才会设置正确的值
    4. 分配器API
      1. API

        分配最终都归于allocpagesnode函数

        2020-11-15_09-49-09_screenshot.png

        释放最终都归于_freepages函数

        2020-11-15_09-51-43_screenshot.png

      2. 分配掩码

        上面的所以函数都强制使用了分配掩码mask参数:

        2020-11-15_09-53-58_screenshot.png 他们的定义为:

        /*
         * GFP bitmasks..
         *
         * Zone modifiers (see linux/mmzone.h - low three bits)
         *
         * Do not put any conditional on these. If necessary modify the definitions
         * without the underscores and use the consistently. The definitions here may
         * be used in bit comparisons.
         */
        #define __GFP_DMA ((__force gfp_t)0x01u)
        #define __GFP_HIGHMEM ((__force gfp_t)0x02u)
        #define __GFP_DMA32 ((__force gfp_t)0x04u)
        
        /*
         * Action modifiers - doesn't change the zoning
         *
         * __GFP_REPEAT: Try hard to allocate the memory, but the allocation attempt
         * _might_ fail.  This depends upon the particular VM implementation.
         *
         * __GFP_NOFAIL: The VM implementation _must_ retry infinitely: the caller
         * cannot handle allocation failures.
         *
         * __GFP_NORETRY: The VM implementation must not retry indefinitely.
         *
         * __GFP_MOVABLE: Flag that this page will be movable by the page migration
         * mechanism or reclaimed
         */
        #define __GFP_WAIT  ((__force gfp_t)0x10u)  /* Can wait and reschedule? */
        #define __GFP_HIGH  ((__force gfp_t)0x20u)  /* Should access emergency pools? */
        #define __GFP_IO  ((__force gfp_t)0x40u)  /* Can start physical IO? */
        #define __GFP_FS  ((__force gfp_t)0x80u)  /* Can call down to low-level FS? */
        #define __GFP_COLD  ((__force gfp_t)0x100u) /* Cache-cold page required */
        #define __GFP_NOWARN  ((__force gfp_t)0x200u) /* Suppress page allocation failure warning */
        #define __GFP_REPEAT  ((__force gfp_t)0x400u) /* Retry the allocation.  Might fail */
        #define __GFP_NOFAIL  ((__force gfp_t)0x800u) /* Retry for ever.  Cannot fail */
        #define __GFP_NORETRY ((__force gfp_t)0x1000u)/* Do not retry.  Might fail */
        #define __GFP_COMP  ((__force gfp_t)0x4000u)/* Add compound page metadata */
        #define __GFP_ZERO  ((__force gfp_t)0x8000u)/* Return zeroed page on success */
        #define __GFP_NOMEMALLOC ((__force gfp_t)0x10000u) /* Don't use emergency reserves */
        #define __GFP_HARDWALL   ((__force gfp_t)0x20000u) /* Enforce hardwall cpuset memory allocs */
        #define __GFP_THISNODE  ((__force gfp_t)0x40000u)/* No fallback, no policies */
        #define __GFP_RECLAIMABLE ((__force gfp_t)0x80000u) /* Page is reclaimable */
        /* 不会影响内核的决策,除非它与__GFP_HIGHMEM同时指定。这中情况下,会使用特殊的虚拟
        *  内存域ZONE_MOVABLE满足内存分配要求 */
        #define __GFP_MOVABLE ((__force gfp_t)0x100000u)  /* Page is movable */
        
        #define __GFP_BITS_SHIFT 21 /* Room for 21 __GFP_FOO bits */
        #define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1))
        
        /* This equals 0, but use constants in case they ever change */
        #define GFP_NOWAIT  (GFP_ATOMIC & ~__GFP_HIGH)
        /* GFP_ATOMIC means both !wait (__GFP_WAIT not set) and use emergency pool */
        #define GFP_ATOMIC  (__GFP_HIGH)
        #define GFP_NOIO  (__GFP_WAIT)
        #define GFP_NOFS  (__GFP_WAIT | __GFP_IO)
        #define GFP_KERNEL  (__GFP_WAIT | __GFP_IO | __GFP_FS)
        #define GFP_TEMPORARY (__GFP_WAIT | __GFP_IO | __GFP_FS | \
               __GFP_RECLAIMABLE)
        #define GFP_USER  (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
        #define GFP_HIGHUSER  (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \
               __GFP_HIGHMEM)
        #define GFP_HIGHUSER_MOVABLE  (__GFP_WAIT | __GFP_IO | __GFP_FS | \
                 __GFP_HARDWALL | __GFP_HIGHMEM | \
                 __GFP_MOVABLE)
        #define GFP_NOFS_PAGECACHE  (__GFP_WAIT | __GFP_IO | __GFP_MOVABLE)
        #define GFP_USER_PAGECACHE  (__GFP_WAIT | __GFP_IO | __GFP_FS | \
                 __GFP_HARDWALL | __GFP_MOVABLE)
        #define GFP_HIGHUSER_PAGECACHE  (__GFP_WAIT | __GFP_IO | __GFP_FS | \
                 __GFP_HARDWALL | __GFP_HIGHMEM | \
                 __GFP_MOVABLE)
        
        #ifdef CONFIG_NUMA
        #define GFP_THISNODE  (__GFP_THISNODE | __GFP_NOWARN | __GFP_NORETRY)
        #else
        #define GFP_THISNODE  ((__force gfp_t)0)
        #endif
        
        /* This mask makes up all the page movable related flags */
        #define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
        
        /* Control page allocator reclaim behavior */
        #define GFP_RECLAIM_MASK (__GFP_WAIT|__GFP_HIGH|__GFP_IO|__GFP_FS|\
              __GFP_NOWARN|__GFP_REPEAT|__GFP_NOFAIL|\
              __GFP_NORETRY|__GFP_NOMEMALLOC)
        
        /* Control allocation constraints */
        #define GFP_CONSTRAINT_MASK (__GFP_HARDWALL|__GFP_THISNODE)
        
        /* Do not use these with a slab allocator */
        #define GFP_SLAB_BUG_MASK (__GFP_DMA32|__GFP_HIGHMEM|~__GFP_BITS_MASK)
        
        /* Flag - indicates that the buffer will be suitable for DMA.  Ignored on some
           platforms, used as appropriate on others */
        
        #define GFP_DMA   __GFP_DMA
        
        /* 4GB DMA on some platforms */
        #define GFP_DMA32 __GFP_DMA32
        
      3. 伙伴系统入口 allocpagesnode和_allocpages

        所有的API函数都追溯到allocpagesnode,它相当于伙伴系统的总入口.

        static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
                                                    unsigned int order)
        {
          /* 避免分配过大的内存块 */
          if (unlikely(order >= MAX_ORDER))
            return NULL;
        
          /* Unknown node is current node */
          if (nid < 0)
            nid = numa_node_id();
        
          return __alloc_pages(gfp_mask, order,
                               NODE_DATA(nid)->node_zonelists + gfp_zone(gfp_mask));
        }
        
        
        /* 选择内存域 */
        static inline enum zone_type gfp_zone(gfp_t flags)
        {
          int base = 0;
        
        #ifdef CONFIG_NUMA
          if (flags & __GFP_THISNODE)
            base = MAX_NR_ZONES;
        #endif
        
        #ifdef CONFIG_ZONE_DMA
          if (flags & __GFP_DMA)
            return base + ZONE_DMA;
        #endif
        #ifdef CONFIG_ZONE_DMA32
          if (flags & __GFP_DMA32)
            return base + ZONE_DMA32;
        #endif
          if ((flags & (__GFP_HIGHMEM | __GFP_MOVABLE)) ==
              (__GFP_HIGHMEM | __GFP_MOVABLE))
            return base + ZONE_MOVABLE;
        #ifdef CONFIG_HIGHMEM
          if (flags & __GFP_HIGHMEM)
            return base + ZONE_HIGHMEM;
        #endif
          return base + ZONE_NORMAL;
        }
        
        
        
        

        可以看到,该函数在执行避免分配分配过大内存块的检查后,马上把委托给_allocpages函数。 从它前面的两个 __ 就不难猜到,这个函数才是完成整个分配任务最核心的后台函数:

        #define ALLOC_NO_WATERMARKS 0x01 /* don't check watermarks at all */
        #define ALLOC_WMARK_MIN   0x02 /* use pages_min watermark */
        #define ALLOC_WMARK_LOW   0x04 /* use pages_low watermark */
        #define ALLOC_WMARK_HIGH  0x08 /* use pages_high watermark */
        #define ALLOC_HARDER    0x10 /* try to alloc harder */
        #define ALLOC_HIGH    0x20 /* __GFP_HIGH set */
        #define ALLOC_CPUSET    0x40 /* check for correct cpuset */
        
        /*
         * This is the 'heart' of the zoned buddy allocator.
         */
        struct page * fastcall
        __alloc_pages(gfp_t gfp_mask, unsigned int order,
            struct zonelist *zonelist)
        {
          const gfp_t wait = gfp_mask & __GFP_WAIT;
          struct zone **z;
          struct page *page;
          struct reclaim_state reclaim_state;
          struct task_struct *p = current;
          int do_retry;
          int alloc_flags;
          int did_some_progress;
        
          might_sleep_if(wait);
        
          if (should_fail_alloc_page(gfp_mask, order))
            return NULL;
        
        restart:
          z = zonelist->zones;  /* the list of zones suitable for gfp_mask */
        
          if (unlikely(*z == NULL)) {
            /*
             * Happens if we have an empty zonelist as a result of
             * GFP_THISNODE being used on a memoryless node
             */
            return NULL;
          }
        
          /* 1.最简单的情形,空闲内存足够 */
          page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,
                zonelist, ALLOC_WMARK_LOW|ALLOC_CPUSET);
          if (page)
            goto got_pg;
        
          /*
           * GFP_THISNODE (meaning __GFP_THISNODE, __GFP_NORETRY and
           * __GFP_NOWARN set) should not cause reclaim since the subsystem
           * (f.e. slab) using GFP_THISNODE may choose to trigger reclaim
           * using a larger set of nodes after it has established that the
           * allowed per node queues are empty and that nodes are
           * over allocated.
           */
          if (NUMA_BUILD && (gfp_mask & GFP_THISNODE) == GFP_THISNODE)
            goto nopage;
        
          /* 2.第一次没有取得内存,说明内存不太够了,内核再次遍历备用列表中的所有
          *    内存域,每次都调用wakeup_kswapd来唤醒换出页守护进程*/
          for (z = zonelist->zones; *z; z++)
            wakeup_kswapd(*z, order);
        
          /*
           * OK, we're below the kswapd watermark and have kicked background
           * reclaim. Now things get more complex, so set up alloc_flags according
           * to how we want to proceed.
           *
           * The caller may dip into page reserves a bit more if the caller
           * cannot run direct reclaim, or if the caller has realtime scheduling
           * policy or is asking for __GFP_HIGH memory.  GFP_ATOMIC requests will
           * set both ALLOC_HARDER (!wait) and ALLOC_HIGH (__GFP_HIGH).
           */
          /* 并且会调整分配标志,使的分配更容易成功,将水印降低到最小值 */
          alloc_flags = ALLOC_WMARK_MIN;
          if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait)
            alloc_flags |= ALLOC_HARDER;
          if (gfp_mask & __GFP_HIGH)
            alloc_flags |= ALLOC_HIGH;
          if (wait)
            alloc_flags |= ALLOC_CPUSET;
        
          /*
           * Go through the zonelist again. Let __GFP_HIGH and allocations
           * coming from realtime tasks go deeper into reserves.
           *
           * This is the last chance, in general, before the goto nopage.
           * Ignore cpuset if GFP_ATOMIC (!wait) rather than fail alloc.
           * See also cpuset_zone_allowed() comment in kernel/cpuset.c.
           */
          page = get_page_from_freelist(gfp_mask, order, zonelist, alloc_flags);
          if (page)
            goto got_pg;
        
          /* This allocation should allow future memory freeing. */
        
          /* 3.如果再次失败,内核会采取更强有力的措施,这次完全忽略水印 */
        rebalance:
          if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE)))
              && !in_interrupt()) {
            if (!(gfp_mask & __GFP_NOMEMALLOC)) {
        nofail_alloc:
              /* go through the zonelist yet again, ignoring mins */
              page = get_page_from_freelist(gfp_mask, order,
                zonelist, ALLOC_NO_WATERMARKS);
              if (page)
                goto got_pg;
              if (gfp_mask & __GFP_NOFAIL) {
                congestion_wait(WRITE, HZ/50);
                goto nofail_alloc;
              }
            }
            goto nopage;
          }
        
          /* Atomic allocations - we can't balance anything */
          if (!wait)
            goto nopage;
        
          cond_resched();
        
          /* We now go into synchronous reclaim */
          /* 4.同步回收状态,和换出守护进程的区别? */
          cpuset_memory_pressure_bump();
          p->flags |= PF_MEMALLOC;
          reclaim_state.reclaimed_slab = 0;
          p->reclaim_state = &reclaim_state;
        
          did_some_progress = try_to_free_pages(zonelist->zones, order, gfp_mask);
        
          p->reclaim_state = NULL;
          p->flags &= ~PF_MEMALLOC;
        
          cond_resched();
        
          /* 如果分配多页,缓存中的页也会被拿回伙伴系统 */
          if (order != 0)
            drain_all_local_pages();
        
          /* 如果try_to_free_pages释放了一些页,再次尝试获取 */
          if (likely(did_some_progress)) {
            page = get_page_from_freelist(gfp_mask, order,
                    zonelist, alloc_flags);
            if (page)
              goto got_pg;
          } else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {
            if (!try_set_zone_oom(zonelist)) {
              schedule_timeout_uninterruptible(1);
              goto restart;
            }
        
            /*
             * Go through the zonelist yet one more time, keep
             * very high watermark here, this is only to catch
             * a parallel oom killing, we must fail if we're still
             * under heavy pressure.
             */
            /* 5.out_of_memory,选择一个内核认为犯有分配过多内存“罪行”的进程,并杀掉该进程 */
            page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,
                zonelist, ALLOC_WMARK_HIGH|ALLOC_CPUSET);
            if (page) {
              clear_zonelist_oom(zonelist);
              goto got_pg;
            }
        
            /* The OOM killer will not help higher order allocs so fail */
            /* 如果要分配大页,那么内核会选择放过选择的“罪行”进程 */
            if (order > PAGE_ALLOC_COSTLY_ORDER) {
              clear_zonelist_oom(zonelist);
              goto nopage;
            }
        
            out_of_memory(zonelist, gfp_mask, order);
            clear_zonelist_oom(zonelist);
            goto restart;
          }
        
          /*
           * Don't let big-order allocations loop unless the caller explicitly
           * requests that.  Wait for some write requests to complete then retry.
           *
           * In this implementation, __GFP_REPEAT means __GFP_NOFAIL for order
           * <= 3, but that may not be true in other implementations.
           */
          do_retry = 0;
          if (!(gfp_mask & __GFP_NORETRY)) {
            if ((order <= PAGE_ALLOC_COSTLY_ORDER) ||
                    (gfp_mask & __GFP_REPEAT))
              do_retry = 1;
            if (gfp_mask & __GFP_NOFAIL)
              do_retry = 1;
          }
          if (do_retry) {
            congestion_wait(WRITE, HZ/50);
            goto rebalance;
          }
        
        nopage:
          if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {
            printk(KERN_WARNING "%s: page allocation failure."
              " order:%d, mode:0x%x\n",
              p->comm, order, gfp_mask);
            dump_stack();
            show_mem();
          }
        got_pg:
          return page;
        }
        
        
        
        
        
        
        /*
         * get_page_from_freelist goes through the zonelist trying to allocate
         * a page.
         */
        /* 重要的辅助函数,通过标志集和分配阶来判断是否能进行分配 */
        static struct page *
        get_page_from_freelist(gfp_t gfp_mask, unsigned int order,
            struct zonelist *zonelist, int alloc_flags)
        {
          struct zone **z;
          struct page *page = NULL;
          int classzone_idx = zone_idx(zonelist->zones[0]);
          struct zone *zone;
          nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
          int zlc_active = 0;   /* set if using zonelist_cache */
          int did_zlc_setup = 0;    /* just call zlc_setup() one time */
          enum zone_type highest_zoneidx = -1; /* Gets set for policy zonelists */
        
        zonelist_scan:
          /*
           * Scan zonelist, looking for a zone with enough free.
           * See also cpuset_zone_allowed() comment in kernel/cpuset.c.
           */
          z = zonelist->zones;
        
          /* 遍历备用列表的所有内存域,用最简单的方式查找一个适当的空闲内存块 */
          do {
            /*
             * In NUMA, this could be a policy zonelist which contains
             * zones that may not be allowed by the current gfp_mask.
             * Check the zone is allowed by the current flags
             */
            if (unlikely(alloc_should_filter_zonelist(zonelist))) {
              if (highest_zoneidx == -1)
                highest_zoneidx = gfp_zone(gfp_mask);
              if (zone_idx(*z) > highest_zoneidx)
                continue;
            }
        
            if (NUMA_BUILD && zlc_active &&
              !zlc_zone_worth_trying(zonelist, z, allowednodes))
                continue;
            zone = *z;
            if ((alloc_flags & ALLOC_CPUSET) &&
                /* 辅助函数,用于检查给定内存域是否属于该进程允许允许的cpu */
              !cpuset_zone_allowed_softwall(zone, gfp_mask))
                goto try_next_zone;
        
            if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
              unsigned long mark;
              if (alloc_flags & ALLOC_WMARK_MIN)
                mark = zone->pages_min;
              else if (alloc_flags & ALLOC_WMARK_LOW)
                mark = zone->pages_low;
              else
                mark = zone->pages_high;
              /* 重要的辅助函数,检查所遍历到的内存域是否有足够的空闲页,并试图
              *  分配一个连续的内存块*/
              if (!zone_watermark_ok(zone, order, mark,
                    classzone_idx, alloc_flags)) {
                if (!zone_reclaim_mode ||
                    !zone_reclaim(zone, gfp_mask, order))
                  goto this_zone_full;
              }
            }
        
            /**** 重要的核心函数,伙伴系统的核心 */
            page = buffered_rmqueue(zonelist, zone, order, gfp_mask);
            if (page)
              break;
        this_zone_full:
            if (NUMA_BUILD)
              zlc_mark_zone_full(zonelist, z);
        try_next_zone:
            if (NUMA_BUILD && !did_zlc_setup) {
              /* we do zlc_setup after the first zone is tried */
              allowednodes = zlc_setup(zonelist, alloc_flags);
              zlc_active = 1;
              did_zlc_setup = 1;
            }
          } while (*(++z) != NULL);
        
          if (unlikely(NUMA_BUILD && page == NULL && zlc_active)) {
            /* Disable zlc cache for second zonelist scan */
            zlc_active = 0;
            goto zonelist_scan;
          }
          return page;
        }
        
      4. 伙伴系统核心 bufferedrmqueue

        到目前为止,我们只知道,如果判断出内存域中有足够的空闲页就会委托给bufferedrmqueue,但是 足够的空闲页是包括所有的阶的总量,所以不一定能保证分配成功。

        bufferedrmqueue的任务主要有两个

        1. 检查这些页是否是连续的
        2. 从freelists移除这些页,这可能需要分解并重排内存区

        其主要流程图为:

        2020-11-15_17-02-34_screenshot.png

        /*
         * Really, prep_compound_page() should be called from __rmqueue_bulk().  But
         * we cheat by calling it from here, in the order > 0 path.  Saves a branch
         * or two.
         */
        static struct page *buffered_rmqueue(struct zonelist *zonelist,
              struct zone *zone, int order, gfp_t gfp_flags)
        {
          unsigned long flags;
          struct page *page;
          /* 设置了COLD标志,那么必须从per-CPU缓存中取得冷页,
          *  专门设置冷页的目的是:有些页被访问的次数很少,所以把他
          *  放到catch中太浪费了 */
          /* 两次取反确保cold为0或1 */
          int cold = !!(gfp_flags & __GFP_COLD);
          int cpu;
          /* 根据标志确定迁移列表, (重要)页的迁移类型存储在struct page的private成员中*/
          int migratetype = allocflags_to_migratetype(gfp_flags);
        
        again:
          cpu  = get_cpu();
          /* 如果只请求一页,内核会借助per-CPU缓存加速请求的处理 */
          if (likely(order == 0)) {
            struct per_cpu_pages *pcp;
        
            pcp = &zone_pcp(zone, cpu)->pcp[cold];
            local_irq_save(flags);
            if (!pcp->count) {
              /* 重新用伙伴系统中的页填充缓存 */
              pcp->count = rmqueue_bulk(zone, 0,
                  pcp->batch, &pcp->list, migratetype);
              if (unlikely(!pcp->count))
                goto failed;
            }
        
            /* Find a page of the appropriate migrate type */
            /* 检查是否有指定迁移类型的页可用,如果前一次用其他迁移类型的页填充了缓存,
            *  可能找不到,*/
            list_for_each_entry(page, &pcp->list, lru)
              if (page_private(page) == migratetype)
                break;
        
            /* Allocate more to the pcp list if necessary */
            /* 找不到的话就要向缓存中添加一些符合当前迁移类型要求的页 */
            if (unlikely(&page->lru == &pcp->list)) {
              pcp->count += rmqueue_bulk(zone, 0,
                  pcp->batch, &pcp->list, migratetype);
              page = list_entry(pcp->list.next, struct page, lru);
            }
        
            list_del(&page->lru);
            pcp->count--;
        
            /* 需要分配多页 */
          } else {
            spin_lock_irqsave(&zone->lock, flags);
            /* (伙伴系统核心函数)从伙伴系统获得内存块,如有必要会自动分解大块内存 */
            page = __rmqueue(zone, order, migratetype);
            spin_unlock(&zone->lock);
            if (!page)
              goto failed;
          }
        
          __count_zone_vm_events(PGALLOC, zone, 1 << order);
          zone_statistics(zonelist, zone);
          local_irq_restore(flags);
          put_cpu();
        
          VM_BUG_ON(bad_range(zone, page));
          /* 对页进行必要的检查,并将第一页的引用计数置为初始值1 */
          if (prep_new_page(page, order, gfp_flags))
            goto again;
          return page;
        
        failed:
          local_irq_restore(flags);
          put_cpu();
          return NULL;
        }
        
        /*
         * Do the hard work of removing an element from the buddy allocator.
         * Call me with the zone->lock already held.
         */
        static struct page *__rmqueue(struct zone *zone, unsigned int order,
                                      int migratetype)
        {
          struct page *page;
        
          /* 根据传递进来的分配阶,用于获取页的内存域,迁移类型,
           扫描页的列表,知道找到适当的连续内存块*/
          page = __rmqueue_smallest(zone, order, migratetype);
        
          if (unlikely(!page))
            /* 如果指定的迁移列表不能满足分配请求,则调用__rmqueue_fallback尝试
            *  其他迁移列表,作为应急措施*/
            page = __rmqueue_fallback(zone, order, migratetype);
        
          return page;
        }
        
        /*
         * Go through the free lists for the given migratetype and remove
         * the smallest available page from the freelists
         */
        static struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                                               int migratetype)
        {
          unsigned int current_order;
          struct free_area * area;
          struct page *page;
        
          /* Find a page of the appropriate size in the preferred list */
          /* 遍历每个大于等于当前分配阶的正确迁移列表 */
          for (current_order = order; current_order < MAX_ORDER; ++current_order) {
            area = &(zone->free_area[current_order]);
            if (list_empty(&area->free_list[migratetype]))
              continue;
        
            page = list_entry(area->free_list[migratetype].next,
                              struct page, lru);
            list_del(&page->lru);
            /* 从页中删除PG_buddy标志位,并将struct page的private成员设置为0(存的不是迁移类型吗?) */
            rmv_page_order(page);
            area->nr_free--;
            /* 更新当前内存域的统计量 */
            __mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order));
            /* 如果取到的内存块的页数大于请求的数, 拆分该内存块 */
            expand(zone, page, order, current_order, area, migratetype);
            return page;
          }
        
          return NULL;
        }
        
        /* Remove an element from the buddy allocator from the fallback list */
        /* 备用次序中分配 */
        static struct page *__rmqueue_fallback(struct zone *zone, int order,
                    int start_migratetype)
        {
          struct free_area * area;
          int current_order;
          struct page *page;
          int migratetype, i;
        
          /* Find the largest possible block of pages in the other list */
          /* 遍历备用列表的不同迁移类型,(重要)按阶从大到小遍历,因为
             这样能尽可能得到大块的内存,从而减少向其他类型引入碎片*/
          for (current_order = MAX_ORDER-1; current_order >= order;
                    --current_order) {
            for (i = 0; i < MIGRATE_TYPES - 1; i++) {
              migratetype = fallbacks[start_migratetype][i];
        
              /* MIGRATE_RESERVE handled later if necessary */
              if (migratetype == MIGRATE_RESERVE)
                continue;
        
              area = &(zone->free_area[current_order]);
              if (list_empty(&area->free_list[migratetype]))
                continue;
        
              page = list_entry(area->free_list[migratetype].next,
                  struct page, lru);
              area->nr_free--;
        
              /*
               * If breaking a large block of pages, move all free
               * pages to the preferred allocation list. If falling
               * back for a reclaimable kernel allocation, be more
               * agressive about taking ownership of free pages
               */
              /* 这样做是因为如果剩余部分也是一个比较大的内存块,那么将整个内存块
              *  都转移到当前分配类型对应的迁移列表是有意义的,这样可以减少碎片*/
              if (unlikely(current_order >= (pageblock_order >> 1)) ||
                  start_migratetype == MIGRATE_RECLAIMABLE) {
                unsigned long pages;
                pages = move_freepages_block(zone, page,
                        start_migratetype);
        
                /* Claim the whole block if over half of it is free */
                if (pages >= (1 << (pageblock_order-1)))
                  set_pageblock_migratetype(page,
                        start_migratetype);
        
                migratetype = start_migratetype;
              }
        
              /* Remove the page from the freelists */
              list_del(&page->lru);
              rmv_page_order(page);
              __mod_zone_page_state(zone, NR_FREE_PAGES,
                      -(1UL << order));
        
              if (current_order == pageblock_order)
                set_pageblock_migratetype(page,
                      start_migratetype);
        
              expand(zone, page, order, current_order, area, migratetype);
              return page;
            }
          }
        
          /* Use MIGRATE_RESERVE rather than fail an allocation */
          /* 如果所以备用列表也都分配失败,尝试从保留类型中分配 */
          return __rmqueue_smallest(zone, order, MIGRATE_RESERVE);
        }
        /*
         * This array describes the order lists are fallen back to when
         * the free lists for the desirable migrate type are depleted
         */
        static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
          [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_RESERVE },
          [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_RESERVE },
          [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
          [MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,   MIGRATE_RESERVE }, /* Never used */
        };
        
        
        
        
        
        /*
         * This page is about to be returned from the page allocator
         */
        static int prep_new_page(struct page *page, int order, gfp_t gfp_flags)
        {
          if (unlikely(page_mapcount(page) |
            (page->mapping != NULL)  |
            (page_count(page) != 0)  |
            (page->flags & (
              1 << PG_lru |
              1 << PG_private |
              1 << PG_locked  |
              1 << PG_active  |
              1 << PG_dirty |
              1 << PG_slab    |
              1 << PG_swapcache |
              1 << PG_writeback |
              1 << PG_reserved |
              1 << PG_buddy ))))
            bad_page(page);
        
          /*
           * For now, we report if PG_reserved was found set, but do not
           * clear it, and do not allocate the page: as a safety net.
           */
          if (PageReserved(page))
            return 1;
        
          page->flags &= ~(1 << PG_uptodate | 1 << PG_error | 1 << PG_readahead |
              1 << PG_referenced | 1 << PG_arch_1 |
              1 << PG_owner_priv_1 | 1 << PG_mappedtodisk);
          set_page_private(page, 0);
          set_page_refcounted(page);
        
          arch_alloc_page(page, order);
          kernel_map_pages(page, 1 << order, 1);
        
          if (gfp_flags & __GFP_ZERO)
            prep_zero_page(page, order, gfp_flags);
        
          /* 将请求的多页组成复合页,详见下文 */
          if (order && (gfp_flags & __GFP_COMP))
            prep_compound_page(page, order);
        
          return 0;
        }
        

        复合页:

        2020-11-15_17-01-52_screenshot.png

      5. 释放页 _freepages

        _freepages是释放内存的基础函数,其代码流程如下

        2020-11-15_17-25-41_screenshot.png

        fastcall void __free_pages(struct page *page, unsigned int order)
        {
          if (put_page_testzero(page)) {
            if (order == 0)    /* 如果分配单页 */
              free_hot_page(page);
            else               /* 分配多页 */
              __free_pages_ok(page, order);
          }
        }
        
        static void fastcall free_hot_cold_page(struct page *page, int cold)
        {
          struct zone *zone = page_zone(page);
          struct per_cpu_pages *pcp;
          unsigned long flags;
        
          if (PageAnon(page))
            page->mapping = NULL;
          if (free_pages_check(page))
            return;
        
          if (!PageHighMem(page))
            debug_check_no_locks_freed(page_address(page), PAGE_SIZE);
          arch_free_page(page, 0);
          kernel_map_pages(page, 1, 0);
        
          pcp = &zone_pcp(zone, get_cpu())->pcp[cold];
          local_irq_save(flags);
          __count_vm_event(PGFREE);
          list_add(&page->lru, &pcp->list);
          /* 将private设置为迁移类型 */
          set_page_private(page, get_pageblock_migratetype(page));
          pcp->count++;
          /* 懒惰合并,能减少合并次数而节约时间 */
          if (pcp->count >= pcp->high) {  /* 如果缓存中的页超出范围,将batch个内存页还给伙伴系统 */
            free_pages_bulk(zone, pcp->batch, &pcp->list, 0);
            pcp->count -= pcp->batch;
          }
          local_irq_restore(flags);
          put_cpu();
        }
        
        static void __free_pages_ok(struct page *page, unsigned int order)
        {
          unsigned long flags;
          int i;
          int reserved = 0;
        
          for (i = 0 ; i < (1 << order) ; ++i)
            reserved += free_pages_check(page + i);
          if (reserved)
            return;
        
          if (!PageHighMem(page))
            debug_check_no_locks_freed(page_address(page),PAGE_SIZE<<order);
          arch_free_page(page, order);
          kernel_map_pages(page, 1 << order, 0);
        
          local_irq_save(flags);
          __count_vm_events(PGFREE, 1 << order);
          /* 相关内存区被添加到伙伴系统中适当的free_area列表中,并递归合并,具体
          *   例子看下图*/
          free_one_page(page_zone(page), page, order);
          local_irq_restore(flags);
        }
        

        2020-11-15_18-36-03_screenshot.png

        2020-11-15_18-36-59_screenshot.png

      6. 内核不连续页的分配vmalloc

        我们知道,物理上连续的映射对内核是最好的:

        1. 有些求地址操作可以直接线性求出,不需要经过页表计算,也不需要复杂的数据结构
        2. 减少TLB占用
        3. catch命中率更高,所以速度更快

        但是这并不总是能成功,特别是开机很久之后,因为这时内存可能已经比较碎片化了。 所以内核也需要使用像用户态一样的技术:通过处理器的分页机制,分配物理上不连续但虚拟空间 连续的内存。当然这会降低速度并占用TLB.

        内核分配了其虚拟空间的一部分用于建立这种连续映射(32和64位都有)。并提供vmalloc接口函数 来为内核建立连续映射。

        vmalloc最著名的实例是内核对模块的实现。还有设备和声音驱动中。vmalloc内存页总是优先使用 高端内存。 其实现对应的数据结构是:

        struct vm_struct {
          /* keep next,addr,size together to speedup lookups */
          struct vm_struct  *next;
          void      *addr;
          unsigned long   size;
          unsigned long   flags;
          struct page   **pages;
          unsigned int    nr_pages;
          unsigned long   phys_addr;
        };
        

        书中给除了该结构的一个实例:

        2020-11-15_19-14-27_screenshot.png

        • vmalloc分配流程:

          1. getvmarea在vmalloc地址空间中找到适当的区域
          2. 从伙伴系统中分配各个物理页,重点是注意他是一页一页分配的,而不是一次分配一大块, 确保在物理内存有严重碎片的情况下,vmalloc依然可以工作。

          3, mapvmarea将这些页连续地映射到vmalloc区域中,并正确设置相关的页表项

          2020-11-15_19-24-47_screenshot.png

        • 内核虚拟映射内存的释放:

          2020-11-15_19-26-47_screenshot.png

      7. 内核其他映射类型

        尽管vmalloc可以用于从高端内存域向内核映射页帧,但是内核还提供了其他函数用于将ZONEHIGHMEM 页帧显式映射到内核地址空间中:

        1. 持久映射kmap
        2. 固定映射kmapatomic

        因为现在用的基本是64位机器,已经没有了ZONEHIGMEM,所以这里不详细了解这些映射了,因为 64下这些操作就是一些很简单的宏:

        static inline void *kmap(struct page *page)
        {
          might_sleep();
          return page_address(page);
        }
        
        #define kunmap(page) do { (void) (page); } while (0)
        
        #include <asm/kmap_types.h>
        
        static inline void *kmap_atomic(struct page *page, enum km_type idx)
        {
          pagefault_disable();
          return page_address(page);
        }
        #define kmap_atomic_prot(page, idx, prot) kmap_atomic(page, idx)
        
        #define kunmap_atomic(addr, idx)  do { pagefault_enable(); } while (0)
        #define kmap_atomic_pfn(pfn, idx) kmap_atomic(pfn_to_page(pfn), (idx))
        #define kmap_atomic_to_page(ptr)  virt_to_page(ptr)
        
        #define kmap_flush_unused() do {} while(0)
        #endif
        
  6. slab分配器

    伙伴系统只支持页级别的内存分配,这个单位太大了,我们需要一个可以以字节为单位的分配器。 它的实现应该尽可能紧凑,以便不要对处理器的高速缓存和TLB带来显著的影响。linux实现了 一个这样的分配器slab。它不仅可以用来分配小内存,而且也用做一个缓存。

    1. slab相对直接使用伙伴系统的优势:
      1. 由于内核不必使用伙伴系统算法,分配内存处理时间会变短
      2. slab作为缓存将释放的内存保存在它的内部列表中,并不马上还给伙伴系统,所以再次使用 该内存块时,它依然在cpu高速缓存中的概率较高
      3. 有助于防止不受欢迎的缓存“污染”。因为调用伙伴系统的操作对系统的数据和指令的高速 缓存有相当的影响。
      4. 实现缓存的均匀利用。伙伴系统取的内存,其地址总是在2的幂次的整数倍附近。这种地址 分布使得某些缓存行过度使用,而其他的则几乎为空。多处理器下还可能因为不同内存地址 在不同的总线上传输,使得有些总线拥塞,而其他几乎没有使用。 slab通过slab着色能够 实现均匀地利用cpu缓存。

      slab着色: http://www.360doc.com/content/12/0828/15/7982302_232808690.shtml

    2. slob和slub

      这两个函数用于特定的硬件,实现的功能和slab差不多,slob用于小型的嵌入式系统,slub 用于大型系统上

    3. 三个函数提供的标准接口

      不管slab,slob还是slub,都实现了一组特定的函数,用于内存分配和缓存。它们与分配器 的关系如图:

      2020-11-15_21-49-32_screenshot.png

    4. slab数据结构

      slab主要由struct kmemcache结构来表示,其中,每个kmemcache只缓存一种对象。 多个对象会合并成一个组,覆盖一个或多个连续页帧。这种组称作slab;每个kmemcache 由多个slab组成。其概览图如下:

      2020-11-16_10-15-31_screenshot.png

      struct kmemcache主要成员有:

      struct kmem_cache {
      /* 1) per-cpu data, touched during every alloc/free */
        struct array_cache *array[NR_CPUS];  /* 每次分配/释放期间都会访问, 其具体结构在下面代码给出 */
      /* 2) Cache tunables. Protected by cache_chain_mutex */
        /* 可调整的缓存参数 */
        unsigned int batchcount; /* per-CPU列表为空时,从缓存的slab中获取对象的数目,
                                  还表示 在缓存增长时分配的对象数目*/
        unsigned int limit;      /* 指定per-CPU列表中保存的对象的最大数目, 如果超出该值,
                                  内核会将batchcount个对象返回给slab */
        unsigned int shared;
      
        unsigned int buffer_size;  /* 缓存中管理的对象的长度 */
        u32 reciprocal_buffer_size;  /* 用来加快除法的计算 */
      /* 3) touched by every alloc & free from the backend */
      
        unsigned int flags;   /* constant flags */
        /* 表示最多可有多少对象能放入slab */
        unsigned int num;   /* # of objs per slab */
      
      /* 4) cache_grow/shrink */
        /* order of pgs per slab (2^n) */
        unsigned int gfporder;   /* 表示每个slab包含2^gfproder页 */
      
        /* force GFP flags, e.g. GFP_DMA */
        gfp_t gfpflags;
      
        size_t colour;      /* cache colouring range */ /* 着色相关 */
        unsigned int colour_off;  /* colour offset */
        struct kmem_cache *slabp_cache;
        unsigned int slab_size;
        unsigned int dflags;    /* dynamic flags */
      
        /* constructor func */
        void (*ctor)(struct kmem_cache *, void *);
      
      /* 5) cache creation/removal */
        const char *name;    /* /proc/slabinfo中的名称 */
        struct list_head next; /* 将所有实例保存在全局链表cache_chain上 */
      
      /* 6) statistics */
      #if STATS
        unsigned long num_active;
        unsigned long num_allocations;
        unsigned long high_mark;
        unsigned long grown;
        unsigned long reaped;
        unsigned long errors;
        unsigned long max_freeable;
        unsigned long node_allocs;
        unsigned long node_frees;
        unsigned long node_overflow;
        atomic_t allochit;
        atomic_t allocmiss;
        atomic_t freehit;
        atomic_t freemiss;
      #endif
      #if DEBUG
        /*
         * If debugging is enabled, then the allocator can add additional
         * fields and/or padding to every object. buffer_size contains the total
         * object size including these internal fields, the following two
         * variables contain the offset to the user object and its size.
         */
        int obj_offset;
        int obj_size;
      #endif
        /*
         * We put nodelists[] at the end of kmem_cache, because we want to size
         * this array to nr_node_ids slots instead of MAX_NUMNODES
         * (see kmem_cache_init())
         * We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache
         * is statically defined, so we reserve the max number of nodes.
         */
        struct kmem_list3 *nodelists[MAX_NUMNODES]; /* 每个数组项对应于系统中一个可能的
                                                     内存结点,具体结构在下面讨论*/
        /*
         * Do not add fields after nodelists[]
         */
      };
      
      
      /*
       * struct array_cache
       *
       * Purpose:
       * - LIFO ordering, to hand out cache-warm objects from _alloc
       * - reduce the number of linked list operations
       * - reduce spinlock operations
       *
       * The limit is stored in the per-cpu structure to reduce the data cache
       * footprint.
       *
       */
      struct array_cache {
        unsigned int avail;  /* 当前可用对象的数目 */
        unsigned int limit;
        unsigned int batchcount;
        unsigned int touched;  /* 在从缓存移除一个对象时,将touched置为1;缓存收缩时,置为0 */
        spinlock_t lock;
        void *entry[];  /*
                         * Must have this definition in here for the proper
                         * alignment of array_cache. Also simplifies accessing
                         * the entries.
                         */
      };
      
      /*
       * The slab lists for all objects.
       */
      struct kmem_list3 {
        /* 部分空闲,全满,全空闲slab链表 */
        struct list_head slabs_partial; /* partial list first, better asm code */
        struct list_head slabs_full;
        struct list_head slabs_free;
        unsigned long free_objects;  /* slabs_partial和slabs_free的所有slab中空闲对象的总数 */
        unsigned int free_limit;     /* 空闲对象的最大数目限制 */
        unsigned int colour_next; /* Per-node cache coloring */
        spinlock_t list_lock;
        struct array_cache *shared; /* shared per node */
        struct array_cache **alien; /* on other nodes */
        unsigned long next_reap;  /* updated without locking */
        /* 表示缓存是否是活动的 */
        int free_touched;   /* updated without locking */
      };
      

      2020-11-16_11-22-18_screenshot.png

      可以看到,该结构不是直接从slab中取对象的,而是针对每个cpu有一个per-CPU指针,指向slab 中的未使用对象。这样做是为了更好地利用CPU高速缓存。它在分配和释放对象时,采用后进先出的 原理(LIFO)。其背后的原理是,刚释放的对象很可能仍然处于CPU高速缓存中。更进一步,slab 对象分配体系按分配成本和操作对CPU高速缓存及TLB的负面影响从低到高可以排序为三层:

      1. 仍然处于CPU高速缓存中的per-CPU对象
      2. 现存slab中未使用的对象
      3. 刚使用伙伴系统分配的新slab中未使用的对象

      一个slab组的精细结构:

      2020-11-16_13-46-02_screenshot.png 其中,它的头部管理数据中有一个数组,每个数组项对于一个对象,用来记录下一个空闲对象的索引, 所以只有在对象没有分配时,相应的数组项才有意义。如下图:

      2020-11-16_13-49-39_screenshot.png

      最后,内核有时候需要从对象自身识别对象所在的slab,内核利用如下机制来完成该任务:

      1. 根据对象的物理内存地址,可以找到相关的页,因此可以在全局memmap数组中找到对应的page 实例
      2. slab缓存中的页面而言,page结构中的链表是没有用到的,所以可以复用它来完成该任务: page->lru.next 指向页驻留的缓存的管理结构 page->lru.prev 指向保存该页的slab的管理结构
    5. API
      1. TODO 初始化(自举)
      2. 创建缓存 kmemcachecreate

        2020-11-16_18-46-06_screenshot.png

      3. 分配对象

        2020-11-16_18-47-50_screenshot.png 其中,cachegrow代表缓存的增长

        2020-11-16_18-52-21_screenshot.png

      4. 释放对象

        2020-11-16_18-53-15_screenshot.png

      5. 销毁缓存 kmemcachedestroy
      6. 通用缓存

        如果不涉及对象缓存,则必须调用kmalloc和kfree函数,这两个函数相当于用户空间C 标准库malloc和free函数的内核等价物

        /* 每次调用kmalloc时,内核找到最适合的缓存,并从中分配一个对象来满足要求
           最合适的意思是:如果没有刚刚适合大小的缓存,则分配稍大的*/
        void *kmalloc(size, flags);
        
        void kfree(*ptr);
        

        kmalloc的基础是一个数组,数组项是cachesizes结构,其中是一些分别用于不同内存长度的slab缓存,

        /* Size description struct for general caches. */
        struct cache_sizes {
          /* 该数组项负责的内存区的长度 */
          size_t      cs_size;
          struct kmem_cache *cs_cachep;
        #ifdef CONFIG_ZONE_DMA
          struct kmem_cache *cs_dmacachep;
        #endif
        };
        
    6. TODO 处理器高速缓存和TLB控制

3.9.3 进程虚拟内存

每个进程都有虚拟地址空间,它起始于地址0,延伸到TASKSIZE - 1,其上是内核地址空间。 用户程序只能访问整个地址空间的下半部分,不能访问内核部分。 如果没有预先达成“协议”, 用户进程也不可能操作另一个进程的地址空间,因为后者的地址空间对前者不可见。

虚拟地址空间由许多不同长度的段组成,用于不同的目的,必须分别处理。例如text段是代码段,应该 是只读的;而映射到地址空间的文件内容不能执行其内容,但是可以修改。

  1. 进程地址空间布局

    虚拟地址空间包含了若干区域,每个体系结构可能不一样,但是一定会有下面这些段:

    • text段. 当前运行代码的二进制代码。
    • 程序使用的动态库代码
    • 存储全局变量和动态数据的堆
    • 保存局部变量和实现函数/过程调用的栈
    • 环境变量和命令行参数
    • 将文件内容映射到虚拟地址空间的内存映射

    2020-11-16_23-01-21_screenshot.png 其中,randomizedvariable用于使栈的地址偏移一个随机值,用于防止缓冲区溢出攻击,用户可以通过 /proc/sys/kernel/randomizevaspace停用这个特性

    值得一体的是,32位体系结构中可能会有另外一种体系结构:

    2020-11-16_23-13-42_screenshot.png 这种布局的主要好处是mmap是往下涨的,这样可以增多堆空间的大小(mmap的起始地址上,经典布局 堆只有1G空间);但是这样也有一个缺点就是栈的大小是固定的。

  2. 数据结构表示(struct mmstruct)

    每个进程的taskstruct中都有一个mmstruct成员,它保存了进程的内存管理信息:

    struct mm_struct {
      /* 虚拟内存区域列表 */
      struct vm_area_struct * mmap;   /* list of VMAs */
      struct rb_root mm_rb;
      /* 上一次find_vma的结果 */
      struct vm_area_struct * mmap_cache; /* last find_vma result */
      unsigned long (*get_unmapped_area) (struct file *filp,
            unsigned long addr, unsigned long len,
            unsigned long pgoff, unsigned long flags);
      void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
      /* 内存映射的起始地址 */
      unsigned long mmap_base;    /* base of mmap area */
      /* 地址空间长度 */
      unsigned long task_size;    /* size of task vm space */
      unsigned long cached_hole_size;   /* if non-zero, the largest hole below free_area_cache */
      unsigned long free_area_cache;    /* first hole of size cached_hole_size or larger */
      pgd_t * pgd;
      atomic_t mm_users;      /* How many users with user space? */
      atomic_t mm_count;      /* How many references to "struct mm_struct" (users count as 1) */
      int map_count;        /* number of VMAs */
      struct rw_semaphore mmap_sem;
      spinlock_t page_table_lock;   /* Protects page tables and some counters */
    
      struct list_head mmlist;    /* List of maybe swapped mm's.  These are globally strung
                 * together off init_mm.mmlist, and are protected
                 * by mmlist_lock
                 */
    
      /* Special counters, in some configurations protected by the
       * page_table_lock, in other configurations by being atomic.
       */
      mm_counter_t _file_rss;
      mm_counter_t _anon_rss;
    
      unsigned long hiwater_rss;  /* High-watermark of RSS usage */
      unsigned long hiwater_vm; /* High-water virtual memory usage */
    
      unsigned long total_vm, locked_vm, shared_vm, exec_vm;
      unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
      /* 可执行代码和已初始化数据段, AMD64的start_code为0x400000 */
      unsigned long start_code, end_code, start_data, end_data;
      /* 堆 和 栈*/
      unsigned long start_brk, brk, start_stack;
      unsigned long arg_start, arg_end, env_start, env_end;
    
      unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
    
      cpumask_t cpu_vm_mask;
    
      /* Architecture-specific MM context */
      mm_context_t context;
    
      /* Swap token stuff */
      /*
       * Last value of global fault stamp as seen by this process.
       * In other words, this value gives an indication of how long
       * it has been since this task got the token.
       * Look at mm/thrash.c
       */
      unsigned int faultstamp;
      unsigned int token_priority;
      unsigned int last_interval;
    
      unsigned long flags; /* Must use atomic bitops to access the bits */
    
      /* coredumping support */
      int core_waiters;
      struct completion *core_startup_done, core_done;
    
      /* aio bits */
      rwlock_t    ioctx_list_lock;
      struct kioctx   *ioctx_list;
    };
    
  3. 虚拟地址空间建立过程(loadelfbinary)

    exec系统调用使用loadelfbinary来装载一个ELF二进制文件:

    2020-11-16_23-05-48_screenshot.png

    void arch_pick_mmap_layout(struct mm_struct *mm)
    {
    #ifdef CONFIG_IA32_EMULATION
      if (current_thread_info()->flags & _TIF_IA32)
        return ia32_pick_mmap_layout(mm);
    #endif
      mm->mmap_base = TASK_UNMAPPED_BASE;
      if (current->flags & PF_RANDOMIZE) {
        /* Add 28bit randomness which is about 40bits of address space
           because mmap base has to be page aligned.
           or ~1/128 of the total user VM
           (total user address space is 47bits) */
        unsigned rnd = get_random_int() & 0xfffffff;
        mm->mmap_base += ((unsigned long)rnd) << PAGE_SHIFT;
      }
      mm->get_unmapped_area = arch_get_unmapped_area;
      mm->unmap_area = arch_unmap_area;
    }
    
  4. 内存映射的原理

    由于虚拟地址空间比物理内存大得多,所以只有最常用的那部分内容才和物理页帧关联。 由于局部性原理,这样做是没有问题的。

    内核必须提供数据结构来建立虚拟地址空间的区域和相关数据所在位置之间的关联。比如在映射文本文件时, 映射的虚拟内存区必须关联到文件系统在硬盘上存储文件内容的区域。

    2020-11-16_23-27-41_screenshot.png 特别需要提到的是,上图只是简化的图示。因为文件数据在硬盘上的存储通常不是连续的,而是分布到 若干的小区域(块)。所以内核利用addressspace数据结构来提供一组方法从后备存储器(如文件系统) 读取数据。也就是说,addressspace形成了一个中间层,将映射的数据表示为连续的线性区域提供给 内存管理子系统。

    下图展示了这种机制在缺页异常时的处理过程:

    2020-11-16_23-41-23_screenshot.png 需要理解的是,图中只有少部分虚拟地址空间与物理页关联了,这也证实了上面的说法。

    1. struct vmareastruct

      每个虚拟内存区域都通过一个vmareastruct实例来描述:

      /*
       * This struct defines a memory VMM memory area. There is one of these
       * per VM-area/task.  A VM area is any part of the process virtual memory
       * space that has a special rule for the page-fault handlers (ie a shared
       * library, the executable area etc).
       */
      struct vm_area_struct {
        struct mm_struct * vm_mm; /* The address space we belong to. */
        unsigned long vm_start;   /* Our start address within vm_mm. */
        unsigned long vm_end;   /* The first byte after our end address
                   within vm_mm. */
      
        /* linked list of VM areas per task, sorted by address */
        struct vm_area_struct *vm_next;
      
        pgprot_t vm_page_prot;    /* Access permissions of this VMA. */
        /* 下面单独列出 */
        unsigned long vm_flags;   /* Flags, listed below. */
      
        struct rb_node vm_rb;
      
        /*
         * For areas with an address space and backing store,
         * linkage into the address_space->i_mmap prio tree, or
         * linkage to the list of like vmas hanging off its node, or
         * linkage of vma in the address_space->i_mmap_nonlinear list.
         */
        /* 详见下文 */
        union {
          struct {
            struct list_head list;
            /* 与prio_tree_node结构的最后一个成员是相同的,vm_set并不使用parent,
               内核可以用parent != NULL 检查vm_area_struct是否已经在 树中*/
            void *parent; /* aligns with prio_tree_node parent */
            struct vm_area_struct *head;
          } vm_set;
      
          /* 优先树 */
          struct raw_prio_tree_node prio_tree_node;
        } shared;
      
        /*
         * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
         * list, after a COW of one of the file pages.  A MAP_SHARED vma
         * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
         * or brk vma (with NULL file) can only be in an anon_vma list.
         */
        /* 管理匿名映射的共享页 */
        struct list_head anon_vma_node; /* Serialized by anon_vma->lock */
        struct anon_vma *anon_vma;  /* Serialized by page_table_lock */
      
        /* Function pointers to deal with this struct. */
        /* 操作集合,下面代码单独列出 */
        struct vm_operations_struct * vm_ops;
      
        /* Information about our backing store: */
        /* 文件映射的偏移量,用于只映射了文件部分内容时,如果映射了整个文件,则改值为0,
           偏移量的单位是页而不是字节,因为内核只支持以整页为单位的映射*/
        unsigned long vm_pgoff;   /* Offset (within vm_file) in PAGE_SIZE
                   units, *not* PAGE_CACHE_SIZE */
        /* 被映射的文件 */
        struct file * vm_file;    /* File we map to (can be NULL). */
        /* 少数声音和视频驱动使用了该选项 */
        void * vm_private_data;   /* was vm_pte (shared mem) */
        unsigned long vm_truncate_count;/* truncate_count or restart_addr */
      
      #ifndef CONFIG_MMU
        atomic_t vm_usage;    /* refcount (VMAs shared if !MMU) */
      #endif
      #ifdef CONFIG_NUMA
        struct mempolicy *vm_policy;  /* NUMA policy for the VMA */
      #endif
      };
      
      /*
       * vm_flags..
       */
      #define VM_READ   0x00000001  /* currently active flags */
      #define VM_WRITE  0x00000002
      #define VM_EXEC   0x00000004
      #define VM_SHARED 0x00000008
      
      /* mprotect() hardcodes VM_MAYREAD >> 4 == VM_READ, and so for r/w/x bits. */
      #define VM_MAYREAD  0x00000010  /* limits for mprotect() etc */
      #define VM_MAYWRITE 0x00000020
      #define VM_MAYEXEC  0x00000040
      #define VM_MAYSHARE 0x00000080
      
      #define VM_GROWSDOWN  0x00000100  /* general info on the segment */
      #define VM_GROWSUP  0x00000200
      #define VM_PFNMAP 0x00000400  /* Page-ranges managed without "struct page", just pure PFN */
      #define VM_DENYWRITE  0x00000800  /* ETXTBSY on write attempts.. */
      
      #define VM_EXECUTABLE 0x00001000
      #define VM_LOCKED 0x00002000
      #define VM_IO           0x00004000  /* Memory mapped I/O or similar */
      
                /* Used by sys_madvise() */
      #define VM_SEQ_READ 0x00008000  /* App will access data sequentially */
      #define VM_RAND_READ  0x00010000  /* App will not benefit from clustered reads */
      
      #define VM_DONTCOPY 0x00020000      /* Do not copy this vma on fork */
      #define VM_DONTEXPAND 0x00040000  /* Cannot expand with mremap() */
      #define VM_RESERVED 0x00080000  /* Count as reserved_vm like IO */
      #define VM_ACCOUNT  0x00100000  /* Is a VM accounted object */
      #define VM_HUGETLB  0x00400000  /* Huge TLB Page VM */
      #define VM_NONLINEAR  0x00800000  /* Is non-linear (remap_file_pages) */
      #define VM_MAPPED_COPY  0x01000000  /* T if mapped copy of data (nommu mmap) */
      #define VM_INSERTPAGE 0x02000000  /* The vma has had "vm_insert_page()" done on it */
      #define VM_ALWAYSDUMP 0x04000000  /* Always include in core dumps */
      
      #define VM_CAN_NONLINEAR 0x08000000 /* Has ->fault & does nonlinear pages */
      
      #ifndef VM_STACK_DEFAULT_FLAGS    /* arch can override this */
      #define VM_STACK_DEFAULT_FLAGS VM_DATA_DEFAULT_FLAGS
      #endif
      
      #ifdef CONFIG_STACK_GROWSUP
      #define VM_STACK_FLAGS  (VM_GROWSUP | VM_STACK_DEFAULT_FLAGS | VM_ACCOUNT)
      #else
      #define VM_STACK_FLAGS  (VM_GROWSDOWN | VM_STACK_DEFAULT_FLAGS | VM_ACCOUNT)
      #endif
      
      #define VM_READHINTMASK     (VM_SEQ_READ | VM_RAND_READ)
      #define VM_ClearReadHint(v)   (v)->vm_flags &= ~VM_READHINTMASK
      #define VM_NormalReadHint(v)    (!((v)->vm_flags & VM_READHINTMASK))
      #define VM_SequentialReadHint(v)  ((v)->vm_flags & VM_SEQ_READ)
      #define VM_RandomReadHint(v)    ((v)->vm_flags & VM_RAND_READ)
      
      
      /*
       * These are the virtual MM functions - opening of an area, closing and
       * unmapping it (needed to keep files on disk up-to-date etc), pointer
       * to the functions called when a no-page or a wp-page exception occurs. 
       */
      struct vm_operations_struct {
        /* 创建和删除区域 */
        void (*open)(struct vm_area_struct * area);
        void (*close)(struct vm_area_struct * area);
        /* 缺页处理函数 */
        int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
        /* 老的缺页处理函数,新代码不应该用 */
        struct page *(*nopage)(struct vm_area_struct *area,
            unsigned long address, int *type);
        unsigned long (*nopfn)(struct vm_area_struct *area,
            unsigned long address);
      
        /* notification that a previously read-only page is about to become
         * writable, if an error is returned it will cause a SIGBUS */
        int (*page_mkwrite)(struct vm_area_struct *vma, struct page *page);
      #ifdef CONFIG_NUMA
        int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
        struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
                unsigned long addr);
        int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from,
          const nodemask_t *to, unsigned long flags);
      #endif
      };
      

      进程的各个区域按两种方法排序:

      1. 在一个单链表上(mmstruct->mmap)
      2. 在红黑树中,根结点位于mmrb

      2020-11-17_09-47-16_screenshot.png

      1. 对区域的操作

        内核提供了各种函数来操作进程的虚拟内存区域:

        2020-11-17_14-56-29_screenshot.png

        2020-11-17_14-57-01_screenshot.png

    2. 地址空间(addressapace)

      文件的内存映射可以认为是两个不同的地址空间之间的映射,一个是用户进程的虚拟地址空间, 另一个是文件系统所在的地址空间。

      在内核创建一个映射时,必须建立两个地址空间之间的关联,以支持二者以读写请求的形式通信。 所以内核通过addressspace结构来充当这两者通信的桥梁。事实上,每个文件映射都有一个 相关的addressspace实例。而且每个实例都有一组相关的操作:

      struct address_space {
        struct inode    *host;    /* owner: inode, block_device */
        struct radix_tree_root  page_tree;  /* radix tree of all pages */
        rwlock_t    tree_lock;  /* and rwlock protecting it */
        unsigned int    i_mmap_writable;/* count VM_SHARED mappings */
        struct prio_tree_root i_mmap;   /* tree of private and shared mappings */
        struct list_head  i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
        spinlock_t    i_mmap_lock;  /* protect tree, count, list */
        unsigned int    truncate_count; /* Cover race condition with truncate */
        unsigned long   nrpages;  /* number of total pages */
        pgoff_t     writeback_index;/* writeback starts here */
        const struct address_space_operations *a_ops; /* methods */
        unsigned long   flags;    /* error bits/gfp mask */
        struct backing_dev_info *backing_dev_info; /* device readahead, etc */
        spinlock_t    private_lock; /* for use by the address_space */
        struct list_head  private_list; /* ditto */
        struct address_space  *assoc_mapping; /* ditto */
      } __attribute__((aligned(sizeof(long))));
      
      struct address_space_operations {
        int (*writepage)(struct page *page, struct writeback_control *wbc);
        int (*readpage)(struct file *, struct page *);
        void (*sync_page)(struct page *);
      
        /* Write back some dirty pages from this mapping. */
        int (*writepages)(struct address_space *, struct writeback_control *);
      
        /* Set a page dirty.  Return true if this dirtied it */
        int (*set_page_dirty)(struct page *page);
      
        int (*readpages)(struct file *filp, struct address_space *mapping,
            struct list_head *pages, unsigned nr_pages);
      
        /*
         * ext3 requires that a successful prepare_write() call be followed
         * by a commit_write() call - they must be balanced
         */
        int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
        int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
      
        int (*write_begin)(struct file *, struct address_space *mapping,
              loff_t pos, unsigned len, unsigned flags,
              struct page **pagep, void **fsdata);
        int (*write_end)(struct file *, struct address_space *mapping,
              loff_t pos, unsigned len, unsigned copied,
              struct page *page, void *fsdata);
      
        /* Unfortunately this kludge is needed for FIBMAP. Don't use it */
        sector_t (*bmap)(struct address_space *, sector_t);
        void (*invalidatepage) (struct page *, unsigned long);
        int (*releasepage) (struct page *, gfp_t);
        ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
            loff_t offset, unsigned long nr_segs);
        struct page* (*get_xip_page)(struct address_space *, sector_t,
            int);
        /* migrate the contents of a page to the specified target */
        int (*migratepage) (struct address_space *,
            struct page *, struct page *);
        int (*launder_page) (struct page *);
      };
      
    3. 优先查找树

      优先查找树用于建立文件中的一个区域与该区域映射到的所有虚拟地址空间之间的关联。 每个文件(和块设备,因为块设备也可以通过设备文件进行内存映射)都表示为一个struct file, 该结构包括一个指向地址空间对象struct addressspace的指针, 该结构是优先查找树的基础:

      struct file{
        ...
        struct address_space *f_mapping;
        ...
      }
      
      struct address_space {
        struct inode    *host;    /* owner: inode, block_device */
        struct radix_tree_root  page_tree;  /* radix tree of all pages */
        rwlock_t    tree_lock;  /* and rwlock protecting it */
        unsigned int    i_mmap_writable;/* count VM_SHARED mappings */
        struct prio_tree_root i_mmap;   /* tree of private and shared mappings */
        struct list_head  i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
        spinlock_t    i_mmap_lock;  /* protect tree, count, list */
        unsigned int    truncate_count; /* Cover race condition with truncate */
        unsigned long   nrpages;  /* number of total pages */
        pgoff_t     writeback_index;/* writeback starts here */
        const struct address_space_operations *a_ops; /* methods */
        unsigned long   flags;    /* error bits/gfp mask */
        struct backing_dev_info *backing_dev_info; /* device readahead, etc */
        spinlock_t    private_lock; /* for use by the address_space */
        struct list_head  private_list; /* ditto */
        struct address_space  *assoc_mapping; /* ditto */
      } __attribute__((aligned(sizeof(long))));
      

      struct file是open系统调用打开的文件的抽象,而在文件系统中,文件被表示为struct inode:

      struct inode {
        ...
        struct address_apace *i_mapping;
        ...
      }
      

      在打开文件时,内核通过addressspace将file->fmapping设置到inode->imapping把每个进程 的文件抽象与文件系统的文件表示联系起来:

      2020-11-17_10-57-31_screenshot.png

      优先查找树是通过vmareastruct中的shared联合体实现的

      union {
        struct {
          struct list_head list;
          /* 与prio_tree_node结构的最后一个成员是相同的,vm_set并不使用parent,
             内核可以用parent != NULL 检查vm_area_struct是否已经在 树中*/
          void *parent; /* aligns with prio_tree_node parent */
          struct vm_area_struct *head;
        } vm_set;
      
        /* 优先树 */
        struct raw_prio_tree_node prio_tree_node;
      } shared;
      
      

      这种结构即能够处理重合的区间:

      2020-11-17_14-20-33_screenshot.png 还能处理相同的文件区间:

      2020-11-17_14-41-03_screenshot.png 上图表示的是许多进程映射到 相同区间 的情况下,这些进程的相关区间的vmareastruct 通过vmset中的链表联系在一起。其中,union shared可以同时用它的两个成员priotreenode 和vmset. 也就是说,一个priotreenode代表的区间可能有多个vmset组成一个链表

    4. 内存映射相关系统调用
      • mmap 如我们熟悉的,C标准库提供了mmap函数建立映射,内核也提供了两个系统调用mmap和mmap2:

        #include <sys/mman.h>
        
        void *
        mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
        

        上面是标准库mmap的接口,两个系统调用的接口也差不多。 这些调用都会在用户虚拟地址空间中的addr位置建立长度为len的映射,prot指定了其访问权限, 相关文件由文件描述符fd来标识, offset表示文件的开始位置。 mmap最终会委托给dommappgoff函数其调用流程为:

        2020-11-17_15-58-34_screenshot.png

      • munmap 从虚拟地址空间删除现存映射,必须使用munmap系统调用,sysmunmap是系统调用的入口, 它会将工作委托给domunmap函数:

        2020-11-17_16-01-30_screenshot.png

      • 非线性映射 如果需要将文件的不同部分以不同的顺序映射到虚拟内存的连续区域中,通常必须使用几个映射, 这种代价是非常高的(需要多个vmareastruct),非线性映射解决了这个问题:

        asmlinkage long sys_remap_file_pages(unsigned long start, unsigned long size,
                                             unsigned long prot, unsigned long pgoff, unsigned long flags)
        {
        

        该系统调用通过操作进程的页表来实现重排映射中的页,使的内存与文件中的顺序不再等价。 其代码流程为:

        2020-11-17_16-07-22_screenshot.png

      • 反向映射 内核利用之前讨论的数据结构,可以建立虚拟地址和物理地址之间的联系(通过页表); 也可以建立进程的内存区域与其虚拟内存页地址之间的关联(通过vmareastruct中的vmstart); 仍然缺失的一个联系是:一个物理页与所有使用该页的进程的对应页表项之间的联系,这种联系也 叫反向映射,该机制主要通过struct page结构体中的两个成员来实现: 对于匿名页来说,主要通过page->mapping指向vmareastruct的annovma来完成 对于文件映射的页来说,通过page->mapcount就可以完成,因为struct addressspace 中的immap保存了映射该页的所有vmareastruct
    5. 堆的管理

      堆是进程中用于动态分配变量和数据的内存区域,前面提到的mmstruct结构,包含了堆在虚拟地址 空间中的起始和当前结束地址(startbrk和brk)molloc和内核之间的经典接口是brk系统调用,负责 扩展/收缩堆。的malloc的实现都使用brk和匿名映射的组合。该方法提供了更好的性能,而且在分配 较大的内存区时具有某些优点。

      brk系统调用:

      #include <unistd.h>
      
      /* addr表示堆在地址空间中新的结束地址 */
      void *
      brk(const void *addr);
      

      brk系统调用并不是内核独立的概念,而是基于匿名映射实现的,以减少内部的开销。brk系统调用 实现的入口是sysbrk函数

      2020-11-17_17-48-14_screenshot.png

  5. TODO 缺页异常处理

    处理器在产生缺页异常时会自动把错误地址存在cr2寄存器中,可以用readcr2来获取

    处理代码流程的粗略概观:

    2020-11-17_18-29-08_screenshot.png 其代码主要为dopagefault:

    2020-11-17_18-30-28_screenshot.png 其中handlemmfault会把任务委托给handleptefault函数,后者分析缺页异常的原因:

    int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                        unsigned long address, int write_access)
    {
      pgd_t *pgd;
      pud_t *pud;
      pmd_t *pmd;
      pte_t *pte;
    
      __set_current_state(TASK_RUNNING);
    
      count_vm_event(PGFAULT);
    
      if (unlikely(is_vm_hugetlb_page(vma)))
        return hugetlb_fault(mm, vma, address, write_access);
    
      pgd = pgd_offset(mm, address);
      pud = pud_alloc(mm, pgd, address);
      if (!pud)
        return VM_FAULT_OOM;
      pmd = pmd_alloc(mm, pud, address);
      if (!pmd)
        return VM_FAULT_OOM;
      pte = pte_alloc_map(mm, pmd, address);
      if (!pte)
        return VM_FAULT_OOM;
    
      return handle_pte_fault(mm, vma, address, pte, pmd, write_access);
    }
    
    /*
     * These routines also need to handle stuff like marking pages dirty
     * and/or accessed for architectures that don't do it in hardware (most
     * RISC architectures).  The early dirtying is also good on the i386.
     *
     * There is also a hook called "update_mmu_cache()" that architectures
     * with external mmu caches can use to update those (ie the Sparc or
     * PowerPC hashed page tables that act as extended TLBs).
     *
     * We enter with non-exclusive mmap_sem (to exclude vma changes,
     * but allow concurrent faults), and pte mapped but not yet locked.
     * We return with mmap_sem still held, but pte unmapped and unlocked.
     */
    static inline int handle_pte_fault(struct mm_struct *mm,
        struct vm_area_struct *vma, unsigned long address,
        pte_t *pte, pmd_t *pmd, int write_access)
    {
      pte_t entry;
      spinlock_t *ptl;
    
      entry = *pte;
      if (!pte_present(entry)) {
        if (pte_none(entry)) {
          if (vma->vm_ops) {
            if (vma->vm_ops->fault || vma->vm_ops->nopage)
              return do_linear_fault(mm, vma, address,
                pte, pmd, write_access, entry);
            if (unlikely(vma->vm_ops->nopfn))
              return do_no_pfn(mm, vma, address, pte,
                   pmd, write_access);
          }
          return do_anonymous_page(mm, vma, address,
                 pte, pmd, write_access);
        }
        if (pte_file(entry))
          return do_nonlinear_fault(mm, vma, address,
              pte, pmd, write_access, entry);
        return do_swap_page(mm, vma, address,
              pte, pmd, write_access, entry);
      }
    
      ptl = pte_lockptr(mm, pmd);
      spin_lock(ptl);
      if (unlikely(!pte_same(*pte, entry)))
        goto unlock;
      if (write_access) {
        if (!pte_write(entry))
          return do_wp_page(mm, vma, address,
              pte, pmd, ptl, entry);
        entry = pte_mkdirty(entry);
      }
      entry = pte_mkyoung(entry);
      if (ptep_set_access_flags(vma, address, pte, entry, write_access)) {
        update_mmu_cache(vma, address, entry);
      } else {
        /*
         * This is needed only for protection faults but the arch code
         * is not yet telling us if this is a protection fault or not.
         * This still avoids useless tlb flushes for .text page faults
         * with threads.
         */
        if (write_access)
          flush_tlb_page(vma, address);
      }
    unlock:
      pte_unmap_unlock(pte, ptl);
      return 0;
    }
    

    如果页不存在物理内存中,即上面代码中!ptepresent(entry),则分下面三种情况分别处理:

    2020-11-17_18-37-16_screenshot.png 其中每种情况的具体实现待TODO

3.9.4 TODO 锁与进程间通信

TODO 同步与通信 通常,各个进程必须尽可能保持独立,避免彼此干扰。但有时候,应用程序必须彼此通信:

  1. 一个进程生成的数据传输到另一个进程时
  2. 数据由多个进程共享时
  3. 进程必须彼此等待时
  4. 需要协调资源的使用时
  5. 竞态条件 如果多个进程共享一个资源,很容易彼此干扰。几个进程在访问资源时彼此干扰的情况通常称为 竞态条件
  6. 锁 用户空间程序和内核都需要保护资源。为阻止CPU彼此干扰,需要通过锁保护内核的某些范围。 确保每次只能有一个CPU访问被保护的范围。

内核提供了四种锁选项:

  • 原子操作 最简单的锁操作
  • 自旋锁 最常用的锁
  • 信号量 经典实现,互斥量是信号量的特例
  • 读写锁
  1. 信号量

    为什么把信号量放在最前面,因为这是第一个提出的同步原语,比互斥量要早。 在用户层可以正常工作,但是其开销对于内核来说还是太大了,所以内核提供了许多不同的锁和同步机制。

  2. 原子操作

    从汇编的角度来说,加一操作通常分为3步:

    1. 将值从内存读入寄存器
    2. 将寄存器中的值+1
    3. 将寄存器数据回写到内存

    原子操作由CPU的特殊指令来完成,如x86系统上这个指令为lock

  3. 自旋锁

    spinlock和spinunlock 通常在启用了内核抢占的单处理器内核中,spinlock基本上等价于preemptdisable,而spinunlock 则等价于preemptenable

  4. RCU(read-copy-update)机制
  5. 内存和优化屏障
  6. 读写锁
  7. 大内核锁
  8. 互斥量

3.9.5 设备驱动程序

应用程序与外设的通信是层次化的:

2020-11-17_20-47-23_screenshot.png

外设并不直接连接到cpu,而是通过总线连接起来,无论采用什么体系结构的处理器,系统都不会只有一种总线, 而是一些总线的组合:

2020-11-17_20-52-17_screenshot.png

  1. 与外设交互的方式
    1. I/O端口 处理器管理了一个独立的虚拟地址空间,可用于管理所有I/O地址。
    2. I/O内存映射
    3. 通过总线控制设备 并非所有的设备都是直接通过I/O语句寻址,也有通过总线系统访问的。
  2. 中断和轮询

    判断设备数据是否可用的两种方法

  3. 访问设备

    设备特殊文件(设备文件) 用于访问扩展设备。这些文件与硬盘或其他存储介质没有任何关系,而是 建立了与 某个 设备驱动程序 的连接,以支持与扩展设备的通信。

    1. 设备文件标识(主从设备号)

      设备主要通过主从设备号来标识,而不是设备文件名。 这识因为内核采用主从设备号来标识匹配的驱动程序,主设备号用于寻址驱动程序自身, 而且一个驱动程序可以分配多个主设备号。 采用主从两个号码主要有以下原因:

      1. 系统可能包含几个同类型的设备,他们由同一个设备驱动程序管理。
      2. 通过从设备号可以将同类设备合并起来,便于管理

      如下面的例子:

      2020-11-18_09-23-27_screenshot.png

      两个硬盘sda和sdb所在的第一个SATA控制权的主设备号是8.而驱动程序管理的这两个硬盘则通过 不同的设备号来指定。sda对于0,sdb对应16.而0与16之间的其它数字,主要用于识别各个 分区。如下面的sda:

      2020-11-18_09-20-28_screenshot.png

    2. 设备文件的创建

      可用设备结点有20000多项,但一般系统只需要包含其中少量设备,因此,linux将/dev内容的管理 工作切换到 udevd守护进程, 允许从用户层动态创建设备文件。其基本思想如下:

      2020-11-18_09-31-52_screenshot.png 每当内核检测到一个设备时,都会创建一个 内核对象kobject, 该对象借助于 sysfs文件系统 导出到用户层。

    3. 设备寻址(进行设备配置)

      /dev放在tmpfs文件系统中,他是RAM磁盘文件系统ramfs的一种变体。所以设备文件结点不是持久的。 可以使用输入输出控制接口ioctl来寻址设备文件。它是用来配置和修改特定设备属性的通用接口。

    4. 设备的数据结构表示

      每个字符设备都表示为一个struct cdev,块设备表示为一个struct bdev,有两个全局数组 bdevmap(用于块设备),cdevmap(用于字符设备)来实现散列表,使用的散列键是主设备号。 两个数组都是kobjmap的实例

      2020-11-18_10-33-44_screenshot.png

      struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;
      };
      
      
      
      
      struct bdev_inode {
        struct block_device bdev;
        struct inode vfs_inode;
      };
      
      struct block_device {
        dev_t     bd_dev;  /* not a kdev_t - it's a search key */
        struct inode *    bd_inode; /* will die */
        int     bd_openers;
        struct mutex    bd_mutex; /* open/close mutex */
        struct semaphore  bd_mount_sem;
        struct list_head  bd_inodes;
        void *      bd_holder;
        int     bd_holders;
      #ifdef CONFIG_SYSFS
        struct list_head  bd_holder_list;
      #endif
        struct block_device * bd_contains;
        unsigned    bd_block_size;
        struct hd_struct *  bd_part;
        /* number of times partitions within this device have been opened. */
        unsigned    bd_part_count;
        int     bd_invalidated;
        struct gendisk *  bd_disk;
        struct list_head  bd_list;
        struct backing_dev_info *bd_inode_backing_dev_info;
        /*
         * Private data.  You must have bd_claim'ed the block_device
         * to use this.  NOTE:  bd_claim allows an owner to claim
         * the same device multiple times, the owner must take special
         * care to not mess up bd_private for that case.
         */
        unsigned long   bd_private;
      };
      
    5. 与文件系统的关联

      除极少数例外,设备文件都是由标准函数处理,类似于普通文件。它们都通过虚拟文件系统管理。 在打开一个设备文件时,各种文件系统的实现回调用initspecialinode函数,为块设备或 字符设备文件创建一个inode,inode中设备文件驱动程序有关的成员如下:

      struct inode {
        ...
        /* 主从设备号 */
        dev_t i_rdev;
        ...
        /* 存储了文件类型 */
        unode_t i_mode;
        ...
        /* 由虚拟文件系统使用来处理块设备 */
        struct file_operations *i_fop;
        ...
        /* 内核根据文件类型来选择块设备还是字符设备 */
        union {
          ...
          struct block_device *i_bdev;
          struct cdev *i_cdev;
        };
        ...
      };
      
      void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
      {
        inode->i_mode = mode;
        if (S_ISCHR(mode)) {
          inode->i_fop = &def_chr_fops;
          inode->i_rdev = rdev;
        } else if (S_ISBLK(mode)) {
          inode->i_fop = &def_blk_fops;
          inode->i_rdev = rdev;
        } else if (S_ISFIFO(mode))
          inode->i_fop = &def_fifo_fops;
        else if (S_ISSOCK(mode))
          inode->i_fop = &bad_sock_fops;
        else
          printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o)\n",
                 mode);
      }
      
      /*
       * Dummy default file-operations: the only thing this does
       * is contain the open that then fills in the correct operations
       * depending on the special file...
       */
      const struct file_operations def_chr_fops = {
        .open = chrdev_open,
      };
      
      
      const struct file_operations def_blk_fops = {
        .open   = blkdev_open,
        .release  = blkdev_close,
        .llseek   = block_llseek,
        .read   = do_sync_read,
        .write    = do_sync_write,
        .aio_read = generic_file_aio_read,
        .aio_write  = generic_file_aio_write_nolock,
        .mmap   = generic_file_mmap,
        .fsync    = block_fsync,
        .unlocked_ioctl = block_ioctl,
      #ifdef CONFIG_COMPAT
        .compat_ioctl = compat_blkdev_ioctl,
      #endif
        .splice_read  = generic_file_splice_read,
        .splice_write = generic_file_splice_write,
      };
      
    6. TODO 设备文件各种操作的流程

3.9.6 TODO 模块

3.9.7 虚拟文件系统

为了支持各种文件系统,linux内核在用户进程(C标准库)和文件系统实现之间引入了一个 抽象层 虚拟文件系统

2020-11-18_14-02-01_screenshot.png

  1. 文件系统分类
    1. 基于磁盘的文件系统
    2. 虚拟文件系统 由内核生成,是一种使用户应用程序与用户通信的方法。如proc文件系统. 内核建立了一个层次化的文件结构,其中的项包含了与系统特定部分相关的信息
    3. 网络文件系统
  2. 通用文件模型

    在处理文件时,内核与用户空间使用的主要对象是不同的。 对用户程序来说,一个文件主要由一个文件描述符标识,它只在每个进程内部有效; 对于内核来说,一个文件则由inode来标识,一个文件(目录)都有且只有一个对应的inode

    • inode的成员可以分为下面两类:

      1. 描述文件状态的元数据
      2. 保存实际文件内容的数据段(或指向数据的指针)

      要注意的是,inode并不包含文件名

  3. VFS结构
    1. 结构概观

      2020-11-18_16-12-04_screenshot.png 其中,在抽象对底层文件系统访问时,并未使用固定的函数,而是使用了函数指针。他们分为两类:

      1. inode操作:创建链接,文件重命名,在目录中生成新文件,删除文件
      2. 文件操作:作用于文件的数据内容
    2. inode

      VFS的inode结构如下: 我们应该记住这里的inode是用于内存中进行处理的,因而包含了一些实际介质上存储的inode 所没有的成员。这些是由内核自身在从底层文件系统读入信息时生成或动态建立。

      /* 该结构包括几个链表元素,用于分类管理各个inode。 */
      struct inode {
        /* 用于散列表inode_hashtable支持,散列表则用来根据inode编号和超级块快速访问inode,这两项的组合在系统范围内是唯一的 */
        struct hlist_node i_hash;
        /* 内核定义了两个全局变量用作表头,inode_unused用于有效但非活动的inode;
           inode_in_use用于所有使用但未改变的inode;脏的inode保存在一个特定于
           超级块的链表super_block->s_dirty中 */
        struct list_head  i_list;
        /* inode也通过一个特定于超级块的链表super_block->s_inodes维护 */
        struct list_head  i_sb_list;
        struct list_head  i_dentry;
        /* 每个VFS inode都由一个唯一的编号标识 */
        unsigned long   i_ino;
        /* 访问该inode结构的进程数目 */
        atomic_t    i_count;
        unsigned int    i_nlink;
        uid_t     i_uid;
        gid_t     i_gid;
        /* 当表示设备文件时需要,表示与哪个设备进行通信,我们最终会找到struct block_device的一个实例 */
        dev_t     i_rdev;
        unsigned long   i_version;
        /* 文件长度,按字节计算 */
        loff_t      i_size;
      #ifdef __NEED_I_SIZE_ORDERED
        seqcount_t    i_size_seqcount;
      #endif
        /* 最后访问时间 */
        struct timespec   i_atime;
        /* 最后修改时间 */
        struct timespec   i_mtime;
        /* 最后修改inode时间 */
        struct timespec   i_ctime;
        unsigned int    i_blkbits;
        /* 文件按块计算的长度 */
        blkcnt_t    i_blocks;
        unsigned short          i_bytes;
        /* 文件访问权限和所有权 */
        umode_t     i_mode;
        spinlock_t    i_lock; /* i_blocks, i_bytes, maybe i_size */
        struct mutex    i_mutex;
        struct rw_semaphore i_alloc_sem;
        /* 负责管理结构性的操作(如删除一个文件)和文件相关的元数据(如属性) */
        const struct inode_operations *i_op;
        /* 用于操作文件中包含的数据 */
        const struct file_operations  *i_fop; /* former ->i_op->default_file_ops */
        struct super_block  *i_sb;
        struct file_lock  *i_flock;
        struct address_space  *i_mapping;
        struct address_space  i_data;
      #ifdef CONFIG_QUOTA
        struct dquot    *i_dquot[MAXQUOTAS];
      #endif
        struct list_head  i_devices;
        /* 如果表示设备文件,则包含指向设备专用数据结构 */
        union {
          struct pipe_inode_info  *i_pipe;
          struct block_device *i_bdev;
          struct cdev   *i_cdev;
        };
        int     i_cindex;
      
        __u32     i_generation;
      
      #ifdef CONFIG_DNOTIFY
        unsigned long   i_dnotify_mask; /* Directory notify events */
        struct dnotify_struct *i_dnotify; /* for directory notifications */
      #endif
      
      #ifdef CONFIG_INOTIFY
        struct list_head  inotify_watches; /* watches on this inode */
        struct mutex    inotify_mutex;  /* protects the watches list */
      #endif
      
        unsigned long   i_state;
        unsigned long   dirtied_when; /* jiffies of first dirtying */
      
        unsigned int    i_flags;
      
        atomic_t    i_writecount;
      #ifdef CONFIG_SECURITY
        void      *i_security;
      #endif
        void      *i_private; /* fs or device private pointer */
      };
      
    3. 特定于进程的信息

      文件描述符用于在一个进程内唯一的标示打开的文件。每个进程的taskstruct中包含了 用于在进程描述符和内核内部使用的结构之间建立联系的数据成员:

      struct task_struct {
        ...
        /* 文件系统信息 */
        /* 用于在查找环形链表时防止无限循环 */
        int link_cont, total_link_count;
        ...
        /* 文件系统信息 */
        struct fs_struct *fs;
        /* 打开文件信息,包含进程的各个文件描述符 */
        struct files_struct *files;
        /* 命名空间 */
        struct nsproxy *nsproxy;
        ...
      }
      
      struct files_struct {
        /*
         * read mostly part
         */
        atomic_t count;
        /* 多包含一个fatable的指针是用于RCU */
        struct fdtable *fdt;
        struct fdtable fdtab;
        /*
         * written part on a separate cache line in SMP
         */
        spinlock_t file_lock ____cacheline_aligned_in_smp;
        /* 下一次打开新文件时使用的文件描述符 */
        int next_fd;
        /* 位图 */
        struct embedded_fd_set close_on_exec_init;
        struct embedded_fd_set open_fds_init;
        /* 指向每个打开文件的struct file实例(下给出了详细结构), 如果进程试图打开比NR_OPEN_DEFAULT
           更多的文件,内核必须对files_struct中用于管理与进程相关的所有文件信息的各个成员
           分配更多的内存空间,最重要的是fdtable
        */
        struct file * fd_array[NR_OPEN_DEFAULT];
      };
      
      /*
       * The embedded_fd_set is a small fd_set,
       * suitable for most tasks (which open <= BITS_PER_LONG files)
       */
      struct embedded_fd_set {
        unsigned long fds_bits[1];
      };
      
      
      /* 初看起来,fdtable和files_struct之间某些信息似乎是重复的,其实fdtable中的成员
         都是指针,初始时都指向了后者的对应成员, 当需要打开的文件超过了NR_OPEN_DEFAULT时
         ,内核会分配一个fd_set的实例, 替换最初的embedded_fd_set*/
      struct fdtable {
        /* 进程当前可以处理的文件对象和文件描述符的最大数目 */
        unsigned int max_fds;
        /* 指针数组,每个数组项管理一个打开文件的所有信息, 文件描述符充当数组索引,长度由max_fds定义 */
        struct file ** fd;      /* current fd array */
        fd_set *close_on_exec;
        /* 指向管理所有打开文件描述符的位域,bit置位标示文件描述符在使用中 */
        fd_set *open_fds;
        struct rcu_head rcu;
        struct fdtable *next;
      };
      
      
      struct file {
        /*
         * fu_list becomes invalid after file_free is called and queued via
         * fu_rcuhead for RCU freeing
         */
        union {
          struct list_head  fu_list;
          struct rcu_head   fu_rcuhead;
        } f_u;
        /* 封装了:
           1. 文件名和inode之间的关联
           2. 文件所在文件系统的信息
           后面代码给出了详细结构*/
        struct path   f_path;
      #define f_dentry  f_path.dentry
      #define f_vfsmnt  f_path.mnt
        /* 文件操作 */
        const struct file_operations  *f_op;
        atomic_t    f_count;
        /* open系统调用传递的额外标志 */
        unsigned int    f_flags;
        mode_t      f_mode;
        /* 文件位置当前值 */
        loff_t      f_pos;
        /* 处理该文件的进程有关的信息 */
        struct fown_struct  f_owner;
        unsigned int    f_uid, f_gid;
        /* 预读特征,指定是否预读,如何预读*/
        struct file_ra_state  f_ra;
      
        /* 用于文件系统检查一个file实例是否仍然与相关的inode内容兼容,
           这对于确保已缓存对象的一致性很重要 */
        u64     f_version;
      #ifdef CONFIG_SECURITY
        void      *f_security;
      #endif
        /* needed for tty driver, and maybe others */
        void      *private_data;
      
      #ifdef CONFIG_EPOLL
        /* Used by fs/eventpoll.c to link all the hooks to this file */
        struct list_head  f_ep_links;
        spinlock_t    f_ep_lock;
      #endif /* #ifdef CONFIG_EPOLL */
        /* 属于文件相关的inode实例的地址空间映射 */
        struct address_space  *f_mapping;
      };
      
      struct path {
        /* 所在文件系统 */
        struct vfsmount *mnt;
        /* 提供文件名与inode之间的关联 */
        struct dentry *dentry;
      };
      
      
      struct fs_struct {
        atomic_t count;
        rwlock_t lock;
        /* 新文件权限 */
        int umask;
        /* 根目录,当前目录,个性 */
        struct dentry * root, * pwd, * altroot;
        /* 上面目录对应的文件系统 */
        struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
      };
      
    4. VFS命名空间

      VFS命名空间是已经装载、构成某个容器目录树的文件系统的集合。其数据结构为

      struct mnt_namespace {
        /* 使用该命名空间的进程数目 */
        atomic_t    count;
        /* 根目录 */
        struct vfsmount * root;
        /* VFS命名空间中所有文件系统的vfsmount实例 */
        struct list_head  list;
        wait_queue_head_t poll;
        int event;
      };
      

      命令空间操作(mount和umount)并不作用于内核的全局数据结构,而是当前进程的命名空间。

    5. 目录项缓存

      由于块设备速度很慢,所以找一个文件名相关的inode。而且即使inode指向的数据内容都已经在内存 中了(页缓存),仍然会重复整个查找过程(可能会读块设备)。所以linux使用了页目录缓存(dentry 缓存)来快速访问此前查找的结果。在VFS连同文件系统实现读取的一个目录项(目录或文件)的数据 之后,则创建了一个dentry实例,以缓存找到的数据。

      dentry数据结构如下:

      struct dentry {
        atomic_t d_count;
        unsigned int d_flags;   /* protected by d_lock */
        spinlock_t d_lock;    /* per dentry lock */
        /* 指向相关的inode实例的指针,如果dentry对象是为一个不存在的文件名建立,
           则d_ionde为null。这有助于加速查找不存在的文件名,因为没有缓存的话,
           查找一个实际不存在的文件名和查找一个存在的文件同样耗时 */
        struct inode *d_inode;    /* Where the name belongs to - NULL is
                 * negative */
        /*
         * The next three fields are touched by __d_lookup.  Place them here
         * so they all fit in a cache line.
         */
        struct hlist_node d_hash; /* lookup hash list */
        /* 当前结点父目录的dentry实例,当前dentry位于父目录的d_subdirs中,
           根目录指向其自身*/
        struct dentry *d_parent;  /* parent directory */
        /* 文件的名称 */
        struct qstr d_name;
      
        struct list_head d_lru;   /* LRU list */
        /*
         * d_child and d_rcu can share memory
         */
        union {
          struct list_head d_child; /* child of parent list */
          struct rcu_head d_rcu;
        } d_u;
        /* 子目录/文件的目录项链表 */
        struct list_head d_subdirs; /* our children */
        /* 链表元素,用于将dentry连接到inode的i_dentry链表中 */
        struct list_head d_alias; /* inode alias list */
        unsigned long d_time;   /* used by d_revalidate */
        /* 对dentry对象的各种操作,详见下面代码 */
        struct dentry_operations *d_op;
        /* dentry所属文件系统的超级块 */
        struct super_block *d_sb; /* The root of the dentry tree */
        void *d_fsdata;     /* fs-specific data */
      #ifdef CONFIG_PROFILING
        struct dcookie_struct *d_cookie; /* cookie, if any */
      #endif
        /* 1表示当前dentry对象表示一个装载点,否则为0 */
        int d_mounted;
        /* 名字比较短的话存在这,加速访问 */
        unsigned char d_iname[DNAME_INLINE_LEN_MIN];  /* small names */
      };
      
      
      struct dentry_operations {
        int (*d_revalidate)(struct dentry *, struct nameidata *);
        int (*d_hash) (struct dentry *, struct qstr *);
        /* 比较两个dentry对象的文件名 */
        int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
        /* d_count为0时调用 */
        int (*d_delete)(struct dentry *);
        void (*d_release)(struct dentry *);
        /*  从dentry对象中释放inode */
        void (*d_iput)(struct dentry *, struct inode *);
        char *(*d_dname)(struct dentry *, char *, int);
      };
      

      内存中所有活动的dentry实例都保存在一个散列表中,该散列表使用全局变量dentryhashtable实现。 成为全局 dentry散列表

      每个由VFS发送到底层实现的请求,都会导致创建一个新的dentry对象,以保存请求的结果。dentry对象 在内存中的组织,涉及下面两个部分:

      1. 全局dentry散列表 dentryhashtable
      2. 一个LRU链表,用来移除不再使用的对象。该链表的表头是全局变量dentryunused,使用的链表元素 是dentry的dlru成员
  4. 处理VFS对象
    1. 文件系统操作
      1. 注册文件系统 内核通过registerfilesystem来向内核祖泽文件系统。该函数非常简单,所有注册的文件系统会 存于一个链表中,注册新文件系统时会扫描链表确认文件系统没有被注册过(否则返回错误),然后 把新文件系统挂在链表尾。每个文件系统的数据结构表示为:

        struct file_system_type {
          const char *name;
          int fs_flags;
          /* 从底层存储介质读取超级块 */
          int (*get_sb) (struct file_system_type *, int,
                         const char *, void *, struct vfsmount *);
          void (*kill_sb) (struct super_block *);
          struct module *owner;
          struct file_system_type * next;
          /* 对于每个已经装载的文件系统,内存中都创建了一个超级结构,并把
             同类型的所有已装载文件系统放在此链表 */
          struct list_head fs_supers;
        
          struct lock_class_key s_lock_key;
          struct lock_class_key s_umount_key;
        
          struct lock_class_key i_lock_key;
          struct lock_class_key i_mutex_key;
          struct lock_class_key i_mutex_dir_key;
          struct lock_class_key i_alloc_sem_key;
        };
        
      2. 装载和卸载 目录树的装载和卸载比仅仅注册文件系统复杂的多。
        • vfsmount结构 unix采用了一种单一的文件系统层次结构,新的文件系统可以集成到其中:

          2020-11-19_18-35-31_screenshot.png 图中就有三种文件系统,可以用 mount 命令查询目录树的各种文件系统的 装载情况.

          每个装载的文件系统都有一个本地目录。在将文件系统装载到一个目录时,装载点 的内容被替换为即将装载的文件系统的相对根目录内容。前一个目录数据会被隐藏。 知道新的文件系统卸载才重新出现。每个装载的文件系统都对应于一个vfsmount 结构的实例:

          struct vfsmount {
            struct list_head mnt_hash;
            /* 父文件系统 */
            struct vfsmount *mnt_parent;  /* fs we are mounted on */
            /* 当前文件系统的装载点在其父目录中的dentry结构 */
            struct dentry *mnt_mountpoint;  /* dentry of mountpoint */
            /* 文件系统本身的相对根目录, 与上一行表示同一个目录 */
            struct dentry *mnt_root;  /* root of the mounted tree */
            /* 超级块 */
            struct super_block *mnt_sb; /* pointer to superblock */
            /* 子文件系统的链表表头 */
            struct list_head mnt_mounts;  /* list of children, anchored here */
            /* 子文件系统的链表元素 */
            struct list_head mnt_child; /* and going through their mnt_child */
            int mnt_flags;
            /* 4 bytes hole on 64bits arches */
            char *mnt_devname;    /* Name of device e.g. /dev/dsk/hda1 */
            /* 一个命名空间的所有装载的文件系统都保存在namespace->list链表中,这是该链表的元素 */
            struct list_head mnt_list;
            /* 连接装载过期的元素 */
            struct list_head mnt_expire;  /* link in fs-specific expiry list */
            /* 共享装载 */
            struct list_head mnt_share; /* circular list of shared mounts */
            struct list_head mnt_slave_list;/* list of slave mounts */
            struct list_head mnt_slave; /* slave list entry */
            struct vfsmount *mnt_master;  /* slave is on master->mnt_slave_list */
            struct mnt_namespace *mnt_ns; /* containing namespace */
            /*
             * We put mnt_count & mnt_expiry_mark at the end of struct vfsmount
             * to let these frequently modified fields in a separate cache line
             * (so that reads of mnt_flags wont ping-pong on SMP machines)
             */
            atomic_t mnt_count;
            int mnt_expiry_mark;    /* true if marked for expiry */
            int mnt_pinned;
          };
          
        • 超级块的管理 装载操作开始于 超级块的读取

          struct super_block {
            /* 链接 *同一类型* 的所有超级块 */
            struct list_head  s_list;   /* Keep this first */
            /* 文件系统所在块设备的内核内部编号 */
            dev_t     s_dev;    /* search index; _not_ kdev_t */
            /* 块长度,单位字节 */
            unsigned long   s_blocksize;
            /* 前一个成员对2取对数 */
            unsigned char   s_blocksize_bits;
            /* 1表示超级快被改变,需要回写 */
            unsigned char   s_dirt;
            /* 文件系统可以处理的最大文件长度 */
            unsigned long long  s_maxbytes; /* Max file size */
            struct file_system_type *s_type;
            /* 处理超级块相关操作的函数, 实现由底层文件系统代码提供, 详见后面代码 */
            /* 它不会改变inode的内容,但会控制从底层文件系统实现获取和返回inode
               数据的方式 */
            const struct super_operations *s_op;
            struct dquot_operations *dq_op;
            struct quotactl_ops *s_qcop;
            const struct export_operations *s_export_op;
            unsigned long   s_flags;
            unsigned long   s_magic;
            /* 把超级块与全局根目录的dentry项联系起来. 可用于检查文件系统是否已经
               装载, 如果为NULL,则文件系统是一个伪文件系统,只在内核内部可见。
               否则,该文件系统在用户空间中是可见的。*/
            struct dentry   *s_root;
            struct rw_semaphore s_umount;
            struct mutex    s_lock;
            int     s_count;
            int     s_syncing;
            int     s_need_sync_fs;
            atomic_t    s_active;
          #ifdef CONFIG_SECURITY
            void                    *s_security;
          #endif
            /* 处理扩展属性的函数指针 */
            struct xattr_handler  **s_xattr;
          
            struct list_head  s_inodes; /* all inodes */
            /* 脏inode链表 */
            struct list_head  s_dirty;  /* dirty inodes */
            struct list_head  s_io;   /* parked for writeback */
            struct list_head  s_more_io;  /* parked for more writeback */
            struct hlist_head s_anon;   /* anonymous dentries for (nfs) exporting */
            /* 超级块的文件系统上所有打开的文件 */
            struct list_head  s_files;
          
            /* 指向内存中的block_device结构指针 */
            struct block_device *s_bdev;
            struct mtd_info   *s_mtd;
            struct list_head  s_instances;
            struct quota_info s_dquot;  /* Diskquota specific options */
          
            int     s_frozen;
            wait_queue_head_t s_wait_unfrozen;
          
            char s_id[32];        /* Informational name */
            /* 指向文件系统实现的 私有数据 */
            void      *s_fs_info; /* Filesystem private info */
          
            /*
             * The next field is for VFS *only*. No filesystems have any business
             * even looking at it. You had been warned.
             */
            struct mutex s_vfs_rename_mutex;  /* Kludge */
          
            /* Granularity of c/m/atime in ns.
               Cannot be worse than a second */
            /* 文件系统支持的时间的最大可能粒度 */
            u32      s_time_gran;
          
            /*
             * Filesystem subtype.  If non-empty the filesystem type field
             * in /proc/mounts will be "type.subtype"
             */
            char *s_subtype;
          };
          
          
          /*
           * NOTE: write_inode, delete_inode, clear_inode, put_inode can be called
           * without the big kernel lock held in all filesystems.
           */
          struct super_operations {
              struct inode *(*alloc_inode)(struct super_block *sb);
            void (*destroy_inode)(struct inode *);
          
            void (*read_inode) (struct inode *);
          
              void (*dirty_inode) (struct inode *);
            int (*write_inode) (struct inode *, int);
            /* 进程结束数据的使用时,put_inode将inode使用计数器减一 */
            void (*put_inode) (struct inode *);
            void (*drop_inode) (struct inode *);
            /* 将inode从内存和底层存储介质删除 */
            void (*delete_inode) (struct inode *);
            /* 将超级块的私有信息从内存移除,发生在文件系统卸载 */
            void (*put_super) (struct super_block *);
            /* 将超级块写入存储介质 */
            void (*write_super) (struct super_block *);
            /* 将文件系统数据与底层块设备上的数据同步 */
            int (*sync_fs)(struct super_block *sb, int wait);
            /* 将超级块写入存储介质,与write_super区别在于使用内核锁机制的方式 */
            void (*write_super_lockfs) (struct super_block *);
            void (*unlockfs) (struct super_block *);
            /* 文件系统的统计信息,如未使用的数据块的数目,文件名的最大长度 */
            int (*statfs) (struct dentry *, struct kstatfs *);
            /* 从新装载一个已经装载的文件系统 */
            int (*remount_fs) (struct super_block *, int *, char *);
            void (*clear_inode) (struct inode *);
            void (*umount_begin) (struct vfsmount *, int);
          
            /* 用于proc文件系统,显示文件系统装载的选项 */
            int (*show_options)(struct seq_file *, struct vfsmount *);
            /* 提供了文件系统的统计信息,用于proc文件系统 */
            int (*show_stats)(struct seq_file *, struct vfsmount *);
          #ifdef CONFIG_QUOTA
            ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
            ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
          #endif
          };
          
        • mount系统调用

          2020-11-19_19-50-19_screenshot.png

        • umount系统调用

          2020-11-19_19-59-43_screenshot.png

    2. 文件操作
      1. 查找inode

        内核使用pathlookup函数查找路径或文件名,nameidata结构用了向查找函数传递参数并保存结果

        int fastcall path_lookup(const char *name, unsigned int flags,
                                 struct nameidata *nd)
        {
          return do_path_lookup(AT_FDCWD, name, flags, nd);
        }
        
        struct nameidata {
          struct dentry *dentry;
          struct vfsmount *mnt;
          /* 需要查找的名称 */
          struct qstr last;
          unsigned int  flags;
          int   last_type;
          unsigned  depth;
          char *saved_names[MAX_NESTED_LINKS + 1];
        
          /* Intent data */
          union {
            struct open_intent open;
          } intent;
        };
        
        

        查找的核心函数为_linkpathwalk:

        2020-11-19_21-06-11_screenshot.png

        2020-11-19_21-39-06_screenshot.png

      2. 打开文件

        2020-11-19_21-40-24_screenshot.png

        2020-11-19_21-41-40_screenshot.png

      3. 读取和写入

        2020-11-19_21-43-44_screenshot.png

  5. 标准函数

    VFS层为所有文件系统提供了同一的标准函数。

    1. 通用读取例程

      2020-11-19_22-23-27_screenshot.png 其中的aioread通常指向genericfileaioread中

      2020-11-19_22-25-22_screenshot.png

      • 从映射读取

        2020-11-19_22-27-23_screenshot.png 如果预读机制没能将所需的页读入内存,那么由nocachedpage直接进行读取操作

        2020-11-19_22-30-58_screenshot.png

    2. 失效机制 TODO
    3. 权限检查 TODO

3.9.8 TODO Ext文件系统族

3.9.9 无持久存储的文件系统

  1. proc文件系统

    使用proc文件系统,可以获得有关内核各子系统的信息(如内存利用率,附接的外设等),也可以在 不重复编译内核的情况下修改内核的行为。与该文件系统密切相关的是系统控制机制(sysctl)。

    1. /proc的内容

      /proc的内容可以分为以下几大类:

      • 内存管理
      • 系统进程的特征数据
      • 文件系统
      • 设备驱动程序
      • 系统总线
      • 电源管理
      • 终端
      • 系统控制参数
      • 特定于进程的数据 每个系统进程,无论当前状态如何,都有一个对应的子目录(与PID同名),包含了该进程 的有关信息。
      • 一般性系统信息 /proc本身也包含了一些信息,与指定的内核子系统无关(或几个子系统共享)的一般性信息, 一般存放在/proc下的文件中
      • 网络信息 /proc/net子目录提供了内核的各种网络选项的有关数据。其中保存了各种协议和设备数据,如:
        • udp和tcp提供了IPv4的UDP和TCP套接字的统计数据
        • 用于反向地址解析的ARP表,可以在arp文件中查看
        • dev保存了通过系统的网络接口传输的数据量的统计数据。该信息可用于检查网络的传输质量
      • 系统控制参数 用于动态地检查和修改内核行为的系统控制参数,在proc文件系统的数据项中,属于最多的一部分。 也可以通过sysctl系统调用来修改。sysctl参数由一个独立的子目录/proc/sys管理,它进一步 划分为各种子目录,对应于内核的各个子系统。
    2. 数据结构
      1. proc数据项 proc文件系统中的每个数据项都由procdirentry的一个实例描述

        struct proc_dir_entry {
          /* inode编号*/
          unsigned int low_ino;
          unsigned short namelen;
          const char *name;
          /* 数据项的类型 */
          mode_t mode;
          nlink_t nlink;
          uid_t uid;
          gid_t gid;
          /* 文件长度 */
          loff_t size;
          /* 下面两个操作是对inode和文件的操作,充当与虚拟文件系统之间的接口,所
             使用的操作依赖具体的文件类型 */
          const struct inode_operations *proc_iops;
          /*
           * NULL ->proc_fops means "PDE is going away RSN" or
           * "PDE is just created". In either case, e.g. ->read_proc won't be
           * called because it's too late or too early, respectively.
           *
           * If you're allocating ->proc_fops dynamically, save a pointer
           * somewhere.
           */
          const struct file_operations *proc_fops;
        
        
          /* 相关子系统中返回所需数据的例程 */
          get_info_t *get_info;
          struct module *owner;
          struct proc_dir_entry *next, *parent, *subdir;
          void *data;
          read_proc_t *read_proc;
          write_proc_t *write_proc;
          atomic_t count;   /* use count */
          int pde_users;  /* number of callers into module in progress */
          spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
          struct completion *pde_unload_completion;
          shadow_proc_t *shadow_proc;
        };
        
      2. proc inode 内核提供了一个procinode数据结构,支持以面向inode的方式来查看proc文件系统的数据项:

        struct proc_inode {
          /* 指向进程的pid实例,用于访问大量特定进程的信息 */
          struct pid *pid;
          /* 对应于/proc/<pid>/fd/中的某个文件 */
          int fd;
          union proc_op op;
          /* 关联到proc数据项的proc_dir_entry实例 */
          struct proc_dir_entry *pde;
          /* VFS inode实例,注意不是指针 */
          struct inode vfs_inode;
        };
        
        union proc_op {
          /* 用于获得特定于进程的信息 */
          int (*proc_get_link)(struct inode *, struct dentry **, struct vfsmount **);
          /* 用于在虚拟文件系统中建立链接,指向特定进程的数据 */
          int (*proc_read)(struct task_struct *task, char *page);
        };
        

        可以通过辅助函数PROCI来根据vfs inode获取proc inode:

        static inline struct proc_inode *PROC_I(const struct inode *inode)
        {
          return container_of(inode, struct proc_inode, vfs_inode);
        }
        

        2020-11-20_10-39-52_screenshot.png

      3. 初始化

        2020-11-20_11-08-05_screenshot.png

      4. TODO 装载
      5. 管理/proc数据项
        • 创建和注册 TODO
        • 查找proc数据项

      6. 读取和写入信息
        • procfileread

          2020-11-20_11-53-42_screenshot.png

        • procfilewrite

          static ssize_t
          proc_file_write(struct file *file, const char __user *buffer,
                          size_t count, loff_t *ppos)
          {
            struct inode *inode = file->f_path.dentry->d_inode;
            struct proc_dir_entry * dp;
          
            dp = PDE(inode);
          
            if (!dp->write_proc)
              return -EIO;
          
            /* FIXME: does this routine need ppos?  probably... */
            return dp->write_proc(file, buffer, count, dp->data);
          }
          

          2020-11-20_11-55-50_screenshot.png

      7. 进程相关信息 TODO

3.9.10 内核活动

  1. 等待队列
    struct __wait_queue_head {
      spinlock_t lock;
      struct list_head task_list;
    };
    typedef struct __wait_queue_head wait_queue_head_t;
    
  2. 互斥量和条件变量等的实现机制

    用到了等待队列

    /*
     * We use this hashed waitqueue instead of a normal wait_queue_t, so
     * we can wake only the relevant ones (hashed queues may be shared).
     *
     * A futex_q has a woken state, just like tasks have TASK_RUNNING.
     * It is considered woken when plist_node_empty(&q->list) || q->lock_ptr == 0.
     * The order of wakup is always to make the first condition true, then
     * wake up q->waiters, then make the second condition true.
     */
    struct futex_q {
      struct plist_node list;
      wait_queue_head_t waiters;
    
      /* Which hash list lock to use: */
      spinlock_t *lock_ptr;
    
      /* Key which the futex is hashed on: */
      union futex_key key;
    
      /* For fd, sigio sent using these: */
      int fd;
      struct file *filp;
    
      /* Optional priority inheritance state: */
      struct futex_pi_state *pi_state;
      struct task_struct *task;
    };
    

3.10 unix&Linux大学教程

3.10.1 reference

Go To Statement Considered Harmful - Edsger Dijkstra

  • 在Internet上,有两种非常常见的主机类型,不需要终端:
    1. Web服务器
    2. 路由器
  • pwd print working directory
  • 封闭eof信号 shell默认会在按下D时退出,可以通过给IGNOREEOF设置一个值来表明必须按这么多次D才能 退出shell
  • 修改键绑定 stty 信号名称 新的键
  • 系统中查找程序 which, type, whence
  • 显示时间和日历 时间:

    date
    date -u 格林威治
    

    日历:

    cal
    cal -j (一年中的第几天)
    
    ~/org ❯❯❯ cal -j                                                        20:55:08
    十一月 2020
    日  一  二  三  四  五  六
    306 307 308 309 310 311 312
    313 314 315 316 317 318 319
    320 321 322 323 324 325 326
    327 328 329 330 331 332 333
    334 335
    
  • 日程提醒 当前目录创建一个calendar文件,calendar命令会在当前文件夹查找calendar文件,并显示所有 拥有今天或明天日期的文本行

    ~/org ❯❯❯ cat calendar                                                  21:19:25
    Nov 21  snigefjdsinageadsghef
    ~/org ❯❯❯ calendar                                                      21:19:28
    11 21*  snigefjdsinageadsghef
    
  • 查看系统信息
    1. uptime(运行时间)

      ~/org ❯❯❯ uptime                                                        21:19:37
      21:20  up 11 days, 23:31, 3 users, load averages: 1.57 1.79 1.91
      
      当前时间    运行了11天23时31分, 3个用户登陆   1,5,15分钟的平均负载
      
    2. 查看机器名称 hostname
    3. 查看操作系统名称 uname <-a(ll)>
    4. 查看用户名 whoami who am i
  • 获取其他用户信息users,who,w(ho is doing what)

    ~/org ❯❯❯ users                                                         21:36:13
    binbinyang
    ~/org ❯❯❯ who                                                           21:37:39
    binbinyang console  Nov  9 21:48
    binbinyang ttys000  Nov 21 21:36
    binbinyang ttys004  Nov  9 21:48
    ~/org ❯❯❯ w                                                             21:37:45
    21:37  up 11 days, 23:49, 3 users, load averages: 2.40 2.18 2.04
    USER     TTY      FROM              LOGIN@  IDLE WHAT
    binbinyang console  -                091120  11days -
    binbinyang s004     -                091120      - w
    binbinyang s000     -                21:36       1 -fish
    
  • 设置临时提醒 leave
  • 计数器bc sqrt() s() sin c() cos a() arctan ln()

    scale=3(设置小数点为3位,默认0位) ibase = 8 以基8输入数字 obase = 16 以基16显示答案

  • man手册
    1. 命令
    2. 系统调用
    3. 库函数
    4. 特殊文件
    5. 文件格式
    6. 游戏
    7. 杂项信息
    8. 系统管理
  • 搜索命令 apropos 当我们知道想做什么,但是想不起来用那条命令时,可以用

    man -k(eyword)
    apropos
    

    这两个命令

  • info 获取GNU项目的信息

3.10.2 命令语法 命令名称 选项 参数 ** 选项 也称开关(switches)或标志(flags。 通常由一个连字符后跟一个字母或者两个连字符跟一个单词组成。 也可以把多个单字符组合在一起。 ** 规则 1. 方括号中的项是可选的 2. 不在方括号中的项是必选项,必须作为命令的一部分 3. 黑体字必须原样输入 4. 非黑体字必须用适当的值代替 5. 后面接省略号的参数可以重复任意多次(包括0) 6. 如果一个单独选项和一个参数结合在一起,那么该选项和参数必须同时使用 #+beginsrc shell man [-acdfFhkKtwW] [–path] [-m system] [-p string] [-C configfile] [-M pathlist] [-P pager] [-B browser] [-H htmlpager] [-S sectionlist] [section] name … #+endsrc 7. 由|字符分开的两个或多个选项,表示可以从这个列表中选择一个项 who [-abdHilmpqrstTu ] [file | arg1 arg2] * 环境变量与shell变量(bash) 在日常的交互式工作中,重要的是环境变量,而不是shell变量 - 显示默认环境变量: env/printenv - 显示shell变量 set - 创建变量 1. shell变量 创建shell变量语法为: NAME=value 如果希望使用一个包含空白符的值,则需要将值放在双引号中: #+beginsrc shell WEEDLY="a cool cat" #+endsrc 2. 环境变量 可以使用export命令将变量导出到环境中: #+beginsrc shell export NAME[=value]… # HARLEY WEEDLY同时由shell变量变为"shell+环境"变量 export HARLEY WEEDLY # 同时设置了PAGER变量并导出到环境中 export PAGER=less #+endsrc 可以用unset(复位)删除变量: #+beginsrc shell unset NAME… #+endsrc 可以使用下面命令修改命令行提示符: #+beginsrc shell export PS1="我的提示符" #+endsrc

3.10.3 第13章 使用shell:命令和定制

  1. 元字符

    使用时除了通常意义的 字母数字字符, 还有一些表示特殊含义的字符,我们称 这样的字符为 元字符 。shell中使用的元字符有:

    2020-11-22_14-13-10_screenshot.png

  2. 引用和转义

    shell中有三种方法可以对元字符进行转义:

    # 1 使用反斜杆引用单个字符
    echo It is warm and sunny\; come over and visit
    # 2 使用单引号引用一串字符
    echo 'It is warm (and sunny); come over & visit'
    # 3 使用双引号引用一串字符(保留$, `(用于命令嵌套) 和 \ 的特殊含义
    echo "My userid is <$USER>; my terminal is <$Term>"
    
    
  3. 命令类型(内部和外部)
    ~/org ❯❯❯ type date time set                                            13:49:43
    date is /bin/date
    time is a builtin
    set is a builtin
    

    内部命令可以使用help命令来获取帮助,shell会根据PATH变量查找外部命令,向PATH加路径的命令为:

    export PATH="$PATH:$HOME/bin"
    

3.10.4 第14章 初始化文件

环境文件(初始化文件)的名称都以rc结尾,如.bashrc,.mailrc. rc表示 run commands ,运行命令,也就是 特定程序每次启动时自动运行的命令。

3.10.5 15章

tee - T型链接器:

cat a b c | tee d | greap e

3.10.6 16章 过滤器

过滤器就是任何能够从标准输入读取文本数据并向标准输出写入文本数据(每次一行)的程序。 管道中的第一个和最后一个程序不必是过滤器。 最有用的过滤器列表:

过滤器 章号 参阅 作用
awk - perl 编程语言:操作文本
cat 16 split,tac,rev 组合文件:复制标准输入到标准输出
colrm 16 cut,join,paste 删除指定的数据列
comm 17 cmp,diff,sdiff 比较两个有序文件,显示区别
cmp 17 cmmm,diff,sdiff 比较两个有序文,
cut 17 colrm,join,paste 从数据中抽取指定的列(字段)
diff 17 cmp,comm,sdiff 比较两个有序文件,显示区别
expand 18 unexpand 将制表符转换为空格
fold 18 fmt,pr 将长行格式化为较短的行
fmt 18 fold,pr 格式化段落,从而使它们看上去更漂亮
grep 19 look,strings 选择包含指定模式的行
head 16 tail 从数据的开头选择行
join 19 colrm,cut,paste 基于公用字段,组合数据列
look 19 greap 选择以指定模式开头的行
nl 18 wc 创建行号
paste 17 colrm,cut,join 组合数据列
perl - awk 编程语言:操作文本,文件,进程
pr 18 fold,fmt 将文本格式化为页或者列
rev 16 cat,tac 每行数据中的字符反序排练
sdiff 17 cmp,comm,diff 比较两个文件,显示区别
sed 19 tr 非交互式文本编辑
sort 19 tsort,uniq 排序数据,检查数据是否有序
split 16 cat 将大文件分隔成较小的文件
strings 19 grep 在二进制文件中搜索字符串
tac 16 cat,rev 组合文件,同时将文本行的顺序反转
tail 16 head 从数据的末尾选择行
tr 19 sed 改变或者删除选定的字符
tsort 19 sort 根据偏序创建全序
unexpand 18 expand 将空格转变成制表符
uniq 19 sort 选择重复/唯一行
wc 18 nl 统计行数,单词数和字符数
  1. cat(catenate,chain的拉丁单词)

    -n(unmber) , 在每行前面加一个行号 -b(lank), 和-n一起用,不要对空白行算编号 -s(queeze), 将多个连续空白行替换为一个空白行

  2. tac
    设log文件为:
    01
    02
    03
    04
    那么tac log为
    04
    03
    02
    01
    

    与less配合很好用

  3. rev
    ~ ❯❯❯ cat log; rev log                                                  19:56:27
    01
    02
    03
    04
    
    10
    20
    30
    40
    
  4. colrm

    2020-11-22_20-08-46_screenshot.png

3.10.7 17章

  1. cut
    cut -c list [file...]  以字符分隔
    cut -f list [-d deliniter] [-s] [file...] 以字段分隔
    list:要抽取的列的列表
    deliniter:分隔符
    1,8,10表示取第1,8,10列字符
    10-15表示抽取第10列到第15列
    也可以联合起来使用,如1,8,10-15
    
  2. paste
    paste [-d char...] [file...]
    组合分离的文件
    char:分隔符
    file:输入文件的名称
    

    2020-11-22_23-12-40_screenshot.png

3.10.8 18章 统计和格式化

  1. nl 创建行号
    nl [-v start] [-i increment] [-b a] [-n ln|rn|rz] [file...]
    其中start是行号,increment是增量,file是文件的名称
    
  2. wc

    wc [-clLw] [file…] -c(har) -l(ine) -w(rods) -L(ongest)

  3. fold 格式化行

    fold [-s] [-w width] [file…] width 新行的宽度 -s 不分割单词

    2020-11-23_10-20-43_screenshot.png

  4. fmt 格式化段落

    fmt [-su] [-w width] [file…] -u(niform spacing) 合并多个空格 -s(plit only) 只拆分长行,不连接短行

  5. pr 按页格式化文本

    目的是将文本格式化为适合打印的页

3.10.9 19章 选取,排序,组合及变换

  1. grep grep中的re表示regular expression, g代表global,p代表print

    grep [-cilLnrsvwx] pattern [file…] -c(ount) -i(gnore) 忽略大小写 -n(umber) 行号 -l(ist filename) 将包含模式的文件名输出 -L 将不包含模式的文件名输出 -v(reverse) 选取不包含指定模式的所有行 -x 完全与搜索模式匹配的行 -w 准确匹配单词(如 grep -w now file,那么know就是不匹配) -r(ecursive) 搜索整个目录树 -s(uppress,抑制) 不输出错误信息

  2. look

    搜索以 字母顺序排列(用的二分查找) 的数据,并查找所有以特定模式开头的行 look 不是一个过滤器,所以不能在管道中间 look与grep的区别:look要求排序,所以速度快(因为现在cpu很快,所以一般用grep) look [-df] pattern file… -d(ictionary) 只考虑字母和数字的排序 -f(old,同等) 忽略大小写的区别

  3. sort

    排序数据以及查看数据是否已经有序 sort非常灵活,既可以比较整行,也可以从每行中选取部分进行比较 sort [-dfnru] [-o outfile] [infile…]

    2020-11-23_11-25-00_screenshot.png

    -d(ictionary) 同look -f(old) 同look -n(umeric) 识别行开头或字段开头的数字,按这些数字排序 -r(everse) 反向排序 -u(nique) 去重 -c(heck) 只查看是否有序(输出为从第几行开始无序)

  4. uniq

    uniq要求输入有序 uniq可以完成四个任务:

    1. 消除重复行
    2. 选取重复行(-d)
    3. 选取唯一行(-u)
    4. 统计重复行的数量(-c)

    uniq [-cdu] [infile [outfile]]

  5. join

    和数据库sql的join有点类似 join [-i] [-a1|v1] [-a2|v2] [-1 field1] [-2 field2] file1 file2 -i(gnore) -a(ll) -v(reverse) -1/2 指定file1/2中的字段(field1/2)连接

  6. sed(stream editor) 非交互式文本编辑

    sed [-i] command | -e command… [file…] -i(n-place) 原地修改文件

    在允许搜索和替换操作的程序中,一般都使用替换为空来达到删除效果

    2020-11-23_13-24-26_screenshot.png

3.10.10 20章 正则表达式

  1. 普通字符

    2020-11-23_14-08-25_screenshot.png

    2020-11-23_14-09-00_screenshot.png

3.10.11 23章 文件系统(569页)

  1. 文件类型
    1. 普通文件
    2. 目录
    3. 伪文件 其中最重要的是特殊文件,也称设备文件, 其他的还有命名管道,proc文件 设备文件:

      2020-11-23_14-38-20_screenshot.png

      proc文件:

      2020-11-23_14-39-36_screenshot.png

  2. 根目录内容

    2020-11-23_15-09-07_screenshot.png

    目录 内容
    /dev 所有的特殊文件,还包含一个makedev程序来创建新的特殊文件
    /etc 配置文件。配置文件是某程序启动时处理的文本文件,其中包含影响程序操作的命令或信息,与rc文件类似
    /home  
    /lib 包含/bin和/sbin目录中的程序所需的基本库和内核模块
    /media  
    /mnt 不会在其他位置挂载的固定介质的挂载点
    /opt optional software 安装第三方应用程序的位置。每个程序都根据自己的需要拥有自己的子目录。
    /sbin system binaries,系统二进制文件。存放用于系统管理的程序,通常这些程序必须由超级用户运行
    /srv 为本地服务保留的(cgi,web,ftp,cvs,rsync)
    /tmp 临时文件,内容将自动移除
    /usr 辅助文件系统,用来放静态数据(即没有管理员干涉不会改变的数据)。
    /var variable,用来存放可变数据,如日志文件,打印文件,电子邮件等
  3. /usr 目录

    2020-11-23_15-32-03_screenshot.png

    • /usr/bin : 系统中大多数程序的存放位置,比/bin多很多。
    • /usr/include: 头文件存储区。
    • /usr/lib:和/lib功能一样
    • /usr/local:为系统管理员准备,系统管理员使用它来支持本地用户。存放本地程序和文档资料。 将软件存放在这里可以确保在系统升级时,不会覆盖软件。
    • /usr/sbin: 同/sbin
    • /usr/share:存放需要在用户和程序之间共享的文件
    • /usr/src: 这个目录可以发现一些包含系统源代码的子目录,如linux源代码位于/usr/src/linux中
  4. 使用多个目录存放程序的原因

    可以看到,文件系统存在很多存放同类内容的目录,如/bin和/usr/bin等。 这主要是由于历史原因造成的。以前内存很小,所以把最重要的程序放在根目录下, 等启动引导完成后,再把/usr挂载进来.

    2020-11-24_11-44-44_screenshot.png

3.10.12 24章 目录操作

  1. 检查文件类型
    1. ls -F(lag)

      2020-11-24_13-06-52_screenshot.png

    2. file

      file [name…]

  2. 掌握磁盘空间的情况
    1. ls -s(ize)[h(uman-readable)] 单位KB

      查看文件使用磁盘空间的情况

      ~/org ❯❯❯ ls -s                                                         13:16:07
      total 640
      8 calendar       8 macinit.org    0 reading/
      8 leetcode.c   616 notes.org
      
    2. du(disk usage)

      du [-achs] [name…] -h(uman-readable) -s(um) -c(ount) -a(ll)

      ~/org ❯❯❯ du -s                                                         13:27:57
      133600  .
      ~/org ❯❯❯ du -sh                                                        13:28:09
      65M .
      ~/org ❯❯❯ du -shc                                                       13:28:19
      65M .
      65M total
      
    3. df(disk free-space)

      -h(uman-readable)

  3. 通配符(p.631)

    通配符只用一种用途:在键入一条命令时匹配一组文件名

    2020-11-24_13-45-25_screenshot.png 花括号扩展: {,}

3.10.13 25章 文件操作

  1. find
    1. 语法:

      find path… test… action… 一旦输入该命令,find就会遵循一个3个步骤的处理过程:

      2020-11-24_14-16-46_screenshot.png

    2. test:

      2020-11-24_14-18-28_screenshot.png

      最重要的两个test:

      1. -type 控制查找的文件类型,后面跟一个单字母的标识(也可以多个标识组合)

        字母标识 代表的文件类型
        f 普通文件
        d 目录
        b 块设备
        c 字符设备
        p 命名管道
        l 符号连接
      2. -name 查找文件名匹配指定模式的文件 注意事项:不能忘记引用通配符

        find . -type f -name '*.c' -print
        find . -type f -name *.c -print
        

        第一条命令工作正常,第二条会产生语法错误,因为不引用的通配符会直接被 shell解释掉,扩展成一组实际的文件名,而实际这个通配符是给-name测试 用的。

      还可以用 ! 对测试取反:

      find ~ \! -type f \! -type d -print
      
    3. action (默认为-print)

      2020-11-24_14-37-40_screenshot.png

  2. xargs

    我们可以把find的输出管道传送给xargs,它可以运行任何使用参数指定的命令 xargs [-prt] [-istirng] [command [argument…]] -i(nsert) string为占位符:

    find . -type f | xargs -ifk mv fk ~/backups/fk.old
    

    上面的例子,fk就是占位符,xargs会用管道传过来的结果代替占位符,默认占位符是{}

    -p(rompt,提示) 就是在每个命令之前询问一下是否确定执行,是的话输入'y',输入其他忽略该命令

    -t(ell me what you are doing) 显示每一条命令

3.11 Effective C++

3.11.1 让自己习惯c++

  1. 01 视c++为一个语言联邦

    c++是一个多重范式的编程语言:过程编程,面向对象编程,函数式,泛型编程,源编程。 c++可以分为4个次语言:

    1. C
    2. C with classes
    3. Template C++
    4. STL
  2. 02 尽量以const,enum,inline替换#define
    • 对于单纯常量,最好以const对象或enums替换#define #define定义的符号不再符号表中,浪费调试时间。
    • 对于形似函数的宏,最好改用inline函数替换#define
  3. 03 尽可能使用const
    • const允许你指定一个语义约束,编译器会帮助检查这个约束不被违反.const可以被施加于任何作用域 的对象,函数参数,函数返回类型,成员函数本体。const甚至能检查一些打字错误:

      class Rational {};
      const Rational operator * (const Rational &lhs, const Rational &rhs);
      Rational a, b,c;
      if (a*b = c) {
        ...
       }
      

      本来想键入“==”却打成“=”, 但是const可以预防这样的错误

    • 编译器强制实施bitwise constness(不改变对象内任何一个bit),但编写程序时应该使用 “概念上的常量性(可以修改对象的某些bits,但必须符合逻辑(logical constness))”:

      class CTextBlock {
      public:
        ...
      char &
      operator[](std::size_t position) const //bitwise声明,但其实不合适
        {return pText[position];}
      private:
        char *pText;
      };
      
    • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码 重复:

      class TextBlock {
      public:
        ...
        const char& operator[](std::size_t position) const
        {
        ...
          return text[position];
        }
      
        char& operator[](std::size_t position)
        {
          return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
        }
      };
      
  4. 04 确定对象被使用前已先被初始化

    通常使用Cpart of C++而且初始化可能招致运行期成本时,那么就不保证发生初始化。而non-C parts of C++,一般就会初始化。前者如array, 后者如vector的对比。

    所以最好是永远使用对象之前将它初始化。具体来说:

    1. 对于无任何成员的内置类型,必须手工初始化
    2. 对于内置类型以外的任何东西,确保每个构造函数都将对象的每个成员初始化(别混淆复制和初始化, 也就是说成员初始化列表才是初始化,构造函数体中的是赋值操作)。 也就是说 总是在初始列中列出所有成员变量以免还得记住哪些成员变量可以没有初始值
    3. 对于不同编译单元定义的non-local static对象,他们的初始化次序编译器是无法决定的, 所以为了消除这个问题,我们需要:将每个non-local static对象搬到自己的专属函数内 (该对象在此函数内被声明static)。这些函数返回一个reference指向它所含的对象。 也即是non-local static对象被local static对象替换了。这也是单例模式的常见实现 手法。

3.11.2 构造/析构/赋值运算

  1. 05 了解c++默默编写并调用了哪些函数

    编译器可以暗自为class超级default构造函数,copy构造函数,copy assignment操作符,以及析构函数。 但是编译器会拒绝自动生成如下赋值操作, 必须自己定义copy assignment:

    1. 内含reference成员
    2. 内含const成员
    3. 某个base class将copy assignment操作符声明为private
  2. 06 若不想使用编译器自动生成的函数,就该明确拒绝(private 或 delete)
  3. 07 为多态基类声明virtual析构函数
    1. polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数, 因为这种基类的设计 目的是为了用了“通过base class接口处理derived class对象; 如果class带有任何virtual函数,它就应该拥有一个virtual析构函数.
    2. classes的设计目的如果不是作为base classes使用,或者不是为了具备多态性,就不 该声明virtual析构函数
  4. 08 别让异常逃离析构函数

    TODO : 汇编层面看异常的实现

    1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常, 然后吞下它们(不传播)或结束程序
    2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而 不是在析构函数中)执行该函数
  5. 09 绝不在构造和析构过程中调用virtual函数

    在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class (比起当前执行构造函数和析构函数那层)。

    解决这个问题的一个方法是把原本要调用的虚函数变为非虚函数,通过继承类的构造函数传递 必要信息给基类构造函数:

    class Transaction {
    public:
      explicit Transaction(const std::string &logInfo);
      void logTransaction(const std::string &logInfo) const; //改为非虚函数
      ...
    };
    
    Transaction::Transaction(const std::string &logInfo)
    {
      ...
        logTransaction(logInfo);  //现在是非虚函数调用
    }
    
    class BuyTransaction: public Transaction {
    public:
      // 将log信息传递给base class 构造函数
      BuyTransaction(para) : Transaction(createLogString(para)) {}
    private:
      static std::string createLogString(para);
    };
    
  6. 10 令operator=返回一个reference to *this
  7. 11 在operator= 中处理“自我赋值”
    1. 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象” 的地址、精心周到的语句顺序、以及copy-and-swap:

      Widget& Widget::operator=(const Widget &rhs)
      {
        Widget temp(rhs);
        swap(temp);
        return *this;
      }
      
    2. 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然是正确的。
  8. 12 复制对象时不要忘记任何一个成分
    1. copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
    2. 不要以某个copying函数实现另一个copying函数。而是将它们共同机制放在第三个函数中,并由两个 copying函数共同调用。因为:
      • 拷贝复制运算符不能调用拷贝构造函数,因为不能构造一个已经存在的对象
      • 拷贝构造函数也不能调用拷贝复制运算符,因为对象还没构造完

3.11.3 资源管理

所谓资源,就是一旦使用了它,将来必须还给系统。c++程序中常见的资源包括: 动态内存,文件描述符,互斥锁,数据库连接,网络sockets。

  1. 13 以对象管理资源

    RAII(resource acquisition is initialization):资源获取时机便是初始化时机

    1. 为防止资源泄露,使用RAII()对象,他们在构造函数中获得资源并在析构函数中释放资源。
    2. sharedptr和autoptr很好用
  2. 14 在资源管理类中小心copying行为
    1. 复制RAII对象必须一并复制它所管理的资源,所以资源copying行为决定RAII对象的copying行为。
    2. 常见的RAII class copying行为是:抑制copying,施行引用计数法,不过也可以实现成其他行为.
  3. 15 在资源管理类中提供对原始资源的访问
    1. APIs往往要求访问原始资源,所以每个RAII class应该提供一个“取得其所管理的资源”的办法。
    2. 对原始资源的访问可能由显示转换或隐式转换。一般来说显示转换比较安全,但隐式转换对比较 方便(不用到处调用get()方法)
  4. 16 成对使用new和delete时要采取相同形式

    new [] -> delete [] new -> delete

    std::string *stringArray = new std::string[100];
    ...
    delete stringArray;
    

    上面的例子中,100个string对象中的99个的析构函数很可能没被调用.

  5. 17 以独立语句将newed对象置入智能指针

    如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露:

    processWidget(std::shared_ptr<Widget>(new Widget), priority());
    

    上面的代码可能会产生资源泄露,因为priority() 和sharedptr(new Widget)的执行 顺序是不确定的(为了效率,c++编译器可能产生不同顺序(不像java顺序是固定的)),所以 编译器产生的代码可能如下:

    1. 执行 new Widget
    2. 调用priority
    3. 调用sharedptr构造函数

    如果priority()执行时发生了异常,那么就会发生资源泄露

    正确的做法是:

    std::shared_ptr<Widget> pw(new Widget);
    
    processWidget(pw, priority());
    

3.11.4 设计与声明

  1. 18 让接口容易被正确使用,不易被误用
    1. 好的接口不容易被误用
    2. 接口一致性和与内置类型行为兼容能“促进正确使用”
    3. “阻止误用“的方法包括建立新类型,限制类型上的操作,束缚对象值,消除客户 的资源管理责任
    4. sharedptr支持定制删除器。这可以防止DLL问题,还可以用来自动解除互斥锁等
  2. 19 设计class犹如设计types

    Class的设计就是type的设计。在定义一个新type之前,请确定已经考虑过以下主题: TODO

  3. 20 宁以pass-by-reference-to-const替换pass-by-value
    1. 因为前者比较高校,并且可以避免切割问题
    2. 以上规则不适用于内置类型和STL的迭代器(值传递更符合语义)及函数对象。对它们而言, pass-by-value往往比较适当
  4. 21 必须返回对象时,不要返回reference

    绝对不要返回pointer或reference指向一个local stack对象,或返回reference指向一个 heap-allocated对象 ,或返回pointer/referece指向一个local static对象而有可能 同时需要多个这样的对象。

  5. 22 将成员变量声明为private
    1. 把成员声明为private可赋予客服访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证,并 提供class作者以充分的实现弹性
    2. protected并不比public更具封装性
  6. 23 宁以non-member、non-friend替换member函数

    这样做可以,减少能够访问class内的private成分的函数数量,增加封装性,弹性和扩展性

  7. 24 把 所有参数都需要类型转换的函数 设计为非成员函数

    因为编译器无法为成员函数的this成员类型转换

  8. TODO 25 把swap函数写成不抛异常的函数

3.11.5 实现

  1. 26 尽可能延后变量定义式的出现时间
  2. 27 尽量少做转型动作
    1. 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamiccasts. 如果有个设计需要转型动作,试着发展无需转型的替代设计
    2. 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数, 而不是将转型放进他们自己的代码中
    3. 使用c++ style转型而不是旧式转型。因为前者很容易识别出来而且也职责分类也比较明确

    看下面的例子:

    #include <iostream>
    using namespace std;
    class B {
    public:
      virtual void fk() {
        i = 10;
      }
      int i = 1;
    };
    
    class A : public B {
    public:
      virtual void fk() {
        static_cast<B>(*this).fk();
      }
    };
    
    int main()
    {
      A a;
      a.fk();
      cout << a.i << endl;
      return 0;
    }
    

    其汇编如下:

    B::fk():
            pushq   %rbp
            movq    %rsp, %rbp
            movq    %rdi, -8(%rbp)
            movq    -8(%rbp), %rax
            movl    $10, 8(%rax)
            nop
            popq    %rbp
            ret
    B::B(B const&):
            pushq   %rbp
            movq    %rsp, %rbp
            movq    %rdi, -8(%rbp)
            movq    %rsi, -16(%rbp)
            movl    $vtable for B+16, %edx
            movq    -8(%rbp), %rax
            movq    %rdx, (%rax)
            movq    -16(%rbp), %rax
            movl    8(%rax), %edx
            movq    -8(%rbp), %rax
            movl    %edx, 8(%rax)
            nop
            popq    %rbp
            ret
    A::fk():
            pushq   %rbp
            movq    %rsp, %rbp
            subq    $32, %rsp
            movq    %rdi, -24(%rbp)
            movq    -24(%rbp), %rdx
            leaq    -16(%rbp), %rax
            movq    %rdx, %rsi
            movq    %rax, %rdi
            call    B::B(B const&)
            leaq    -16(%rbp), %rax
            movq    %rax, %rdi
            call    B::fk()
            nop
            leave
            ret
    main:
            pushq   %rbp
            movq    %rsp, %rbp
            subq    $16, %rsp
            movl    $vtable for A+16, %eax
            movq    %rax, -16(%rbp)
            movl    $1, -8(%rbp)
            leaq    -16(%rbp), %rax
            movq    %rax, %rdi
            call    A::fk()
            movl    -8(%rbp), %eax
            leave
            ret
    vtable for A:
            .quad   0
            .quad   typeinfo for A
            .quad   A::fk()
    vtable for B:
            .quad   0
            .quad   typeinfo for B
            .quad   B::fk()
    typeinfo for A:
            .quad   vtable for __cxxabiv1::__si_class_type_info+16
            .quad   typeinfo name for A
            .quad   typeinfo for B
    typeinfo name for A:
            .string "1A"
    typeinfo for B:
            .quad   vtable for __cxxabiv1::__class_type_info+16
            .quad   typeinfo name for B
    typeinfo name for B:
            .string "1B"
    

    可以看到,A的fk函数中,staticcast转型会产生一个a的B类型部分的副本,而 没有直接改a所在内存.

    所以类型转换要非常小心,尽量不要用

  3. 28 避免返回handles指向对象内部成分

    这可以增加封装性,帮助const成员函数的行为像个const,并将发生空悬的可能性降到最低

  4. 29 为“异常安全”而努力是值得的
    1. 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构破坏。这样 的函数区分为三种可能的保证:
      • 基本保证 如果异常被抛出,程序内的任何事物都仍然保持在有效状态下,没有任何 对象或数据结构出现预料范围以外的值
      • 强烈保证 函数的操作是原子的,要不就完全成功,要不就失败回到调用之前的状态
      • 不抛异常(nothrow) 不抛出异常,作用于内置类型上的所有操作都提供nothrow保证
    2. 强烈保证 往往能够以 copy-and-swap实现出来,但“强烈保证”并非对所有 函数都可以实现或具有意义
    3. 函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全保证” 中的最弱者
  5. 30 透彻了解inlineing

    inline的成本:

    • 代码膨胀,catch命中率降低
    • 调试困难
    • 一点改动就得重新编译所有引用它的模块
    • 将inline限制在小型(太大的化代码膨胀问题),被频繁调用(这样inline消除的函数调用过程开销越多,收益越大, 而且因为函数是小型的,也不太会有代码膨胀问题)的函数上,这可使日后的调试过程和二进制升级更容易 ,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
    • 不要只因为function templates出现在头文件就把他们声明为inline. inline与 function templates没有必然联系。
  6. 31 将文件间的编译依存关系降到最低
    1. 这种原则的一般思想是:相依于声明式而不是定义式,而实现这种思想的手段一般是 handle classes和interface classes配合使用
    2. 程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及templates都适用

3.11.6 继承与面向对象设计

  1. 32 确定你的public继承塑膜出is-a关系

    public继承意味着is-a,适用于base class身上的每一件事 也一定适用于继承类身上的情况。

  2. 33 避免遮掩继承而来的名称
    1. 继承类中的名称会遮掩基类中的名称。
    2. 为了防止名称被遮掩,可以使用using声明或转交函数
  3. 34 区分接口继承和实现继承
    1. 纯虚函数只指定接口的继承, 要注意的是纯虚函数可以被定义,然后被继承类调用
    2. 非纯虚函数会具体指定接口继承以及缺省的实现继承
    3. 非虚函数则会具体指定接口继承以及强制性实现继承
  4. TODO 35 考虑virtual函数以外的其他选择
  5. 36 绝不重新定义继承来的非虚函数
  6. 37 绝不重新定义继承来的缺省参数值

    因为缺省的参数值是静态绑定的,而virtual函数却是动态绑定的; 可以用35中的NVI来代替这种设计

  7. 38 通过复合塑膜出has-a或“根据某物实现出”
  8. 39 明智而谨慎的使用private继承
    1. EBO(empty base optimization)空白基类优化

      通常c++官方规定默默安插一个char到独立空对象中,所以sizeof一个这种空类得1 但是当空类作为基类时则不同 这段代码运行结果为 4 4

      #include <iostream>
      using namespace std;
      
      class Empty {
      
      };
      class A : public Empty {
        int x;  
      };
      int main() {
        cout  << sizeof(A) << endl;
        cout << sizeof(int) << endl;
        return 0;
      }
      

      而这段代码运行结果为 8 4

      #include <iostream>
      using namespace std;
      
      class Empty {
      
      };
      class A {
        Empty e;
        int x;  
      };
      int main() {
        cout  << sizeof(A) << endl;
        cout << sizeof(int) << endl;
        return 0;
      }
      
      1. private继承意味着根据某物实现出。通常应该尽量用复合来代替它。 但是当继承类需要访问protected base class的成员,或需要 重新定义继承而来的virtual函数时,这么设计是合理的。
      2. 和复合不同,private继承可以造成EBO优化。
  9. TODO 40 明智而谨慎的使用多重继承
    1. 多重继承比单一继承复杂。它可能导致新的歧义,以及对virtual继承的需要
    2. virtual继承会增加大小,速度,初始化(及赋值)复杂度等成本。如果virtual base classes不带任何数据,将是最具有实用价值的情况
    3. 多重继承的确有正当的用途。其中一个情节是:public继承某个接口和 private继承某个协助实现的类 的结合

3.11.7 模版与泛型编程

  1. 41 了解隐式接口和编译期多态

    显示接口就是在类的头文件中显示定义的接口,而隐式接口一般是指模版函数中的 T需要支持的操作接口.

    1. classes和templates都支持接口和多肽
    2. 对classes而言接口是显示的,以函数签名(函数名,参数列表,返回类型)为中心。 多态则是通过virtual函数发生于运行期。
    3. 对template参数而言,接口是隐式的,多态则是通过template具现化和函数 重载解析发生于编译期
  2. 42 了解typename的双重意义
    1. 声明template参数时,关键字class和typename可互换
    2. 嵌套从属类型需要用typename来标识;但是不能在base class lists (如class A : public B)或member initialization list内 使用。
  3. 43 学习处理模版化基类内的名称
    1. 在处理模版时,编译器不会进入base class templates的作用域内 查找成员名称,我们可以在derived class templates内通过“this->" 来指导编译器进入base class的作用域找;或者借助一个明白写出的“base class 资格修饰符" 完成。
  4. 44 将与参数无关的代码抽离templates
    1. templates生成多个classes和多个函数,所以任何template代码 豆瓣不该与某个造成膨胀的template参数产生相依关系
    2. 因非类型模版参数而参数而造成的代码膨胀,往往可以消除,做法是以 函数参数或class成员变量替换template参数
    3. 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制 表述的具现类型共享实现代码
  5. 45 运用成员函数模版接受所有兼容类型
    1. 请使用member function templates生成“可接受所以兼容 类型”的函数。
    2. 就算声明member templates用于“泛化copy构造”或泛化assignment操作, 还是需要声明正常的copy构造函数和copy赋值运算符,因为templates不改变 语言的基本规则。
  6. 46 函数模版需要类型转换时定义为非成员函数(24的扩充)
    1. 当编写一个class tempaltes,而它所提供的与此templates相关 的函数 支持”所有参数的隐式类型转换“时,请将那些函数定义为”class template 内部的friend 函数。因为如果不这样,编译器无法推断 出该为需要类型转换的参数具现化怎样的函数。
  7. 47 使用traits classes表现类型信息
    1. Traits classes使得“类型相关信息”在编译期可用。它们以templates和 “templates特化”完成实现. 用templates的原因是trais 必须能够用于 内置类型,这意味着把类型相关信息放在在类型内行不通。
    2. 重载技术使得traits classes有可能在编译期对类型执行jif…else测试。
  8. 48 template模版元编程

3.11.8 定制new和delete

  1. 49 了解new handler的行为

3.12 TODO MYSQL必知必会 pdf

3.12.2 第一章 了解SQL

数据库:保存有组织的数据的容器(通常是一个或一组文件)。 DBMS:数据库管理系统,创建和操纵数据库 表:某种特定类型数据的结构化清单 模式(schema):关于数据库和表的布局及特性的信息 列:表中的一个字段 行:表中的一个记录 主键:一列,其值能够唯一区分表中的行

3.12.3 第二章 MySQL简介

mac启动mysql:

mysql.server start
mysql -uroot

3.12.4 第三章 使用MySQL

  1. 连接

    为了连接到mysql,需要下面信息:

    • 主机名 本地为localhost
    • 端口 如果使用了端口3306以外的端口
    • 一个合法用户名
    • 用户口令 如果需要
  2. 选择数据库

    USE 关键字

    mysql> show databases;
    +--------------------+
    | Database           |
    +--------------------+
    | information_schema |
    | mysql              |
    | performance_schema |
    | sys                |
    +--------------------+
    4 rows in set (0.00 sec)
    
    mysql> use sys;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    
  3. 显示可用数据库和表
    1. 显示数据库
      mysql> show databases;
      +--------------------+
      | Database           |
      +--------------------+
      | information_schema |
      | mysql              |
      | performance_schema |
      | sys                |
      +--------------------+
      4 rows in set (0.01 sec)
      
    2. 显示表

      show tables;

    3. 显示列

      show columns from 表名;

3.12.5 第四章 基本SELECT

  1. DISTINCT

    必须直接放在列名前面,并且会应用于所有列而不仅是前置它的列

    mysql> select distinct vend_id from products;
    +---------+
    | vend_id |
    +---------+
    |    1001 |
    |    1002 |
    |    1003 |
    |    1005 |
    +---------+
    4 rows in set (0.01 sec)
    
  2. LIMIT
    mysql> select prod_name from products limit 5;
    +--------------+
    | prod_name    |
    +--------------+
    | .5 ton anvil |
    | 1 ton anvil  |
    | 2 ton anvil  |
    | Detonator    |
    | Bird seed    |
    +--------------+
    5 rows in set (0.00 sec)
    
    mysql> select prod_name from products limit 5,5;
    +--------------+
    | prod_name    |
    +--------------+
    | Carrots      |
    | Fuses        |
    | JetPack 1000 |
    | JetPack 2000 |
    | Oil can      |
    +--------------+
    5 rows in set (0.00 sec)
    

3.12.6 第五章 排序检索数据 ORDER BY

  1. 子句

    SQL语句由子句组成,有些子句是必须的,有些是可选的。一个子句通常由一个关键字和 所提供的数据组成。子句的例子有FROM子句, ORDER BY子句。

    子句之间是有顺序的, 次序不对会产生错误信息:FORM > WHERE > GROUP BY > HAVING > ORDER BY > LIMIT

  2. ORDER BY
    mysql> select prod_name from products order by prod_name;
    +----------------+
    | prod_name      |
    +----------------+
    | .5 ton anvil   |
    | 1 ton anvil    |
    | 2 ton anvil    |
    | Bird seed      |
    | Carrots        |
    | Detonator      |
    | Fuses          |
    | JetPack 1000   |
    | JetPack 2000   |
    | Oil can        |
    | Safe           |
    | Sling          |
    | TNT (1 stick)  |
    | TNT (5 sticks) |
    +----------------+
    14 rows in set (0.01 sec)
    
    
    通过非选择列排序
    mysql> select prod_name from products order by prod_price;
    +----------------+
    | prod_name      |
    +----------------+
    | Carrots        |
    | TNT (1 stick)  |
    | Fuses          |
    | Sling          |
    | .5 ton anvil   |
    | Oil can        |
    | 1 ton anvil    |
    | Bird seed      |
    | TNT (5 sticks) |
    | Detonator      |
    | 2 ton anvil    |
    | JetPack 1000   |
    | Safe           |
    | JetPack 2000   |
    +----------------+
    14 rows in set (0.00 sec)
    
    
    按多列排序
    mysql> select prod_id,prod_price,prod_name from products order by prod_price,prod_name;
    +---------+------------+----------------+
    | prod_id | prod_price | prod_name      |
    +---------+------------+----------------+
    | FC      |       2.50 | Carrots        |
    | TNT1    |       2.50 | TNT (1 stick)  |
    | FU1     |       3.42 | Fuses          |
    | SLING   |       4.49 | Sling          |
    | ANV01   |       5.99 | .5 ton anvil   |
    | OL1     |       8.99 | Oil can        |
    | ANV02   |       9.99 | 1 ton anvil    |
    | FB      |      10.00 | Bird seed      |
    | TNT2    |      10.00 | TNT (5 sticks) |
    | DTNTR   |      13.00 | Detonator      |
    | ANV03   |      14.99 | 2 ton anvil    |
    | JP1000  |      35.00 | JetPack 1000   |
    | SAFE    |      50.00 | Safe           |
    | JP2000  |      55.00 | JetPack 2000   |
    +---------+------------+----------------+
    14 rows in set (0.00 sec)
    
    
    
    指定排序方向(默认升序), DESC只应用到直接位于其前面的列名,所以对多个列降序排序必须对每个列指定DESC关键字 
    mysql> select prod_id,prod_price,prod_name from products order by prod_price DESC, prod_name;
    +---------+------------+----------------+
    | prod_id | prod_price | prod_name      |
    +---------+------------+----------------+
    | JP2000  |      55.00 | JetPack 2000   |
    | SAFE    |      50.00 | Safe           |
    | JP1000  |      35.00 | JetPack 1000   |
    | ANV03   |      14.99 | 2 ton anvil    |
    | DTNTR   |      13.00 | Detonator      |
    | FB      |      10.00 | Bird seed      |
    | TNT2    |      10.00 | TNT (5 sticks) |
    | ANV02   |       9.99 | 1 ton anvil    |
    | OL1     |       8.99 | Oil can        |
    | ANV01   |       5.99 | .5 ton anvil   |
    | SLING   |       4.49 | Sling          |
    | FU1     |       3.42 | Fuses          |
    | FC      |       2.50 | Carrots        |
    | TNT1    |       2.50 | TNT (1 stick)  |
    +---------+------------+----------------+
    14 rows in set (0.00 sec)
    

3.12.7 第六章 过滤数据 WHERE

  1. WHERE子句操作符
    =
    <> 不等于, 和!=等价
    !=
    <
    <=
    >
    >=
    BETWEEN … AND …
    IS NULL

3.12.8 第七章 高级WHERE AND OR NOT IN

  1. AND比OR优先级高
    mysql> select prod_name,prod_price from products
    -> where vend_id = 1002 OR vend_id = 1003 AND prod_price >= 10;
    +----------------+------------+
    | prod_name      | prod_price |
    +----------------+------------+
    | Fuses          |       3.42 |
    | Oil can        |       8.99 |
    | Detonator      |      13.00 |
    | Bird seed      |      10.00 |
    | Safe           |      50.00 |
    | TNT (5 sticks) |      10.00 |
    +----------------+------------+
    6 rows in set (0.01 sec)
    
    可以看到,返回的行中有两行价格小于10,证明AND是先于OR处理的. 正确的写法是
    mysql> select prod_name,prod_price from products
    where (vend_id = 1002 OR vend_id = 1003) AND prod_price >= 10;
    +----------------+------------+
    | prod_name      | prod_price |
    +----------------+------------+
    | Detonator      |      13.00 |
    | Bird seed      |      10.00 |
    | Safe           |      50.00 |
    | TNT (5 sticks) |      10.00 |
    +----------------+------------+
    4 rows in set (0.00 sec)
    
  2. IN

    检索出供应商1002和1003的所有产品

    mysql> select prod_name,prod_price from products
    -> where vend_id IN (1002,1003)
    -> order by prod_name;
    +----------------+------------+
    | prod_name      | prod_price |
    +----------------+------------+
    | Bird seed      |      10.00 |
    | Carrots        |       2.50 |
    | Detonator      |      13.00 |
    | Fuses          |       3.42 |
    | Oil can        |       8.99 |
    | Safe           |      50.00 |
    | Sling          |       4.49 |
    | TNT (1 stick)  |       2.50 |
    | TNT (5 sticks) |      10.00 |
    +----------------+------------+
    9 rows in set (0.01 sec)
    

    IN 功能与 OR相当,但有以下优点:

    1. IN语法更清楚直观
    2. 计算的次序更容易管理
    3. 一般执行更快
    4. 最大的优点是可以包含其他SELECT语句
  3. NOT

    mysql除了允许使用not对各种条件取反,还支持对in,between和exists子句取反

    mysql> select prod_name,prod_price from products where not vend_id > 1002;
    +--------------+------------+
    | prod_name    | prod_price |
    +--------------+------------+
    | .5 ton anvil |       5.99 |
    | 1 ton anvil  |       9.99 |
    | 2 ton anvil  |      14.99 |
    | Fuses        |       3.42 |
    | Oil can      |       8.99 |
    +--------------+------------+
    5 rows in set (0.00 sec)
    
    
    mysql> select prod_name,prod_price from products where vend_id not IN (1002,1003);
    +--------------+------------+
    | prod_name    | prod_price |
    +--------------+------------+
    | .5 ton anvil |       5.99 |
    | 1 ton anvil  |       9.99 |
    | 2 ton anvil  |      14.99 |
    | JetPack 1000 |      35.00 |
    | JetPack 2000 |      55.00 |
    +--------------+------------+
    5 rows in set (0.01 sec)
    

3.12.9 第八章 用通配符进行过滤

  1. LIKE操作符

    通配符本身实际是SQL的 WHERE子句 中有特殊含义的字符, 通配符前必须接LIKE操作符

  2. %(相当于shell的*)

    不能匹配NULL

    mysql> select prod_id, prod_name from products where prod_name LIKE 'jet%'
    -> ;
    +---------+--------------+
    | prod_id | prod_name    |
    +---------+--------------+
    | JP1000  | JetPack 1000 |
    | JP2000  | JetPack 2000 |
    +---------+--------------+
    2 rows in set (0.00 sec)
    
  3. _(相当于shell的.)
    mysql> select prod_id, prod_name from products where prod_name LIKE '_ ton anvil';
    +---------+-------------+
    | prod_id | prod_name   |
    +---------+-------------+
    | ANV02   | 1 ton anvil |
    | ANV03   | 2 ton anvil |
    +---------+-------------+
    2 rows in set (0.00 sec)
    

3.12.10 第九章 正则表达式

正则与通配符的区别:通配符合只能匹配整个字符,正则可以部分匹配.

3.12.11 第十章 计算字段

正则,匹配一部分
mysql> select prod_name from products where prod_name regexp '1000' order by prod_name;
+--------------+
| prod_name    |
+--------------+
| JetPack 1000 |
+--------------+
1 row in set (0.05 sec)


通配符,只能匹配全部,相当于正则加了 ^$
mysql> select prod_name from products where prod_name like '1000' order by prod_name;
Empty set (0.02 sec)
  1. concat 拼接字段
    mysql> select concat(vend_name, ' (', vend_country, ')')
    -> from vendors
    -> order by vend_name;
    +--------------------------------------------+
    | concat(vend_name, ' (', vend_country, ')') |
    +--------------------------------------------+
    | ACME (USA)                                 |
    | Anvils R Us (USA)                          |
    | Furball Inc. (USA)                         |
    | Jet Set (England)                          |
    | Jouets Et Ours (France)                    |
    | LT Supplies (USA)                          |
    +--------------------------------------------+
    6 rows in set (0.01 sec)
    
  2. 去掉首尾空格

    去掉左右两边 Trim(), 去掉左边 LTrim(), 去掉右边 RTrim()

  3. 别名 AS
    mysql> select concat(vend_name, ' (', vend_country, ')') as vend_title from vendors order by vend_name;
    +-------------------------+
    | vend_title              |
    +-------------------------+
    | ACME (USA)              |
    | Anvils R Us (USA)       |
    | Furball Inc. (USA)      |
    | Jet Set (England)       |
    | Jouets Et Ours (France) |
    | LT Supplies (USA)       |
    +-------------------------+
    6 rows in set (0.01 sec)
    
  4. 执行算术计算
    mysql> select prod_id,
    -> quantity,
    -> item_price,
    -> quantity*item_price as expanded_price
    -> from orderitems
    -> where order_num = 20005;
    +---------+----------+------------+----------------+
    | prod_id | quantity | item_price | expanded_price |
    +---------+----------+------------+----------------+
    | ANV01   |       10 |       5.99 |          59.90 |
    | ANV02   |        3 |       9.99 |          29.97 |
    | TNT2    |        5 |      10.00 |          50.00 |
    | FB      |        1 |      10.00 |          10.00 |
    +---------+----------+------------+----------------+
    4 rows in set (0.01 sec)
    

    `k`k`

3.12.12 第十一章 数据处理函数

大多数SQL实现支持以下类型的函数:

  1. 文本函数 用于处理文本串(如删除或填充值,转换大小写)

    2020-11-25_10-10-12_screenshot.png

  2. 数值函数 用于在数值进行算术操作(如返回绝对值,进行代数运算)

    2020-11-25_10-11-27_screenshot.png

  3. 日期和时间函数 用于处理日期和时间值并从这些值中提取特定成分(如返回两个日期之差,检查日期有效性)

    2020-11-25_10-10-52_screenshot.png

  4. 系统函数 返回DBMS正使用的特殊信息(如用户登录信息,检查版本细节)

3.12.13 第十二章 汇总数据(聚集函数)

  1. 聚集函数
    AVG() 返回某列的平均值
    COUNT() 返回某列的行数
    MAX() 返回某列的最大值
    MIN() 返回某列的最小值
    SUM() 返回某列值之和
    mysql> select avg(prod_price) as ap
    -> from products;
    +-----------+
    | ap        |
    +-----------+
    | 16.133571 |
    +-----------+
    1 row in set (0.01 sec)
    
    
    mysql> select prod_price as ap from products;
    +-------+
    | ap    |
    +-------+
    |  5.99 |
    |  9.99 |
    | 14.99 |
    | 13.00 |
    | 10.00 |
    |  2.50 |
    |  3.42 |
    | 35.00 |
    | 55.00 |
    |  8.99 |
    | 50.00 |
    |  4.49 |
    |  2.50 |
    | 10.00 |
    +-------+
    14 rows in set (0.00 sec)
    

    组合聚集函数

    mysql> select count(*) as num_items,
    -> min(prod_price) AS price_min,
    -> max(prod_price) AS price_max,
    -> AVG(prod_price) AS price_avg
    -> from products;
    +-----------+-----------+-----------+-----------+
    | num_items | price_min | price_max | price_avg |
    +-----------+-----------+-----------+-----------+
    |        14 |      2.50 |     55.00 | 16.133571 |
    +-----------+-----------+-----------+-----------+
    1 row in set (0.00 sec)
    

3.12.14 第十三章 分组数据 GROUP BY, HAVING

  1. GROUP BY
    mysql> select vend_id, count(*) as num_prods
    -> from products
    -> group by vend_id;
    +---------+-----------+
    | vend_id | num_prods |
    +---------+-----------+
    |    1001 |         3 |
    |    1002 |         2 |
    |    1003 |         7 |
    |    1005 |         2 |
    +---------+-----------+
    4 rows in set (0.00 sec)
    

    2020-11-25_10-48-19_screenshot.png

  2. HAVING 过滤分组

    与where的区别

    2020-11-25_10-52-11_screenshot.png

    mysql> select vend_id from products;
    +---------+
    | vend_id |
    +---------+
    |    1001 |
    |    1001 |
    |    1001 |
    |    1002 |
    |    1002 |
    |    1003 |
    |    1003 |
    |    1003 |
    |    1003 |
    |    1003 |
    |    1003 |
    |    1003 |
    |    1005 |
    |    1005 |
    +---------+
    14 rows in set (0.00 sec)
    
    
    mysql> select vend_id, count(*) as num_prods
    -> from products
    -> group by vend_id
    -> having count(*) >= 2;
    +---------+-----------+
    | vend_id | num_prods |
    +---------+-----------+
    |    1001 |         3 |
    |    1002 |         2 |
    |    1003 |         7 |
    |    1005 |         2 |
    +---------+-----------+
    4 rows in set (0.00 sec)
    
  3. 子句顺序

    2020-11-25_11-01-54_screenshot.png

3.12.15 第十四章 子查询

  1. 作为 where…in… 的过滤条件
mysql> select cust_name, cust_contact
-> from customers
-> where cust_id in (select cust_id
->                   from orders
->                   where order_num in (select order_num
->                                       from orderitems
->                                       where prod_id = 'TNT2'));
+----------------+--------------+
| cust_name      | cust_contact |
+----------------+--------------+
| Coyote Inc.    | Y Lee        |
| Yosemite Place | Y Sam        |
+----------------+--------------+
2 rows in set (0.01 sec)

可以看到,一般where后的字段与select子句中要查询的字段相同

  1. 作为计算字段使用子查询

    mysql> select cust_name
    -> ,cust_state,
    -> (select count(*)
    -> from orders
    -> where orders.cust_id = customers.cust_id) as orders
    -> from customers
    -> order by cust_name;
    +----------------+------------+--------+
    | cust_name      | cust_state | orders |
    +----------------+------------+--------+
    | Coyote Inc.    | MI         |      2 |
    | E Fudd         | IL         |      1 |
    | Mouse House    | OH         |      0 |
    | Wascals        | IN         |      1 |
    | Yosemite Place | AZ         |      1 |
    +----------------+------------+--------+
    5 rows in set (0.00 sec)
    

3.12.16 第十五章 联结表(join)

mysql> select * from vendors;
+---------+----------------+-----------------+-------------+------------+----------+--------------+
| vend_id | vend_name      | vend_address    | vend_city   | vend_state | vend_zip | vend_country |
+---------+----------------+-----------------+-------------+------------+----------+--------------+
|    1001 | Anvils R Us    | 123 Main Street | Southfield  | MI         | 48075    | USA          |
|    1002 | LT Supplies    | 500 Park Street | Anytown     | OH         | 44333    | USA          |
|    1003 | ACME           | 555 High Street | Los Angeles | CA         | 90046    | USA          |
|    1004 | Furball Inc.   | 1000 5th Avenue | New York    | NY         | 11111    | USA          |
|    1005 | Jet Set        | 42 Galaxy Road  | London      | NULL       | N16 6PS  | England      |
|    1006 | Jouets Et Ours | 1 Rue Amusement | Paris       | NULL       | 45678    | France       |
+---------+----------------+-----------------+-------------+------------+----------+--------------+
6 rows in set (0.00 sec)

mysql> select * from products;
+---------+---------+----------------+------------+----------------------------------------------------------------+
| prod_id | vend_id | prod_name      | prod_price | prod_desc                                                      |
+---------+---------+----------------+------------+----------------------------------------------------------------+
| ANV01   |    1001 | .5 ton anvil   |       5.99 | .5 ton anvil, black, complete with handy hook                  |
| ANV02   |    1001 | 1 ton anvil    |       9.99 | 1 ton anvil, black, complete with handy hook and carrying case |
| ANV03   |    1001 | 2 ton anvil    |      14.99 | 2 ton anvil, black, complete with handy hook and carrying case |
| DTNTR   |    1003 | Detonator      |      13.00 | Detonator (plunger powered), fuses not included                |
| FB      |    1003 | Bird seed      |      10.00 | Large bag (suitable for road runners)                          |
| FC      |    1003 | Carrots        |       2.50 | Carrots (rabbit hunting season only)                           |
| FU1     |    1002 | Fuses          |       3.42 | 1 dozen, extra long                                            |
| JP1000  |    1005 | JetPack 1000   |      35.00 | JetPack 1000, intended for single use                          |
| JP2000  |    1005 | JetPack 2000   |      55.00 | JetPack 2000, multi-use                                        |
| OL1     |    1002 | Oil can        |       8.99 | Oil can, red                                                   |
| SAFE    |    1003 | Safe           |      50.00 | Safe with combination lock                                     |
| SLING   |    1003 | Sling          |       4.49 | Sling, one size fits all                                       |
| TNT1    |    1003 | TNT (1 stick)  |       2.50 | TNT, red, single stick                                         |
| TNT2    |    1003 | TNT (5 sticks) |      10.00 | TNT, red, pack of 10 sticks                                    |
+---------+---------+----------------+------------+----------------------------------------------------------------+
14 rows in set (0.00 sec)


mysql> select vend_name, prod_name, prod_price
    -> from vendors, products
    -> where vendors.vend_id = products.vend_id
    -> order by vend_name, prod_name;
+-------------+----------------+------------+
| vend_name   | prod_name      | prod_price |
+-------------+----------------+------------+
| ACME        | Bird seed      |      10.00 |
| ACME        | Carrots        |       2.50 |
| ACME        | Detonator      |      13.00 |
| ACME        | Safe           |      50.00 |
| ACME        | Sling          |       4.49 |
| ACME        | TNT (1 stick)  |       2.50 |
| ACME        | TNT (5 sticks) |      10.00 |
| Anvils R Us | .5 ton anvil   |       5.99 |
| Anvils R Us | 1 ton anvil    |       9.99 |
| Anvils R Us | 2 ton anvil    |      14.99 |
| Jet Set     | JetPack 1000   |      35.00 |
| Jet Set     | JetPack 2000   |      55.00 |
| LT Supplies | Fuses          |       3.42 |
| LT Supplies | Oil can        |       8.99 |
+-------------+----------------+------------+
14 rows in set (0.00 sec  )

那么14章的子查询例子也可以写成联结:

mysql> select cust_name, cust_contact
-> from customers, orders, orderitems
-> where customers.cust_id = orders.cust_id
-> and orderitems.order_num = orders.order_num
-> and prod_id = 'TNT2';
    +----------------+--------------+
    | cust_name      | cust_contact |
    +----------------+--------------+
    | Coyote Inc.    | Y Lee        |
    | Yosemite Place | Y Sam        |
    +----------------+--------------+
    2 rows in set (0.00 sec)

2020-11-25_14-35-22_screenshot.png

3.12.17 第十六章 高级联结

  1. 自联结

    下面例子找到prodid = 'DINTR'的商品所属生产商的所有商品:

    mysql> select p1.prod_id, p1.prod_name
    -> from products as p1, products as p2
    -> where p1.vend_id = p2.vend_id
    -> and p2.prod_id = 'DTNTR';
    +---------+----------------+
    | prod_id | prod_name      |
    +---------+----------------+
    | DTNTR   | Detonator      |
    | FB      | Bird seed      |
    | FC      | Carrots        |
    | SAFE    | Safe           |
    | SLING   | Sling          |
    | TNT1    | TNT (1 stick)  |
    | TNT2    | TNT (5 sticks) |
    +---------+----------------+
    7 rows in set (0.03 sec)
    

    上面的例子还有个点就是表别名p1,p2

  2. 自然联结

    自然联结排除相同列的多次出现,使每个列只返回一次

  3. 外部联结
    内联结,只有order_num中有关联行时cust_id才会保留
    mysql> select customers.cust_id, orders.order_num
    -> from customers inner join orders
    -> on customers.cust_id = orders.cust_id
    -> ;
    +---------+-----------+
    | cust_id | order_num |
    +---------+-----------+
    |   10001 |     20005 |
    |   10001 |     20009 |
    |   10003 |     20006 |
    |   10004 |     20007 |
    |   10005 |     20008 |
    +---------+-----------+
    5 rows in set (0.00 sec)
    
    
    外联结,不管order_num中有没有关联的数据,所有cust_id都会出现
    mysql> select customers.cust_id, orders.order_num from customers left outer join orders on customers.cust_id = orders.cust_id;
    +---------+-----------+
    | cust_id | order_num |
    +---------+-----------+
    |   10001 |     20005 |
    |   10001 |     20009 |
    |   10002 |      NULL |
    |   10003 |     20006 |
    |   10004 |     20007 |
    |   10005 |     20008 |
    +---------+-----------+
    6 rows in set (0.00 sec)
    
  4. 注意事项

    2020-11-25_15-36-24_screenshot.png

3.12.18 第十七章 组合查询 UNION

mysql> select vend_id,prod_id,prod_price
-> from products
-> where prod_price <= 5
-> union
-> select vend_id, prod_id, prod_price
-> from products
-> where vend_id in (1001,1002)
-> order by vend_id, prod_price;
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id:    10
Current database: mysql必知必会

+---------+---------+------------+
| vend_id | prod_id | prod_price |
+---------+---------+------------+
|    1001 | ANV01   |       5.99 |
|    1001 | ANV02   |       9.99 |
|    1001 | ANV03   |      14.99 |
|    1002 | FU1     |       3.42 |
|    1002 | OL1     |       8.99 |
|    1003 | FC      |       2.50 |
|    1003 | TNT1    |       2.50 |
|    1003 | SLING   |       4.49 |
+---------+---------+------------+
8 rows in set (0.13 sec)

union all (保留重复行)

mysql> select vend_id,prod_id,prod_price
from products
where prod_price <= 5
union all
select vend_id, prod_id, prod_price
from products
where vend_id in (1001,1002)
order by vend_id, prod_price;

+---------+---------+------------+
| vend_id | prod_id | prod_price |
+---------+---------+------------+
|    1001 | ANV01   |       5.99 |
|    1001 | ANV02   |       9.99 |
|    1001 | ANV03   |      14.99 |
|    1002 | FU1     |       3.42 |
|    1002 | FU1     |       3.42 |    重复
|    1002 | OL1     |       8.99 |
|    1003 | FC      |       2.50 |
|    1003 | TNT1    |       2.50 |
|    1003 | SLING   |       4.49 |
+---------+---------+------------+
9 rows in set (0.00 sec)

3.12.19 TODO 第十八章 全文搜索

3.12.20 第十九章 插入数据 INSERT

  1. 1. 插入完整的行

    一般不要使用没有明确给出列的列表的insert语句,降低耦合性

    mysql> insert into customers(cust_name,
    -> cust_address,
    -> cust_city,
    -> cust_state,
    -> cust_zip,
    -> cust_country,
    -> cust_contact,
    -> cust_email)
    -> value('Pep E. LaPew',
    -> '100 Main Street',
    -> 'Los Angeles',
    -> 'CA',
    -> '90046',
    -> 'USA',
    -> null,
    -> null);
    Query OK, 1 row affected (0.06 sec)
    

    2020-11-26_09-50-46_screenshot.png

  2. 2. 插入部分行

    2020-11-26_09-59-07_screenshot.png

  3. 3. 插入多行

    单条insert语句处理多个插入比使用多条insert语句快.

    2020-11-26_09-53-15_screenshot.png

  4. 4. 插入select检索出的数据

    2020-11-26_09-55-12_screenshot.png

3.12.21 使用数据处理函数

3.12.22 附录B 例子安装

mysql> create database mysql必知必会;
mysql> use mysql必知必会
mysql> source /Users/binbinyang/Downloads/mysql_scripts/create.sql
mysql> source /Users/binbinyang/Downloads/mysql_scripts/populate.sql

3.13 linux shell 脚本攻略 pdf

3.13.1 第一章

  1. !#

    现在假设当前目录有一个名为script.sh的脚本文件 如果将脚本作为sh的命令行参数来运行,那么脚本中的shebang(#!)行也就没什么用处了: sh script.sh

    我们可以给它加上执行权限来去掉sh直接运行它

    #加执行权限
    chmod a+x script.sh
    
    #执行
    ./script.sh
    
    #如果script.sh所在目录在环境变量PATH中,甚至可以直接运行
    script.sh
    
  2. 引用 \ '和"

    ' " 和什么都不加都各有各的副作用:

    ~/l/l/linux-shell-脚本攻略 ❯❯❯ echo hello;hello                         14:59:02
    hello
    fish: Unknown command: hello
    fish:
    echo hello;hello
    ^
    ~/l/l/linux-shell-脚本攻略 ❯❯❯ echo '$var'                              15:01:38
    $var
    ~/l/l/linux-shell-脚本攻略 ❯❯❯ echo $var                                15:02:28
    
    bash-3.2$ echo "hello !"
    bash: !": event not found
    

    相当于\是细粒度的强引用,'是粗粒度的强(全部)引用, "是粗粒度的弱(部分)引用(不对$ ` \ 三个符号起作用)

  3. 变量
    1. 变量赋值语法: var=value =左右不能有空格,有空格的是相等操作
    2. 获取字符串变量长度

      var=1234567890
      echo ${#var}
      
      结果为10
      
  4. 通过shell进行数学运算

    在bash shell环境中,可以利用 let(())[] 执行基本的算术操作。

    nol=4
    no2=5
    let result=no1+no2
    let no1++
    let no1--
    let no1+=6
    let no1-=6
    
    result=$[ no1 + no2 ]
    result=$[ $no1 + 5]
    result=$[ $no1 + $no2]
    
    result=$(( no1 + 5 ))
    result=$((no1+5))
    
    result=`expr 3 + 4`
    result=$(expr $no1 + 5)
    
    

    而在进行高级操作时,可以用 exprbc

    echo "4 * 0.56" | bc
    
    no=54
    result=`echo "$no * 1.5` | bc
    
    
  5. 数组和关联数组(map)
    bash-3.2$ arr={1 2 3}       #错误,必须用()
    bash: 2: command not found
    
    bash-3.2$ arr=(1 2 3)
    bash-3.2$ echo $arr[1]     #错误,必须用{}括起来,不然会直接解析成$arr,而这个值是arr的第一个元素
    1[1]
    bash-3.2$ echo $arr[2]
    1[2]
    bash-3.2$ echo $arr[3]
    1[3]
    bash-3.2$ echo ${arr[3]}
    
    bash-3.2$ echo ${arr[2]}
    3
    
    #!/bin/bash
    arr=(1 2 3)
    arr=["fk"]="keyi"
    echo ${arr[*]}
    echo ${arr["fk"]}
    echo ${arr[0]}
    
    

    上面的运行结果为:

    keyi 2 3 keyi keyi

    所以在数组上执行map类似的操作会总是覆盖数组的第一个元素 TODO map怎么声明

  6. 调试
    #!/bin/bash
    function DEBUG(){
        [ "$_DEBUG" == "on" ] && $@ || :
    }
    
    for i in {1..10}
    do
        DEBUG echo $i
    done
    
    
    ~/l/l/linux-shell-脚本攻略 ❯❯❯ ./debug.sh                               17:59:16
    ~/l/l/linux-shell-脚本攻略 ❯❯❯ _DEBUG=on ./debug.sh                     17:59:18
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
  7. 函数

    2020-11-26_18-03-18_screenshot.png

    #!/bin/bash
    func(){
        echo $1,$2
        echo $1
        return 0
    }
    
    func 1 2 3
    
    
    执行结果:
    ~/l/l/linux-shell-脚本攻略 ❯❯❯ ./func.sh                             18:05:47
    1,2
    1
    
    1. 递归
      #!/bin/bash
      f(){
          echo $1
          f hello
          sleep 1
          return 0
      }
      f parent
      
      
    2. fork炸弹
    3. 导出函数

      export -f fname

  8. read命令
    #!/bin/bash
    #读入2个字符作为var的值
    read -n 2 var
    echo $var
    
    #用不回显的方式读取密码
    read -s var
    
    #显示提示信息
    read -p "Enter input:" var
    
    #在2秒内读输入
    read -t 2 var
    
    #用:作为定界符
    read -d ":" var
    
    
  9. 字段分隔符和迭代器
    #!/bin/bash
    #IFS 分隔符 internal field separator
    line="root:x:0:0:root:/root:/bin/bash"
    IFS=":"
    count=0
    for it in $line
    do
        [ $count -eq 0 ] && user=$it;
        [ $count -eq 6 ] && shell=$it;
        let count++
    done
    echo $user\'s shell is $shell
    
    
    #迭代器
    #语法:for var in list       list 可以是string或序列{*..*}
    for var in {1..100}    #不能有空格,否则会直接输出{ 1..100 }
    do
        echo $var
    done
    
  10. 比较和测试
    [ condition ] && action
    [ condition ] || action
    
    -gt >
    -lt <
    -ge >=
    -le <=
    -eq ==
    -ne !=
    
    #字符串比较
    str1="a"
    str2="b"
    [[ $str1 = $str2 ]]
    [[ $str1 == $str2 ]]
    [[ $str1 != $str2 ]]
    [[ $str1 > $str2 ]]
    [[ $str1 < $str2 ]]
    [[ -z $str1]]       #zero 是否为空
    [[ -n $str1]]       #not zero
    
    

    文件系统相关测试:

    [ -f $file ] 正常文件
    [ -x $file ] 可执行文件
    [ -d $file ] 目录
    [ -e $file ] 存在文件
    [ -c $file ] 字符设备文件
    [ -b $file ] 块设备文件
    [ -w $file ] 可写文件
    [ -r $file ] 可读文件
    [ -L $file ] 符号链接文件

3.13.2 第三章

  1. dd
    #测内存速度
    ~/l/l/linux-shell-脚本攻略 ❯❯❯ dd if=/dev/zero of=/dev/null bs=1m count=100
    100+0 records in
    100+0 records out
    104857600 bytes transferred in 0.005783 secs (18132535605 bytes/sec)
    
    #测硬盘速度
    ~/l/l/linux-shell-脚本攻略 ❯❯❯ dd if=/dev/zero of=junk.data bs=1m count=100
    100+0 records in
    100+0 records out
    104857600 bytes transferred in 0.243733 secs (430214734 bytes/sec)
    

3.14 设计模式 视频

3.14.1 8个设计原则

  1. 依赖倒置原则 高层模块(较稳定)不应该依赖于底层模式(易变化),二者都应该依赖于抽象(稳定) 抽象不应该依赖于实现细节,实现细节应该依赖于抽象 错误设计:

    2020-12-03_20-38-55_screenshot.png 正确设计:

    2020-12-03_20-38-29_screenshot.png

  2. 开放封闭原则(OCP) 对扩展开放,对更改封闭 类模块应该是可扩展的,但是不可改变
  3. 单一职责原则(SRP)
    • 一个类应该只有一个引起他变化的原因
    • 变化的方向隐含着类的责任
  4. 替换原则(LSP)
    • 一个类应该能够替换他们的子类(is a)
    • 继承表达类型抽象
  5. 接口隔离原则(ISP)
    • 不应该强迫客户程序依赖他们不用的方法
    • 接口应该小而完备
  6. 优先使用对象组合而不是继承
  7. 封装变化点
  8. 针对接口编程,而不是针对实现编程

3.14.2 组件协作模式

  1. p3 模版方法

    2020-12-15_21-01-14_screenshot.png

  2. p4 策略模式
  3. p5 观察者模式

3.14.3 单一职责模式

  1. TODO p6 装饰模式
  2. p7 桥模式
  3. p8 工厂模式

3.15 TODO redis设计与实现

3.15.1 第二章 简单动态字符串(SDS)

redis不直接使用c语言的传统字符串,而是建立了一种名为简单动态字符串 (SDS,simple dynamic string)的抽象类型。 除了用来存字符串之外,SDS还被用作缓冲区。

  1. 定义

3.16 高性能mysql

3.17 TODO 掘金小册子

3.18 TODO 15445

3.19 TODO tcp/ip详解

3.20 TODO 图解http

3.21 TODO unp

3.22 TODO 自我修养

3.23 b站chenshuo

3.23.1 2 TCP简单实验(测速度)

  1. dd | nc, nc -l > /dev/null (580M/s)

    数据的流动过程: /dev/zero(kernel)–>dd(user)–>pipe(k)–>nc(u)–>tcp(k) –>nc(u)–>/dev/null(k)

  2. nc < file, nc -l > /dev/null (1074M/s)

    数据流动过程: memory(k)–>nc(u)–>tcp(k) –>nc(u)–>/dev/null(k)

3.23.2 UDP vs TCP

UDP TCP
无链接 有链接
不可靠 可靠
数据报 字节流
所有客户可以共享一个fd 一个客户需要一个fd
线程安全(能同时多线程读一个fd 非线程安全

3.23.3 第三个例子 netcat

  1. tcp程序的标准动作
    1. SOREUSEADDR
    2. ignore SIGPIPE
    3. TCPNODELAY
  2. why non-blocking IO is a must(为什么IO多路复用中必须用非阻塞)
    1. 如果accept是阻塞的,那么当IO复用通知可以accept,而调用accept的时候,对方刚好关闭 了连接,那么accept将永远阻塞。
    2. linux下,select,poll,epoll可能被假唤醒,比如来了个checksum不对的数据包(被丢弃)
  3. What if sink is slow?

    如果带宽不匹配,那么待发送的数据会越积越多,最后爆内存。一个简单的办法是写buffer达到一定高水位 后关闭读,然后等写buffer到了一个低水位再打开

  4. Level-trigger and edge trigger

    读数据电平更简单,不会饥饿 写数据边沿简单,不会busy-loop

4 tools

4.1 cmake

4.1.1 cmake入门实战 https://www.hahack.com/codes/cmake/

4.2 makefile

4.2.2 自动化变量

\(@ 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,"\)@"就是匹配于目标中模式定义的集合。 $% 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是"foo.a(bar.o)",那么,"\(%"就是 "bar.o","\)@"就是"foo.a"。如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空。 \(< 依赖目标中的第一个目标名字。如果依赖目标是以模式(即"%")定义的,那么"\)<"将是符合模式的一系列的文件集。注意,其是一个一个取出来的。 $? 所有比目标新的依赖目标的集合。以空格分隔。 $^ 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。 \(+ 这个变量很像"\)^",也是所有依赖目标的集合。只是它不去除重复的依赖目标。

5 mac软件安装

5.1 mysql

We've installed your MySQL database without a root password. To secure it run:
    mysql_secure_installation

MySQL is configured to only allow connections from localhost by default

To connect run:
    mysql -uroot

To have launchd start mysql now and restart at login:
  brew services start mysql
Or, if you don't want/need a background service you can just run:
mysql.server start

6 todolist

6.1 shell

  1. 批量telnet脚本怎么写

6.2 操作系统

进程退出时内核要处理哪些东西(特别内存)

7 leetcode

7.1

链接:https://leetcode-cn.com/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/solution/zhong-gui-zhong-ju-biao-zhun-floyd-warshall-mo-ban/

for (int k = 0; k < n; k++) {
  // n个顶点依次作为插入点
  // 注意插点k是放在第一层循环,因为这是状态压缩的dp
  for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
      // 遍历各个顶点之间的距离,并用插入点进行更新
      D[i][j] = min(D[i][k]+D[k][j], D[i][j]);
    }
  }
 }

7.2 并查集

7.2.1 TODO 1319

提交出错,bug没调,并查集模版总结

7.4 差分数组

简单来说,差分数组 diff[i],存储的是 res[i] - res[i - 1];而差分数组 diff[0…i] 的和,就是 res[i] 的值。 大家可以用一个小数据试验一下,很好理解。 如果我们想给 [l, r] 的区间加上一个数字 a, 只需要 diff[l] += a,diff[r + 1] -= a。 这样做,diff[0…i] 的和,就是更新后 res[i] 的值。 依然是,大家可以用一个小数据试验一下,其实很好理解。 这里不赘述,更严谨的分析看上面这个文档:https://oi-wiki.org/basic/prefix-sum/#_6 https://leetcode-cn.com/problems/minimum-moves-to-make-array-complementary/solution/jie-zhe-ge-wen-ti-xue-xi-yi-xia-chai-fen-shu-zu-on/

7.7 DFS

7.8 next

7.9 括号问题

7.9.1 1249 https://leetcode-cn.com/problems/minimum-remove-to-make-valid-parentheses/

可以把要删除的括号置为特殊字符来代表删除h

7.10 前缀和

1248 https://leetcode-cn.com/problems/count-number-of-nice-subarrays/ 想到个思路:把所有奇数的坐标先用个数组记录下来

7.16 分治

8 offer

TODO https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/ TODO https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/comments/ TODO https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/ TODO https://leetcode-cn.com/problems/jian-sheng-zi-lcof/comments/ TODO 递归解法 https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/ TODO https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/ TODO https://leetcode-cn.com/problems/dui-cheng-de-er-cha-shu-lcof/comments/ 不会 https://leetcode-cn.com/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/ 不会 https://leetcode-cn.com/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof/comments/ https://leetcode-cn.com/problems/add-two-numbers/ https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/ https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/solution/mian-shi-ti-33-er-cha-sou-suo-shu-de-hou-xu-bian-6/ https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/comments/ https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/ https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/solution/mian-shi-ti-38-zi-fu-chuan-de-pai-lie-hui-su-fa-by/ https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/zui-xiao-de-kge-shu-by-leetcode-solution/ https://leetcode-cn.com/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/comments/ https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/comments/ https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/submissions/ https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/submissions/ https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/solution/java-dong-tai-gui-hua-by-zhi-xiong/ https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/submissions/ https://leetcode-cn.com/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/comments/ https://leetcode-cn.com/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof/submissions/ https://leetcode-cn.com/problems/3sum/ https://leetcode-cn.com/problems/generate-parentheses/ https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/ https://leetcode-cn.com/problems/course-schedule-ii/comments/ map实现的桶排序 https://leetcode-cn.com/problems/contains-duplicate-iii/solution/cun-zai-zhong-fu-yuan-su-iii-by-leetcode/ 二维dp https://leetcode-cn.com/problems/maximal-square/ TODO 怎么证明 完全二叉树结点个数 https://leetcode-cn.com/problems/count-complete-tree-nodes/

会做,总结一下:https://leetcode-cn.com/problems/rectangle-area/submissions/ 判断两个矩形有没有重叠: 实现一个计算器 https://leetcode-cn.com/problems/basic-calculator-ii/comments/

链表排序 https://leetcode-cn.com/problems/sort-list/comments/ https://leetcode-cn.com/problems/implement-trie-prefix-tree/

9 速查表

9.1 languages

9.1.1 bash

##############################################################################

##############################################################################

##############################################################################

##############################################################################

CTRL+A # 移动到行首,同 <Home> CTRL+B # 向后移动,同 <Left> CTRL+C # 结束当前命令 CTRL+D # 删除光标前的字符,同 <Delete> ,或者没有内容时,退出会话 CTRL+E # 移动到行末,同 <End> CTRL+F # 向前移动,同 <Right> CTRL+G # 退出当前编辑(比如正在 CTRL+R 搜索历史时) CTRL+H # 删除光标左边的字符,同 <Backspace> CTRL+K # 删除光标位置到行末的内容 CTRL+L # 清屏并重新显示 CTRL+N # 移动到命令历史的下一行,同 <Down> CTRL+O # 类似回车,但是会显示下一行历史 CTRL+P # 移动到命令历史的上一行,同 <Up> CTRL+R # 历史命令反向搜索,使用 CTRL+G 退出搜索 CTRL+S # 历史命令正向搜索,使用 CTRL+G 退出搜索 CTRL+T # 交换前后两个字符 CTRL+U # 删除字符到行首 CTRL+V # 输入字符字面量,先按 CTRL+V 再按任意键 CTRL+W # 删除光标左边的一个单词 CTRL+X # 列出可能的补全 CTRL+Y # 粘贴前面 CTRL+u/k/w 删除过的内容 CTRL+Z # 暂停前台进程返回 bash,需要时可用 fg 将其切换回前台 CTRL+_ # 撤销(undo),有的终端将 CTRL+_ 映射为 CTRL+/ 或 CTRL+7

ALT+b # 向后(左边)移动一个单词 ALT+d # 删除光标后(右边)一个单词 ALT+f # 向前(右边)移动一个单词 ALT+t # 交换字符 ALT+BACKSPACE # 删除光标前面一个单词,类似 CTRL+W,但不影响剪贴板

CTRL+X CTRL+X # 连续按两次 CTRL+X,光标在当前位置和行首来回跳转 CTRL+X CTRL+E # 用你指定的编辑器,编辑当前命令

##############################################################################

##############################################################################

exit # 退出当前登陆 env # 显示环境变量 echo $SHELL # 显示你在使用什么 SHELL

bash # 使用 bash,用 exit 返回 which bash # 搜索 $PATH,查找哪个程序对应命令 bash whereis bash # 搜索可执行,头文件和帮助信息的位置,使用系统内建数据库 whatis bash # 查看某个命令的解释,一句话告诉你这是干什么的

clear # 清初屏幕内容 reset # 重置终端(当你不小心 cat 了一个二进制,终端状态乱掉时使用)

##############################################################################

##############################################################################

cd # 返回自己 $HOME 目录 cd {dirname} # 进入目录 pwd # 显示当前所在目录 mkdir {dirname} # 创建目录 mkdir -p {dirname} # 递归创建目录 pushd {dirname} # 目录压栈并进入新目录 popd # 弹出并进入栈顶的目录 dirs -v # 列出当前目录栈 cd - # 回到之前的目录 cd -{N} # 切换到目录栈中的第 N个目录,比如 cd -2 将切换到第二个

##############################################################################

##############################################################################

ls # 显示当前目录内容,后面可接目录名:ls {dir} 显示指定目录 ls -l # 列表方式显示目录内容,包括文件日期,大小,权限等信息 ls -1 # 列表方式显示目录内容,只显示文件名称,减号后面是数字 1 ls -a # 显示所有文件和目录,包括隐藏文件(.开头的文件/目录名) ln -s {fn} {link} # 给指定文件创建一个软链接 cp {src} {dest} # 拷贝文件,cp -r dir1 dir2 可以递归拷贝(目录) rm {fn} # 删除文件,rm -r 递归删除目录,rm -f 强制删除 mv {src} {dest} # 移动文件,如果 dest 是目录,则移动,是文件名则覆盖 touch {fn} # 创建或者更新一下制定文件 cat {fn} # 输出文件原始内容 anycmd > {fn} # 执行任意命令并将标准输出重定向到指定文件 more {fn} # 逐屏显示某文件内容,空格翻页,q 退出 less {fn} # 更高级点的 more,更多操作,q 退出 head {fn} # 显示文件头部数行,可用 head -3 abc.txt 显示头三行 tail {fn} # 显示文件尾部数行,可用 tail -3 abc.txt 显示尾部三行 tail -f {fn} # 持续显示文件尾部数据,可用于监控日志 nano {fn} # 使用 nano 编辑器编辑文件 vim {fn} # 使用 vim 编辑文件 diff {f1} {f2} # 比较两个文件的内容 wc {fn} # 统计文件有多少行,多少个单词 chmod 644 {fn} # 修改文件权限为 644,可以接 -R 对目录循环改权限 chgrp group {fn} # 修改文件所属的用户组 chown user1 {fn} # 修改文件所有人为 user1, chown user1:group1 fn 可以修改组 file {fn} # 检测文件的类型和编码 basename {fn} # 查看文件的名字(不包括路径) dirname {fn} # 查看文件的路径(不包括名字) grep {pat} {fn} # 在文件中查找出现过 pat 的内容 grep -r {pat} . # 在当前目录下递归查找所有出现过 pat 的文件内容 stat {fn} # 显示文件的详细信息

##############################################################################

##############################################################################

whoami # 显示我的用户名 who # 显示已登陆用户信息,w / who / users 内容略有不同 w # 显示已登陆用户信息,w / who / users 内容略有不同 users # 显示已登陆用户信息,w / who / users 内容略有不同 passwd # 修改密码,passwd {user} 可以用于 root 修改别人密码 finger {user} # 显示某用户信息,包括 id, 名字, 登陆状态等 adduser {user} # 添加用户 deluser {user} # 删除用户 w # 查看谁在线 su # 切换到 root 用户 su - # 切换到 root 用户并登陆(执行登陆脚本) su {user} # 切换到某用户 su -{user} # 切换到某用户并登陆(执行登陆脚本) id {user} # 查看用户的 uid,gid 以及所属其他用户组 id -u {user} # 打印用户 uid id -g {user} # 打印用户 gid write {user} # 向某用户发送一句消息 last # 显示最近用户登陆列表 last {user} # 显示登陆记录 lastb # 显示失败登陆记录 lastlog # 显示所有用户的最近登陆记录 sudo {command} # 以 root 权限执行某命令

##############################################################################

##############################################################################

ps # 查看当前会话进程 ps ax # 查看所有进程,类似 ps -e ps aux # 查看所有进程详细信息,类似 ps -ef ps auxww # 查看所有进程,并且显示进程的完整启动命令 ps -u {user} # 查看某用户进程 ps axjf # 列出进程树 ps xjf -u {user} # 列出某用户的进程树 ps -eo pid,user,command # 按用户指定的格式查看进程 ps aux | grep httpd # 查看名为 httpd 的所有进程 ps –ppid {pid} # 查看父进程为 pid 的所有进程 pstree # 树形列出所有进程,pstree 默认一般不带,需安装 pstree {user} # 进程树列出某用户的进程 pstree -u # 树形列出所有进程以及所属用户 pgrep {procname} # 搜索名字匹配的进程的 pid,比如 pgrep apache2

kill {pid} # 结束进程 kill -9 {pid} # 强制结束进程,9/SIGKILL 是强制不可捕获结束信号 kill -KILL {pid} # 强制执行进程,kill -9 的另外一种写法 kill -l # 查看所有信号 kill -l TERM # 查看 TERM 信号的编号 killall {procname} # 按名称结束所有进程 pkill {procname} # 按名称结束进程,除名称外还可以有其他参数

top # 查看最活跃的进程 top -u {user} # 查看某用户最活跃的进程

anycommand & # 在后台运行某命令,也可用 CTRL+Z 将当前进程挂到后台 jobs # 查看所有后台进程(jobs) bg # 查看后台进程,并切换过去 fg # 切换后台进程到前台 fg {job} # 切换特定后台进程到前台

trap cmd sig1 sig2 # 在脚本中设置信号处理命令 trap "" sig1 sig2 # 在脚本中屏蔽某信号 trap - sig1 sig2 # 恢复默认信号处理行为

nohup {command} # 长期运行某程序,在你退出登陆都保持它运行 nohup {command} & # 在后台长期运行某程序 disown {PID|JID} # 将进程从后台任务列表(jobs)移除

wait # 等待所有后台进程任务结束

##############################################################################

##############################################################################

ssh user@host # 以用户 user 登陆到远程主机 host ssh -p {port} user@host # 指定端口登陆主机 ssh-copy-id user@host # 拷贝你的 ssh key 到远程主机,避免重复输入密码 scp {fn} user@host:path # 拷贝文件到远程主机 scp user@host:path dest # 从远程主机拷贝文件回来 scp -P {port} … # 指定端口远程拷贝文件

uname -a # 查看内核版本等信息 man {help} # 查看帮助 man -k {keyword} # 查看哪些帮助文档里包含了该关键字 info {help} # 查看 info pages,比 man 更强的帮助系统 uptime # 查看系统启动时间 date # 显示日期 cal # 显示日历 vmstat # 显示内存和 CPU 使用情况 vmstat 10 # 每 10 秒打印一行内存和 CPU情况,CTRL+C 退出 free # 显示内存和交换区使用情况 df # 显示磁盘使用情况 du # 显示当前目录占用,du . –max-depth=2 可以指定深度 uname # 显示系统版本号 hostname # 显示主机名称 showkey -a # 查看终端发送的按键编码

ping {host} # ping 远程主机并显示结果,CTRL+C 退出 ping -c N {host} # ping 远程主机 N 次 traceroute {host} # 侦测路由连通情况 mtr {host} # 高级版本 traceroute host {domain} # DNS 查询,{domain} 前面可加 -a 查看详细信息 whois {domain} # 取得域名 whois 信息 dig {domain} # 取得域名 dns 信息 route -n # 查看路由表 netstat -a # 列出所有端口 netstat -an # 查看所有连接信息,不解析域名 netstat -anp # 查看所有连接信息,包含进程信息(需要 sudo) netstat -l # 查看所有监听的端口 netstat -t # 查看所有 TCP 链接 netstat -lntu # 显示所有正在监听的 TCP 和 UDP 信息 netstat -lntup # 显示所有正在监听的 socket 及进程信息 netstat -i # 显示网卡信息 netstat -rn # 显示当前系统路由表,同 route -n ss -an # 比 netstat -an 更快速更详细 ss -s # 统计 TCP 的 established, wait 等

wget {url} # 下载文件,可加 –no-check-certificate 忽略 ssl 验证 wget -qO- {url} # 下载文件并输出到标准输出(不保存) curl -sL {url} # 同 wget -qO- {url} 没有 wget 的时候使用

sz {file} # 发送文件到终端,zmodem 协议 rz # 接收终端发送过来的文件

##############################################################################

##############################################################################

varname=value # 定义变量 varname=value command # 定义子进程变量并执行子进程 echo $varname # 查看变量内容 echo $$ # 查看当前 shell 的进程号 echo $! # 查看最近调用的后台任务进程号 echo $? # 查看最近一条命令的返回码 export VARNAME=value # 设置环境变量(将会影响到子进程)

array[0]=valA # 定义数组 array[1]=valB array[2]=valC array=([0]=valA [1]=valB [2]=valC) # 另一种方式 array=(valA valB valC) # 另一种方式

${array[i]} # 取得数组中的元素 ${#array[@]} # 取得数组的长度 ${#array[i]} # 取得数组中某个变量的长度

declare -a # 查看所有数组 declare -f # 查看所有函数 declare -F # 查看所有函数,仅显示函数名 declare -i # 查看所有整数 declare -r # 查看所有只读变量 declare -x # 查看所有被导出成环境变量的东西 declare -p varname # 输出变量是怎么定义的(类型+值)

${varname:-word} # 如果变量不为空则返回变量,否则返回 word ${varname:=word} # 如果变量不为空则返回变量,否则赋值成 word 并返回 ${varname:?message} # 如果变量不为空则返回变量,否则打印错误信息并退出 ${varname:+word} # 如果变量不为空则返回 word,否则返回 null ${varname:offset:len} # 取得字符串的子字符串

${variable#pattern} # 如果变量头部匹配 pattern,则删除最小匹配部分返回剩下的 ${variable##pattern} # 如果变量头部匹配 pattern,则删除最大匹配部分返回剩下的 ${variable%pattern} # 如果变量尾部匹配 pattern,则删除最小匹配部分返回剩下的 ${variable%%pattern} # 如果变量尾部匹配 pattern,则删除最大匹配部分返回剩下的 ${variable/pattern/str} # 将变量中第一个匹配 pattern 的替换成 str,并返回 ${variable//pattern/str} # 将变量中所有匹配 pattern 的地方替换成 str 并返回

${#varname} # 返回字符串长度

*(patternlist) # 零次或者多次匹配 +(patternlist) # 一次或者多次匹配 ?(patternlist) # 零次或者一次匹配 @(patternlist) # 单词匹配 !(patternlist) # 不匹配

array=($text) # 按空格分隔 text 成数组,并赋值给变量 IFS="/" array=(\(text) # 按斜杆分隔字符串 text 成数组,并赋值给变量 text="\){array[*]}" # 用空格链接数组并赋值给变量 text=\((IFS=/; echo "\){array[*]}") # 用斜杠链接数组并赋值给变量

A=( foo bar "a b c" 42 ) # 数组定义 B=("\({A[@]:1:2}") # 数组切片:B=( bar "a b c" ) C=("\){A[@]:1}") # 数组切片:C=( bar "a b c" 42 ) echo "\({B[@]}" # bar a b c echo "\){B[1]}" # a b c echo "\({C[@]}" # bar a b c 42 echo "\){C[@]: -2:2}" # a b c 42 减号前的空格是必须的

\((UNIX command) # 运行命令,并将标准输出内容捕获并返回 varname=\)(id -u user) # 将用户名为 user 的 uid 赋值给 varname 变量

num=\((expr 1 + 2) # 兼容 posix sh 的计算,使用 expr 命令计算结果 num=\)(expr $num + 1) # 数字自增 expr 2 \* \( 2 + 3 \) # 兼容 posix sh 的复杂计算,输出 10

num=$((1 + 2)) # 计算 1+2 赋值给 num,使用 bash 独有的 \(((..)) 计算 num=\)((\(num + 1)) # 变量递增 num=\)((num + 1)) # 变量递增,双括号内的 $ 可以省略 num=$((1 + (2 + 3) * 2)) # 复杂计算

##############################################################################

##############################################################################

!! # 上一条命令 !^ # 上一条命令的第一个单词 !:n # 上一条命令的第n个单词 !:n-$ # 上一条命令的第n个单词到最后一个单词 !$ # 上一条命令的最后一个单词 !-n:$ # 上n条命令的最后一个单词 !string # 最近一条包含string的命令 !string1string2 # 最近一条包含string1的命令, 快速替换string1为string2 !# # 本条命令之前所有的输入内容 !#:n # 本条命令之前的第n个单词, 快速备份cp /etc/passwd !#:1.bak

##############################################################################

##############################################################################

function myfunc() {

{shell commands …} }

myfunc # 调用函数 myfunc myfunc arg1 arg2 arg3 # 带参数的函数调用 myfunc "\(@" # 将所有参数传递给函数 myfunc "\){array[@]}" # 将一个数组当作多个参数传递给函数 shift # 参数左移

unset -f myfunc # 删除函数 declare -f # 列出函数定义

##############################################################################

##############################################################################

statement1 && statement2 # and 操作符 statement1 || statement2 # or 操作符

exp1 -a exp2 # exp1 和 exp2 同时为真时返回真(POSIX XSI扩展) exp1 -o exp2 # exp1 和 exp2 有一个为真就返回真(POSIX XSI扩展) ( expression ) # 如果 expression 为真时返回真,输入注意括号前反斜杆 ! expression # 如果 expression 为假那返回真

str1 = str2 # 判断字符串相等,如 [ "$x" = "$y" ] && echo yes str1 != str2 # 判断字符串不等,如 [ "$x" != "$y" ] && echo yes str1 < str2 # 字符串小于,如 [ "$x" \< "$y" ] && echo yes str2 > str2 # 字符串大于,注意 < 或 > 是字面量,输入时要加反斜杆 -n str1 # 判断字符串不为空(长度大于零) -z str1 # 判断字符串为空(长度等于零)

-a file # 判断文件存在,如 [ -a /tmp/abc ] && echo "exists" -d file # 判断文件存在,且该文件是一个目录 -e file # 判断文件存在,和 -a 等价 -f file # 判断文件存在,且该文件是一个普通文件(非目录等) -r file # 判断文件存在,且可读 -s file # 判断文件存在,且尺寸大于0 -w file # 判断文件存在,且可写 -x file # 判断文件存在,且执行 -N file # 文件上次修改过后还没有读取过 -O file # 文件存在且属于当前用户 -G file # 文件存在且匹配你的用户组 file1 -nt file2 # 文件1 比 文件2 新 file1 -ot file2 # 文件1 比 文件2 旧

num1 -eq num2 # 数字判断:num1 = num2 num1 -ne num2 # 数字判断:num1 ! num2 num1 -lt num2 # 数字判断:num1 < num2 num1 -le num2 # 数字判断:num1 <= num2 num1 -gt num2 # 数字判断:num1 > num2 num1 -ge num2 # 数字判断:num1 >= num2

##############################################################################

##############################################################################

test {expression} # 判断条件为真的话 test 程序返回0 否则非零 [ expression ] # 判断条件为真的话返回0 否则非零

test "abc" = "def" # 查看返回值 echo $? 显示 1,因为条件为假 test "abc" != "def" # 查看返回值 echo $? 显示 0,因为条件为真

test -a /tmp; echo $? # 调用 test 判断 /tmp 是否存在,并打印 test 的返回值 [ -a /tmp ]; echo $? # 和上面完全等价,/tmp 肯定是存在的,所以输出是 0

test cond && cmd1 # 判断条件为真时执行 cmd1 [ cond ] && cmd1 # 和上面完全等价 [ cond ] && cmd1 || cmd2 # 条件为真执行 cmd1 否则执行 cmd2

if test -e /etc/passwd; then echo "alright it exists … " else echo "it doesn't exist … " fi

if [ -e /etc/passwd ]; then echo "alright it exists … " else echo "it doesn't exist … " fi

[ -e /etc/passwd ] && echo "alright it exists" || echo "it doesn't exist"

if [ "$varname" = "foo" ]; then echo "this is foo" elif [ "$varname" = "bar" ]; then echo "this is bar" else echo "neither" fi

if [ $x -gt 10 ] && [ $x -lt 20 ]; then echo "yes, between 10 and 20" fi

[ $x -gt 10 ] && [ $x -lt 20 ] && echo "yes, between 10 and 20"

if [ \( $x -gt 10 \) -a \( $x -lt 20 \) ]; then echo "yes, between 10 and 20" fi

[ \( $x -gt 10 \) -a \( $x -lt 20 \) ] && echo "yes, between 10 and 20"

[ -x /bin/ls ] && /bin/ls -l

https://www.ibm.com/developerworks/library/l-bash-test/index.html

##############################################################################

##############################################################################

while condition; do statements done

i=1 while [ $i -le 10 ]; do echo \(i; i=\)(expr $i + 1) done

for i in {1..10}; do echo $i done

for name [in list]; do statements done

for f in /home/*; do echo $f done

for (( initialisation ; ending condition ; update )); do statements done

for ((i = 0; i < 10; i++)); do echo $i; done

case expression in pattern1 ) statements ;; pattern2 ) statements ;;

  • ) otherwise ;;

esac

until condition; do statements done

select name [in list]; do statements that can use $name done

##############################################################################

##############################################################################

command ls # 忽略 alias 直接执行程序或者内建命令 ls builtin cd # 忽略 alias 直接运行内建的 cd 命令 enable # 列出所有 bash 内置命令,或禁止某命令 help {builtincommand} # 查看内置命令的帮助(仅限 bash 内置命令)

eval $script # 对 script 变量中的字符串求值(执行)

##############################################################################

##############################################################################

cmd1 | cmd2 # 管道,cmd1 的标准输出接到 cmd2 的标准输入 < file # 将文件内容重定向为命令的标准输入 > file # 将命令的标准输出重定向到文件,会覆盖文件 >> file # 将命令的标准输出重定向到文件,追加不覆盖 >| file # 强制输出到文件,即便设置过:set -o noclobber n>| file # 强制将文件描述符 n的输出重定向到文件 <> file # 同时使用该文件作为标准输入和标准输出 n<> file # 同时使用文件作为文件描述符 n 的输出和输入 n> file # 重定向文件描述符 n 的输出到文件 n< file # 重定向文件描述符 n 的输入为文件内容 n>& # 将标准输出 dup/合并 到文件描述符 n n<& # 将标准输入 dump/合并 定向为描述符 n n>&m # 文件描述符 n 被作为描述符 m 的副本,输出用 n<&m # 文件描述符 n 被作为描述符 m 的副本,输入用 &>file # 将标准输出和标准错误重定向到文件 <&- # 关闭标准输入 >&- # 关闭标准输出 n>&- # 关闭作为输出的文件描述符 n n<&- # 关闭作为输入的文件描述符 n diff <(cmd1) <(cmd2) # 比较两个命令的输出

##############################################################################

##############################################################################

cut -c 1-16 # 截取每行头16个字符 cut -c 1-16 file # 截取指定文件中每行头 16个字符 cut -c3- # 截取每行从第三个字符开始到行末的内容 cut -d':' -f5 # 截取用冒号分隔的第五列内容 cut -d';' -f2,10 # 截取用分号分隔的第二和第十列内容 cut -d' ' -f3-7 # 截取空格分隔的三到七列 echo "hello" | cut -c1-3 # 显示 hel echo "hello sir" | cut -d' ' -f2 # 显示 sir ps | tr -s " " | cut -d " " -f 2,3,4 # cut 搭配 tr 压缩字符

##############################################################################

##############################################################################

awk '{print $5}' file # 打印文件中以空格分隔的第五列 awk -F ',' '{print $5}' file # 打印文件中以逗号分隔的第五列 awk 'str {print $2}' file # 打印文件中包含 str 的所有行的第二列 awk -F ',' '{print $NF}' file # 打印逗号分隔的文件中的每行最后一列 awk '{s+=$1} END {print s}' file # 计算所有第一列的合 awk 'NR%3==1' file # 从第一行开始,每隔三行打印一行

sed 's/find/replace/' file # 替换文件中首次出现的字符串并输出结果 sed '10s/find/replace/' file # 替换文件第 10 行内容 sed '10,20s/find/replace/' file # 替换文件中 10-20 行内容 sed -r 's/regex/replace/g' file # 替换文件中所有出现的字符串 sed -i 's/find/replace/g' file # 替换文件中所有出现的字符并且覆盖文件 sed -i '/find/i\newline' file # 在文件的匹配文本前插入行 sed -i 'find/a\newline' file # 在文件的匹配文本后插入行 sed '/line/s/find/replace' file # 先搜索行特征再执行替换 sed -e 's/f/r/' -e 's/f/r' file # 执行多次替换 sed 's#find#replace#' file # 使用 # 替换 / 来避免 pattern 中有斜杆 sed -i -r 's/^\s+//g' file # 删除文件每行头部空格 sed '^$/d' file # 删除文件空行并打印 sed -i 's\s\+$//' file # 删除文件每行末尾多余空格 sed -n '2p' file # 打印文件第二行 sed -n '2,5p' file # 打印文件第二到第五行

##############################################################################

##############################################################################

sort file # 排序文件 sort -r file # 反向排序(降序) sort -n file # 使用数字而不是字符串进行比较 sort -t: -k 3n /etc/passwd # 按 passwd 文件的第三列进行排序 sort -u file # 去重排序

##############################################################################

##############################################################################

source /path/to/z.sh # .bashrc 中初始化 z.sh z # 列出所有历史路径以及他们的权重 z foo # 跳到历史路径中匹配 foo 的权重最大的目录 z foo bar # 跳到历史路径中匹配 foo 和 bar 权重最大的目录 z -l foo # 列出所有历史路径中匹配 foo 的目录及权重 z -r foo # 按照最高访问次数优先进行匹配跳转 z -t foo # 按照最近访问优先进行匹配跳转

##############################################################################

##############################################################################

bind '"\eh":"\C-b"' # 绑定 ALT+h 为光标左移,同 CTRL+b / <Left> bind '"\el":"\C-f"' # 绑定 ALT+l 为光标右移,同 CTRL+f / <Right> bind '"\ej":"\C-n"' # 绑定 ALT+j 为下条历史,同 CTRL+n / <Down> bind '"\ek":"\C-p"' # 绑定 ALT+k 为上条历史,同 CTRL+p / <Up> bind '"\eH":"\eb"' # 绑定 ALT+H 为光标左移一个单词,同 ALT-b bind '"\eL":"\ef"' # 绑定 ALT+L 为光标右移一个单词,同 ALT-f bind '"\eJ":"\C-a"' # 绑定 ALT+J 为移动到行首,同 CTRL+a / <Home> bind '"\eK":"\C-e"' # 绑定 ALT+K 为移动到行末,同 CTRL+e / <End> bind '"\e;":"ls -l\n"' # 绑定 ALT+; 为执行 ls -l 命令

##############################################################################

##############################################################################

ip a # 显示所有网络地址,同 ip address ip a show eth1 # 显示网卡 IP 地址 ip a add 172.16.1.23/24 dev eth1 # 添加网卡 IP 地址 ip a del 172.16.1.23/24 dev eth1 # 删除网卡 IP 地址 ip link show dev eth0 # 显示网卡设备属性 ip link set eth1 up # 激活网卡 ip link set eth1 down # 关闭网卡 ip link set eth1 address {mac} # 修改 MAC 地址 ip neighbour # 查看 ARP 缓存 ip route # 查看路由表 ip route add 10.1.0.0/24 via 10.0.0.253 dev eth0 # 添加静态路由 ip route del 10.1.0.0/24 # 删除静态路由

ifconfig # 显示所有网卡和接口信息 ifconfig -a # 显示所有网卡(包括开机没启动的)信息 ifconfig eth0 # 指定设备显示信息 ifconfig eth0 up # 激活网卡 ifconfig eth0 down # 关闭网卡 ifconfig eth0 192.168.120.56 # 给网卡配置 IP 地址 ifconfig eth0 10.0.0.8 netmask 255.255.255.0 up # 配置 IP 并启动 ifconfig eth0 hw ether 00:aa:bb:cc:dd:ee # 修改 MAC 地址

nmap 10.0.0.12 # 扫描主机 1-1000 端口 nmap -p 1024-65535 10.0.0.12 # 扫描给定端口 nmap 10.0.0.0/24 # 给定网段扫描局域网内所有主机 nmap -O -sV 10.0.0.12 # 探测主机服务和操作系统版本

##############################################################################

##############################################################################

man hier # 查看文件系统的结构和含义 man test # 查看 posix sh 的条件判断帮助 man ascii # 显示 ascii 表 getconf LONGBIT # 查看系统是 32 位还是 64 位 bind -P # 列出所有 bash 的快捷键 mount | column -t # 漂亮的列出当前加载的文件系统 curl ip.cn # 取得外网 ip 地址和服务商信息 disown -a && exit # 关闭所有后台任务并退出 cat /etc/issue # 查看 Linux 发行版信息 lsof -i port:80 # 哪个程序在使用 80 端口? showkey -a # 取得按键的 ASCII 码 svn diff | view - # 使用 Vim 来显示带色彩的 diff 输出 mv filename.{old,new} # 快速文件改名 time read # 使用 CTRL-D 停止,最简单的计时功能 cp file.txt{,.bak} # 快速备份文件 sudo touch /forcefsck # 强制在下次重启时扫描磁盘 find ~ -mmin 60 -type f # 查找 $HOME 目录中,60 分钟内修改过的文件 curl wttr.in/~beijing # 查看北京的天气预报 echo ${SSHCLIENT%% *} # 取得你是从什么 IP 链接到当前主机上的 echo $[RANDOM%X+1] # 取得 1 到 X 之间的随机数 bind -x '"\C-l":ls -l' # 设置 CTRL+l 为执行 ls -l 命令 find / -type f -size +5M # 查找大于 5M 的文件 chmod –reference f1 f2 # 将 f2 的权限设置成 f1 一模一样的 curl -L cheat.sh # 速查表大全

##############################################################################

##############################################################################

history | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head

netstat -n | awk '^tcp {++tt[$NF]} END {for (a in tt) print a, tt[a]}'

sshfs name@server:/path/to/folder /path/to/mount/point

ps aux | sort -nk +4 | tail

while sleep 1;do tput sc;tput cup 0 \(((\)(tput cols)-29));date;tput rc;done&

wget -qO - "http://www.tarball.com/tarball.gz" | tar zxvf -

python -c "import test.pystone;print(test.pystone.pystones())"

dd if=/dev/zero of=/dev/null bs=1M count=32768

mount /path/to/file.iso /mnt/cdrom -oloop

ssh -t hostA ssh hostB

wget -r -l1 –no-parent -nH -nd -P/tmp -A".gif,.jpg" http://example.com/images

mkdir -p work/{project1,project2}/{src,bin,bak}

find . -type f -newermt "2010-01-01" ! -newermt "2010-06-01"

lsof -P -i -n | cut -f 1 -d " "| uniq | tail -n +2

:w !sudo tee > /dev/null %

source ~/github/profiles/mybashinit.sh

ssh -CqTnN -R 0.0.0.0:8443:192.168.1.2:443 user@202.115.8.1

ssh -CqTnN -L 0.0.0.0:8443:192.168.1.2:443 user@192.168.1.3

ssh -CqTnN -D localhost:1080 user@202.115.8.1

http://www.skywind.me/blog/archives/2021

##############################################################################

##############################################################################

function q-extract() { if [ -f $1 ] ; then case $1 in *.tar.bz2) tar -xvjf $1 ;; *.tar.gz) tar -xvzf $1 ;; *.tar.xz) tar -xvJf $1 ;; *.bz2) bunzip2 $1 ;; *.rar) rar x $1 ;; *.gz) gunzip $1 ;; *.tar) tar -xvf $1 ;; *.tbz2) tar -xvjf $1 ;; *.tgz) tar -xvzf $1 ;; *.zip) unzip $1 ;; *.Z) uncompress $1 ;; *.7z) 7z x $1 ;; *) echo "don't know how to extract '$1'…" ;; esac else echo "'$1' is not a valid file!" fi }

function q-compress() { if [ -n "$1" ] ; then FILE=$1 case $FILE in .tar) shift && tar -cf $FILE $ ;; .tar.bz2) shift && tar -cjf $FILE $ ;; .tar.xz) shift && tar -cJf $FILE $ ;; .tar.gz) shift && tar -czf $FILE $ ;; .tgz) shift && tar -czf $FILE $ ;; .zip) shift && zip $FILE $ ;; .rar) shift && rar $FILE $ ;; esac else echo "usage: q-compress <foo.tar.gz> ./foo ./bar" fi }

function ccat() { local style="monokai" if [ $# -eq 0 ]; then pygmentize -P style=$style -P tabsize=4 -f terminal256 -g else for NAME in $@; do pygmentize -P style=$style -P tabsize=4 -f terminal256 -g "$NAME" done fi }

##############################################################################

##############################################################################

export LESSTERMCAPmb=\('\E[1m\E[32m' export LESS_TERMCAP_mh=\)'\E[2m' export LESSTERMCAPmr=\('\E[7m' export LESS_TERMCAP_md=\)'\E[1m\E[36m' export LESSTERMCAPZW="" export LESSTERMCAPus=\('\E[4m\E[1m\E[37m' export LESS_TERMCAP_me=\)'\E(B\E[m' export LESSTERMCAPue=\('\E[24m\E(B\E[m' export LESS_TERMCAP_ZO="" export LESS_TERMCAP_ZN="" export LESS_TERMCAP_se=\)'\E[27m\E(B\E[m' export LESSTERMCAPZV="" export LESSTERMCAPso=$'\E[1m\E[33m\E[44m'

"\eh": backward-char "\el": forward-char "\ej": next-history "\ek": previous-history "\eH": backward-word "\eL": forward-word "\eJ": beginning-of-line "\eK": end-of-line

##############################################################################

##############################################################################

https://github.com/Idnan/bash-guide http://www.linuxstall.com/linux-command-line-tips-that-every-linux-user-should-know/ https://ss64.com/bash/syntax-keyboard.html http://wiki.bash-hackers.org/commands/classictest https://www.ibm.com/developerworks/library/l-bash-test/index.html https://www.cyberciti.biz/faq/bash-loop-over-file/ https://linuxconfig.org/bash-scripting-tutorial https://github.com/LeCoupa/awesome-cheatsheets/blob/master/languages/bash.sh https://devhints.io/bash https://github.com/jlevy/the-art-of-command-line https://yq.aliyun.com/articles/68541

9.2 tools

9.2.1 git

##############################################################################

##############################################################################

##############################################################################

############################################################################## git config –global "Your Name" git config –global "Email Address" git config –global credential.helper store 保存密码(每次要输密码/重复输密码)

##############################################################################

############################################################################## git init

##############################################################################

############################################################################## git add <file> git add -u 提交work directory中所有已track的文件至staging area git commit -m "descriptions" git commit –amend 对最近一次的提交做内容修改 git commit –amend –author "username <useremail>" 修改最近提交用户名和邮箱

##############################################################################

############################################################################## git status git status -s 文件状态缩略信息, 常见 A:新增; M:文件变更; ?:未track; D:删除 git diff <file> git diff HEAD – <file> 查看工作区和版本库里面最新版本的区别 git diff –check <file> 检查是否有空白错误(regex:' \{1,\}$') git diff –cached <file> 查看已add的内容(绿M) git diff branch1 branch2 –stat 查看两个分支差异 git diff branch1 branch2 <file…> 查看分支文件具体差异

##############################################################################

############################################################################## git log git reflog git log -n 最近n条的提交历史 git log <branchname> -n 分支branchname最近n条的提交历史 git log –stat 历次commit的文件变化 git log –shortstat 对比–stat只显示最后的总文件和行数变化统计(n file changed, n insertions(+), n deletion(-)) git log –name-status 显示新增、修改、删除的文件清单 git log lhshash..rhshash 对比两次commit的变化(增删的主语为lhs, 如git log HEAD~2..HEAD == git log HEAD -3) git log -p 历次commit的内容增删 git log -p -W 历次commit的内容增删, 同时显示变更内容的上下文 git log origin/EI-1024 -1 –stat -p -W 查看远端分支EI-1024前一次修改的详细内容 git log origin/master..dev –stat -p -W 查看本地dev分支比远端master分支变化(修改)的详细内容

git log <branchname> –oneline 对提交历史单行排列 git log <branchname> –graph 对提交历史图形化排列 git log <branchname> –decorate 对提交历史关联相关引用, 如tag, 本地远程分支等 git log <branchname> –oneline –graph –decorate 拼接一下, 树形化显示历史 git log –graph –pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen%ai(%cr) %C(bold blue)<%an>%Creset' –abbrev-commit 同上, 建议alais保存

git log –pretty=format 常用的选项(摘自progitv2.1.9) %H 提交对象(commit)的完整哈希字串 %h 提交对象的简短哈希字串 %T 树对象(tree)的完整哈希字串 %t 树对象的简短哈希字串 %P 父对象(parent)的完整哈希字串 %p 父对象的简短哈希字串 %an 作者(author)的名字 %ae 作者的电子邮件地址 %ad 作者修订日期(可以用 –date= 选项定制格式) %ar 作者修订日期,按多久以前的方式显示 %cn 提交者(committer)的名字 %ce 提交者的电子邮件地址 %cd 提交日期 %cr 提交日期,按多久以前的方式显示 %s 提交说明

git log –since –after 显示时间之后的提交 git log –until –before 显示时间之前的提交 git –author 显示指定作者的提交 git –committer 显示指定committer的提交(注:committer不一定是author) git log -S [keyword] 仅显示添加或移除了某个关键字的提交(某些场景比单独git log -p | grep [keyword] 好用很多) git log origin/b3.3/master –author=yx-ren –since="2019-10-01" –before="2019-11-01" 查看某作者在某发布版本最近一个月的提交, 常见于线上背锅 git log origin/b3.0/master –author=someleave –since="1 month ago" 查看某刚离职同事过去一个月的提交, 常见于背锅 git log –since=1.weeks 过去一周的提交(写周报的时候可以看看我这一周干了啥) git log –since=1.days 过去一天的提交(下班的时候可以看看我这一天干了啥) git log –since="1 weeks 2 days 3 hours 40 minutes 50 seconds ago" 过去1周2天3小时40分50秒之内的提交

##############################################################################

############################################################################## git reset –hard HEAD^ 回退到上1版本 git reset –hard HEAD~5 回退到上5个版本 git reset –hard id 回退到指定版本

##############################################################################

############################################################################## git checkout – <file> 撤销修改:误修改工作区文件,未git add/commit git restore <file> 撤销修改:误修改工作区文件,未git add/commit git reset HEAD <file> 撤销git add:误将文件加入暂存区(git add),未git commit git reset –hard HEAD^ 撤销git commit:误将文件提交(一旦提交,只能通过版本回退进行撤销)

##############################################################################

############################################################################## git rm/add <file> git commit -m "remove <file>" 删除版本库中的<file>:删除工作区文件后,继续删除版本库中相应的文件 git checkout – <file> 根据版本库中的<file>恢复工作区<file>

##############################################################################

############################################################################## git clean -i #交互式清理, 不常用 git clean -n #查看清理文件列表(不包括文件夹), 不执行实际清理动作 git clean -n -d #查看清理文件列表(包括文件夹), 不执行实际清理动作 git clean -f #清理所有未track文件 git clean -df #清理所有未track文件和文件夹, 常用, 但使用前确保新增加的文件或文件夹已add, 否则新创建的文件或者文件夹也会被强制删除

##############################################################################

############################################################################## git remote add origin <remote address> 在本地工作区目录下按照 GitHub 提示进行关联 git remote rm origin 解除错误关联 git push -u origin master 第一次将本地仓库推送至远程仓库(每次在本地提交后进行操作) git push origin master 以后每次将本地仓库推送至远程仓库(每次在本地提交后进行操作) <remote address>: git@github.com:<username>/<repository>.git https://github.com/<username>/<repository>.git

##############################################################################

############################################################################## git clone <remote address> git协议速度更快但通常公司内网不允许,https协议速度慢

##############################################################################

############################################################################## git branch <branch name> 创建<branch name>分支 git checkout <branch name> 切换至<branch name>分支 git switch <branch name> 切换至<branch name>分支 git checkout -b <branch name> 创建并切换至<branch name>分支 git switch -c <branch name> 创建并切换至<branch name>分支 git branch 查看已有分支(* 表示当前分支) git merge <branch name> 合并<branch name>到当前分支(通常在master分支下操作) git branch -d <branch name> 删除分支 git branch -m oldbranchname newname 删除分支

##############################################################################

############################################################################## 合并时报错“分支发生冲突”,首先vim相应文件,修改冲突位置,然后按照git add/commit重新提交,最后删除多余分支即可。 git log –graph –pretty=oneline –abbrev-commit git log –graph

##############################################################################

############################################################################## git merge –no-ff -m "descriptions" <branch name>

##############################################################################

############################################################################## master分支 发布稳定版本 dev分支 发布开发版本 <developer name>分支 个人开发分支(个人开发完成将该分支并入dev,同时保留该分支,继续开发)

##############################################################################

############################################################################## 软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。 git stash 保存当前工作现场(在dev未完成开发,但master有bug需要修复) git stash pop 回到dev分支后恢复工作现场(list中的现场会同时被删除) git stash list 查看当前存储的工作现场 git stash apply stash@{#} 回到指定工作现场(list中的现场不会被删除,需要用git stash drop) git stash drop stash@{#} 删除指定工作现场 git cherry-pick <id> 在master修复好bug后,在dev复制一遍bug修复流程

##############################################################################

############################################################################## 软件开发中,总有无穷无尽的新的功能要不断添加进来。添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。 git branch -D <branch name> 强制删除分支(丢弃未合并分支)

##############################################################################

############################################################################## User 1: git remote [-v] 查看远程库信息(-v 查看详细信息) git remote update origin –prune 更新分支列表(更新远程分支列表) git remote update origin -p 更新分支列表(更新远程分支列表) git push origin [master/dev/…] 推送指定分支到远程 User 2: git clone <remote address> 克隆到本地(只能克隆master) git checkout -b dev origin/dev 本地新建分支并关联远程 git add/commit/push 添加、提交、推送更新 User 1: git add/commit/push 推送时报错(与user 2推送的更新冲突) git pull <remote> <branch> git branch –set-upstream-to=origin/<branch> <branch> 本地与远程关联 git pull 拉取远程文件(并解决冲突) git commit/push 重新提交并推送

##############################################################################

############################################################################## git tag 查看标签 git show <tag name> 查看指定标签 git log –pretty=oneline –abbrev-commit –decorate=full 在log中显示标签 git tag <tag name> 为上次commit位置打标签 git tag <tag name> <commit id> 为指定commit位置打标签 git tag -a <tag name> -m "descriptions" <commit id> 为指定commit打标并添加描述 git tag -d <tag name> 删除本地标签 git push origin <tag name> 推送指定标签到远程 git push origin –tags 推送所有本地标签到远程 git push origin :refs/tags/<tag name> 删除远程标签(先删除本地标签)

##############################################################################

##############################################################################

git rebase master 常见于多人开发, 每个开发人员从master checkout出自己的分支, 开发一段时间后提交至master之前最好rebase一下, 防止冲突, 就算真有冲突在本地解决好过强制提交, 开发流程中尽量保证master的干净整洁

举个例子: master分支上有三个提交C1, C2, C3 某一时刻usr1在C3的master分支上checkout出新的分支, 用于开发服务端支持ipv6新特性, 并提交了C4, C5 git checkout -b ipv6support …… git commit -m C4 …… git commit -m C5 此时提交状态如下所示 (origin/master branch)

C1 <- C2 <- C3 \ \ \ C4 <- C5

(ipv6support branch)

某同事usr2修改了master上的内存泄漏错误, 并提交了C6, C7, C8三个commit, 然后直接推送origin/master(假设这个期间无其他人推新内容到master) 此时提交状态如下所示 (origin/usr2/fixmemleak branch)

C1 <- C2 <- C3 <- C6 <- C7 <- C8 \ | \ (origin/master branch) \ C4 <- C5

(ipv6support branch)

如果此时usr1希望将ipv6的新特性提交至master, 那么在其直接push origin master时会提示master需要合并分支ipv6support 虽然C4, C5的改动内容完全独立于C6, C7, C8的改动 但git仍会抓取C5和C8的提交并产生一个新的C9 commit(因两者分支的base不同), 如下图所示 C1 <- C2 <- C3 <- C6 <- C7 <- C8 \ \ \ \ \ \ C4 <- C5 <-–— C9

如果是为了保证master提交记录的"干净完整" 或者是某分支不着急提交, 仍需要更多的测试与开发, 但又不想分支开发周期结束后"偏离"当初checkout的master分支太久远(容易造成更多的冲突) 可以考虑(定期)利用rebase来进行变基 即上面提到过的多人协同开发, 定期rebase master是个好习惯 git checkout ipv6support git rebase master 结果提交状态如下所示 (origin/master origin/usr2/fixmemleak branch)

C1 <- C2 <- C3 <- C6 <- C7 <- C8 \ \ \ C4' <- C5'

(ipv6support branch) 这种rebase在功能上类似将某分支所有的改动做成多个patch并依次打在指定的新base上 此时再提交master就不会产生抓取效果, 会将C4'和C5'直接提交至master, 即can be fast-forwarded, 同时也保证了master提交记录的整洁性 (注: 虽然C4'和C5'的内容和C4, C5完全一致, 但两者base不同, commit hash code也完全不同)

git rebase –noto <branchlhs> <branchrhs> #重放, 用于变基在分支branchlhs中而不在branchrhs中的commit #某项目状态分支如下所示, 其中Cn的数字代表提交时间顺

(master branch)

C1 <- C2 <- C5 <- C6 \ \ \ C3 <- C4 <- C10 \ | \ (server branch) \ C8 <- C9

(client branch)

git rebase –noto client server

(master branch)(client branch)

 

C1 <- C2 <- C5 <- C6 <- C8' <- C9' \ \ \ C3 <- C4 <- C10 \ | \ (server branch) \ [#####disable######] [ C8 <- C9 ] [ | ] [ (client branch) ]

#can be fast-forwarded git checkout master git merge client

(client branch)

C1 <- C2 <- C5 <- C6 <- C3' <- C8' <- C9' \ | \ (master branch) \ C3 <- C4 <- C10

(server branch)

git rebase -i HEAD~n 压缩当前分支的n个commit并合并为1个commit, 常见第一行为pick, 剩下的n-1行为squash

git rebase –abort # rebase过程中发生错误, 可以利用该命令终止整个rebase过程 git rebase –continue # rebase过程中发生冲突, 在解决冲突后可以利用该命令进行后续过程

##############################################################################

##############################################################################

git <branch> log -n -p > diff.patch # 生成某分支过去n个commit的文件diff信息至单个diff文件 git diff <–cached> diff.patch # 针对当前缓存区的内容生成diff文件

git apply –check diff.patch #检查是否可以正常应用, 无回显证明无冲突 git apply –stat diff.patch #查看应用diff文件后的文件变化 git apply diff.patch #打patch, 仅仅改变文件信息, 无commit信息, 仍然需要add, commit

git format-patch <branch> -n   #生成分支<branch>最近的n次commit的patch git format-patch <r1>..<r2> #生成两个commit间的修改的patch(包含两个commit. <r1>和<r2>都是具体的commit号) git format-patch -1 <r1> #生成单个commit的patch git format-patch <r1> #生成某commit以来的修改patch(不包含该commit) git format-patch –root <r1>  #生成从根到r1提交的所有patch

git apply –check 0001-update-bash.sh.patch #检查patch是否冲突可用 git apply –stat 0001-update-bash.sh.patch #检查patch文件变更情况, 无回显证明无冲突 git am 0001-update-bash.sh.patch #将该patch打上到当前分支, 带commit信息 git am ./*.patch #将当前路径下的所有patch按照先后顺序打上 git am –abort #终止整个打patch的过程, 类似rebase –abort git am –resolved #解决冲突后, 可以执行该命令进行后续的patch, 类似rebase –continue

##############################################################################

##############################################################################

##############################################################################

git bundle create awesome-cheatsheets.bundle HEAD master #打包重建master分支的所有数据 git clone awesome-cheatsheets.bundle # 重建工程

git bundle create awesome-cheatsheets.bundle HEAD~10 git bundle create awesome-cheatsheets.bundle HEAD~10..HEAD git bundle create awesome-cheatsheets.bundle lhscommitmd5..rhscommitmd5 git bundle create awesome-cheatsheets.bundle origin/master..master git bundle create awesome-cheatsheets.bundle master ^origin/master

##############################################################################

############################################################################## fork –> clone –> add/commit/push –> pull request

##############################################################################

############################################################################## git config –global color.ui true 显示颜色

##############################################################################

############################################################################## <dir name> 忽略文件夹 *.zip 忽略.zip文件 /<dir name>/<file name> 忽略指定文件

##############################################################################

############################################################################## git add -f <file> 强制添加 git check-ignore -v <file> 查看生效规则

##############################################################################

############################################################################## git config [–global] alias.<alias> '<original command>' 为所有工作区/当前工作区配置别名 .git/config 当前工作区的配置文件 ~/.gitconfig 当前用户的配置文件

##############################################################################

############################################################################## https://www.liaoxuefeng.com/wiki/896043488029600 https://git-scm.com/book/en/v2

##############################################################################

############################################################################## git submodule foreach git pull 子模块更新

10 备份

// Type your code here, or load an example.
/*
#include <memory>
using namespace std;
class B {
    private:
    int pri_mem;
    protected:
    int prot_mem;
};

class S : private B {
    friend void c(S&);
    friend void c(B&);
    int j;

    int f1() const {return prot_mem;} //不报错
};

class G : public S {
    int g() {return prot_mem;}   //报错
    int g1() {return pri_mem;}  //报错,
};

void c(S &s) {
    s.j = s.prot_mem = 0;
}
*/

/*
int foo() {
    int bar;
    return bar;
}
*/

/*
class B {
  public:
  virtual void fk() {
    i = 'B';
  }
  char i = 'A';
};

class A : public B {
  public:
  virtual void fk() {
    static_cast<B>(*this).fk();
  }
};
*/

// p.39
/**
class Foo {
public:
int val;
Foo *pnext;
};

void foo_bar() {
    Foo *bar = new Foo;
    if (bar->val || bar->pnext) {}
}
*/

/*p.41
class Foo {
    public:
};
class Bar {
    public:
    Foo foo;
    char *str;
};

void foo_bar() {
    Bar bar;
}
*/

/*
class str {
    public:
    str(const str&);
    private:
};

class Word {
    public:
    Word( const str&);
    private:
    int cnt;
    str a;
};

void foo_bar(const Word &wd) {
    Word w = wd;
}
*/

/*
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
int main()
{
    int a = sizeof(X);
    a = sizeof(Y);
    a = sizeof(Z);
    a = sizeof(A);
}
*/
// p. 110
/*
struct no_virts {
    int d1, d2;
};
class has_virts : public no_virts{
public:
    virtual void foo();
    int d3;
};

int main() {
    has_virts h;
    no_virts n;
    int res = ((void*)&n == (void*)&n.d1);
    res = ((void*)&h == (void*)&h.d1);
}
*/

/*
class A {
    public:
virtual ~A() = 0;
};

class B : public A {

};

void bar() {
    B b;
}
*/

class A{
    int x = 7;
};
void foo(A &&a) {
    A &&b = A(); //b是左值,意思是虽然b引用的对象没有名字,
                 //是个右值,但是b是有名字的,所以他是个
                 //左值
                 //同样的道理,a也是个左值
    A c;
    b = c;
   //foo(b);
}
int main() {
   foo(A()); 
}

am redis 2 unp 1 pm c++ 1 linux 2

Author: BINBIN YANG

Created: 2021-04-04 日 15:46

Validate