从0开始的操作系统手搓教程21:进程子系统的一个核心功能——简单的进程切换

目录

具体说说我们的简单RR调度

处理时钟中断处理函数

调度器 schedule

switch_to


我们下面,就要开始真正的进程切换了。在那之前,笔者想要说的是——我们实现的进程切换简单的无法再简单了——也就是实现一个超级简单的轮询调度器。

每一个进程按照一个priority作为一个拥有时间的开始,然后,我们的调度器就分配给这个进程priority个时间片,每一次时钟中断发生的时候,我们当前的进程就发生时间片剥夺减少一次,当我们的运行的elapsed_ticks的值达到了priority,也就是我们预计分配的时间片的时候,剥夺这个进程的运行资格给下一个进程。很简单吧!

所以,我们需要让进程们按照一个队列组织起来,这样才方便管理。是的,我们就按照一个最好想到的——时不时就会发生动态的插入删除的一个经典数据结构,也就是我们之前搓的一个重要的数据结构:双向链表。这里笔者就不重复谈论这个玩意了。遗忘的朋友就自行到笔者的第六章中稍微查找一下吧!

我们看看我们新Task Struct长啥样。

/*** @brief Task control block structure.** This structure represents a thread and stores its execution context.*/
typedef struct __cctaskstruct
{uint32_t *self_kstack;         // Kernel stack pointer for the threadTaskStatus status;             // Current status of the threadchar name[TASK_NAME_ARRAY_SZ]; // Thread nameuint8_t priority;              // Thread priority leveluint8_t ticks;uint32_t elapsed_ticks;list_elem general_tag;list_elem all_list_tag;uint32_t *pg_dir;uint32_t stack_magic; // Magic value for stack overflow detection
} TaskStruct;

很显然,我们多了elapsed_ticks,ticks和priority三个作为一组变量,这个是用来衡量线程的处理器时间的。

它是任务每次被调度到处理器上执行的时间嘀嗒数,也就是我们所说的任务的时间片,每次时钟中断都会将当前任务的 ticks 减1,当减到0 时就被换下处理器。 ticks 和上面的 priority 要配合使用。priority 表示任务的优先级,咱们这里优先级体现在任务执行的时 间片上,即优先级越高,每次任务被调度上处理器后执行的时间片就越长。当 ticks 递减为 0 时,就要被 时间中断处理程序和调度器换下处理器,调度器把 priority 重新赋值给 ticks,这样当此线程下一次又被调 度时,将再次在处理器上运行 ticks 个时间片。 elapsed_ticks 用于记录任务在处理器上运行的时钟嘀嗒数,从开始执行,到运行结束所经历的总时钟数。

下面多的两个,是general_tag和all_list_tag。当线程被加入到就绪队列thread_ready_list 或其他等待队列中时,我们的general_tag就会发挥作用,剩下的一个all_list_tag是一个完全被加到了所有的全局的线程管理队列中的。

另一个多的新东西是任务自己的页表,pg_dir成员。线程与进程的最大区别就是进程独享自己的地址空间,即进程有自己的页,而线程共享所在进程的地址空间,即线程无页表。如果该任务为线程,那么我们的pg_dir则为 NULL,这个时候我们就找共享的那部分页表。否则就是页表的虚拟地址。

好了,让我们看看实现吧。我们会在实现的地方上,好好聊聊。

static TaskStruct *main_thread;
static list_elem *thread_tag;
list thread_ready_list;
list thread_all_list;
​
extern void switch_to(TaskStruct *cur, TaskStruct *next);

TaskStruct* main_thread是定义主线程的PCB,咱们进入内核后一直执行的是main 函数,其实它就是一个线程,我们在后面会将其完善成线程的结构,因此为其先定义了个PCB。

调度器要选择某个线程上处理器运行的话,必然要先将所有线程收集到某个地方,这个地方就是线程就绪 队列。list thread_ready_list便是就绪队列,以后每创建一个线程就将其加到此队列中。

就绪队列中的线程都是可用于直接上处理器运行的,可有时候线程因为某些原因阻塞了,不能放在就 绪队列中,但我们得有个地方能找到它,得知道我们共创建了多少线程,为此我们创建了所有(全部)线程队列,也就是 thread_all_list。

很好,下面,我们开始梭哈新东西:首先是一个叫做current_thread函数,如你所见,current_thread是一个迫真含义的返回当前线程的PCB地址的。

