0%

线程

概述

线程(内核线程)是操作系统进行任务调度的基本单位,一般来说,操作系统以线程为单位进行处理机调度。

通常一个线程只具有控制流、处理机和栈等少量运行中必不可少资源,因此必须依靠进程提供相应的系统资源等才能正常运行。因此线程必须与进程绑定,当进程退出时,其内部的线程都将全部退出。一个进程可以有多个线程,它们共享该线程提供的系统资源等。

线程状态

线程存在许多状态,其转换图如下:

分离线程与可结合线程

Linux线程被分为分离线程与可结合线程,默认情况下创建的线程为可结合线程。两者的区别如下:

  1. 是否可以join:分离线程不可以使用join进行结合,可结合线程能够使用join进行结合。

  2. 资源释放时机:分离线程在线程运行结束后自动释放,可结合线程在调用join结束后释放。

CPP线程库

在C++11中引入了std::thread类用于创建和管理线程,并在std::this_thread命名空间中引入了一些方法用于更好的对当前线程进行控制。

std::thread

API

创建
方法 含义 备注
thread() noexcept; 默认构造函数 创建一个空线程对象,该对象不对应任何实际线程
template <class Fn, class… Args>
explicit thread(Fn&& fn, Args&&… args);
初始化构造函数 根据输入参数创建一个线程,该线程为可结合线程,并且创建后线程立即进入就绪状态。
线程中将会自动运行fn(args...)函数
thread(thread&& x) noexcept; 移动构造函数 只涉及线程控制权的转移,调用后新建的线程对象将能够控制输入对象原先管理的实际线程,输入对象变为空线程对象
移动
方法 含义
thread& operator=(thread&& rhs) noexcept; 移动赋值,仅涉及线程管理权的转移,使用后调用对象获取传入参数实际对应的实际线程的管理权,传入参数变为空对象。
void swap(std::thread& other) noexcept; 交换两个线程对象内实际管理线程的管理权。
状态获取
方法 含义
bool joinable() const noexcept; 返回是否为活动进程,通常相当于get_id()!=std::thread::id ()
std::thread::id get_id() const noexcept; 返回线程id,线程id在操作系统范围内是唯一的。
static unsigned int hardware_concurrency() noexcept; 返回硬件支持的最大并行线程数。对于计算密集型任务,通常用于进行参考。
线程管理
方法 含义
void join(); join该对象管理的实际线程,调用该方法的线程将被阻塞直到对应线程对象运行完成且资源完成回收。
Linux下,可结合线程只有在调用join后才会释放栈等相关资源。
若在不是可结合线程的线程对象上调用该方法将抛出std::system_error。
调用结束后原线程对象变为空对象。
void detach(); detach相应的线程,调用该方法后该对象管理的线程将处于分离状态。
Linux中,分离线程在程序运行结束后将会立即释放资源。
若在不是可结合线程的线程对象上调用该方法将抛出std::system_error。
调用结束后原线程对象变为空对象。

参数

函数参数传递

对函数而言,共有4种参数:

  1. 值参数

  2. 左值引用

  3. 常量左值引用

  4. 右值引用

同时有两种参数传递方式:

  1. 值传递:在调用函数时创建对应副本,可以被分为初始化构造、拷贝构造与移动构造。

  2. 引用传递:不发生拷贝,类似传入指针。

测试代码与执行结果

使用如下代码进行测试,其中被注释的部分表示会导致编译错误:

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
#include <iostream>
#include <string>
#include <thread>

class TestClass {
public:
TestClass(const std::string &s) : str(s) {
print("DCtor");
}

TestClass(const TestClass &t) : str("Copy_" + t.str) {
print("CCtor");
}

TestClass(TestClass &&t) : str("Move_" + t.str) {
print("MCtor");
}

TestClass &operator=(const TestClass &t) {
str = "CopyA_" + t.str;
print("CAssign");
return *this;
}

TestClass &operator=(TestClass &&t) {
str = "MoveA_" + t.str;
print("MAssign");
return *this;
}

~TestClass() {
print("Dtor");
}

friend std::ostream &operator<<(std::ostream &out, const TestClass &tc) {
return out << "Print: " << tc.str;
}
private:
void print(const char *s) {
std::cout << s << ": " << str << std::endl;
}
private:
std::string str;
};

