《操作系统》by李治军 | 实验6 - 信号量的实现和应用

news2024/10/8 20:51:36

目录

一、实验目的

二、实验内容

(一)用信号量解决生产者—消费者问题

(二)实现信号量,用生产者—消费者程序检验

三、实验准备

1、信号量

2、多进程共享文件

3、终端也是临界资源

4、原子操作、睡眠和唤醒

四、实验过程

(一)编写生产者—消费者检验程序

1. 编写 pc.c

2. 挂载 pc.c

(二)实现信号量

1. 添加系统调用 API

2. 新建 sem.h

3. 新建 sem.c

4. 修改 unistd.h

5. 修改 system_call.s

6. 修改 sys.h

7. 修改 Makefile

8. 挂载文件

9. 重新编译

(三)运行生产者—消费者程序

1. 编译运行 pc.c

2. 查看输出

3. 输出结果


一、实验目的

1、加深对进程同步与互斥概念的认识。

2、掌握信号量的使用,并应用它解决生产者——消费者问题。

3、掌握信号量的实现原理。

二、实验内容

(一)用信号量解决生产者—消费者问题

在 Ubuntu 上编写应用程序 pc.c ,解决经典的生产者—消费者问题,实现如下功能:

  • 建立一个生产者进程,N 个消费者进程(N > 1)
  • 用文件建立一个共享缓冲区
  • 生产者进程依次向共享缓冲区写入整数 0,1,2,...,M(M >= 500)
  • 消费者进程从共享缓冲区读数,每次读一个,并将读出的数字从缓冲区中删除,然后将 “本进程 ID + 删除数字” 输出到标准输出
  • 缓冲区同时最多只能保存 10 个数

【例】一种可能的输出效果

10: 0
10: 1
10: 2
10: 3
10: 4
11: 5
11: 6
12: 7
10: 8
12: 9
12: 10
12: 11
12: 12
……
11: 498
11: 499
  • 其中进程 ID 的顺序可能会有较大变化,但冒号后的数字一定是从 0 开始递增加 1 的

       另外 pc.c 中将会用到 sem_open()sem_close()sem_wait() 和 sem_post() 等和信号量相关的系统调用,需要我们自己在 Linux 0.11 中进行实现。

(二)实现信号量,用生产者—消费者程序检验

       Linux 0.11 版本还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX(Portable Operating System Interface of UNIX)规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以可以先实现一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下四个系统调用:

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_open()
功能创建一个信号量,或打开一个已经存在的信号量。
参数

sem_t :信号量的类型,根据实现的需要自定义。

name :信号量的名字。如果该信号量不存在,就创建一个新的名为 name 的信号量;如果该信号量存在,就打开这个已经存在的名为 name 的信号量。不同的进程可以通过提供同样的 name 而共享同一个信号量。

value :信号量的初值,只有在新建信号量时,此参数才有效,其余情况下该 value 被忽略。

返回值 sem_open() 新建或打开成功时,返回值是该信号量的唯一标识(如:在内核的地址、ID 等),由另两个系统调用使用;失败时,返回值是 NULL。

 

  • sem_wait()
功能

就是信号量的 P 原子操作,其功能就是对信号量的值减 1 。如果继续运行的条件不满足,则令调用进程等待在信号量 sem 上。

参数sem :指向信号量的指针。
返回值返回 0 表示成功,返回 -1 表示失败。

 

  • sem_post()
功能就是信号量的 V 原子操作,其功能就是对信号量的值加 1 。如果有等待在 sem 上的进程,它会唤醒其中的一个。
参数sem :指向信号量的指针。
返回值返回 0 表示成功,返回 -1 表示失败。

 

  • sem_unlink()
功能删除名为 name 的信号量。
参数name :信号量的名字。
返回值返回 0 表示成功,返回 -1 表示失败。

