操作系统实验5:信号量的实现与应用

news2024/11/27 3:52:56

写在最前的总结

下面的实验内容是在完整做完实验时候补充的,这里先把踩过的坑记录一下。

调试总结

  1. 先在Ubuntu上模拟生产者—消费者问题。这个实验分为两大部分,一个是实现信号量,另一个是验证信号量。对于第二个,建议先在Ubuntu上模拟生产者—消费者问题,确保自己的代码思路是正确的,之后再稍微修改一下代码就可以在linux-0.11上运行了。这样做的好处是减少问题,因为直接在linux-0.11上运行,一旦出现错误,不好确定是信号量的问题还是模拟生产者–消费者代码的问题。
  2. 在linux-0.11上运行pc.c的时候如果出现问题,可以用打印日志的方式来查看运行过程,因为在linux-0.11上不能使用GDB来调试。另外,运行信息直接打印在屏幕上在Linux-0.11上不方便看,可以保存到文件中用Ubuntu来查看。具体做法是在运行程序的时候在后面加上>output,其中output中保存了原本打印在屏幕上的信息。如果编译生成的可执行文件名为pc,则:
./pc>output

这样就把打印信息保存到文件了,退出Linux-0.11之前要用命令sync来保存文件到磁盘。
在Ubuntu上编译的时候会报错,需要连接pthread库,编译命令如下:

gcc -o test test.c -lpthread

其中的test.c就是在Ubuntu上实现的生产者–消费者问题的模拟。

  1. 减少数据量。实验要求的是生产者生产500个数据,在调试的时候改称100个就可以,在调试通过之后改回500个也一样能跑。实验数据太多,不方便调试。

代码注释方式和变量定义位置

在Linux-0.11中只支持/*…*/这样的注释方式,使用双斜杠注释会报错,这应该是和gcc的版本有关系。另外,在早期C标准中,函数内部的变量定义只能定义在函数最开头位置,否则会报错(也可能是gcc版本的原因)。

文件拷贝

将编写的pc.c、sem.h和unistd.h文件拷贝到Linux-0.11中,pc.c是编写的测试代码;sen.h是信号量实现的头文件,因为在ppc.c中会用到;unistd.h在实验过程修改过,也需要拷贝一份到Linux-0.11的用户空间中,否则会报错或者得不到正确的实验现象。
具体拷贝过程如下:

  1. 挂载。在lab5实现目录下,执行以下命令进行挂载:
sudo ./mount-hdc

这样就将Linux-0.11挂载到./hdc/目录下了。

2.拷贝。unistd.h文件的拷贝位置和原来的相同,直接覆盖原来的文件。sem.h文件放在/usr/include/linux/路径下,pc.c放在/usr/root/目录下。

阻塞后没有调度

在sys_sem_wait()函数中,如果信号量的值为0,则把当前进程添加到该信号量的阻塞队列中,然后进行调度,这里很好理解,只是写代码的时候忘了,导致程序卡死。

进程间通信问题

这里涉及到多个消费者进程,因此存在通信问题,即某个消费者进程要从共享缓冲区中读取数据时应该从哪个位置开始读呢?注意,这里不能使用全局变量的方式来标记读取位置,因为一个进程肯定是不能直接访问另一个进程的数据的。这里的解决办法是将这个标记位置写道共享缓冲区的最后,这样每个进程在读取共享缓冲区时先要从共享缓冲区的最后位置读取这个标记位置,然后再读取数据,读取完后再将下一个位置写回到该位置中,这样就能保证多个进程能正确读取数据。
在这里插入图片描述

内核态下不能直接使用用户态的函数

在创建信号量的函数里使用了字符串拷贝函数strcpy(),虽然没有报错,但是没有执行成功,搜了一下才知道在内核空间不能使用用户空间里的函数,因此可以自己写一个拷贝函数就可以了。

对队列的操作 pop 指向指针的指针

信号量的实现中会创建一个阻塞队列,用于保存阻塞在该信号量上的进程。原来的代码实现如下:

