物联网操作系统Zephyr入门教程4调度(scheduling)

这篇具有很好参考价值的文章主要介绍了物联网操作系统Zephyr入门教程4调度(scheduling)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

什么是调度

调度器决定哪个线程被允许在任何时间点上执行;这个线程被称为当前线程。

在不同的时间点有机会改变当前线程的身份。这些点被称为重新安排点。一些潜在的重排点是:

  • 从运行状态过渡到暂停或等待状态,例如通过k_sem_take()或k_sleep()。
  • 过渡到准备状态,例如通过k_sem_give()或k_thread_start()。
  • 处理完中断后返回到线程上下文
  • 调用k_yield()

当线程主动发起将自身转换为暂停或等待状态的操作时,它就会进入睡眠状态。

每当调度器改变了当前线程的身份,或者当前线程的执行被ISR所取代时,内核会首先保存当前线程的CPU寄存器值。当线程后来恢复执行时,这些寄存器的值会被恢复。

调度器是如何工作的?

内核的调度器选择最高优先级的就绪线程作为当前线程。当存在多个相同优先级的就绪线程时,调度器会选择等待时间最长的那个。

注意:ISR的执行要优先于线程的执行。除非中断被屏蔽了,否则当前的线程可以在任何时候被ISR取代。

内核在构建时可以选择几种就绪队列的实现方式之一。这种选择是一种权衡:

  • 代码大小
  • 恒定系数的运行时间开销
  • 当涉及到许多线程时的性能扩展

你的Kconfig文件(prj.conf)应该包含以下内容之一(或者它将默认为简单的链接列表)。

队列类型:

  • 简单链接列表准备队列(CONFIG_SCHED_DUMB)
    • 简单无序列表
    • 对于单线程来说,具有非常快的恒定时间性能
    • 代码大小非常低
    • 对有以下情况的系统有用:
      • 有限的代码大小
      • 在任何时候都有少量的线程(<=3)。
  • 红/黑树就绪队列(CONFIG_SCHED_SCALABLE)
    • 红/黑树
    • 较慢的恒定时间插入和移除开销
    • 需要额外的2Kb代码
    • 可以干净利落地扩展到成千上万的线程
    • 适用于有以下情况的系统:许多并发的可运行线程(>20个左右)。
  • 传统的多队列就绪队列(CONFIG_SCHED_MULTIQ)
    • 经典的列表阵列,每个优先级一个(最多32个优先级)。
    • 与 "dumb "调度器相比,其代码开销很小
    • 在0(1)时间内运行,恒定系数很低
    • 需要相当大的RAM预算来存储列表头
    • 与截止日期调度和SMP亲和性不兼容
    • 有少量线程的系统(但通常DUMB已经足够好了)。
  • 可扩展的wait_q实现(CONFIG_WAITQ_SCALABLE)。
  • 简单的链接列表wait_q (CONFIG_WAITQ_DUMB)

线程优先级是如何工作的?

线程的优先级是整数值,可以是负数,也可以是非负数。从数字上看,较低的优先级优先于较高的值(-5>6)。

调度器根据每个线程的优先级来区分两类线程。

  • 协作(cooperative)线程有负的优先级值。一旦它成为当前线程,合作线程就一直是当前线程,直到它执行了使其unready的动作。
  • 可抢占的线程有非负的优先权值。一旦成为当前线程,如果协作线程或更高或同等优先级的可抢占线程准备就绪,可抢占线程可以在任何时候被取代。

线程的初始优先级值可以在该线程启动后被向上或向下改变。因此,可抢占的线程有可能成为协作线程,反之亦然。

内核支持几乎无限数量的线程优先级。配置选项CONFIG_NUM_COOP_PRIORITIES和CONFIG_NUM_PREEMPT_PRIORITIES为每一类线程指定了优先级的数量,导致以下可用的优先级范围:

  • 协作性线程:(-CONFIG_NUM_COOP_PRIORITIES)到-1
  • 抢占式线程: 0到(CONFIG_NUM_PREEMPT_PRIOTIES-1)

什么是协作式时间切分?

