0%

2020年5月16日 下午10:47

总结:

  1. 这个过程最大的收获是:设计函数接口时,灵活的使用const,&引用。
    1. 这里面的技巧是:自己要理解这个函数的”语义“,这个”语义“里其实就回答了:这部分与语言无关,用人类正常的生活思维反而更加容易理解
      1. 是否需要构造新的临时变量
        1. 返回参数是否使用&
      2. 是否会对传入的参数值进行改变
        1. 涉及到对参数的const
        2. 对成员函数的const
        3. 对临时对象的const 引用
    2. operator= ,operator+=,operator++() 这三个都是可以直接对传入的进行操作,所以没必要这三个重载有两个要求:
      1. 函数不能声明为const
      2. 返回值要声明为&
  2. 我纠结过的一个点:Complex operator+(const Complex &c) const;
    1. 这个+的重载,从语义的角度来说,的确需要返回一个临时变量
    2. 但是,我在想能不能尝试使用Complex& 作为返回值?
    3. 答案是不行:
      1. 首先,从语义上分析是必须有一个临时变量的!所以这就从理论上保证了其实是不可能做到没临时变量的。
      2. 其次,让我们看一个int &a = 1,这句话会导致error: non-const lvalue reference to type ‘double’ cannot bind to a value of unrelated type ‘int’
        1. 在编译器中就报错原因: That is because a temporary can not bind to a non-const reference.
        2. 可以更改为const int &a = 1,就可以正常使用了
      3. 同样的道理Complex& Complex::operator+ (const Complex& c) const这样的写法等同于int &a = 1
      4. 需要我们改为const Complex& Complex::operator+ (const Complex& c) const
      5. 现在有两个关键现象:
        1. 使用了引用作为返回值,依然不较少临时变量的生成!
        2. Complex c = a + b;这个语句中a+b这个步骤,产生的临时变量在执行=重载函数之前就被析构了!
        3. 在下面的程序中可以看出来
          1
          2
          3
          4
          Complex::Complex(double r, double i)//执行+
          Complex::~Complex()//执行+
          Complex::Complex(const Complex& c)//执行=
          real value is 6.95313e-310 image value is 2.16112e-314

封装类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#pragma once

#include <iostream>
using namespace std;

class Complex
{
public:
Complex(); // 默认构造函数
Complex(double r, double i); // 构造函数
virtual ~Complex(); // 析构函数
Complex(const Complex& x); // 拷贝构造
Complex& operator=(const Complex &c); // =号运算符


double GetReal( ) const { return _real; }
void SetReal(double d) { _real = d; }
double GetImage() const { return _image; }
void SetImage(double i) { _image = i; }

// 运算符重载
Complex operator+(const Complex &c) const;
Complex& operator+=(const Complex &c);
Complex operator-(const Complex &c) const;
Complex& operator-=(const Complex &c);
Complex operator*(const Complex &c) const;
Complex& operator*=(const Complex &c);
Complex operator/(const Complex &c) const;
Complex& operator/=(const Complex &c);

bool operator==(const Complex &c) const;
bool operator!=(const Complex &c) const;
bool operator>(const Complex &c) const;
bool operator>=(const Complex &c) const;
bool operator<(const Complex &c) const;
bool operator<=(const Complex &c) const;


// 前置和后置++
Complex& operator++(); //前置++
Complex operator++(int); //后置++
Complex& operator--(); //前置--
Complex operator--(int); //后置--

//protected:

friend ostream& operator<<(ostream& os, const Complex &x);
friend istream& operator>>(istream& is, Complex &x);

private:
double _real; // 复数的实部
double _image; // 复数的虚部
};

附录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#include "stdafx.h"

Complex::Complex()
{
_real = 0.0;
_image = 0.0;
cout << "Complex::Complex()" << endl;
}

Complex::Complex(double r, double i)
{
_real = r;
_image = i;
cout << "Complex::Complex(double r, double i)" << endl;
}

Complex::Complex(const Complex& c)
{
_real = c._real;
_image = c._image;
cout << "Complex::Complex(const Complex& c)" << endl;
}

