手写简易操作系统(十七)--编写键盘驱动

前情提要

上一节我们实现了锁与信号量,这一节我们就可以实现键盘驱动了,访问键盘输入的数据也属于临界区资源,所以需要锁的存在。

一、键盘简介

之前的 ps/2 键盘使用的是中断驱动的,在当时,按下键盘就会触发中断,引导操作系统去处理这个按键行文。但是当今的usb键盘,使用的是轮询机制,cpu会定时访问键盘看有没有按下键盘。

我个人认为这是cpu技术的进步导致的,在之前,cpu的频率比较低,使用轮询可能会导致漏掉用户按键的行为。但是在今天,cpu的主频已经非常高了,处理一个按键行为就触发中断,这个开销太大了,而且轮询的频率也上来了,现在每秒访问几千次对电脑一点影响都没有,所以现在大多采用了轮询机制。

不过据说中断驱动的还是比较快,现在一些电竞主板还是支持ps/2的接口,这个未经论证。

1.1、键盘的通码与断码

键盘的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键按下时的编码叫做通码,键盘上的触电接通了电路,使硬件产生了一个编码,故此通码叫makecode。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode。

无论是按下键,或是松开键,当键的状态改变后,键盘中的8048芯片把按键对应的扫描码(通码或断码)发送到主板上的8042芯片,由8042处理后保存在自己的寄存器中,然后向8259A发送中断信号,这样处理器便去执行键盘中断处理程序,将8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。

1.2、键盘扫描码

键的扫描码是由键盘中的键盘编码器决定的,不同的编码方案便是不同的键盘扫描码,也就是说,相同的键在不同的编码方案下产生的通码和断码也是不同的。

根据不同的编码方案,键盘扫描码有三套,分别称为scan code set 1、scan code set 2、scan code set 3。

其中scan code set 1是XT键盘用的扫描码,这个历史就比较久远了。scan code set 2是AT键盘的扫描码,这个键盘和我们当今的键盘也不是很一样,但是已经比较接近了。scan code set 3是IBM PS/2系列高端计算机所用的键盘上,IBM蓝色巨人现在都凉了,这个键盘也就很少看到了。

第二套键盘扫描码几乎是目前所使用的键盘的标准,因此大多数键盘向8042发送的扫描码都是第二套扫描码。但是难免有别的键盘,所以才会出现8042这个芯片,这个芯片做一个中间层,为了兼容第一套键盘扫描码对应的中断处理程序,不管键盘用的是何种键盘扫描码,当键盘将扫描码发送到8042后,都由8042转换成第一套扫描码,我们再从8042中读取扫描码。

这里我们给出常用键位的扫描码(这里的扫描码就是通码,加0x80就是断码)

