2024年1月

本文介绍在
Visual Studio
软件中调用
C++
各种配置、编译完毕的第三方库的方法。

在撰写
C++
代码时,如果需要用到他人撰写的第三方库(例如地理数据处理库
GDAL
、矩阵运算库
Armadillo
等),并不能像
Python
等语言那样,安装好库后直接在不同代码文件中使用;而是需要每一次新建一个代码文件(在
Visual Studio
软件中就是新建一个
项目
)后,还需要额外配置一下才可以使用。

本文就以之前的文章
Visual Studio部署C++矩阵库Armadillo的方法
中介绍的矩阵运算库
Armadillo
为例,介绍安装完某一个第三方库后,如何在
Visual Studio
软件新的
项目
中调用这个库。

首先,按照文章
Visual Studio部署C++矩阵库Armadillo的方法
中提到的方法,我们配置、编译好这个矩阵运算库
Armadillo
。随后,在我们
配置这一库
时所创立的
项目
中,是可以在
源文件
中调用该库的;如下图所示,我们在这一个
项目

源文件
中输入一段调用该库的代码,其在引入
<armadillo>
时是不会报错的,且代码也可以正常运行。

但是,在实际使用过程中,我们往往需要在很多其他的
项目
中调用刚刚配置好的
Armadillo
库;如下图所示,我们在电脑的另一个位置新建一个
项目
,假设这个
项目
也需要使用
Armadillo
库。

随后,在刚刚新建立的
项目
中输入调用
Armadillo
库的代码,可以看到其在引入
<armadillo>
时就已经报错。

且代码亦无法正常运行。

这就需要我们在每一个调用
Armadillo
库的新的
项目
开始运行前,进行如下的设置。

首先,在
Visual Studio
软件左侧的内容列表中选中
项目名称
(即下图的
Testlib
),并选择“
属性
”按钮;随后,在弹出的窗口中依次选择“
C/C++
”→“
常规
”→“
附加包含目录
”→“
编辑
”。

接下来,选择我们配置
Armadillo
库时得到的
include
文件夹。

完成后,会出现如下图所示的情况。

接下来,在“
链接器
”选项中,选择“
附加库目录
”。

依据同样的方法,将我们配置
Armadillo
库时得到的
lib_win64
文件夹导入其中。这里需要注意,不同的第三方库在进行这一步时所选的文件夹名称可能不一致,但一般这里选择带有
lib
字样的文件夹就可以。

导入后,如下图所示。

最后,再在“
链接器
”→“
输入
”中选择“
附加依赖项
”。

并将我们配置
Armadillo
库时得到的
lib
文件(对于
Armadillo
库而言,这里就是
libopenblas.lib
文件)的路径与其文件名称输入其中;我们可以在按下
Shift
按钮的同时右键选中对应
lib
文件,即可出现“
复制文件地址
”的选项。

将复制好的文件地址粘贴至“
附加依赖项
”中即可,如下图所示。

此时,可以看到刚刚新建立的项目中,其在引入
<armadillo>
时就已经不再报错。

同时,代码也可以正常运行。

至此,大功告成。

前面两篇介绍了
分类模型
评估的两类方法,
准确率分析

损失分析

本篇介绍的
杰卡德相似系数

马修斯相关系数
为我们提供了不同的角度来观察模型的性能,
尤其在不平衡数据场景中,它们更能体现出其独特的价值。

接下来,让我们一起了解这两个评估指标的原理与特点。

1. 杰卡德相似系数

杰卡德相似系数

Jaccard similarity coefficient
)用于衡量两个集合的相似度。
在分类模型中,通常将每个类别看作一个集合,然后计算模型预测结果与实际结果之间的杰卡德相似系数。

杰卡德相似系数
能够直观地反映模型预测的准确性,并且对于不平衡数据集具有一定的鲁棒性。
它特别适用于二元分类问题,但也可以扩展到多类分类问题中。

1.1. 计算公式

\(J(y, \hat{y}) = \frac{|y \cap \hat{y}|}{|y \cup \hat{y}|}\)
其中,
\(y\)
是真实值,
\(\hat y\)
是预测值。

