动手学操作系统(七、实现内存分页机制)

news2024/10/5 17:28:08

动手学操作系统(七、实现内存分页机制)

在上一节中,我们成功读取了物理内存的容量,并且在之前的学习中,我们的程序已经进入了保护模式,地址空间能够达到4GB,但是所有的进程(包括操作系统)都需要共享这4GB的空间,为了更好得维护这4GB的虚拟内存空间,我们需要使用内存分页机制。

物理内存:指计算机中实际存在的硬件内存,即RAM(随机存取存储器)。物理内存由具体的内存芯片组成,直接用于存储正在使用的数据和程序。

虚拟内存:是操作系统提供的一种内存管理技术,它为每个进程提供了一个连续的、私有的地址空间,使得每个进程认为自己拥有独立且足够大的内存空间。虚拟内存通过硬件和操作系统的协作,将物理内存与磁盘存储结合起来,以实现对物理内存的扩展和高效利用。

文章目录

  • 动手学操作系统(七、实现内存分页机制)
    • 1. 内存管理的基本原理
    • 2. 一级页表
      • 2.1 分段机制
      • 2.2 分页机制
    • 3. 二级页表
    • 4. 代码
    • Reference

1. 内存管理的基本原理

简单介绍一下内存段是怎样被换出的,在第五节的学习之后,我们知道在保护模式下,段描述符是内存段的身份证,CPU在引用一个段的时候,都需要先查看段描述符,很多时候,段描述符存在于描述符表中(GDT和LDT),但此描述符对应的段并不在内存中,也就是说,CPU允许在描述符表中已注册的段不在内存中存在,这就是它提供给软件使用的策略,我们利用它实现段式内存管理。段描述符的各位如下所示(详细的解释可以参考第五节)

Image
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| P | DPL | S | Type   | A |
  • P (Present, bit 7): 段是否存在。
  • DPL (Descriptor Privilege Level, bits 5-6): 段的特权级。
  • S (Descriptor Type, bit 4): 描述符类型(0表示系统段,1表示代码或数据段)。
  • Type (bits 1-3): 段类型和权限。
  • A (Accessed, bit 0): 已访问标志。

加载内存段

如果该描述符中的P位为1,表示该段在内存中存在,访问过该段之后,CPU将段描述符中的A位置1,表示最近访问过该段,相反如果P位为0,说明内存中并不存在该段,这时候CPU将会抛出个NP(段不存在)异常,转而去执行中断描述符表中NP异常对应的中断处理程序,此中断处理程序是操作系统负责提供的,该程序的工作是将相应的段从外存(比如硬盘)中载入到内存,并将段描述符的P位置1,中断处理函数结束后返回,CPU重复执行这个检查,继续查看该段描述符的P位,此时已经为1了,在通过检查后,将该段描述符的A位置1。

移出内存段

段描述符A位由CPU置1,但是清零工作是由操作系统来完成的,操作系统每发现该位为1之后就将该位清0,这样一来,在一个周期内统计该位为1的次数就知道该段的使用频率了,从而可以找到使用频率最低的段。当物理内存不足的时候,可以将使用频率最低的段换出到硬盘。

2. 一级页表

2.1 分段机制

尽管在保护模式下,段寄存器中的内容已经是选择子,但选择子最终就是要为了找到段基地址,其内存访问的核心仍然是“段基地址:段内偏移”,这两个地址相加之后才是绝对地址,也就是我们所说的线性地址,此线性地址在分段机制下被CPU认为是物理地址,直接拿来就用,整个内存访问过程如图所示:

Image

2.2 分页机制

分页机制是要建立在分段机制的基础之上,分页只能在分段之后进行,CPU在不打开分页机制的情况下,是按照默认的分段方式进行的,段基地址和段内偏移地址经过段部件处理之后所输出的线性地址,CPU就认为是物理地址。如果打开了分页机制,段部件输出的线性地址就不再等同于物理地址了,而是虚拟地址,此虚拟地址对应的物理地址必须要在页表中进行查找,这项查找功能是由页部件自动完成的,如图所示:

