这才是计科之 Onix XV6 源码分析(1、XV6-x86的启动)

这才是计科之 Onix & XV6 源码分析(1、XV6-x86的启动)

前言

Onix是一款相对于XV6来说功能更为健全的单核OS,由于功能更加完善,Onix也更加复杂。代码阅读起来会比较绕。

XV6是一款简单易读的多核操作系统,但其功能相对Onix来说更为简陋,比如Onix对物理内存的管理采用了位图、内核内存和进程相关的内存进行了分开管理,页目录使用单独的内核内存,没有和页表、页框混用等。而XV6显得非常简陋。尽管XV6的实验可以弥补部分缺陷。

Onix操作系统也实现了bootloader,对于将OS加载到内存的操作,Onix是采用汇编进行内核加载,并且在加载内核前,还会进行一个内存探测的操作,所以Onix的bootloader稍微有些复炸。而XV6操作系统的启动操作写的非常简洁,加载内核的操作采用的是C语言的形式,非常利于阅读学习,但是XV6不会进行内存探测。为求方便,本文主要叙述XV6的启动流程。

Onix相关链接:

  • github仓库链接

  • B站配套视频链接

XV6-x86的github链接:

  • 链接

Makefile & kernel.ld文件的分析

在叙述os启动前,必须要了解其Makefile是怎么写的。同时,在了解bootmain从镜像中加载os的代码到内存中前,因为os是elf格式,所以我们需要了解os的link脚本是怎么布局的。以便我们能更好掌握os的内存布局。

Makefile

这里贴出makefile比较关键的代码:

# 利用dd命令制作OS镜像,依赖bootblock和kernel,
# 首先划分了10000个扇区
# 然后将bootblock写到了第0号扇区
# 最后从1号扇区开始,填入OS的代码文件。
xv6.img: bootblock kerneldd if=/dev/zero of=xv6.img count=10000dd if=bootblock of=xv6.img conv=notruncdd if=kernel of=xv6.img seek=1 conv=notrunc# 单独产生os的bootloader模块,并且该模块是使用$(OBJCOPY)产生,
# 所以没有elf文件头信息,只是单纯的二进制可执行文件。并且$(LD)规定
# 代码的入口点是start、并且从地址0x7C00(物理地址)开始,最最的文件名是bootblock
bootblock: bootasm.S bootmain.c$(CC) $(CFLAGS) -fno-pic -O -nostdinc -I. -c bootmain.c$(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c bootasm.S$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o$(OBJDUMP) -S bootblock.o > bootblock.asm$(OBJCOPY) -S -O binary -j .text bootblock.o bootblock./sign.pl bootblock# 产生AP cpu的boot代码
# 指定程序的加载地址是0x7C00(物理地址)
# 可执行代码的格式和bootblock相同,纯粹的二进制程序,没有elf的头部信息
# 但最终输出的entryother会和kernel合并
entryother: entryother.S$(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c entryother.S$(LD) $(LDFLAGS) -N -e start -Ttext 0x7000 -o bootblockother.o entryother.o$(OBJCOPY) -S -O binary -j .text bootblockother.o entryother$(OBJDUMP) -S bootblockother.o > entryother.asm# 产生第一个进程,init进程的boot代码
# 同样以$(OBJCOPY)提取纯粹的可执行代码,没有elf头部信息,输出文件名为initcode
# 指定程序的加载地址是0(虚拟地址)
initcode: initcode.S$(CC) $(CFLAGS) -nostdinc -I. -c initcode.S$(LD) $(LDFLAGS) -N -e start -Ttext 0 -o initcode.out initcode.o$(OBJCOPY) -S -O binary initcode.out initcode$(OBJDUMP) -S initcode.o > initcode.asm# 产生内核的elf可执行文件
kernel: $(OBJS) entry.o entryother initcode kernel.ld$(LD) $(LDFLAGS) -T kernel.ld -o kernel entry.o $(OBJS) -b binary initcode entryother$(OBJDUMP) -S kernel > kernel.asm$(OBJDUMP) -t kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > kernel.sym

这里编译器、链接器的选项具体作用读者可以自行百度,这里只阐述比较关键的部分。

首先是利用dd命令制作xv6.img镜像,从代码中可以很清楚的看到,bootblock填到了第0号扇区、kernel填到了1号以及之后的扇区。bootblock作用就是使cpu从实时模式转换为保护模式,然后将kernel从磁盘上加载到内存。这里要注意一个特殊的数字0x7C00,它出现在生成bootblock二进制文件的$(LD)阶段,这里暗示了bootblock代码在加载进内存时应该被放在0x7C00的位置。事实也是如此,在BIOS完成硬件初始化后,就会将第0号扇区(一个扇区一般就是512字节)的512字节代码加载到内存的0x7C00的位置,然后BIOS就会让eip指向0x7C00的位置,去执行bootasm.S里面的汇编代码。

这里有个关键点:在使用$(LD)命令生成bootblock.o时,命令参数部分bootasm.o放在bootmain.o前面,会导致链接时,bootasm.o代码就会靠前,这样在eip执行0x7C00位置的代码时,一定是从start开始。

bootblock是由$(OBJCOPY)生成,$(OBJCOPY)的作用就是去除elf文件中的各种头部,因为BIOS只负责从第0号扇区加载bootblock,不会解析elf文件,所以,$(OBJCOPY)去提取纯粹的二进制是非常有必要的!

bootblock具体细节下面会详细探讨

kernel.ld

这里贴出kernel.ld比较关键的代码:

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)SECTIONS
{/* Link the kernel at this address: "." means the current address *//* Must be equal to KERNLINK */. = 0x80100000; // 定义代码起始的虚拟地址.text : AT(0x100000) {  // 定义了起始加载地址*(.text .stub .text.* .gnu.linkonce.t.*)}/*省略...*/
}

从kernel的链接脚本我们可以看到,kernel的起始虚拟内存地址是0x8010 0000,内核实际加载的物理地址是x100000,由AT定义。这里我反复标注了 虚拟地址 / 物理地址 ,这两者一定要分清!

从实时模式到保护模式

本段代码实现位于xv6的bootasm.S文件。具体细节如下,xv6使用的AT&T的汇编,还是比较好懂的,读者有疑问的话,可以百度去搜相关指令的作用。英文注释已经非常详细,我就直接引用了。

#include "asm.h"
#include "memlayout.h"
#include "mmu.h"# Start the first CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00..code16                       # Assemble for 16-bit mode
.globl start
start:cli                         # BIOS enabled interrupts; disable# Zero data segment registers DS, ES, and SS.xorw    %ax,%ax             # Set %ax to zeromovw    %ax,%ds             # -> Data Segmentmovw    %ax,%es             # -> Extra Segmentmovw    %ax,%ss             # -> Stack Segment# 硬件相关,主线是OS的启动,该部分不深究也没影响,其实就是一个固定步骤。# Physical address line A20 is tied to zero so that the first PCs # with 2 MB would run software that assumed 1 MB.  Undo that.
seta20.1:inb     $0x64,%al               # Wait for not busytestb   $0x2,%aljnz     seta20.1movb    $0xd1,%al               # 0xd1 -> port 0x64outb    %al,$0x64seta20.2:inb     $0x64,%al               # Wait for not busytestb   $0x2,%aljnz     seta20.2movb    $0xdf,%al               # 0xdf -> port 0x60outb    %al,$0x60# Switch from real to protected mode.  Use a bootstrap GDT that makes# virtual addresses map directly to physical addresses so that the# effective memory map doesn't change during the transition.lgdt    gdtdescmovl    %cr0, %eaxorl     $CR0_PE, %eaxmovl    %eax, %cr0########################### 以下正式进入32位保护模式
//PAGEBREAK!# Complete the transition to 32-bit protected mode by using a long jmp# to reload %cs and %eip.  The segment descriptors are set up with no# translation, so that the mapping is still the identity mapping.ljmp    $(SEG_KCODE<<3), $start32.code32  # Tell assembler to generate 32-bit code now.
start32:# Set up the protected-mode data segment registersmovw    $(SEG_KDATA<<3), %ax    # Our data segment selectormovw    %ax, %ds                # -> DS: Data Segmentmovw    %ax, %es                # -> ES: Extra Segmentmovw    %ax, %ss                # -> SS: Stack Segmentmovw    $0, %ax                 # Zero segments not ready for usemovw    %ax, %fs                # -> FSmovw    %ax, %gs                # -> GS# Set up the stack pointer and call into C.movl    $start, %esp            # 这里将esp栈设置到了start,由于栈向低地址处增长,所以刚好和bootasm文件的代码背道而驰。call    bootmain# If bootmain returns (it shouldn't), trigger a Bochs# breakpoint if running under Bochs, then loop.movw    $0x8a00, %ax            # 0x8a00 -> port 0x8a00movw    %ax, %dxoutw    %ax, %dxmovw    $0x8ae0, %ax            # 0x8ae0 -> port 0x8a00outw    %ax, %dx
spin:jmp     spin# Bootstrap GDT
.p2align 2                                # force 4 byte alignment
gdt:SEG_NULLASM                             # null segSEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)   # code segSEG_ASM(STA_W, 0x0, 0xffffffff)         # data seggdtdesc:.word   (gdtdesc - gdt - 1)             # sizeof(gdt) - 1.long   gdt                             # address gdt

这里先普及一下实时模式和保护模式的区别:

  • 实时模式:为兼容以前的PC。特点是:寄存器都是16位。寻址方式:16位的段寄存器 + 16位的偏移寄存器,最大寻址范围是20位。

  • 保护模式:现代CPU的寻址方式。特点是:寄存器有16位、32为、64位。寻址方式:段描述符 + 32位偏移寄存器。最大寻址范围4G+。

bootasm.S是操作系统被加载内存前,最先开始执行的代码。BIOS是运行在实时模式下的程序,只拥有1M的寻址空间(boot代码被加载到0x7C00 < 1M 就能证明存在1M的限制),所以在cpu拥有4G寻址空间前,还需要进行一些初始化操作,

从实时模式 -> 保护模式的转变流程非常固定,在xv6的bootasm中实现如下:

  1. 对于多核处理器,最先启动的CPU(我们称为BSP(bootstrap processor)),BSP一上电就是实时模式。其余的从处理器(我们称为AP)在后面的内核初始化阶段会被BSP依次唤醒并初始化。

  2. 关中断。清空各种段寄存器,包括ds、es、ss。

  3. 打开A20地址线.

  4. 加载全局描述符表。即使用lgdt指令将表的地址和大小放在GDTR中。(这里的全局描述符表是临时的,后面内核初始化会更换一张gdt表,那张表更加完善。

  5. 将CR0寄存器第0位设置为1(PE位),此时正式转换成保护模式。

  6. 使用ljmp(长跳转指令)指令刷新段寄存器,跳到start32。

    # xv6对ljmp的注释如下:
    # Complete the transition to 32-bit protected mode by using a long jmp
    # to reload %cs and %eip. 
    
  7. 初始化数据段、栈段寄存器,将esp设置到0x7C00处,跳到bootmain函数中,该函数会将XV6 OS加载到内存。

相关的结构如下:

段描述符(Descriptor),描述一段地址的特性,包括基地址、范围、权限、粒度(范围以字节为单位还是以4K为单位)、类型(代码/数据)等信息:

在这里插入图片描述

全局描述符表,由许多个8字节段描述符组成的表:

在这里插入图片描述

全局描述符表寄存器,其基地址的内容是全局描述符表的首地址,界限是全局描述符表的大小:

在这里插入图片描述

段选择子,【代码、数据、栈等】段使用了哪个段描述符,索引号指使用段描述符在全局描述符表的偏移(8字节为单位),第2位指明是全局描述符还是局部描述符,0~1位指示段的特权级:

在这里插入图片描述

从低特权级的段空间 跳到 高特权级的段空间就会发生cpu特权级的切换,cpu就是通过全局描述符表来确定一个段的特权级。最典型的就是用户进程调用系统调用产生的特权级切换,这中间涉及查tss段、切栈等复杂操作,我们后面在进行详细的讨论。

对于GDT的详细描述,这里推荐两篇博客,这两篇博客写的真的非常好!相信阅读之后,对全局描述符会有一个清晰的认识,全局描述符的几幅图片也是取自这两篇文章,如有侵权,可告知删除:

详细介绍了段描述符各个位的作用

可以作为扩展,里面介绍了局部描述符的作用

正式将XV6加载到内存

分析该部分细节前,先需要了解一下elf文件的格式,我们重点关注elf文件的ELF header和Program header table。直接上各个字段的描述:

引用自博客https://zhuanlan.zhihu.com/p/165336511如有侵权,可告知删除:

#define ELF_MAGIC 0x464C457FU  // "\x7FELF" in little endian// ELF 文件的头部
struct elfhdr {uint magic;       // 4 字节,为 0x464C457FU(大端模式)或 0x7felf(小端模式)// 表明该文件是个 ELF 格式文件uchar elf[12];    // 12 字节,每字节对应意义如下://     0 : 1 = 32 位程序;2 = 64 位程序//     1 : 数据编码方式,0 = 无效;1 = 小端模式;2 = 大端模式//     2 : 只是版本,固定为 0x1//     3 : 目标操作系统架构//     4 : 目标操作系统版本//     5 ~ 11 : 固定为 0ushort type;      // 2 字节,表明该文件类型,意义如下://     0x0 : 未知目标文件格式//     0x1 : 可重定位文件//     0x2 : 可执行文件//     0x3 : 共享目标文件//     0x4 : 转储文件//     0xff00 : 特定处理器文件//     0xffff : 特定处理器文件ushort machine;   // 2 字节,表明运行该程序需要的计算机体系架构,// 这里我们只需要知道 0x0 为未指定;0x3 为 x86 架构uint version;     // 4 字节,表示该文件的版本号uint entry;       // 4 字节,该文件的入口地址,没有入口(非可执行文件)则为 0uint phoff;       // 4 字节,表示该文件的“程序头部表”相对于文件的位置,单位是字节uint shoff;       // 4 字节,表示该文件的“节区头部表”相对于文件的位置,单位是字节uint flags;       // 4 字节,特定处理器标志ushort ehsize;    // 2 字节,ELF文件头部的大小,单位是字节ushort phentsize; // 2 字节,表示程序头部表中一个入口的大小,单位是字节ushort phnum;     // 2 字节,表示程序头部表的入口个数,// phnum * phentsize = 程序头部表大小(单位是字节)ushort shentsize; // 2 字节,节区头部表入口大小,单位是字节ushort shnum;     // 2 字节,节区头部表入口个数,// shnum * shentsize = 节区头部表大小(单位是字节)ushort shstrndx;  // 2 字节,表示字符表相关入口的节区头部表索引
};// 程序头表
struct proghdr {uint type;        // 4 字节, 段类型//         1 PT_LOAD : 可载入的段//         2 PT_DYNAMIC : 动态链接信息//         3 PT_INTERP : 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小//         4 PT_NOTE : 指定辅助信息的位置和大小//         5 PT_SHLIB : 保留类型,但具有未指定的语义//         6 PT_PHDR : 指定程序头表在文件及程序内存映像中的位置和大小//         7 PT_TLS : 指定线程局部存储模板uint off;         // 4 字节, 段的第一个字节在文件中的偏移uint vaddr;       // 4 字节, 段的第一个字节在内存中的虚拟地址uint paddr;       // 4 字节, 段的第一个字节在内存中的物理地址(适用于物理内存定位型的系统)uint filesz;      // 4 字节, 段在文件中的长度uint memsz;       // 4 字节, 段在内存中的长度uint flags;       // 4 字节, 段标志//         1 : 可执行//         2 : 可写入//         4 : 可读取uint align;       // 4 字节, 段在文件及内存中如何对齐
};

如果感兴趣的话,可以参考文章:ELF文件格式的详解,这篇文章讲解的更为详细,但是对于现阶段来说,可以不用了解太仔细,知道elf文件的ELF header和Program header table各个字段的作用就足够你继续学习XV6操作系统。

对kernel elf文件解析的主流程:

void
bootmain(void)
{struct elfhdr *elf;struct proghdr *ph, *eph;void (*entry)(void);uchar* pa;// 预留足够空间// 46Kelf = (struct elfhdr*)0x10000;  // scratch space// 以0为偏移读4096个字节,读elf文件头// Read 1st page off diskreadseg((uchar*)elf, 4096, 0);// 判断elf文件魔数。// Is this an ELF executable?if(elf->magic != ELF_MAGIC)return;  // let bootasm.S handle error// 定位到Program header table// Load each program segment (ignores ph flags).ph = (struct proghdr*)((uchar*)elf + elf->phoff);// 程序头部表个数eph = ph + elf->phnum;// 一个段一个段的读for(; ph < eph; ph++){  // 以struct proghdr为单位自增。// 应该加载到的物理内存,xv6中是0x100000pa = (uchar*)ph->paddr;// 读取整个段到pa中readseg(pa, ph->filesz, ph->off);if(ph->memsz > ph->filesz)// mem大小比file大小大,多余的补零stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);}// Call the entry point from the ELF header.// Does not return!entry = (void(*)(void))(elf->entry);  // 在xv6的kernel.ld中描述为_startentry();
}

前半部分做的操作就是不断从kernel的elf文件中按照程序头将各个段读取到内存。后面通过elf头中的entry,该字段保存内核入口点_start(这可以通过阅读kernel.ld文件来证明),去执行_start开始的代码,这里要提醒读者的是,到目前为止,对cpu来说状态是:保护模式 & 已经装填了全局描述符 & 一切地址皆是物理地址。而kernel的elf文件中代码都是使用的(0x8010 0000开始的)虚拟地址,所以我们后面在entry.S中会看到,在给_start赋值前,会通过一个宏将真正的入口地址的虚拟地址转换为XV6加载到内存的物理地址。从而我们在物理地址寻址模式下,可以利用_start准确跳转到xv6的入口代码。

需要注意的是:在执行bootasm汇编代码时,esp的位置是不确定的,唯一能确定的是esp在1M空间之内。在bootasm汇编最后才会将esp挪到0x7c00的位置!

在bootmain执行完毕后,内存布局如下:

在这里插入图片描述

接下来深入分析一下从磁盘读取文件的细节,这些内容都是和硬件相关的,通过操作硬件寄存器来实现读写磁盘,这部分深入下去也是挺让人头大的,作者也是菜鸟一个,也就力所能及的叙述一些自己明白的东西吧。

void
waitdisk(void)
{// Wait for disk ready.while((inb(0x1F7) & 0xC0) != 0x40);
}// readsect也引用自https://zhuanlan.zhihu.com/p/165336511,如有侵权,可联系我删除
// Read a single sector at offset into dst.
void
readsect(void *dst, uint offset)
{// Issue command.waitdisk();outb(0x1F2, 1);   // count = 1          // 要读取的扇区数量 count = 1outb(0x1F3, offset);                    // 扇区 LBA 地址的 0-7 位outb(0x1F4, offset >> 8);               // 扇区 LBA 地址的 8-15 位outb(0x1F5, offset >> 16);              // 扇区 LBA 地址的 16-23 位outb(0x1F6, (offset >> 24) | 0xE0);     // offset | 11100000 保证高三位恒为 1//         第7位     恒为1//         第6位     LBA模式的开关,置1为LBA模式//         第5位     恒为1//         第4位     为0代表主硬盘、为1代表从硬盘//         第3~0位   扇区 LBA 地址的 24-27 位outb(0x1F7, 0x20);  // cmd 0x20 - read sectors  // 20h为读,30h为写// Read data.waitdisk();insl(0x1F0, dst, SECTSIZE/4); // 读的时候以4字节位单位读,所以需要扇区除以4,代表要读的次数
}// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked.
void
readseg(uchar* pa, uint count, uint offset)
{uchar* epa; // end phy addrepa = pa + count; // 结束地址// 这里将起始物理地址回退了offset的SECTSIZE(512)余数个Byte// 因为在从磁盘上读数据的时候,以512字节进行读取。所以offset会以512为单位向下取整,// 被换算成扇区号的偏移。如果offset原来不是SECTSIZE的整数倍,向下取整会导致offset截断,// 记换算后的offset为ofs,此时如果直接读取ofs扇区会多读offset % SECTSIZE个字节,// 所以需要提前将pa的地址减去offset % SECTSIZE个字节来排除offset被截断的多余的字节。// Round down to sector boundary.pa -= offset % SECTSIZE;    // 将offset换算成扇区号// Translate from bytes to sectors; kernel starts at sector 1.offset = (offset / SECTSIZE) + 1; // 依次读取每个扇区,最后一个扇区多读了也没关系。for(; pa < epa; pa += SECTSIZE, offset++)readsect(pa, offset);
}

仔细推敲readseg函数中pa回滚的操作,总感觉这样贸然回滚,会覆盖之前已经被加载到内存中的代码。既然xv6敢这样写,八成说明是没有问题的,具体的原因,作者实在琢磨不透,如果有了解的朋友,可以在评论区交流一下。

跳到_start,正式进入内核前的预初始化

相关代码文件是entry.S

#include "asm.h"
#include "memlayout.h"
#include "mmu.h"
#include "param.h"# Multiboot header.  Data to direct multiboot loader.
.p2align 2
.text
.globl multiboot_header
multiboot_header:#define magic 0x1badb002#define flags 0.long magic.long flags.long (-magic-flags)# 这里就能看到,因为elf描述的一些标签是使用的虚拟地址,
# 而在进入entry开启分页前都是使用的物理地址,所以使用了
# V2P_WO宏将entry转换成了物理地址。方便bootmain跳转到entry.
# V2P_WO展开就是将输入的地址减去 0x8000 0000 的偏移。
.globl _start
_start = V2P_WO(entry)# Entering xv6 on boot processor, with paging off.
.globl entry
entry:# 打开4M big page开关# Turn on page size extension for 4Mbyte pagesmovl    %cr4, %eaxorl     $(CR4_PSE), %eaxmovl    %eax, %cr4# 将页目录设置为entrypgdir,同样由于在启用虚拟内存前,# 需要将虚拟地址entrypgdir转换为物理地址,有关entrypgdir# 的定义在内存管理章节进行详细叙述。# Set page directorymovl    $(V2P_WO(entrypgdir)), %eaxmovl    %eax, %cr3# 开启分页# Turn on paging.movl    %cr0, %eaxorl     $(CR0_PG|CR0_WP), %eaxmovl    %eax, %cr0########################### 以下正式进入分页模式,地址皆是虚拟地址# 再一次修改esp指针,将esp移到内核代码范围中# Set up the stack pointer.movl $(stack + KSTACKSIZE), %esp# 真正进入内核main函数,开始各种初始化。mov $main, %eaxjmp *%eax.comm stack, KSTACKSIZE

该部分代码负责进入main函数前的初始化,主要工作如下:

  1. 打开4M big page分页开关,让cpu支持4M大页。entrypgdir会将内核区域与映射为物理地址低4M的大页,我们后面会详细进行讨论,entrypgdir的生命周期非常短,在main函数中初始化过程中,会另外产生一个粒度更小的页表kpgdir(4K为一页),该页表会一直作为xv6的内核页表。

  2. 设置entrypgdir为BSP(booststrap processor)的页目录。

  3. 开启分页。

  4. 修改esp。指向内核自己分配的4K大小的栈上。

  5. 进入main。

在entry.S代码执行完后,cpu开启分页模式,所有的地址都将以虚拟地址的形式存在。此时内存布局如下:

在这里插入图片描述

至此,cpu的预初始化进行完毕。下面几章将围绕内核的初始化去讲解类unix操作系统的内存管理、进程调度、文件系统子模块。


本章完结

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

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

相关文章

【JMeter接口测试工具】第一节.JMeter简介和安装【入门篇】

文章目录 前言一、JMeter简介 1.1 JMeter基本介绍 1.2 JMeter优缺点二、JMeter安装 2.1 JMeter安装步骤 2.2 JMeter环境配置三、项目介绍 3.1 项目简介 3.2 API接口清单总结 前言 一、JMeter简介 1.1 JMeter基本介绍 JMeter 是 Apache 组织使用…

java---程序逻辑控制(详解)

目录 一、概述二、顺序结构三、分支结构3.1 if语句3.1.1 语法格式13.1.2 语法格式23.1.3 语法格式3 3.2 练习3.2.1 判断一个数字是奇数还是偶数3.2.2 判断一个数字是正数&#xff0c;负数&#xff0c;还是零3.2.3 判断一个年份是否为闰年 3.3.switch语句 四、循环结构4.1 while…

Flutter vscode环境如何进行真机测试

目录 1. 准备工作 1.1 安装Flutter和VS Code 1.2 安装必要的VS Code扩展 1.3 手机设置 2. 配置VS Code调试环境 3. 手机如何退出开发者模式 1. 准备工作 1.1 安装Flutter和VS Code 确保你已经在电脑上安装了Flutter SDK和VS Code。如果还没有&#xff0c;可以参考以下指…

项目文章 | Nature Commun蓝藻转录因子PhoB对磷/铁的营养元素限制的调控机制

近日&#xff0c;华中师范大学邱保胜教授团队在《Nature Communications》发表题为“Phosphorus deficiency alleviates iron limitation in Synechocystis cyanobacteria through direct PhoB-mediated gene regulation”文章&#xff0c;其重点研究了Synechocystis蓝藻转录因…

硬件产品经理

边端协调管理平台 主页模型管理配置管理设备管理设备检测组态数据服务传输通道服务 定义与范围&#xff1a; 边测&#xff1a;通常指的是边缘计算的测试&#xff0c;这里的“边缘”可以理解为离用户更近的计算节点或设备&#xff0c;如小 型数据中心、具有计算能力的小基站等。…

深度学习课程设计:构建未来的教育蓝图

深度学习课程设计&#xff1a;构建未来的教育蓝图 在近年来&#xff0c;深度学习已经从一项前沿的技术发展成为计算机科学领域不可或缺的一部分。随着其在多个行业中的应用日益增多&#xff0c;对深度学习教育的需求也在急剧上升。对于计划将深度学习纳入学术课程的教育者而言…

【WRF理论第二期】运行模型的基础知识

WRF理论第二期&#xff1a;运行模型的基础知识 1 Basics for Running the Model2 Geogrid程序2.1 Geogrid2.2 Terrestrial Input Data 3 Ungrid程序3.1 Ungrid3.2 Intermediate Files3.3 Required Fields 4 Metgrid程序参考 官方介绍-Basics for Running the Model 本博客主要…

耐用好用充电宝有哪些?畅销排行榜前四款充电宝推荐

在日常生活中&#xff0c;一款耐用且好用的充电宝是我们出行必备的利器&#xff0c;它可以为我们的手机、平板等设备提供持续的电力支持。然而&#xff0c;在市面上琳琅满目的充电宝品牌中&#xff0c;究竟哪些才是真正耐用又好用的选择&#xff1f;为了帮助大家更好地了解市场…

Qt5学习笔记(一):Qt Widgets Application项目初探

笔者长期使用MFC开发Windows GUI软件。随着软件向Linux平台迁移的趋势越发明朗&#xff0c;GUI程序的跨平台需求也越来越多。因此笔者计划重新抓一下Qt来实现跨平台GUI程序的实现。 0x01. 看看Qt Widgets Application项目结构 打开Qt5&#xff0c;点击“ New”按钮新建项目。…

基于Kubernetes和DeepSpeed进行分布式训练的实战教程

目录 ​编辑 一、前期准备 二、部署和配置训练任务 三、编写和运行训练代码 四、监控和调优 五、代码实现 5.1. Dockerfile 5. 2. DeepSpeed 配置文件 (ds_config.json) 5.3. Kubernetes 部署文件 (deployment.yaml) 5.4. PyTorch 训练脚本 (train.py) 注意事项&am…

windows任意窗口置顶/前台显示/不被最小化或遮挡

问题&#xff1a;在办公时&#xff0c;当同时需要打开好几个重要的窗口&#xff0c;比如需要对若干个文件夹里的文件进行操作&#xff0c;几个窗口都需要一直在桌面前台显示&#xff0c;但这样的话容易在打开其他页面或是切其他窗口的时候被遮挡&#xff0c;因此考虑如何让几个…

我们如何用npm发布自己的插件包?详细的教程来了

一、什么是npm插件&#xff1f; npm&#xff08;“Node 包管理器”&#xff09;是 JavaScript 运行时 Node.js 的默认程序包管理器。npm插件是指通过npm安装的第三方包&#xff0c;可以在Node.js项目中直接使用。这些插件涵盖了各种领域&#xff0c;包括Web开发、数据测试、构建…

用于精准治疗和预防细菌感染的生物功能脂质纳米颗粒

引用信息 文 章&#xff1a;Biofunctional lipid nanoparticles for precision treatment and prophylaxis of bacterial infections. 期 刊&#xff1a;Science Advances&#xff08;影响因子&#xff1a;13.6&#xff09; 发表时间&#xff1a;2024年4月5日 作 者&a…

【Python Cookbook】S01E21 文本模式的匹配和查找 match()、search()、findall() 以及 捕获组和 + 的含义

目录 问题解决方案讨论 问题 本文讨论一些按照特定的文本模式进行的查找和匹配。 解决方案 如果想要匹配的只是简单文字&#xff0c;通常我们使用一些内置的基本字符串方法即可&#xff0c;如&#xff1a;str.find()&#xff0c;str.startwith()&#xff0c;str.endswith() …

Docker:搭建实用的个人IT工具箱IT-Tools

请关注微信公众号&#xff1a;拾荒的小海螺 博客地址&#xff1a;http://lsk-ww.cn/ 1、简述 IT-Tools是一款开源的个人工具箱&#xff0c;专为IT从业人员打造&#xff0c;支持Docker私有化部署&#xff0c;包含众多实用的IT工具。其功能丰富多样&#xff0c;涵盖二维码生成、…

SpringBootWeb 篇-深入了解 AOP 面向切面编程与 AOP 记录操作日志案例

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 AOP 概述 1.1 构造简单 AOP 类 2.0 AOP 核心概念 2.1 AOP 执行流程 3.0 AOP 通知类型 4.0 AOP 通知顺序 4.1 默认按照切面类的类名字母排序 4.2 用 Order(数字) 注…

Redis集群之高可用可水平扩展

文章目录 一、Redis集群方案比较二、Redis高可用集群搭建三、Java操作redis集群四、集群的Spring Boot整合Redis 一、Redis集群方案比较 在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态&#xff0c;如果master节点异 常&#xff0c;则会做主…

解决nvidia驱动和CUDA升级问题

解决nvidia驱动和CUDA升级问题 注释&#xff1a;升级高版本的nvidia驱动和cuda是不影响现有的docker镜像和容器的。因为是向下兼容的。仅仅升级后重启服务器即可。 ERROR: An NVIDIA kernel module ‘nvidia-drm’ appears to already be loaded in your kernel. This may be…

Java(十二)——Comparable接口与Comparator接口

文章目录 Comparable与Comparator接口Comparable接口Comparator接口 Comparable与Comparator接口 我们可能会遇到这样的问题&#xff1a;怎么对一个对象数组进行排序&#xff1f; 比如对一个狗类对象数组进行排序&#xff0c;而想到这&#xff0c;我们又会有一个问题&#xff…

Java学习中,如何理解注解的概念及常用注解的使用方法

一、简介 Java注解&#xff08;Annotation&#xff09;是一种元数据&#xff0c;提供了一种将数据与程序元素&#xff08;类、方法、字段等&#xff09;关联的方法。注解本身不改变程序的执行逻辑&#xff0c;但可以通过工具或框架进行处理&#xff0c;从而影响编译、运行时的…