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

目录

一、实验目的

二、实验内容

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

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

三、实验准备

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 方式调用这些函数,它们工作起来就趋于正常了。

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

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

相关文章

C++中string类的常用函数

文章目录 默认成员函数常见构造函数(constructor) string类的容量操作size()empty()capacity()reserve()clear()resize() string类对象的访问及遍历操作重载 [ ]begin()end()begin() end() 遍历字符串rbegin()rend()rbegin() rend()反向遍历字符串C11范围for string类对象修改…

广域网技术——ppp,pppoe

目录 PPP协议概述 PPP协议原理 PPP协议三大组件&#xff1a; PPP链路建立流程 PPP连接建立接口状态&#xff1a; LCP协议报文格式 PPP协议报文格式&#xff1a; 1&#xff0c;链路层建立连接过程 LCP协商过程-正常协商 LCP协商-参数不一致 LCP协商-参数不识别 2&#xf…

YOLOv5/v7 添加注意力机制,30多种模块分析③,GCN模块,DAN模块

目录 一、注意力机制介绍1、什么是注意力机制&#xff1f;2、注意力机制的分类3、注意力机制的核心 二、GCN 模块1、GCN 模块的原理2、实验结果3、应用示例 三、DAN模块1、DAN模块的原理2、实验结果3、应用示例 大家好&#xff0c;我是哪吒。 &#x1f3c6;本文收录于&#xf…

AMC12和高考数学哪个更难?知识点有哪些不同?

AMC12和高考数学哪个更难&#xff1f;知识点有哪些不同&#xff1f;今天小编给大家来详细介绍一下&#xff01; 难度对比 从难度上看&#xff0c;高考数学的计算量更大&#xff0c;并且知识点比AMC10/12超前&#xff0c;需要用到极限和微积分的知识。 反观AMC10/12不需要用到…

数据结构与算法之美 | 栈

栈结构&#xff1a;后进者先出&#xff0c;先进者后出 栈是一种“操作受限”的线性表 当某个数据集合只涉及在一端插入和删除数据&#xff0c;并且满足后进先出、先进后出的特性&#xff0c;这时我们就应该首选“栈”这种数据结构 栈的实现 使用数组实现&#xff1a;顺序栈…

初探图神经网络——GNN

title: 图神经网络(GNN) date: tags: 随笔知识点 categories:[学习笔记] 初探图神经网络(GNN) 文章来源&#xff1a;https://distill.pub/2021/gnn-intro/ 前言&#xff1a;说一下为什么要写这篇文章&#xff0c;因为自己最近一直听说“图神经网络”&#xff0c;但是一直不了…

pycharm使用之torch_sparse安装

正式安装之前要先查看一下torch的版本 一、查看torch版本 1、winR &#xff0c;输入cmd 2、输入python 3、 输入import torch&#xff0c;然后输入torch.__version__&#xff0c;最后回车 可以看到我的torch版本是1.10.0 二、下载合适的torch_sparse版本 1、打开链接 https…

接口反应慢优化

遇到某个功能&#xff0c;页面转圈好久&#xff0c;需要优化 1.F12 查看接口时间 2.看参数 总共耗时9.6s Waiting for sercer response 时间是2秒 Content Download 7秒 慢在Content Download F12查看接口响应 显示Failed to load response data:Request content was e…

spark入门 高可用部署HA(五)

一、standalone基于修改部署 https://blog.csdn.net/weixin_43205308/article/details/131070277?spm1001.2014.3001.5501 二、安装ZOOKEEPER zookeeper 安装下载与集群 三、修改conf下的spark-env.sh vim conf/spark-env.sh注释以下内容&#xff08;根据自己环境修改&am…

visual studio 2022,ADO.NET 实体数据模型添加 sqlite数据库对象

文章目录 前言前期环境博客github 文档解析文件安装说明文件下载省流版nuget环境配置成功标志sqlite连接测试 前言 我们知道ADO.NET 实体数据模型特别适合动态开发数据库。因为ADO.NET可以使用DB First 开发 我们在开发一个程序的时候&#xff0c;经常会动态更新数据库字段&a…