Image

分页机制的原理:经过段部件的处理之后,保护模式的寻址空间是4GB,这个寻址空间是线性的地址空间,它在逻辑上是连续的。分页机制的思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。

Image

页表(Page Table)是一个N行1列的表格,页表中的每一行称为页表项(Page Table Entry, PTE),其大小为4B,页表项的作用是存储内存物理地址。当访问一个线性地址时,实际上在访问页表项中所记录的物理内存地址。分页机制的本质是将大小不同的大内存段拆分成大小相等的小内存块。我们只需要确保:内存块数量*内存块大小=4GB即可,当我们选择内存块大小为4KB,是内存块数量为1M,换种说法即是,页的大小为4KB,页表的数量为1MB,这样一来4GB的内存就被我们划分成了4GB/4KB=1MB个页,也就是说4GB空间中可以容纳1048576个页,这就是我们所说的一级页表。

Image

页表如何使用?任意一个地址最终都会落到某一个物理页中,32位地址空间一共有1MB(1048756)个物理页,首先要做的是定位到某一个具体的物理页,然后给出物理页中的偏移量就可以访问到任意1字节的内存了。用20位二进制可以表示全部的物理页,标准的页都是4KB,所以只需要12位就能表达4KB之内的任意内存地址。

总结一下页部件的工作:用线性地址的高20位在页表中索引页表项,用线性地址的低12位与页表项中的物理地址相加,所求的和便是最终线性地址对应的物理地址。

Image

3. 二级页表

为了节省内存和提高效率,引入了多级页表。例如,二级页表将页表拆分为多个层级。通过将页表分层,可以将需要的页表项分散到多级页表中,避免一次性分配大量连续的内存空间。二级页表的占用示意图如下:

Image

二级页表的地址转换原理是将32位虚拟地址拆分成高10位,中10位,低12位三部分,其中:高10位, 2 10 = 1024 2^{10}=1024 210=1024,作为页表的索引,用于在页目录中定位一个页目录项PDE,页目录项中有页表的物理地址,也就是定位到了某个页表。中间的10位, 2 10 = 1024 2^{10}=1024 210=1024,作为物理页的索引,用于在页表内定位到某个页表项PTE,页表项中有分配的物理页地址,也就是定位到了某个物理页。低12位, 2 12 = 4 KB 2^{12}=4\text{KB} 212=4KB,作为页内偏移量用于在已经定位的物理页中寻址。

Image

页目录项和页表项

Image

其中,
P, Present,是存在位,若为1表示该物理页存在于内存中,若为0表示该物理页不存在于内存中。
RW, Read/Write,是读写位,若为1表示可读可写,若为0表示可读不可写。
US, User/Supervisor,是普通用户/超级用户位
PWT, Page-level Write-Through,是页级通写位,若为1表示此项采用通写方式,我们直接设置成0即可
PCD, Page-level Cache Disable,是页级告诉缓存禁止位,1表示启用,0表示禁用
A, Accessed,是访问位,该位是由CPU进行设置
D, Dirty,是脏页位,当CPU对一个页面进行写操作时,就会设置对应页表的D位为1,此项仅针对页表项有效。
PAT, Page Attribute Table,是页属性表位,直接置0即可
G, Global,是全局位
AVL, Avaliable,是可用位

4. 代码

开启分页机制,需要做好三件事

  1. 准备好页目录表及页表
  2. 将页表地址写入控制寄存器cr3
  3. 寄存器cr0PG位置1

目录如下

.
├── bin
│   ├── loader.bin
│   └── mbr.bin
├── bochs_out.log
├── command_compile.sh
├── command_run.sh
├── src
│   └── boot
│       ├── lib
│       │   └── boot.inc
│       ├── loader.S
│       └── mbr.S
└── test

5 directories, 8 files

boot.inc

; ~/d2los/src/lib/boot.inc
LOADER_BASE_ADDR equ 0x600
LOADER_START_SECTOR equ 1   ; loader的LBA扇区号


