Linux0.12内核源码解读(5)-head.s

news2024/11/23 18:24:37

大家好,我是呼噜噜,好久没有更新old linux了,本文接着上一篇文章图解CPU的实模式与保护模式,继续向着操作系统内核的世界前进,一起来看看heads.s

as86 与GNU as

首先我们得了解一个事实,在Linux0.12内核源码中,其实是使用了2套汇编器Assembler的,一种是Intel8086汇编编译器as86和配套的链接器ld86,并一种就是GNU as(gas),使用 GNU ld 链接器来链接产生的目标文件。

为什么使用了2套汇编器?

我们知道Linux0.12bootsect.s和setup.s实模式下运行的16位代码程序,而那个时候的GNU as 汇编编译器无法支持16位实模式代码程序编译,所以Linus不得不使用as86和ld86,其语法近似Intel语法

而从head.s开始的,内核完全都是在保护模式下运行了,操作系统system模块中其余所有汇编语言程序(包括 C 语言产生的汇编程序)都是使用GNU as 汇编编译器,使用的是AT&T语法。直到Linux内核2.4.x后,bootsect.s和head.s程序才完全使用统一的GNU as 来编写

2种语法虽然是有所区别,但其实都是类似的,需要注意的最基本的区别是,AT&T语法中,mov赋值的方向是从左到右

在Linux0.12内核源码解读(3)-Setup.S中,最后我们说到CPU 进入了 32 位保护模式,跳到了内存零地址处开始执行代码。先来回顾一下执行完setup.S时的内存分布情况:

作者:小牛呼噜噜


此时从内存零地址处存放的system模块,其首部是head.s代码,即head.s代码从地址0处开始存放,因此setup结束后执行的就是head.s文件

head.s主要是进入进行保护模式之后的初始化,主要初始化些什么呢?呼噜噜,画了个流程图,建议大家跟着下面流程图,阅读以下全文

如果有人对本文中操作系统一系列初始化操作,感到疑惑,比如为什么要设置的话等之类的问题,建议先看笔者前一篇文章图解CPU的实模式与保护模式

设置段寄存器和系统堆栈

_pg_dir: # 页目录将会存放在这里
startup_32:
	movl $0x10,%eax # 32位ax寄存器赋值0x10
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss _stack_start,%esp #设置栈(系统栈)

我们可以看到上面这段源代码中_pg_dir,这个很重要,和分页机制有关,主要是标识内核分页机制完成后的内核起始地址(零地址),页目录将会存放在这里,这个我们下文再讲。

movl $0x10,%eax,将32位ax寄存器赋值0x10,MOV类指令是最简单的数据传送指令,这类指令把数据从源位置复制到目的位置,需要声明要传送的数据元素的长度,一般有以下几种:

指令描述位数
movb传送字节8位
movw传送字16位
movl传送双字32位
movq传送四字64位

对于 GNU 汇编,每个直接操作数要以$开始,否则表示地址。每个寄存器名都要以%开头,eax 表示是 32 位的 ax 寄存器。

如果面试官提问head.s中0x10这个地址具体是指向哪呢?

这个是虽然简单,但很有迷惑性的,首先我们得知道当操作系统执行head.s的时候,已经进入了保护模式,此时段寄存器不再表示段的基地址,而是表示段选择符(也叫段选择子)

段选择符描述
b1-b0请求特权级(RPL)
b20:全局描述符表 1:局部描述符表
b15-b3描述符表项的索引, 指出选择第几项描述符(从0开始)

所以我们需要先0x10写成16位二进制形式(高位补零)0b0000 0000 0001 0000,所以对应的段选择符:请求特权级为 0(RPL=00)、所指向的描述符存放在GDT(T1=0)、所指向的描述符索引为2(DI=0000 000000010),也就是指向GDT全局段描述符表第3项(从0开始)

接着分别给 ds、es、fs、gs 这几个段寄存器赋值为0x10,让这些寄存器都指向GDT的第3项