int sys_sem_post(sem_t* sem)
{
	struct task_struct *p;
	......
	popQueue(&(sem->wait_queue),p);
	......
}

int popQueue(waitQueue_t* queue,struct task_struct *p)
{
	if(getQueueLength(queue) == 0)	return -1;
	p = queue->wait_tasks[queue->front];
	queue->front = (queue->front+1) % (SEM_WAIT_MAX_NUM + 1);
	return 0;
}

popQueue函数中传入了一个指向struct task_struct的指针,然后在函数内部给这个指针赋值。这个涉及到的是C语言传参问题!如果在函数内部要改变传入的参数,那必须传指针而不能传值!(c语言有传指针和传值两种传参方式)上面的实现用的是传值的方式,显然不可能在函数内部改变p的指向。修改之后的代码如下:

int sys_sem_post(sem_t* sem)
{
	struct task_struct *p;
	......
	popQueue(&(sem->wait_queue),&p);
	......
}

int popQueue(waitQueue_t* queue,struct task_struct **p)
{
	if(getQueueLength(queue) == 0)	return -1;
	*p = queue->wait_tasks[queue->front];
	queue->front = (queue->front+1) % (SEM_WAIT_MAX_NUM + 1);
	return 0;
}

fork的使用

在实验中要创建1个生产者进程和5个消费者进程,进程的创建是通过fork()函数来实现的,fork()函数的使用可参照Linux中fork()函数的使用.

文件读写

实验中读写共享缓冲区其实就是读写文件,文件的读写可参照Linux 文件读写的简单介绍.

实验内容

本实验的目标有两个。
第一,在linux-0.11(没有实现信号量)上实现信号量有关的系统调用:

sem_t *sem_open(const char *name,unsigned int value);
int sem_wait(sem_t* sem);
int sem_post(sem)t* sem);
int sem_unlink(const char* name);

其中sem_t是信号量类型,要根据需要自行定义;sem_open()的功能是创建一个信号量,或打开一个已经存在的信号量,其中name是信号量的名字。不同的进程可以通过同样的name来共享一个信号量,如果该信号量不存在,就创建一个名为name的新信号量;如果存在,就打开已经存在的名为name的信号量。value是信号量的初始值,仅当创建信号量时,该参数才有效,其余情况下被忽略。在创建或打开成功时,返回值是该信号量的唯一标识。sem_wait()就是信号量的P操作,sem_post就是信号量的V操作。sem_unlink()的功能是删除名为name的信号量。

第二,利用上面实现的信号量系统调用,编写一个应用程序pc.c来模拟经典的生产者—消费者之间的同步。在这个程序中,要建立1个生产者进程和5个消费者进程,用文件建立一个共享缓冲区,生产者进程依次向这个缓冲区里写入正数0,1,2,3,…,499;每个消费者进程从缓存区中读取100个数,每读取1个数据就打印到标准输出上;缓存区文件最多只能保存10个数。
最终输出的效果应该是下面的样子,其中10:0中的10就是消费者进程的PID号,“:”后面的0就是从文件缓冲区中取出来的数据。不难看出,不论具体是哪个消费者进程取出了0~499中的哪个数,最终输出的结果都应该保持0,1,2,3,…,499这样的顺序。

10:0
10:1
10:2
10:3
10:4
11:5
......
11:498
11:499

实验过程

系统调用的添加在实验2的时候已经做过了,按照那个过程添加本实验的四个系统调用。

1.添加系统调用的编号

打开include/unistd.h文件,在如下图所示位置添加系统调用编号:
在这里插入图片描述

2.添加IDT(中断描述符表)

打开include/linux/sys.h文件,在文件中的sys_call_table[]的数组中添加sys_sem_open、sys_sem_wait、sys_sem_post和sys_sem_unlink,注意这里的前后顺序要和之前的系统调用编号的前后关系对应起来。同时,将sys_sem_open()、sys_sem_wait()、sys_sem_post()和sys_sem_unlink()声明全局函数。
在这里插入图片描述

3.修改系统调用数量