按键扫描码按键扫描码按键扫描码
Esc0x01F10x3BF20x3C
F30x3DF40x3EF50x3F
F60x40F70x41F80x42
F90x43F100x44F110x57
F120x58PrintSc0x37ScrollLk0x46
Pause/Brk0x45`0x2910x02
20x0330x0440x05
50x0660x0770x08
80x0990x0A00x0B
-0x0C=0x0DBackspace0x0E
Tab0x0FQ0x10W0x11
E0x12R0x13T0x14
Y0x15U0x16I0x17
O0x18P0x19[0x1A
]0x1B|0x2BCapsLock0x3A
A0x1ES0x1FD0x20
F0x21G0x22H0x23
J0x24K0x25L0x26
;0x270x28Enter0x1C
Shift左0x2AZ0x2CX0x2D
C0x2EV0x2FB0x30
N0x31M0x32,0x33
.0x34/0x35Shift右0x36
Ctrl左0x1DWin左0xE0Alt左0x38
Space0x39Alt右0xE038Win右0xE0
Menu0xE0Ctrl右0xE01D

问:为什么会有通码和断码,通码不就够了嘛

**答:**如果按一个组合键的话,比如ctrl+a,是先按下ctrl,再按a,再松开ctrl,再松开a。如果没有断码,我们无法判断ctrl是否松开。

1.3、键盘的芯片

和键盘相关的芯片只有8042和8048,它们都是独立的处理器,都有自己的寄存器和内存。Intel 8048芯片或兼容芯片位于键盘中,它是键盘编码器,Intel 8042芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的IO接口,因此它是8048的代理,也是前面所得到的处理器和键盘的“中间层”。我们只需要学习8042就够了

他的端口如下

寄存器端口读写
Output Buffer(输出缓冲区)0x60
Input Buffer(输入缓冲区)0x60
Status Register(状态寄存器)0x64
Control Register(控制寄存器)0x64

状态寄存器8位宽度的寄存器,只读,反映8048和8042的内部工作状态。各位意义如下。

(1)位0:置1时表示输出缓冲区寄存器已满,处理器通过in指令读取后该位自动置0。
(2)位1:置1时表示输入缓冲区寄存器已满,8042将值读取后该位自动置0。
(3)位2:系统标志位,最初加电时为0,自检通过后置为1。
(4)位3:置1时,表示输入缓冲区中的内容是命令,置0时,输入缓冲区中的内容是普通数据。
(5)位4:置1时表示键盘启用,置0时表示键盘禁用。
(6)位5:置1时表示发送超时。
(7)位6:置1时表示接收超时。
(8)位7:来自8048的数据在奇偶校验时出错。

8位宽度的寄存器,只写,用于写入命令控制字。每个位都可以设置一种工作方式,意义如下。

(1)位0:置1时启用键盘中断。
(2)位1:置1时启用鼠标中断。
(3)位2:设置状态寄存器的位2。
(4)位3:置1时,状态寄存器的位4无效。
(5)位4:置1时禁止键盘。
(6)位5:置1时禁止鼠标。
(7)位6:将第二套键盘扫描码转换为第一套键盘扫描码。
(8)位7:保留位,默认为0。

二、环形队列

键盘中断的数据是放在队列中的,等待其他线程的读取。如果我们之前做过关于软件相关的工作,很容易理解这个概念,就是buffer,缓冲区。因为我们是一直在输入的,所以这里设计成了环形队列。

我们看一下环形队列的数据结构

#define bufsize 256/* 环形队列 */
struct ioqueue {// 生产者消费者问题struct lock lock;// 生产者,缓冲区不满时就继续往里面放数据struct task_struct* producer;// 消费者,缓冲区不空时就继续从往里面拿数据struct task_struct* consumer;char buf[bufsize];			// 缓冲区大小int32_t head;			    // 队首,数据往队首处写入int32_t tail;			    // 队尾,数据从队尾处读出
};

这个就很明朗了。一个生产者一个消费者,生产者向buf中添加数据,消费者从buf中取出数据,为了防止buf中的数据出错,生产者和消费者同时只能有一个可以访问到buf。如果buf中数据满了,生产者就不能放了,此时阻塞生产者,如果buf中数据为空,消费者就不能拿了,此时阻塞消费者。

我们看一下具体的实现

/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue* ioq) {lock_init(&ioq->lock);                 // 初始化io队列的锁ioq->producer = ioq->consumer = NULL;  // 生产者和消费者置空ioq->head = ioq->tail = 0;             // 队列的首尾指针指向缓冲区数组第0个位置
}/* 返回pos在缓冲区中的下一个位置值 */
static inline int32_t next_pos(int32_t pos) {return (pos + 1) % bufsize;
}/* 判断队列是否已满 */
bool ioq_full(struct ioqueue* ioq) {return next_pos(ioq->head) == ioq->tail;
}/* 判断队列是否已空 */
bool ioq_empty(struct ioqueue* ioq) {return ioq->head == ioq->tail;
}/* 使当前生产者或消费者在此缓冲区上等待 */
static void ioq_wait(struct task_struct** waiter) {// 二级指针不为空,指向的pcb指针地址为空ASSERT(*waiter == NULL && waiter != NULL);*waiter = running_thread();thread_block(TASK_BLOCKED);
}/* 唤醒waiter */
static void wakeup(struct task_struct** waiter) {// 二级指针指向不为空ASSERT(*waiter != NULL);thread_unblock(*waiter);*waiter = NULL;
}/* 消费者从ioq队列中获取一个字符 */
char ioq_getchar(struct ioqueue* ioq) {// 若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,等待生产者唤醒while (ioq_empty(ioq)) {lock_acquire(&ioq->lock);ioq_wait(&ioq->consumer);lock_release(&ioq->lock);}char byte = ioq->buf[ioq->tail];	  // 从缓冲区中取出ioq->tail = next_pos(ioq->tail);	  // 把读游标移到下一位置if (ioq->producer != NULL) {wakeup(&ioq->producer);		      // 唤醒生产者}return byte;
}/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {// 若缓冲区(队列)已经满了,把生产者ioq->producer记为自己,等待消费者线程唤醒自己while (ioq_full(ioq)) {lock_acquire(&ioq->lock);ioq_wait(&ioq->producer);lock_release(&ioq->lock);}ioq->buf[ioq->head] = byte;      // 把字节放入缓冲区中ioq->head = next_pos(ioq->head); // 把写游标移到下一位置if (ioq->consumer != NULL) {wakeup(&ioq->consumer);      // 唤醒消费者}
}

我们看一下后面两个函数,waitwakeup,这两个函数,这两个函数传入的是一个pcb指针的地址,所以这里是一个二级指针。所以无论是阻塞还是解除阻塞都是取这个二级指针的地址,也就得到了pcb指针。这里对于不熟悉指针的人来说可能会有点扰。

三、键盘驱动

#define KBD_BUF_PORT 0x60	 // 键盘buffer寄存器端口号为0x60/* 用转义字符定义部分控制字符 */
#define esc		    '\033'	 // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace	'\b'
#define tab		    '\t'
#define enter		'\r'
#define delete		'\177'	 // 八进制表示字符,十六进制为'\x7f'/* 以上不可见字符一律定义为0 */
#define char_invisible	0
#define ctrl_l_char	    char_invisible
#define ctrl_r_char	    char_invisible
#define shift_l_char    char_invisible
#define shift_r_char	char_invisible
#define alt_l_char	    char_invisible
#define alt_r_char	    char_invisible
#define caps_lock_char	char_invisible/* 定义控制字符的通码和断码 */
#define shift_l_make	0x2a
#define shift_r_make 	0x36 
#define alt_l_make   	0x38
#define alt_r_make   	0xe038
#define alt_r_break   	0xe0b8
#define ctrl_l_make  	0x1d
#define ctrl_r_make  	0xe01d
#define ctrl_r_break 	0xe09d
#define caps_lock_make 	0x3astruct ioqueue kbd_buf;	   // 定义键盘缓冲区/* 定义以下变量记录相应键是否按下的状态,* ext_scancode用于记录makecode是否以0xe0开头 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {/* 扫描码   未与shift组合  与shift组合*//* ---------------------------------- *//* 0x00 */	{0,	0},/* 0x01 */	{esc,	esc},/* 0x02 */	{'1',	'!'},/* 0x03 */	{'2',	'@'},/* 0x04 */	{'3',	'#'},/* 0x05 */	{'4',	'$'},/* 0x06 */	{'5',	'%'},/* 0x07 */	{'6',	'^'},/* 0x08 */	{'7',	'&'},/* 0x09 */	{'8',	'*'},/* 0x0A */	{'9',	'('},/* 0x0B */	{'0',	')'},/* 0x0C */	{'-',	'_'},/* 0x0D */	{'=',	'+'},/* 0x0E */	{backspace, backspace},/* 0x0F */	{tab,	tab},/* 0x10 */	{'q',	'Q'},/* 0x11 */	{'w',	'W'},/* 0x12 */	{'e',	'E'},/* 0x13 */	{'r',	'R'},/* 0x14 */	{'t',	'T'},/* 0x15 */	{'y',	'Y'},/* 0x16 */	{'u',	'U'},/* 0x17 */	{'i',	'I'},/* 0x18 */	{'o',	'O'},/* 0x19 */	{'p',	'P'},/* 0x1A */	{'[',	'{'},/* 0x1B */	{']',	'}'},/* 0x1C */	{enter,  enter},/* 0x1D */	{ctrl_l_char, ctrl_l_char},/* 0x1E */	{'a',	'A'},/* 0x1F */	{'s',	'S'},/* 0x20 */	{'d',	'D'},/* 0x21 */	{'f',	'F'},/* 0x22 */	{'g',	'G'},/* 0x23 */	{'h',	'H'},/* 0x24 */	{'j',	'J'},/* 0x25 */	{'k',	'K'},/* 0x26 */	{'l',	'L'},/* 0x27 */	{';',	':'},/* 0x28 */	{'\'',	'"'},/* 0x29 */	{'`',	'~'},/* 0x2A */	{shift_l_char, shift_l_char},/* 0x2B */	{'\\',	'|'},/* 0x2C */	{'z',	'Z'},/* 0x2D */	{'x',	'X'},/* 0x2E */	{'c',	'C'},/* 0x2F */	{'v',	'V'},/* 0x30 */	{'b',	'B'},/* 0x31 */	{'n',	'N'},/* 0x32 */	{'m',	'M'},/* 0x33 */	{',',	'<'},/* 0x34 */	{'.',	'>'},/* 0x35 */	{'/',	'?'},/* 0x36	*/	{shift_r_char, shift_r_char},/* 0x37 */	{'*',	'*'},/* 0x38 */	{alt_l_char, alt_l_char},/* 0x39 */	{' ',	' '},/* 0x3A */	{caps_lock_char, caps_lock_char}/*其它按键暂不处理*/
};/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */bool ctrl_down_last = ctrl_status;bool shift_down_last = shift_status;bool caps_lock_last = caps_lock_status;uint16_t scancode = inb(KBD_BUF_PORT);// 若扫描码是e0开头的, 结束此次中断处理函数,等待下一个扫描码进来if (scancode == 0xe0) {ext_scancode = true;    // 打开e0标记return;}// 如果上次是以0xe0开头,将扫描码合并if (ext_scancode) {scancode = ((0xe000) | scancode);ext_scancode = false;   // 关闭e0标记}// 若是断码(按键弹起时产生的扫描码)if ((scancode & 0x0080) != 0) {// 获得相应的通码uint16_t make_code = (scancode &= 0xff7f);// 若是任意以下三个键弹起了,将状态置为falseif (make_code == ctrl_l_make || make_code == ctrl_r_make) {ctrl_status = false;}else if (make_code == shift_l_make || make_code == shift_r_make) {shift_status = false;}else if (make_code == alt_l_make || make_code == alt_r_make) {alt_status = false;}// 若是其他非控制键位,不需要处理,那些键位我们只需要知道通码return;}// 若是通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_codeelse if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)) {// keymap的二维索引bool shift = false;// 按下的键不是字母if ((scancode < 0x0e) || (scancode == 0x29) || \(scancode == 0x1a) || (scancode == 0x1b) || \(scancode == 0x2b) || (scancode == 0x27) || \(scancode == 0x28) || (scancode == 0x33) || \(scancode == 0x34) || (scancode == 0x35)) {  if (shift_down_last) {shift = true;}}// 如果按下的键是字母,需要和CapsLock配合else {if (shift_down_last && caps_lock_last) {      // 如果shift和capslock同时按下shift = false;}else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下shift = true;}else {shift = false;}}// 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.uint8_t index = (scancode &= 0x00ff);// 在数组中找到对应的字符char cur_char = keymap[index][shift];// 如果cur_char不为0,也就是ascii码为除'\0'外的字符就加入键盘缓冲区中if (cur_char) {// 如果ctrl按下,且输入的字符为‘l’或者‘u’,那就保存为 cur_char-‘a’,主要是‘a’前面26位没啥用if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {cur_char -= 'a';}// 如果缓冲区未满,就将其加入缓冲区if (!ioq_full(&kbd_buf)) {ioq_putchar(&kbd_buf, cur_char);}return;}// 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键if (scancode == ctrl_l_make || scancode == ctrl_r_make) {ctrl_status = true;}else if (scancode == shift_l_make || scancode == shift_r_make) {shift_status = true;}else if (scancode == alt_l_make || scancode == alt_r_make) {alt_status = true;}// 这里注意,大写的锁定键是取反else if (scancode == caps_lock_make) {caps_lock_status = !caps_lock_status;}}else {put_str("unknown key\n");}
}/* 键盘初始化 */
void keyboard_init() {put_str("keyboard init start\n");ioqueue_init(&kbd_buf);register_handler(0x21, intr_keyboard_handler);put_str("keyboard init done\n");
}

