14 Day:同步锁与操作系统输入输出

news2024/11/25 13:17:30

前言:在上一期的线程章节中,我们的线程输出貌似有大问题,今天我们便要来学习同步锁来解决这个问题,同时再次基础上拿下键盘输入,实现操作系统的输入和输出。从今天开始我们的操作系统不在是一块“看板”了!!!


一,上期线程输出的不足

其实13 Day的线程输出并不完美,有很大的问题。但是这个问题经过我多天的观察其实有两种。

1,我们会发现有的线程输出的内容,并没有输出完成,而是被很大的一片空白取代

2,触发global exception 异常

至于为什么会导致这样子,相信聪明的小伙伴已经知道了,如果有学过并发编程相关的知识肯定会对该情况十分熟悉。

首先我们知道,屏幕输出的操作由多个指令组成,且需要操作显存,那么就会出现以下问题

  • 显存作为公共空间,也就是临界区(不过多介绍),所有的线程进行输出都需要操作这块空间,就会导致线程安全问题
  • 屏幕输出操作有多个指令组成,这就说明屏幕输出并不是原子性的,也就是说线程的切换可能导致另一个线程屏幕输出只执行了一半,就被别人踹下台去了,就会出现原本要输出Hello,可是只输出了Hell

 那我们要怎么解决这种问题呢?没错就是关闭中断,这样操作就变成原子性的,线程无阻碍的往下执行。我们可以在原本main函数的两个线程方法while的上下加上一个 关中断与开中断,当大家再次进行输出就会发现输出正常了。

但这大家仔细想想整个while模块里面有些操作并不需要都是原子性的,我们只有某些关键操作是原子性,把整个线程的函数都进行关中断那不就变成以前的串行操作了吗?这就涉及到一个关键性问题,关中断与开中断之间的粒度,也就是JUC中经常讨论到的一点:锁的粒度,他锁定的范围越小,那么线程切换工作的效率就越高。在这里我们显然只要对输出函数进行加锁处理,那么如何让一个方法加锁请接着往下看

① 信号量

有学过操作系统大学课程的同学肯定知道信号量,其中P,V代表信号量的操作,这是一个荷兰语。P代表减少,V代表上升。那么对应锁的话就是如下几个操作:

  • up:

(1) 信号量的值+1

(2) 唤醒在此信号量上等待的线程

  • down:

(1)判断信号量是否>1

(2)信号量大于0,信号量-1

(3)信号量等于0,线程阻塞,在此信号量上等待

② 同步锁的实现

知道以上操作之后,我们便可以来实现一个锁了,在此之前我们还需要完善线程的两个操作:阻塞与唤醒

thread/thread.c

void thread_block(enum task_status stat) {
	ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGINH)));
	enum intr_status old_status = intr_disable();

	struct task_struct* cur_thread = running_thread();
	cur_thread->status = stat;
	schedule();
	intr_set_status(old_status);
}

void thread_unblock(struct task_struct* pthread) {
	enum intr_status old_status = intr_disable();
	ASSERT((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITING) || (pthread->status == TASK_HANGINH));

	if (pthread->status != TASK_READY) {
		ASSERT(!find(&thread_ready_list, &pthread->general_tag));
		if (find(&thread_ready_list, &pthread->general_tag)) {
			PANIC("thread_unblock");
		}
		push(&thread_ready_list, &pthread->general_tag);

		pthread->status = TASK_READY;
	}
	intr_set_status(old_status);
}

接下来我们来实现同步锁

/thread/sync.h

#ifndef  _THREAD_SYNC_H
#define	 _THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "thread.h"


struct semaphore {
	uint8_t value;        //信号量值
	struct list waiters;   //等待队列
};

struct lock {
	struct task_struct* holder;    //持有锁的线程
	struct semaphore semaphore;
	uint32_t holder_repeat_nr;    //锁重入次数
};
void lock_init(struct lock* plock);
void try_lock(struct lock* plock);
void try_release(struct lock* plock);
#endif // ! _THREAD_SYNC_H

 /thread/sync.c