【实验提示】

       我们可以在 Linux 0.11 的 kernel 目录下新建一个 sem.c 文件实现如上四个系统调用的功能。然后将 pc.c 从 Ubuntu 移植到 Linux 0.11 下运行,测试实现的信号量。

 

 

三、实验准备

1、信号量

       信号量,英文为 semaphore,最早由荷兰科学家、图灵奖获得者 E. W. Dijkstra 设计,任何操作系统教科书的 “进程同步” 部分都会有详细叙述,信号量保证多个进程的合作变得合理有序。

       Linux 的信号量秉承 POSIX 规范,用  man sem_overview  可以查看相关信息。

       本次实验涉及到的信号量相关的系统调用包括:sem_open()sem_wait()sem_post() sem_unlink()

生产者—消费者问题

生产者—消费者问题的解法几乎在所有操作系统教科书上都有,其基本结构为:

Producer()
{
    // 生产一个产品 item;

    /* 空闲缓存资源 */
    P(Empty);

    /* 互斥信号量 */
    P(Mutex);

    // 将item放到空闲缓存中;

    V(Mutex);

    /* 产品资源 */
    V(Full);
}

Consumer()
{
    P(Full);
    P(Mutex);

    //从缓存区取出一个赋值给item;

    V(Mutex);

    // 消费产品item;
    V(Empty);
}
  • 显然在演示这一过程时需要创建两类进程,一类执行函数 Producer(),另一类执行函数 Consumer()

2、多进程共享文件

Linux 下使用 C 语言,可以通过以下三种方法进行文件的读写(但在 Linux 0.11 上只能使用前两种方法):

(1)使用标准 C 的 fopen()fread()fwrite()fseek() fclose() 等。

(2)使用系统调用 open()read()write()lseek() 和 close() 等。

(3)通过内存镜像文件,使用 mmap() 系统调用。

       fork() 调用成功后,创建的子进程会继承父进程拥有的大多数资源,包括父进程打开的文件。所以子进程可以直接使用父进程创建的文件指针/描述符/句柄,访问的是与父进程相同的文件。

       使用标准 C 的文件操作函数时要注意,它们使用的是进程空间内的文件缓冲区,父进程和子进程之间并不共享这个缓冲区。因此,任何一个进程做完写操作后,必须 fflush() 一下,将数据强制更新到磁盘,其它进程才能读到所需数据。

       综上所诉,建议直接使用系统调用进行文件操作。

3、终端也是临界资源

       用 printf() 向终端输出信息是很自然的事,但是当多个进程同时输出时,终端也成为了一个临界资源,所以也需要做好互斥保护,否则输出的信息可能错乱。

       另外,printf()后,信息只是保存在输出缓冲区内,还没有真正输出到标准输出(通常为终端控制台),这也可能造成输出信息的时序不一致。所以每次 printf() 后都调用一下 stdio.h 中的 fflush(stdout),以确保数据送到终端。

4、原子操作、睡眠和唤醒

       Linux 0.11 是一个支持并发的现代操作系统,虽然它还没有面向应用实现任何锁或者信号量,但它内部一定使用了机制,即在多个进程访问共享的内核数据时一定需要通过锁来实现互斥和同步。

       锁必然是一种原子操作(不会被调度机制打断的操作,这种操作一旦开始,就一直运行到结束)。通过模仿 Linux 0.11 的锁,就可以实现信号量。

       比如,多个进程对磁盘的并发访问就是一个需要锁的地方。Linux 0.11 访问磁盘的基本处理办法是在内存中划出一段磁盘缓存,用来加快对磁盘的访问。进程提出的磁盘访问请求首先要到磁盘缓存中去找,如果找到就直接返回;如果没有找到则申请一段空闲的磁盘缓存,以这段磁盘缓存为参数发起磁盘读写请求。请求发出后,进程要睡眠等待(因为磁盘读写很慢,这时应该让出 CPU 给其他进程执行)。这种方法是许多操作系统(包括现代 Linux、UNIX 等)采用的较通用的方法。这里涉及到多个进程共同操作磁盘缓存,而进程在操作过程可能会被调度而失去 CPU,因此操作磁盘缓存时需要考虑互斥问题,所以其中必定用到了锁,而且也一定用到了让进程睡眠和唤醒。