void func1(TestClass a) {
std::cout << a << std::endl;

}

void func2(TestClass &b) {
std::cout << b << std::endl;
}

void func3(const TestClass &c) {
std::cout << c << std::endl;
}

void func4(TestClass &&d) {
std::cout << d << std::endl;
}

void test1() {
TestClass t(__func__);
std::cout << __func__ << '+' << std::endl;
// std::thread(func2, __func__).join();
// std::cout << "==============================" << std::endl;
std::thread(func1, t).join();
std::cout << "==============================" << std::endl;
std::thread(func1, std::ref(t)).join();
std::cout << "==============================" << std::endl;
std::thread(func1, std::cref(t)).join();
std::cout << "==============================" << std::endl;
std::thread(func1, std::move(t)).join();
std::cout << __func__ << '-' << std::endl;
}

void test2() {
TestClass t(__func__);
std::cout << __func__ << '+' << std::endl;
// std::thread(func2, __func__).join();
// std::cout << "==============================" << std::endl;
// std::thread(func2, t).join();
// std::cout << "==============================" << std::endl;
std::thread(func2, std::ref(t)).join();
std::cout << "==============================" << std::endl;
// std::thread(func2, std::cref(t)).join();
// std::cout << "==============================" << std::endl;
// std::thread(func2, std::move(t)).join();
// std::cout << __func__ << '-' << std::endl;
}

void test3() {
TestClass t(__func__);
std::cout << __func__ << '+' << std::endl;
// std::thread(func3, __func__).join();
// std::cout << "==============================" << std::endl;
std::thread(func3, t).join();
std::cout << "==============================" << std::endl;
std::thread(func3, std::ref(t)).join();
std::cout << "==============================" << std::endl;
std::thread(func3, std::cref(t)).join();
std::cout << "==============================" << std::endl;
std::thread(func3, std::move(t)).join();
std::cout << __func__ << '-' << std::endl;
}

void test4() {
TestClass t(__func__);
std::cout << __func__ << '+' << std::endl;
// std::thread(func4, __func__).join();
// std::cout << "==============================" << std::endl;
std::thread(func4, t).join();
std::cout << "==============================" << std::endl;
// std::thread(func4, std::ref(t)).join();
// std::cout << "==============================" << std::endl;
// std::thread(func4, std::cref(t)).join();
// std::cout << "==============================" << std::endl;
std::thread(func4, std::move(t)).join();
std::cout << __func__ << '-' << std::endl;
}


int main(int argc, char *argv[]) {
test1();
std::cout << std::endl;
test2();
std::cout << std::endl;
test3();
std::cout << std::endl;
test4();
return 0;
}

使用如下命令进行编译运行:

1
g++ test.cpp -o test -std=c++17

结果如下:

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
DCtor: test1
test1+
CCtor: Copy_test1
MCtor: Move_Copy_test1
Print: Move_Copy_test1
Dtor: Move_Copy_test1
Dtor: Copy_test1
==============================
CCtor: Copy_test1
Print: Copy_test1
Dtor: Copy_test1
==============================
CCtor: Copy_test1
Print: Copy_test1
Dtor: Copy_test1
==============================
MCtor: Move_test1
MCtor: Move_Move_test1
Print: Move_Move_test1
Dtor: Move_Move_test1
Dtor: Move_test1
test1-
Dtor: test1

DCtor: test2
test2+
Print: test2
==============================
Dtor: test2

DCtor: test3
test3+
CCtor: Copy_test3
Print: Copy_test3
Dtor: Copy_test3
==============================
Print: test3
==============================
Print: test3
==============================
MCtor: Move_test3
Print: Move_test3
Dtor: Move_test3
test3-
Dtor: test3

DCtor: test4
test4+
CCtor: Copy_test4
Print: Copy_test4
Dtor: Copy_test4
==============================
MCtor: Move_test4
Print: Move_test4
Dtor: Move_test4
test4-
Dtor: test4
分析