因此,如果协作线程执行冗长的计算,它可能会导致其他线程的调度出现不可接受的延迟,包括那些更高的优先级。

物联网操作系统Zephyr入门教程4调度(scheduling)

为了克服这样的问题,协作线程可以不时地自愿放弃CPU,以允许其他线程执行。线程可以通过两种方式放弃CPU:

  • 调用k_yield()将线程放在调度器的就绪线程优先级列表的后面,然后调用调度器。所有优先级高于或等于该线程的就绪线程被允许在该线程被重新安排之前执行。如果没有这样的线程存在,调度器会立即重新安排该线程,而不进行上下文切换。
  • 调用k_sleep()使线程在指定的时间段内unready。然后,所有优先级的就绪线程都被允许执行;然而,并不保证优先级低于休眠线程的线程会在休眠线程再次变得就绪之前实际被调度。

什么是抢占式时间切分?

物联网操作系统Zephyr入门教程4调度(scheduling)

图中显示了同等优先级的线程相互抢占的情况

抢占式线程可以执行合作性的时间切分(如上所述),或者利用调度器的时间切分能力来允许其他相同优先级的线程执行。

物联网操作系统Zephyr入门教程4调度(scheduling)

调度器将时间划分为一系列的时间片,这里的时间片是以系统时钟刻度来衡量的。时间片的大小是可配置的,但这个大小可以在应用程序运行时改变。

在每个时间片结束时,调度器会检查当前线程是否是可抢占的,如果是,就隐式地代表线程调用k_yield()。这给了其他相同优先级的准备好的线程在当前线程再次被调度之前执行的机会。如果没有相同优先级的线程准备好了,那么当前线程仍然保留。

优先级高于指定限制的线程不受抢占式时间切分的影响,也不会被同等优先级的线程抢占。这允许应用程序只在处理对时间不太敏感的低优先级线程时使用抢占式时间切分。

注意:内核的时间切分算法并不能确保一组同等优先级的线程获得公平的CPU时间,因为它并不衡量线程实际得到的执行时间的多少。然

不希望在执行关键操作时被抢占的可预选线程可以通过调用k_sched_lock()指示调度器暂时将其作为协作线程。这可以防止其他线程在执行关键操作时受到干扰。

一旦关键操作完成,可抢占的线程必须调用k_sched_unlock()来恢复其正常的、可抢占的状态。

如果线程调用k_sched_lock(),并随后执行了unready的操作,调度器将把锁定的线程切换出来,并允许其他线程执行。当锁定的线程再次成为当前线程时,它的不可抢占状态将被保持。

注意:对于可抢占的线程来说,锁定调度器是一种比将其优先级改为负值更有效的防止抢占的方法。

实例:协作式时间切分

线程_1,即使优先级较低,也不会让给线程_2(直到完成)。

#include <zephyr/zephyr.h>

/* size of stack area used by each thread */
#define STACKSIZE 1024

#define PRIORITY_THREAD_1	(-1)
#define PRIORITY_THREAD_2	(-2)


K_THREAD_STACK_DEFINE(thread_1_stack_area, STACKSIZE);
static struct k_thread thread_1_data;

K_THREAD_STACK_DEFINE(thread_2_stack_area, STACKSIZE);
static struct k_thread thread_2_data;

void thread_1(void *dummy1, void *dummy2, void *dummy3)
{
	ARG_UNUSED(dummy1);
	ARG_UNUSED(dummy2);
	ARG_UNUSED(dummy3);

	int i = 0;

	printk("thread_1: thread started \n");
	k_thread_start(&thread_2_data);
	printk("thread_1: thead_2 started \n");

	while (1)
	{
		i++;
		printk("thread_1: thread loop %d\n", i);
		if (i == 3)
		{
			printk("thread_1: thread abort\n");
			k_thread_abort(&thread_1_data);
		}
	}

}