;--------------   gdt描述符属性  -------------
DESC_G_4K   equ	  1_00000000000000000000000b    ; 段界限单位:4KB
DESC_D_32   equ	   1_0000000000000000000000b    ; 有效地址和操作数是 32 位
DESC_L	    equ	    0_000000000000000000000b	; 64位代码标记,此处标记为0便可。
DESC_AVL    equ	     0_00000000000000000000b	; 保留位
DESC_LIMIT_CODE2  equ 1111_0000000000000000b    ; 代码段段界限的高位
DESC_LIMIT_DATA2  equ 1111_0000000000000000b    ; 数据段段界限的高位
DESC_LIMIT_VIDEO2 equ 0000_0000000000000000b    ; 视频段段界限的高位
DESC_P	    equ		  1_000000000000000b        ; 段是否存在标志位
DESC_DPL_0  equ		   00_0000000000000b        ; 0特权级内存
DESC_DPL_1  equ		   01_0000000000000b        ; 1特权级内存
DESC_DPL_2  equ		   10_0000000000000b        ; 2特权级内存
DESC_DPL_3  equ		   11_0000000000000b        ; 3特权级内存
DESC_S_CODE equ		     1_000000000000b        ; 代码段的段描述为数据段 
DESC_S_DATA equ	         1_000000000000b        ; 数据段的段描述为数据段 
DESC_S_sys  equ		     0_000000000000b        ; 系统段的段描述为系统段  
DESC_TYPE_CODE  equ	      1000_00000000b	;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.  
DESC_TYPE_DATA  equ	      0010_00000000b	;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.

DESC_CODE_HIGH4  equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4  equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b

;--------------   选择子属性  ---------------
RPL0  equ   00b
RPL1  equ   01b
RPL2  equ   10b
RPL3  equ   11b
TI_GDT	 equ   000b
TI_LDT	 equ   100b

;--------------   页表属性  ---------------
PAGE_DIR_TABLE_POS equ 0x100000
PG_P  equ   1b
PG_RW_R	 equ  00b 
PG_RW_W	 equ  10b 
PG_US_S	 equ  000b 
PG_US_U	 equ  100b 

loader.S

; ~/d2los/src/boot/loader.s
%include "boot.inc" 
section loader vstart=LOADER_BASE_ADDR ; 程序开始的地址

jmp loader_start

LOADER_STACK_TOP equ LOADER_BASE_ADDR ; 栈顶地址

;构建gdt及其内部的描述符
GDT_BASE:  dd    0x00000000 
	       dd    0x00000000

CODE_DESC: dd    0x0000FFFF 
	       dd    DESC_CODE_HIGH4

DATA_STACK_DESC:  dd    0x0000FFFF
		          dd    DESC_DATA_HIGH4

VIDEO_DESC: dd    0x80000007	       ; limit=(0xbffff-0xb8000)/4k=0x7
	        dd    DESC_VIDEO_HIGH4     ; 此时dpl为0

GDT_SIZE   equ   $ - GDT_BASE
GDT_LIMIT  equ   GDT_SIZE -	1 
times 60 dq 0					 ; 此处预留60个描述符的slot
SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0   ; 第一个选择子
SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0	 ; 第二个选择子
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	 ; 第三个选择子

; 以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr  dw  GDT_LIMIT 
	     dd  GDT_BASE

total_mem_bytes dd 0		; 保存内存容量,以字节为单位
ards_buf times 244 db 0     ; 人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,256字节
ards_nr dw 0		        ; 用于记录ards结构体数量

loader_start:
    mov byte [gs:160],'L'
    mov byte [gs:161],0x0F
    mov byte [gs:162],'O'
    mov byte [gs:163],0x0F
    mov byte [gs:164],'A'
    mov byte [gs:165],0x0F   
    mov byte [gs:166],'D'
    mov byte [gs:167],0x0F
    mov byte [gs:168],'E'
    mov byte [gs:169],0x0F
    mov byte [gs:170],'R'
    mov byte [gs:171],0x0F