实际上,在调用初始化构造函数创建一个实际线程时需要进行三次次参数传递,分别是:

  1. 从调用函数传递到构造函数中。在这一步中将会使用万能引用,因此全是引用传递。

  2. 在构造函数函数中在堆上建立相应的_Invoker对象,传递到新线程中。在这一步中将会创建对应变量,其传递方式由目标函数参数的种类和传入方式决定,其具体传递方法如下表:

    T T& const T& T&&
    t 拷贝 拷贝 拷贝
    std::ref(t) 拷贝 引用 引用
    std::cref(t) 拷贝 引用
    std::move(t) 移动 移动 移动
  3. 在新线程中调用目标函数。在这一步中将会采用目标函数对应参数的种类决定传递方式,其中值传递将会以移动形式调用,引用传递将使用进行引用传递。

std::this_thread

方法

std::this_thread命名空间中引入了一些方法,这些方法只作用于调用改代码的线程,用于更好的控制线程运行。这些方法如下:

方法 含义
void yield() noexcept; 请求操作系统进行新一轮的线程调度,在希望当前运行线程让出处理器时调用。
std::thread::id get_id() noexcept; 返回当前线程的线程id。
template <class Rep, class Period>
void sleep_for(const std::chrono::duration<Rep, Period>& sleep_duration);
当前线程休眠一段时间。通常情况下是当前线程进入阻塞状态sleep_duration,随后进入就绪状态等待运行。
由于存在调度延时等,在时间上可能存在较大误差。
template <class Clock, class Duration>
void sleep_until(const std::chrono::time_point<Clock, Duration>& sleep_time);
当前线程休眠直到指定时间。通常情况下是当前线程进入阻塞状态直到sleep_time,随后进入就绪状态等待运行。
由于存在调度延时等,在时间上可能存在较大误差。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>
#include <chrono>

using namespace std::chrono_literals;

void func() {
auto id = std::this_thread::get_id();
std::cout << id << std::endl;
std::this_thread::sleep_for(2000ms);
std::this_thread::sleep_until(std::chrono::steady_clock::now() + 2000ms);std::this_thread::yield();
}

int main(int argc, char *argv[]) {
std::thread(func).join();
return 0;
}

概况

CPP foreach循环是CPP11引入的新特性,其能够很轻易地完成对可迭代对象的遍历,一般而言,一个典型的foreach循环结构如下:

1
2
3
for (const auto &item : contain) {
do_something(item);
}

可迭代对象

可迭代对象(上述示例中的contain)是一个可以使用迭代器遍历的对象,其要求满足以下条件:

  1. 实现了begin方法,并要求该方法返回一个迭代器且无参数,一个典型的声明如下:

    iterator begin() const;

  2. 实现了end方法,并要求该方法返回一个迭代器且无参数,一个典型的声明如下:

    iterator end() const;

而迭代器要求满足以下条件:

  1. 实现了相应的构造函数和赋值函数。

  2. 实现了operator++方法,一个典型的声明如下:

    iterator &operator++();

  3. 实现了operator!=方法,一个典型的声明如下:

    bool operator!=(const iterator &) const;

  4. 实现了operator*方法,一个典型的声明如下:

    T &operator*() const;

等价代码

实际上CPP中的foreach循环是一种语法糖,其等价于如下代码:

1
2
3
4
5
6
7
8
{
iterator first = contain.begin();
iterator last = contain.end();
while (first != last) {
do_something(*first);
++first;
}
}

代码示例

下面展示了一个对自定义可迭代对象进行迭代的Demo示例:

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
#include <iostream>