#include "sync.h"
#include "stdint.h"
#include "list.h"
#include "thread.h"
#include "interrupt.h"
#include "debug.h"
void sema_init(struct semaphore* psema, uint8_t value) {
	psema->value = value;
	list_init(&psema->waiters);
}

void lock_init(struct lock* plock) {
	plock->holder = NULL;
	plock->holder_repeat_nr = 0;
	sema_init(&plock->semaphore,1);
}

void sema_down(struct semaphore* psema) {
	enum intr_status old_status = intr_disable();
	while (psema->value == 0) {
		//检测当前线程是否在等待队列中,在的话则报错
		if (find(&psema->waiters, &running_thread()->general_tag)) {
			PANIC("sema_down: thread blocked has been in waiter");
		}
		append(&psema->waiters, &running_thread()->general_tag);
		thread_block(TASK_BLOCKED);
	}
	--psema->value;
	ASSERT(psema->value == 0);
	intr_set_status(old_status);
}

void sema_up(struct semaphore* psema) {
	enum intr_status old_status = intr_disable();
	//从waiter队列中唤醒第一个线程
	if (!empty(&psema->waiters)) {
		struct task_struct* thread_blocked = elem2entry(struct task_struct, general_tag, pop(&psema->waiters));
		thread_unblock(thread_blocked);
	}
	++psema->value;
	ASSERT(psema->value == 1);
	intr_set_status(old_status);
}

void try_lock(struct lock* plock) {
	
	if (plock->holder != running_thread()) {
		sema_down(&plock->semaphore);
		
		plock->holder = running_thread();
		ASSERT(plock->holder_repeat_nr == 0);
		plock->holder_repeat_nr = 1;
		
	}
	else {
		++plock->holder_repeat_nr;
	}
}

void try_release(struct lock* plock) {
	ASSERT(plock->holder == running_thread());

	if (plock->holder_repeat_nr > 1) {
		--plock->holder_repeat_nr;
		return;
	}

	ASSERT(plock->holder_repeat_nr == 1);
    //注意这里不允许调换sema_up的位置,如果调换位置,就会导致中断开启,但是plock->hodler还没设为空就进行线程切换了
	plock->holder = NULL;
	plock->holder_repeat_nr = 0;
	sema_up(&plock->semaphore);
}

② 实现终端输出

这样子信号量我们就完成了,接下来我们用锁来实现一个终端输出,在此之前我们先来了解了解

什么是虚拟终端。

虚拟终端(tty):为了能让更多的人同时使用计算机,必须在一个显示器下实现多个用户,为每个用户虚拟出一个显示器

原理:不同的用户使用不同显存区域,让显存分快显示,就达到了虚拟终端的效果

 device/console.h

#include "stdint.h"

void console_init();
void console_acquire();
void console_release();
void console_put_str(char* str);
void console_put_char(uint8_t ch);
void console_put_int(uint32_t num);

 device/console.c

#include "console.h"
#include "stdint.h"
#include "sync.h"
#include "print.h"
static struct lock console_lock;

void console_init() {
	lock_init(&console_lock);
	put_str("\nconsole init done!\n");
}

void console_acquire() {
	try_lock(&console_lock);
}

void console_release() {
	try_release(&console_lock);
}

void console_put_str(char* str) {
	console_acquire();
	put_str(str);
	console_release();
}

void console_put_char(uint8_t ch) {
	console_acquire();
	put_char(ch);
	console_release();
}

void console_put_int(uint32_t num) {
	console_acquire();
	put_int(num);
	console_release();
}

将main函数中的普通put_str,改为console_put_str,接下来修改一下makefile文件,运行结果是密密麻麻的输出字符即代表我们锁编写成功(时代太过久远,忘记截图了)


 

 三,键盘键入的原理

1,两个芯片

