ChCore-lab3

news/2025/2/7 19:07:00/文章来源:https://www.cnblogs.com/mumujun12345/p/18583848

lab 3: 进程与线程

前言:timeout情况不再赘述。

有没有感到编译时间已经长到难以忍受?是的,作者在第一次编译的时候甚至深受编译的困扰(长达10分钟!)评分的时候,大家也想要很快地看到绿色的100分,因此,作者提供一个歪招给大家参考(慎用!)

Scripts/kernel.mk中的grade处,注释掉make distclean
慎用!慎用!

实验报告仅供个人参考,不对正确性负责!

Contents

用户进程是操作系统对在用户模式运行中的程序的抽象。在Lab 1 和Lab 2 中,已经完成了内核的启动和物理内存的管理,以及一个可供用户进程使用的页表实现。现在,我们将一步一步支持用户态程序的运行。 本实验包括五个部分:

  1. 代码导读,了解Chcore微内核的核心机制以及用户态和内核态是如何进行交互的。
  2. 线程管理: 支持创建第一个用户态进程和线程,分析代码如何从内核态切换到用户态。
  3. 异常处理: 完善异常处理流程,为系统添加必要的异常处理的支持。
  4. 系统调用:正确处理部分系统调用,保证用户程序的正常输出。
  5. 用户态程序编写:编写一个简单用户程序,使用提供的 ChCore libc 进行编译,并加载至内核镜像中。

3.1 代码导读

首先我们需要结合lab2中的main函数来了解内核初始化的过程。本次代码导读主要聚焦从main函数开始自上而下讲解Lab2 Lab3内核态的资源管理机制以及用户态和内核态的互相调用。

内核初始化

/** @boot_flag is boot flag addresses for smp;* @info is now only used as board_revision for rpi4.*/
void main(paddr_t boot_flag, void *info)
{u32 ret = 0;/* Init big kernel lock */ret = lock_init(&big_kernel_lock);kinfo("[ChCore] lock init finished\n");BUG_ON(ret != 0);/* Init uart: no need to init the uart again */uart_init();kinfo("[ChCore] uart init finished\n");/* Init per_cpu info */init_per_cpu_info(0);kinfo("[ChCore] per-CPU info init finished\n");/* Init mm */mm_init(info);kinfo("[ChCore] mm init finished\n");void lab2_test_buddy(void);lab2_test_buddy();void lab2_test_kmalloc(void);lab2_test_kmalloc();void lab2_test_page_table(void);lab2_test_page_table();
#if defined(CHCORE_KERNEL_PM_USAGE_TEST)void lab2_test_pm_usage(void);lab2_test_pm_usage();
#endif/* Mapping KSTACK into kernel page table. */map_range_in_pgtbl_kernel((void*)((unsigned long)boot_ttbr1_l0 + KBASE), KSTACKx_ADDR(0),(unsigned long)(cpu_stacks[0]) - KBASE, CPU_STACK_SIZE, VMR_READ | VMR_WRITE);/* Init exception vector */arch_interrupt_init();timer_init();kinfo("[ChCore] interrupt init finished\n");/* Enable PMU by setting PMCR_EL0 register */pmu_init();kinfo("[ChCore] pmu init finished\n");/* Init scheduler with specified policy */
#if defined(CHCORE_KERNEL_SCHED_PBFIFO)sched_init(&pbfifo);
#elif defined(CHCORE_KERNEL_RT)sched_init(&pbrr);
#elsesched_init(&rr);
#endifkinfo("[ChCore] sched init finished\n");init_fpu_owner_locks();/* Other cores are busy looping on the boot_flag, wake up those cores */enable_smp_cores(boot_flag);kinfo("[ChCore] boot multicore finished\n");#ifdef CHCORE_KERNEL_TESTkinfo("[ChCore] kernel tests start\n");run_test();kinfo("[ChCore] kernel tests done\n");
#endif /* CHCORE_KERNEL_TEST */#if FPU_SAVING_MODE == LAZY_FPU_MODEdisable_fpu_usage();
#endif/* Create initial thread here, which use the `init.bin` */create_root_thread();kinfo("[ChCore] create initial thread done\n");kinfo("End of Kernel Checkpoints: %s\n", serial_number);/* Leave the scheduler to do its job */sched();/* Context switch to the picked thread */eret_to_thread(switch_context());
}

以下为Chcore内核初始化到运行第一个用户线程的主要流程图

flowchart TD lock["lock_init() 锁初始化"] uart["uart_init() uart初始化"] cpu["init_per_cpu_info() cpu结构体初始化"] mm["mm_init() 内存管理初始化"] sched["sched_init() 调度初始化"] fpu["init_fpu_owner_locks() fpu初始化"] root_thread["create_root_thread() 创建原始线程"] eret["eret_to_thread()"] pmo["create_pmo() pmo创建"] vmspace["vmspace_map_range() vm映射"] cap_group["create_root_cap_group()"] thread_alloc["thread_alloc"] memory_mapping["memory_mapping"] subgraph main lock-->uart-->cpu-->mm-->sched-->fpu-->root_thread-.->eret endsubgraph thread_init root_thread-->pmo-->vmspace-->cap_group-->thread_alloc-->memory_mapping-->eret end

我们在Lab2中主要完成mm_init以及内存管理器与vmspace和pmo的互联,现在我们再从第一个线程创建的数据流来梳理并分析
Chcore微内核的资源管理模式。

内核对象管理

在Chcore中所有的系统资源都叫做object(对象),用面向对象的方法进行理解的话,object即为不同内核对象例如vmspace, pmo, thread(等等)的父类,
Chcore通过能力组机制管理所有的系统资源,能力组本身只是一个包含指向object的指针的数组

  1. 所有进程/线程都有一个独立的能力组,拥有一个全局唯一ID (Badge)
  2. 所有对象(包括进程或能力组本身)都属于一个或多个能力组当中,也就是说子进程与线程将属于父进程的能力组当中,在某个能力组的对象拥有一个能力组内的能力ID(cap)。
  3. 对象可以共享,即单个对象可以在多个能力组中共存,同时在不同cap_group中可以有不同的cap
  4. 对所有对象的取用和返还都使用引用计数进行追踪。当引用计数为0后,当内核垃圾回收器唤醒后,会自动回收.
  5. 能力组内的能力具有权限,表明该能力是否能被共享(CAP_RIGHT_COPY)以及是否能被删除(CAP_RIGHT_REVOKE)

img

