11 Day : 编写操作系统中断程序,加快时钟

news2024/9/22 23:31:22

前言:昨天学习了中断,今天就废话不多说,直接编写程序吧

内容更新:之前有朋友说看不太懂我的代码写的是啥,能不能详细讲讲,所以本期开始我会详细讲解代码,也会同步更新之前的博客,大多部分代码解释会在注释里面说明,有一些重点会在下面详细分析。

 一,编写流程

  • init_all() 初始化所有设备以及数据结构
  • idt_init() 初始化idt 
  • plc_init() plc即可编程控制器,8259A可变成控制器是一种,这个就是初始化8259A
  • idt_desc_init() idt表中描述符的初始化

 二,简单的说一下宏

 首先关于%define 就不用多说了,写过C语言的同学肯定都知道,我们具体说说%macro

  用法:

%macro 宏名字 参数名字

代码体

%endmacro

  例子:

%macro mul_add 3
mov eax,%1
add eax,%2
add eax,%3
%endmaco

调用方法 mul_add 45,24,33
%1,%2,%3即 45 24 33

三,用汇编编写中断程序

这一部分代码有点多,逻辑稍微是这样的,我们稍微理一理


① 一些系统函数的编写,以及全局变量的编写

/lib/kernel/io.h

ps:这里有一大部分关于内嵌汇编的使用,这里不想详细叙述,有不懂的参考这个博客

asm volatile语法详解

#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"

static inline void outb(uint16_t port, uint8_t data) {

	//端口指定0~255,%b0对应al,%wl对应dx
	asm volatile("outb %b0, %w1" : : "a" (data), "Nd"(port));
}

static inline void outsw(uint16_t port, const void* addr, uint32_t word_cnt) {
	asm volatile("cld; rep outsw" : "+S" (addr), "+c" (word_cnt) : "d" (port));
}

static inline uint8_t inb(uint16_t port) {
	uint8_t data;
	asm volatile("inb %wl,%b0" : "=a" (data) : "Nd" (port));
	return data;
}

static inline void insw(uint16_t port, void* addr, uint32_t word_cnt) {
	asm volatile("cld; rep insw" : "+D" (addr), "+c" (word_cnt) : "d" (port) : "memory");
}

#endif

 /kernel/global.h

#ifndef  _KERNEL_GLOBAL_H
#define _KERNEL_GLOBAL_H
#include "stdint.h"

/**--------------选择子-------------**/
//权限级别
#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3

//GDT LDT选择
#define TI_GDT 0
#define TI_LDT 1

#define SELECTOR_K_CODE ((1<<3)+(TI_GDT<<2) + RPL0)
#define SELECTOR_K_DATA ((2<<3)+(TI_GDT<<2) + RPL0)
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_GS ((3<<3)+(TI_GDT<<2) + RPL0)

/**-------------中断描述符-----------**/
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE
#define IDT_DESC_16_TYPE 0x6

#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P<<7) + (IDT_DESC_DPL0<<5) +IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P<<7) + (IDT_DESC_DPL3<<5) +IDT_DESC_32_TYPE)
#endif // ! _KERNNEL_GLOBAL_H

这里在之前的loader.S里面有写过,大家可以参考一下,然后参考中断描述符的格式看一看就懂了


② 首先我们先编写中断向量table,以及中断调用时入栈出栈的处理。

在汇编层面我们有一个intr_entry_table,在C的层面我们有一个idt_table,他们分别是干嘛的呢?

idt_table:即存储中断函数的代码,他的下标对应着每一个中断向量所对应的中断函数。

intr_entry_table:即对应中断函数的入口,idt_table的入口,触发中断时进入它,然后根据中断向量号找到idt_table中的中断函数。

kernel/kernel.S

[bits 32]
;--------------前期相关参数引入以及定义-----------------
%define ERROR_CODE nop   ;定义错误码
%define ZERO push 0      ;定义填充码

extern put_str;          ;不管
extern idt_table;        ;引入C中的idt_table

section .data

global intr_entry_table  ;使intr_entry_table变为全局变量,方便C中调用