Complex& Complex::operator= (const Complex& c)
{
if (this != &c)
{
_real = c._real;
_image = c._image;
}
return *this;
}

Complex::~Complex()
{
_real = _image = 0.0;
cout << "Complex::~Complex()" << endl;
}

Complex Complex::operator+ (const Complex& c) const
{
//Complex tmp;
//tmp._real = _real + x._real;
//tmp._image = _image + x._image;
//return tmp;

return Complex(_real + c._real, _image + c._image);
}


Complex& Complex::operator+= (const Complex& c)
{
_real += c._real;
_image += c._image;

return *this;
}

Complex Complex::operator-(const Complex &c) const
{
return Complex(_real - c._real, _image - c._image);
}

Complex& Complex::operator-=(const Complex &c)
{
_real -= c._real;
_image -= c._image;

return *this;
}

Complex Complex::operator*(const Complex &c) const
{
return Complex(_real*c._real - _image*c._image, _real*c._image + _image*c._real);
}

Complex& Complex::operator*=(const Complex &c)
{
Complex tmp(*this); //拷贝构造函数
_real = tmp._real*c._real - _image*c._image;
_image = tmp._real*c._image + tmp._image*c._real;
return *this;
}

Complex Complex::operator/(const Complex &c) const
{
double t = c._real*c._real + c._image*c._image;
return Complex((_real*c._real - _image*(-c._image)) / t, (_real*(-c._image) + _image*c._real) / t);
}

Complex& Complex::operator/=(const Complex &c)
{
Complex tmp(*this); //拷贝构造函数
double t = c._real*c._real + c._image*c._image;
_real = (tmp._real*c._real - tmp._image*(-c._image)) / t;
_image = (tmp._real*(-c._image) + tmp._image*c._real) / t;
return *this;
}

bool Complex::operator==(const Complex& c) const
{
return (_real == c._real) && (_image == c._image);
}

bool Complex::operator!=(const Complex& c) const
{
return !( (_real == c._real) && (_image == c._image) );
}

bool Complex::operator>(const Complex &c) const
{
return (_real > c._real) && (_image > c._image);
}

bool Complex::operator>=(const Complex &c) const
{
return (_real >= c._real) && (_image >= c._image);
}

bool Complex::operator<(const Complex &c) const
{
return (_real < c._real) && (_image < c._image);
}

bool Complex::operator<=(const Complex &c) const
{
return (_real <= c._real) && (_image <= c._image);
}


Complex& Complex::operator++ () // 前置++
{
_real++;
_image++;
return *this;
}

Complex Complex::operator++ (int) // 后置++
{
//Complex tmp(*this);
//_real++;
//_image++;
//return tmp;
return Complex(_real++, _image++);
}

Complex& Complex::operator--() //前置--
{
_real--;
_image--;
return *this;
}

Complex Complex::operator--(int) //后置--
{
return Complex(_real--, _image--);
}

ostream& operator<<(ostream& os, const Complex &x)
{
os << "real value is " << x._real << " image value is " << x._image;
return os;
}

istream& operator >> (istream& is, Complex &x)
{
is >> x._real >> x._image;
return is;
}

2020年5月16日 下午6:01
观察者模式实现的几点收获
单例模式
[摘抄]C++ typename的起源与用法
简单的C++线程:锁粒度、线程的交换、线程的移动
里有用的函数
函数 -> 函数模板 && struct仿函数 -> struct仿函数模板
自定义类型的封装String:移动构造函数 + 移动赋值运算符
自己动手封装类型
前七章习题

2020年5月15日 下午3:39
C++虚函数表原理浅析

1
2
Base *b = new Derive();
b->f();
  1. 核心设计:每个类使用一个虚函数表,每个类对象用一个虚表指针
  2. 具体举例:由b所指的内存中的虚函数表的f()的位置已经被Derive: :f()函数地址所取代,于是在实际调用发生时,是Derive: :f()被调用了,这就实现了C++的动态多态

2020年5月12日 下午10:15

单点登录的进化