class Range {
public:
class RangeIterator {
public:
explicit RangeIterator(std::size_t i) :m_number_(i) {
std::cout << __FILE__ << ':' << __LINE__ << " ictor" << std::endl;
}

RangeIterator(const RangeIterator &it) :RangeIterator(it.m_number_) {
std::cout << __FILE__ << ':' << __LINE__ << " icopy_ctor" << std::endl;
}

RangeIterator(RangeIterator &&it) :RangeIterator(it.m_number_) {
std::cout << __FILE__ << ':' << __LINE__ << " imove_ctor" << std::endl;
}

RangeIterator &operator=(const RangeIterator &it) {
m_number_ = it.m_number_;
std::cout << __FILE__ << ':' << __LINE__ << " icopy" << std::endl;
return *this;
}

RangeIterator &operator=(RangeIterator &&it) {
m_number_ = it.m_number_;
std::cout << __FILE__ << ':' << __LINE__ << " imove" << std::endl;
return *this;
}

bool operator!=(const RangeIterator &rit) const {
std::cout << __FILE__ << ':' << __LINE__ << " operator!=" << std::endl;
return m_number_ != rit.m_number_;
}

RangeIterator &operator++() {
std::cout << __FILE__ << ':' << __LINE__ << " operator++" << std::endl;
++m_number_;
return *this;
}

std::size_t operator*() const {
std::cout << __FILE__ << ':' << __LINE__ << " *" << std::endl;
return m_number_;
}
private:
std::size_t m_number_;
};
public:
explicit Range(std::size_t first = 0, std::size_t last = SIZE_MAX) :
m_first_(first), m_last_(last) {
std::cout << __FILE__ << ':' << __LINE__ << " ctor" << std::endl;
}

Range(const Range &range) :
m_first_(range.m_first_), m_last_(range.m_last_) {
std::cout << __FILE__ << ':' << __LINE__ << " copy_ctor" << std::endl;
}

Range(Range &&range) :
m_first_(std::move(range.m_first_)), m_last_(std::move(range.m_last_)) {
std::cout << __FILE__ << ':' << __LINE__ << " move_ctor" << std::endl;
}

Range &operator=(const Range &range) {
m_first_ = range.m_first_;
m_last_ = range.m_last_;
std::cout << __FILE__ << ':' << __LINE__ << " copy" << std::endl;
return *this;
}

Range &operator=(Range &&range) {
m_first_ = std::move(range.m_first_);
m_last_ = std::move(range.m_last_);
std::cout << __FILE__ << ':' << __LINE__ << " move_ctor" << std::endl;
return *this;
}

RangeIterator begin() const {
std::cout << __FILE__ << ':' << __LINE__ << " begin" << std::endl;
return m_first_;
}

RangeIterator end() const {
std::cout << __FILE__ << ':' << __LINE__ << " end" << std::endl;
return m_last_;
}
private:
RangeIterator m_first_;
RangeIterator m_last_;
};

int main() {
for (const auto &i : Range(0, 4)) {
std::cout << __FILE__ << ':' << __LINE__ << ' ' << i << std::endl;
}
return 0;
}

编译运行后的结果为:

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
test0.cpp:8 ictor
test0.cpp:8 ictor
test0.cpp:52 ctor
test0.cpp:80 begin
test0.cpp:8 ictor
test0.cpp:12 icopy_ctor
test0.cpp:85 end
test0.cpp:8 ictor
test0.cpp:12 icopy_ctor
test0.cpp:32 operator!=
test0.cpp:43 *
test0.cpp:95 0
test0.cpp:37 operator++
test0.cpp:32 operator!=
test0.cpp:43 *
test0.cpp:95 1
test0.cpp:37 operator++
test0.cpp:32 operator!=
test0.cpp:43 *
test0.cpp:95 2
test0.cpp:37 operator++
test0.cpp:32 operator!=
test0.cpp:43 *
test0.cpp:95 3
test0.cpp:37 operator++
test0.cpp:32 operator!=

OpenGL渲染管线

渲染管线表示的是一次完整的图形图像渲染过程。OpenGL使用的渲染管线是可编程渲染管线,能够对图形进行复杂操作。