【例】下面是从 kernel/blk_drv/ll_rw_blk.c 文件中取出的两个函数:

static inline void lock_buffer(struct buffer_head * bh)
{
    // 关中断
    cli();

    // 将当前进程睡眠在 bh->b_wait
    while (bh->b_lock)
        sleep_on(&bh->b_wait);
    bh->b_lock = 1;

    // 开中断
    sti();
}

static inline void unlock_buffer(struct buffer_head * bh)
{
    if (!bh->b_lock)
        printk("ll_rw_block.c: buffer not locked\n\r");
    bh->b_lock = 0;

    // 唤醒睡眠在 bh->b_wait 上的进程
    wake_up(&bh->b_wait);
}

       分析 lock_buffer() 可以看出,访问锁变量 b_lock 时通过开、关中断来实现原子操作,阻止进程切换的发生。当然这种方法也有缺点,不适合用于多处理器环境中,但对于 Linux 0.11,它是一种简单、直接而有效的机制。因为我们实验中 bochs 模拟出的 Linux 0.11 就是一个单 CPU 的系统。

       另外,上面的函数表明 Linux 0.11 提供了这样的接口:用 sleep_on() 实现进程的睡眠,用 wake_up() 实现进程的唤醒。它们的参数都是一个结构体指针 —— struct task_struct *(即进程的 PCB ,在 sched.h 中定义),也就是说进程都睡眠或唤醒在该参数指向的一个进程 PCB 结构链表上。

       因此,在本次的实验中,我们也可以用开关中断的方式实现原子操作,还可以通过调用 Linux 0.11 自带的 sleep_on()  wake_up() 实现进程的睡眠和唤醒。

【注】

  • sleep_on() 的功能是将当前进程睡眠在参数指定的链表上(注意,这个链表是一个非常隐蔽的链表,详见《注释》一书)
  • wake_up() 的功能是唤醒链表上睡眠的所有进程。这些进程都会被调度运行,所以它们被唤醒后,还要重新判断一下是否可以继续运行。可参考 lock_buffer() 中的那个 while 循环

 

 

四、实验过程

       总的来说,本次实验的基本内容就是在 Linux 0.11 的内核中实现信号量,并向用户提供使用信号量的接口,用户使用该接口解决一个实际的进程同步问题。

(一)编写生产者—消费者检验程序

1. 编写 pc.c

oslab/exp_06 目录下新建一个 pc.c

【pc.c】

#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>

/* 添加系统调用API */
_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)

const char *FILENAME = "/usr/root/buffer_file";  /* 消费or生产的产品存放的缓冲文件的路径 */
const int NR_CONSUMERS = 5;                      /* 消费者数量 */
const int NR_ITEMS = 520;                        /* 产品最大量 */
const int BUFFER_SIZE = 10;                      /* 缓冲区大小,表示可同时存在的产品数量 */
sem_t *mutex, *full, *empty;                     /* 3个信号量 */
unsigned int item_pro, item_used;                /* 刚生产的产品号,刚消费的产品号 */
int fi, fo;                                      /* 供生产者写入或消费者读取的缓冲文件的句柄 */