lss _stack_start,%esp主要作用是设置系统栈,汇编指令lss会分别给一个段寄存器和一个16位通用寄存器赋值,那么也就是说将操作数_stack_start的值传送给指定ss:esp,其中ss就是堆栈寄存器,存放堆栈段的段基址(实模式),保护模式下存放的就是段选择符,只能存放16位的数据,esp是指向栈顶的通用寄存器,能够存放32位的数据

stack_start是一个标号,它定义在kernel/sched.c文件中:

#定义用户堆栈, PAGE_SIZE=4096,所以user_stack长度为1024
long user_stack [PAGE_SIZE=4096>>2 ] ;

struct {
	long * a;
	short b;
	} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

我们可以发现这是一个结构体,将stack_start的值传给ss:esp,lss指令会把stack_start指向的内存地址的前四字节(32位)装入ESP寄存器,后两字节(16位)装入SS段寄存器,即ss=0x10,esp=& user_stack [1024]

设置IDT

call setup_idt #设置IDT


setup_idt:
	lea ignore_int,%edx  #将 ignore_int 的有效地址(偏移值)值 赋值给 edx 寄存器
	
	movl $0x00080000,%eax # 将段选择符 0x0008 置入 eax 的高 16 位中

	movw %dx,%ax		/* selector = 0x0008 = cs */ # 偏移值的低 16 位置入 eax 的低 16 位中。此时 eax 含有门描述符低 4 字节的值
	
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */ #此时 edx 含有门描述符高 4 字节的值


	lea _idt,%edi # _idt 是中断描述符表的地址, 取idt的偏移给edi
	mov $256,%ecx #循环256次
rp_sidt:
	movl %eax,(%edi) # 将哑中断门描述符存入表中
	movl %edx,4(%edi) # eax 内容放到 edi+4 所指内存位置处。
	addl $8,%edi    # edi 指向表中下一项
	dec %ecx # 循环减1 
	jne rp_sidt  jne 表示zf=0跳转
	lidt idt_descr  # 加载IDTR !!!
	ret

idt_descr:
	.word 256*8-1		# idt contains 256 entries ,共 256 项,是CPU寄存器中的值
	.long _idt
.align 2
.word 0


_idt:	.fill 256,8,0		# idt is uninitialized,这个是在内存中的

IDT,Interrupt Descriptor Table,即中断描述符表,记录着0~255的中断号和调用函数之间的关系,与中段向量表有些相似,但要包含更多的信息。

不知道大家还记不记得,在setup.S中临时将IDT临时设置为一个空表,自此int n 不再是DOS中断了,而是去IDT表中找到中断函数的地址,再执行

上面这段代码实现了256 个中断描述符的设置,各个中断描述符表项都指向一个ignore_int的函数地址,其中ignore_int是一个只报错误的哑中断子程序,内核在随后的初始化过程中,会替换覆盖那些真正实用的中断描述符项

我们查看ignore_int,会发现它就是去打印一串字符Unknown interrupt,提示报错

int_msg:
	.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds ## 注意!!ds,es,fs,gs 等虽然是 16 位的寄存器,但入栈后仍然会以 32 位的形式入栈,也即需要占用 4 个字节的堆栈空间
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg # 把调用 printk 函数的参数指针(地址)入栈
	call _printk
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret # 中断返回(把中断调用时压入栈的 CPU 标志寄存器(32 位)值也弹出)

中断对操作系统来说非常重要,可以跟硬件(例如键盘鼠标显卡等)产生交互,没有中断操作系统就缺胳膊少腿,当中断发生时,CPU获取到中断向量后,通过IDTR的值,去查找IDT中断描述符表,得到相应的中断描述符,再根据中断描述符记录的信息来作权限判断,运行级别转换,最终调用相应的中断处理程序

设置GDT

我们来看下其相关源码:

call setup_gdt #设置GDT

setup_gdt:
	lgdt gdt_descr # 加载全局描述符表寄存器(内容已设置好)
	ret

gdt_descr:
	.word 256*8-1		# so does gdt (not that that's any
	.long _gdt		# magic number, but it works for me :^)

	.align 3


_gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */

这段代码就是重新设置GDT,其实这里和我们在Setup.S设置的GDT是一样的,笔者这里再贴一下之前的代码,比较一下发现是初始化出来的GDT是基本是一模一样的,除了此时段限长不是原来的8MB,而是现在的16MB

