从零自制docker-10-【cgroup进行容器资源限制】

文章目录

  • 目的
  • 导入包的相关公开原则
  • 当前进程的挂载信息
  • defer
  • `for scanner.Scan()`
  • 判断字符串包含
  • 新建的cgroup的默认文件
  • cpu相关配置
  • 对应到ubuntu 22.04版本的cpu相关配置
  • top
  • 注意
  • 查看你可使用的cpu
  • 注意
  • 启动后的top查看
  • 显示进程使用的cpu序号
  • 代码
  • 结果

目的

启动容器时通过-mem、-cpu 等 flag 相关命令行参数来实现容器 cpu、内存资源限制

导入包的相关公开原则

在Go语言中,导入包中的结构体(struct)和接口(interface)内的字段、方法也同样遵循导出规则:

  1. 结构体(struct):
    • 结构体内的字段如果是公开(导出)的,也需要以大写字母开头。公开的字段可以在包外部访问和修改。
    • 结构体内的方法(不论接收者是谁)如果是公开的,方法名也需要以大写字母开头,这样才能在包外部调用。
// 在 mypackage 包中
package mypackage

type ExportedStruct struct {
    PublicField string // 可以在其他包中访问
    privateField string // 不能在其他包中访问
}

func (s ExportedStruct) PublicMethod() {} // 可以在其他包中调用
func (s ExportedStruct) privateMethod() {} // 不能在其他包中调用
  1. 接口(interface):
    • 接口内的方法全部都是公开的,因为接口本身就是一种契约,定义了一组方法签名,无论接口本身还是其内的方法,都需要以大写字母开头才能在包外部看到和使用。
// 在 mypackage 包中
package mypackage

type ExportedInterface interface {
    PublicMethod() // 可以在其他包中实现和调用
}

总结:无论是结构体还是接口,其内部的公开字段和方法都需要以大写字母开头,以便在其他包中正常使用。对于接口而言,因为接口本身就是方法的集合,所以接口内的所有方法都会自动视为公开的。

当前进程的挂载信息

/proc/self/mountinfo 是Linux系统中的一个虚拟文件,位于 /proc 文件系统中,该文件提供了系统当前挂载点的详细信息,尤其是关于当前进程(self)的挂载点信息。

每一行内容都代表一个挂载点的详细描述,包括:

  1. 挂载ID(unique ID identifying the mount point across all mounts within the system)。
  2. 父挂载ID(the ID of the parent mount (or of self for the rootfs))。
  3. 主设备号和次设备号(major and minor numbers of the device mounted)。
  4. 挂载点路径(the mount point’s pathname)。
  5. 根目录的设备ID(root directory ID)。
  6. 挂载选项(options used when mounting the filesystem)。
  7. 文件系统类型(filesystem type)。
  8. 挂载源(where the filesystem was mounted from, e.g., device path or NFS server/share)。
  9. 特殊挂载标志(additional flags specific to the mount)。

通过读取 /proc/self/mountinfo 文件,可以详细了解当前进程所能看到的文件系统挂载情况,这对于系统管理和故障排查非常有用。例如,如果要检查某个目录挂载的文件系统类型、挂载来源、挂载选项等信息,可以查阅这个文件的内容。

defer

在Go语言中,defer f.Close() 是用来保证文件或资源一定会在函数执行完毕前被关闭的语句。这里的 f 是一个实现了 io.Closer 接口的对象,通常是一个文件句柄或其他需要关闭的资源对象。Close 方法用于关闭资源,释放系统资源。

defer 关键字在 Go 语言中用于延迟执行函数,即该函数会在包含它的函数执行结束(无论是正常返回还是通过 panic 导致的异常退出)前调用。defer 语句的执行顺序是 LIFO(后进先出),也就是说,最后被 defer 的函数会在其他 defer 函数之前被执行。

例如:

func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // 当函数返回时,file.Close()会被调用,确保文件最终会被关闭

    // 对文件进行读写操作...

    return nil // 或者返回其他错误
}

在上面的代码中,无论 processFile 函数的执行是否成功,也不论函数是如何结束的(正常返回或抛出错误),file.Close() 都会在函数返回前被调用,确保了文件资源的正确释放。

for scanner.Scan()

在Go语言中,bufio.NewScanner(f) 创建了一个新的扫描器(Scanner),它可以从给定的输入流(在这里是 f)逐行读取数据。f 应该是一个实现了 io.Reader 接口的对象,如文件、网络连接或类似的数据流。