int main(int argc, char *argv[])
{
    char *filename;
    int pid;
    int i;

    filename = argc > 1 ? argv[1] : FILENAME;

    /* 
     * O_TRUNC 表示:当文件以只读或只写打开时,若文件存在,则将其长度截为0(即清空文件)
     * 0222 表示:文件只写(前面的0是八进制标识)
     * 0444 表示:文件只读
    */

    /* 以只写方式打开文件给生产者写入产品编号 */
    fi = open(filename, O_CREAT| O_TRUNC| O_WRONLY, 0222);
    /* 以只读方式打开文件给消费者读出产品编号 */
    fo = open(filename, O_TRUNC| O_RDONLY, 0444);

    mutex = sem_open("MUTEX", 1);    /* 互斥信号量,防止生产和消费同时进行 */
    full = sem_open("FULL", 0);      /* 产品剩余信号量,大于0则可消费 */
    empty = sem_open("EMPTY", BUFFER_SIZE);    /* 空信号量,它与产品剩余信号量此消彼长,大于0时生产者才能继续生产 */

    item_pro = 0;

    if ( (pid = fork()) )    /* 父进程用来执行生产者动作 */
    {
        printf("pid %d:\tproducer created....\n", pid);

        /* 
         * printf输出的信息不会马上输出到标准输出(通常为终端控制台),而是先保存到输出缓冲区。
         * 为避免偶然因素的影响造成输出信息时序不一致,
         * 每次printf()后都调用一下 stdio.h 中的 fflush(stdout),
         * 来确保将输出内容立刻输出到标准输出。 
        */

        fflush(stdout);

        while (item_pro <= NR_ITEMS)    /* 生产完所需产品 */
        {
            sem_wait(empty);  /* P(empty) */
            sem_wait(mutex);  /* P(mutex) */

            /* 
             * 生产完一轮产品(文件缓冲区只能容纳 BUFFER_SIZE 个产品编号)后,
             * 将缓冲文件的位置指针重新定位到文件首部。
            */
            if( !(item_pro % BUFFER_SIZE) )  /* item_pro = 10 */
                lseek(fi, 0, 0);

            write(fi, (char *) &item_pro, sizeof(item_pro));  /* 写入产品编号 */ 
            printf("pid %d:\tproduces item %d\n", pid, item_pro);
            fflush(stdout);
            item_pro++;

            sem_post(full);        /* 唤醒消费者进程 */
            sem_post(mutex);
        }
    }
    else    /* 子进程来创建消费者 */
    {
        i = NR_CONSUMERS;
        while(i--)
        {
            if( !(pid=fork()) )    /* 创建i个消费者进程 */
            {
                pid = getpid();
                printf("pid %d:\tconsumer %d created....\n", pid, NR_CONSUMERS-i);
                fflush(stdout);

                while(1)
                {
                    sem_wait(full);
                    sem_wait(mutex);

                    /* read()读到文件末尾时返回0,将文件的位置指针重新定位到文件首部 */
                    if(!read(fo, (char *)&item_used, sizeof(item_used)))
                    {
                        lseek(fo, 0, 0);
                        read(fo, (char *)&item_used, sizeof(item_used));
                    }

                    printf("pid %d:\tconsumer %d consumes item %d\n", pid, NR_CONSUMERS-i+1, item_used);
                    fflush(stdout);

                    sem_post(empty);    /* 唤醒生产者进程 */
                    sem_post(mutex);

                    if(item_used == NR_ITEMS)    /* 如果已经消费完最后一个商品,则结束 */
                        goto OK;
                }
            }
        }
    }
OK:
    close(fi);
    close(fo);
    return 0;
}

2. 挂载 pc.c

pc.c 拷贝到虚拟机 Linux 0.11 /usr/root/ 目录下。

// oslab 目录下
sudo ./mount-hdc
cp ./exp_06/pc.c ./hdc/usr/root/
sudo umount hdc/

 

(二)实现信号量

这部分内容可以参考实验 3 的系统调用:《操作系统》by李治军 | 实验3 - 系统调用_Amentos的博客-CSDN博客

1. 添加系统调用 API

以下代码添加到应用程序 pc.c 中(上面已添加)。

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

2. 新建 sem.h

linux-0.11/include/linux 目录下新建 sem.h ,定义信号量的数据结构,包括信号量名称、信号量值和一个等待进程队列。

【sem.h】

#ifndef _SEM_H
#define _SEM_H

#include <linux/sched.h>