; 获取内存容量,int 15, ax = E820h
.get_total_mem_bytes:
    xor ebx, ebx              ;第一次调用时,ebx值要为0
    mov edx, 0x534d4150	      ;edx只赋值一次,循环体中不会改变
    mov di, ards_buf	      ;ards结构缓冲区
.e820_mem_get_loop:	          ;循环获取每个ARDS内存范围描述结构
    mov eax, 0x0000e820	      ;执行int 0x15,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
    mov ecx, 20		          ;ARDS地址范围描述符结构大小是20字节
    int 0x15
    jc .failed_so_try_e801    ;若cf位为1则有错误发生,尝试0xe801子功能
    add di, cx		          ;使di增加20字节指向缓冲区中新的ARDS结构位置
    inc word [ards_nr]	      ;记录ARDS数量
    cmp ebx, 0		          ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
    jnz .e820_mem_get_loop

    ;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
    mov cx, [ards_nr]	      ;遍历每一个ARDS结构体,循环次数是ARDS的数量
    mov ebx, ards_buf 
    xor edx, edx		      ;edx为最大的内存容量,在此先清0
.find_max_mem_area:	          ;无须判断type是否为1,最大的内存块一定是可被使用
    mov eax, [ebx]	          ;base_add_low
    add eax, [ebx+8]	      ;length_low
    add ebx, 20		          ;指向缓冲区中下一个ARDS结构
    cmp edx, eax		      ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
    jge .next_ards
    mov edx, eax		      ;edx为总内存大小
.next_ards:
    loop .find_max_mem_area
    jmp .mem_get_ok

; 获取内存容量,int 15, ax = E801h
.failed_so_try_e801:
    mov ax,0xe801
    int 0x15
    jc .failed_so_try88       ;若当前e801方法失败,就尝试0x88方法

    ; 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
    mov cx,0x400	          ;cx和ax值一样,cx用做乘数
    mul cx 
    shl edx,16
    and eax,0x0000FFFF
    or edx,eax
    add edx, 0x100000         ;ax只是15MB,故要加1MB
    mov esi,edx	              ;先把低15MB的内存容量存入esi寄存器备份

    ; 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
    xor eax,eax
    mov ax,bx		
    mov ecx, 0x10000	      ;0x10000十进制为64KB
    mul ecx		              ;32位乘法,默认的被乘数是eax,积为64,32位存入edx,32位存入eax.
    add esi,eax		          ;由于此方法只能测出4G以内的内存,32位eax足够了,edx肯定为0,只加eax便可
    mov edx,esi		          ;edx为总内存大小
    jmp .mem_get_ok

; 获取内存容量,int 15, ah = 0x88
.failed_so_try88: 
    ;int 15后,ax存入的是以kb为单位的内存容量
    mov ah, 0x88
    int 0x15
    jc  .error_hlt
    and eax,0x0000FFFF
      
    ;16位乘法,被乘数是ax,积为32.积的高16位在dx中,积的低16位在ax中
    mov cx, 0x400      ;0x400等于1024,将ax中的内存容量换为以byte为单位
    mul cx
    shl edx, 16	       ;把dx移到高16位
    or  edx, eax	   ;把积的低16位组合到edx,32位的积
    add edx,0x100000   ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB
    jmp .mem_get_ok

;将内存换为byte单位后存入total_mem_bytes处。
.mem_get_ok:
    mov [total_mem_bytes], edx	 

; 打开A20地址线
.open_A20:
    in   al,0x92
    or   al,0000_0010B
    out  0x92,al

; 加载gdt描述符
.load_gdt:
    lgdt [gdt_ptr]

; 修改cr0标志寄存器的PE位
.change_cr0_PE:
    mov  eax, cr0
    or   eax, 0x00000001
    mov  cr0, eax

.jmp_bit_32:
    jmp  SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响
					                ; 远跳将导致之前做的预测失效,从而起到了刷新的作用。