接着的 for scanner.Scan() 是一个循环,它会一直执行直到扫描器不再有可读的行为止:

  • scanner.Scan() 是一个方法调用,它尝试从输入流中读取下一行数据。如果成功读取到一行数据,则返回 true,并将读取到的数据存储在 scanner.Text()scanner.Bytes() 等方法可以访问的地方。
  • 如果 scanner.Scan() 读取到流的末尾,或者在读取过程中遇到了错误,它将返回 false 并且可以通过 scanner.Err() 检查发生的错误。

循环体内部就是对每一行数据进行处理的地方,例如:

f, err := os.Open("example.txt")
if err != nil {
    // 处理错误
}
defer f.Close()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
    line := scanner.Text() // 获取当前行的内容
    // 对line进行处理,如打印、解析或存入数据库等操作
    fmt.Println(line)
}

if err := scanner.Err(); err != nil {
    // 在循环结束后检查是否有读取错误
    fmt.Fprintln(os.Stderr, "Reading standard input:", err)
}

这样,for scanner.Scan() 循环就完成了一个从输入流中逐行读取数据并进行处理的过程。

判断字符串包含

在Go语言中,判断一个字符串(子串)是否存在于另一个字符串中,可以使用 strings.Contains 函数。以下是一个示例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	// 主字符串
	mainStr := "Hello, world! This is a test string."

	// 要查找的子字符串
	subStr := "test"

	// 判断子字符串是否存在于主字符串中
	isFound := strings.Contains(mainStr, subStr)

	if isFound {
		fmt.Printf("Substring '%s' found in the main string.\n", subStr)
	} else {
		fmt.Printf("Substring '%s' not found in the main string.\n", subStr)
	}
}

在上述代码中,strings.Contains(mainStr, subStr) 会返回一个布尔值,如果 subStr 存在于 mainStr 中,则返回 true,否则返回 false

新建的cgroup的默认文件

在这里插入图片描述

  • cgroup.controllers:显示当前cgroup所使用的控制器。
  • cgroup.events:显示与cgroup相关的事件。
  • cgroup.freeze:用于冻结或解冻cgroup。
  • cgroup.kill:用于杀死cgroup中的所有进程。
  • cgroup.max.depth:显示cgroup的最大深度。
  • cgroup.max.descendants:显示cgroup的最大后代数。
  • cgroup.pressure:显示cgroup的压力状况。
  • cgroup.procs:显示属于cgroup的进程列表。
  • cgroup.stat:显示有关cgroup的统计信息。
  • cgroup.subtree_control:用于控制cgroup的子树。
  • cgroup.threads:显示属于cgroup的线程数。
  • cgroup.type:显示cgroup的类型。

接下来是与CPU和内存资源控制相关的文件:

  • cpuset.cpus:显示cpuset分配的CPU的列表。
  • cpuset.cpus.effective:显示实际生效的cpuset分配的CPU的列表。
  • cpuset.cpus.partition:显示cpuset的CPU分区。
  • cpuset.mems:显示cpuset分配的内存节点的列表。
  • cpuset.mems.effective:显示实际生效的cpuset分配的内存节点的列表。
  • cpu.idle:显示CPU处于空闲状态的时间。
  • cpu.max:显示CPU可用的最大频率。
  • cpu.stat:显示CPU的统计信息。
  • cpu.pressure:显示CPU的压力状况。
  • cpu.max.burst:显示CPU的最大突发频率。
  • cpu.uclamp.max:显示CPU的最大utilization clamp值。
  • cpu.uclamp.min:显示CPU的最小utilization clamp值。
  • cpu.weight:显示CPU权重。
  • cpu.weight.nice:显示CPU权重的“nice”值。
  • io.max:显示I/O最大限制。
  • io.pressure:显示I/O的压力状况。
  • io.prio.class:显示I/O的优先级类别。
  • io.stat:显示I/O的统计信息。