键盘嘛,相信大家都见过的哈,没见过的同学我相信你也肯定看不到我的文章捏。在计算机这个系统中,键盘作为一个外部设备,他并不是完完全全独立于操作系统存在的,他的功能实现涉及到了两个功能独立的芯片

  •  Intel 8048(或兼容芯片):位于键盘内部,称作键盘编码器负责监听键盘按键事件并向键盘控制器报告哪个按键按下,哪个按键弹起
  • Intel 8042(或兼容芯片):位于计算机主板,称作键盘控制器,负责接受键盘编码起的按键消息,并将其解码保存,然后向中断代码发送中断,之后处理器读取8042处理过并保存的数据

 当然为了知道键盘到底安乐哪个键,8042与8048之间必须保持一个协议,那就是所有按键与对应竖直组成一个类似于hash映射的键盘扫描码


2,键的编码

键的编码

一个键通常对应两个码,按下的通码与松开的断码当你一直按着键不松开,会产生练习相同的码,当你松开的时候才会终止这个就叫做断码

 键盘扫描码

键盘的扫描码有三套:scan code set 1,scan code set 2,scan code set 3,下图分别是三套扫描码的附图

 

 在此我们使用第一套键盘扫描码

 

  • 当今键盘内部芯片默认使用第二套编码,但是我们使用第一套编码,所以说8048将编码信息发送给8042的时候会进行一次转换
  • 8042每次接受到一个字节的扫描码后就会向中断代理发送信号,一个按键操作至少触发两次中断(通码和断码各一次)
  • 一般中断处理程序不会处理断码信息
  • 当按下a键时,8048向8042发出a键的第二套扫描码0x1c,8042将其转换为第一套编码0x1e并保存到自己的缓冲区,当保存完毕后向中断代理发送中断,中断处理程序开始执行并从8042缓冲区读取0x1e,当松开后重复上述操作。

3,8042简介

8048是键盘的控制者(键盘监控,键盘设置,灯光),8042是键盘的IO接口,因此8042是8048的代理,8048通过PS/2,USB接口 与8042通信,处理器通过端口与8042通信。

8042有4个8位寄存器

  •  8042作为8048的中转站,8048通过out 0x60,将自己的数据写入8042中,让自己的数据通过8042被操作系统读入
  • 8042作为8048的输入缓冲区,将8048的信息暂存在8042缓冲区,操作系统通过 in 0x60读入数据

 要注意一点的是,当8042缓冲区已经有数据时,就不会再次读入数据,要等到数据被中断程序读取后才会再次读入8048的数据,他是根据一个状态寄存器的第0位来判断的。

8042有3个寄存器,状态寄存器,输入缓冲区寄存器,控制寄存器,由于我们只需要用到部分功能我也就贴出图大家稍微参考一下即可

 


三,编写键盘驱动

#include "interrupt.h"
#include "stdint.h"
#include "keyboard.h"
#include "io.h"
#include "print.h"


#define KB_BUF_PORT 0x60

#define esc '\033'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177'

#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

static bool ctrl_status, alt_status, caps_status, shift_status, ext_scancode;