.error_hlt:		      ;出错则挂起
    hlt


; 下面就是保护模式下的程序了
[bits 32]
p_mode_start:
    mov ax, SELECTOR_DATA
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp,LOADER_STACK_TOP
    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov byte [gs:320], 'M'
    mov byte [gs:322], 'A'
    mov byte [gs:324], 'I'
    mov byte [gs:326], 'N'

    call setup_page ; 创建页目录及页表并初始化页内存位图

    ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
    sgdt [gdt_ptr]	      ; 存储到原来gdt的位置

    ;将gdt描述符中视频段描述符中的段基址+0xc0000000
    mov ebx, [gdt_ptr + 2]  
    or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,0x18;段描述符的高4字节的最高位是段基址的31~24;将gdt的基址加上0xc0000000使其成为内核所在的高地址
    add dword [gdt_ptr + 2], 0xc0000000

    add esp, 0xc0000000        ; 将栈指针同样映射到内核地址

    ; 把页目录地址赋给cr3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax

    ; 打开cr0的pg位(31)
    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax

    ;在开启分页后,用gdt新的地址重新加载
    lgdt [gdt_ptr]             ; 重新加载

    mov byte [gs:320], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr
    mov byte [gs:322], 'i'     ;视频段段基址已经被更新,用字符v表示virtual addr
    mov byte [gs:324], 'r'     ;视频段段基址已经被更新,用字符v表示virtual addr
    mov byte [gs:326], 't'     ;视频段段基址已经被更新,用字符v表示virtual addr
    mov byte [gs:328], 'u'     ;视频段段基址已经被更新,用字符v表示virtual addr
    mov byte [gs:330], 'a'     ;视频段段基址已经被更新,用字符v表示virtual addr
    mov byte [gs:332], 'l'     ;视频段段基址已经被更新,用字符v表示virtual addr

    jmp $

setup_page:                      ; 创建页目录及页表
    mov ecx, 4096
    mov esi, 0
.clear_page_dir:                 ; 清理页目录空间
    mov byte [PAGE_DIR_TABLE_POS + esi], 0
    inc esi
    loop .clear_page_dir

.create_pde:				         ; 创建页目录
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性,属性全为0
    mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

    ;   下面将页目录项00xc00都存为第一个页表的地址,
    ;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
    ;   这是为将地址映射为内核地址做准备
    or eax, PG_US_U | PG_RW_W | PG_P	      ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
    mov [PAGE_DIR_TABLE_POS + 0x0], eax       ;1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
    mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
					                          ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
    sub eax, 0x1000
    mov [PAGE_DIR_TABLE_POS + 4092], eax	  ; 使最后一个目录项指向页目录表自己的地址

;下面创建第一个页表PTE,其地址为0x101000,也就是1MB+4KB的位置,需要映射前1MB内存
    mov ecx, 256				              ; 1M低端内存 / 每页大小4k = 256
    mov esi, 0
    mov edx, PG_US_U | PG_RW_W | PG_P	      ; 属性为7,US=1,RW=1,P=1
.create_pte:
    mov [ebx+esi*4],edx			              ; 此时的ebx已经在上面成为了第一个页表的地址,edx地址为0,属性为7
    add edx,4096                              ; edx+4KB地址
    inc esi                                   ; 循环256次
    loop .create_pte

;创建内核其它页表的PDE
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000 		                     ; 此时eax为第二个页表的位置
    or eax, PG_US_U | PG_RW_W | PG_P             ; 页目录项的属性为7
    mov ebx, PAGE_DIR_TABLE_POS
    mov ecx, 254			                     ; 范围为第769~1022的所有目录项数量
    mov esi, 769
.create_kernel_pde:
    mov [ebx+esi*4], eax
    inc esi
    add eax, 0x1000
    loop .create_kernel_pde
    ret


Reference

[1]《一个64位操作系统的设计与实现》
[2]《操作系统真象还原》

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

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

相关文章

开发TEE的踩坑之配置PCCS