接着是与内存资源控制相关的文件:

  • memory.events:显示内存事件的统计信息。
  • memory.peak:显示内存使用量的峰值。
  • memory.swap.peak:显示交换空间使用量的峰值。
  • memory.events.local:显示本地内存事件的统计信息。
  • memory.pressure:显示内存的压力状况。
  • memory.reclaim:显示内存的回收情况。
  • memory.zswap.current:显示zswap的当前状态。
  • memory.high:显示内存的高阈值。
  • memory.low:显示内存的低阈值。
  • memory.max:显示内存的最大限制。
  • memory.min:显示内存的最小限制。
  • memory.numa_stat:显示NUMA节点的内存统计信息。
  • memory.oom.group:显示内存OOM的组信息。
  • memory.swap.current:显示交换空间的当前状态。
  • memory.swap.events:显示交换空间事件的统计信息。
  • memory.swap.high:显示交换空间的高阈值。
  • memory.swap.max:显示交换空间的最大限制。
  • memory.swap.events:显示交换空间事件的统计信息。

最后是与进程控制相关的文件:

  • pids.current:显示当前进程数。
  • pids.events:显示与进程相关的事件。
  • pids.max:显示最大进程数限制。
  • pids.peak:显示进程数的峰值。

cpu相关配置

让我们通过一些例子来解释 cpu.cfs_period_us、cpu.cfs_quota_us 和 cpu.shares 之间的关系以及它们如何共同控制 CPU 资源分配:

假设我们有一个系统,其中 CPU 时间可以分配给不同的控制组(cgroup):

  1. cpu.shares 示例

    • 假设我们有两个 cgroup,即“A”和“B”,并且我们希望“A”获得两倍于“B”的 CPU 时间。
    • 我们将“A”的 cpu.shares 设置为 1024,将“B”的 cpu.shares 设置为 512。
    • 在这种情况下,cgroup“A”将获得两倍于 cgroup“B”的 CPU 时间。如果“A”和“B”都在积极使用 CPU,则“A”将获得 2/3 的 CPU 时间,“B”将获得 1/3。
  2. cpu.cfs_period_us 和 cpu.cfs_quota_us 示例

    • 现在,让我们使用 cpu.cfs_period_us 和 cpu.cfs_quota_us 来实现更细粒度的控制。
    • 假设我们有一个 cgroup“C”,我们希望限制它在 1 秒内只能使用 500 毫秒的 CPU 时间。
    • 我们将“C”的 cpu.cfs_period_us 设置为 1,000,000(表示 1 秒),并将 cpu.cfs_quota_us 设置为 500,000(表示 500 毫秒)。
    • 在这种情况下,“C”中的进程在 1 秒的周期内将仅获得 500 毫秒的 CPU 时间。一旦它们达到配额,它们将在该周期的剩余时间内被暂停。
  3. 结合使用 cpu.shares、cpu.cfs_period_us 和 cpu.cfs_quota_us

    • 现在,让我们将这三个设置结合起来。

    • 假设我们有两个 cgroup,“D”和“E”,我们希望“D”获得三倍于“E”的 CPU 时间,但我们还希望限制“D”在 2 秒内只能使用 1 秒的 CPU 时间。

    • 我们将“D”的 cpu.shares 设置为 1536,将“E”的 cpu.shares 设置为 512。

    • 对于“D”,我们将 cpu.cfs_period_us 设置为 2,000,000(2 秒),并将 cpu.cfs_quota_us 设置为 1,000,000(1 秒)。

    • 对于“E”,我们将 cpu.cfs_period_us 设置为 2,000,000(2 秒),并将 cpu.cfs_quota_us 设置为 2,000,000(2 秒)。

    • 在这种情况下,“D”将获得三倍于“E”的 CPU 时间,但受到 1 秒 CPU 时间配额的限制。一旦“D”达到了 1 秒的配额,它在当前周期的剩余时间内将被暂停。

对应到ubuntu 22.04版本的cpu相关配置

在这里插入图片描述

  1. cpuset.cpus:
    这个文件显示了当前 cgroup 可以使用的 CPU 列表。如果这个文件是空的,表示当前 cgroup 可以使用所有 CPU 资源。

  2. cpuset.cpus.effective:
    这个文件显示了当前 cgroup 实际可以使用的 CPU 列表。这个列表可能会比 cpuset.cpus 更加精确,因为它考虑了父 cgroup 的 CPU 分配情况。