char keymap[][2] = {
	/* 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 = casps_lock_status;

	bool break_code;
	uint16_t scancode = inb(KB_BUF_PORT);

	//判断是不是为多余字符,是的话马上退出处理下一个
	if (scancode == 0xe0) {
		ext_scancode = true;
		return;
	}

	//如果上次以0xee0开头,则合并通码,并去除ext_scancode标志
	if (ext_scancode) {
		scancode |= 0xe000;
		ext_scancode = false;
	}

	//获取break code
	break_code = ((scancode & 0x0080) != 0);

	//如果是断码,对ctrl和shift和alt进行状态判断
	if (break_code) {
		uint16_t make_code = (scancode &= 0xff7f);
		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_char || make code == alt_r_make) {
			alt_status = false;
		}
		return;

	}
	//如果为通码
	else if ((scancode > 0x00 && scancode < 0x3b) ||
		scancode == alt_r_make || scancode == ctrl_r_make) {
		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;
			}
		}else {
			if (shift_down_last && caps_lock_last) {
				shift = false;
			}
			else if (shift_down_last || caps_lock_last) {
				shift = true;
			}
			else {
				shift = false;
			}
		}
		uint8_t index = (scancode &= 0x00ff);

		char cur_char = keymap[index][shift];

		if (cur_char) {
			put_char(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_char || 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");
	register_intr(0x21, intr_keyboard_handler, "keyboard");
	put_str("keyboard_init done\n");
}

四,环形输入缓冲区

我们知道,操作键盘通常是为了和系统交互,而和系统交互一般都是写入某些shell指令,而这些shell指令需要用一个缓冲区存入起来,当形成一个完整的命令时,再一并由其他模块处理。


① 生产者消费者模型

对于缓冲区我们要有以下几个认识:

  • 缓存数据
  • 公共区域,多个线程同时使用该空间,需要对存取操作进行上锁
  • 数据可能存在存满和取空两种状态 

线程之间要相互合作,存在资源共享问题。对此我们便使用由Dijkstra提出的生产者消费者模型

 生产者消费者模型是什么,我不太想用学术的语言来介绍,我在此来举个例子:

 以前去吃过KFC的同学应该都了解,吃KFC的时候是需要排队的,那么此时我们把服务员当作生产者(就当作他又做饭又服务吧),备餐口就是缓冲区,而顾客则是消费者


 ① 当客流量比较少时,供大于求,服务员做好一大堆汉堡放在备餐区,当餐做满时便停下来休息并告诉顾客:”新鲜的汉堡做好了,来取餐嘞“(唤醒消费者),休息的顾客听到了便来到备餐区取餐

当客流量中等时,供等于求,只要服务员看到备餐口有空位,马上开始忙活起来。

当客流量很大时,供不应求,餐取完的时候,顾客便会在等待队列中等待,并催促服务员:“搞快点咯,肚子饿了“(唤醒生产者),摸鱼的服务员便会快马加鞭的做汉堡。


总结:对于有限大小的公共缓冲区,同步生产者和消费者的运行,对共享缓冲区互斥访问,并不会过度消费和过度生产,这便是生产者消费者模型,以及疯狂星期四KFCV50😋

② 环形缓冲区实现

其实线形的缓冲区也是OK的,怎么设计因人而异,我这里用队列来实现一个环形缓冲区,环形缓冲区的本质就是没有起始地址,没有终止地址,很简单我们直接开始吧。

device/ioqueue.h

#ifndef  _DEVICE_IOQUEUE_H
#define  _DEVICE_IOQUEUE_H
#include "stdint.h"
#include "thread.h"
#include "sync.h"

#define bufsize 64 //一个缓冲区的大小

struct ioqueue {
	struct lock lock;
	struct task_struct* producer;
	struct task_struct* consumer;
	char buf[bufsize];
	int32_t head;	//生产是头指针移动
	int32_t tail;	//消费时尾指针移动
};

void ioqueue_init(struct ioqueue* ioq);
int32_t next_pos(int32_t pos);
bool ioq_full(struct ioqueue* ioq);
bool ioq_empty(struct ioqueue* ioq);
void ioq_wait(struct task_struct** waiter);
void ioq_wakeup(struct task_struct** waiter);
char ioq_getchar(struct ioqueue* ioq);
void ioq_setchar(struct ioqueue* ioq, char byte);
#endif // ! _DEVICE_IOQUEUE_H

device/ioqueue.c

#include "debug.h"
#include "ioqueue.h"
#include "interrupt.h"

void ioqueue_init(struct ioqueue* ioq) {
	lock_init(&ioq->lock);
	ioq->producer = ioq->consumer = NULL;
	ioq->head = ioq->tail = 0;
}

int32_t next_pos(int32_t pos) {
	return (pos + 1) % bufsize;
}

bool ioq_full(struct ioqueue* ioq) {
	ASSERT(intr_get_status() == INTR_OFF);
	return (ioq->tail - ioq->head) == 1;
}

bool ioq_empty(struct ioqueue* ioq) {
	ASSERT(intr_get_status() == INTR_OFF);
	return ioq->head == ioq->tail;
}

//使当前的生产者或消费之在缓冲区上等待
void ioq_wait(struct task_struct** waiter) {
	ASSERT(*waiter == NULL && waiter != NULL);
	*waiter == running_thread();
	thread_block(TASK_BLOCKED);
}

//唤醒生产者或消费者
void ioq_wakeup(struct task_struct** waiter) {
	ASSERT(*waiter != NULL);
	thread_unblock(*waiter);
	*waiter = NULL;
}

char ioq_getchar(struct ioqueue* ioq) {
	//先判断是否关闭中断
	ASSERT(intr_get_status() == INTR_OFF);

	//判断是否为空,如果为空消费者需要休眠等待
	while (ioq_empty(ioq)) {
		try_lock(&ioq->lock);
		ioq_wait(&ioq->consumer);
		try_release(&ioq->lock);
	}

	//获取buf中的数据
	char ch = ioq->buf[ioq->tail];
	ioq->tail = next_pos(ioq->tail);

	if (ioq->producer != NULL) {
		ioq_wakeup(&ioq->producer);
	}

	return ch;
}

void ioq_setchar(struct ioqueue* ioq, char byte) {
	//先判断是否关闭中断
	ASSERT(intr_get_status() == INTR_OFF);

	//判断是否满缓冲区,如果满了生产需要休眠等待
	while (ioq_full(ioq)) {
		try_lock(&ioq->lock);
		ioq_wait(&ioq->producer);
		try_release(&ioq->lock);
	}

	//获取buf中的数据
	ioq->buf[ioq->head]=byte;
	ioq->head = next_pos(ioq->head);

	if (ioq->consumer != NULL) {
		ioq_wakeup(&ioq->consumer);
	}

	return byte;
}


然后我们完善一下键盘操作

device/keyboard.c

struct ioqueue ioqueue;

void keyboard_init()
{
	put_str("keyboard init start\n");
	register_handler(0x21, intr_keyboard_handler);
	init_ioqueue(&ioqueue);
	put_str("keyboard init done\n");
}
void intr_keyboard_handler(void)
{
//......
        if (cur_char)
		{
			if (!ioq_full(&ioqueue))
				ioq_putchar(&ioqueue, cur_char);
			return;
		}
//......
}

接下来我们让两个内核线程变成消费者

kernel/main.c

#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
#include "ioqueue.h"
#include "interrupt.h"

void g_thread(void* arg);
void g_thread2(void* arg);
void main(void) {
	
	
	put_str("Hello GeniusOS\n");
	put_int(2023);
	put_str("\n");
	init_all();
	thread_start("genius", 5, g_thread, "A_");
	thread_start("genius2", 31, g_thread2, "B_");
	
	intr_enable();
	while (1) {
		//console_put_str("Main ");
	}
}

void g_thread(void* arg) {
	while (1) {
		enum intr_status old_status = intr_disable();
		if (!ioq_empty(&kbd_buf)) {
			console_put_str(arg);
			char byte = ioq_getchar(&kbd_buf);
			console_put_char(byte);
			console_put_char(' ');
		}
		intr_set_status(old_status);
	}
}

void g_thread2(void* arg) {
	while (1) {
		enum intr_status old_status = intr_disable();
		if (!ioq_empty(&kbd_buf)) {
			console_put_str(arg);
			char byte = ioq_getchar(&kbd_buf);
			console_put_char(byte);
			console_put_char(' ');
		}
		intr_set_status(old_status);
	}
}

改一下makefile,运行一下,当你敲下键盘,且字符在屏幕上显示,这就代表你成功了,恭喜你!!!完成本章任务!!!

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

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

相关文章

Python|数学|贪心|数组|动态规划|单选记录:实现保留3位有效数字(四舍六入五成双规则)|用Python来创造一个提示用户输入数字的乘法表|最小路径和

1、实现保留3位有效数字&#xff08;四舍六入五成双规则&#xff09;&#xff08;数学&#xff0c;算法&#xff09; 贡献者&#xff1a;weixin_45782673 输入&#xff1a;1234 输出&#xff1a;1234 12 12.0 4 4.00 0.2 0.200 0.32 0.320 1.3 1.30 1.235 1.24 1.245 1.24 1.…

Docker 入门建议收藏 第一部分

一、Docker 是什么&#xff1f; Docker&#xff0c;翻译过来就是码头工人 Docker是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可抑制的容器中&#xff0c;然后发布到任何流行的Linux机器上&#xff0c;也可以实现虚拟化。容器完全使用沙盒…

JVM概览:内存空间与数据存储

核心的五个部分虚拟机栈&#xff1a;局部变量中基础类型数据、对象的引用存储的位置&#xff0c;线程独立的。堆&#xff1a;大量运行时对象都在这个区域存储&#xff0c;线程共享的。方法区&#xff1a;存储运行时代码、类变量、常量池、构造器等信息&#xff0c;线程共享。程…

ClassMix: Segmentation-Based Data Augmentation for Semi-Supervised Learning学习笔记

ClassMix相关介绍主要思想方法Mean-Teacher损失函数交叉熵损失标签污染实验实验反思参考资料相关介绍 从DAFormer溯源到这篇文章&#xff0c;ClassMix主要是集合了伪标签和一致性正则化&#xff0c;思想来源于CutMix那条研究路线&#xff0c;但是优化了CutMix中的标签污染的情…

使用 HTML5 轻松验证表单插件

下载:https://download.csdn.net/download/mo3408/87559594 效果图: 当您通过表单从人们那里收集信息时,必须应用某种验证。如果不这样做,可能会导致客户流失、数据库中的垃圾数据甚至网站的安全漏洞。从历史上看,构建表单验证一直很痛苦。在服务器端,全栈框架会为您处理…

【AI绘图学习笔记】深度前馈网络(一)

有关深度前馈网络的部分知识&#xff0c;我们已经在吴恩达的机器学习课程中有过了解了&#xff0c;本章主要是对《深度学习》花书中第六章&#xff1a;深度前馈网络的总结笔记。我希望你在看到这一章的时候&#xff0c;能回忆起机器学习课程中的一些环节或者细节&#xff0c;这…

【现代机器人学】学习笔记十一:抓握与操作

本章是比较独特的一章&#xff0c;相对于前面的内容&#xff0c;内容较为独立&#xff0c;主要描述的是力学相关的一些理论。因此&#xff0c;读者也完全不必根据题目产生一些不必要的幻想&#xff0c;认为似乎看完这章我就可以学会机器人抓取。不过&#xff0c;我仍然认为这章…

新入职的项目经理,如何击破权力微薄的困境?

“从此找到了上班的意义”这个话题最近登上了热搜&#xff0c;在“铜三铁四”的招聘季&#xff0c;大家停止了内卷&#xff0c;给自己安排得明明白白&#xff0c;每天上班的动力就是&#xff1a;充电、蹭网、干饭、灌水、睡午觉、上厕所。但咱项目经理们却没办法Get这些动力&am…

【UEFI基础】HOB介绍

综述 HOB的全称是Hand-Off Block&#xff0c;从名字上也可以看出来&#xff0c;它表示的是一种用于交接的数据。按照HOB的使用情况&#xff0c;可以将BIOS的启动阶段分为两个部分&#xff1a; HOB生成阶段&#xff08;HOB producer phase&#xff09;&#xff0c;用来创建和修…

PMP项目管理项目质量管理

目录1 项目质量管理概述2 规划质量管理3 管理质量4 控制质量1 项目质量管理概述 项目质量管理包括把组织的质量政策应用于规则、管理、控制项目和产品质量要求&#xff0c;以满足相关方目标的各个过程。项目质量管理还将以组织的名义支持过程的持续改进活动。 核心概念 质量是…

Elasticsearch:集群管理

在今天的文章中&#xff0c;我们应该学习如何管理我们的集群。 备份和分片分配是我们应该能够执行的基本任务。 分片分配过滤 Elasticsearch 将索引配到一个或多个分片中&#xff0c;我们可以将这些分片保存在特定的集群节点中。 例如&#xff0c;假设你有多个数据集群节点&am…

感应电机数学模型(电机控制应用基础系列)

电机在工业控制中的地位不言而喻&#xff0c;所以对电机模型的了解是我们理解各种算法的基础。这篇博客帮大家整理总结。张力控制离不开电机控制&#xff0c;有关张力控制的详细内容请参看下面的文章链接&#xff1a; PLC张力控制&#xff08;开环闭环算法分析&#xff09;_张…

字符函数和字符串函数详解(1)

目录前言strlen函数strlensizeofstrcpy函数strcat函数strcmp函数总结前言 最近要调整状态&#xff0c;写的文章质量不佳让大家失望&#xff0c;我现在也在反思我在做什么&#xff0c;我会什么&#xff0c;我学了什么。等我想明白的那天&#xff0c;我一定能跟大家顶峰相见的&a…

身份推理桌游

目录 sha人游戏&#xff08;天黑请闭眼&#xff09; &#xff08;1&#xff09;入门版 &#xff08;2&#xff09;标准版 &#xff08;3&#xff09;延伸版——百度百科 待更新 &#xff08;4&#xff09;延伸版——推理学院 待更新 狼人杀 1&#xff0c;基本玩法 2&am…

Android 12.0 Launcher3 app图标长按去掉应用信息按钮

1.前言 在12.0的rom定制化开发中,在Launcher3定制化开发中,对Launcher3的定制化功能中,在Launcher3的app列表页会在长按时,弹出微件和应用信息两个按钮,点击对应的按钮跳转到相关的功能页面, 现在由于产品需求要求禁用应用信息,不让进入到应用信息页面所以要去掉应用信息…

QT入门基础(一)

文章目录零.Qt背景1.什么是Qt2.Qt的发展史3.Qt的优势4.Qt应用一.第一个Qt程序0.项目创建1.main函数文件2.类头文件3.pro文件4.qt命名规范二.Qt按钮1.按钮创建和父子关系2.按钮常用api3.Qt窗口坐标体系4.对象树模型零.Qt背景 1.什么是Qt Qt是一个跨平台的C图形用户界面应用程序…

快速排序/快速选择算法

一.快速排序 1.基本介绍 快速排序&#xff08;Quicksort〉是对冒泡排序的一种改进,都属于交换排序。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分(每次选择中轴值)&#xff0c;中轴值左边的元素小于中轴值,中轴值右边的元素全部大于中轴值(但不要求有序)&#x…

Canvas详细使用方法(一)

Canvas Canvas的注意事项 < canvas > 和 < img > 元素很相像&#xff0c;唯一的不同就是它并没有 src 和 alt 属性。 -< canvas > 标签只有两个属性——width和height( 单位默认为px )。当没有设置宽度和高度时&#xff0c;canvas 会初始化宽为 300px 和高…

C#基础之面向对象编程(二)

总目录 文章目录总目录前言一、概述1. 定义2. 面向对象的三大特性二、封装1. 定义2. 属性三、继承1. 定义2. 继承的使用3. base 和this四、多态1. 定义2. 重写和重载3. 多态性的实现1、静态多态性2、动态多态性4. 向上转型和向下转型1、定义2、语法格式3、案例结语前言 本文主…

Docker常用项目实战演练

docker镜像源的修改 linux环境下编辑 /etc/docker/daemon.json vi /etc/docker/daemon.json #如添加如下网易镜像源 { "registry-mirrors": ["http://hub-mirror.c.163.com"] }docker run命令详细解释 日常工作中用的比较多的是docker run命令&#xff…