1.2. 使用示例

import numpy as np
from sklearn.metrics import jaccard_score

n = 100
k = 2
y_true = np.random.randint(0, k, n)
y_pred = np.random.randint(0, k, n)

s = jaccard_score(y_true, y_pred)
print("jaccard score:{}".format(s))

# 运行结果:
jaccard score:0.36585365853658536

上面的示例中,是针对二分类数据(
k=2
)来计算
杰卡德相似系数
的。

如果是多分类的数据,需要设置
jaccard_score

average
参数,否则会报错。
average
参数主要有以下选项:

  • binary
    :仅当目标 是
    二分类
    时才适用
  • micro
    :通过计算总的
    真阳性

    假阴性

    假阳性
    来计算全局指标
  • macro
    :计算每个标签的指标,并找到它们的
    未加权平均值
  • weighted
    :计算每个标签的指标,并找到其平均值,并按支持度(每个标签的真实实例数)加权
  • samples
    :计算每个实例的指标,并找到它们的平均值(仅对多标签分类有意义)
import numpy as np
from sklearn.metrics import jaccard_score

n = 100
k = 5
y_true = np.random.randint(0, k, n)
y_pred = np.random.randint(0, k, n)

s1 = jaccard_score(y_true, y_pred, average="micro")
s2 = jaccard_score(y_true, y_pred, average="macro")
s3 = jaccard_score(y_true, y_pred, average="weighted")
print("jaccard score:\nmicro:{}\nmacro:{}\nweighted:{}".format(s1, s2, s3))

# 运行结果:
jaccard score:
micro:0.0989010989010989
macro:0.09772727272727273
weighted:0.09639935064935062

上面的示例是多分类的情况,不设置
average
参数的话,会报错。

2. 马修斯相关系数

马修斯相关系数

Matthews correlation coefficient
)是一种用于衡量二分类模型性能的指标,
它考虑了真阳性(TP)、真阴性(TN)、假阳性(FP)和假阴性(FN)四个方面的信息。

马修斯相关系数
对于不平衡数据集具有较好的适应性,并且能够综合考虑模型的真正例率和真负例率。
与准确率、召回率等指标相比,
马修斯相关系数
在处理不平衡数据集时更为可靠。

2.1. 计算公式

\(MCC = \frac{tp \times tn - fp \times fn}{\sqrt{(tp + fp)(tp + fn)(tn + fp)(tn + fn)}}\)

关于
\(tp\)

\(tn\)

\(fp\)

\(fn\)
的概念,参见下表:


实际结果(真) 实际结果(假)
预测结果(真) tp(true positive)真阳性 fp(false positive)假阳性
预测结果(假) fn(false negative)假阴性 tn(true negative)真阴性

2.2. 使用示例

import numpy as np
from sklearn.metrics import matthews_corrcoef

n = 100
k = 2
y_true = np.random.randint(0, k, n)
y_pred = np.random.randint(0, k, n)

s = matthews_corrcoef(y_true, y_pred)
print("matthews corrcoef:{}".format(s))

# 运行结果
matthews corrcoef:0.028231544780468854

上面示例中,用的随机样本数据是二分类(
k=2
)的,不过,
马修斯相关系数
对多分类数据也是支持的。

3. 总结

总的来说,
杰卡德相似系数

马修斯相关系数
为我们提供了更全面、更深入的分类模型性能评估视角。
尤其是在处理不平衡数据集时,这两个指标的表现尤为突出。

杰卡德相似系数
从集合相似度的角度展现了模型的预测准确性,

马修斯相关系数
则综合考虑了各类别的预测情况,为模型的整体性能给出了更为准确的反馈。

介绍

按位非运算符(
~
)将操作数的位反转。
它将操作数转化为 32 位的有符号整型
。也就是可以对数字进行取整操作(保留整数部分,舍弃小数部分)。

~-2 // 1
~-2.222 // 1

并且按位非运算时,任何数字
x
(已被转化为 32 位有符号整型) 的运算结果都是
-(x + 1)