打开kernel/system_call.s文件,将系统调用的数量由原来的72改为76:
在这里插入图片描述

4. 实现系统调用函数

kernel/目录下新建一个sem.c文件,在include/目录下新建一个sem.h文件,用来保存新添加的4个系统调用函数。

sem.h文件

#ifndef __SEM_H
#define __SEM_H

#include <linux/sched.h>

#define SEM_NAME_MAX_LEN	32
#define SEM_WAIT_MAX_NUM	32

/* 阻塞队列 */
typedef struct{
	struct task_struct *wait_tasks[SEM_WAIT_MAX_NUM+1]; 
	int front;
	int rear;
}waitQueue_t;

/*信号量*/
typedef struct{
	char name[SEM_NAME_MAX_LEN];
	unsigned int value;	
	int valid;	
	waitQueue_t wait_queue;
}sem_t;

#endif

因为会有队列阻塞在某个信号量上,因此这里需要定义一个队列,用来保存阻塞进程。

sem.c文件涉及的内容较多,这里分开介绍。

阻塞队列接口

void initQueue(waitQueue_t* queue)
{
	queue->front = 0;
	queue->rear = 0;
}

int getQueueLength(waitQueue_t* queue)
{
	return (queue->rear - queue->front + SEM_WAIT_MAX_NUM + 1) % (SEM_WAIT_MAX_NUM + 1);
}

int pushQueue(waitQueue_t* queue,struct task_struct *p)
{
	if(getQueueLength(queue) == SEM_WAIT_MAX_NUM)
		return -1;
	queue->wait_tasks[queue->rear] = p;
	queue->rear = (queue->rear + 1) % (SEM_WAIT_MAX_NUM + 1);
	
	return 0;
}

int popQueue(waitQueue_t* queue,struct task_struct **p)
{
	if(getQueueLength(queue) == 0)	return -1;
	*p = queue->wait_tasks[queue->front];
	queue->front = (queue->front+1) % (SEM_WAIT_MAX_NUM + 1);
	return 0;
}

int isFull(waitQueue_t* queue)
{
	return (queue->rear+1) % SEM_WAIT_MAX_NUM == queue->front;
}

上面的函数是队列的基本操作函数,如果对队列的操作不熟悉可以参考这篇文章:03_数据结构:栈与队列。

字符串拷贝和比较

int kstrcpy(char* des,char* src)
{
	int index = 0;
	while(src[index] != '\0'){
		des[index] = src[index];
		index++;
	}	
	des[index] = '\0';
	return index;
}

int kstrcmp(char* str1,char* str2)
{
	int index = 0;
	while(str1[index] != '\0' && str2[index] != '\0'){
		if(str1[index] != str2[index])
			return -1;
        index++;
	}
    if(str1[index] != '\0' || str2[index] != '\0')  return -1;
	return 0;
}

因为在内核空间不能直接使用用户空间的函数,因此这里需要单独实现拷贝和比较函数。

sys_sem_open

//sys_sem_open()
sem_t* sys_sem_open(const char* name,unsigned int value)
{
	int i;
	char buf[SEM_MAX_NUM];
	//从用户空间获取名字
	for(i = 0; i < SEM_MAX_NUM; i++){
		buf[i] = get_fs_byte(name+i);
		if(buf[i] == '\0')	break;
	}
	//判断name长度
	if(i >= SEM_MAX_NUM){
		printk("The semaphore's name is too long and fail to create the semaphore!\r\n");
		return NULL;
	}
	//判断该信号量是否已经创建
	for(i = 0; i < SEM_MAX_NUM; i++){
		if(semaphores[i].valid == 1 && strcmp(buf,semaphores[i].name) == 0)
			return &semaphores[i];  //存在则直接返回
	}
	
	//创建信号量
	for(i = 0; i < SEM_MAX_NUM; i++){
		if(semaphores[i].valid != 1) {	//找一个无效信号量的位置
			kstrcpy(semaphores[i].name,buf);	//名字赋值
			semaphores[i].valid = 1;	//该信号量有效
			semaphores[i].value = value;	//赋值
			initQueue(&(semaphores[i].wait_queue));
			return (sem_t*)&semaphores[i];
		}
	}
	return NULL;
}