;--------------编写intr_entry_table部分---------------
intr_entry_table:
%macro VECTOR 2          ;代表宏函数,引入两个参数
section .text
intr%1entry:	;intr[0~32] entry
	
	%2			;压入错误码 或者是填充 0 字符
    ;-----------函数调用前的入栈---------------- 
	push ds     
	push es
	push fs
	push gs
	pushad		;压入32位寄存器,EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI,EAX

	;从片中断,除了往从片发送EOI,还要往主片发送EOI
	mov al,0x20
	out 0xa0,al
	out 0x20,al

	push %1		;压入中断号,由于下面一句是call函数,所以以防万一,可能有参数要传递,这里处理的是异常,所以传一个中断号以便于辨认

	call [idt_table+%1*4] ;寻找idt_table中的函数地址,需要向量号*4 因为数组中地址元素大小为4字节,所以需要在idt_table的地址基础上+向量号
	jmp intr_exit;        ;中断退出调用
	
section .data
	dd intr%1entry	;section的特性,会将相同的section紧合在一起,这样就会形成一个 intr数组

%endmacro

section .text
global intr_exit
intr_exit:
	add esp,4	;跳过中断号
    ;--------正常出栈操作
	popad        
	pop gs
	pop fs
	pop es
	pop ds
	add esp,4	;跳过错误码
	iretd

;-----------------调用宏 构建中断函数入口数组--------
;前19个都是异常,不记得了可以看day10的博客有图表,但是我这里为了方便 先都写zero了
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ZERO
VECTOR 0x09,ZERO
VECTOR 0x0a,ZERO
VECTOR 0x0b,ZERO
VECTOR 0x0c,ZERO
VECTOR 0x0d,ZERO
VECTOR 0x0e,ZERO
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ZERO
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ZERO
VECTOR 0x19,ZERO
VECTOR 0x1a,ZERO
VECTOR 0x1b,ZERO
VECTOR 0x1c,ZERO
VECTOR 0x1d,ZERO
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO

首先是错误码和填充码,在day10我们有说过,在异常发生的过程中有可能会要压入错误码,然后我们需要手动跳过它。但是有可能我们此时的中断触发的不是异常无需压入错误码,那这个时候我们跳还是不跳呢? 难道我们还要判断吗 ?所以为了防止这种情况发生,我们干脆压入填充码(4字节)进行跳过

② 压栈出栈过程中,eflags,esp,cs,ip都去哪了?

这个是处理器自动执行的,无需我们手动压入,不相信的话可以写完代码后自己debug来验证该过程,具体debug环节我就不多多介绍了。


③ 中断描述符表初始化编写

kernel/interrupt.c

#include "interrupt.h"
#include "global.h"
#include "io.h"
#include "print.h"

/** PIC主从片端口 **/
#define PIC_M_CTRL 0x20
#define PIC_M_DATA 0x21
#define PIC_S_CTRL 0xa0
#define PIC_S_DATA 0xa1
#define IDT_DESC_CNT 0x21 //IDT中断描述符数量

//中断描述符的数据结构
struct gate_desc {
	uint16_t func_offset_low_word;
	uint16_t selector;
	uint8_t dcount;

	uint8_t attribute;
	uint16_t func_offset_high_word;
};

//中断描述符表
static struct gate_desc idt[IDT_DESC_CNT];

//中断函数入口表
extern intr_handler intr_entry_table[IDT_DESC_CNT];

//中断名称表
char* intr_name[IDT_DESC_CNT];
//中断函数表
intr_handler idt_table[IDT_DESC_CNT];

/** 中断函数(暂时的) 传入一个8位的中断向量号,方便了解是什么中断**/
static void general_intr_handler(uint8_t vec_nr) {

    //忽略IRQ7和IRQ15,一个是intel备用,一个是伪中断,可以参考day 10的图
	if (vec_nr == 0x27 || vec_nr == 0x2f) {
		return;
	}
	put_str("int vector: 0x");
	put_int(vec_nr);
	put_char('\n');
}