键盘驱动就稍显复杂一点,主要是涉及到了shiftctrlaltcaplock这些个控制键,这些键位是否按下所表示的通码断码是不一样的。这里就是处理字符,相信大家看代码就可以看明白。

四、仿真

我们创建一个线程,键盘输入什么,打印什么

image-20240325171327961

结束语

本节我们编写了键盘驱动以及其使用的环形队列数据结构。下一节我们将实现一个用户进程,即特权级为3的进程。

老规矩,代码地址为 https://github.com/lyajpunov/os

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

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

相关文章

乐理通识

2023 年搞了台雅马哈 61 键的电子琴&#xff0c;顺手看了下啊 B 的上的课程 《零基础自学音乐学乐理合集-第一季》&#xff0c;这里是部分笔记&#xff08;给博客加点不一样的东西&#x1f440;&#xff09;。 简谱各部分一览 C 表示音名竖线为小节线 音名 完整钢琴键盘 88 键…

数据结构

一、栈 先进后出 二、队列 先进先出 三、数组 查询快&#xff0c;增加修改慢 四、链表 查询慢&#xff0c;增加修改慢 五、二叉树 节点&#xff1a; 查找二叉树 二叉查找树的特点 二叉查找树,又称二叉排序树或者二叉搜索树 每一个节点上最多有两个子节点 左子树上所…