那么双重按位非(
~~
)对数字的运算结果就是
-(-(x + 1) + 1)
,结果就是
x

所以利用
~~
操作数字时就可对其进行取整操作(右移操作符
x >> 0
和按位或操作符
x | 0
也有相同作用)。

如果操作的不是
Number
类型的,操作的对象会先转化
Number
类型,下面一起来看看。

操作原始数据类型时

~~(-2.999);  // => -2
~~null; // => 0
~~undefined; // => 0
~~0;         // => 0
~~(1/0);     // => 0
~~false;     // => 0
~~true;      // => 1
~~'1234'     // => 1234
~~'1234asdf' // => 0
~~NaN        // => 0

~~
对于不能转化为数字的数据(
NaN
) ,操作的结果为 0

右移操作符
>>
和按位或操作符
|
也是如此。

(-2.999) >> 0   // => -2
null >> 0       // => 0
undefined >> 0  // => 0
0 >> 0          // => 0
(1/0) >> 0      // => 0
false >> 0      // => 0
true >> 0       // => 1
'1234' >> 0     // => 1234
'1234asdf' >> 0 // => 0
NaN >> 0        // => 0

(-2.999) | 0   // => -2
null | 0       // => 0
undefined | 0  // => 0
0 | 0          // => 0
(1/0) | 0      // => 0
false | 0      // => 0
true | 0       // => 1
'1234' | 0     // => 1234
'1234asdf' | 0 // => 0
NaN | 0        // => 0

操作对象数据类型时


~~
作用于对象类型时,对象类型会先隐式转化为数字,转化的结果取决于对象的
valueOf
方法和
toString
方法返回的结果。如果对象类型转化后最终的结果是
NaN
,那么
~~
操作
NaN
则会直接返回
0

详细的转换过程:

  1. 调用对象的
    valueOf
    方法


    如果该方法返回一个原始值,JavaScript会尝试将这个原始值转换为一个数字。如果
    valueOf
    方法返回的还是一个对象,JavaScript会继续调用对象的
    toString
    方法。

  2. 调用对象的
    toString
    方法


    这个方法返回一个字符串,然后JavaScript会尝试将这个字符串转换为一个数字。

  3. 转换字符串为数字

    一旦从
    valueOf

    toString
    方法中获得了一个原始值(通常是字符串),JavaScript会按照字符串到数字的转换规则来处理这个值。

如果
valueOf

toString
返回的值是
NaN
,那么结果就是
NaN
。如果字符串不能被解析为一个有效的数字,结果也是
NaN

~~
操作
NaN
返回
0

所以也就有下面的结果:

~~{};    // => 0
~~{a:1}  // => 0
~~[];    // => 0
~~[1];   // => 1
~~[1,2]; // => 0

对于数组而言,将数组转化为数字,会调用数组的
toString()

数组的
toString
方法实际上在内部调用了
join()
方法来拼接数组并返回一个包含所有数组元素的字符串,元素之间用逗号分隔。如果
join
方法不可用或者不是函数,则会使用
Object.prototype.toString
来代替,并返回
[object Array]

上面的 [1,2] 经过
toString()
后是
'1,2'
, 转为数字则是
NaN
。所以
~~[1,2]
结果为 0。

下面是对象有自定义的
valueOf()
或者
toString()
情况

var a = {
    valueOf:function(){return '11'},  // 字符串'11' 可被转化为 数字 11
    toString:function(){return 12}
}
~~a // => 11

var b = {
    valueOf:function(){return 'asdf'}, // 字符串'asdf' 转化为 NaN
    toString:function(){return 12}
}
~~b // => 0

var c = {
    toString:function(){return 12} // 没有 valueOf() ,则调用 toString()
}
~~c // => 12

var d = {
    toString:function(){return 'asdf'} // 字符串'asdf' 转化为 NaN
}
~~d // => 0

可进行运算的数字的有效范围

由于
按位运算总是将操作数转换为 32 位整数。
超过 32 位的数字将丢弃其最高有效位。如下例子中(来自MDN),超过 32 位的整数将转换为 32 位整数:

Before: 66666600110666666110100000000000000110000000000001
After:              10100000000000000110000000000001

再比如
~~
操作日期类型数据,
Date

valueOf
方法返回以数值格式表示的一个
Date
对象的原始值,从 1970 年 1 月 1 日 0 时 0 分 0 秒到该日期对象所代表时间的毫秒数。

返回的毫秒数是超过 32 位的整数,不在
~~
操作的有效范围内,结果就不会是期望的那样。

var date = new Date()
Number(date) // 1706671595364
~~date // 1569578852  结果失真

所以只有对 32位浮点数(经测试,有效范围为:
[-2^31,2^31-1]
,即
[-2147483648,2147483647]
) 进行按位运算时才会得到期望的结果。

~~2147483647.1 // => 2147483647  正确
~~2147483648.1 // => -2147483648  不正确

~~-2147483648.1 // => -2147483648  正确
~~-2147483649.1 // => 2147483647 不正确

需要注意的是,如果整数部分和小数部分数字之和超过了 16 位(不包括小数点),那么双重按位非操作符的结果也会不正确:(
Number编码的精度
的算术会受到
舍入
的影响。)

image

image

使用场景

对一些函数入参校验及处理方面有用,比如传入的可能是任意数字,需要排除掉极端情况(
NaN

Infinity
),然后取整;

function fn(){
    var param = arguments[1]
    if(param === 'number' && !isNaN(foo) && foo !== Infinity){
        var value = Number(param) || 0;
        value = (value < 0)
             ? Math.ceil(value)
             : Math.floor(value);
    }
}

使用
~~
后:

function fn(){
    var value = ~~arguments[1]
}

拓展

左移(<<)和右移(>>)运算符可以用来进行快速的二进制乘法和除法操作。左移一个数值实际上等于将这个数值乘以2的某个幂,而右移一个数值则等于将这个数值除以2的某个幂(忽略余数)。

但这个使用场景只适用对 2 的乘法除法操作(。。。)

let result = 6 * 2; // 结果是12

let base = 6; // 二进制表示为 110 
let shift = 1; // 左移1位 
let result = base << shift; // 结果是12,二进制表示为 1100
let result = 12 / 2; // 结果是6,但可能得到一个浮点数 let roundedResult = Math.floor(12 / 2); // 结果是6,确保得到整数

let base = 12; // 二进制表示为 1100 
let shift = 1; // 右移1位 
let result = base >> shift; // 结果是6,二进制表示为 110

总结

本文探讨了使用双重按位非运算符
~~
对操作数取整的原理。

  • ~~
    之所以可以用来取整,是因为按位运算操作数转化为 32 位的有符号整型,会舍弃掉小数部分。并且按位非运算(
    ~
    )时,任何数字
    x
    (已被转化为 32 位有符号整型) 的运算结果都是
    -(x + 1)
    。那么双重按位非(
    ~~
    )对数字的运算结果就是
    -(-(x + 1) + 1)
    ,结果就是
    x
  • 操作数是数字并且在位运算的有效范围内(
    [-2^31,2^31-1]
    ),
    ~~
    取整才会得到期望的结果。
  • 使用场景方面对一些函数入参校验及处理方面可能有用


折腾完毕

技术背景

当今众多的基于Python的AI框架(如MindSpore、PyTorch等)给了开发者非常便利的编程的条件,我们可以用Python的简单的语法写代码,然后由框架在后端自动编译成可以在GPU上高效计算的程序。而对于一些定制化比较高的算法,MindSpore也支持了相关的接口,允许开发者自己开发相应的CUDA算子(需要统一接口),然后编译成
.so
动态链接库,再用MindSpore内置的函数加载为本地算子。本文针对这种方案写一个简单的示例。

程序结构

本地自己手写一个CUDA算子,一般至少需要两个文件和一个nvcc的环境,最好是在安装完成MindSpore的GPU版本之后,再尝试CUDA算子的引入。具体MindSpore的安装方法,可以参考
MindSpore官网
,这里不做赘述。我这里使用的环境是10.1版本的nvcc:

$ nvcc -V
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2019 NVIDIA Corporation
Built on Sun_Jul_28_19:07:16_PDT_2019
Cuda compilation tools, release 10.1, V10.1.243
$ python3 -m pip show mindspore
Name: mindspore
Version: 2.1.0
Summary: MindSpore is a new open source deep learning training/inference framework that could be used for mobile, edge and cloud scenarios.
Home-page: https://www.mindspore.cn
Author: The MindSpore Authors
Author-email: contact@mindspore.cn
License: Apache 2.0
Location: /home/dechin/anaconda3/envs/mindspore-latest/lib/python3.7/site-packages
Requires: numpy, protobuf, pillow, astunparse, scipy, psutil, asttokens, packaging
Required-by: 

需要准备的两个文件,一个是CUDA算子本身的
.cu
文件,另一个是用来调用CUDA算子的
.py
文件。操作流程是:先按照自己的需求写好CUDA算子,然后用nvcc进行编译,编译输出为
.so
的动态链接库,然后在python脚本中使用
mindspore.ops.Custom
生成相应的算子。在MindSpore2.1之后的版本中,对于本地CUDA算子的调用定义了统一的接口,其格式为:

extern "C" int CustomFunc(int nparam, void **params, int *ndims, int64_t **shapes, const char **dtypes, void *stream, void *extra);

具体方法可以参考官网的这一篇
文档说明
。这样的话,在
.cu
文件中至少有两个函数,一个是原本用于计算的
Kernel函数
,另一个是用于统一标准接口的
CustomFunc函数
。需要说明的是,旧版本的MindSpore是没有这样的规范的,所以旧版本的算子没有
CustomFunc函数
也能够用nvcc编译,但是无法在新版本的MindSpore中调用。

一维张量求和

我们用一个一维的张量求和的示例来演示一下如何在本地写一个可以用MindSpore来调用的CUDA算子,一维张量求和的算法是比较简单的:

\[C_i=A_i+B_i
\]

那么对应的CUDA算子的代码如下所示:

// custom_add.cu
// nvcc --shared -Xcompiler -fPIC -o custom_add.so custom_add.cu
// 常量,一般可以放在.cuh头文件中
constexpr int THREADS = 1024;
// 用于CUDA计算的Kernel函数
__global__ void CustomAddKernel(float *input1, float *input2, float *output, size_t size) {
    auto idx = blockIdx.x * THREADS + threadIdx.x;
    if (idx < size) {
        // 逐元素操作,CUDA算子的基本写法
        output[idx] = input1[idx] + input2[idx];
    }
}
// 标准算子接口
extern "C" int CustomAdd(int nparam, void **params, int *ndims, int64_t **shapes, const char **dtypes, void *stream,
                         void *extra) {
    cudaStream_t custream = static_cast<cudaStream_t>(stream);
    // 输出变量的位置
    constexpr int OUTPUT_INDEX = 2;
    // 传入的参数都是指针形式
    float *input1 = static_cast<float *>(params[0]);
    float *input2 = static_cast<float *>(params[1]);
    float *output = static_cast<float *>(params[2]);
    // 获取输入张量的大小
    int size = shapes[OUTPUT_INDEX][0];
    // GPU运算中的block和thread,一般要求block*thread大于或者等于size即可
    int blocks = ceil(size / THREADS) + 1;
    // 调用Kernel函数
    CustomAddKernel<<<blocks, THREADS, 0, custream>>>(input1, input2, output, size);
    return 0;
}

值得注意的是,上述CustomAdd函数中的params有3个输入,但是实际上其中一个是返回值,这也是MindSpore对于标准接口的设定,不需要我们额外传入一个变量。保存好上述的CUDA算子代码之后,可以用如下指令直接编译成python可以调用的动态链接库:

$ nvcc --shared -Xcompiler -fPIC -o custom_add.so custom_add.cu

编译完成后,会在当前目录下生成一个新的
.so
文件,然后就可以在python代码中进行调用:

# test_custom_ops.py
# python3 test_custom_ops.py
import mindspore as ms
from mindspore import ops, Tensor, context
ms.set_context(device_target="GPU", mode=context.GRAPH_MODE)

t1 = Tensor([1., 2., 3.], ms.float32)
t2 = Tensor([3., 2., 1.], ms.float32)
CustomAdd = ops.Custom("./custom_add.so:CustomAdd",
                       out_shape=[t1.shape[0]],
                       out_dtype=ms.float32,
                       func_type="aot"
                       )
res = CustomAdd(t1, t2)
ops.print_(res)

上述的CustomAdd就是我们导入的基于CUDA算子来写的本地MindSpore算子,并且这种算子还可以使用MindSpore进行注册,这样就不需要每次使用都去加载这个动态链接库,感兴趣的童鞋可以自己研究一下算子注册的方法。上述Python代码的运行结果如下:

$ python3 test_custom_ops.py 
Tensor(shape=[3], dtype=Float32, value= [ 4.00000000e+00,  4.00000000e+00,  4.00000000e+00])

可见跟我们预期的结果是一致的,那么就完成了一个本地CUDA算子的实现和调用。

自定义算子反向传播

在前面的章节里面我们已经实现了一个本地的一维张量求和算子,但是这还不是一个完整的算子实现,因为我们在AI框架中一般要求算子可自动微分,光实现一个算子是不完整的,因此这里我们再通过bprop函数,来实现一下自定义算子的反向传播:

# test_custom_ops.py
# python3 test_custom_ops.py
import mindspore as ms
from mindspore import ops, Tensor, context
from mindspore import nn, grad
ms.set_context(device_target="GPU", mode=context.GRAPH_MODE)

t1 = Tensor([1., 2., 3.], ms.float32)
t2 = Tensor([3., 2., 1.], ms.float32)
CustomAdd = ops.Custom("./custom_add.so:CustomAdd",
                       out_shape=[t1.shape[0]],
                       out_dtype=ms.float32,
                       func_type="aot"
                       )
为了自动微分,我们需要定义一个Cell类来封装我们的自定义算子:
class Add(nn.Cell):
    # 反向传播函数
    def bprop(self, x, y, out, dout):
        return (y, )
    # 计算函数
    def construct(self, x, y):
        return CustomAdd(x, y)
# 把Cell类加载为custom_add函数
custom_add = Add()
# 计算求和结果
res = custom_add(t1, t2)
# 计算自动微分结果
res_g = grad(custom_add, grad_position=(0, ))(t1, t2)
print(res)
print(res_g)

在这个代码中,主要就是增加了一个Cell类和两个新的函数
bprop

construct
,分别用于计算函数的反向传播和正向值,代码运行的结果如下:

$ python3 test_custom_ops.py 
[4. 4. 4.]
[3. 2. 1.]

当然,这里我们没有再额外写一个用于返回反向传播值的CUDA算子,但是原则上对于较为复杂的函数,是需要自己手动写一个用于求微分数值的CUDA算子的。

总结概要

本文介绍了在MindSpore标准格式下进行CUDA算子开发的方法和流程,可以让开发者在现有的AI框架下仍然可以调用基于CUDA实现的高性能的算子。并且,除了常规的数值计算之外,在MindSpore框架下,我们还可以通过实现一个bprop函数,使得我们手写的这个CUDA算子也可以使用MindSpore框架本身自带的自动微分-端到端微分技术。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/mindspore-cuda.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

Ubuntu22.04 & Win11 双系统hibernate热切换实现

[lab.magiconch.com][福音戰士標題生成器]-1695021721029

使用命令
free -m
来查看当前还剩余的内存

neo@NeoNeuxs:~$ free -m
               total        used        free      shared  buff/cache   available
Mem:           31711        4557       23753         193        3400       26519
Swap:           2047           0        2047

可以看到这台电脑,可以用内存是32GB,swap区的大小只有2G,这是不正常的,应该是安装系统的时候没有设置好,swap分区大小会影响电脑的休眠状态,即无法进入休眠状态,因为休眠的时候需要将内存的内容写入到交换分区当中。