其中的get_fs_byte()函数的作用是从用户空间addr地址处取出一个字节char,数组semaphores[]是用来存储信号量的一个数组,也就是说,系统支持的信号量的数量是有限的。sys_sem_open()的作用是创建一个信号量,如果该信号量存在就直接返回信号量地址。具体创建过程是在semaphores[]数组中找一个空位置,然后设置好信号量名称、数值、信号量状态以及初始化阻塞队列,最终返回该信号量的地址。

sys_sem_wait

//sys_sem_wait()
int sys_sem_wait(sem_t* sem)
{
	struct task_struct *p;
	if(sem == NULL)	return -1;
	cli();	//关中断,保护临界区
	while(sem->value == 0){			//注意这里要使用while,而不能使用if
		/*将当前进程加入到阻塞队列中*/
		if(isFull(&(sem->wait_queue)))
		{ 	
			printk("error!The semaphore wait queue is full!");
			return -1;
		}
		//添加到阻塞队列
		current->state = TASK_UNINTERRUPTIBLE; //阻塞当前进程
		pushQueue(&(sem->wait_queue),current);
		schedule();
	}
	sem->value --;
	
	sti();	//开中断
	return sem->value;
}

这里临界段的保护采用关中断的方式,如果信号量的值为0,则将该进程加入该信号量的阻塞队列中,然后阻塞该队列,并重新进行调度。特别注意,判断信号量数值是否为0那里要用while()循环,而不能用if判断,因为如果有多个进程阻塞在一个信号量上,当唤醒任务时的做法时唤醒所有任务,具体哪个进程能获得信号量由调度函数schedule()来决定,因为进程存在优先级,一般会先唤醒高优先级的任务。所以while()循环的意义是,当该进程被唤醒之后要再次判断能否获得信号量,如果不能则再次将自己阻塞。

sys_sem_post

//sys_sem_post
int sys_sem_post(sem_t* sem)
{
	struct task_struct *p;
	if(sem == NULL)	return -1;
	cli(); //关中断,保护临界区
	sem->value++;
	/*唤醒所有阻塞在该信号量上的进程*/
	while(getQueueLength(&(sem->wait_queue)) > 0){
		popQueue(&(sem->wait_queue),&p);
		p->state = TASK_RUNNING;
	}
	sti(); //开中断
	return sem->value;
}

这个函数将信号量的值加1,然后唤醒所有阻塞在该信号量上的进程,这里就对应于sys_sem_post()函数中的while()循环,这里一次唤醒所有队列,具体哪个任务能获得信号量由调度函数决定。

sys_sem_unlink

//sys_sem_unlink
int sys_sem_unlink(const char* name)
{
	int i;
	char buf[SEM_MAX_NUM];
	//从用户空间获取名字
	for(i = 0; i < SEM_MAX_NUM; i++){
		buf[i] = get_fs_byte(name+i);
		if(buf[i] == '\0')	break;
	}
	//判断name长度
	if(i >= SEM_MAX_NUM){
		printk("The semaphore's name is too long and fail to delete the semaphore!\r\n");
		return -1;
	}
	//查找该信号量
	for(i = 0; i < SEM_MAX_NUM; i++){
		if(semaphores[i].valid == 1 && kstrcmp(buf,semaphores[i].name) == 0){
			semaphores[i].valid = 0;
			break;
		}
	}
	if(i == SEM_MAX_NUM){
		printk("The semaphore does not exist\r\n");
		return -1;
	}
	return 0;
}

这个函数的作用是在semaphores[]数组中找到要删除的信号量,然后改变该信号量的状态,这样就释放资源也就删除了。

5.修改Makefile(kernel/目录下)

修改两地地方:
第一处:
在这里插入图片描述
第二处:
在这里插入图片描述

6.编写用户程序pc.c