#define SEMTABLE_LEN    20
#define SEM_NAME_LEN    20

typedef struct semaphore
{
    char name[SEM_NAME_LEN];    /* 信号量名称 */
    int value;                  /* 信号量值 */
    struct task_struct *queue;  /* 信号量等待队列 */
} sem_t;

extern sem_t semtable[SEMTABLE_LEN];  /* 定义一个信号量表 */

#endif

这里 #ifndef、#define、#endif 的作用是防止头文件被重复引用而导致重复编译。具体原理可以看看这篇文章:头文件为什么要加#ifndef #define #endif

3. 新建 sem.c

linux-0.11/kernel 目录下,新建源代码文件 sem.c,实现四个信号量函数。

【sem.c】

#include <linux/sem.h>
#include <linux/sched.h>
#include <unistd.h>
#include <asm/segment.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
//#include <string.h>

sem_t semtable[SEMTABLE_LEN];  /* 定义一个信号量表 */
int cnt = 0;

sem_t *sys_sem_open(const char *name,unsigned int value)
{
    char kernelname[100];   
    int isExist = 0;
    int i = 0;
    int name_cnt = 0;

    while( get_fs_byte(name+name_cnt) != '\0' )
        name_cnt++;

    if( name_cnt > SEM_NAME_LEN )
        return NULL;

    /* 从用户态复制到内核态 */
    for(i=0;i<name_cnt;i++)
        kernelname[i] = get_fs_byte(name+i);

    int name_len = strlen(kernelname);
    int sem_name_len = 0;
    sem_t *p = NULL;

    for(i=0;i<cnt;i++)
    {
        sem_name_len = strlen(semtable[i].name);
        if(sem_name_len == name_len)
        {
                if( !strcmp(kernelname,semtable[i].name) )
                {
                    isExist = 1;
                    break;
                }
        }
    }

    if(isExist == 1)
    {
        p = (sem_t*)(&semtable[i]);
        //printk("find previous name!\n");
    }
    else
    {
        i = 0;
        for(i=0;i<name_len;i++)
        {
            semtable[cnt].name[i] = kernelname[i];
        }
        semtable[cnt].value = value;
        p = (sem_t*)(&semtable[cnt]);
        //printk("creat name!\n");
        cnt++;
    }
    return p;
}


int sys_sem_wait(sem_t *sem)
{
    cli();   /* 关中断 */

    while( sem->value <= 0 )
        sleep_on( &(sem->queue) );    /* 所有小于0的进程都阻塞 */
    sem->value--;
             
    sti();   /* 开中断 */
    return 0;   
}


int sys_sem_post(sem_t *sem)
{
    cli();
    sem->value++;
    if( (sem->value) <= 1 )
        wake_up( &(sem->queue) );
    sti();
    return 0;
}


int sys_sem_unlink(const char *name)
{
    char kernelname[100];   /* 应该足够大了 */
    int isExist = 0;
    int i = 0;
    int name_cnt = 0;

    while( get_fs_byte(name+name_cnt) != '\0' )
        name_cnt++;

    if( name_cnt > SEM_NAME_LEN )
        return NULL;

    for(i=0;i<name_cnt;i++)
        kernelname[i] = get_fs_byte(name+i);

    int name_len = strlen(name);
    int sem_name_len = 0;

    for(i=0;i<cnt;i++)
    {
        sem_name_len = strlen(semtable[i].name);
        if(sem_name_len == name_len)
        {
            if( !strcmp(kernelname,semtable[i].name) )
            {
                isExist = 1;
                break;
            }
        }
    }

    if(isExist == 1)
    {
        int tmp = 0;

        for(tmp=i;tmp<=cnt;tmp++)
        {
            semtable[tmp] = semtable[tmp+1];
        }
        cnt = cnt-1;
        return 0;
    }
    else
        return -1;
}

4. 修改 unistd.h