本质是一个验证问题

  1. Client - 单服务器
    1. 方案一:每次都用username password到数据库中进行验证
    2. 方案二:使用SessionID作为验证的已登录用户的方法,client和server端有共同的ID,client保存在cookie中,server保存在tomcat的缓存中,一旦tomcat重启,那么缓存就消失了,我们必须做持久化到硬盘才可以
    3. 方案三:使用redis作为这个持久化的地方,解决重启需要重新验证的问题
  2. Client - 多服务器
    1. 方案一:跟上面一致
    2. 方案二:那么多台服务器之间无法共享登录状态,其实就是无法共享tomcat私有的sessionID
    3. 方案三:不仅解决了单服务器重启的问题,也做到了SessionID多服务器之间的共享
  3. Client - 多服务器 — redis集群
    1. 什么场景下缓存一致性:
      1. 主要考虑的是redis集群扩展的情况下,如何保证仍然有较高的缓存命中率的问题。也就是说本质问题是:缓存命中率的问题
    2. 一致性hash有哪些问题,如何解决:
      1. hash倾斜性:由于hash的倾斜,会导致hash结果分布不均匀
      2. 什么是hash倾斜性?
        1. 首先我们一致性哈希中有两个过程:
          1. 第一步:正常的hash操作,得到一个hash结果
          2. 第二步:数据的hash向redis的hash在环上进行靠拢
        2. 如果数据的hash和redis的hash结果在环上分布“不搭”,eg:数据hash密集的地方,redis的hash不密集。靠拢之后的结果就会倾斜
      3. 解决方法:
        1. 我们有一个直观的感受:如果redis越多,那么在环上的分布就越有可能均匀。
        2. 那么,我们就假设出更多的虚拟redis节点。然后再将虚拟节点二次hash到实在的redis节点中,注意这次的hash是直接hash到实在的redis节点,而没有了“靠拢”这一步骤。
        3. 二次hash就是最最普通的hash,反而不容器造成hash的倾斜性

什么场景下需要分布式锁

  1. 本质问题:对有限资源(仅一个)的争抢下,如何确定第一个获得者
    1. 这其中需要设计两个:
      1. 争抢时:需要设计一个游戏规则,争抢的人都遵守这个游戏规则
      2. 争抢完之后:需要设计一个条件,能区分出第一名和其他人。
    2. 一个好的锁条件 + 游戏规则 设计需要保证:有且只有一个被筛选出来
      1. 必须有,不能一个都没有
      2. 有,但只能有一个
  2. 我们虽然很难设计出这样一个锁条件,但是我们反过来验证一下是否满足有且只有一个:
    1. 我们可以想象一个图
    2. 我觉得这个问题本质和编程无关,更像是一道中学数学逻辑题:
      1. 问:你是否能够设计一个游戏规则,保证在资源争抢的时候,有且只有一个人获得资源
      2. 也就是说,所有参与游戏的人都遵循一套规则,并没有暗箱操作,就可以保证唯一性,这个游戏规则在并发的时候,会出现相互的制约,从而保证的结果的正确,也只有并发的时候才会体现出来
    3. 分析这种游戏规则的方法:就如图中所示的一样,在一条时间线上,我们假设一个人已经获得了锁,此时我们枚举出所有的可能情况,看是否能找到第二人再次获得锁,如果我们再所有的情况中都无法获得锁,那么说明这个规则有效。

架构思维:对问题的本质进行认识,做到举一反三

涉及到的java语法知识:

  1. 什么场景下需要static
    1. 场景一:在服务启动时,就默认初始化好redis的连接池
    2. 场景二:在使用的时候,不用声明对象,直接通过类来使用
  2. 什么场景下需要,多态,强制类型转换
  3. 什么场景下需要泛型
    1. 场景一:在对象序列化的时候,序列化函数的接口就需要满足各种类型进行序列化的要求,甚至在嵌套类型list中使用<?>,当代码中不使用这个类型的时候,可以指定为?,对于多类型的话,可以使用变长参数。

2020年5月12日 下午7:44