struct object {u64 type;u64 size;/* Link all slots point to this object */struct list_head copies_head;/* Currently only protect copies list */struct lock copies_lock;/** refcount is added when a slot points to it and when get_object is* called. Object is freed when it reaches 0.*/volatile unsigned long refcount;/** opaque marks the end of this struct and the real object will be* stored here. Now its address will be 8-byte aligned.*/u64 opaque[];
};
const obj_deinit_func obj_deinit_tbl[TYPE_NR] = {[0 ... TYPE_NR - 1] = NULL,[TYPE_CAP_GROUP] = cap_group_deinit,[TYPE_THREAD] = thread_deinit,[TYPE_CONNECTION] = connection_deinit,[TYPE_NOTIFICATION] = notification_deinit,[TYPE_IRQ] = irq_deinit,[TYPE_PMO] = pmo_deinit,[TYPE_VMSPACE] = vmspace_deinit,
#ifdef CHCORE_OPENTRUSTEE[TYPE_CHANNEL] = channel_deinit,[TYPE_MSG_HDL] = msg_hdl_deinit,
#endif /* CHCORE_OPENTRUSTEE */[TYPE_PTRACE] = ptrace_deinit
};
void *obj_alloc(u64 type, u64 size)
{u64 total_size;struct object *object;total_size = sizeof(*object) + size;object = kzalloc(total_size);if (!object)return NULL;object->type = type;object->size = size;object->refcount = 0;/** If the cap of the object is copied, then the copied cap (slot) is* stored in such a list.*/init_list_head(&object->copies_head);lock_init(&object->copies_lock);return object->opaque;
}
void __free_object(struct object *object)
{
#ifndef TEST_OBJECTobj_deinit_func func;if (object->type == TYPE_THREAD)clear_fpu_owner(object);/* Invoke the object-specific free routine */func = obj_deinit_tbl[object->type];if (func)func(object->opaque);
#endifBUG_ON(!list_empty(&object->copies_head));kfree(object);
}

所有的对象都有一个公共基类,并定义了虚构函数列表,当引用计数归零即完全被能力组移除后内核会执行deinit代码完成销毁工作。

根据上面的描述,梳理根进程创建以及普通进程创建的异同,梳理出创建进程的方式。

相同点:

  1. 首先,我们需要为从用户态的进程分配一个能力组,提供给一个全局的id。那么就需要分配一个object,将这个类型选择为cap_group.
  2. 初始化我们的cap_group,创建PCB。
  3. 需要注意的是,所有的进程相关信息都通过object来进行抽象。因此vmspace也是通过分配object分配而来的。
  4. 创建进程的线程。在线程内,为线程分配内存对象PMO和初始执行环境,分配虚拟地址空间,处理器上下文,内核栈。

不同点:

  1. 创建根进程的全局id和能力(capability)和其他进程的相对应id和能力不同。
  2. 根进程所具有的虚拟地址与普通进程不相同。

用户态构建

我们在Lab1的代码导读阶段说明了kernel目录下的代码是如何被链接成内核镜像的,我们在内核镜像链接中引入了procmgr这个预先构建的二进制文件。在Lab3中,我们引入了用户态的代码构建,所以我们将procmgr的依赖改为使用用户态的代码生成。下图为具体的构建规则图。

flowchart LR topcmake["CMakeLists.txt"] chcorelibc["chcore-libc"] libcso["libc.so"] procmgr["procmgr"] ramdisk["ramdisk"] ramdisk_cpio["ramdisk.cpio"] tmpfs["ramdisk/tmpfs.srv"] procmgr_tool["procmgr_tool"] kernel["kernel"] kernel_img["kernel.img"]subgraph libcchcorelibc-->|autotools|libcso endsubgraph system_services ramdisk-->|cpio|ramdisk_cpio ramdisk_cpio-->tmpfs tmpfs-->procmgr libcso-->procmgr procmgr-->procmgr_tool procmgr_tool-->procmgr endtopcmake-->system_services topcmake-->libc procmgr-->kernel_img kernel-->kernel_img

procmgr是一个自包含的ELF程序,其代码在procmgr中列出,其主要包含一个ELF执行器以及作为Chcore微内核的init程序启动,其构建主要依赖于fsm.srv以及tmpfs.srv,其中fsm.srv为文件系统管理器其扮演的是虚拟文件系统的角色用于桥接不同挂载点上的文件系统的实现,而tmpfs.srv则是Chcore的根文件系统其由ramdisk下面的所有文件以及构建好libc.so所打包好的ramdisk.cpio构成。当构建完tmpfs.srv后其会跟libc.so进行动态链接,最终tmpfs.srv以及fsm.srv会以incbin脚本的形式以二进制的方式被连接至procmgr的最后。在构建procmgr的最后一步,cmake会调用read_procmgr_elf_toolprocmgr这个ELF文件的缩略信息粘贴至procmgr之前。此后procmgr也会以二进制的方式进一步嵌套进入内核镜像之后,最终会在create_root_thread的阶段通过其elf符号得以加载。 最终,Chcore的Kernel镜像的拓扑结构如下

flowchart LR kernel_img("kernel.img") kernel_objects("kernel/*.o") procmgr("procmgr") chcore_libc("libc.so") ramdisk("ramdisk") ramdisk_cpio("ramdisk.cpio") tmpfs("tmpfs.srv") fsm("fsm.srv") kernel_img-->kernel_objects kernel_img-->procmgr procmgr-->fsm procmgr-->tmpfs tmpfs-->ramdisk_cpio ramdisk_cpio-->ramdisk ramdisk_cpio-->chcore_libc

3.2 线程管理

权利组创建(我觉得应该是权力组)

在AArch64体系结构中,我们拥有从低到高4个异常级别EL0,EL1,EL2,EL3.在Chcore中采用两个特权级别,EL0,EL1。后者为内核模式。

/kernel目录下的代码运行于内核级别,也就是EL1。在/user目录下的代码运行于用户态,也就是EL0。我们在ChCore中的第一个进程为procmgr,也接受cap_groupcapability的管理。在创建该进程后,再进行内核态向用户态的切换。

创建用户程序至少需要包括创建对应的 cap_group、加载用户程序镜像并且切换到程序。在内核完成必要的初始化之后,内核将会跳转到创建第一个用户程序的操作中,该操作通过调用 create_root_thread 函数完成,本函数完成第一个用户进程的创建,其中的操作包括从procmgr镜像中读取程序信息,调用create_root_cap_group创建第一个 cap_group 进程,并在 root_cap_group 中创建第一个线程,线程加载着信息中记录的 elf程序(实际上就是procmgr系统服务)。此外,用户程序也可以通过 sys_create_cap_group 系统调用创建一个全新的 cap_group.

练习题1:
kernel/object/cap_group.c 中完善 sys_create_cap_groupcreate_root_cap_group 函数。在完成填写之后,你可以通过 Cap create pretest 测试点。