新增了四个系统调用,进入 linux-0.11/include 目录,打开 unistd.h ,增添新的系统调用编号。

#define __NR_sem_open	xx
#define __NR_sem_wait	xx
#define __NR_sem_post	xx
#define __NR_sem_unlink	xx

5. 修改 system_call.s

进入 linux-0.11/kernel 目录,打开 system_call.s ,修改系统调用总数。

6. 修改 sys.h

进入 linux-0.11/include/linux ,打开 sys.h ,为新增的四个系统调用添加系统调用函数名并维护系统调用表。

  注   系统调用函数名在 sys_call_table 数组中的位置必须和 unistd.h 中 __NR_name 的值相同

7. 修改 Makefile

linux-0.11/kernel 目录下的 Makefile 进行如下修改。

第一处,在【OBJS】后添加:

sem.o

第二处,在【Dependencies】后添加:

sem.s sem.o: sem.c ../include/linux/sem.h ../include/linux/kernel.h \
../include/unistd.h

8. 挂载文件

将编写的 sem.h 和修改后的 unistd.h 拷贝到 Linux 0.11 系统中,这和实验三 “系统调用” 的原理是一样的。

// oslab 目录下
sudo ./mount-hdc
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/
cp ./linux-0.11/include/linux/sem.h ./hdc/usr/include/linux/
sudo umount hdc/

9. 重新编译

// linux-0.11 目录下
make all

(三)运行生产者—消费者程序

1. 编译运行 pc.c

oslab 目录下 ./run  进入 Linux 0.11,编译并运行 pc.c ,将输出信息重定向到 pc.txt 文件。

gcc -o pc pc.c
./pc > pc.txt
sync

注意最后一定要 sync !

2. 查看输出

pc.txt 拷贝到 Ubuntu 下查看。

sudo ./mount-hdc
sudo cp ./hdc/usr/root/pc.txt ./exp_06
sudo chmod 777 exp_06/pc.txt
cat exp_06/pc.txt | more

可以在终端通过 cat 命令查看,也可以直接双击 pc.txt 打开查看。

注意,如果显示 “您没有打开文件所需的权限” ,就通过下以下命令修改权限:

sudo chmod 777 exp_06/pc.txt

 

3. 输出结果

……

 

【实验提示】

1、应对混乱的 bochs 虚拟屏幕

       不知是 Linux 0.11 还是 bochs 的 bug,如果向终端输出的信息较多,bochs 的虚拟屏幕会产生混乱。此时按 Ctrl+L 可以重新初始化一下屏幕,但输出信息一多,还是会混乱。比如一开始直接通过 ./pc 运行程序,结果显示如下。

 

       所以建议把输出信息重定向到一个文件: ./pc > pc.txt (即重定向到 pc.txt),然后用 vi、more 等工具按屏查看这个文件,可以基本解决此问题。也可以把文件拷贝到 Ubuntu 系统下进行查看。

vi pc.txt:

 

2、关于 string.h 的提示

       下面描述的问题未必具有普遍意义,仅做为提醒,请实验者注意。

       include/string.h 实现了全套的 C 语言字符串操作,而且都是采用汇编 + inline 方式优化。但在使用中,某些情况下可能会遇到一些奇怪的问题。比如某人就遇到 strcmp() 会破坏参数内容的问题。如果调试中遇到有些 “诡异” 的情况,可以试试不包含头文件,一般都能解决。因为不包含 string.h,就不会用 inline 方式调用这些函数,它们工作起来就趋于正常了。

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

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

相关文章

接口测试 —— 接口测试定义

1、接口测试概念 &#xff08;重点&#xff09; 接口测试是测试系统组件间接口的一种测试&#xff0c;它界于单元测试与系统测试中间。 接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。 测试的重点是要检查数据的交换&#xff0c;传递和控制管理过…

pinia 持久化插件pinia-plugin-persistedstate 安装、使用(图文详解)

