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

news2024/11/25 14:33:26

前情提要

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

一、键盘简介

之前的 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 	0x3a

struct 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);
        // 若是任意以下三个键弹起了,将状态置为false
        if (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_code
    else 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.coloradmin.cn/o/1559157.html

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

相关文章

【STM32嵌入式系统设计与开发】——12IWDG(独立看门狗应用)

这里写目录标题 一、任务描述二、任务实施1、ActiveBeep工程文件夹创建2、函数编辑&#xff08;1&#xff09;主函数编辑&#xff08;2&#xff09;USART1初始化函数(usart1_init())&#xff08;3&#xff09;USART数据发送函数&#xff08; USART1_Send_Data&#xff08;&…

【C++】递归快速幂

class Solution { public:double myPow(double x, int n) {if(n<0){long long a -(long long)n;double temp dfs(x,a);return 1.0/temp;}else{double temp dfs(x,n);return temp;}}double dfs(double x,int n)//给一个数&#xff0c;给一个n&#xff0c;求出x的n次幂{//递…

npm软件包管理器

npm软件包管理器 一.npm 使用步骤二.npm安装所有依赖三.npm全局软件包-nodemon pm 简介链接&#xff1a; 软件包管理器&#xff0c;用于下载和管理 Node.js 环境中的软件包 一.npm 使用步骤 1.初始化清单文件&#xff1a; npm init -y &#xff08;得到 package.json 文件&am…

【C++庖丁解牛】自平衡二叉搜索树--AVL树

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 前言1 AVL树的概念2. AVL…

2024最新版Android studio安装入门教程(非常详细)

目录 JDK安装与配置 一、下载JDK 二、JDK安装 三、JDK的环境配置 四、JDK的配置验证 Android studio安装 Android studio连接手机真机调试&#xff08;以华为鸿蒙为例&#xff09; 一、新建一个android项目 二、进入项目面板 三、配置Android Studio 四、安装手机驱…

最大限度地提高生产力:ChatGPT 如何改变您的日常生活

智能生活新潮流&#xff1a;如何用ChatGPT提升你的工作效率 拥抱人工智能革命 在当今快节奏的世界中&#xff0c;寻找提高生产力的方法就像找到一张成功的金票。 但不要害怕&#xff0c;因为我有一些令人兴奋的消息要告诉你&#xff01; 进入 GPT 工具的世界&#xff0c;这是一…

vue3+ts项目 | axios 的测试 | 测试接口

在 App.vue 中&#xff0c;测试接口 // 测试接口import request from /utils/request;import { onMounted } from vue;onMounted(() > {request.get(/hosp/hospital/1/10).then((res) > {console.log("APP组件展示获取的数据",res);})}) 在request.ts中&…

深入探索位图技术:原理及应用

文章目录 一、引言二、位图&#xff08;Bitset&#xff09;基础知识1、位图的概念2、位图的表示3、位图操作 三、位图的应用场景1、数据查找与存储2、数据去重与排序 四、位图的实现 一、引言 位图&#xff0c;以其高效、简洁的特性在数据处理、存储和检索等多个领域发挥着举足…

JJJ:linux系统中第一个进程

以linux4.19内核linux系统中第一个进程。 执行shell指令 ps -ef 结果如下&#xff1a; xxxxxx-virtual-machine:~$ ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 20:55 ? 00:00:02 /sbin/init splash root …

Postgresql导出数据和结构后再去另外一个Postgresql数据库中导入失败

参考教程&#xff1a; postgresql 在导入建表sql时 遇到错误 &#xff1a;https://blog.csdn.net/weixin_37706944/article/details/132321731 是因为原表定义了自增字段&#xff0c;解决办法&#xff1a; 解决方法&#xff1a; 执行如下sql后再新建表&#xff0c;就可以了 DR…

【docker】nexus 本地 maven配置

1、这篇文章中说明了如何搭建私服 【docker】搭建Nexus私服-CSDN博客文章浏览阅读2次。4、点击登陆&#xff08;账号&#xff1a;admin 秘密&#xff1a;在容器内 /nexus-data/admin.password 文件中)注意我的端口号是 10002&#xff0c;注意你的端口号。7、设置maven-central…

【数据分析面试】1. 计算年度收入百分比(SQL)

题目 你需要为公司的营收来源生成一份年度报告。计算截止目前为止&#xff0c;在表格中记录的第一年和最后一年所创造的总收入百分比。将百分比四舍五入到两位小数。 示例&#xff1a; 输入&#xff1a; annual_payments 表 列名类型amountINTEGERcreated_atDATETIMEstatusV…

COSMIC 方法 - 需求评估 映射阶段

通用软件模型 原则 - COSMIC 通用软件模型 a) 软件块跨越边界与功能用户交互、并与边界内的持久存储介质进行交互。 b) 被度量软件块的 FUR 能够被映射到唯一的一组功能处理。 c) 每个功能处理由一系列子处理组成 d) 一个子处理可以是一个数据移动或者是一个数据运算。 e) 有四…

[flink 实时流基础] 转换算子

flink学习笔记 数据源读入数据之后&#xff0c;我们就可以使用各种转换算子&#xff0c;将一个或多个DataStream转换为新的DataStream。 文章目录 基本转换算子&#xff08;map/ filter/ flatMap&#xff09;聚合算子&#xff08;Aggregation&#xff09;按键分区&#xff08;…

【Spring MVC】快速学习使用Spring MVC的注解及三层架构

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;【Spring MVC】快速学习使用Spring MVC的注解及三层架构 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 Spring Web MVC一: 什么是Spring Web MVC&#xff1…

成绩管理系统|基于springboot成绩管理系统的设计与实现(附项目源码+论文)

基于springboot成绩管理系统的设计与实现 一、摘要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装毕业设计成绩管…

全局自定义指令实现图片懒加载,vue2通过js和vueuse的useintersectionObserver实现

整体逻辑&#xff1a; 1.使用全局自定义指令创建图片懒加载指令 2.在全局自定义指令中获取图片距离顶部的高度和整个视口的高度 3.实现判断图片是否在视口内的逻辑 一、使用原生js在vue2中实现图片懒加载 1.创建dom元素,v-lazy为自定义指令&#xff0c;在自定义指令传入图片…

瑞吉外卖实战学习--8、人员编辑更新

人员编辑更新 前言1、进入编辑窗口需要先获取用户信息2、通过上篇文章的update的方法来改变数据3、测试效果 前言 1、进入编辑窗口需要先获取用户信息 通过注解PathVariable 来获取路径需要携带的id然后赋值到路径上&#xff0c;再通过id查询用户信息 /*** 通过id查询用户信…

Java毕业设计-基于springboot开发的招聘信息管理系统平台-毕业论文+答辩PPT(附源代码+演示视频)

文章目录 前言一、毕设成果演示&#xff08;源代码在文末&#xff09;二、毕设摘要展示1、开发说明2、需求分析3、系统功能结构 三、系统实现展示1、系统功能模块2、管理员功能模块3、企业后台管理模块4、用户后台管理模块 四、毕设内容和源代码获取总结 Java毕业设计-基于spri…

黄金涨是商品牛市的领先信号

自2022年11月以来&#xff0c;黄金价格持续上涨&#xff0c;目前已经突破历史新高&#xff0c;历史上黄金上涨&#xff0c;大多是商品全面牛市的领先信号。在2008年Q4、2019年也出现过&#xff0c;黄金比其他商品更强&#xff0c;但随后的2009年和2020年均是商品的全面牛市。同…