其基本流程如下:

  1. 顶点数组:输入的基础数据,只包含顶点信息。

  2. 顶点着色器:可编程部分,其操作对象为输入的顶点数据,通常用于调整坐标空间。

  3. 图元装配:按照装配规则和顶点顺序进行图元组装,OpenGL仅支持点、线和三角形平面。

  4. 几何着色器:可选的可编程部分,其操作对象为输入的图元数据,其可以动态的生成顶点和新的形状等。

  5. 光栅化:将输入的图元信息裁剪映射到窗口的像素上,并对图元上未处在顶点上的数据进行插值处理获得输入值。

  6. 片段着色器:可编程部分,其操作对象为光栅化输出的片段,通常用于着色。

  7. 片段测试:对输入的像素点进行测试,包括深度测试等,获得最后需要显示在屏幕上的像素。

  8. 帧缓冲:完成渲染的图片将会放置在帧缓冲区等待使用。

顶点数组

原始的顶点数据,通常包含该顶点的位置、颜色、法线向量、纹理映射等信息。

顶点着色器

顶点着色器从接收顶点数组信息,并对在各个顶点上进行处理。通常这些处理包括坐标空间转换等,同时还可以将部分数据传入后续着色器。

图元装配

在图元装配阶段,将目前已有的顶点按照一定的规则装配为图元,OpenGL支持的图元有三角形、直线或者点精灵等。

OpenGL支持的图元装配规则如下:

几何着色器

几何着色器是一个可选的着色器,可以选择是否对其进行实现。

几何着色器的操作单位为图元,其可以获得该图元所有相连顶点的输出信息,并修改顶点信息甚至创建新的顶点,从而修改图形的形状。

光栅化

在OpenGL中,光栅化能够将图元投射到二维屏幕上,从而生成一个一个的待填充像素。在这一过程中,对于非顶点的像素片段,其输入值由其关联顶点通过插值计算得到。

光栅化实质上是离散化,其将逻辑上连续的图形分割成为一个个离散的像素:

片段着色器

片段着色器作用在每一个片段上,对于每一个片段,通过对所在图元的顶点输出进行插值获得相应的输入。这一部分一般用于赋予片段颜色、添加纹理或者处理光线等。

片段测试

片段测试部分主要判断对应的片段是否需要进行显示或如何显示,其主要过程如下:

  1. 像素所有权测试:清除在当前视窗中但无法看到的像素。这种情况常见于窗口遮挡。

  2. 裁剪测试:裁剪测试用于限制绘制区域,只有在指定区域的像素才有机会被绘制。

  3. 多重采样片段操作:组合多个不同多边形产生的片段的颜色来决定最终的像素颜色,可以减少多边形边缘的走样现象。

  4. 模板测试:模板测试可以根据设置的模板对输入的图像进行过滤。当输入像素对应模板的值为0时被过滤;值为1时通过测试。

  5. 深度测试:用于测试像素深度,当深度测试关闭时,新的颜色将直接覆盖旧的颜色;深度测试开启后,只有当新像素的深度小于旧像素时,才会进行覆盖。

  6. 颜色混合:主要用于表现半透明效果,半透明时无法简单的用新颜色覆盖旧颜色,而是需要进行颜色混合。

帧缓冲

帧缓冲是一种数据缓冲区,其缓冲单元数量与一帧图像中的帧数量相同。常用的帧缓冲包含颜色缓冲、深度缓冲、模板缓冲等。

设置GitHub免密登录

  1. 安装Git

    sudo apt install git -y

  2. 配置Git

    git config --global user.name <你的名称>
    git config --global user.email <你的邮箱>

  3. Git生成密钥,输入以下命令,之后连续按3次回车:

    ssh-keygen -t rsa -C <你的邮箱>

  4. 打印生成的公钥,将输出复制下来:

    cat ~/.ssh/id_rsa.pub

  5. 将复制后的公钥添加到GitHub中:点击页面右上角头像——Setting——SSH And GPG keys——New SSH Keys,填写后添加即可。

  6. 运行如下命令,输入yes添加服务器,未报错则完成免密设置。

    ssh -T git@github.com

安装Hexo

运行如下命令,运行前请确保能够访问raw.githubusercontent.com,详见我之前写的解决raw.githubusercontent.com无法访问的问题

1
2
3
4
5
6
7
sudo apt install curl -y
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
export NVM_NPM_ORG_MIRROR=https://npmmirror.com/mirrors/npm/
export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node/
nvm install stable
npm config set registry https://registry.npmmirror.com
npm install hexo-cli -g