Hint
阅读kernel/object/capability.c中的各个与cap机制相关的函数以及相关参考文档。

我们首先先看一下我们的能力组和对象的相关结构。

img

我们可以看见,object(对象)可以看作是一切系统资源的基类,其中在object_type中有着其派生出来的类别,包括能力组,线程,连接,提示,中断请求(irq),物理内存对象(pmo),虚拟地址空间等。object对象中,type指明了该对象的类型,size是大小,copies_head提供了从链表中获取该资源的方式(可以说是后门hhh),copies_lock是锁结构,用于保证多线程访问和共享安全。refcount可以用于垃圾回收装置(gc),而真正的内容存储在64位的opaque内存中。

接下来我们来看cap_group的相关定义。

img

首先是有关object的链表结构的定义,这里定义了能力组指向的链表,以及链表的每个槽slot指向的object。object_slot内储存着slot的id,所属于的能力组,所含有的object,索引方式copies,以及对象所拥有的权力(不是“利”)。在kernel/user-include/uapi/types.h中的注释中我们可以发现:该权力分为两类,一种是对象独有的权力,一种是普遍的权力。这会在后面有所体现。

img

img

接下来就是有关能力组的定义。如上图所示,首先,能力组包含了slot_table. 其次,thread_cnt记录了该能力组所拥有的线程的数目。badge是全局的id,由procmgr设置,也可以视为内核态的id。pid则是用户态中该进程的id。

我们接着来理清我们创建进程所需要的过程:

  1. 首先,我们需要为进程分配一个能力组,并且为该能力组提供给一个全局的id。那么就需要分配一个object(所有进程,线程和能力组都是对象的派生),将这个类型选择为cap_group.
  2. 调用初始化函数,将object初始化我们的cap_group。
  3. 为我们的进程,也就是这个能力组分配虚拟空间。
  4. 创建线程。一个进程至少有一个以上的线程,相关的上下文信息和PMO通过线程进行管理。

参考我们的capability.c,具体的部分可以参见注释。

cap_t sys_create_cap_group(unsigned long cap_group_args_p)
{struct cap_group *new_cap_group;struct vmspace *vmspace;cap_t cap;int r;struct cap_group_args args = {0};r = hook_sys_create_cap_group(cap_group_args_p);if (r != 0)return r;if (check_user_addr_range((vaddr_t)cap_group_args_p,sizeof(struct cap_group_args))!= 0)return -EINVAL;r = copy_from_user(&args, (void *)cap_group_args_p, sizeof(struct cap_group_args));if (r) {return -EINVAL;}/* cap current cap_group *//* LAB 3 TODO BEGIN *//* Allocate a new cap_group object *//** 为我们的能力组分配对应类型的对象。* 我们的函数在capability.c: * void *obj_alloc(u64 type, u64 size)*/new_cap_group = obj_alloc(TYPE_CAP_GROUP, sizeof(*new_cap_group));/* LAB 3 TODO END */if (!new_cap_group) {r = -ENOMEM;goto out_fail;}/* LAB 3 TODO BEGIN *//* initialize cap group from user*/// 初始化我们的能力组。为进程分配用户态id。// 函数原型:注意最后一个需要传入引用。// __maybe_unused static int cap_group_init_user(struct cap_group *cap_group, unsigned int size, struct cap_group_args *args)cap_group_init_user(new_cap_group, BASE_OBJECT_NUM, &args);new_cap_group->pid = args.pid;/* LAB 3 TODO END */// 在能力组内分配槽位。如果没有足够的槽位则前往新的能力组。cap = cap_alloc(current_cap_group, new_cap_group);if (cap < 0) {r = cap;goto out_free_obj_new_grp;}/* 1st cap is cap_group */// 分配自身能力capability,放入槽表中的第一个槽。if (cap_copy(current_thread->cap_group,new_cap_group,cap,CAP_RIGHT_NO_RIGHTS,CAP_RIGHT_NO_RIGHTS)!= CAP_GROUP_OBJ_ID) {kwarn("%s: cap_copy fails or cap[0] is not cap_group\n",__func__);r = -ECAPBILITY;goto out_free_cap_grp_current;}/* 2st cap is vmspace *//* LAB 3 TODO BEGIN */// 分配虚拟空间并且放入能力组的槽表的第二个槽。vmspace = obj_alloc(TYPE_VMSPACE, sizeof(*vmspace));/* LAB 3 TODO END */if (!vmspace) {r = -ENOMEM;goto out_free_obj_vmspace;}vmspace_init(vmspace, args.pcid);r = cap_alloc(new_cap_group, vmspace);if (r != VMSPACE_OBJ_ID) {kwarn("%s: cap_copy fails or cap[1] is not vmspace\n",__func__);r = -ECAPBILITY;goto out_free_obj_vmspace;}return cap;
out_free_obj_vmspace:obj_free(vmspace);
out_free_cap_grp_current:cap_free(current_cap_group, cap);new_cap_group = NULL;
out_free_obj_new_grp:obj_free(new_cap_group);
out_fail:return r;
}/* This is for creating the first (init) user process. */
// procmgr的创建,和上面大同小异。有不同的地方将会注释标出。
struct cap_group *create_root_cap_group(char *name, size_t name_len)
{struct cap_group *cap_group = NULL;struct vmspace *vmspace = NULL;cap_t slot_id;/* LAB 3 TODO BEGIN */// UNUSED(vmspace);// UNUSED(cap_group);// 分配能力组对象。cap_group = obj_alloc(TYPE_CAP_GROUP, sizeof(*cap_group));/* LAB 3 TODO END */BUG_ON(!cap_group);/* LAB 3 TODO BEGIN *//* initialize cap group with common, use ROOT_CAP_GROUP_BADGE */// 按照注释调用common初始化能力组。cap_group_init_common(cap_group, BASE_OBJECT_NUM, ROOT_CAP_GROUP_BADGE);/* LAB 3 TODO END */slot_id = cap_alloc(cap_group, cap_group);BUG_ON(slot_id != CAP_GROUP_OBJ_ID);/* LAB 3 TODO BEGIN */// 分配虚拟地址空间vmspace = obj_alloc(TYPE_VMSPACE, sizeof(*vmspace));/* LAB 3 TODO END */BUG_ON(!vmspace);/* fixed PCID 1 for root process, PCID 0 is not used. */vmspace_init(vmspace, ROOT_PROCESS_PCID);/* LAB 3 TODO BEGIN */// 为自身分配槽id。对照上一个函数的写法并且适配下面的assert。slot_id = cap_alloc(cap_group, vmspace);/* LAB 3 TODO END */BUG_ON(slot_id != VMSPACE_OBJ_ID);/* Set the cap_group_name (process_name) for easing debugging */memset(cap_group->cap_group_name, 0, MAX_GROUP_NAME_LEN + 1);if (name_len > MAX_GROUP_NAME_LEN)name_len = MAX_GROUP_NAME_LEN;memcpy(cap_group->cap_group_name, name, name_len);root_cap_group = cap_group;return cap_group;
}