gdt:              ! 描述符表由多个8字节长的描述符项组成。这里给出了 3 个描述符项。
	.word	0,0,0,0		! dummy 第1个为空描述符,无用,但必须存在

	.word	0x07FF		! 段界限为 8M,limit=2047 (2048*4096=8Mb) 第2个为空描述符
	.word	0x0000		! 段基址为 0
	.word	0x9A00		! code read/exec P=1, DPL=00, S=1, 代码段,只读,可执行
	.word	0x00C0		! granularity=4096, 386 

	.word	0x07FF		! 段界限为 8M - limit=2047 (2048*4096=8Mb) 第3个为空描述符
	.word	0x0000		! 段基址为 0
	.word	0x9200		! P=1, DPL=00, S=1, 数据段,可读可写
	.word	0x00C0		! granularity=4096, 386

这里主要是为了防止GDT这块内存区域被其他程序覆盖使用,head废除Setup.S设置的GDT,并在内存中重新创建一个全新的全局描述符表

重复设置段寄存器与系统堆栈

	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs
	mov %ax,%gs
	lss _stack_start,%esp

这里重复设置段寄存器与系统堆栈,也是为了安全起见,因为它们所指向的原描述符所指向的段的段限长为 8MB,而刚刚在setup_gdt** 修改了 GDT,段限长已经变为 16MB**,所以当访问 8MB 以上的地址空间时,有可能会产生段限长超限报警。为了防止这类可能发生的情况,在这里重载刷新所有的段寄存器

检查A20是否打开

xorl %eax,%eax  #清零,xorl只需要2个字节,而是用movl实现清零需要5个字节!
1:	incl %eax		#  检查A20是否开启
	movl %eax,0x000000	# 如果不是,则永远循环
	cmpl %eax,0x100000
	je 1b               # '1b'表示向后(backward)跳转到标号 1 去

引入A20是为解决80286的一个bug而引入的,什么bug?请移步看前文Linux0.12内核源码解读(3)-Setup.S

在A20关闭的情况下,系统仍然使用8086/8088的方式,计算机处于20位的寻址模式,访问超过0xFFFFF=2^20=1MB内存时,会自动回卷,比如0x100000会回卷到0x000000;当在A20打开的情况下,才会突破地址信号线20位的宽度,变成32位可用,实现最大寻址空间4GB

所以这部分代码,是通过在内存0x000000处写入任意数据,并和0x100000处比较是否一致,来检查A20是否打开。如果一直相同的话,说明内存回卷, A20没有打开,然后就会一直比较下去,即死循环。

检查x87协处理器是否存在

为了弥补 x86 系列在进行浮点计算时的不足,Intel 于 1980 年推出了 x87 系列数学协处理器,那时是一个外置的、可选的芯片。1989 年,Intel 发布了 486 处理器。自从 486 开始,以后的 CPU 一般都内置了协处理器。这样,对于 486 以前的计算机而言,操作系统检验 x87 协处理器是否存在就非常有必要了

/*
 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
 * mode. Then it would be unnecessary with the "verify_area()"-calls.
 * 486 users probably want to set the NE (#5) bit also, so as to use
 * int 16 for math errors.
 *注意! 在下面这段程序中,486 应该将位 16 置位,以检查在超级用户模式下的写保护,
 * 此后 "verify_area()" 调用就不需要了。486 的用户通常也会想将 NE(#5)置位,以便
 * 对数学协处理器的出错使用 int 16
 */

	movl %cr0,%eax		# 校验数学芯片
	andl $0x80000011,%eax	# Save PG,PE,ET

	orl $2,%eax		# set MP
	movl %eax,%cr0
	call check_x87
	jmp after_page_tables

check_x87:
	fninit      # 向协处理器发出初始化命令
	fstsw %ax   # 取协处理器状态字到 ax 寄存器中
	cmpb $0,%al # 初始化后状态字应该为 0,否则说明协处理器不存在
	je 1f			/* no coprocessor: have to set bits */
	movl %cr0,%eax   # 如果存在则向前跳转到标号 1 处,否则改写 cr0
	xorl $6,%eax		/* reset MP, set EM */
	movl %eax,%cr0
	ret