设置GitHub托管

  1. 创建新的博客目录

    hexo init .

  2. 在GitHub新建仓库,并复制下该仓库的Git URL。

  3. 修改博客根目录下的_config.yml

    将其中的deploy部分修改改为如下格式:

    1
    2
    3
    4
    deploy:
    type: git
    repo: <目标仓库Git URL>
    branch: main

WSL访问Windows文件

Window的各个盘符会挂载到WSL/mnt目录下,即C盘在/mnt/c、D盘在/mnt/d,以此类推。

Windows访问WSL文件

  1. WSL的命令行中输入explorer.exe .,将使用windows资源管理器打开WSL当前目录。

  2. WSL根目录挂在在\\wsl$\<系统版本>下,例如\\wsl$\Debian

启用相关特性

以管理员运行CMD,输入:

1
2
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart

之后重启Windows

安装WSL2 Debian

以管理员运行CMD,先测试网络是否联通:

1
wsl.exe --list --online

若输出无法解析服务器的名称或地址则说明无法访问raw.githubusercontent.com,解决方案详见我之前写的解决raw.githubusercontent.com无法访问的问题

确定能够访问后,运行如下命令:

1
wsl.exe --install --no-distribution

之后重启Windows

以管理员运行CMD,运行如下命令:

1
2
3
wsl.exe --set-default-version 2
wsl.exe --set-default Debian
wsl.exe --install --distribution Debian

快照管理方式

Git本地文件管理一文中,给出了Git如何将修改变为快照的方法。本文将会给出Git管理快照的方式——分支树。

Git使用链式结构组织提交,并使用hash标识链上的各个节点。但是,为了便于协作,需要一种方式使得Git能够使用一种方式使得多个人能够对项目的不同部分进行修改和提交。Git使用分支的方式提供这一服务。引入分支后,Git中的快照从原本的链型变成了树形:

分支

分支的实质

一个分支实质上是一条快照链表,其具有一个头指针,其指向当前快照链表的头。多个分支具有共同部分,于是连成树形:

创建分支

创建分支实质上是创建一个分支头指针,该头指针与其他指针相互独立:

在两个分支分别完成一笔提交后,Git内部变为下图:

切换分支

切换分支实质上是切换头节点指向的分支,如果上图中切换到b0分支的话,Git内部变为下图:

合并分支

合并分支是Git分支最重要的操作之一。其实质上是将两个分支后新建一个包含两个分支更改的新分支:

合并后的新节点包换合并的两个分支的全部修改,其具体计算方式如下:

  1. 找到合并的两个节点所在链表的分叉点。

  2. 通过git diff的方式获得两个节点相对于分叉点的修改。

  3. 将两份修改进行合并,产生新的镜像。

可以看出,在这一过程中,如果合并节点在同一位置更改存在不同的更改,这将产生冲突。此时需要管理员手动解决该项冲突,之后继续合并即可。

分支的持久化存储

分支头指针存储

各个分支头指针存储在项目路径下的.git/refs/heads中,其中使用分支名作为对应的文件名,存储着该分支头指针指向的节点的标识。

HEAD存储

HEAD的引用存储在项目路径下的.git/HEAD文件中,其内容为目前其指向的分支头或是其指向的镜像的hash标识符。

Git文件存储

Git将历史文件、快照等信息存储在工作区根目录的.git/objects目录下。objects用于存放镜像和实际文件等信息,其内部文件分为三种类型,分别为:

  • Blob:用于文件,将文件内容压缩成二进制文件后进行存储。

  • Tree:用于文件夹,存储的内容为文件夹下的文件类型、文件名和文件哈希值等,同样是使用二进制压缩存储。

  • Commit:代表快照,包含该快照所包含的文件和一些控制信息等,使用二进制压缩存储。

同时,Git将这些文件的哈希值作为分组和标识的依据。具体来说,将文件放置在以文件哈希值的前两位命名的文件夹中,并使用文件哈希值除前两位的部分作为文件名。