Linux shell编程学习笔记43:cut命令

0 前言 在 Linux shell编程学习笔记42&#xff1a;md5sum 中&#xff0c;md5sum命令计算md5校验值后返回信息的格式是&#xff1a; md5校验值 文件名 包括两项内容&#xff0c;前一项是md5校验值 &#xff0c;后一项是文件名。 如果我们只想要前面的md5 校验值&#xff0c…

视频监控联网平台的评价指标体系

目录 一、视频应用系统评价指标体系的设计思路 &#xff08;一&#xff09;、明确评价目标和原则 &#xff08;二&#xff09;、确定评价指标 &#xff08;三&#xff09;、收集和处理数据 &#xff08;四&#xff09;、建立评价模型 &#xff08;五&#xff09;、进行综…

哔哩哔哩直播姬有线投屏教程

1 打开哔哩哔哩直播姬客户端并登录(按下图进行操作) 2 手机用usb数据线连接电脑(若跳出安装驱动的弹窗点击确定或允许),usb的连接方式为仅充电(手机差异要求为仅充电),不同品牌手机要求可能不一样,根据实际的来 3 在投屏过程中不要更改usb的连接方式(不然电脑会死机需要重启) …

IDEA的Scala环境搭建

目录 前言 Scala的概述 Scala环境的搭建 一、配置Windows的JAVA环境 二、配置Windows的Scala环境 编写一个Scala程序 前言 学习Scala最好先掌握Java基础及高级部分知识&#xff0c;文章正文中会提到Scala与Java的联系&#xff0c;简单来讲Scala好比是Java的加强版&#x…