/** 异常中断初始化,初始化名称以及函数地址 **/
static void exception_init(void) {
	int i;
	for (i = 0;i < IDT_DESC_CNT;i++) {
		idt_table[i] = general_intr_handler;
		intr_name[i] = "unknown";
	}
//以下的day 10的表都有
	intr_name[0] = "#DE Divide Error";
	intr_name[1] = "#DB Debug Exception";
	intr_name[2] = "NMI Interrupt";
	intr_name[3] = "#BP Breakpoint Exception";
	intr_name[4] = "#OF Overflow Exception";
	intr_name[5] = "#BR BOUND Range Exceeded Exception";
	intr_name[6] = "#UD Invalid Opcode Exception";
	intr_name[7] = "#NM Device Not Available Exception";
	intr_name[8] = "#DF Double Fault Exception";
	intr_name[9] = "Coprocessor Segment Overrun";
	intr_name[10] = "#TS Invalid TSS Exception";
	intr_name[11] = "#NP Segment Not Present";
	intr_name[12] = "#SS Stack Fault Exception";
	intr_name[13] = "#GP General Protection Exception";
	intr_name[14] = "#PF Page-Fault Exception";
	// intr_name[15] 第15项是intel保留项,未使用
	intr_name[16] = "#MF x87 FPU Floating-Point Error";
	intr_name[17] = "#AC Alignment Check Exception";
	intr_name[18] = "#MC Machine-Check Exception";
	intr_name[19] = "#XF SIMD Floating-Point Exception";
}

//初始化中断描述符
void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
	p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
	p_gdesc->selector = SELECTOR_K_CODE;
	p_gdesc->dcount = 0;
	p_gdesc->attribute = attr;
	p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}

/**-----初始化主从PIC,一般家用电脑是两块8259A芯片-----**/
static void pic_init(void) {
	//主片初始化
	outb(PIC_M_CTRL, 0x11); //级联,边沿触发,x86
	outb(PIC_M_DATA, 0x20); //起始号为 0x20  即 0x20~ 0x27
	outb(PIC_M_DATA, 0x04); //连接IRQ2引脚
	outb(PIC_M_DATA, 0x01);	//x86 手动关闭

	outb(PIC_S_CTRL, 0x01);
	outb(PIC_S_DATA, 0x28); //起始中断向量号0x28
	outb(PIC_S_DATA, 0x02); //连接主片IR2引脚
	outb(PIC_S_DATA, 0x01);

	outb(PIC_M_DATA, 0xfe); //放行时钟中断(重要),待会屏幕上就会显示0x20的信息
	outb(PIC_S_DATA, 0xff);
	put_str("pic_init done\n");

}

/** 初始化中断描述符表 **/
void idt_desc_init(void) {
	int i;
	for (i = 0;i < IDT_DESC_CNT;i++) {
		//	make_idt_desc(&idt[i],0, intr_entry_table[i]);
		make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
	}
	put_str(" idt_desc_init done\n");
}

void idt_init(void) {
	put_str("idt init start....\n");
	idt_desc_init();
	exception_init();
	pic_init();
	
    //中断描述符表48位的构建,由于是48位高32位是idt的位置,低16位是idt的大小
	uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)((uint32_t)idt << 16)));
	asm volatile("lidt %0": : "m" (idt_operand));
	//put_str("idt init done\n");
}

 kernel\interrupt.h

#ifndef  _KERNEL_INTERRUPT_H
#define _KERNEL_INTERRUPT_H

typedef void* intr_handler;

void idt_init(void);

#endif

kernel/init.c 

#include "init.h"
#include "print.h"
#include "interrupt.h"

void init_all(void) {
	put_str("init all\n");
	idt_init();
}

kernel/init.h

#ifndef  _KERNEL_INIT_H
#define _KERNEL_INIT_H

void init_all(void);

#endif

kernel/main.c

#include "print.h"
#include "init.h"
void main(void) {
	
	put_str("Hello GeniusOS\n");
	put_int(2023);
	init_all();
	asm volatile("sti");
	while (1) {
		
	}
	return 0;
}

ps:关于put_int函数我之前编写的有一些问题,我已经参考了 别人大佬的代码修改了day 9 put_int的代码,大家可以前去copy 修改一下


 ④ 运行代码