void thread_2(void *dummy1, void *dummy2, void *dummy3)
{
	ARG_UNUSED(dummy1);
	ARG_UNUSED(dummy2);
	ARG_UNUSED(dummy3);

	int i = 0;

	printk("thread_2: thread started \n");

	while (1)
	{
		i++;
		printk("thread_2: thread loop %d\n", i);
		if (i == 3)
		{
			printk("thread_2: thread abort\n");
			k_thread_abort(&thread_2_data);
		}
	}

}

void main(void)
{
	k_thread_create(&thread_1_data, thread_1_stack_area,
			K_THREAD_STACK_SIZEOF(thread_1_stack_area),
			thread_1, NULL, NULL, NULL,
			PRIORITY_THREAD_1, 0, K_FOREVER);
	k_thread_name_set(&thread_1_data, "thread_1");

	k_thread_create(&thread_2_data, thread_2_stack_area,
			K_THREAD_STACK_SIZEOF(thread_2_stack_area),
			thread_2, NULL, NULL, NULL,
			PRIORITY_THREAD_2, 0, K_FOREVER);
	k_thread_name_set(&thread_2_data, "thread_2");

	k_thread_start(&thread_1_data);
}

执行结果:

$ ninja run
[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: qemu32,+nx,+pae
SeaBIOS (version zephyr-v1.0.0-0-g31d4e0e-dirty-20200714_234759-fv-az50-zephyr)
Booting from ROM..
*** Booting Zephyr OS build zephyr-v3.4.0-554-g33b116407b03 ***
thread_1: thread started 
thread_1: thead_2 started 
thread_1: thread loop 1
thread_1: thread loop 2
thread_1: thread loop 3
thread_1: thread abort
thread_2: thread started 
thread_2: thread loop 1
thread_2: thread loop 2
thread_2: thread loop 3
thread_2: thread abort

实例:抢占式(不同优先级)

#include <zephyr/zephyr.h>

/* size of stack area used by each thread */
#define STACKSIZE 1024

#define PRIORITY_THREAD_1	(3)
#define PRIORITY_THREAD_2	(2)
#define PRIORITY_THREAD_3 	(1)


K_THREAD_STACK_DEFINE(thread_1_stack_area, STACKSIZE);
static struct k_thread thread_1_data;

K_THREAD_STACK_DEFINE(thread_2_stack_area, STACKSIZE);
static struct k_thread thread_2_data;

K_THREAD_STACK_DEFINE(thread_3_stack_area, STACKSIZE);
static struct k_thread thread_3_data;

void thread_1(void *dummy1, void *dummy2, void *dummy3)
{
	ARG_UNUSED(dummy1);
	ARG_UNUSED(dummy2);
	ARG_UNUSED(dummy3);

	int i = 0;

	printk("thread_1: thread started \n");
	k_thread_start(&thread_2_data);

	while (1)
	{
		i++;
		printk("thread_1: thread loop %d\n", i);
		if (i == 3)
		{
			printk("thread_1: thread abort\n");
			k_thread_abort(&thread_1_data);
		}
	}

}

void thread_2(void *dummy1, void *dummy2, void *dummy3)
{
	ARG_UNUSED(dummy1);
	ARG_UNUSED(dummy2);
	ARG_UNUSED(dummy3);

	int i = 0;

	printk("thread_2: thread started \n");
	k_thread_start(&thread_3_data);

	while (1)
	{
		i++;
		printk("thread_2: thread loop %d\n", i);
		if (i == 3)
		{
			printk("thread_2: thread abort\n");
			k_thread_abort(&thread_2_data);
		}
	}

}

void thread_3(void *dummy1, void *dummy2, void *dummy3)
{
	ARG_UNUSED(dummy1);
	ARG_UNUSED(dummy2);
	ARG_UNUSED(dummy3);

	int i = 0;

	printk("thread_3: thread started \n");

	while (1)
	{
		i++;
		printk("thread_3: thread loop %d\n", i);
		if (i == 3)
		{
			printk("thread_3: thread abort\n");
			k_thread_abort(&thread_3_data);
		}
	}

}

void main(void)
{
	k_thread_create(&thread_1_data, thread_1_stack_area,
			K_THREAD_STACK_SIZEOF(thread_1_stack_area),
			thread_1, NULL, NULL, NULL,
			PRIORITY_THREAD_1, 0, K_FOREVER);
	k_thread_name_set(&thread_1_data, "thread_1");

	k_thread_create(&thread_2_data, thread_2_stack_area,
			K_THREAD_STACK_SIZEOF(thread_2_stack_area),
			thread_2, NULL, NULL, NULL,
			PRIORITY_THREAD_2, 0, K_FOREVER);
	k_thread_name_set(&thread_2_data, "thread_2");

	k_thread_create(&thread_3_data, thread_3_stack_area,
			K_THREAD_STACK_SIZEOF(thread_3_stack_area),
			thread_3, NULL, NULL, NULL,
			PRIORITY_THREAD_3, 0, K_FOREVER);
	k_thread_name_set(&thread_3_data, "thread_3");

	k_thread_start(&thread_1_data);
}

执行结果:

 ninja run
[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: qemu32,+nx,+pae
SeaBIOS (version zephyr-v1.0.0-0-g31d4e0e-dirty-20200714_234759-fv-az50-zephyr)
Booting from ROM..
*** Booting Zephyr OS build zephyr-v3.4.0-554-g33b116407b03 ***
thread_1: thread started 
thread_2: thread started 
thread_3: thread started 
thread_3: thread loop 1
thread_3: thread loop 2
thread_3: thread loop 3
thread_3: thread abort
thread_2: thread loop 1
thread_2: thread loop 2
thread_2: thread loop 3
thread_2: thread abort
thread_1: thread loop 1
thread_1: thread loop 2
thread_1: thread loop 3
thread_1: thread abort

实例:抢占式(相同优先级)

include <zephyr/zephyr.h>

/* size of stack area used by each thread */
#define STACKSIZE 1024

#define PRIORITY_THREAD_1	(1)
#define PRIORITY_THREAD_2	(1)
#define PRIORITY_THREAD_3 	(1)


K_THREAD_STACK_DEFINE(thread_1_stack_area, STACKSIZE);
static struct k_thread thread_1_data;

K_THREAD_STACK_DEFINE(thread_2_stack_area, STACKSIZE);
static struct k_thread thread_2_data;

K_THREAD_STACK_DEFINE(thread_3_stack_area, STACKSIZE);
static struct k_thread thread_3_data;

void thread_1(void *dummy1, void *dummy2, void *dummy3)
{
	ARG_UNUSED(dummy1);
	ARG_UNUSED(dummy2);
	ARG_UNUSED(dummy3);
	
	int i = 0;
	printk("thread_1: thread started \n");
	

	while (1)
	{
		i++;
		printk("thread_1: thread loop %d\n", i);
		if (i == 300)
		{
			printk("thread_1: thread abort\n");
			k_thread_abort(&thread_1_data);
		}
	}

}

void thread_2(void *dummy1, void *dummy2, void *dummy3)
{
	ARG_UNUSED(dummy1);
	ARG_UNUSED(dummy2);
	ARG_UNUSED(dummy3);
	
	int i = 0;
	printk("thread_2: thread started \n");

	while (1)
	{
		i++;
		printk("thread_2: thread loop %d\n", i);
		if (i == 300)
		{
			printk("thread_2: thread abort\n");
			k_thread_abort(&thread_2_data);
		}
	}

}

void thread_3(void *dummy1, void *dummy2, void *dummy3)
{
	ARG_UNUSED(dummy1);
	ARG_UNUSED(dummy2);
	ARG_UNUSED(dummy3);
	
	int i = 0;
	printk("thread_3: thread started \n");

	while (1)
	{
		i++;
		printk("thread_3: thread loop %d\n", i);
		if (i == 300)
		{
			printk("thread_3: thread abort\n");
			k_thread_abort(&thread_3_data);
		}
	}

}

void main(void)
{
	k_thread_create(&thread_1_data, thread_1_stack_area,
			K_THREAD_STACK_SIZEOF(thread_1_stack_area),
			thread_1, NULL, NULL, NULL,
			PRIORITY_THREAD_1, 0, K_FOREVER);
	k_thread_name_set(&thread_1_data, "thread_1");

	k_thread_create(&thread_2_data, thread_2_stack_area,
			K_THREAD_STACK_SIZEOF(thread_2_stack_area),
			thread_2, NULL, NULL, NULL,
			PRIORITY_THREAD_2, 0, K_FOREVER);
	k_thread_name_set(&thread_2_data, "thread_2");

	k_thread_create(&thread_3_data, thread_3_stack_area,
			K_THREAD_STACK_SIZEOF(thread_3_stack_area),
			thread_3, NULL, NULL, NULL,
			PRIORITY_THREAD_3, 0, K_FOREVER);
	k_thread_name_set(&thread_3_data, "thread_3");

	k_thread_start(&thread_1_data);
	k_thread_start(&thread_2_data);
	k_thread_start(&thread_3_data);
}

执行结果

$ ninja run
[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: qemu32,+nx,+pae
SeaBIOS (version zephyr-v1.0.0-0-g31d4e0e-dirty-20200714_234759-fv-az50-zephyr)
Booting from ROM..
*** Booting Zephyr OS build zephyr-v3.4.0-554-g33b116407b03 ***
thread_1: thread started 
thread_1: thread loop 1
...
thread_1: thread loop 66
threthread_2: thread started 
thread_2: thread loop 1
...
thread_2: thread loop 101
ththread_3: thread started 
thread_3: thread loop 1
...
thread_3: thread loop 101
thad_1: thread loop 67
thread_1: thread loop 68
...
thread_1: thread loop 164
threadread_2: thread loop 102
thread_2: thread loop 103
...
thread_2: thread loop 197
thread_2: threaread_3: thread loop 102
thread_3: thread loop 103
...
thread_3: thread loop 197
thread_3: threa_1: thread loop 165
thread_1: thread loop 166
...
thread_1: thread loop 260
thread_1: thread lood loop 198
thread_2: thread loop 199
...
thread_2: thread loop 293
thread_2: thread loop 2d loop 198
thread_3: thread loop 199
...
thread_3: thread loop 293
thread_3: thread loop 2p 261
thread_1: thread loop 262
...
thread_1: thread loop 300
thread_1: thread abort
94
thread_2: thread loop 295
...
thread_2: thread loop 300
thread_2: thread abort
94
thread_3: thread loop 295
...
thread_3: thread loop 300
thread_3: thread abort

参考资料

  • 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
  • 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
  • python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md

什么是线程睡眠?

线程可以调用k_sleep()来延迟它的处理过程,直到指定的时间段。在线程休眠期间,CPU被放弃以允许其他准备好的线程执行。一旦指定的延迟时间过了,线程就会准备好,并有资格再次被调度。

沉睡的线程可以被另一个线程用k_wakeup()提前唤醒。这种技术有时可以用来允许辅助线程向沉睡的线程发出信号,告诉它有事情发生了,而不需要线程定义内核同步对象,比如semaphore。唤醒没有睡眠的线程是允许的,但没有任何效果。

什么是忙于等待?

线程可以调用k_busy_wait()来执行繁忙等待,在指定的时间段内延迟其处理,而不把CPU让给其他准备好的线程。

当所需的延迟时间太短,以至于调度器的上下文从当前线程切换到另一个线程,然后再返回时,通常会使用繁忙等待来代替线程睡眠。文章来源地址https://www.toymoban.com/news/detail-534145.html

协作式还是抢占式?

  • 设备驱动和其他性能关键的工作 -> 协作线程
  • 使用协作线程来实现互斥,而不需要内核对象,例如mutex。
  • 使用抢占式线程,使时间敏感的处理优先于时间不敏感的处理。

到了这里,关于物联网操作系统Zephyr入门教程4调度(scheduling)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 常见的物联网操作系统介绍

            物联网(Internet of Things,IoT)是指将各种物理设备、车辆、家用电器、工业设备等通过网络连接起来,实现数据交换和通信的技术。物联网操作系统是管理这些设备并使其能够相互通信的软件平台。以下是一些常见的物联网操作系统:         1. Amazon FreeRTOS

    2024年02月20日
    浏览(18)
  • 3DSlicer入门操作教程

    目录 简介 用户界面 数据探针 应用程序菜单 工具栏 状态栏 查看加载的数据 选择显示的数据 与视图交互 查看交叉引用 鼠标模式 3D 视图 切片视图 数据格式 DICOM 格式 NIFTI 格式 DICOM 和 NIFTI 的区别 Nrrd 格式 .mhd+raw mhd格式数据 3DSlicer数据格式 数据保存 数据格式之间的关系

    2024年02月03日
    浏览(26)
  • 8.物联网操作系统之事件标志组

    事件标志组定义 FreeRTOS事件标志组介绍 FreeRTOS事件标志组工作原理 一。事件标志组定义 信号量信号量只能实现任务与单个事件或任务间的同步。但是某些任务可能会需要与多个事件或任务进行同步,此时就可以使用事件标志组来解决。事件标志组能够实现某个任务与多个事

    2024年02月14日
    浏览(17)
  • C++ opencv操作入门教程

    OpenCV是一个功能强大的计算机视觉库,广泛应用于图像和视频处理领域。它提供了许多图像和视频处理的函数和工具,包括加载、保存、缩放、旋转、裁剪、滤波、特征检测、跟踪、分割等等。本文将介绍一些常见的OpenCV函数及其作用,适合初学者入门。 cv::Mat:代表一个n维

    2024年02月16日
    浏览(18)
  • 【ESP8266教程】零基础入门学用物联网-基础知识篇(太极创客团队)学习笔记

    提示:这里可以添加本文要记录的大概内容: 例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。 提示:以下是本篇文章正文内容,下面案例可供参考         NodeMCU上的D2引脚引出ESP82

    2024年04月29日
    浏览(19)
  • OpenCV 入门教程:膨胀和腐蚀操作

    膨胀和腐蚀是图像处理中常用的形态学操作,用于改变图像的形状和结构。在 OpenCV 中,膨胀和腐蚀是基于结构元素的像素操作,可以用于图像增强、边缘检测、图像分割等多个领域。本文将以膨胀和腐蚀操作为中心,为你介绍使用 OpenCV 进行形态学操作的基本步骤和实例。

    2024年02月13日
    浏览(16)
  • 物联网实时操作系统1RTOS简介(上)

    在大型计算机的世界里,操作系统(OS operating systems)已经存在了相当长的一段时间。最基本的操作系统可以追溯到20世纪50年代。到20世纪70年代中期,操作系统的概念、结构、功能和界面已经非常成熟。 微型系统大约在 1970年出现。在基于微处理器的设备中迅速应用操作系统

    2024年02月16日
    浏览(37)
  • 9.物联网操作系统之软件定时器

    一。软件定时器概念及应用 1.软件定时器定义         就是软件实现定时器。 2.FreeRTOS软件定时器介绍         如上图所示,Times的左边为设置定时器时间,设置方式可以为任务设置或者中断设置;Times的右边为定时器的定时响应,使用CallBack响应。  3.FreeRTOS软件定时器工作

    2024年02月14日
    浏览(20)
  • ESP32-C3入门教程 IoT篇⑧——微软云 Microsoft Azure 物联网 IoT 中心 EspAzureIoT 实战

    本文基于VS Code IDE进行编程、编译、下载、运行等操作 基础入门章节请查阅:ESP32-C3入门教程 基础篇①——基于VS Code构建Hello World 教程目录大纲请查阅:ESP32-C3入门教程——导读 ESP32固件端源码已经全部开源:小康师兄 / EspAzureIoT (gitee地址) ESP32-C3入门教程

    2024年02月06日
    浏览(58)
  • 【Git 入门教程】第六节、Git高级操作

    Git是一种非常强大的分布式版本控制系统,可以帮助开发者轻松地管理和协调代码库。在本文中,我们将介绍一些Git高级操作,包括如何管理Git仓库、标签、子模块和忽略文件。 Git提供了许多命令来管理本地Git仓库。以下是一些常用的管理命令: 初始化一个新的Git仓库: 查

    2024年02月05日
    浏览(21)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包