在 cgroup 系统中,每个 cgroup 都可以设置自己的 CPU 分配策略。当一个子 cgroup 被创建时,它会继承父 cgroup 的 CPU 分配策略。
cpuset.cpus 文件表示当前 cgroup 可以使用的 CPU 列表。但是,这个列表可能并不完全准确,因为它没有考虑父 cgroup 的 CPU 分配。
而 cpuset.cpus.effective 文件则考虑了父 cgroup 的 CPU 分配情况,因此它显示的 CPU 列表更加精确。它实际上是根据当前 cgroup 和它的父 cgroup 的 CPU 分配策略计算出来的。
例如,如果父 cgroup 只允许使用 CPU 0 和 CPU 1,那么即使子 cgroup 可以使用所有 CPU,但 cpuset.cpus.effective 也只会显示 CPU 0 和 CPU 1。

  • cpuset.cpus 是空的,表示当前 cgroup 可以使用所有 CPU 资源。
  • cpuset.cpus.effective 显示 0-1,表示当前 cgroup 实际可以使用 CPU 0 和 CPU 1。

这意味着,即使当前 cgroup 可以使用所有 CPU 资源,但由于父 cgroup 的限制,它实际只能使用 CPU 0 和 CPU 1。

通过查看这两个文件,可以了解当前 cgroup 的 CPU 使用情况,并根据需要进行调整。如果需要更改 CPU 分配,可以修改这些文件的内容。

  1. cpu.max
    会根据前一个和后一个的比值来决定cpu的使用率,max代表最大,就是百分之百
  2. memory.max
    max代表最大

top

使用 top 命令可以查看进程使用的 CPU 情况。具体步骤如下:

  1. 运行 top 命令:
top
  1. 按下 1 键切换到 CPU 信息视图。这时你会看到每个 CPU 核心的使用情况。

  2. 要查看进程使用的 CPU 信息,可以按下 c 键切换到显示命令行模式。这样可以看到每个进程使用的 CPU 核心。

在这里插入图片描述

注意

如果父 cgroup 的 cpuset.cpus.effective 设置小于子 cgroup 的 cpuset.cpus.effective 设置,那么子 cgroup 中的进程实际上只能使用父 cgroup 允许的 CPU 范围。

具体来说:

  1. 当父 cgroup 的 cpuset.cpus.effective 被设置为某个范围(比如 0-1)时,所有子 cgroup 都会继承这个 CPU 范围。

  2. 即使子 cgroup 将 cpuset.cpus 设置为一个更大的范围(比如 0-3),但实际可用的 CPU 范围仍然被限制在父 cgroup 的 cpuset.cpus.effective 中(即 0-1)。

  3. 子 cgroup 进程只能使用父 cgroup 允许的 CPU 范围,即使子 cgroup 自己的设置允许使用更多 CPU。

  4. 如果父 cgroup 的 cpuset.cpus.effective 被修改,子 cgroup 的可用 CPU 范围也会相应地更新。