序&#xff1a; 1、博主vue3、ts 5.x、pinia 2.1.3版本&#xff0c; 2、所以如果试了不行的你看看是不是自己版本和博主的对不上 3、其实就是省略掉localStorage 这一步&#xff0c;会自己写的小伙伴自己写个也是蛮快的 4、放个中文文档》Home | pinia-plugin-persistedstate 5…

【Verilog】汉明码

文章目录 汉明码定义校验位个数编码规则一个例子编码解码 C实现功能编写测试结果 Verilog实现.v功能代码testbench波形 汉明码 定义 在传输的信息流中插入验证码&#xff0c;侦测单一比特错误只能发现和修正一位错误&#xff0c;对于两位或两位以上的错误无法发现与修正 校验…

iSCSI共享存储搭建

1.简介 iSCSI&#xff1a;Internet Small Computer System Interface&#xff0c;Internet小型计算机系统接口&#xff0c;又称为IP-SAN&#xff0c;是一种基于因特网及SCSI-3协议下的存储技术。 2.iSCSI的作用 基于客户端和服务端架构的虚拟磁盘技术&#xff0c;服务端提供…

如何让url在新页面打开路由页面,并脱离vue-admin-template的壳,即不包裹在侧边栏和顶栏中

文章目录 一、打开的页面不包裹在侧边栏和顶栏中二、新窗口打开&#xff08;_blank&#xff09;三、最终效果 一、打开的页面不包裹在侧边栏和顶栏中 在使用vue-admin-template新建的页面中&#xff0c;打开的页面都是在框架内的内容区。 但假如我需要在左侧点击一个链接&…

面试题丨android面试问题合集

1、项目里静态分析和基于xposed动态工具介绍一下&#xff0c;如果不使用xposed&#xff0c;怎么实现动态分析工具&#xff1f; 静态分析工具是指在不运行程序的情况下&#xff0c;通过对程序文件进行源代码分析&#xff0c;从而对程序的安全性、可靠性、性能等进行分析的工具。…

HOOPS技术如何助力企业数字化转型?

近年来&#xff0c;随着科技的迅速发展&#xff0c;数字技术的应用已经深入到各个行业和领域。云计算、人工智能、物联网、大数据分析等技术的成熟和普及&#xff0c;为企业提供了丰富的数字化工具和解决方案。企业意识到利用这些技术可以提高效率、降低成本、创新业务模式&…

学习中心上新丨Python教程-Django框架快速入门到实战

腾讯云千锋教育强强联手&#xff0c;一同研发重磅推出全新课程《千锋图片云存储》Python 教程-Django 框架从入门到实战-基于腾讯云 COS Django框架实战教程发布 腾讯云开发者社区“学习中心”直达&#xff1a; 腾讯云开发者社区-腾讯云 扫码加入“腾讯云开发者社区学习中心交…

618小红书推广种草达人,品牌运营4大块是什么

当今电商行业的竞争越来越激烈&#xff0c;品牌宣推变得尤为重要。其中&#xff0c;小红书是一个备受关注的电商平台之一。618小红书推广种草达人&#xff0c;品牌运营4大块是什么&#xff0c;今天和大家一起分享下。 618期间的小红书推广落地&#xff0c;应从关键词优化、内容…

流量矩阵估计综述Traffic Matrix Estimation Techniques- A Survey on Current Practices

Paper: Traffic Matrix Estimation Techniques- A Survey on Current Practices | IEEE Conference Publication | IEEE Xplore 来源&#xff1a;2023 International Conference on Sustainable Computing and Data Communication Systems (ICSCDS) (强烈建议搭配英文原文看&…

Vue 有哪些经典面试题?

前言 下面总结了vue的一些经典的面试题&#xff0c;希望对正在找工作面试的小伙伴们提供一些帮助&#xff0c;我们废话少说直接进入整体、 简述一下什么是MVVM模型 MVVM&#xff0c;是Model-View-ViewModel的简写&#xff0c;其本质是MVC模型的升级版。其中 Model 代表数据模…