#!/bin/bash
#rm -rf /usr/geniux/img/geniusos.img
nasm -I ./include/ -o mbr.bin mbr.S
dd if=mbr.bin of=../geniusos.img bs=512 count=1 conv=notrunc
echo "disk write success!!"
nasm -I ./include/ -o loader.bin loader.S
dd if=loader.bin of=../geniusos.img bs=512 count=3 seek=2 conv=notrunc
nasm -f elf -o './build/print.o' './lib/kernel/print.S'
nasm -f elf -o './build/kernel.o' './kernel/kernel.S'
#gcc -m32 -I ./lib/kernel -c -o ./build/timer.o ./device/timer.c
gcc -m32 -I ./lib/kernel -m32 -I ./kernel -m32 -I ./lib -c -fno-builtin -o ./build/interrupt.o ./kernel/interrupt.c
gcc -m32 -I ./lib/kernel -m32 -I ./kernel -m32 -I ./lib -c -fno-builtin  -o ./build/init.o ./kernel/init.c
gcc -m32 -I ./lib/kernel -m32 -I ./kernel -m32 -I ./lib -c -fno-builtin  -o ./build/main.o ./kernel/main.c
ld -m elf_i386 -Ttext 0xc0001500 -e main -o ./build/kernel.bin \
       	./build/main.o ./build/init.o ./build/interrupt.o './build/print.o' ./build/kernel.o
dd if=./build/kernel.bin of=../geniusos.img bs=512 count=200 seek=6 conv=notrunc

 丰富一下之前的脚本文件,然后就开开心心的运行它吧!

目前的运行结果:

当你出现如下结果那么恭喜你,你的代码成功了

 我们可以先暂停操作系统的运行,查看一下我们idt表里面的内容,内容如下:


四, 加快时钟频率,学习8253计数器

以为现在可以放假了吗,可惜呢,还没有的噢,现在我们开启了时钟中断,为了让我们时钟中断来的更加猛烈一点,我绝对来加快时钟中断的频率,再次之前我们先要学习8253可编程计数器。


①,什么是时钟/计数器 

众所周之,生活中处处充满了时钟,处处充满了计数器,有一点十分重要,大家要清楚时钟出现的意义是什么,对于我而言,时钟的意义是为了进行协调同步(不知道严不严谨嗷),打个比方,大家在学校应该都练过舞蹈,排练过各种操,往往领操员嘴中会念着1234、2234,这种拍子其实就是一种计数器,为了让各个队员之间步伐上整体一致。

那在时钟对于计算机而言呢,它的作用就是使各个设备之间通信井然有序,计算机的时钟又分为:内部时钟外部时钟

  • 内部时钟

产生原理:晶体振荡器

作用:作为外频来说是处理器和南桥北桥通信的根基,作为主频来说是处理器读取指令所消耗时钟周期的依据

  • 外部时钟

产生原理:软件方面有for循环,while等等,硬件方面有可编程和不可编程定时器

作用:协同外部设备和处理器之间的通信,比如将处理器内部时钟的高频率交给定时器分频产生定时信号


 ②,8253A定时器

下图即为8253A定时器内部结构图,他有三个独立的计数器,每个计数器三个引脚,端口分别为0x40~0x42

1,计数器介绍

 2, 寄存器介绍:

计数初值寄存器:存入要定时的初值

减法计数器:执行减法操作,减去计数器的值

输出锁存器:保存当前计数器的值,当计数值为0时表明工作结束

3,引脚介绍:

CLK:时钟输入信号,接受时钟信号,每接收一次,减法计数器-1,最高接受脉冲频率10MHZ

GATE:门控输入信号,在某些工作方式下控制该计数器是否可以开始技术

OUT:输出信号,当计数完毕时,发出信号通知处理器或者某些设备。


 ③,8253A控制字 

操作端口为0x43,8位大小的寄存器,规定计数器的工作方式,通道以及读写格式,看图能看懂的不多介绍了。


 ④ 工作方式 

在聊工作方式前,我们应先聊一聊启动方式和一些电路知识

上升沿和下降沿

在数字电路中,高于3.5V的电压为高电平(数字1),低于0.3V的电压为低电平(数字0)

上升沿:由0到1的电平变化

下降沿:由1到0的电平变化

计数器启动与终止 

  • 启动条件:

Ⅰ,GATE为高电平,即为1,由硬件控制

Ⅱ,计数器初值以及写入计数器中的减法计数器,由软件out控制

  • 启动模式:

Ⅰ,硬件启动:即计数初值已经载入,等待GATE由低电平变为高电平,在1,5工作方式中需要

Ⅱ,软件启动:GATE已经为1,等待软件来完成计数初值的写入,在0,2,3,4工作方式中需要

  • 终止方式:

Ⅰ,自动终止:定时一到期就停止,不进行下一轮计数,所以计数过程就自然终止了,工作方式0,1,4,5都是单次计数,完成后自动终止,将GATE置为0可以强制终止