附录:不想阅读这部分的可以跳过。我们将会结合capability.c来梳理整个流程。创建一个进程后,用户态下陷到内核态,接下来就会在内核态创建该进程的PCB内核态部分。

img

  1. 判断内核是否能够唤起创建能力组。采用hook_sys_create_cap_group
  2. 从用户态中的proc_cap部分获取信息,传递到内核态。
  3. 分配内核态的能力组。该能力组也是对象。obj_alloc.
  4. 调用普通进程的能力组初始化。cap_group_init_user。在这一操作中,首先,将进程pid赋予给能力组。然后,初始化线程数和ptrace,并且分配内核态全局标识(badge)。
  5. 接下来初始化能力组的槽表(slot_table)。cap_alloc。首先获取当前进程中的线程的能力组(通过current_cap_group宏定义得到),然后进入到cap_alloc_with_rights中。
  6. 分配能力与权力cap_alloc_with_rights。首先获得当前进程能力组对象真正存储的地址(也就是我们熟悉的container_of宏定义,计算地址量的偏差然后回溯到结构体的所在地址),并且关联到线程拥有的槽表。
  7. 为槽表分配id,并且在槽表中分配一个槽。将线程自身的cap_group放入第一个槽(如图中所示)。
  8. 将新的对象加入到链表中(前面的虚拟内存/物理内存管理需要),并且更新进程的能力组的槽表的id。install_slot
  9. 接下来为vmspace(虚拟地址空间)分配对象和能力组。

这样我们就完成了线程管理的第一部分。

ELF加载

然而,完成 cap_group 的分配之后,用户程序并没有办法直接运行,因为cap_group只是一个资源集合的概念。线程才是内核中的调度执行单位,因此还需要进行线程的创建,将用户程序 ELF 的各程序段加载到内存中。

(此为内核中 ELF 程序加载过程,用户态进行 ELF 程序解析可参考user/system-services/system-servers/procmgr/libs/libchcoreelf/libchcoreelf.c,如何加载程序可以对user/system-services/system-servers/procmgr/srvmgr.c中的procmgr_launch_process函数进行详细分析)

练习题2: 在 kernel/object/thread.c 中完成 create_root_thread 函数,将用户程序 ELF 加载到刚刚创建的进程地址空间中。

  • 程序头可以参考kernel/object/thread_env.h
  • 内存分配操作使用 create_pmo,可详细阅读kernel/object/memory.c了解内存分配。

回顾我们创建进程的过程。我们整体的进程创建有五步:

  1. 创建PCB。(√)
  2. 虚拟内存初始化。(√)
  3. 内核栈初始化。
  4. 加载可执行文件到内存。
  5. 初始化用户栈和运行环境。

接下来我们需要完成3,4,5步来完成我们根进程的第一个线程。
首先,我们先了解我们的ELF文件。可以参见Lec 04 系统调用的第五节。我们的ELF文件具有如下的格式:

img

其次,我们来到我们的create_root_thread函数中。

1.我们需要将我们的启动服务(也就是procmgr)的ELF头部加载进入我们的内存中。因为这是我们的根进程。如下图所示。

img

2.然后为我们的根进程内的第一个线程创建所属于的能力组,并且分配和初始化虚拟内存空间。

img

3.接下来创建物理内存对象,创建页表,建立物理内存与虚拟内存之间的映射。

img

4.创建线程(线程也是对象),然后正式进入加载程序ELF的程序头部表部分。
其实程序都给我们一个例子。我们有:

memcpy(data,(void *)((unsigned long)&binary_procmgr_bin_start+ ROOT_PHDR_OFF + i * ROOT_PHENT_SIZE+ PHDR_FLAGS_OFF),sizeof(data));
flags = (unsigned int)le32_to_cpu(*(u32 *)data);

来获得flags的信息。
这样我们直接照葫芦画瓢,找到对应的在thread_env.h偏移定义即可。

/* LAB 3 TODO BEGIN */
/* Get offset, vaddr, filesz, memsz from image*/
// UNUSED(flags);
// UNUSED(filesz);
// UNUSED(offset);
// UNUSED(memsz);
memcpy(data,(void *)((unsigned long)&binary_procmgr_bin_start+ ROOT_PHDR_OFF + i * ROOT_PHENT_SIZE+ PHDR_OFFSET_OFF),sizeof(data));
offset = (unsigned long)le64_to_cpu(*(u64 *)data);
memcpy(data,(void *)((unsigned long)&binary_procmgr_bin_start+ ROOT_PHDR_OFF + i * ROOT_PHENT_SIZE+ PHDR_VADDR_OFF),sizeof(data));
vaddr = (unsigned long)le64_to_cpu(*(u64 *)data);
memcpy(data,(void *)((unsigned long)&binary_procmgr_bin_start+ ROOT_PHDR_OFF + i * ROOT_PHENT_SIZE+ PHDR_FILESZ_OFF),sizeof(data));
filesz = (unsigned long)le64_to_cpu(*(u64 *)data);
memcpy(data,(void *)((unsigned long)&binary_procmgr_bin_start+ ROOT_PHDR_OFF + i * ROOT_PHENT_SIZE+ PHDR_MEMSZ_OFF),sizeof(data));
memsz = (unsigned long)le64_to_cpu(*(u64 *)data);
/* LAB 3 TODO END */

5.接下来还要为程序头部表分配物理内存对象。在分配这里是我们使用段物理内存对象,也就是segment_pmo。然后,每次分配一个物理页大小的段内存,并且映射到虚拟空间中。因此这里需要计算程序头部表中需要多少个物理页,并且从虚拟地址中计算出物理地址,填写到对应的段成员中。我们有:

struct pmobject *segment_pmo = NULL;
/* LAB 3 TODO BEGIN */
// UNUSED(segment_pmo);
size_t pmo_size = ROUND_UP(memsz, PAGE_SIZE)
vaddr_t segment_content_kvaddr = ((unsignelong) &binary_procmgr_bin_start) + offset;
/* LAB 3 TODO END */
BUG_ON(filesz != memsz);
ret = create_pmo(PAGE_SIZE,PMO_DATA,root_cap_group,0,&segment_pmo,PMO_ALL_RIGHTS);
BUG_ON(ret < 0);
kfree((void *)phys_to_virt(segment_pmo -> start));
/* LAB 3 TODO BEGIN */
/* Copy elf file contents into memory*/
segment_pmo -> start = virt_to_phys(segment_content_kvaddr);
segment_pmo -> size = pmo_size;
/* LAB 3 TODO END */