不入耳蓝牙耳机哪种好?音质好佩戴舒适的四款不入耳蓝牙耳机推荐

普通入耳式蓝牙耳机长时间佩戴会有挤压感、不适感&#xff0c;而不入耳蓝牙耳机则没有这种烦恼&#xff0c;不入耳设计&#xff0c;佩戴更稳固舒适&#xff0c;运动佩戴也不会轻易甩掉。我来推荐四款好用、不可错过的不入耳蓝牙耳机给大家&#xff0c;来看看有没有心仪那款吧。…

华为和思科如何实现双机热备?

概要&#xff1a; 在当今高度依赖网络的时代&#xff0c;网络设备的高可用性和可靠性变得尤为重要。网络设备的故障可能导致服务中断、数据丢失以及生产力下降等问题。为了应对这些挑战&#xff0c;一种常见的解决方案是使用双机热备&#xff08;High Availability, HA&…

「Win」HOOK钩子技术

✨博客主页&#xff1a;何曾参静谧的博客 &#x1f4cc;文章专栏&#xff1a;「Win」Windows程序设计 相关术语 HOOK技术&#xff1a;是一种在Windows系统中常用的技术&#xff0c;它可以截获并修改操作系统或应用程序的行为。通过使用Hook技术&#xff0c;我们可以实现以下功能…

网络编程重点

1> OIS 7层模型 TCP/IP 4层模型 5层模型 2> 传输层的功能 网络层的功能&#xff1f;以及分别是第几层 传输层&#xff1a;提供端到端的可靠传输&#xff0c;指定哪个进程哪个发送进程接收 第四层 网络层&#xff1a;寻址和路由选择 第三层 3>MAC地址&#xff1a; a. …

JAVA 巧用 Robot 类(应用于网课)

目录 前言&#xff1a;理论依据&#xff1a;现实依据&#xff1a;朴素版只能循环阅读不能翻页&#xff1a;升级版 翻页 阅读&#xff1a;如何使用&#xff1a; 前言&#xff1a; 最近发现有个阅读得读300分钟&#xff0c;懒得去找软件&#xff0c;于是就自己写了一个代码去实现…

(1Gb)S28HS01GTGZBHA030/ S28HS01GTGZBHV033/ S28HS01GTGZBHA033 FLASH - NOR闪存器件

产品简介&#xff1a; Infineon 带有HyperBus™的S26HSxT以及S26HLxT Semper™闪存是一种高性能、安全可靠的NOR闪存解决方案。 这些组件集成了关键的安全功能&#xff0c;用于汽车、工业、通信等行业的各种应用。S26HSxT和S26HLxT Semper闪存采用HyperBus接口&#xff0c;符…

仙境传说RO:添加限购物品刷新物品库存教程

仙境传说RO&#xff1a;添加限购物品刷新物品库存教程 大家好我是艾西&#xff0c;在游戏中我们会有普通的基础装备那么必然就会有到顶的套装&#xff0c;往往可能一套到顶的套装就可能霸服。那么就需要GM去做游戏的设定以及限制&#xff0c;上一篇文章中我给大家讲述了如果创…

风控系统设计

一、思路 要实现一个简单的业务风控组件,要做什么工作呢? 1.风控规则的实现 a.需要实现的规则: 自然日计数 自然小时计数 自然日自然小时计数 自然日自然小时计数 这里并不能单纯地串联两个判断,因为如果自然日的判定通过,而自然小时的判定不通过的时候,需要回退,自然日…

NetApp 混合云技术

为何选择 NetApp 的混合云 NetApp 可帮助您构建一个现代化的混合云&#xff0c;从而统一您的基础架构&#xff0c;并让您的数据可以自由流动到所需的任何位置&#xff0c;确保以数据为中心的业务能够快速应对变化&#xff0c;灵活调整方向&#xff0c;并获得竞争优势。 什么是…