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

news/2024/5/3 18:38:22/文章来源:https://blog.csdn.net/qq_70244454/article/details/128017713

写在最前的总结

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

调试总结

  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_outlseek(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_outbuf_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.luyixian.cn/news_show_228820.aspx

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

相关文章

【MySQL基础】MySQL常用的图形化管理工具有那些?

目录 一、为什么要使用MySQL图形化管理工具 原因 / 目的 / 作用 二、什么是DOS窗口? 三、常见的MySQL图形化管理工具有那些&#xff1f; 四、 常见几个MySQL图形工具的介绍 Navicat SQLyog MySQL Workbench DataGrip 五、Navicat图形工具的安装与使用 第一步&#x…

学习响应式布局

针对性内容 页面设计在不同设备的显示情况布局只会使用float定位&#xff0c;而不会掌握flex不能很好的使用rem作为设计单位掌握响应式布局、弹性等常见布局 学习内容 css中媒体查询的作用和使用方法flex弹性盒子的用法rem的作用和使用方法目录 针对性内容 学习内容 Media…

[iOS]App Store Connect添加银行卡时的CNAPS代码查询

App Store Connect 协议、税务和银行业务中&#xff0c;给付费APP类型添加银行卡需要填写CNAPS代码CNAPS代码&#xff0c;其实就是联行号。 联行号又称大额行号、银联号、银行行号或CNAPS号。 银行联行号查询

Java+JSP+MySQL基于SSM的会议交接平台的设计与实现-计算机毕业设计

项目介绍 随着社会竞争压力的不断加强&#xff0c;企事业单位内部的会议都在不断的增加&#xff0c;有效的会议可以提高企事业内部的沟通&#xff0c;更好的做出符合战略目标的决策&#xff0c;但是传统的会议交接有一定的问题存在&#xff0c;首先就是必须面对面进行传达&…

matlab图像的增强

1.灰度变换增强 &#xff08;1&#xff09;图像直方图 &#xff08;2&#xff09;图像直方图的均衡化 2.频域滤波增强 &#xff08;1&#xff09;低通滤波器 &#xff08;2&#xff09;高通滤波器 &#xff08;3&#xff09;同态滤波器 3.彩色增强 &#xff08;1&#xff09;真…

.vcxproj.filters 误删后如何重建

背景&#xff1a; 今天碰到这样一种情况&#xff0c;我在删除这个VS文件夹下的.user文件时&#xff0c;不小心把.vcxproj.filters也删除了。当然为什么删.user呢&#xff0c;因为换电脑了。 删除之后&#xff0c;我发现&#xff1a;我的解决方案目录变成这样了&#xff1a; 对…

[附源码]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…

深入理解ThreadLocal源码

1. 预备知识&#xff1a;强软弱虚引用 在Java中有四种引用的类型&#xff1a;强引用、软引用、弱引用、虚引用。 设计这四种引用的目的是可以用程序员通过代码的方式来决定对象的生命周期&#xff0c;方便GC。 强引用 强引用是程序代码中最广泛使用的引用&#xff0c;如下&a…

CSDNtop1全栈接口测试教程 jmeter接口测试,接口自动化测试【2】

延时等待&#xff08;全局性&#xff09; api 测试⽤例执⾏速度⾮常快&#xff0c;某些时候因为业务的特性想让它延迟⼏秒执⾏&#xff0c;那么这个时候就使⽤延时等待。 参数化 可以理解为&#xff1a;⼀个测试点需要多次操作&#xff0c;并且每次操作数据都是不⼀样但测试步…

最好的天线基础知识!超实用 随时查询

天线作为无线电的发射和接收设备是影响信号强度和质量的重要设备,其在移动通信领域的重要性非常关键。通过对天线选型,天线安装,天线调整从而保障基站覆盖区域的信号强度与质量。对其的 掌握程度是网规与网优工程师的技能基本要求之一。下文重点说明天线要掌握哪些方面及其原理…

【软件安装】Linux中RabbitMQ的安装

① 本篇是基于Linux操作系统中的安装&#xff0c;故先准备一个干净的Linux操作系统。本文中所有的操作基于CentOS8进行安装演示&#xff1b; ② 接下来的演示文本中&#xff0c;红色字体为操作步骤&#xff0c;黑色字体为解释说明&#xff1b; ③ 确保Linux系统中已经安装好必…

【python】 int、float、double与16进制字符串的互相转换

import structdef intToHex(num): # int转16进制return hex(num)[2:].upper()def hexToInt(hexString): # 16进制转intreturn int(hexString, 16)def floatToHex(floatValue): # float转16进制return struct.pack(>f, floatValue).hex().upper()def hexToFloat(hexString…

Lactoferrin-PEG-MTX/Paclitaxel 乳铁蛋白-聚乙二醇-甲氨蝶呤/紫杉醇

产品名称&#xff1a;乳铁蛋白-聚乙二醇-甲氨蝶呤 英文名称&#xff1a;Lactoferrin-PEG-MTX 纯度&#xff1a;95% 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 外观:固体或粘性液体&#xff0c;取决于分子量 PEG分子量可选&#xff1a;350、550、750、1k、2k、…

全球领先飞瞳引擎™云服务全球两千+企业用户,集装箱识别集装箱箱况残损检测,正常箱号识别率99.98%以上,箱信息识别及铅封识别免费

全球领先飞瞳引擎™AI集装箱识别检测云服务全球两千企业用户&#xff0c;集装箱识别集装箱箱况残损检测&#xff0c;正常箱号识别率99.98%以上&#xff0c;箱信息识别及铅封识别免费。CIMCAI中集飞瞳是全球应用落地最广&#xff0c;规模最大&#xff0c;最先进的的港航人工智能…

操作系统学习笔记(Ⅳ):文件

目录 1 文件管理 1.1 初识文件管理 1.文件属性 2.文件数据组织 3.向上功能 1.2 文件逻辑结构 1.无结构文件 2.有结构文件 3.顺序文件 4.索引文件 5.索引顺序文件 1.3 文件目录 1.文件控制块 2.目录结构 3.索引结点 1.4 文件物理结构 1.连续分配 2.链接分配 …

自定义表单、自定义流程、自定义页面、自定义报表应用开发平台

真正的大师,永远都怀着一颗学徒的心&#xff01; 一、项目简介 Java开发框架&#xff0c;自定义表单、自定义页面、自定义流程、自定义报表应用开发平台 二、实现功能 支持系统文件在线管理 支持代码在线编辑 支持URL 路由 支持黑白名单 支持定时任务 支持在线监控 支持…

Charles抓取接口报文并修改各种参数信息调试

1.首先介绍Charles面板 图上顶部工具栏常用介绍&#xff1a; 1是清除按钮&#xff1a;点击后将清空左侧抓取的接口列表&#xff0c;如果接口太多&#xff0c;可以点击该按钮清空列表&#xff0c;重新发起请求&#xff0c;一目了然&#xff1b; 2.是停止按钮&#xff1a;点击该按…

Windows OpenGL ES 图像色调

目录 一.OpenGL ES 图像色调 1.原始图片2.效果演示 二.OpenGL ES 图像色调源码下载三.猜你喜欢 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 基础 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 特效 零基础 OpenGL E…

能迪科技智能控制系统对中央空调进行精准、单独调控医院案例

案例背景​ 梅州市妇女儿童医院新院区&#xff08;以下简称“新院区”&#xff09;是省、市重点项目工程&#xff0c;建设地点位于江南新城客都大道北侧&#xff0c;一期项目总投资4.8亿元&#xff0c;占地面积50亩&#xff0c;总建筑面积87000平方米&#xff0c;按照三级妇幼保…

mybatis实战:二、mybatis xml 方式的基本用法

注释都在代码里&#xff0c;最好复制了再看&#xff01; 1.创建表 CREATE TABLE sys_user( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户 ID, user_name VARCHAR(50) COMMENT 用户名, user_password VARCHAR(50) COMMENT 密码, user_email VARCHAR(50) COMMENT 邮箱, user…