6.接下来设置虚拟空间的权限。因为这是程序头部表部分,需要让程序能够对虚拟空间进行读写和执行。回顾我们在lab2中设置flag的经验,我们采用或的方式设置。(位于OS-Course-Lab/lab2/kernel/mm/vmspace.c)中。

img

因此我们有:

/* LAB 3 TODO BEGIN */
/* Set flags*/
if(flags & PHDR_FLAGS_R)vmr_flags |= VMR_READ;
if(flags & PHDR_FLAGS_W)vmr_flags |= VMR_WRITE;
if(flags & PHDR_FLAGS_X)vmr_flags |= VMR_EXEC;
/* LAB 3 TODO END */

这样我们就完成了ELF加载过程。


以下内容可以跳过。关于srvmgr.c中的procmgr_launch_process的相关解读

int procmgr_launch_process(int argc, char **argv, char *name,bool if_has_parent, badge_t parent_badge,struct new_process_args *np_args, int proc_type,struct proc_node **out_proc)

这是一个通过根进程procmgr加载指定名字的可执行文件的程序。首先,先读取我们的ELF文件,判断这是不是一个动态链接程序。

  • 动态链接VS静态链接?
  • 静态链接:这是我们最为常用的方式。对于几个已经编写好的程序,我们利用make,cmake等确定编译规则和链接规则。然后进行以下三步:(1)预处理。也就是粗暴地将头文件,预编译指令等等直接插入程序中生成.i文件。(2)编译,详情参见编译原理,通过编译器将程序生成.s汇编文件。(3)汇编:将汇编指令翻译成机器指令,生成可重定位二进制目标文件。(4)链接:我们将生成的.o文件根据依赖关系进行链接,最后生成一个可以执行的程序。最后一个过程就是静态链接。在linux中可以将可执行文件打包生成静态链接库为.a。
  • 动态链接:为了解决空间问题和更新困难问题,可以将程序按照模块拆分成相对独立的部分。只有在程序运行时才将他们连接在一起,而不是在运行前就链接形成可执行文件。并且,可复用性很强。当程序一已经加载了动态链接程序时,程序二也需要使用时,只需要将相同的物理地址映射到不同的程序的不同虚拟空间内的虚拟地址即可。这样大大减少了物理内存的占用,不需要重复加载相同的程序块到内存中。

接下来从文件中读取elf的文件头,再根据文件头读取elf程序文件。

img

随后进入到启动进程部分。首先检查是否有父节点(是否是一个进程唤起的子线程,例如fork)。接着就是创建线程节点。一样的过程(分配object,初始化进程节点)。

然后,将elf内的相关信息和传入该函数的相关信息用于初始化新的进程的相关成员。接着根据是否为动态/静态链接来启动这个进程。最后初始化进程节点的相关状态,成员组等元信息。

这就是整个函数的大致过程。

user/system-services/system-servers/procmgr/include/libchcoreelf.h中具有elf的详细定义和相关信息。

进程调度

完成用户程序的内存分配后,用户程序代码实际上已经被映射在root_cap_group的虚拟地址空间中。接下来需要对创建的线程进行初始化,以做好从内核态切换到用户态线程的准备。

练习题3:在 kernel/arch/aarch64/sched/context.c 中完成 init_thread_ctx 函数,完成线程上下文的初始化。

首先,我们需要回顾我们的上下文结构。

img

在进行上下文切换时,我们需要保存上述的通用寄存器,特殊寄存器中的用户栈寄存器(我们的stack虚拟地址)以及系统寄存器中用于保存PC(也就是我们传进来的函数的虚拟地址)和PSTATE的寄存器。此时进行初始化的时候,应该设定我们的PSTATE在用户态上,也就是SPSR_EL0t.

img

img

这样初始化就可以直接完成如下:

void init_thread_ctx(struct thread *thread, vaddr_t stack, vaddr_t func,u32 prio, u32 type, s32 aff)
{/* Fill the context of the thread *//* LAB 3 TODO BEGIN *//* SP_EL0, ELR_EL1, SPSR_EL1*/thread->thread_ctx->ec.reg[SP_EL0] = stack; // 用户栈寄存器thread->thread_ctx->ec.reg[ELR_EL1] = func; // PC寄存器thread->thread_ctx->ec.reg[SPSR_EL1] = SPSR_EL1_EL0t; // 状态寄存器/* LAB 3 TODO END *//* Set the state of the thread */thread->thread_ctx->state = TS_INIT;/* Set thread type */thread->thread_ctx->type = type;/* Set the cpuid and affinity */thread->thread_ctx->affinity = aff;/* Set the budget and priority of the thread */if (thread->thread_ctx->sc != NULL) {thread->thread_ctx->sc->prio = prio;thread->thread_ctx->sc->budget = DEFAULT_BUDGET;}thread->thread_ctx->kernel_stack_state = KS_FREE;/* Set exiting state */thread->thread_ctx->thread_exit_state = TE_RUNNING;thread->thread_ctx->is_suspended = false;
}

至此,我们完成了第一个用户进程与第一个用户线程的创建。接下来就可以从内核态向用户态进行跳转了。 回到kernel/arch/aarch64/main.c,在create_root_thread()完成后,分别调用了sched()eret_to_thread(switch_context())sched()的作用是进行一次调度,在此场景下我们创建的第一个线程将被选择。

switch_context()函数的作用则是进行线程上下文的切换,包括vmspace、fpu、tls等。并且将cpu_info中记录的当前CPU线程上下文记录为被选择线程的上下文(完成后续实验后对此可以有更深的理解)。switch_context() 最终返回被选择线程的thread_ctx地址,即target_thread->thread_ctx

eret_to_thread最终调用了kernel/arch/aarch64/irq/irq_entry.S中的 __eret_to_thread 函数。其接收参数为target_thread->thread_ctx,将 target_thread->thread_ctx 写入sp寄存器后调用了 exception_exit 函数,exception_exit 最终调用 eret 返回用户态,从而完成了从内核态向用户态的第一次切换。

注意此处因为尚未完成exception_exit函数,因此无法正确切换到用户态程序,在后续完成exception_exit后,可以通过 gdb 追踪 pc 寄存器的方式查看是否正确完成内核态向用户态的切换。

思考内核从完成必要的初始化到第一次切换到用户态程序的过程是怎么样的?尝试描述一下调用关系。

我们有如下的流程图