.align 2      # align 是一汇编指示符。其含义是指存储边界对齐调整,"2"表示把随后的代码或数据的偏移位置
 							# 调整到地址值最后 2 比特位为零的位置(2^2),即按 4 字节方式对齐内存地址
1:	.byte 0xDB,0xE4		/* fsetpm for 287, ignored by 387 */
	ret

这部分源码主要是,用于检查数学协处理器芯片是否存在。方法是修改控制寄存器 CR0,在假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在, 需要设置 CR0 中的协处理器仿真位 EM(位 2),并复位协处理器存在标志 MP(位 1),这部分简单了解一下即可

构建分页管理机制

检查完数学协处理器芯片是否存在,紧接着就执行jmp after_page_tables跳转after_page_tables这个标号处:

after_page_tables:
  # 先将main函数参数,L6标号和main函数入口地址压栈
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $_main

	jmp setup_paging 
L6:
	jmp L6			# main应该永远不会回到这里,但以防万一,我们需要知道发生了什么

先将main函数参数,L6标号和main函数入口地址压入栈中,等待被使用,我们这里先卖个关子,讲完分页再讲解

jmp setup_paging 跳到分页设置,想要理解这部分,你得先了解什么是段页机制,详情见图解CPU的实模式与保护模式


记住这张图的分页机制,理解线性地址前10位,中间10位,后12位分别代表什么,CR3指向哪边,分页机制的原理,我们接着阅读以下部分

内存页清零

setup_paging:
	movl $1024*5,%ecx		
	xorl %eax,%eax      # 清零
	xorl %edi,%edi			# 清零,并让页目录从 0x000 地址开始
	cld;rep;stosl       # eax 内容存到 es:edi 所指内存位置处,且 edi 增 4

其中:

  1. ecx是计数器, 是重复(rep)前缀指令和loop指令的内定计数器,表示控制循环次数
  2. cld相对应的指令是std,二者均是用来操作方向标志位DF(Direction Flag)。cld使DF 复位,即是让DF=0,std使DF置位,即DF=1.这两个指令用于串操作指令中。通过执行cld或std指令可以控制方向标志DF,决定内存地址是增大(DF=0,向高地址增加)还是减小(DF=1,向地地址减小)
  3. rep 表示当 ecx>0 时,循环继续;反之停止
  4. stosl指令相当于将eax中的值保存到es:edi指向的地址中,若设置了EFLAGS中的方向位置位(即在STOSL指令前使用STD指令)则EDI自减4,否则(使用CLD指令)EDI自增4

这一小段代码连起来就是按4字节的速度循环清空内存,每次循环清空的内存范围** **1024*4=4096字节,恰好是一个页,也就是最终清空5页内存(1 页目录 + 4 页页表)

设置页目录表、页表

因为我们(内核)共有 4 个页表,所以只需设置 4 项。

  # 分别设置4个页表
	movl $pg0+7,_pg_dir		/* set present bit/user r/w */
	movl $pg1+7,_pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,_pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,_pg_dir+12		/*  --------- " " --------- */

可能就有人会问了,为啥就只有 4 个页表?不是可以设置1024项嘛?

Linx0.12 当时规定最大寻址空间0xFFFFFF,也就是16M,而1个页目录表或者一个页表最多有1024 个项,页的大小固定为4KB,4(页表数)* 1024* 4KB= 16MB,所以只需前4个页表就能够支持16M寻址

咳咳,还记得我们本文一开始讲的_pg_dir,表示页目录表将会存放在这里(零地址处),紧挨着的其实还有4个页表

.org 0x1000 # .ORG伪指令用来表示起始的偏移地址,紧接着ORG的数值就是偏移地址的起始值
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000

页目录项的结构与页表中项的结构一样,4 个字节为 1 项。
我们简单举个例子:

  1. 这里的$pg0+7其实就表示0x00001007,是页目录表中的第 1 项,我们按线性地址转换为对应的0b0000000000 0000000001 000000000111
  2. 按照页目录和页表的结构,我们知晓第 1 个页表所在的地址 =0000000001= 0x1000
  3. 第 1 个页表的属性标志 =000000000111=0x07,在二进制下,根据这3个1分别表示:页存在P=1、用户可读写RW=1、特权为用户态US=1,表示该页存在、用户可读写