Ⅱ,强制终止:当计时到期,减法计数器优惠重新将计数初值寄存器的值载入进来,进行下一轮计数,工作方式2,3都是周期性计数。

 工作方式如下,这里我就不详细介绍了,看看应该就能明白,由于我们是要实现时钟频率加快,所以我们选择方式2来进行工作

 为了有确实想要了解的大佬,我就把书上的内容贴出来了,整理然后敲出来实在太累QAQ,需要的可以参考下面的链接,不想了解的可以跳过噢

知识补给站:8253A的工作方式


⑤ 了解中断的产生,让频率来的更猛烈些

时钟中断利用了分频器的原理,让高频信号输入分频器产生低频信号。

CLK引脚上的时钟脉冲信号是计数器的工作频率节拍,频率为1.19318MHz,即每秒1193180次脉冲信号,每次产生一次脉冲信号,减法计数器就会-1,当值为0时,由OUT引脚发出输出信号,此时这个输出信号就可以作为处理器的中断信号。

相信聪明的你已经发现了,时钟频率和计数器初值有关,初值越低,频率也就越快,初值寄存器为65536时,每秒钟减少1193180次,那么1s钟即可生成 1193180/65536=18.206,也就是18.206Hz,每55ms一次中断

                                    1193180/计数器的初始计数值 = 中断信号的频率

我们这次要将中断频率改为每秒钟100次,相信大家也能够算出来是多少了,既然如此,我们就开始初始化8253并且加快时间频率吧!


⑥ 8253初始化

1,往控制寄存器的0x43端口中写入控制字

2,往计数器端口写入计数初值

/device/timer.c

#include "timer.h"
#include "io.h"
#include "print.h"

#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY/IRQ0_FREQUENCY
#define COUNTER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43

/** 初始化频率 **/
static void frequency_set(uint8_t counter_port,
	uint8_t counter_no,
	uint8_t rw,
	uint8_t mode,
	uint16_t counter_value) {
	outb(PIT_CONTROL_PORT, (uint8_t)(counter_no<<6 | rw<<4 |mode<<1)); //规定PIT的工作模式
	outb(counter_port, (uint8_t)counter_value);
	outb(counter_port, (uint8_t)counter_value >> 8);
}

/** 初始化timer **/
void timer_init() {
	put_str("timer_init start\n");
	frequency_set(COUNTER0_PORT,
		COUNTER0_NO,
		READ_WRITE_LATCH,
		COUNTER_MODE,
		COUNTER0_VALUE);
	put_str("timer_init done\n");
}

/device/timer.h

#ifndef _DEVICE_TIMER_H
#define _DEVICE_TIMER_H
#include "stdint.h"

void timer_init();
#endif

然后在我们init.c文件中下面加上一行timer_init()即可

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
void init_all(void) {
	put_str("init all\n");
	idt_init();
	timer_init();
}


⑦ 大功告成,准备运行睡觉!! 

#!/bin/bash
#rm -rf /usr/geniux/img/geniusos.img
nasm -I ./include/ -o mbr.bin mbr.S
dd if=mbr.bin of=../geniusos.img bs=512 count=1 conv=notrunc
echo "disk write success!!"
nasm -I ./include/ -o loader.bin loader.S
dd if=loader.bin of=../geniusos.img bs=512 count=3 seek=2 conv=notrunc
nasm -f elf -o './build/print.o' './lib/kernel/print.S'
nasm -f elf -o './build/kernel.o' './kernel/kernel.S'
gcc -m32 -I ./lib/kernel -c -o ./build/timer.o ./device/timer.c
gcc -m32 -I ./lib/kernel -m32 -I ./kernel -m32 -I ./lib -c -fno-builtin -o ./build/interrupt.o ./kernel/interrupt.c
gcc -m32 -I ./lib/kernel -m32 -I ./kernel -m32 -I ./lib -c -fno-builtin  -o ./build/init.o ./kernel/init.c
gcc -m32 -I ./lib/kernel -m32 -I ./kernel -m32 -I ./lib -c -fno-builtin  -o ./build/main.o ./kernel/main.c
ld -m elf_i386 -Ttext 0xc0001500 -e main -o ./build/kernel.bin \
       	./build/main.o ./build/init.o ./build/interrupt.o ./build/timer.o  './build/print.o' ./build/kernel.o