并且最开始的/sys/fs/cgroup没有 cpuset.cpus,只有 cpuset.cpus.effective ,但把当前可用的都包括到cpuset.cpus.effective里面去了,后面新建的cgroup中就有cpuset.cpuscpuset.cpus.effective 了,而且在父cgroup cpuset.cpus.effective 的允许范围内和cpuset.cpu的双重限制后cpuset.cpus.effective会自动变化(当cpuset.cpus`变化后)

查看你可使用的cpu

要查看当前可使用的 CPU 序号,您可以使用以下方法:

  1. 使用 lscpu 命令:
lscpu

这个命令会显示系统中 CPU 的详细信息,包括 CPU 数量和 CPU 序号。

  1. 使用 cat /proc/cpuinfo 命令:
cat /proc/cpuinfo

这个命令会输出系统中所有 CPU 的详细信息,包括 CPU 序号。
在这里插入图片描述

注意

发现运行一次后,第二次运行失败,原因是挂载的文件系统/proc没有卸载掉,卸载或者重新挂载到当前/proc即可

context.Args():所有flag识别后剩余的就是Args()的
context.Bool():只要有-it就行,不会识别后面的一个参数
context.String():会识别后面的一个参数,将其作为标签对应的字符串

启动后的top查看

由于启动后将/proc文件系统挂载到运行的地方去了,本地使用top时发现不行,此时再挂载回来即可
在这里插入图片描述

显示进程使用的cpu序号

要查看当前进程使用哪个 CPU,可以在 top 命令中使用一些额外的选项。

步骤如下:

  1. 首先确保 /proc 文件系统已经正确挂载,如果没有挂载可以使用以下命令进行挂载:

    sudo mount -t proc proc /proc
    
  2. 运行 top 命令,并按下 1 键查看每个 CPU 的使用情况。

  3. 要查看进程使用的具体 CPU 核心,可以按下 f 键进入配置模式,然后选择 %CPUP 两个字段。

    • %CPU 显示进程使用的 CPU 百分比
    • P 显示进程使用的 CPU 编号
  4. 按下 d 键可以设置刷新频率,然后按下 Enter 键确认。

  5. 现在 top 界面上就会显示每个进程使用的具体 CPU 编号了。

代码

https://github.com/FULLK/llkdocker/tree/main/cgroup_docker

结果

在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/543648.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Day23_学点儿JSON_定义、数据格式、和XML比较、插件

1 JSON定义 定义&#xff1a;是一种轻量级的数据交换格式 JSON是JavaScript Object Notation缩写 特点&#xff1a; 易于程序员阅读和编写。易于计算机解析和生成。其实是javascript的子集&#xff1a;原生javascript支持JSON <script type"text/javascript">…

带头节点单向非循环链表的基本操作(c语言实现)

头节点 头节点是数据结构中的一个概念&#xff0c;特别是在链表结构中。 它通常被设置为链表的第一个节点之前的一个节点&#xff0c;其数据域一般不存储链表中的实际数据&#xff0c;而它的指针域则存储指向链表中第一个实际节点的指针。 头节点的主要作用如下&#xff1a;…

Pandas相比Excel的优势是哪些?

熟悉Pandas的同学会知道&#xff0c;Pandas相当于Python中的Excel&#xff0c;都是基于二维表的进行数据处理分析&#xff0c;不同的是&#xff0c;Pandas基于代码操作数据&#xff0c;Excel是图形化的分析工具。 不少人会问Excel比Pandas更简单&#xff0c;为什么还要学习Pan…

【NLP】大语言模型基础之Transformer结构

大语言模型基础之Transformer结构 1. Transformer结构总览2. 嵌入表示层2. 注意力层3. 前馈层4. 残差连接与层归一化5. 编码器和解码器结构参考文献 Transformer是一种深度学习模型架构&#xff0c;由Vaswani等人于2017年在论文《Attention is All You Need》中首次提出。它在自…

消除 BEV 空间中的跨模态冲突,实现 LiDAR 相机 3D 目标检测

Eliminating Cross-modal Conflicts in BEV Space for LiDAR-Camera 3D Object Detection 消除 BEV 空间中的跨模态冲突&#xff0c;实现 LiDAR 相机 3D 目标检测 摘要Introduction本文方法Single-Modal BEV Feature ExtractionSemantic-guided Flow-based AlignmentDissolved…

vue控制台报错Duplicate keys detected: ‘xxxxx‘. This may cause an update error.解决方案

截图报错&#xff1a; 错误分析&#xff1a; 1、提示 Duplicate keys detected &#xff0c;翻译为&#xff1a;检测到重复的密钥 2、检查 v-for 代码&#xff0c;具体如下&#xff1a; 发现问题&#xff1a;v-for中的key是一个相同的值 解决问题 因此处使用的是测试数据&…

【示例】MySQL-4类SQL语言-DDL-DML-DQL-DCL

前言 本文主要讲述MySQL中4中SQL语言的使用及各自特点。 SQL语言总共分四类&#xff1a;DDL、DML、DQL、DCL。 SQL-DDL | Data Definition Language 数据定义语言&#xff1a;用来定义/更改数据库对象&#xff08;数据库、表、字段&#xff09; 用途 | 操作数据库 # 查询所…

MATLAB 计算点投影到平面上的坐标(59)

MATLAB 计算点投影到平面上的坐标(59) 一、算法介绍二、算法实现1.代码2.结果一、算法介绍 点投影到平面,计算投影点的坐标,下面提供MATLAB版本的计算程序,直接运行即可,内有验证数据,具体看代码即可。 二、算法实现 1.代码 代码如下(示例): % 平面上的三个点分…

力扣--图论/Prim1584.连接所有点的最小费用

思路分析&#xff1a; 初始化&#xff1a;获取点的数量&#xff0c;并创建两个辅助数组 adjvex 和 lowcost&#xff0c;分别用于记录最小生成树的边信息和每个顶点到最小生成树的距离。Prim算法循环&#xff1a;在每一次循环中&#xff0c;选择一个未加入最小生成树的顶点 k&a…

登陆qq,经常收到qq游戏中心的推送信息,关闭推送信息

手动关闭推送信息的步骤&#xff1a; 1.点开左侧游戏中心 2、在打开界面&#xff0c;点击左下角自己的头像 3、打开设置中心&#xff0c;关闭所有的推送 4、完成关闭&#xff0c;不会推送了

使用 Prometheus 在 KubeSphere 上监控 KubeEdge 边缘节点(Jetson) CPU、GPU 状态

作者&#xff1a;朱亚光&#xff0c;之江实验室工程师&#xff0c;云原生/开源爱好者。 KubeSphere 边缘节点的可观测性 在边缘计算场景下&#xff0c;KubeSphere 基于 KubeEdge 实现应用与工作负载在云端与边缘节点的统一分发与管理&#xff0c;解决在海量边、端设备上完成应…

基于SSM+Jsp+Mysql的旅游网站设计与实现

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

数据链路层(上):以太网、二层交换机和网络风暴

目录 数据链路层知识概览 数据链路层设备 1、二层交换机 2、拓展&#xff1a;二层交换机与三层交换机有啥区别&#xff1f; 3、广播风暴 4、交换机以太网接口的工作模式 数据链路层的功能 数据链路层--以太网 1、以太网是什么&#xff1f; 2、以太网地址 数据链路层知…

手把手教你安装深度学习框架PyTorch:一键式安装指南

随着人工智能和深度学习的飞速发展&#xff0c;PyTorch作为一个强大而灵活的深度学习框架&#xff0c;受到了越来越多研究者和开发者的青睐。PyTorch不仅易于上手&#xff0c;而且支持动态计算图&#xff0c;使得调试和实验变得非常方便。本文将手把手教你如何安装PyTorch&…

端口协议(爆破、未授权)

常见端口服务及攻击方向&#xff1a; 弱口令爆破 工具&#xff1a;https://github.com/vanhauser-thc/thc-hydra hydra是一个支持多协议的自动化的爆破工具。 支持的服务、协议&#xff1a; telnet ftp pop3[-ntlm] imap[-ntlm] smb smbnt http-{head|get} http-{get|post}-…

Flutter第八弹 构建拥有不同项的列表

目标&#xff1a;1&#xff09;项目中&#xff0c;数据源可能涉及不同的模版&#xff0c;显示不同类型的子项&#xff0c;类似RecycleView的itemType, 有多种类型&#xff0c;列表怎么显示&#xff1f; 2&#xff09;不同的数据源构建列表 一、创建不同的数据源 采用类似Rec…

直播弹幕系统设计

本文仅提供思路参考&#xff0c;并非完备的详细设计。 特点 其实很类似IM即时通讯系统&#xff0c;是个变种&#xff0c;本质也是在一个空间内收发消息 消息及时性强&#xff0c;过期消息意义不大用户松散&#xff0c;随时来随时走可能有瞬时大批量弹幕&#xff08;比如比赛精…

漫途水产养殖水质智能监测方案,科技助力养殖业高效生产!

随着水产养殖业的蓬勃发展&#xff0c;水质和饲料等多重因素逐渐成为影响其持续健康发展的关键因素。由于传统养殖模式因监控和调节手段不足&#xff0c;往往造成养殖环境的恶化。需要通过智能化养殖&#xff0c;调控养殖环境&#xff0c;实现养殖的精细化管理模式&#xff0c;…

【Git教程】(九)版本标签 —— 创建、查看标签,标签的散列值,将标签添加到日志输出中,判断标签是否包含特定的提交 ~

Git教程 版本标签&#xff08;tag&#xff09; 1️⃣ 创建标签2️⃣ 查看存在的标签3️⃣ 标签的散列值4️⃣ 将标签添加到日志输出中5️⃣ 判断tag是否包含特定的提交&#x1f33e; 总结 大多数项目都是用 1.7.3.2和 “ gingerbread” 这样的数字或名称来标识软件版本的。在 …

《由浅入深学习SAP财务》:第2章 总账模块 - 2.6 定期处理 - 2.6.6 年初操作:科目余额结转

2.6.6 年初操作&#xff1a;科目余额结转 在使用事务代码 FAGLB03 查询科目余额时&#xff0c;可以看到按期间的发生额清单。其中&#xff0c;第一行称为“余额结转”&#xff0c;该行的累计余额代表上年度遗留下来的余额&#xff0c;也就是年初余额。对于资产负债表科目而言&a…