原本页表0到页表3处的代码(也就是head.s17行到114行之间所有执行过的代码),全部清空,此时页目录表和页表在内存的分布情况:

+
| ...
+——————— 0x5000
| 页表3
+——————— 0x4000
| 页表2
+——————— 0x3000
| 页表1
+——————— 0x2000,页的大小4K
| 页表0
+——————— 0x1000
| 页目录表
+——————— 0x0000

接着就是填充4个页表中所有项的内容,下面是从最后一个页表的最后一项开始按倒退顺序填充数据

	movl $pg3+4092,%edi   # edi最后一页表的最后一项

	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */

	std              #方向位置位,edi 值递减(4 字节)。
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax # 每填写好一项,物理地址值减 0x1000。
	jge 1b      /*1b 表示向后跳转到标号1处,如果小于 0 则说明全添写好了*/

设置CR3和CR0

接着设置页目录表基址寄存器cr3,指向页目录表。cr3中保存的是页目录表的物理内存地址,然后设置启动使用分页处理(cr0 的 PG 标志),cr0中含有控制处理器操作模式和状态的系统控制标志

xorl %eax,%eax		# 页目录表在 0x0000 处。
	movl %eax,%cr3		# 设置页目录基址寄存器CR3的值,指向页目录表。页目录表在0x0000处
	movl %cr0,%eax
	orl $0x80000000,%eax  
	movl %eax,%cr0		/* 设置启动使用分页处理,CR0的PG标志置位 */
	ret			/* this also flushes prefetch-queue */

需要注意的是,当执行完这行代码movl %eax,%cr0后,标志着操作系统正式开启分页,此时段部件产生的地址就不再被看成物理地址,被称为线性地址,而是要送往页部件进行变换,以得到真正的物理地址。

最后ret指令很重要,它这里有2个作用:

  1. 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
  2. 将之前压入栈中的 main()程序入口地址弹出,并跳转到 init/main.c 程序去运行。

乍眼一看ret指令怎么就和main函数联系到一起了?我们马上详细来聊聊其中的缘由

跳转至main函数

跳转至main函数的准备工作其实在head.s的早就开始了,但最后一步由ret指令执行的

after_page_tables:
    pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $_main

...
setup_paging:
    ...
    ret

after_page_tables标号处,先将main函数参数,L6标号和main函数入口地址压入中,等待被使用。这些参数比如3个0,后续实际上也没有用到。 L6标号是main函数返回时的跳转地址。

汇编中的参数一般是通过寄存器传递的,而C语言中的参数一般是通过栈来传递

直到setup_paging标号处的ret指令,正好将之前压入栈中的 main()程序入口地址弹出,这个时候CPU会把esp寄存器(始终指向栈顶地址)指向的内存地址处的值,赋值给eip寄存器

eip指令指针寄存器存储着下一条指令的地址,通过CS:EIP联合指向即将执行的下一条指令。对于顺序执行的指令,EIP从前一条指令边界移到下一条指令边界上;对于控制转移指令,例如JMP,JCC, CALL,RET和IRET指令,EIP会向前或先后跳跃数条指令。

一般情况下,程序是不能直接读取或修改EIP寄存器的值,但是可以隐式地通过控制转移指令(JMP,J,CALL和RET),中断,和异常来间接控制EIP。要想读取到EIP寄存器的值,唯一的手段是执行CALL指令,然后从程序栈中读取返回指令指针。这里是通过修改程序栈中返回指令指针的值,然后执行RET指令,间接的加载EIP寄存器

最终CPU跳转到 init/main.c处去运行程序代码。

当执行完ret指令,标志着head.s程序到此就真正结束了!

后续就进入了我们倍感亲切的C程序世界,我们下期再见~~


参考资料:
https://elixir.bootlin.com/linux/0.12/source/boot/head.s
英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A-英特尔®
《Linux内核完全注释5.0》