设备控制器:这是个硬件

  1. 职责:
    1. 字面意思,告诉你如何控制设备
  2. 通信方式:
    1. CPU 如何同控制器的寄存器和数据缓冲区进行通信呢?
      1. 每个控制寄存器被分配一个 I_O 端口,我们可以通过特殊的汇编指令(例如 in_out 类似的指令)操作这些寄存器。
      2. 数据缓冲区,可内存映射 I/O,可以分配一段内存空间给它,就像读写内存一样读写数据缓冲区。如果你去看内存空间的话,有一个原来我们没有讲过的区域 ioremap,就是做这个的。
    2. 控制器的寄存器一般会有状态标志位,可以通过检测状态标志位,来确定输入或者输出操作是否完成。
      1. 第一种方式就是轮询等待,就是一直查,一直查,直到完成。
      2. 当然这种方式很不好,于是我们有了第二种方式,就是可以通过中断的方式,通知操作系统输入输出操作已经完成。
  3. 输入输出设备我们大致可以分为两类:块设备(Block Device)和字符设备(Character Device)。
    1. 块设备将信息存储在固定大小的块中,每个块都有自己的地址。硬盘就是常见的块设备。
    2. 字符设备发送或接收的是字节流。而不用考虑任何块结构,没有办法寻址。鼠标就是常见的字符设备。

中断控制器:硬件

  1. 职责:
    1. 为了响应中断,我们一般会有一个硬件的中断控制器,当设备完成任务后触发中断到中断控制器,中断控制器就通知 CPU,一个中断产生了,CPU 需要停下当前手里的事情来处理中断。

DMA:硬件

  1. 职责:
    1. 有的设备需要读取或者写入大量数据
    2. CPU 只需要对 DMA 控制器下指令,说它想读取多少数据,放在内存的某个地方就可以了,接下来 DMA 控制器会发指令给磁盘控制器,读取磁盘上的数据到指定的内存位置,传输完毕之后,DMA 控制器发中断通知 CPU 指令完成,CPU 就可以直接用内存里面现成的数据了。

驱动程序:软件

  1. 职责:
    1. 向上:操作系统的内核代码可以像调用本地代码一样调用驱动程序的代码
    2. 向下:而驱动程序的代码需要发出特殊的面向设备控制器的指令,才能操作设备控制器。
    3. 既然设备驱动程序是用来对接设备控制器的,中断处理也应该在设备驱动里面完成。
  2. 工作流程:
    1. 一般的流程是,一个设备驱动程序初始化的时候,要先注册一个该设备的中断处理函数。咱们讲进程切换的时候说过,中断返回的那一刻是进程切换的时机。不知道你还记不记得,中断的时候,触发的函数是 do_IRQ。这个函数是中断处理的统一入口。在这个函数里面,我们可以找到设备驱动程序注册的中断处理函数 Handler,然后执行它进行中断处理。

块设备层:软件:

  1. 职责:
    1. 因为块设备类型非常多,而 Linux 操作系统里面一切是文件。我们也不想文件系统以下,就直接对接各种各样的块设备驱动程序,这样会使得文件系统的复杂度非常高。

文件系统:软件

  1. 职责:
    1. 到通用设备层,我们只是对linux内核来说,屏蔽了设备的区别,但是对linux系统的使用者来说,目前还无法操作硬件
    2. 这里我们就需要把文件系统当做是对用户态的封装接口
  2. 文件系统对设备的管理方式:
    1. 特殊的文件系统:devtmpfs 建立与某个设备驱动程序的连接
      1. 这个设备本身也不对应硬盘上的任何一个文件,_dev_sdb 其实是在一个特殊的文件系统 devtmpfs 中。
      2. 这个设备特殊文件_dev_sdb也有 inode,但是它不关联到硬盘或任何其他存储介质上的数据,而是建立了与某个设备驱动程序的连接。
    2. 特殊的文件系统:sysfs 管理设备
      1. 管理设备的文件系统,也就是 /sys 路径下面的 sysfs 文件系统。它把实际连接到系统上的设备和总线组成了一个分层的文件系统。这个文件系统是当前系统上实际的设备数的真实反映。
      2. 有了 sysfs 以后,我们还需要一个守护进程 udev。当一个设备新插入系统的时候,内核会检测到这个设备,并会创建一个内核对象 kobject 。 这个对象通过 sysfs 文件系统展现到用户层,同时内核还向用户空间发送一个热插拔消息。udevd 会监听这些消息,在 /dev 中创建对应的文件。