建议先在Ubuntu上实现生产者–消费者进程的模拟,在对函数稍作修改再在Linux-0.11上运行,如果直接在Linux-0.11上运行,出现错误不好确定是pc.c出现的问题还是信号量的实现出现的问题。
Ubuntu上的代码如下:

#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

#define TOTAL_ITEM_NUM	100		/*生产者生产的物品总数*/
#define CONSUMER_NUM	5		/*消费者的数量*/
#define BUF_SIZE		10		/*共享缓冲区的数量*/

int fd;

sem_t* empty;
sem_t* full;
sem_t* mutex;

void consumer();
void producer();

int main()
{
	int buf_out = 0;
	pid_t pid;

	mutex = sem_open("mutex", O_CREAT | O_EXCL, 0644, 1); 
    full = sem_open("full", O_CREAT | O_EXCL, 0644, 0); 
    empty = sem_open("empty", O_CREAT | O_EXCL, 0644, BUF_SIZE); 

	fd = open("file.txt",O_RDWR | O_CREAT,0644);
	lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
	write(fd,(char*)&buf_out,sizeof(int));

	if(!fork()){
		producer();
		exit(0);
	}
	for(int i = 0; i < CONSUMER_NUM; i++){
		if(!fork()){
			consumer();
			exit(0);
		}
	}
	while(pid = wait(NULL),pid != -1){
		printf("pid %d process terminated!\r\n",(int)pid);
	}
	close(fd);
	sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("mutex");
	return 0;
}

void consumer()
{
	int i;
	int item;
	int buf_out =  0;
	for(i = 0; i < TOTAL_ITEM_NUM / CONSUMER_NUM; i++)
	{
		sem_wait(full);
		sem_wait(mutex);

		//读出buf_out
		lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);  
		read(fd,(char*)&buf_out,sizeof(buf_out));

		lseek(fd,buf_out*sizeof(int),SEEK_SET);
		read(fd,(char*)&item,sizeof(item));
		printf("pid %d : consume item %d\r\n",getpid(),item);
		fflush(stdout);

		//写会buf_out
		buf_out = (buf_out + 1) % BUF_SIZE;
		lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
		write(fd,(char*)&buf_out,sizeof(buf_out));

		sem_post(mutex);
		sem_post(empty);
	}
}

void producer()
{
	int i;
	int buf_in = 0;
	for(i = 0; i < TOTAL_ITEM_NUM; i++){
		sem_wait(empty);
		sem_wait(mutex);

		lseek(fd,buf_in*sizeof(int),SEEK_SET);
		write(fd,(char*)&i,sizeof(i));
		buf_in = (buf_in + 1) % BUF_SIZE;
		//printf("pid %d : produce %d\r\n",getpid(),i);
		//fflush(stdout);

		sem_post(mutex);
		sem_post(full);
	}
}

编译命令:

gcc -o pc pc_Ubuntu.c -lpthread

其中的pc_Ubuntu.c就是Ubuntu上的pc.c,编译时要链接pthread库,否则会报错。

编译后的运行结果(部分)如下:
在这里插入图片描述
在上面的代码上修改一下就可以在Linux-0.11上运行了,代码实现如下:

#define __LIBRARY__

#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>


static inline _syscall2(sem_t*,sem_open,const char*,name,unsigned int,value)  /*sem_open*/
static inline _syscall1(int,sem_wait,sem_t*,sem)                              /*sem_wait*/
static inline _syscall1(int,sem_post,sem_t*,sem)                              /*sem_post*/
static inline _syscall1(int,sem_unlink,const char*,name)                      /*sem_unlink*/

#define TOTAL_ITEM_NUM	500		/*生产者生产的物品总数*/
#define CONSUMER_NUM	5		/*消费者的数量*/
#define BUF_SIZE		10		/*共享缓冲区的数量*/

int fd;

sem_t* empty;
sem_t* full;
sem_t* mutex;

void consumer();
void producer();