作者:小牛呼噜噜 ,首发于公众号小牛呼噜噜,系列文章还有:

  1. 聊聊x86计算机启动发生的事?
  2. Linux0.12内核源码解读(2)-Bootsect.S
  3. Linux0.12内核源码解读(3)-Setup.S
  4. 图解CPU的实模式与保护模式
  5. Linux0.12内核源码解读(5)-head.s
  6. Linux0.12内核源码解读(6)-main.c
  7. Linux0.12内核源码解读(7)-陷阱门初始化
  8. 图解计算机中断
  9. Linux0.12内核源码解读(9)-blk_dev_init和chr_dev_init
  10. 什么是系统调用机制?结合Linux0.12源码图解
  11. tty是什么?聊聊linux0.12中tty与time的初始化
  12. linux0.12内核源码解读(12)-任务调度初始化sched_init

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

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

相关文章

怎么优化亚马逊Listing?看这一篇就够了!

运营亚马逊最重要的工作之一就是优化listing,精心优化好亚马逊标题、五点描述、图片和关键词才能提高产品的可见性和吸引力,很多小伙伴对于怎么写出专业的亚马逊listing还是不知道如何下手,今天为大家分享一套实用的亚马逊listing优化指南&am…

软考系统架构师系统工程与信息系统基础考点

软考系统架构师系统工程与信息系统基础考点 系统工程 定义:一种组织管理技术,一种现代的科学决策方法 目的:以最好的方式实现系统 目标:整体最优 意义:利用计算机为工具,对系统的结构、元素、信息和反馈…

2024车载测试还可以冲吗?

2024年已过接近1/4了,你是不是还在围观车载测试行业的发展?同时也在思考着:现在进入车载测试行业还来得及吗?如何高效学习车载测试呢? 我们先来了解一下车载测试行情发展,通过某大平台,我们获取…

使用Ghostscript将PostScript(.ps)文件转换为PDF文件格式

如何使用Ghostscript将PostScript文件转换为PDF文件格式: /* Example of using GS DLL as a ps2pdf converter. */#if defined(_WIN32) && !defined(_Windows) # define _Windows #endif #ifdef _Windows /* add this source to a project with gsdll32.dll, or comp…

学习笔记——动态路由——OSPF(报头信息、报文信息、三张表)

六、OSPF协议的报头信息、报文信息、三张表 OSPF的协议报文在一个广播域内进行传递,是直接封装在IP报文中的,协议号为89。 OSPF本身5种类型:分别是Hello报文、DD报文、LSR报文、LSU报文、LSAck报文,各种不同类型的LSA其实只是包含…

深度解析观测云智能监控的核心设计原理

背景 在监控高度分布式的应用程序时,可能依赖于多个基于云的和本地环境中的数百个服务和基础设施组件,在识别错误、检测高延迟的原因和确定问题的根因都是比较有挑战性的。即使已经具备了强大的监控和警报系统,但是基础设施和应用程序也可能…

求出某空间曲面下的体积

求出某空间曲面下的体积 flyfish 用小长方体的体积和来逼近该体积 import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation# 定义函数 f(x, y) def f(x, y):return np.sin(np.pi * x) * np.sin(np.pi * y)# 创建网格 x np.linspac…

HMI 的 UI 风格,精妙无比

HMI 的 UI 风格,精妙无比

使用vue + canvas绘制仪表盘

使用vue canvas绘制仪表盘 效果图&#xff1a; 父容器 <template><div class"panelBoard-page"><h1>panelBoard</h1><Demo1 :rate"rate" /></div> </template> <script setup> import { ref } from …

2024最新总结:1500页金三银四面试宝典 记录35轮大厂面试(都是面试重点)

学习是你这个职业一辈子的事 手里有个 1 2 3&#xff0c;不要想着去怼别人的 4 5 6&#xff0c;因为还有你不知道的 7 8 9。保持空瓶心态从 0 开始才能学到 10 全。 毕竟也是跳槽高峰期&#xff0c;我还是为大家准备了这份1500页金三银四宝典&#xff0c;记录的都是真实大厂面…

表格截图怎么转换成表格?6个软件帮助你快速进行表格转换