TaskStruct *current_thread(void)
{uint32_t esp;asm volatile("mov %%esp, %0" : "=g"(esp));return (TaskStruct*)(esp & PG_FETCH_OFFSET);
}

PG_FETCH_OFFSET是笔者在memory文件夹下建立的一个叫做memory_settings.h中的一个定义。这个意味着是取出来页表转换的部分的高20位,如你所见:

#define PG_FETCH_OFFSET     (0xfffff000)

这个函数是咋做的呢?显然,我们要准备获取的是当前的内核栈的PCB地址,咋做的呢?我们知道,PCB是被安排在了一个页的页首,那么,直接将当前页的页首取出来不久完事了?esp & PG_FETCH_OFFSET就是在做这个事情!

kernel_thread别看只添加了一行,实际上变化非常大。我们这里强迫中断在线程调度之后是必须开启的。因为我们的任务调度机制基于时钟中断,由时钟中断这种“不可抗力”来中断所有任务 的执行,借此将控制权交到内核手中,由内核的任务调度器 schedule考虑将处理器使用权发放到某个任务的手中,下次中断再发生时,权利将再被回收,周而复始,这样便保证操作系统不会被“架空”,而且保证所有任务都有运行的机会。

static void kernel_thread(TaskFunction function, void *func_arg)
{set_intr_status(INTR_ON);function(func_arg);
}

下面这个函数,就是将我们的main函数入正了!

static void make_main_thread(void)
{main_thread = current_thread();init_thread(main_thread, "main", 31);KERNEL_ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));list_append(&thread_all_list, &main_thread->all_list_tag);
}

首先,我们在Loader中,设置了0xc009f000作为我们的ESP,然后分配好了页之后,显然,求得的PCB的地址位于页首也就是0xc009e000(牢记我们的内核栈始终在页首上!)

下一步就是init_thread,初始化我们的成员变量,这个没啥好说的。KERNEL_ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));是一个例行判断,检测一下我们当前的main_thread不应该出现在thread_all_list里,因为我们还没添加呢!

void init_thread(TaskStruct *pthread, char *name, int priority)
{k_memset(pthread, 0, sizeof(TaskStruct));k_strcpy(pthread->name, name);
​if (pthread == main_thread){pthread->status = TASK_RUNNING;}else{pthread->status = TASK_READY;}
​pthread->priority = priority;pthread->ticks = priority;pthread->elapsed_ticks = 0;pthread->pg_dir = NULL;pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);pthread->stack_magic = TASK_MAGIC;
}

看到main_thread特化了嘛,因为我们是需要对当前线程也归纳到init_thread初始化中,我们选择了对main_thread搞特殊——因为他已经在事实上运行了。所以是TASK_RUNNING。

TaskStruct *thread_start(char *name, int priority,TaskFunction function, void *func_arg)
{TaskStruct *thread = get_kernel_pages(PCB_SZ_PG_CNT);init_thread(thread, name, priority);create_thread(thread, function, func_arg);KERNEL_ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));list_append(&thread_ready_list, &thread->general_tag);KERNEL_ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));list_append(&thread_all_list, &thread->all_list_tag);return thread;
}

thread_start函数封装了这一系列流程,同时安排线程/进程进入我们的链表中,就像这样了:

具体说说我们的简单RR调度

调度器是从就绪队列 thread_ready_list 中“取出”上处理器运行的线程,所有待执行的线程都在 thread_ready_list 中,我们的调度机制很简单,就是 Round-Robin Scheduling,俗称 RR,即轮询调度,说 白了就是让候选线程按顺序一个一个地执行,咱们就是按先进先出的顺序始终调度队头的线程。注意,这 里说的是“取出”,也就是从队列中弹出,意思是说队头的线程被选中后,其结点不会再从就绪队列 thread_ready_list 中保存,因此,按照先入先出的顺序,位于队头的线程永远是下一个上处理器运行的线 程。

就绪队列 thread_ready_list 中的线程都属于运行条件已具备,但还在等待被调度运行的线程,因此 thread_ready_list 中的线程的状态都是TASK_READY。而当前运行线程的状态为 TASK_RUNNING,它仅保存在全部队列 thread_all_list 当中。

调度器 schedule 并不仅由时钟中断处理程序来调用,它还有被其他函数调用的情况,比如后面要说的函数 thread_block。