dd if=./build/kernel.bin of=../geniusos.img bs=512 count=200 seek=6 conv=notrunc

运行以上代码,当你的bochs屏幕出现和你之前一模一样的界面后,就代表成功啦,有的同学说这忙了一堆没变化啊?实际上你的时钟中断速度大大加快,你说你感受不出来?那你用心慢慢感受吧😋,我要准备睡觉了,收拾收拾准备明天的内存管理编写吧!!!

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

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

相关文章

java基础巩固-宇宙第一AiYWM:为了维持生计,做项目经验之~高速项目大数据及机器学习算法方面的思路总结~整起

原始项目可能主要的功能是接收下位机传送来的很多参数&#xff0c;然后将参数以不同形式表达出来&#xff0c;在此过程中会涉及到文件上传下载、excel表格导出…等&#xff0c;但是呢&#xff0c;这么多数据不玩一下岂不是太浪费。于是&#xff0c;额们决定这样来: 项目中有一个…

Metasploit工具使用(上)

Metasploit工具使用1.Metasploit简介1.1.Metasploit下载1.2.Metasploit框架结构1.2.1.框架路径1.2.2.框架内容介绍1.2.2.1.data目录文件1.2.2.2.modules目录文件1.2.2.3.scripts目录文件1.2.2.4.tools目录文件1.2.2.5.plugins目录文件1.3.Metasploit更新2.MSF中数据库设置2.1.数…

动态与静态函数库的的使用 和 区别 及 优缺点

这里写目录标题初识静态库与动态库静态函数库动态函数库初识静态库与动态库 静态函数库与动态函数库的使用中&#xff0c;有人也把他称为程序的静态链接及动态链接。 静态链接&#xff1a;指程序链接时使用静态库的链接方式&#xff0c;把所有需要的库函数加入&#xff08;拷贝…

Vue3商店后台管理系统设计文稿篇(七)

记录使用vscode构建Vue3商店后台管理系统&#xff0c;这是第七篇&#xff0c;主要记录系统登录页面的创建过程&#xff0c;包含完整vue登录页面代码&#xff1b;Vuex的相关知识以及具体的使用&#xff0c;对state中值得获取&#xff0c;修改&#xff0c;异步修改&#xff0c;分…

Gradle学习笔记之Hook生命周期

简介 Gradle生命周期中的hook&#xff08;钩子&#xff09;函数是由gradle自动回调的&#xff0c;可以用来帮助我们实现一些功能&#xff1a; Gradle在生命周期各个阶段都提供了用于回调的钩子函数: Gradle初始化阶段: 在settings.gradle执行完后&#xff0c;会回调Gradle对…

2022爱分析・智能客服厂商全景报告 | 爱分析报告

报告编委 张扬 爱分析联合创始人&首席分析师 文鸿伟 爱分析高级分析师 王鹏 爱分析分析师 目录 研究范围定义厂商全景地图市场分析与厂商评估入选厂商列表研究范围定义 研究范围 在数字化快速发展的大背景下&#xff0c;随着消费人群及其消费意识的转变&#xff0c;客户对…

亚马逊云科技凭借多年云业务经验,协同合作伙伴快速展开生态化创新

在过去的两周里&#xff0c;ChatGPT的热度居高不下&#xff0c;引发全网讨论。虽然AlphaGo这类AI产品也曾引起热议&#xff0c;但是在应用层面终究还是离用户太远了。而ChatGPT更像是「民用级」的产品&#xff0c;真正意义上让AI技术跨入广泛破圈应用时代。在当下&#xff0c;机…

大数据-Hive

第1章 Hive入门 1.1 什么是Hive 1&#xff09;Hive简介 Hive是由Facebook开源&#xff0c;基于Hadoop的一个数据仓库工具&#xff0c;可以将结构化的数据文件映射为一张表&#xff0c;并提供类SQL查询功能。 2&#xff09;Hive本质 Hive是一个Hadoop客户端&#xff0c;用于…

springboot项目解决@ResponseBody注解返回xml格式数据而不是json格式的问题

目录 1.说明 2.解决 1.说明 一般情况下&#xff0c;RestController中的接口默认响应数据格式都是 json 格式的数据&#xff0c;但有时候使用某些依赖包&#xff0c;会影响ResponseBody的响应数据类型为xml格式&#xff0c; 例&#xff1a; 2.解决 但我们希望响应数据格式是…