面试题:JVM的垃圾回收

一、GC概念 为了让程序员更专注于代码的实现&#xff0c;而不用过多的考虑内存释放的问题&#xff0c;所以&#xff0c;在Java语言中&#xff0c;有了自动的垃圾回收机制&#xff0c;也就是我们熟悉的GC(Garbage Collection)。 有了垃圾回收机制后&#xff0c;程序员只需要关…

ATTCK学习笔记

ATT&CK 前言知识 威胁情报&#xff1a;一般为网络流量中或者操作系统上观察到的能高度表明计算机被入侵的痕迹&#xff0c;例如某病毒的Hash值、服务器的IP地址等等。简单来说&#xff0c;威胁情报就像是当计算机被入侵时所表现出来的某种特征&#xff0c;我们将这些威胁…

文件操作(顺序读写篇)

1. 顺序读写函数一览 函数名功能适用于fgetc字符输入函数所有输入流fputc字符输出函数所有输出流fgets文本行输入函数所有输入流fputs文本行输出函数所有输出流fscanf格式化输入函数所有输入流fprintf格式化输出函数所有输出流fread二进制输入文件fwrite二进制输出文件 上面说…

【ReadPapers】A Survey of Large Language Models

LLM-Survey的llm能力和评估部分内容学习笔记——思维导图 思维导图 参考资料 A Survey of Large Language Models论文的github仓库