Git进行本地管理的过程图解

图中各元素含义如下:

  • 方形:实际存储数据的文件。

  • 菱形:目录,即文件的索引。

  • 圆形:快照文件。

  • 圆角矩形:头指针或分支指针。

  • 实线:被指对象被另一端对象的引用,可以被另一端对象索引查找。

  • 虚线:被指对象是另一端对象的压缩。

接下来给出Git进行本地文件管理的过程对应的图解:

  1. 假设目前项目中只有两个文件,并完成了一次提交,这回生成对应的压缩对象、索引文件、快照文件,并设置好索引和指针。其对应的内部状态如下:

  1. 现在修改其中一个文件,这会丢失文件与压缩对象的对应关系,于是文件变为modified状态。此时内部状态变为:

  1. 现在使用git add添加修改,此时会对其修改文件创建压缩对象,并修改暂存区索引,于是文件变为staged状态。此时内部状态变为:

  1. 现在使用git commit提交修改,这会生成此时的暂存区索引的压缩文件,并创建新的镜像对象,并将其指向上一提交镜像和当前暂存区压缩文件,之后相应的移动头指针和分支指针。此时内部状态变为:

一些常见问题

  1. 修改文件的局部时,是否会存储整个文件?

是的。当修改一个文件的局部时,将会重新生成对应文件的整个文件的镜像,而不是生成文件修改的镜像。

同时,只会新增被修改文件的镜像,而未修改文件不会形成相应的镜像。

因此,Git不适合管理那些经常会进行修改的较大文件,而更加适合对大量的、通常只会做局部修改的较小文件和不经常修改的较大文件进行管理。这符合编程项目的特点——大量较小的编程文本文件,不经常修改的较大的库文件。

  1. 当多次git add同一文件时,是否会生成对应的镜像文件?

视该文件hash是否改变而定。

git add会计算待添加文件的hash,并在当前的文件仓库中进行查找,只有当未找到时才会创建对应的文件。之后会设置相应的索引。

因此若多次添加的文件hash一致时,不会生成对应的镜像文件。甚至如果多个文件的hash相同时也只会生成一个对应的镜像文件。

当多次添加的文件hash不一致时,一般就会生成对应的镜像文件了。

Git四个区域

  • 工作区(workspack):实际修改代码的地方,是程序员能够直接操作的地方。

  • 暂存区(staging area):用于存放临时的改动,修改后不可恢复。

  • 本地仓库(local repository):安全存放数据的位置,将各个版本的信息存放在此,可以随时回滚。

  • 远程仓库(remote repository):托管代码的服务器,方便在不同的机器上进行开发。

文件状态

Git为文件存在4种状态:

  • untracked:未被Git管理的文件。

  • modified:代表已经被Git管理的文件,但该文件与暂存区对应的文件相比存在修改。Git只关心文件整体的修改,通过对比文件的SHA-1值判断文件是否被修改。

  • staged:当前已经被提交到暂存区的文件。

  • unmodified:当前已经提交形成镜像的文件。

除此之外,对于具有远端仓库的文件,根据是否上传到远端仓库还可以分为pushedunPushed

本地操作

创建仓库

1
git init <初始化路径>

git init <初始化路径>用于初始化本地仓库,使用后对应目录下的文件全部变为untracked状态。

提交修改

1
2
3
git add <目标文件或目录>

git commit

git add <目标文件或目录>untrackedmodified状态的文件加入暂存区且将状态转为staged

git commit将读取暂存区的文件,并生成对应快照挂载到快照树中,同时移动头指针指向新提交的快照。之后所有处于staged的文件全部转换为unmodify状态。

分支的创建与选择

1
2
3
4
5
6
7
8
# 创建并转到目标分支
git checkout -b <分支名>

# 创建目标分支
git branch <分支名>

# 转到目标分支
git checkout <分支名>

合并分支和解决冲突

1
2
3
4
5
6
7
8
9
# 合并当前分支和目标分支,可能会因为发生冲突而没有完成合并。之后需要手动将解决冲突
git merge <分支名>

# 解决了冲突文件后需要将修改提交到缓存区
git add <冲突文件名>