算法模板(3):搜索(4):高等图论

高等图论 有向图的强连通分量 相关概念 强连通分量&#xff1a;Strongly Connected Component (SCC).对于一个有向图顶点的子集 S S S&#xff0c;如果在 S S S 内任取两个顶点 u u u 和 v v v&#xff0c;都能找到一条 u u u 到 v v v 的路径&#xff0c;那么称 S S…

C++多态和文件读写

C黑马&#xff0c;每天1.5倍速2个视频&#xff08;1小时&#xff09;&#xff0c;看到9月1日完成314个视频 目录 &#x1f511;多态 &#x1f333;基本语法 &#x1f333;原理剖析 &#x1f333;案例1 -- 计算器类 &#x1f333;纯虚函数和抽象类 &#x1f333;案例2 --…

redis知识复习

redis知识复习 redis基础知识一. redis的认识1. 非关系型数据库 与 传统数据库 的区别2. 安装redis并设置自启动3. 熟悉命令行客户端4. 熟悉图形化工具RDM 二. redis的命令与数据结构1. 数据结构介绍2. redis通用命令&#xff08;熟练掌握&#xff09; 三. redis的Java客户端1.…

SpringBoot整合Flyway实现数据库的初始化和版本管理

文章目录 一、Flyway1、介绍2、业务痛点3、个人理解 二、SpringBoot整合flyway1、整合2、SQL文件命名3、版本号校验算法4、工作流程5、注意事项 一、Flyway 1、介绍 Flyway 是一款开源的数据库版本管理工具。它可以很方便的在命令行中使用&#xff0c;或者在Java应用程序中引入…

【MySQL】数据表的基本操作

目录 1. 创建表 2. 创建表案例 2.1 创建一个users表 2.2 查看表结构 2.3 修改表 3. 删除表 MySQL&#x1f337; 1. 创建表 语法&#xff1a; CREATE TABLE table_name (field1 datatype,field2 datatype,field3 datatype ) character set 字符集 collate 校验规则 engine 存储…

chatgpt赋能python:如何升级Python的pip版本

如何升级Python的pip版本 如果你使用Python来进行程序开发&#xff0c;那么你一定需要用到pip&#xff0c;它是Python的包管理器&#xff0c;用于安装和管理各种Python库。 不过&#xff0c;一旦你开始使用pip&#xff0c;你可能会遇到一个问题&#xff1a;你的pip版本可能会…

几种技巧让大模型(ChatGPT、文心一言)帮你提高写代码效率!

代码神器 自从大模型推出来之后&#xff0c;似乎没有什么工作是大模型不能做的。特别是在文本生成、文案写作、代码提示、代码生成、代码改错等方面都表现出不错的能力。下面我将介绍运用大模型写代码的几种方式&#xff0c;帮助程序员写出更好的代码&#xff01;&#xff08;…

利用AI点亮副业变现:5个变现实操案例的启示

AI变现副业实操案例 宝宝起名服务AI科技热点号头像壁纸职业头像收徒&#xff1a;萌娃头像定制头像平台挂载 小说推广号流量营销号百家号AI共创计划公众号流量主 知识付费知识星球小报童&#xff1a; 整体思维导图&#xff1a; 在这里先分享五个实操案例: 宝宝起名服务AI科技热…

cvte 前端一面 凉经

cvte 前端一面 凉经 原文面试题地址&#xff1a;https://www.nowcoder.com/discuss/353159272857018368?sourceSSRsearch 1. vuex原理 和vuerouter的原理差不多 2. vuerouter的原理 ​ 首先在main.js中&#xff0c;import router from ‘./router’ 引入在router文件夹下面…

学习WooCommerce跨境电商社交媒体营销

WooCommerce 长期以来一直为电子商务店主提供多样化的服务。大约 500 万家商店啓用安装了免费的 WooCommerce 插件。 官方 WooCommerce 插件从 WordPress.org 下载了161,908,802次&#xff0c;并且还在增加。 超过5,106,506 个网站正在使用 WooCommerce。 本文网址: https…