腾讯云优惠券领取方法大公开,省钱不再是难事

腾讯云—腾讯倾力打造的云计算品牌&#xff0c;以卓越科技能力助力各行各业数字化转型&#xff0c;为全球客户提供领先的云计算、大数据、人工智能服务&#xff0c;以及定制化行业解决方案和提供可靠上云服务&#xff0c;助力企业和开发者稳定上云&#xff01; 然而&#xff0…

粉丝免费福利第一期-海浪型手机支架

&#x1f341; 作者&#xff1a;知识浅谈&#xff0c;CSDN签约讲师&#xff0c;CSDN博客专家&#xff0c;华为云云享专家&#xff0c;阿里云专家博主 &#x1f4cc; 擅长领域&#xff1a;全栈工程师&#xff0c;大模型&#xff0c;爬虫、ACM算法 &#x1f492; 公众号&#xff…

Vue系列-el挂载

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>el:挂载点</title> </head> <body&g…

基于单片机微波炉加热箱系统设计

**单片机设计介绍&#xff0c;基于单片机微波炉加热箱系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的微波炉加热箱系统设计是一个融合了硬件与软件技术的综合性项目。以下是对该设计概要的详细描述&#xf…

P15:PATH环境变量

为什么要配置环境变量 当我们打开DOS窗口&#xff0c;输入&#xff1a;javac&#xff0c;出现下面问题。 原因&#xff1a;windows操作系统在当前目录中无法找到javac命令文件。Windows操作系统是如何搜索硬盘上某一个命令&#xff1f; 首先从当前目录中搜索该命令如果当前目录…

OSCP靶场--Snookums

OSCP靶场–Snookums 考点(RFI信息收集数据库发现凭据bas64解码su切换用户/etc/passwd覆盖提权) 1.nmap扫描 ##┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.216.58 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-30 03:39 E…

期货开户要找到适合自己的系统

物有一个生物圈&#xff0c;大鱼吃小鱼&#xff0c;小鱼吃虾。在期货市场这条生物圈里面&#xff0c;大部分人就是期货市场的虾子&#xff0c;是被吃的&#xff0c;所以必须成长起来&#xff0c;往更高一层走&#xff0c;到可以吃虾子的时候&#xff0c;就是挣钱的时候。学习不…

SpringBoot整合腾讯云邮件发送服务非STMP

SpringBoot整合腾讯云邮箱服务 1、pom配置 <!-- 腾讯云邮箱服务--><dependency><groupId>com.tencentcloudapi</groupId><artifactId>tencentcloud-sdk-java</artifactId><!-- go to https://search.maven.org/search?qtencen…

C++基础之虚函数(十七)

一.什么是多态 多态是在有继承关系的类中&#xff0c;调用同一个指令&#xff08;函数&#xff09;&#xff0c;不同对象会有不同行为。 二.什么是虚函数 概念&#xff1a;首先虚函数是存在于类的成员函数中&#xff0c;通过virtual关键字修饰的成员函数叫虚函数。 性质&am…

Acunetix v24.3 (Linux, Windows) - Web 应用程序安全测试

Acunetix v24.3 (Linux, Windows) - Web 应用程序安全测试 Acunetix | Web Application Security Scanner 请访问原文链接&#xff1a;https://sysin.org/blog/acunetix/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org 重要提…