flowchart TD crt("create_root_thread(创建根进程线程)") rfl("read_from_binary(读取ELF头部)") crcg("create_root_cap_group(创建根能力组)") init("obj_get(初始化虚拟地址)") cpmo("create_pmo(创建物理内存对象)") map("vmspace_map_range(映射虚拟地址)") init_t("obj_alloc(创建线程)") rfl_proc("read_from_program_header(读取程序头部表)") cpmo_p("create_pmo(创建物理内存对象)") map_p("vmspace_map_range(映射虚拟地址)") alloc_page("commit_page_to_pmo(分配页给pmo)") pre("prepare_env(准备环境)") crcg_t("cap_alloc & obj_get(创建线程能力组并分配对象)") sched("sched_enqueue(进入调度队列等待)")subgraph 调用和执行过程 crt --> rfl --> crcg --> init --> cpmo --> map --> init_t --> rfl_proc --> cpmo_p --> map_p --> alloc_page --> pre --> crcg_t --> sched end

然而,目前任何一个用户程序并不能正常退出,也不能正常输出结果。这是由于程序中包括了 svc #0 指令进行系统调用。由于此时 ChCore 尚未配置从用户模式(EL0)切换到内核模式(EL1)的相关内容,在尝试执行 svc 指令时,ChCore 将根据目前的配置(尚未初始化,异常处理向量指向随机位置)执行位于随机位置的异常处理代码,进而导致触发错误指令异常。同样的,由于错误指令异常仍未指定处理代码的位置,对该异常进行处理会再次出发错误指令异常。ChCore 将不断重复此循环,并最终表现为 QEMU 不响应。后续的练习中将会通过正确配置异常向量表的方式,对这一问题进行修复。

3.3 异常处理

由于 ChCore 尚未对用户模式与内核模式的切换进行配置,一旦 ChCore 进入用户模式执行就再也无法正常返回内核模式使用操作系统提供其他功能了。在这一部分中,我们将通过正确配置异常向量表的方式,为 ChCore 添加异常处理的能力。

在 AArch64 架构中,异常是指低特权级软件(如用户程序)请求高特权软件(例如内核中的异常处理程序)采取某些措施以确保程序平稳运行的系统事件,包含同步异常异步异常

img

  • 同步异常:通过直接执行指令产生的异常。同步异常的来源包括同步中止(synchronous abort)和一些特殊指令。当直接执行一条指令时,若取指令或数据访问过程失败,则会产生同步中止。此外,部分指令(包括 svc 等)通常被用户程序用于主动制造异常以请求高特权级别软件提供服务(如系统调用)。
  • 异步异常:与正在执行的指令无关的异常。异步异常的来源包括普通中 IRQ、快速中断 FIQ 和系统错误 SError。IRQ 和 FIQ 是由其他与处理器连接的硬件产生的中断,系统错误则包含多种可能的原因。本实验不涉及此部分。

发生异常后,我们需要从kernel/arch/aarch64/irq/irq_entry.S中找到对应的异常处理程序代码(异常向量)执行。每个异常级别在aarch64中有自己独立的异常向量表,其虚拟地址由该异常级别下的异常向量基地址寄存器(VBAR_EL3VBAR_EL2VBAR_EL1)决定。每个异常向量表中包含 16 个条目,每个条目里存储着发生对应异常时所需执行的异常处理程序代码。以上表格给出了每个异常向量条目的偏移量。
在 ChCore 中,仅使用了 EL0 和 EL1 两个异常级别,因此仅需要对 EL1 异常向量表进行初始化即可。在本实验中,ChCore 内除系统调用外所有的同步异常均交由 handle_entry_c 函数进行处理。遇到异常时,硬件将根据 ChCore 的配置执行对应的汇编代码,将异常类型和当前异常处理程序条目类型作为参数传递,对于 sync_el1h 类型的异常,跳转 handle_entry_c 使用 C 代码处理异常。对于 irq_el1t、fiq_el1t、fiq_el1h、error_el1t、error_el1h、sync_el1t 则跳转 unexpected_handler 处理异常。

按照前文所述的表格填写 kernel/arch/aarch64/irq/irq_entry.S 中的异常向量表,并且增加对应的函数跳转操作。

配置EL1级别下和EL0级别下的异常向量,填写异常向量表。根据上边的表格描述,我们发现针对不同异常发生时的处理器状态(四种)和不同的异常情况(四种)共分成16种情况,每种情况按照0x80对齐(128字节)。因此我们可以使用前面的 exception_entry 汇编宏定义,将我们的异常按照128字节对齐后跳转到对应的异常类型的异常处理向量。因此我们可以:

        /* LAB 3 TODO BEGIN */exception_entry sync_el1t               // Synchronous EL1texception_entry irq_el1t                // IRQ EL1texception_entry fiq_el1t                // FIQ EL1texception_entry error_el1t              // Error EL1texception_entry sync_el1h               // Synchronous EL1hexception_entry irq_el1h                // IRQ EL1hexception_entry fiq_el1h                // FIQ EL1hexception_entry error_el1h              // Error EL1hexception_entry sync_el0_64             // Synchronous 64-bit EL0exception_entry irq_el0_64              // IRQ 64-bit EL0exception_entry fiq_el0_64              // FIQ 64-bit EL0exception_entry error_el0_64            // Error 64-bit EL0exception_entry sync_el0_32             // Synchronous 32-bit EL0exception_entry irq_el0_32              // IRQ 32-bit EL0exception_entry fiq_el0_32              // FIQ 32-bit EL0exception_entry error_el0_32            // Error 32-bit EL0/* LAB 3 TODO END */

进行128字节对齐,这样虽然每种类型的异常处理向量数目不同,但是每种类型都等长地占据相同的空间,减少异常处理的时间。

3.4 系统调用

内核支持

系统调用是系统为用户程序提供的高特权操作接口。在本实验中,用户程序通过 svc 指令进入内核模式。在内核模式下,首先操作系统代码和硬件将保存用户程序的状态。操作系统根据系统调用号码执行相应的系统调用处理代码,完成系统调用的实际功能,并保存返回值。最后,操作系统和硬件将恢复用户程序的状态,将系统调用的返回值返回给用户程序,继续用户程序的执行。

通过异常进入到内核后,需要保存当前线程的各个寄存器值,以便从内核态返回用户态时进行恢复。保存工作在 exception_enter 中进行,恢复工作则由 exception_exit 完成。可以参考kernel/include/arch/aarch64/arch/machine/register.h 中的寄存器结构,保存时在栈中应准备ARCH_EXEC_CONT_SIZE大小的空间。