# 解决冲突后继续进行合并g

it merge --continue

回退版本

1
2
git reset [--soft | --mixed | --hard] [<目标版本>]
git reset [--soft | --mixed | --hard] [<目标版本>] [<目标文件列表>]

git reset用于进行版本回退,在未带任何参数的情况下默认回退到最新一次提交,相当于丢弃工作区的修改。

git reset存在三种标志,若省略则默认为--mixed,其各个标志的作用如下:

  • --soft:只会移动头指针,不会修改工作区和暂存区内容。

  • --mixed:移动头指针并重置暂存区为目标提交的内容,不会修改工作区修改。

  • --hard移动头指针并重置暂存区和工作区为目标提交的内容,注意这会丢失工作区修改!

<目标版本>有多种格式,若省略则默认为HEAD

  • HEAD是一个预设标志,代表当前头指针指向的版本,用于实现相对位置定位:

    • HEAD^HEAD对应版本的上一个版本,^符号可以多次重复,如HEAD^^代表HEAD的上两个版本,以此类推。

    • HEAD~1HEAD对应版本的上一个版本,~符号后的数字可以自行指定,如HEAD~2代表HEAD对应版本的上两个版本,以此类推。

  • <目标版本>也可是是对应版本的版本号,这将会直接转到对应版本,这不仅可以进行版本的回退,也可以用于恢复到当前版本的后几个版本或其他分支。

  • <目标版本>还可以是远程分支,这相当于回退到该远程分支对应的版本。

<目标文件列表>是需要回退版本的文件,通常情况下不会显示指定,也就是回退整个项目。

文件控制

1
2
git rm
git mv

因为Git不会自行跟踪文件的删除情况,因此仅在工作区删除文件时Git并不会自动的在项目中删除对应的文件,这时需要使用git rmgit mv显示指定对文件的删除和移动操作。

默认情况下,git rmgit mv会与rmmv一样直接删除或移动工作区的文件,如果只希望结束对目标文件的追踪而不会修改对应的工作区文件,需要添加--cached标志。

文件状态与文件修改查询

1
2
git status
git diff

git status用于显示上次提交后整个项目中各个被修改的文件对应的状态。

git diff获得两个版本间的文件差异。主要使用方式如下:

  • git diff:比较当前工作区和暂存区的区别。

  • git diff --cached:比较暂存区和HEAD指向版本的区别。

  • git diff <branch0> <branch1>:比较<branch0><branch1>两个版本间的区别。

查看日志

1
2
git log
git reflog

git log打印当前HEAD指针指向的节点向分支树根部的路径上的全部节点的信息,包括节点哈希值标识、注释等。注意,在移动头指针后将会改变其打印出的的节点信息。

git reflog打印出ref log,其记录了Git仓库中HEAD指针引用的变化,其基本上包含全部的提交信息。

Git远程常用操作

下载远程仓库

1
2
# 从URL下载并添加远程仓库
git clone <仓库url>

远程仓库配置

1
2
3
4
5
6
7
8
9
10
11
# 显示远程仓库信息
git remote -v

# 添加仓库
git remote add <仓库名> <仓库url>

# 移除仓库
git remote remove <仓库名>

# 重设仓库url
git remote set-url <仓库名> <仓库url>

推送代码

1
2
3
4
5
6
7
8
# 推送本地的分支版本上传到远程并合并,若在当前分支下工作可省略<本地分支名>
git push <远程主机名> <本地分支名>:<远程分支名>

# 本地版本与远程版本有差异时徐娅强制推送,若在当前分支下工作可省略<本地分支名>
git push --force <远程主机名> <本地分支名>:<远程分支名>

# 删除<远程分支>
git push <远程主机名> --delete <远程分支名>

拉取代码

1
2
3
4
5
# 拉取远程仓库代码
git fetch <远程主机名>

# 拉取远程仓库代码并与指定分支进行合并
git pull [远程仓库名] [分支名]

将本地分支和远程分支关联

1
2
# 将远程分支和本地分支关联
git branch -u <远程分支> <本地分支>