表格截图怎么转换成表格&#xff1f;6个软件帮助你快速进行表格转换 将表格截图转换为可编辑的表格文件是处理数据时常见的需求&#xff0c;特别是在需要分析或编辑图像中包含的信息时。以下是几款帮助你快速进行表格转换的软件和工具&#xff0c;它们提供了不同的功能和适用场…

LearnOpenGL - Android OpenGL ES 3.0 使用 FBO 进行离屏渲染

系列文章目录 LearnOpenGL 笔记 - 入门 01 OpenGLLearnOpenGL 笔记 - 入门 02 创建窗口LearnOpenGL 笔记 - 入门 03 你好&#xff0c;窗口LearnOpenGL 笔记 - 入门 04 你好&#xff0c;三角形OpenGL - 如何理解 VAO 与 VBO 之间的关系LearnOpenGL - Android OpenGL ES 3.0 绘制…

【Linux】对共享库加载问题的深入理解——基本原理概述

原理概述 【linux】详解——库-CSDN博客 共享库被加载后&#xff0c;系统会为该共享库创建一个结构&#xff0c;这个结构体中的字段描述了库的各种属性。在内存中可能会加载很多库&#xff0c;每一个库都用一个结构体描述。把这些结构体用一些数据结构管理起来&#xff0c;系…

短说V4.1.5及PC端V3.1.4正式版发布公告

Hi 大家好&#xff0c; 我是给你们带来惊喜的运营小番茄。 本期更新为短说 4.1.5和PC端3.1.4的正式版。 本次修复上个版本中的问题和功能优化&#xff0c;以及新增了如下功能&#xff1a; PC端支持发布秀米帖&#xff0c;可支持部分秀米格式&#xff1b;后台管理类消息新增…

shell编程之免交互(shell脚本)

Here Document 免交互 Here Document 概述 Here Document是一个特殊的用途的代码块。它在linux shell中使用I/O重定向的方式将命令列表提供给交互式程序或命令&#xff0c;比如ftp&#xff0c;cat或read命令。Here Document 是标准输入的一种替代品&#xff0c;可以帮助脚本开…

postman忘记密码发邮件,久久收不到怎么办?

根本原因是需要FQ&#xff01;&#xff01;&#xff01; 重置密码的链接&#xff1a; https://identity.getpostman.com/trouble-signing-in 找个平台或者软件&#xff0c;访问这个链接即可完成修改密码后续操作&#xff0c;不用再傻傻等着验证码了。 有需要协助的朋友也可私信…

运算放大器(运放)输入偏置电流、失调电流

输入偏置电流定义 理想情况下&#xff0c;并无电流进入运算放大器的输入端。而实际操作中&#xff0c;始终存在两个输入偏置电流&#xff0c;即IB和IB-(参见图1)。 I B I_B IB​的值大小不一&#xff0c;在静电计AD549中低至60 fA(每三微秒通过一个电子)&#xff0c;而在某些高…

Linux CentOS 宝塔中禁用php8.2的eval函数详细图文教程

PHP_diseval_extension 这个方法是支持PHP8的, Suhosin禁用eval函数&#xff0c;不支持PHP8 一、安装 cd / git clone https://github.com/mk-j/PHP_diseval_extension.gitcd /PHP_diseval_extension/source/www/server/php/82/bin/phpize ./configure --with-php-config/ww…

似然 与 概率

概率似然概率函数与似然函数的关系似然与机器学习的关系最大似然估计 似然与概率分别是针对不同内容的估计和近似 概率 概率&#xff1a;概率表达给定参数 θ \theta θ下样本随机向量 X x \textbf{X} {x} Xx的可能性。 概率密度函数的定义形式是 f ( x ∣ θ ) f(x|\t…

【博士每天一篇文献-综述】A survey on few-shot class-incremental learning

阅读时间&#xff1a;2023-12-19 1 介绍 年份&#xff1a;2024 作者&#xff1a;田松松&#xff0c;中国科学院半导体研究所&#xff1b;李璐思&#xff0c;老道明大学助理教授&#xff1b;李伟军&#xff0c;中国科学院半导体研究所AnnLab&#xff1b; 期刊&#xff1a; Neu…