完成保存后,需要进行内核栈切换,首先从TPIDR_EL1寄存器中读取到当前核的per_cpu_info(参考kernel/include/arch/aarch64/arch/machine/smp.h),从而拿到其中的cpu_stack地址。

填写 kernel/arch/aarch64/irq/irq_entry.S 中的 exception_enterexception_exit,实现上下文保存的功能,以及 switch_to_cpu_stack 内核栈切换函数。如果正确完成这一部分,可以通过 Userland 测试点。这代表着程序已经可以在用户态与内核态间进行正确切换。显示如下结果

Hello userland!

首先我们先完成异常进入和异常退出部分。
这一部分可以直接参照书本上的内容。首先,进入异常处理前需要进行保存上下文,然后进行异常处理。最后恢复上下文。我们有:

img

img

先保存x0——x29通用寄存器,再保存x30和三个寄存器(用户栈寄存器SP_EL0,PC寄存器ELR_EL1和用户状态寄存器SPSR_EL0)。恢复时先恢复x30和三个寄存器,再恢复x0-x29.

/* See more details about the bias in registers.h */
.macro  exception_enter/* LAB 3 TODO BEGIN */sub sp, sp, #ARCH_EXEC_CONT_SIZE// saving general register.stp x0, x1, [sp, #16 * 0]stp x2, x3, [sp, #16 * 1]stp    x4, x5, [sp, #16 * 2]stp    x6, x7, [sp, #16 * 3]stp    x8, x9, [sp, #16 * 4]stp    x10, x11, [sp, #16 * 5]stp    x12, x13, [sp, #16 * 6]stp    x14, x15, [sp, #16 * 7]stp    x16, x17, [sp, #16 * 8]stp    x18, x19, [sp, #16 * 9]stp    x20, x21, [sp, #16 * 10]stp    x22, x23, [sp, #16 * 11]stp    x24, x25, [sp, #16 * 12]stp    x26, x27, [sp, #16 * 13]stp    x28, x29, [sp, #16 * 14]/* LAB 3 TODO END */// special register saving.mrs    x21, sp_el0mrs    x22, elr_el1mrs    x23, spsr_el1/* LAB 3 TODO BEGIN */stp x30, x21, [sp, #16 * 15]stp x22, x23, [sp, #16 * 16]/* LAB 3 TODO END */.endm.macro  exception_exit/* LAB 3 TODO BEGIN */// do upside down of enter.ldp     x22, x23, [sp, #16 * 16]ldp     x30, x21, [sp, #16 * 15] /* LAB 3 TODO END */msr     sp_el0, x21msr     elr_el1, x22msr     spsr_el1, x23/* LAB 3 TODO BEGIN */ldp     x0, x1, [sp, #16 * 0]ldp     x2, x3, [sp, #16 * 1]ldp     x4, x5, [sp, #16 * 2]ldp     x6, x7, [sp, #16 * 3]ldp     x8, x9, [sp, #16 * 4]ldp     x10, x11, [sp, #16 * 5]ldp     x12, x13, [sp, #16 * 6]ldp     x14, x15, [sp, #16 * 7]ldp     x16, x17, [sp, #16 * 8]ldp     x18, x19, [sp, #16 * 9]ldp     x20, x21, [sp, #16 * 10]ldp     x22, x23, [sp, #16 * 11]ldp     x24, x25, [sp, #16 * 12]ldp     x26, x27, [sp, #16 * 13]ldp     x28, x29, [sp, #16 * 14]add     sp, sp, #ARCH_EXEC_CONT_SIZE/* LAB 3 TODO END */eret
.endm

接着根据我们前面提及的:

  • 对于 sync_el1h 类型的异常,跳转 handle_entry_c 使用 C 代码处理异常。
  • 对于 irq_el1t、fiq_el1t、fiq_el1h、error_el1t、error_el1h、sync_el1t 则跳转 unexpected_handler 处理异常。

需要注意的是,在sync_el1h类型的异常,跳转到C函数后,退出异常前需要保存好返回值。(如注释里所说)。因为大部分的sync错误来源于以下四种情况,因此需要储存好我们的返回值。

img

这样我们将会有以下方式:

irq_el1t:
fiq_el1t:
fiq_el1h:
error_el1t:
error_el1h:
sync_el1t:/* LAB 3 TODO BEGIN */bl unexpected_handler/* LAB 3 TODO END */sync_el1h:exception_entermov    x0, #SYNC_EL1hmrs    x1, esr_el1mrs    x2, elr_el1/* LAB 3 TODO BEGIN *//* jump to handle_entry_c, store the return value as the ELR_EL1 */bl     handle_entry_cstr x0, [sp, #16 * 16] /* store the return value as the ELR_EL1 *//* LAB 3 TODO END */exception_exit

这样我们完成了异常向量配置和异常进入/退出部分。

用户态libc支持

在本实验中新加入了 libc 文件,用户态程序可以链接其编译生成的libc.so,并通过 libc 进行系统调用从而进行向内核态的异常切换。在实验提供的 libc 中,尚未实现 printf 的系统调用,因此用户态程序无法进行正常输出。实验接下来将对 printf 函数的调用链进行分析与探索。

printf 函数调用了 vfprintf,其中文件描述符参数为 stdout。这说明在 vfprintf 中将使用 stdout 的某些操作函数。

user/chcore-libc/musl-libc/src/stdio/stdout.c 中可以看到 stdoutwrite 操作被定义为 __stdout_write,之后调用到 __stdio_write 函数。

最终 printf 函数将调用到 chcore_stdout_write

printf如何调用到chcore_stdout_write?


chcore_write 中使用了文件描述符,stdout 描述符的设置在user/chcore-libc/musl-libc/src/chcore-port/syscall_dispatcher.c 中。

chcore_stdout_write 中的核心函数为 put,此函数的作用是向终端输出一个字符串。

printf函数定义在user/chcore-libc/musl-libc/src/stdio/printf.c.
我们有如下的调用链:

1.调用vfprintf()函数。
2.使用printf_core()函数检查相关的输入格式是否正确。
3.调用f->write函数。此时,因为传入的流为stdout。这个定义于stdout.c文件中:

img

因此将会调用__stdout_write函数。随后调用到__stdio_write函数。

img

img

4.此时将会调用系统服务,也就是定义在syscall_dispatcher中。syscall有宏定义:

img

5.这里首先根据宏定义确定具体的系统调用(syswritev),随后确定参数量为3,因此最终调用__syscall3,随后在case SYS_writev下调用__syscall6.所以实际上对于需要x个参数的系统调用,需要传入x+1个参数,其中第一个参数为需要内核进行的具体系统调用函数类型。

img

6.调用chcore_write函数,需要从我们的描述符中判断应该调用哪种write函数。我们有:

img

img