int main()
{
    int i;
	int buf_out = 0;
	pid_t pid;

	mutex = sem_open("mutex", 1); 
	/*sem_wait(mutex);
	return 0;*/
    full = sem_open("full", 0); 
    empty = sem_open("empty", BUF_SIZE); 

	fd = open("file.txt",O_RDWR | O_CREAT,0644);
	lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
	write(fd,(char*)&buf_out,sizeof(int));

	if(!fork()){
        /*printf("producer id created,pid = %d\r\n",getpid());*/
		producer();
		exit(0);
	}
	for(i = 0; i < CONSUMER_NUM; i++){
		if(!fork()){
            /*printf("consumer %d is created,pid = %d\r\n",i,getpid());*/
			consumer();
			exit(0);
		}
	}
	while(pid = wait(NULL),pid != -1){
		printf("pid %d process terminated!\r\n",(int)pid);
	}
	close(fd);
	sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("mutex");
	return 0;
}

void consumer()
{
	int i;
	int item;
	int buf_out =  0;
	for(i = 0; i < TOTAL_ITEM_NUM / CONSUMER_NUM; i++)
	{
		sem_wait(full);
		sem_wait(mutex);

		/*读出buf_out*/
		lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);  
		read(fd,(char*)&buf_out,sizeof(buf_out));

		lseek(fd,buf_out*sizeof(int),SEEK_SET);
		read(fd,(char*)&item,sizeof(item));
		printf("pid %d : consume item %d\r\n",getpid(),item);
		fflush(stdout);

		/*写会buf_out*/
		buf_out = (buf_out + 1) % BUF_SIZE;
		lseek(fd,BUF_SIZE*sizeof(int),SEEK_SET);
		write(fd,(char*)&buf_out,sizeof(buf_out));

		sem_post(mutex);
		sem_post(empty);
	}
}

void producer()
{
	int i;
	int buf_in = 0;
	for(i = 0; i < TOTAL_ITEM_NUM; i++){
		sem_wait(empty);
		sem_wait(mutex);

		lseek(fd,buf_in*sizeof(int),SEEK_SET);
		write(fd,(char*)&i,sizeof(i));
		buf_in = (buf_in + 1) % BUF_SIZE;
		/*printf("pid %d : produce %d\r\n",getpid(),i);
		fflush(stdout);*/

		sem_post(mutex);
		sem_post(full);
	}
}

7.Linux-011上的测试

文件拷贝

将编写的pc.c、sem.h和unistd.h文件拷贝到Linux-0.11中,pc.c是编写的测试代码;sen.h是信号量实现的头文件,因为在ppc.c中会用到;unistd.h在实验过程修改过,也需要拷贝一份到Linux-0.11的用户空间中,否则会报错或者得不到正确的实验现象。
具体拷贝过程如下:

  1. 挂载。在lab5实现目录下,执行以下命令进行挂载:
sudo ./mount-hdc

这样就将Linux-0.11挂载到./hdc/目录下了。

2.拷贝。unistd.h文件的拷贝位置和原来的相同,直接覆盖原来的文件。sem.h文件放在/usr/include/linux/路径下,pc.c放在/usr/root/目录下。

编译

编译命令如下,注意不需要上面提高的链接库:

gcc -o pc_0.11.c pc

其中的pc_0.11.c就是Linux-0.11上的pc.c,由于报错提示文件名太长,由原来的pc_Linux-0.11.c改为pc_0.11.c。编译后就得到了pc可执行文件。

运行

由于打印信息直接显示在屏幕上会显示不完整,这里将显示信息保存到文件中,再用Ubuntu打开查看。

./pc>output

其中的>output就表示保存到output文件,执行完成之后一定要执行sync命令将output保存到磁盘中再退出。退出之后,挂载,再打开output文件查看,命令如下:

sudo ./mount-hdc #挂载(lab5路径下)
sudo vim ./hdc/usr/root/output # 打开文件,如果没安装vim,可以替换为vi

打开文件的内容如下:
在这里插入图片描述
可以减少数据量,比如产生100个数,分析结果是否正确,修改文件中的宏定义即可修改数据量,分析发现结果是正确的。(当然,这是调试完成之后写下的,踩的坑和调试过程都记录在开头了)