2020年5月9日 下午11:11

作为初学者,理解系统调用的角度:

  1. 设计了比较好的组合数据结构,目的是达到查找的加速
  2. 系统调用很多都是对已有数据结构的遍历
  3. linux内部的核心数据结构+在这个数据结构上的操作
  4. 如何理解、记忆系统调用过程:千万别背,这和做leetcode是一样的,一行行得被代码忘掉很快,关键抓住两点:数据结构的设计+在这套数据结构下有几个大的操作步骤

系统调用 read 和 write

  1. 在 VFS 层调用的是 vfs_read 和 vfs_write 并且调用 file_operation。在 ext4 层调用的是 ext4_file_read_iter 和 ext4_file_write_iter。
  2. 接下来就是分叉。你需要知道缓存 I_O 和直接 I_O。直接 I_O 读写的流程是一样的,调用 ext4_direct_IO,再往下就调用块设备层了。缓存 I_O 读写的流程不一样。对于读,从块设备读取到缓存中,然后从缓存中拷贝到用户态。对于写,从用户态拷贝到缓存,设置缓存页为脏,然后启动一个线程写入块设备

2020年5月9日 下午10:58

硬件文件系统 VS 虚拟文件系统

  1. 硬件文件系统 :硬件文件存储模式设计,解决如何存储的问题
  2. 虚拟文件系统 :数据结构的设计,来模拟我们需要的关系、逻辑,来达到抽象的目的,进行统一的文件管理,叫做虚拟文件系统

核心:简单做一个梳理,帮助理解记忆

  1. 对于每一个进程,打开的文件都有一个文件描述符,在 files_struct 里面会有文件描述符数组。每个一个文件描述符是这个数组的下标,里面的内容指向一个 file 结构,表示打开的文件。这个结构里面有这个文件对应的 inode,最重要的是这个文件对应的操作 file_operation。如果操作这个文件,就看这个 file_operation 里面的定义了。
  2. 对于每一个打开的文件,都有一个 dentry 对应,虽然叫作 directory entry,但是不仅仅表示文件夹,也表示文件。它最重要的作用就是指向这个文件对应的 inode。
  3. 如果说 file 结构是一个文件打开以后才创建的,dentry 是放在一个 dentry cache 里面的,文件关闭了,他依然存在,因而他可以更长期地维护内存中的文件的表示和硬盘上文件的表示之间的关系。
  4. inode 结构就表示硬盘上的 inode,包括块设备号等。
  5. 几乎每一种结构都有自己对应的 operation 结构,里面都是一些方法,因而当后面遇到对于某种结构进行处理的时候,如果不容易找到相应的处理函数,就先找这个 operation 结构,就清楚了。

从层次划分的角度去理解文件系统所处的位置:中间的核心

虚拟文件系统的三个核心数据结构:

  1. 文件挂载类mount struct + file类 + dentry
    1. Mount struct :用来抽象表示人类的挂载动作
    2. file:表示当前打开的文件
    3. dentry:
      1. 与file的区别:
        1. file会在文件关闭后delete,dentry实体会有缓存
        2. dentry有parent属性,用来抽象表示文件树的链接关系
      2. dentry主要用来指向文件的inode

文件查找缓存的设计:主要是在dentry上面做

  1. 在这里面,我们需要先查找文件路径最后一部分对应的 dentry。如何查找呢?
  2. Linux 为了提高目录项对象的处理效率,设计与实现了目录项高速缓存 dentry cache,简称 dcache。它主要由两个数据结构组成:
    1. 哈希表 dentry_hashtable:dcache 中的所有 dentry 对象都通过 d_hash 指针链到相应的 dentry 哈希链表中;
    2. 未使用的 dentry 对象链表 s_dentry_lru:dentry 对象通过其 d_lru 指针链入 LRU 链表中。LRU 的意思是最近最少使用,我们已经好几次看到它了。只要有它,就说明长时间不使用,就应该释放了。