7.此时我们的文件描述符是STDOUT,因此执行的fd_ops类中的write时,应该执行的是stdout_ops对象描述的write。我们再观察stdout_ops.

img

因此执行到了chcore_stdout_write函数。其相对地址在user/chcore-libc/libchcore/porting/overrides/src/chcore-port/stdio.c中。

img

在其中添加一行以完成系统调用,目标调用函数为内核中的 sys_putstr。使用 chcore_syscallx 函数进行系统调用。

观察,我们的put有两个参数,因此我们需要调用需要(使用)两个参数的系统调用,也就是chcore_syscall2即可。在先前我们已经发现,需要(使用)两个参数的系统调用需要传入三个参数。因此我们有:

static void put(char buffer[], unsigned size)
{/* LAB 3 TODO BEGIN */// 2 arguments for syscall2.chcore_syscall2(CHCORE_SYS_putstr, (vaddr_t)buffer, size);/* LAB 3 TODO END */
}

至此,我们完成了对 printf 函数的分析及完善。从 printf 的例子我们也可以看到从通用 api 向系统相关 abi 的调用过程,并最终通过系统调用完成从用户态向内核态的异常切换。

3.5 编写用户态程序

终于到了最后一刻。万事俱备,我们开始编写我们的用户态程序,通过libc执行系统调用,利用Chcore的libc进行编译,加载到内核镜像中运行。

我们首先编写文件如下。为了防止出现意外,我们的stdio头文件采用libc内的头文件。

# include "build/chcore-libc/include/stdio.h"
int main()
{printf("Hello ChCore!");return 0;
}

接下来使用工具进行编译并且重定向到目标文件夹。在命令行中输入:

./build/chcore-libc/bin/musl-gcc printf.c -o ./ramdisk/hello_world.bin

直接运行程序。

img

我们可以看到我们在userland后运行了hello chcore。

img

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

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

相关文章

AI实战训练营-让AI成为你的核心竞争力

现在各种付费教程,付费培训,反复的割韭菜 想要不被割,那么可以链接我 揭秘割韭菜的常用套路 真正的去实战使用AI,去提升自己竞争力 十年开发经验程序员,离职全心创业中,历时三年开发出的产品《唯一客服系统》一款基于Golang+Vue开发的在线客服系统,软件著作权编号:2021…

【原创】vs2022配置切换多个ollvm环境

不同的ollvm的混淆效果不一样,不同魔改版的命令参数也不一样,有的支持迭代多次混淆,有的不支持。 我想使用ollvm不同的版本,怎么办? 我不想替换vs安装的原装llvm,怎么办? 直接下载别人编译好的文件,不用自己编译了: https://github.com/KomiMoe/Arkari/releases/tag/W…

H5-20 背景属性

CSS背景属性主要有以下几个属性 描述background-color 设置背景颜色background-image 设置背景图片background-position 设置背景图片显示位置background-repeat 设置背景图片如何填充background-size 设置背景图片大小属性 1、background-color属性该属性设置背景颜色<div…

docker 配置文件

、解决方案 (1)查看DNS客户机的配置文件1cat /etc/resolv.conf 发现我的nameserver 是 8.8.8.8了,说明我的DNS出了问题 需要新增DNS:nameserver 114.114.114.114 (2)修改配置文件1vim /etc/resolv.conf修改后如下 第一步:编辑 Docker 配置文件打开 Docker 的配置文件 d…

Natasha v9.0 为 .NET 开发者提供 [热执行] 方案.

项目简介 自 Natasha v9.0 发布起,我将基于 Natasha 的推出热执行方案,这项技术允许基于 控制台(Console) 和新版 Asp.net Core 架构的项目在运行中动态重编译,在不停止工程的情况下获取最新结果,以帮助技术初学者、项目初期开发人员等,进行快速实验以及试错。 为了更形象…

cmu15545笔记-并发控制总结(Concurrency Control Summary)

目录总览ACID串行化与冲突操作隔离级别概念层级二阶段锁原理级联回滚强二阶段锁死锁检测和避免锁层级实践应用实现的隔离级别OOC原理三个阶段实现的隔离级别处理幻读MVC原理写偏差异常(Write Skew Anomaly)版本存储(Version-storage)Append OnlyTime Travel StorageDelta S…

基于Bootstrap的强大jQuery表单验证插件

预览 下载 formvalidation是一款功能非常强大的基于Bootstrap的JQUERY表单验证插件。该jQuery表单验证插件内置了16种表单验证器,你也可以通过Bootstrap Validators APIs写自己的表单验证器。该表单验证插件的可用验证器有:between:检测输入的值是否在两个指定的值之间。 c…

人员跌倒检测算法

人员跌倒检测算法利用基于YOLOv5和CNN,人员跌倒检测算法通过安装在监测区域内的摄像头对人员的行为进行检测,区分正常活动和跌倒等异常行为。一旦检测到跌倒行为,系统会立即触发报警,通过声音警报、短信通知、APP推送等多种方式发出报警通知,确保相关人员能够在第一时间知…

秒开超大文件夹:如何禁止 Windows 自动识别文件夹类型

Windows 的「自动文件类型发现」功能会分析文件夹内容,以便应用最合适的视图模板。但对于包含大量文件和文件类型复杂的超大文件夹,则会导致「文件资源管理器」的打开速度变慢。本文将教你如何关闭这一功能,以加快文件夹的加载速度。 .wwads-img img { width: 150px; margin…

垃圾分类AI视觉识别系统

垃圾分类AI视觉识别系统通过高清摄像头实时捕捉垃圾投放点,垃圾分类AI视觉识别系统通过YOLOv7算法进行图像识别,识别出垃圾乱投、垃圾箱满溢、厨余垃圾误时投放等违规行为。这种智能分析算法不仅提高了识别的准确性,还能够实时监控垃圾投放点的状态,确保垃圾分类的规范性。…

学生上课行为教学评估检测系统

学生上课行为教学评估检测系统的核心在于智能识别技术,学生上课行为教学评估检测系统能够通过人脸识别与表情识别技术,捕捉学生在课堂上的面部表情和情绪变化,从而分析学生的参与度和兴趣点。人脸考勤功能则确保了出勤率的准确性,为教学管理提供了基础数据。声纹识别技术的…

电动车戴头盔智能识别系统方案

电动车戴头盔智能识别系统方案核心在于YOLOv7算法与RNN的结合,电动车戴头盔智能识别系统方案通过部署在交通要道的实时监控摄像头捕捉画面,自动识别出画面中的电动车骑行者,判断是否佩戴了安全头盔。一旦系统检测到未佩戴安全头盔的骑行者,将立即触发报警机制。报警信号不仅…