修改交换分区或交换文件

swap分为两种分别是交换文件和交换分区,交换文件和交换分区是有所不同的,在较高版本的内核情况下,二者的差异不大,二者的差异往往是由文件系统来决定的,尤其是目前常用的都是EXT4的文件系统,据说二者的性能差异几乎可以忽略不计,这里没有测试,希望有人可以测试一下看看,不同的分区在跑代码的时候有没有区别,当然这个程序要占用内存足够大才可以测试出来。下面我们来讲一下二者的设置:

修改交换分区

如果在安装的过程中没有进行手动设置的话,你的swap类型很有可能是swpafile的形式,可以通过如下方式来查看具体类型:

命令:
cat /proc/swaps

neo@NeoNeuxs:/dev$ cat /proc/swaps
Filename				Type		Size		Used		Priority
/swapfile                               file		2097148		0		-2
/dev/nvme0n1p7                          partition	67109884	0		-3

可以看到我有两个交换分区类型,一个是
file
一个是
partition
,两个大小不同,优先级不同。

/swapfile
的是系统直接给我创建的,我寻思是一点用都没有啊,默认的2G太小了,甚至无法休眠,也就没法进行系统的热切换了,遂萌生想法修改分区类型和大小。

首先如果要使用交换分区你需要有可用的硬盘空间,我是双系统多个硬盘,LINUX系统的分区无法进行压缩,也就是说无法将系统盘压缩,如下所示:

neo@NeoNeuxs:/dev$ lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
nvme1n1     259:0    0 953.9G  0 disk 
├─nvme1n1p1 259:1    0    16M  0 part 
├─nvme1n1p2 259:2    0 503.9G  0 part 
├─nvme1n1p3 259:3    0   513M  0 part 
└─nvme1n1p4 259:4    0 449.5G  0 part /
nvme0n1     259:5    0 953.9G  0 disk 
├─nvme0n1p1 259:6    0   260M  0 part /boot/efi
├─nvme0n1p2 259:7    0    16M  0 part 
├─nvme0n1p3 259:8    0 864.4G  0 part 
├─nvme0n1p4 259:9    0   950M  0 part 
├─nvme0n1p5 259:10   0    24G  0 part 
├─nvme0n1p6 259:11   0   260M  0 part 
└─nvme0n1p7 259:12   0    64G  0 part 

nvme1n1

nvme1n1p2
503G的分区存储了windos的资料,无法压缩出来可用空间,目前系统的分区
nvme1n1p4
也无法压缩,通过软件Gparted可以查看当前分区可以分割出来的大小,没办法只能使用另一块硬盘
nvme0n1
分出来了64G的空间
nvme0n1p7
来作为交换空间,分割出来没有进行格式化的空间以后,使用Gparted软件格式化成linuxswap分区格式,效果如下:

image-20240131005139221

有了linux-swap格式的分区之后,我们直接挂载即可:

neo@NeoNeuxs:/dev$ sudo swapon /dev/nvme0n1p7

挂载之后就可以看到:

neo@NeoNeuxs:/dev$ free -m
               total        used        free      shared  buff/cache   available
Mem:           31711        7107       21645         236        2958       23924
Swap:          67584           0       67584
neo@NeoNeuxs:/dev$ cat /proc/swaps
Filename				Type		Size		Used		Priority
/swapfile                               file		2097148		0		-2
/dev/nvme0n1p7                          partition	67109884	0		-3

为了实现开机自动挂载还需要修改一下,以下文件:

使用命令:
sudo gedit /etc/fstab
打开fstab,可以用其他编辑器如vim等。

如下图所示,最后一行是系统默认的缓存文件我们按照一样的格式,将我们的盘添加进去:

image-20240131015521411

修改最后一行:

image-20240131015636137

修改以后保存一下试一试。

经过hibernate测试之后发现了问题,就是重启之后会影响对应硬盘的符号,这个问题很致命,盘符号变更之后系统无法正确识别对应的缓冲区,应使用UUID来识别对应的缓冲分区,修改之后如下:

UUID=4047d76b-15e4-4513-8b79-f0f5d0c32b47  none         swap    sw        0       0

image-20240131155432221

获取对应硬盘的UUID

我们同样可以通过命令:

sudo lsblk -o name,mountpoint,size,uuid

效果如下:

image-20240131161313853

快速获取对应的硬盘的UUID,方法如下 grep后边的参数表明了要获取对应的swap分区的UUID:

blkid | grep swap
/dev/nvme1n1p7: LABEL="swaper" UUID="4047d76b-15e4-4513-8b79-f0f5d0c32b47" TYPE="swap" PARTLABEL="swaper" PARTUUID="c98d4618-281c-438f-8602-b3e34381f4fa"

修改交换文件

我们来修改一下交换文件:

1、关闭交换文件:

neo@NeoNeuxs:~$ sudo swapoff -a
neo@NeoNeuxs:~$ free -m
               total        used        free      shared  buff/cache   available
Mem:           31711        4692       23615         188        3402       26388
Swap:              0           0           0

2、创建swap分区文件:

neo@NeoNeuxs:/var$ sudo dd if=/dev/zero of=/var/swapfile bs=1M count=32768
	32768+0 records in
32768+0 records out
34359738368 bytes (34 GB, 32 GiB) copied, 15.1301 s, 2.3 GB/s

of
代表输出文件(output file),这里指定的是
/var/swapfile
,本质上就是使用dd命令,复制一个特殊文件
/dev/zero

var/swapfile

/dev/zero
全是0,即创建的是一个全是0的文件。

3、创建swap文件

使用命令:

sudo mkswap /var/swapfile

结果如下:

neo@NeoNeuxs:/var$ sudo mkswap /var/swapfile

mkswap: /var/swapfile: insecure permissions 0644, fix with: chmod 0600 /var/swapfile
Setting up swapspace version 1, size = 32 GiB (34359734272 bytes)
no label, UUID=ecf57d17-b84f-4002-9553-a995f6ab5839

4、修改权限并启用swap文件

neo@NeoNeuxs:/var$ sudo chmod 0600 /var/swapfile
neo@NeoNeuxs:/var$ sudo swapon /var/swapfile
neo@NeoNeuxs:/var$ free -m
               total        used        free      shared  buff/cache   available
Mem:           31711        4979         498         211       26233       26059
Swap:          32767           0       32767

使用交换文件按道理也能实现系统的热切换,但我没有具体实验,理论上应那该是可以的,设置的时候应该是相同的,设置为第3步的UUID即可,如果不行的话需要参考分区的热切换。

如何使Ubuntu进入hibernate?

实现双系统切换可以参考:

herbinate实现:
https://askubuntu.com/questions/1240123/how-to-enable-the-hibernate-option-in-ubuntu-20-04

对于前几步为了保证教程的完整,我复制了过来,作为参考:

  1. Install
    pm-utils
    and
    hibernate
    :

    sudo apt install pm-utils hibernate
    
  2. Then:

    cat /sys/power/state
    
  3. You should see:

    freeze mem disk
    
  4. Then run one of the following lines:

    grep swap /etc/fstab
    blkid | grep swap
    
  5. 获取对应的SWAP分区的UUID,保存下来。

  6. 修改grub的配置文件:

    sudo nano /etc/default/grub	
    
  7. 修改对应行内容如下:

    GRUB_CMDLINE_LINUX_DEFAULT="quiet splash resume=UUID=<YOUR_COPIED_UUID>"
    
  8. 更新grub设置:

    sudo update-grub
    
  9. 启动hibernate模式

    sudo systemctl hibernate
    

需要注意的是,热切换系统需要grub引导的支持,如果直接通过Refind等引导Ubuntu直接启动是无法正常热切换的,也就是说需要进入到grub引导的驱动的界面引导Ubuntu启动。

参考文章

swap分区:
https://www.baeldung.com/linux/swap-file-partition

swap分区实操:
https://phoenixnap.com/kb/swap-partition

herbinate实现:
https://askubuntu.com/questions/1240123/how-to-enable-the-hibernate-option-in-ubuntu-20-04