终于是把这个实验认真地做完了,当然这里记录的还不太完整,后面有空再补充,这个实验不知道花了多长时间了,中间也是各种原因断断续续的,好在最终也是做出来了,也学会了很多东西,包括文件的简单读写,信号量的原理和使用,调试能力也得到了提升。总之,认真做试验收获很大。

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

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

相关文章

银河麒麟 linux V10 安装JDK

1、安装JDK之前&#xff0c;先查看系统是否已安装JDK相关软件包&#xff1a; 2. 如果已经安装过&#xff0c;可以先卸载&#xff08;可以跳过&#xff09; 3. 下载并解压jdk包 # 将下载好的jdk压缩包解压到指定目录/usr/local/jdk8 mkdir /usr/local/jdk8 cp jdk-8u271-linu…

详解设计模式:享元模式

享元模式&#xff08;Flyweight Pattern&#xff09;&#xff0c;是对象池的一种体现&#xff0c;也是 GoF 的 23 种设计模式中的一种结构型设计模式。 享元模式 主要用于减少创建对象的数量&#xff0c;以减少内存占用和提高性能。它提供了减少对象数量从而改善应用所需的对象…

[附源码]Python计算机毕业设计SSM跨移动平台的新闻阅读应用(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

二、Eureka服务注册与发现

Eureka服务注册与发现 Eureka基础知识 什么是服务治理 SpringCloud封装了Netflix公司开发的Eureka模块来实现服务治理。 在传统的RPC远程调用框架中&#xff0c;管理每个服务与服务之间依赖关系比较复杂、所以需要进行服务治理&#xff0c;管理服务与服务之间依赖关联&…

网上商店商城购物系统(asp.net,sqlserver,三层架构)

网上商店商城购物系统(asp.net,sqlserver,三层)(毕业论文10000字以上,程序代码,SqlServer数据库) 【运行环境】 VisualStudio SqlServer 代码下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1gzX_-Dzrt5jDHvQOCTN7qQ 提取码&#xff1a;8888 【项目包含内容…

Docker with IPV6

1、绪论 在 Docker 容器或群服务中使用 IPv6 之前&#xff0c;您需要在 Docker 守护进程中启用 IPv6 支持。之后&#xff0c;您可以选择对任何容器、服务或网络使用 IPv4 或 IPv6&#xff08;或两者&#xff09; 2、配置默认 Docker IPv6 注意&#xff1a;IPv6 网络仅在 Lin…

vue - vue使用webpack-bundle-analyzer进行代码打包的可视化数据分析

vue使用webpack-bundle-analyzer插件可以明确的看出每个文件模块的大小&#xff1b;常用来分析打包文件&#xff0c;优化代码等操作&#xff1b; 安装使用之后的效果如下&#xff1a;启动或打包项目&#xff0c;自动会在浏览器打开这个页面; 可以看出&#xff1a;右上角最蓝色的…

蓝牙学习五(广播包分析wireshark)

1.简介 软件工具&#xff1a;wireshark 硬件抓包工具&#xff1a;nrf52840 dongle 2.数据包分析 使用wireshark抓到的数据包分为两个部分&#xff0c;一部分是软件自己添加的内容&#xff0c;另一部分才是广播出来的数据。 2.1软件添加部分 软件添加的大部分内容可以不关注。这…

结构力学常用公式表,早晚用得到!

来源&#xff1a;360个人图书馆 常用截面几何与力学特征表​​​​​​​ 注&#xff1a; I 称为截面对主轴&#xff08;形心轴&#xff09;的截面惯性矩 (mm4)。基本计算公式如下&#xff1a; W称为截面抵抗矩 (mm)&#xff0c;它表示截面抵抗弯曲变形能力的大小&#xff0c…

SQL29 计算用户的平均次日留存率

原题链接 【描述】 题目&#xff1a;现在运营想要查看用户在某天刷题后第二天还会再来刷题的平均概率。请你取出相应数据。 【示例】&#xff1a;question_practice_detail 【题目分析】摘自题解区"Reg333"的题解 所谓次日留存&#xff0c;指的是同一用户&#x…

4-3:点赞功能

点赞 点赞 支持对帖子、评论点赞。第1次点赞&#xff0c;第2次取消点赞。 首页点赞数量统计帖子的点赞数量。 详情页点赞数量统计点赞数量。显示点赞状态。 Redis缓存用于点赞功能&#xff0c;可以提高性能。&#xff08;面向Key编程&#xff09; 1.建立RedisKeyUtil.java p…

力扣刷题记录162.1-----127. 单词接龙

目录一、题目二、代码三、运行结果一、题目 二、代码 class Solution { public://广度优先搜索int ladderLength(string beginWord, string endWord, vector<string>& wordList) {int i,j;//将vector转换成unordered_st 提高查询速度 目前理解不深unordered_set&l…

如何判断对象是否是垃圾

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 我们都Java会自动进行内存管理&#xff0c;JVM会进行垃圾回收&#xff0c;哪它是怎么判定哪些是“垃圾”并决定“垃圾”的生死呢&#xf…

【allegro 17.4软件操作保姆级教程七】布线操作基础之二--铜皮操作

目录 1.1全局动态铜皮参数设置 1.2手动绘制铜皮 1.3手动挖铜 1.4 手动修改铜皮边界 1.5删除孤岛铜皮 1.6动/静态铜皮转换 1.7合并铜皮 1.8平面铺铜和铜皮分割 1.9铜皮颜色设置 今天分享布线操作技巧中的铜皮操作。 1.1全局动态铜皮参数设置 单板上的电源部分、铺地都…

【网安神器篇】——wmic_info信息收集工具

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门 创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座…

2万字一网通办远程视频踏勘建设方案67页

目 录 1. 建设背景 2. 建设周期 3. 需求分析 3.1. 需求定义 3.1.1. 需求活动目的 3.1.2. 需求分析流程 3.2. 技术需求 3.3. 用户分析 3.3.1. 办事企业 3.3.2. 踏勘人员 3.3.3. 审批人员 3.3.4. 系统管理人员 3.4. 性能需求 3.4.1. 系统架构 3.4.2. 响应时间 3.…

π110E30 单通道数字隔离器兼容代替Si8610BC-B-IS

π110E30 单通道数字隔离器兼容代替Si8610BC-B-IS 。具有出色的性能特征和可靠性&#xff0c;整体性能优于光耦和基于其他原理的数字隔离器产品。 传输通道间彼此独立&#xff0c;可实现多种传输方向的配置&#xff0c;可实现 1.5kV rms 到 5.0kV rms 隔离耐压等级和 DC 到 600…

MyBatis-Plus中查询操作知识点总结

系列文章目录 Mybatis-Plus知识点[MyBatisMyBatis-Plus的基础运用]_心态还需努力呀的博客-CSDN博客 Mybatis-PlusSpringBoot结合运用_心态还需努力呀的博客-CSDN博客 MyBaits-Plus中TableField和TableId用法_心态还需努力呀的博客-CSDN博客 MyBatis-Plus删除操作知识点总结…

【Android App】物联网中指南针、计步器、感光器、陀螺仪的讲解及实战演示(附源码 超详细必看)

需要源码请点赞关注收藏后评论区留言~~~ 一、指南针-磁场传感器 顾名思义&#xff0c;指南针只要找到朝南的方向就好了。 可是在App中并非使用一个方向传感器这么简单&#xff0c;事实上单独的方向传感器已经弃用&#xff0c;取而代之的是利用加速度传感器和磁场传感器。 获得…

区块链工作原理(区块链治理系统、比特币、以太坊、智能合约)

文章目录Blockchain Governance SystemOn-Chain GovernanceOff-Chain GovernanceBitCoin BlockchainEthereum BlockchainProperties of Blockchain SystemSmart ContractsScalability Issues in Blockchain SystemsBlockchain Governance System 每个国家或者城市都有自己的一…