因此,在 schedule 中要判断当前线程是出于什么原因才“沦落到”要被换下处理器的地步。是线程的时间片到期了?还是线程时间片未到,但它被阻塞了,以至于不得不换下处理器?其实 这就是查看线程的状态,如果线程的状态为 TASK_RUNNING,这说明时间片到期了,将其 ticks 重新赋 值为它的优先级 prio,将其状态由TASK_RUNNING 置为TASK_READY,并将其加入到就绪队列的末尾。如果状态为其他,这不需要任何操作,因为调度器是从就绪队列中取出下一个线程,而当前运行的线程并 不在就绪队列中。

调度器按照队列先进先出的顺序,把就绪队列中的第1 个结点作为下一个要运行的新线程,将该线程的 状态置为TASK_RUNNING,之后通过函数switch_to 将新线程的寄存器环境恢复,这样新线程便开始执行。因此,完整的调度过程需要三部分的配合。

  • 时钟中断处理函数。

  • 调度器 schedule。

  • 任务切换函数 switch_to。

这样看,我们就来活了。

处理时钟中断处理函数

第一步,稍微优化一下我们的通用处理/

// Function to set up a default callback for exception interrupts
static void __def_exception_callback(uint8_t nvec)
{if (nvec == 0x27 || nvec == 0x2f){ // Ignore certain interruptsreturn;}ccos_puts("\n\n");ccos_puts("----------- Exceptions occurs! ---------------\n");ccos_puts("Man! Exception Occurs! :\n");ccos_puts(interrupt_name[nvec]); // Display the name of the interruptccos_puts("\nSee this fuck shit by debugging your code!\n");if (nvec ==PAGE_FAULT_NUM){ // If it's a page fault, print the missing addressint page_fault_vaddr = 0;asm("movl %%cr2, %0": "=r"(page_fault_vaddr)); // CR2 holds the address causing the page// faultccos_puts("And this is sweety fuckingly page fault, happened with addr is 0x");__ccos_display_int(page_fault_vaddr);ccos_puts("\n\n");}ccos_puts("----------- Exceptions Message End! ---------------\n");
​// Once inside the interrupt handler, the interrupt is disabled,// so the following infinite loop cannot be interrupted.while (1);
}

排除掉之前就说过的伪异常,我们还加进了 Pagefault 的处理。Pagefault 就是通常所说的缺页异常,它表示虚拟地址对应的物理地 址不存在,也就是虚拟地址尚未在页表中分配物理页,这样会导致 Pagefault 异常。导致 Pagefault 的虚拟 地址会被存放到控制寄存器 CR2 中,我们加入的内联汇编代码就是让 Pagefault 发生时,将寄存器 cr2 中 的值转储到整型变量page_fault_vaddr 中,并通过put_str 函数打印出来。因此,如果程序运行过程中出现异常 Pagefault 时,将会打印出导致 Pagefault 出现的虚拟地址。

之后呢,我们会为每一个独特含义的中断专门注册异常,这就是为什么笔者叫他:__def_exception_callback函数。

/* Timer interrupt handler */
static void intr_timer_handler(void) {TaskStruct *cur_thread = current_thread();
​KERNEL_ASSERT(cur_thread->stack_magic == TASK_MAGIC); // Check for stack overflow
​cur_thread->elapsed_ticks++; // Record the CPU time consumed by this threadticks++; // Total ticks since the first kernel timer interrupt, including both kernel and user mode
​if (cur_thread->ticks == 0) { // If the process time slice is exhausted, schedule a new processschedule();} else { // Decrease the remaining time slice of the current processcur_thread->ticks--;}
}
​
/*** init_system_timer initializes the system timer (PIT8253)* This function sets up the timer and configures counter 0 with the appropriate settings.*/
void init_system_timer(void)
{// Print message indicating the timer is being initializedverbose_ccputs("   timer is initing...\n");
​// Configure the timer's counter 0 with appropriate settings// The function frequency_set will configure the counter 0 port, set the mode,// and initialize it with the value defined in COUNTER0_VALUE.frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);register_intr_handler(SCHED_INTERRUPT_CALLBACK_N, intr_timer_handler);// Print message indicating the timer has been successfully initializedverbose_ccputs("   timer is initing! done!\n");
}
​

很简单吧,就是如此!我们定期检查一下栈有没有出问题。然后更新一下ticks。如果触发了cur_thread->ticks没了,那就是到点尝试切换了。

调度器 schedule

/* Task scheduling function */
void schedule(void)
{KERNEL_ASSERT(get_intr_status() == INTR_OFF);
​TaskStruct *cur = current_thread(); // Get the current running threadif (cur->status == TASK_RUNNING){ // If the current thread is still runningKERNEL_ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));list_append(&thread_ready_list,&cur->general_tag); // Add it to the ready listcur->ticks =cur->priority;        // Reset the thread's ticks based on prioritycur->status = TASK_READY; // Set the thread status to ready}
​KERNEL_ASSERT(!list_empty(&thread_ready_list));thread_tag = NULL; // Clear the thread_tag/* Pop the first thread from the ready list to schedule */thread_tag = list_pop(&thread_ready_list);TaskStruct *next = elem2entry(TaskStruct, general_tag, thread_tag);next->status = TASK_RUNNING;
​switch_to(cur, next); // Switch to the next thread
}

我们检查一下:有没有关中断,不然的话多少有点危险,这样调度本身会被打断,不知道跑哪里去了。接下来分两种情况来考虑,如果当前线程 cur 的时间片到期了,就将其通过 list_append 函数重新加入 到就绪队列 thread_ready_list。由于此时它的时间片 ticks 已经为 0,为了下次运行时不至于马上被换下处理器,将 ticks 的值再次赋值为它的优先级 prio,最后将 cur 的状态 status 置 为 TASK_READY。

如果当前线程 cur 并不是因为时间片到期而被换下处理器,肯定是由于某种原因被阻塞了(比如对 0 值的信号量进行 P 操作就会让线程阻塞,到同步机制时会介绍),这时候不需要处理就绪队列,因为当前 运行线程并不在就绪队列中,咱们下面来看当前运行的线程是如何从就绪队列中“出队”的。

我们尚未实现idle 线程,因此有可能就绪队列为空,为避免这种无线程可调度的情况,暂时用“KERNEL_ASSERT(!list_empty(&thread_ready_list))”来保障。

接下来通过“thread_tag = list_pop(&thread_ready_list)”从就绪队列中弹出一个可用线程并存入thread_tag。

注意,thread_tag 并不是线程,它仅仅是线程PCB 中的general_tag 或all_list_tag,要获得线程的信息,必须 将其转换成PCB 才行,因此我们用到了宏elem2entry

// Macro to calculate the offset of a member within a struct type
#define offset(struct_type, member) (int)(&((struct_type *)0)->member)
​
// Macro to convert an element pointer to the corresponding struct type pointer
#define elem2entry(struct_type, struct_member_name, elem_ptr) \(struct_type *)((int)elem_ptr - offset(struct_type, struct_member_name))

在这呢,之前就说过了。贴过来看一眼而已。通过 elem2entry 获得了新线程的 PCB 地址,将其赋值给 next,紧接着通过“next-> status = TASK_RUNNING”将新线程的状态置为 TASK_RUNNING,这表示新线程next 可以上处 理器了,于是准备切换寄存器映像,这是通过调用 switch_to 函数完成的,调用形式为“switch_to(cur, next)”,意为将线程 cur 的上下文保护好,再将线程 next 的上下文装载到处理器,从而完成了任务切换。

switch_to

完整的程序就也因此分为两部分,一部分是做重要工作的内核级代码,另一部分就是做普通工作的用户级代码。所以,“完整的程序=用户代码+内核代码”。而这个完整的程序就是我们所说的任务,也就是线程或进程。换而言之,我们的Application是离不开我们的内核服务的。

当处理器处于低特权级下执行用户代码时我们称之为用户态,当处理器进入高特权级执行到内核代码时, 我们称之为内核态,当处理器从用户代码所在的低特权级过渡到内核代码所在的高特权级时,这称为陷入 内核。因此一定要清楚,无论是执行用户代码,还是执行内核代码,这些代码都属于这个完整的程序,即 属于当前任务,并不是说当前任务由用户态进入内核态后当前任务就切换成内核了,这样理解是不对的。

任务与任务的区别在于执行流一整套的上下文资源,这包括寄存器映像、地址空间、IO 位图等。拥有这些资源才称 得上是任务。因此,处理器只有被新的上下文资源重新装载后,当前任务才被替换为新的任务,这才叫任务切换。当任务进入内核态时,其上下文资源并未完全替换,只是执行了“更厉害”的代码。

每个任务都有个执行流,这都是事先规划好的执行路径,按道理应该是从头执行到结束。不过实际的情况是执行流经常被临时改道,突然就执行了规划外的指令,这在多任务系统中是很正常的,因为操作系 统是由中断驱动的,每一次中断都将使处理器放下手头的工作转去执行中断处理程序。为了在中断处理完成后能够恢复任务原有的执行路径,必须在执行流被改变前,将任务的上下文保护好。

执行流被改变后,在其后续的执行过程中还可能会再次发生被改变“流向”的情况,也就是说随着执行的深入,这种改变的 深度很可能是多层的。如果希望将来能够返回到本层的执行流,依然要在改变前保护好本层的上下文。总之,凡是涉及到执行流的改变,不管被改变了几层,为了将来能够恢复到本层继续执行,必须在改变发生前将 本层执行流的上下文保护好。因此,执行流被改变了几层就要做几次上下文保护。

在咱们的系统中,任务调度是由时钟中断发起,由中断处理程序调用 switch_to 函数实现的。假设当前任 务在中断发生前所处的执行流属于第一层,受时钟中断 的影响,处理器会进入中断处理程序,这使当前的任务 执行流被第一次改变,因此在进入中断时,我们要保护好第一层的上下文,即中断前的任务状态。之后在内核中执行中断处理程序,这属于第二层执行流。当中断处 理程序调用任务切换函数 switch_to 时,当前的中断处 理程序又要被中断,因此要保护好第二层的上下文,即中断处理过程中的任务状态。

因此,咱们系统中的任务调度,过程中需要保护好任务两层执行流的上下文,这分两部分来完成。 第一部分是进入中断时的保护,这保存的是任务的全部寄存器映像,也就是进入中断前任务所属第一层的状态,这些寄存器映像相当于任务中用户代码的上下文

当把这些寄存器映像恢复到处理器中后,任务便完全退出中断,继续执行自己的代码部分。换句话说,当恢复寄存器后,如果此任务是用户进程,任务就完全恢复为用户程序继续在用户态下执行,如果此任务是内核线程,任务就完全恢复为另一段被中断执行的内核代码,依然是在内核态下运行。

第二部分是保护内核环境上下文,根据ABI,除esp 外,只保护esi、edi、ebx 和ebp 这4 个寄存器就够了。这4 个寄存器映像相当于任务中的内核代码的上下文,也就是第二层执行流,此部分只负责恢复第二层的执行 流,即恢复为在内核的中断处理程序中继续执行的状态。下面需要结合咱们的实现来解释为什么这么做了。

[bits 32]
section .text
global switch_to
switch_to:; The return address is located here on the stack.push esipush edipush ebxpush ebp
​mov eax, [esp + 20]         ; Get the parameter `cur` from the stack, cur = [esp + 20].mov [eax], esp              ; Save the stack pointer (esp) into the `self_kstack` field of the task_struct.; The `self_kstack` field is at offset 0 in the task_struct,; so we can directly store 4 bytes at the beginning of the thread structure.
​
;------------------  Above is backing up the current thread's context. Below is restoring the next thread's context. ----------------mov eax, [esp + 24]         ; Get the parameter `next` from the stack, next = [esp + 24].mov esp, [eax]              ; The first member of the PCB is the `self_kstack` member, which records the top of the 0-level stack.; It is used to restore the 0-level stack when the thread is scheduled on the CPU.; The 0-level stack contains all the information of the process or thread, including the 3-level stack pointer.pop ebppop ebxpop edipop esiret                         ; Return to the return address mentioned in the comment below `switch_to`.; If not entered via an interrupt, the first execution will return to `kernel_thread`.

switch_to 的操作对象是线程栈 struct thread_stack,对栈中的返回地址及参数的设置在上面呢。上下文的保护工作分为两部分,第一部分用于恢复中断前的状态,这相对好理解。咱们的函数switch_to 完成的是第二部分,用于任务切换后恢复执行中断处理程序中的后续代码。

注意,不要误以为此时恢复的寄存器映像是在上面刚刚保存过的那些寄存器。你仔细看!我们是将esp切换到了我们的next所指向的地址上去的。所以没有恢复,不过是保存完之后,调用曾经被触发schedule而保存的内容而已!!!

#include "include/library/ccos_print.h"
#include "include/kernel/init.h"
#include "include/library/kernel_assert.h"
#include "include/memory/memory.h"
#include "include/thread/thread.h"
​
void thread_a(void* args);
void thread_b(void* args);
int main(void)
{init_all();thread_start("k_thread_a", 31, thread_a, "argA "); thread_start("k_thread_b", 16, thread_b, "argB "); interrupt_enabled();// code is baddy! we need LOCK!!!!while(1);
}
​
void thread_a(void* args){char* arg = (char*)args;while(1){ccos_puts(arg);}
}
​
void thread_b(void* args){char* arg = (char*)args;while(1){ccos_puts(arg);}
}

上电试一下:

代码开始运行良好,后面崩溃了,发生什么呢?请看后会分解!

代码:CCOperateSystem/Documentations/8_Thread_Management/8.2_Implement_schedule_code at main · Charliechen114514/CCOperateSystemhttps://github.com/Charliechen114514/CCOperateSystem/tree/main/Documentations/8_Thread_Management/8.2_Implement_schedule_code

下一篇

从0开始的操作系统手搓教程22:锁让我们的并发变得更加安全-CSDN博客https://blog.csdn.net/charlie114514191/article/details/146049147

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql新手常见问题解决方法总结

1. 安装与配置问题 1.1 无法安装MySQL Server MySQL Server安装失败是新手常见的问题之一,以下是具体原因及解决方案: 系统要求不满足:MySQL对操作系统有最低版本要求,如Windows 7 SP1及以上、macOS 10.13及以上。若系统版本过…

数字组合(信息学奥赛一本通-1291)

【题目描述】 有n个正整数,找出其中和为t(t也是正整数)的可能的组合方式。如:n5,5个数分别为1,2,3,4,5,t5;那么可能的组合有514和523和55三种组合方式。 【输入】 输入的第一行是两个正整数n和t,用空格隔开&#xff0c…

搜索引擎(基于java在线文档)

背景: 基于java文档的搜索引擎,可以输入搜索词,然后就可以查询出与搜索词相关的文档。该项目的最主要的工作是要构建索引,就是正排和倒排索引。正排索引:根据文档id获取到文档;倒排索引:根据搜…

【每日学点HarmonyOS Next知识】web滚动、事件回调、selectable属性、监听H5内部router、Grid嵌套时高度设置

【每日学点HarmonyOS Next知识】web滚动、事件回调、selectable属性、监听H5内部router、Grid嵌套时高度设置 1、HarmonyOS WebView加载url无法滚动? scroll 里面嵌套webView,demo参考: // xxx.ets import web_webview from ohos.web.webv…

Flink性能指标详解MetricsAnalysis

文章目录 Flink 组成1.JobManager2.TaskManager3.ResourceManager4.Dispatcher5.Client6. Env JobManager MetricsTaskManager Metrics Flink 组成 1.JobManager 管理任务 作业调度:负责接收和调度作业,分配任务到 TaskManager。资源管理:…

Flutter底层实现

1. Dart 语言 Dart 是 Flutter 的主要编程语言。Dart 设计之初就是为了与 JavaScript 兼容,并且可以编译为机器代码运行。Dart 提供了一些特性,如异步支持(通过 async 和 await),这使得编写高效的网络请求和复杂动画变…

< 自用文儿 > CertBot 申请 SSL 证书 使用 challenge 模式 避开防火墙的阻挡

环境: 腾讯 VPS 腾讯会向你销售 SSL , 这个本是免费的。CertBot 默认申请证书要用到 80 端口,会蹭边什么什么条款,备案法律来阻止80端口的通讯,没有网站也一样被阻拦。 通过腾讯买的域名: bestherbs.cn …

【AI】【Unity】关于Unity接入DeepseekAPI遇到的坑

前言 由于deepseek网页端在白天日常抽风,无法正常的使用,所以调用API就成了目前最好的选择,尤其是Deepseek的API价格低得可怕,这不是和白送的一样吗!然后使用过很多本地部署接入API的方式,例如Chatbox、Pa…

【微知】Mellanox驱动中to是什么?有哪些超时时间?(time out,心跳2s,reset 1分钟)

to是tout缩写,tout是time out 单位是毫秒。 static const u32 tout_def_sw_val[MAX_TIMEOUT_TYPES] {[MLX5_TO_FW_PRE_INIT_TIMEOUT_MS] 120000, # 2min。预初始化的总超时时间[MLX5_TO_FW_PRE_INIT_ON_RECOVERY_TIMEOUT_MS] 7200000, #设备恢复过程中的固件预初…

linux | Vim 命令快捷操作

注:本文为过去的 “vim 使用笔记”。 跳转命令 跳转命令 #:向前查找光标当前所在单词,并跳转到该单词的上一个出现位置。*:向后查找光标当前所在单词,并跳转到该单词的下一个出现位置。 行内跳转 0:跳转…

树莓派3B+的初步使用

树莓派3B的初步使用 一、安装使用树莓派系统1.将系统写入SD卡2.登录树莓派系统3.用C和Python编译运行hello world 一、安装使用树莓派系统 1.将系统写入SD卡 首先,准备至少16GB大小的SD卡以便装入树莓派系统,将SD卡插入读卡器后连接电脑准备给SD卡写入…

基于Windows11的DockerDesktop安装和布署方法简介

基于Windows11的DockerDesktop安装和布署方法简介 一、下载安装Docker docker 下载地址 https://www.docker.com/ Download Docker Desktop 选择Download for Winodws AMD64下载Docker Desktop Installer.exe 双点击 Docker Desktop Installer.exe 进行安装 测试Docker安装是…

文档处理控件Aspose.Total教程:使用 C# 将 Obsidian Markdown 转换为 OneNote

Obsidian 是一款广泛使用的基于 Markdown 的笔记应用程序。它提供了一种强大而有效的方式来构建和组织想法。用户可以无缝地连接他们的想法,提高清晰度和工作效率。另一方面,OneNote 是 Microsoft 的一款功能强大的笔记应用程序。它还可以帮助用户组织他…

第5章:vuex

第5章:vuex 1 求和案例 纯vue版2 vuex工作原理图3 vuex案例3.1 搭建vuex环境错误写法正确写法 3.2 求和案例vuex版细节分析源代码 4 getters配置项4.1 细节4.2 源代码 5 mapState与mapGetters5.1 总结5.2 细节分析5.3 源代码 6 mapActions与mapMutations6.1 总结6.2…

迷你世界脚本对象库接口:ObjectLib

对象库接口:ObjectLib 迷你世界 更新时间: 2023-04-26 20:21:09 具体函数名及描述如下: 序号 函数名 函数描述 1 getAreaData(...) 获取区域数据 2 getPositionData(...) 获取位置数据 3 getLivingData(...) 获取生物数据 4 getItemDat…

测试是如何跟进和管理 bug

测试在跟进和管理 Bug定位精确、问题反馈及时、修复闭环高效 三大关键环节中起到了至关重要的作用。Bug定位精确 是整个流程的基础,通过详细记录和复现问题,可以帮助开发团队迅速找出缺陷根源;而及时有效的反馈机制则确保问题不会被遗漏&…

运动控制卡--固高实用

目录 组件 配置参数 编程控制 组件 我手头有固高卡,记录使用。 用运动控制卡 伺服(步进)电机搭建一个运动控制系统,主要包括:1、控制器 2、端子板 1、控制器 2、端子板 3、伺服(步进)…

2025年能源工作指导意见

2025年是“十四五”规划收官之年,做好全年能源工作意义重大。为深入贯彻落实党中央、国务院决策部署,以能源高质量发展和高水平安全助力我国经济持续回升向好,满足人民群众日益增长的美好生活用能需求,制定本意见。 一、总体要求…

键值对(C++实现)

目录 键值对的定义 键值对的底层实现 键值对的作用 键值对的使用 对键值对中的值的搜索 一、键值对的定义 键值对(Key-Value Pair)是一种数据结构,用于存储和表示两个相关联的值。在键值对中,一个值被关联到一个唯一的键上&…

使用 Docker 部署 RabbitMQ 并实现数据持久化

非常好!以下是一份完整的 Docker 部署 RabbitMQ 的博客文档,包含从安装到问题排查的详细步骤。你可以直接将其发布到博客中。 使用 Docker 部署 RabbitMQ 并实现数据持久化 RabbitMQ 是一个开源的消息队列系统,广泛应用于分布式系统中。使用…