2020年5月9日 下午10:30

linux中inode结构设计

  1. inode版本1:
  2. inode版本2:Extents
    1. 实例:
      1. 除了根节点,其他的节点都保存在一个块 4k 里面,4k 扣除 ext4_extent_header 的 12 个 byte,剩下的能够放 340 项,每个 extent 最大能表示 128MB 的数据,340 个 extent 会使你表示的文件达到 42.5GB。
    2. 注意:
      1. Extend本质是一个指针(不是真正的保存数据),只不过他能指向128M的内存连续内存空间,而不是仅仅一个4k的Block

Linux下对硬盘的划分设计方法:

  1. 版本1:组块 Block Group
    1. 理解超级块和块描述符的定义是关键:
      1. 超级块和块组描述符表都是全局信息,也就是整个硬盘的信息,而且这些数据很重要。如果这些数据丢失了,整个文件系统都打不开了,这比一个文件的一个块损坏更严重。所以,这两部分我们都需要备份,但是采取不同的策略。
      2. 块组描述符表我以前错误的理解为描述一个文件用多个块组的信息,没想到描述的是所有文件各自使用多个块组的信息,也就是整个硬盘的信息,这样就非常大了!
    2. 文件大小的计算:
      1. 一个数据块的位图可以表示的最大数据是128M
        1. 数据块的位图是放在一个块里面的,共 4k。每位表示一个数据块,共可以表示 4 * 1024 * 8 = 2^{15} 个数据块。如果每个数据块也是按默认的 4K,最大可以表示空间为 2^{15} * 4 * 1024 = 2^{27} 个 byte,也就是 128M。
      2. 块组描述符的引出,是用来表示大于128M文件的基础:
        1. 如果采用“一个块的位图 + 一系列的块”,外加“一个块的 inode 的位图 + 一系列的 inode 的结构”,最多能够表示 128M。是不是太小了?现在很多文件都比这个大。我们先把这个结构称为一个块组。有 N 多的块组,就能够表示 N 大的文件。
    3. 需要改进的原因:
      1. 对于超级块来讲,由于超级块不是很大,所以就算我们备份多了也没有太多问题。
      2. 但是,对于块组描述符表来讲,如果每个块组里面都保存一份完整的块组描述符表,一方面很浪费空间;另一个方面,由于一个块组最大 128M,而块组描述符表里面有多少项,这就限制了有多少个块组,128M * 块组的总数目是整个文件系统的大小,就被限制住了。
  2. 版本2:Meta Block Group
    1. 核心思想:化零为整(多个Block - > 一个Meta Block Group)
    2. 一个元块组包含 64 个块组,这样一个元块组中的块组描述符表最多 64 项
    3. 根据图中,每一个元块组包含 64 个块组,块组描述符表也是 64 项,备份三份,在元块组的第一个,第二个和最后一个块组的开始处。

文件和文件夹在Inode的设计方法(包含利用哈利进行加速的版本)

  1. 改进的地方:
    1. 如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。然后打开这个块,如果里面不再是索引,而是索引树的叶子节点的话,那里面还是 ext4_dir_entry_2 的列表,我们只要一项一项找文件名就行。
    2. 通过索引树,我们可以将一个目录下面的 N 多的文件分散到很多的块里面,可以很快地进行查找。