系统&#xff1a;Ubuntu20.04&#xff08;双系统&#xff0c;非虚拟机&#xff09; 一、解决node.js的版本问题二、解决开启PCCS服务的问题1、解决开启PCCS服务2、解决访问本地的8081端口 本系列为笔者开发TEE&#xff08;Trusted Execution Environment&#xff0c;可信执行环…

k8s metrics-server服务监控pod 的 cpu、内存

项目场景&#xff1a; 需要开启指标服务&#xff0c;依据pod 的 cpu、内存使用率进行自动的扩容或缩容 pod 的数量 解决方案&#xff1a; 下载 metrics-server 组件配置文件&#xff1a; wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/…

【已解决】Python 中 AttributeError: ‘NoneType‘ object has no attribute ‘X‘ 报错

本文摘要&#xff1a;本文已解决 AttributeError: ‘NoneType‘ object has no attribute ‘X‘ 的相关报错问题&#xff0c;并总结提出了几种可用解决方案。同时结合人工智能GPT排除可能得隐患及错误。 &#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱…

HTML静态网页成品作业(HTML+CSS+JS)—— 美食企业曹氏鸭脖介绍网页(4个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;使用Javacsript代码实现 图片轮播切换&#xff0c;共有4个页面。 二、…

称重显示模块 Modbus RTU 通信

目录 一、智能称重数字显示器模块(带通信)1、称重传感器接线说明称重显示模块称重传感器USB 转 TTL 2、校准传感器&#xff08;标定&#xff1a;零点标定、满度标定&#xff09; 二、Modbus RTU 协议1、Modbus RTU 数据帧2、数据帧格式请求帧响应帧 三、上位机电脑与称重显示模…

使用神卓互联来访问单位内部web【内网穿透神器】

在现代工作环境中&#xff0c;有时我们需要从外部访问单位内部的 web 资源&#xff0c;而神卓互联这款内网穿透神器就能完美地满足这一需求。 使用神卓互联来访问单位内部 web 其实并不复杂&#xff0c;以下是大致的使用步骤和配置方法。 首先&#xff0c;我们需要在单位内部的…

基于Unet++在kaggle—2018dsb数据集上实现图像分割

目录 1. 作者介绍2. 理论知识介绍2.1 Unet模型介绍 3. 实验过程3.1 数据集介绍3.2 代码实现3.3 结果 4. 参考链接 1. 作者介绍 郭冠群&#xff0c;男&#xff0c;西安工程大学电子信息学院&#xff0c;2023级研究生 研究方向&#xff1a;机器视觉与人工智能 电子邮件&#xff…

【电机】开环控制系统和闭环控制系统

1 什么是控制系统 控制系统是指由控制主体、控制客体和控制媒体组成的具有自身目标和功能的管理系统。也可以理解为&#xff1a;为了使控制对象达到预期的稳定状态。例如一个水箱的温度控制&#xff0c;可以通过控制加热设备输出的功率进而来改变水温达到目标温度&#xff0c;…

Linux发邮件的工具推荐有哪些?如何配置?

Linux发邮件的功能怎么样&#xff1f;Linux系统如何设置服务器&#xff1f; 在Linux操作系统中&#xff0c;有多种工具可供选择用来发送电子邮件&#xff0c;每种工具都有其独特的特点和适用场景。AokSend将介绍几种常用的Linux发邮件工具&#xff0c;并分析它们的优缺点和适用…

接口自动化测试的全面解析与实战指南!

&#x1f680; 【引言】&#x1f680; 接口自动化测试&#xff0c;作为现代软件开发生命周期中的关键一环&#xff0c;扮演着“质量守门员”的角色。它不仅关乎提升开发速度&#xff0c;更在于确保每一次更新都能可靠地满足用户期待。接下来&#xff0c;我们将踏上一场深入浅出…

Redis分布式锁的实现、优化与Redlock算法探讨

Redis分布式锁最简单的实现 要实现分布式锁,首先需要Redis具备“互斥”能力,这可以通过SETNX命令实现。SETNX表示SET if Not Exists,即如果key不存在,才会设置它的值,否则什么也不做。利用这一点,不同客户端就能实现互斥,从而实现一个分布式锁。 举例: 客户端1申请加…

RH850---注意问题积累--1

硬件规格(引脚分配&#xff0c;内存映射&#xff0c;外设功能规格、电气特性、时序图)和操作说明 注意:有关使用的详细信息&#xff0c;请参阅应用说明 ---------外围函数。。。 1:存储指令完成与后续同步指令的一代 当控制寄存器被存储指令更新时&#xff0c;从存储的执行开始…

在网站建设时,如何选择适合自己的网站模版

可以根据以下几个地方选择适合的网站模板 1.公司的核心业务 根据公司的业务内容来确定网站展示的内容之一&#xff0c;不同的业务内容可以有不同的展示方式&#xff0c;以此来确定网站的展示风格之一&#xff0c;公司肯定是要有明确的业务内容&#xff0c;并且能够在网站…

[C#]winform使用onnxruntime部署LYT-Net轻量级低光图像增强算法

【训练源码】 https://github.com/albrateanu/LYT-Net 【参考源码】 https://github.com/hpc203/Low-Light-Image-Enhancement-onnxrun 【算法介绍】 一、研究动机 1.研究目标 研究的目标是提出一种轻量级的基于YUV Transformer 的网络&#xff08;LYT-Net&#xff09;&…

neo4j-官网学习

1、cypher 代码学习文档 https://neo4j.com/docs/cypher-cheat-sheet/5/auradb-enterprise 2、APOC函数包安装&#xff08;desktop&#xff09; 直接点击就可以安装&#xff0c;安装完之后重启一下&#xff0c;Cypher查询中使用CALL apoc.help(‘apoc’)来检查APOC插件是否已…

Java技术驱动的工程项目管理系统源码:工程管理的数字化解决方案

工程项目管理系统是一款基于Java技术的专业工程管理软件&#xff0c;它采用了Spring Cloud、Spring Boot、Mybatis、Vue和ElementUI等前沿技术&#xff0c;通过前后端分离架构构建了一个功能全面的工程项目管理系统。 随着公司的发展&#xff0c;工程管理的需求日益增长&#x…

图像处理与视觉感知复习--彩色图像处理

文章目录 三原色原理及其两种应用常用彩色模型及其应用领域各种颜色模型的转换彩色图像处理 三原色原理及其两种应用 三基色原理 自然界中绝大多数的颜色都可看作是由红、绿、蓝三种颜色组合而成&#xff1b;自然界中的绝大多数的颜色都可以分解成红、绿、蓝这三种颜色。这即…

渗透测试模拟实战-tomexam网络考试系统

渗透测试&#xff0c;也称为“pentest”或“道德黑客”&#xff0c;是一种模拟攻击的网络安全评估方法&#xff0c;旨在识别和利用系统中的安全漏洞。这种测试通常由专业的安全专家执行&#xff0c;他们使用各种技术和工具来尝试突破系统的防御&#xff0c;如网络、应用程序、主…

【PyQt5】简要介绍

文章目录 一、PyQt5的简介、安装、配置1.1 简介1.2 安装与配置1.3 QtDesigner1.3.1 基础操作 二、PyQt5的基本控件&#xff08;Widget Box&#xff09;2.1 基类&#xff08;QWidget&#xff09;2.1.1 QWidget 2.2 Button类&#xff08;属于QtWidgets&#xff1a;QPushButton&am…

轮到国产游戏统治Steam榜单

6月10日晚8点&#xff0c;《黑神话:悟空》实体版正式开启全款预售,预售开启不到5分钟,所有产品即宣告售罄。 Steam上&#xff0c;《黑神话:悟空》持续占据着热销榜榜首的位置。 但在《黑神话:悟空》傲人的光环下&#xff0c;还有一款国产游戏取得出色的成绩。 6月10日&#…