使用腾讯云服务器+Nonebot2+go-cqhttp搭建QQ聊天机器人

文章目录一、查看conda版本二、查看系统版本三、配置go-cqhttp1.请切换至同一网络下扫码2.打包Docker镜像四、创建NoneBot环境安装脚手架一、查看conda版本 二、查看系统版本 uname -a arch getconf LONG_BIT三、配置go-cqhttp 下载go-cqhttp 这里有不同版本的cqhttp,并且对…

【数据结构】——如何设计一个链表?(设计链表)

本文主题&#xff1a;通过一道题目&#xff0c;学习链表的基本操作 更多算法&#xff1a;动态规划 ✔️ 边界控制 我的主页&#xff1a;蓝色学者的主页 文章目录一、前言二、题目信息三、解决方案3.0什么是链表&#xff1f;3.1节点的概念虚拟头节点3.2链表创建3.3头插/尾插3…

JUC面试(十三)——锁膨胀

锁膨胀 monitor概念 Monitor是 Java中用以实现线程之间的互斥与协作的主要手段&#xff0c;它可以看成是对象或者 Class的锁。每一个对象都有&#xff0c;也仅有一个 monitor。上面这个图&#xff0c;描述了线程和 Monitor之间关系&#xff0c;以及线程的状态转换图。 进入区…

windows11 永久关闭windows defender的方法

1、按键盘上的windows按键&#xff0c;再点【设置】选项。 2、点击左侧菜单的【隐私和安全性】&#xff0c;再点击列表的【Windows安全中心】选项。 3、点击界面的【病毒和威胁保护】设置项。 4、病毒保护的全部关闭 5、别人的图&#xff08;正常是都开着的&#xff09; 6、终极…

为什么看上去很简单的智慧功能点要价上千万?

人工智能&#xff08;Artificial Intelligence&#xff0c;AI&#xff09;已经不是什么新概念&#xff0c;第三次浪潮于2016年AlphaGo战胜李世石为标志正式开启&#xff0c;至今也已经走过6个年头。 发展至今&#xff0c;AI已经进入老百姓的日常生活&#xff0c;比如随处可见的…

【C语言】从0到1带你学会文件版动态通讯录

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;别人可以拷贝我的模式&#xff0c;但不能拷贝我不断往前的激情 &#x1f6f8;C语言专栏&#xff1a;https://blog.csdn.net/vhhhbb/category_12174730.html 小苏希望大家能从这篇文章中收获到许…

初学者试试,HarmonyOS应用开发者基础认证

一些初学HarmonyOS应用开发的同学往往不知道如何开始&#xff0c;建议先试试《HarmonyOS应用开发者基础认证》&#xff0c;基础认证是华为进一步大范围布局推广“鸿蒙世界”的新举措。也是初学者开启鸿蒙世界的一把钥匙。 【说说鸿蒙世界】 相信大家已经对鸿蒙不陌生了&#x…

IDEA新建js项目和执行js脚本

一)、安装Node.js具体操作参考:https://blog.csdn.net/xijinno1/article/details/128774375二)、IDEA中新建js项目(hello world)1.按照下图&#xff0c;新建js项目2.选中示例代码文件后点击运行->运行3.选择【编辑配置】4.更新一下节点解释器(nodejs.exe)&#xff0c;点击运…

界面组件DevExpress WPF v22.2 - Windows 11暗黑主题发布

DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序&#xff0c;这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 无论是Office办公软件…

Unity 之 Addressable可寻址系统 -- 资源远程加载 | 资源预下载

可寻址系统远程加载 -- 资源预下载 -- 进阶&#xff08;三&#xff09;一&#xff0c;Unity 云资源分发 -- 使用介绍1.1 CCD 的介绍1.2 后台准备工作二&#xff0c;CDD的使用2.1 CCD可视化界面的使用2.2 CDD命令行界面使用2.2.1 准备工作2.2.2 CLI 用法三&#xff0c;AA CCD资…

Qt扫盲-QObject对象和线程

QObject对象和线程一、概述二、QObjectReentrant性三、每个线程事件的循环四、从其他线程访问QObject的子类五、跨线程的信号和槽函数一、概述 QThread继承QObject。QThread它发出信号来指示线程开始或结束执行&#xff0c;还提供了一些任务槽。 Qobject可以在多个线程中使用…