Inode进行软连接和硬链接的设计

  1. 硬链接与原始文件共用一个 inode 的,但是 inode 是不跨文件系统的,每个文件系统都有自己的 inode 列表,因而硬链接是没有办法跨文件系统的。
  2. 而软链接不同,软链接相当于重新创建了一个文件。这个文件也有独立的 inode,只不过打开这个文件看里面内容的时候,内容指向另外的一个文件。这就很灵活了。我们可以跨文件系统,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。
  3. Linux软连接和硬链接 - iTech - 博客园
    1. 删除符号连接f3,对f1,f2无影响;
    2. 删除硬连接f2,对f1,f3也无影响;
    3. 删除原文件f1,对硬连接f2没有影响,导致符号连接f3失效;
    4. 同时删除原文件f1,硬连接f2,整个文件会真正的被删除。
      1
      2
      3
      4
      5
      6
      7
      8
      [oracle@Linux]$ touch f1          #创建一个测试文件f1
      [oracle@Linux]$ ln f1 f2 #创建f1的一个硬连接文件f2
      [oracle@Linux]$ ln -s f1 f3 #创建f1的一个符号连接文件f3
      [oracle@Linux]$ ls -li # -i参数显示文件的inode节点信息
      total 0
      9797648 -rw-r--r-- 2 oracle oinstall 0 Apr 21 08:11 f1
      9797648 -rw-r--r-- 2 oracle oinstall 0 Apr 21 08:11 f2
      9797649 lrwxrwxrwx 1 oracle oinstall 2 Apr 21 08:11 f3 -> f1

总结一下 inode 和数据块在文件系统上的关联关系

  1. 为了表示图中上半部分的那个简单的树形结构,在文件系统上的布局就像图的下半部分一样。无论是文件夹还是文件,都有一个 inode。
  2. inode 里面会指向数据块,对于文件夹的数据块,里面是一个表,是下一层的文件名和 inode 的对应关系,文件的数据块里面存放的才是真正的数据。

2020年5月9日 下午2:22

总结:

  1. TCP与UDP的区别:
    1. 顺序问题 ,稳重不乱;
    2. 丢包问题,承诺靠谱;
    3. 连接维护,有始有终;
    4. 流量控制,把握分寸;
    5. 拥塞控制,知进知退。
  2. 顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的,这其实就相当于你领导和你的工作备忘录,布置过的工作要有编号,干完了有反馈,活不能派太多,也不能太少;
  3. 拥塞控制是通过拥塞窗口来解决的,相当于往管道里面倒水,快了容易溢出,慢了浪费带宽,要摸着石头过河,找到最优值。
  4. 两种wait
    1. server端的close_wait
    2. client端的time_wait
  5. 两种重传机制
    1. 超时重传
    2. 快速重传
  6. 两种拥塞控制
    1. 慢启动
    2. 快速重传下不适用慢启动
  7. 两种window
    1. client端的AdvertisedWindow
      1. 包含两部分:已发送 + 没发送两部分
        1. 已发送,未确认
        2. 没法送,可发送
      2. 关键:client发送了之后,并没有直接空下AdvertisedWindow,还得等待接收
    2. server端MaxRcvBuffer
      1. 关键1:为什么server确认了之后,数据依然还在MaxRcvBuffer中?
        1. 等待应用层读取的
      2. 关键2:如果,来的数据超过了MaxRcvBuffer,这个buffer是放不下的
    3. client和server端的窗口移动的条件要明确
      1. 并不是发送了就可以移动
      2. 并不是接收了就可以移动



建立连接:


滑动窗口:

  1. 发送的数据结构:
    1. AdvertisedWindow的大小:由接收端的数据结构来确定
      1. NextByteExpected 和 LastByteRead 的差其实是还没被应用层读取的部分占用掉的 MaxRcvBuffer 的量,我们定义为 A。
      2. AdvertisedWindow 其实是 MaxRcvBuffer 减去 A。
      3. 也就是:AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)。
  2. 接受的数据结构:
    1. MaxRcvBuffer:最大缓存的量;
    2. LastByteRead 之后是已经接收了,但是还没被应用层读取的;
    3. NextByteExpected 是第一部分和第二部分的分界线。

顺序和丢包问题

  1. 超时重传机制:自适应重传算法
    • TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
  2. 快速重传机制:
    • 有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。
    • 例如,接收方发现 6 收到了,8 也收到了,但是 7 还没来,那肯定是丢了,于是发送 6 的 ACK,要求下一个是 7。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。

流量控制

  • 如果接收端还是一直不处理数据,则随着确认的包越来越多,从接收端传来的AdvertisedWindow窗口越来越小,直到为 0,发送端就不发送了。

拥塞控制:

  1. 慢启动
  2. 快速重传:
    1. 前面我们讲过快速重传算法。当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。