lab3 pgtbl

news2025/1/24 3:04:13

image-20230818114429770

Pre

在这个lab中,你将探索页表,并且修改它们以简化从用户空间拷贝数据到内核空间的函数

在开始之前,需要完成

  1. 阅读xv6 book的第3章
  2. kern/memlayout.h 有关内存的布局
  3. kern/vm.c 包含大部分虚拟内存的代码
  4. kernel/kalloc.c 分配和释放虚拟内存的代码

内存布局

image-20230816152606482

Print a page table

task

  1. 定义一个叫做vmprint(pagetable_t)的函数,用下面的格式打印页表

  2. 添加if(p->pid==1) vmprint(p->pagetable)exec.creturn argc语句前面,这将会打印第一个进程的页表

  3. 通过make gradepte printout进行测试,也可以

    ./grade-lab-pgtbl pte printout
    

打印的格式如下

  1. 首先从根页表开始从上到下地递归地打印,并且只打印有效的页表项
  2. 具体到每一个页表项
    1. 加入是第x级的页表,那就先打印x..
    2. 然后打印这个页表项在这个页表中的索引
    3. pte后面紧跟着的内容是这个页表项的所有内容,总共64bit
    4. pa后面紧跟着的就是这个页表项指向的地址,假设pte页表项的内容是val,那么pa=((val>>10)<<12),xv6中已经定义了一个宏来实现
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

hints

  1. kernel/vm.c中完成这个函数
  2. 使用kernel/riscv.h中定义的宏
  3. freewalk将给你灵感
  4. 记得在kernel/defs.h中声明你的函数,这样才可以在exec函数中使用它
  5. 使用%p去打印64bit的pte和address

思路

  1. 先看看freewalk中是如何遍历这三级页表的,思路就比较清晰了
  2. 这里用了一个depth控制当前位于第几级页表,方便打印和控制递归的终点
int depth = 0;
void print_prefix(int i) {
    for (int i = 0; i <= depth; i++) {
        printf("..");
        if (i < depth) {
            printf(" ");
        }
    }
    printf("%d: ", i);
}
void vmprint(pagetable_t pagetable) {
    printf("page table %p\n", pagetable);
    for (int i = 0; i < 512; i++) {
        pte_t pte = pagetable[i];
        // 如果这一项有效
        if (pte & PTE_V) {
            print_prefix(i);
            printf("pte %p ", pte);
            printf("pa %p", PTE2PA(pte));
            printf("\n");
            if (depth < 2) {
                depth++;
                vmprint((pagetable_t)PTE2PA(pte));
                depth--;
            }
        }
    }
}

image-20230817200840273

A kernel page table per process

这个task和下一个task的目标就是使得内核可以直接解引用进程传递的指针

task

  1. 在这个task中,你先修改kernel,使得每个进程在内核态的时候都可以用它自己对内核页表的拷贝
  2. 修改struct proc为每个进程都维护一个内核页表
  3. 修改scheduler使得进程切换的时候切换内核的页表
  4. 完成到这一步时,每个进程中的内核页表都应该和现在的全局内核页表相同
  5. 如果你能通过usertests就说明完成了这个task

hints

  1. struct proc中增加一个字段表示这个进程独有的内核页表

  2. 为一个新进程创造一个内核页表的合理的方法是

    实现一个kvminit的新版本,这个新版本会创建一个新的页表,而不是修改已有的页表

    你需要在allocproc中调用这个新的函数

  3. 保证每个进程的内核页表都有一个映射,这个映射可以找到进程的内核栈

    在未修改的xv6中,所有的内核栈都在procinit中被创造

    你需要去移动procinit中的部分或者全部到allocproc

  4. 修改scheduler()去将进程的内核页面加载到satp寄存器, 可以通过kvminithart学习这个的用法

    不要忘记在调用w_satp之后调用sfence_vma

  5. 调度器应该在没有进程运行时使用kernal_pagetable

  6. freeproc中释放一个进程的内核页表

  7. 你将需要一个方法,这个方法在释放页表的同时不会释放真正的物理页面

  8. vmprint可以在debug页表的时候办法

思路

可以说,跟着hint一步一步走,就成功了,但是我个人觉得hint或者说这个文档没有说的非常清楚,导致有一点歧义,接下来一个hint一个hint分析

  1. proc中增加一个字段,这个就不用说了
pagetable_t kernel_pgtbl;
  1. 这是创建内核页表的关键。我之前以为是要把现在的内核页表给复刻一遍,结果写了个递归函数去copy当前的内核页表。但是实际上,只需要我们创建一个和kvminit创建出来的的内核页表一样的就行了,也就是最原始的那种,只有内核的代码和数据以及一些外设,这些代码都在kvminit代码中,所以直接抄一份就行了

通过这个代码可以发现,创建一个最原始的内核页表,就三步

  • 先申请一个物理页
  • 将这个物理页清空
  • 写入固定的一系列地址到页表中(大量的kvmmap操作)

最后在allocproc中进程被正确创建之后,给这个进程的内核页表赋值p->kernel_pgtbl = new_kernel_pgtbl();

void init_kernel_pgtbl(pagetable_t pgtbl) {
    memset(pgtbl, 0, PGSIZE);

    // uart registers
    kvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

    // virtio mmio disk interface
    kvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

    // CLINT
    kvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

    // PLIC
    kvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

    // map kernel text executable and read-only.
    kvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);

    // map kernel data and the physical RAM we'll make use of.
    kvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W);

    // map the trampoline for trap entry/exit to
    // the highest virtual address in the kernel.
    kvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}

pagetable_t new_kernel_pgtbl() {
    pagetable_t pgtbl = (pagetable_t)kalloc();
    init_kernel_pgtbl(pgtbl);
    return pgtbl;
}

/*
 * create a direct-map page table for the kernel.
 */
void kvminit() {
    kernel_pagetable = new_kernel_pgtbl();
}

需要注意的是,我们在这里修改了kvmmap函数的声明,因为之前它是默认使用内核页表的,现在需要用每个进程自己的内核页表,这里主要要修改两个函数,分别是kvmmapkvmpa,注意要将修改更新到defs.h文件以及所有用到这两个函数的地方,有个比较隐秘的是在virtio_disk.c

  1. 内核栈相关的修改

正常来说,内核栈是在procinit的时候对proc数组的所有进程进行初始化,然后将地址映射放到唯一的内核页表中

而我们现在只需要在allocproc中申请内存栈并将这个地址变换写到这个进程的内存页表即可

具体步骤如下

  • procinit函数中和内存栈相关的代码给剪切

  • 将代码复制到allocproc的合适位置,放在进程内核页表被初始化的后面就不错

    char *pa = kalloc();
    if (pa == 0)
        panic("kalloc");
    uint64 va = KSTACK((int)(0));
    kvmmap(p->kernel_pgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
    p->kstack = va;
  1. hints的第4和第5点一起考虑

第4点就是要求我们在进程获得cpu的时候把它自己的内核栈给切换上去,即修改寄存器satp

第5点则是要求在没有进程使用的时候,切换到内核唯一的那个页表,这个可以通过在进程执行完之后就切换satp寄存器为唯一的内存页表

具体实现如下,在swtch函数执行前后进行切换即可

w_satp(MAKE_SATP(p->kernel_pgtbl));
sfence_vma();

swtch(&c->context, &p->context);

kvminithart();
  1. 最后一步,在进程被终止的时候,回收这个进程的内核页表

首先补充一个hints没有说的,我们还需要回收这个进程的内核栈的那个页面,否则会造成内存浪费

freeproc函数中加入如下代码

    if (p->kernel_pgtbl) {
        free_kernel_stack(p->kernel_pgtbl, p->kstack);
        p->kstack = 0;
        free_kernel_pgtbl(p->kernel_pgtbl, 0);
        p->kernel_pgtbl = 0;
    }

其中free_kernel_stack就是通过栈的虚拟地址,经过内核页表,找到物理地址,将其free

void free_kernel_stack(pagetable_t pgtbl, uint64 stack_p) {
    void *real_p = (void *)kvmpa(pgtbl, stack_p);
    kfree(real_p);
}

其中free_kernel_pgtbl就复杂一些,需要递归地删除这个内核页表,并且不能真正地删除物理页面

void free_kernel_pgtbl(pagetable_t pgtbl, int depth) {
    if (depth == 2) {
        kfree((void *)pgtbl);
        return;
    }

    for (int i = 0; i < 512; i++) {
        pte_t *pte = &pgtbl[i];
        if (*pte & PTE_V) {
            free_kernel_pgtbl((pagetable_t)(PTE2PA(*pte)), depth + 1);
        }
    }
    kfree((void *)pgtbl);
}

至此,第二个task结束,可以运行./grade-lab-pgtbl usertests检查

这个检查的过程非常长,在我这运行了100s,一度以为是死锁了写错了

image-20230817200806108

Simplify

task

  1. 将用户的映射都加入到进程的内核页表中
  2. vm.c中的copyin函数的函数体替换成对copyin_new的调用,对copyinstr也是一样的处理
  3. make grade通过就说明成功了
  4. 你需要修改xv6使得用户进程的虚拟地址不会超过PLIC寄存器的地址

hints

  1. 先确定copyin正确,再去尝试copyinstr

  2. 每次内核改变用户的映射时,都要同步修改到这个用户的内核页表

    包括fork exec sbrk

  3. 不要忘记了在userinit中将第一个进程的用户也更新到他的内核页表

  4. PTE_U不要也拷贝到了kernel的内核页表中

  5. 不要忘记了PLIC的限制

image-20230817225736470

错误之路

  1. 如何控制用户进程申请的最大虚拟地址,在umalloc.c文件的morecore函数中对sbrk函数的返回值进行判断
  2. 修改fork函数,主要是修改uvmcopy函数,在它使用mappagesnew增加页表项时,成功后给kernal也增加页表项
  3. 修改exec函数,有好多地方需要修改,一个一个来
    1. 调用了proc_pagetable去创建一个只有顶部两个和trap相关的页面,其他的都为空,这应该相当于清空,内核该怎么办呢?也清空自己吗。目前是清空原有的内核页表,然后生成一个新的内核页表,最后将内核栈的映射加上去
    2. uvmalloc中增加对kernel的操作
    3. uvmclear中增加对kernel的操作,这个好像不需要

第二次尝试

  1. vm.c中创造函数copy_to_kernaldealloc_kernal,其中的copy函数会将用户标志位给取消

    copy_to_kernel函数如下

    有几个细节,或者说是有点坑的地方

    1. 不要忽略了mappages函数失败的情况,这个函数失败,说明walk失败,进一步说明是kalloc失败,这本质上就是没有空闲页面了。这时候也不应该用panic报错,而是返回一个特殊值,表示内存不够用了,并且将已经记录的地址映射删除。如果不处理这种情况,会在sbrkmuch这个测试点过不去。
    2. 为什么要用PGROUNDUP,可以不用吗?或者可以用PGROUNDDOWN
      1. 首先,如果每次给的地址都是页面大小的倍数,那用不用都可以
      2. 之所以使用向上取整,而不是向下取整,个人认为是因为oldsz在的那个页面本来就存在于内核页表中,不需要复制,所以就从向上取整的那个开始
    uint64 copy_to_kernal(pagetable_t user, pagetable_t kernel, uint64 oldsize, uint64 newsize) {
        uint64 va, pa;
        pte_t *pte;
        uint flags;
        for (va = PGROUNDUP(oldsize); va < newsize; va += PGSIZE) {
            pte = walk(user, va, 0);
            pa = PTE2PA(*pte);
            flags = PTE_FLAGS(*pte);
            flags &= ~PTE_U;
            if (mappages(kernel, va, PGSIZE, pa, flags) != 0) {
                uvmunmap(kernel, PGROUNDUP(oldsize), (va - PGROUNDUP(oldsize)) / PGSIZE, 0);
                return -1;
            }
        }
        return newsize;
    }
    

    dealloc_kernal函数如下,基本照抄uvmdealloc函数,只需要将uvmunmap最后的dofree参数改成0就行了

    这个函数的本质就是将用户进程的虚拟地址给free掉了,没有影响内核本身的那些外设和代码数据

    uint64 dealloc_kernal(pagetable_t kernel, uint64 oldsz, uint64 newsz) {
        if (newsz >= oldsz)
            return oldsz;
    
        if (PGROUNDUP(newsz) < PGROUNDUP(oldsz)) {
            int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
            uvmunmap(kernel, PGROUNDUP(newsz), npages, 0);
        }
    
        return newsz;
    }
    
  2. fork,在父进程拷贝内存到子进程之后,调用copy_to_kernel函数

    注意,也要判断是否失败

        // Copy user memory from parent to child. // 将child的用户态页表复制到child的内核页表
        if (uvmcopy(p->pagetable, np->pagetable, p->sz) < 0 || copy_to_kernal(np->pagetable, np->kernel_pgtbl, 0, p->sz) < 0) {
            freeproc(np);
            release(&np->lock);
            return -1;
        }
    

    exec,找个合适的位置(进程的页表被初始化完之后就行),先释放再拷贝

        dealloc_kernal(p->kernel_pgtbl, oldsz, 0);
        copy_to_kernal(p->pagetable, p->kernel_pgtbl, 0, sz);
    

    sbrk应该是在sys_sbrk中调用的growproc函数,分别在分配和释放的情况下调用函数。其中在分配的时候,如果我们的copy函数失败了,还需要将进程的用户态页表的映射给抹去再返回-1

        if (n > 0) {
            if ((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
                return -1;
            }
            // 内核页表
            if (copy_to_kernal(p->pagetable, p->kernel_pgtbl, oldsz, oldsz + n) < 0) {
                uvmdealloc(p->pagetable, sz, oldsz);
                return -1;
            }
        } else if (n < 0) {
            sz = uvmdealloc(p->pagetable, sz, sz + n);
            // 内核页表
            dealloc_kernal(p->kernel_pgtbl, oldsz, oldsz + n);
        }
    

    userinit,在uvminit之后调用拷贝函数

    copy_to_kernal(p->pagetable, p->kernel_pgtbl, 0, PGSIZE);
    
  3. PLIC的限制

    1. CLINT变成只有最初的内核页表才分配,后面申请的内核页表都不用,这样每个进程的内核页表就不会在PLIC下面还有虚拟地址了。至于这这个CLINT为什么在只需要在最初的内核页表需要,现在还不太清楚,好像后面会讲的,就当个黑盒子使用了

      具体实现就是将kvmmap(kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);从task2中定义的init_kernel_pgtbl中移到kvminit

    2. 控制用户进程的地址空间,不要超过了PLIC,我觉得这里需要在两个地方进行控制,第一个是exec函数,即进程初始的虚拟地址空间大小,第二个是sbrk函数,即进程在运行的过程中动态申请内存空间,也不能超过PLIC,这个就体现在sbrk调用的growproc函数了

      1. exec中,是在一个for循环里不断通过uvmalloc给这个进程的用户页表建议映射的,因此在这个函数后面加上一个判断即可

                if ((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
                    goto bad;
                if (sz1 >= PLIC) {
                    goto bad;
                }
        
      2. growproc中,先判断一下当前的大小加上n是否超过了PLIC

            if (PGROUNDUP(sz + n) >= PLIC) {
                return -1;
            }
        

    至此,硬核的内容结束了

    但是如果想拿到满分,还需要在项目根目录下创建两个txt文件,一个叫time.txt,一个叫answers-pgtbl,一个用来记录完成lab的总耗时,一个用来回答问题,可以直接乱填

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

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

相关文章

Redisson实现分布式锁示例

一、引入依赖 <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.0</version></dependency>二、配置类 import org.redisson.Redisson; import org.redisson.api.RedissonClient;…

无涯教程-Perl - wantarray函数

描述 如果当前正在执行的函数的context正在寻找列表值,则此函数返回true。在标量context中返回false。 语法 以下是此函数的简单语法- wantarray返回值 如果没有context,则此函数返回undef&#xff1b;如果lvalue需要标量,则该函数返回0。 例 以下是显示其基本用法的示例…

调整mysql 最大传输数据 max_allowed_packet=500M

查看 -- show VARIABLES like %max_allowed_packet%; -- set global max_allowed_packet 1024*1024*64;-- show variables like %timeout%; -- show global status like com_kill; show global variables like max_allowed_packet; -- set global max_allowed_packet1024*102…

机器学习|DBSCAN 算法的数学原理及代码解析

机器学习&#xff5c;DBSCAN 算法的数学原理及代码解析 DBSCAN&#xff08;Density-Based Spatial Clustering of Applications with Noise&#xff09;是一种基于密度的聚类算法&#xff0c;它能够有效地发现任意形状的聚类簇&#xff0c;并且可以识别出噪声点。在本文中&…

NineData通过AWS FTR认证,打造安全可靠的数据管理平台

近日&#xff0c;NineData 作为新一代的云原生智能数据管理平台&#xff0c;成功通过了 AWS&#xff08;Amazon Web Service&#xff09;的 FTR 认证。NineData 在 FTR 认证过程中表现出色&#xff0c;成功通过了各项严格的测试和评估&#xff0c;在数据安全管理、技术应用、流…

element时间选择器el-date-picter使用disabledDate指定禁用的日期

需要的效果 <el-date-pickerclass"selectstyle"v-model"year"value-format"yyyy"type"year":picker-options"disabledCli"placeholder"选择年"> </el-date-picker>data() {return {disabledCli: {/…

PostgreSQL中的密码验证方法

假设您想在客户端/服务器协议中实现密码身份验证方法。 您将如何做到这一点以及可能出现的问题是什么&#xff1f; 以下是 PostgreSQL 中如何完成此操作的故事。 password 一开始&#xff0c;PostgreSQL 只有 pg_hba.conf 中现在称为“password”的方法。 这是你能想象到的最…

数字化施工:解决传统施工难题,提高施工效率和质量的行业革命

建筑行业是我国国民经济的重要组成部分&#xff0c;也是支柱性产业之一。然而&#xff0c;建筑业同时也是一个安全事故多发的高风险行业。如何加强施工现场的安全管理&#xff0c;降低事故发生的频率&#xff0c;避免各种违规操作和不文明施工&#xff0c;提高建筑工程的质量&a…

web即时通讯系统与APP即时通讯系统有什么区别?

随着互联网的不断发展&#xff0c;即时通讯技术也在不断地完善和发展&#xff0c;其中Web即时通讯系统和APP即时通讯系统成为了人们广泛使用的两种通讯方式。那么&#xff0c;这两者之间究竟有什么区别呢&#xff1f;在本文中&#xff0c;我们将为您详细介绍这两种通讯方式的区…

“RFID与光伏板的完美融合:探索能源科技的新时代!“

随着科技的不断发展&#xff0c;人类创造出了许多令人惊叹的发明。其中&#xff0c;RFID&#xff08;Radio Frequency Identification&#xff09;技术的应用在各个领域日益广泛。最近的研究表明&#xff0c;将RFID技术应用于光伏板领域&#xff0c;不仅可以提高光伏板的效率&a…

BY133 整流二极管 1300V 1A DO-41

BY133是什么类型的二极管&#xff1f;BY133厂家&#xff0c;哪家厂家在生产&#xff1f;BY133厂家哪家好&#xff1f;二极管BY133参数怎么看&#xff1f;BY133二极管报价&#xff0c;价格多少&#xff1f;二极管BY133哪家供应商有现货……对于一枚电子元器件而言&#xff0c;其…

Android2:构建交互式应用

一。创建项目 项目名Beer Adviser 二。更新布局 activity_main.xml <?xml version"1.0" encoding"utf-8"?><LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"…

低代码系列——初步认识低代码

低代码系列目录 一、初步认识低代码 二、低代码是什么 三、低代码平台的概念和分类 01.无代码开发平台 02.低代码应用平台(LCAP) 03.多重体验开发平台(MXDP) 04.智能业务流程管理套件(iBPMS) 四、低代码的能力指标 五、低代码平台jnpf 表单 报表 流程 权限 一、初步认识低代码 …

正规的股票杠杆公司_杠杆公司排名(2023年版的)

本文将介绍一些正规的股票杠杆公司&#xff0c;并重点介绍配先查网站的特点&#xff0c;该网站是一家专业查询实盘杠杆平台的网站&#xff0c;提供相关信息和参考。 杠杆公司排名&#xff08;2023年版的&#xff09;&#xff1a;广盛网、一鼎盈、尚红网、盛多网、红腾网、富灯…

神卓互联内网穿透搭建云表系统

云表是什么&#xff1f; 云表系统是一种无代码开发平台&#xff0c;它具备完善的功能和各种业务级应用管理模板&#xff0c;可以帮助中小企业快速开发和定制自己的系统。通过云表系统&#xff0c;用户可以使用表格编程的开发方式&#xff0c;就像使用Excel一样简单&#xff0c…

BT利器之wazuh

目录 一、什么是wazuh 二、wazuh的安装 1.仓库安装 2.虚拟机OVA安装 3.其他安装方式 三、浅析wazuh的规则、解码器等告警原理以及主动响应 1.主动响应(active-response) 2.告警信息(alerts) 3.规则以及解码器(rules and decoders) 3.1.规则 3.2.解码器 4.linux后门r…

大疆飞卡30运载无人机技术分享

大疆飞卡30是大疆公司面向运输领域推出的一款专业运载无人机。它采用了优秀的设计,装备了多种先进传感器,以解决运输中的难题。以下我们来了解一下其主要特点: 【应用领域】 飞卡30适用于山地救灾、农业化肥施用、工程材料运送等交通不便的山区应用,也适用于海岛联通等运输链…

WinSW使用说明

使用说明 前言下载配置介绍示例jar包启动示例 安装服务 前言 由于使用windows自动的自启方法&#xff0c;不管是将程序启动服务放到开机自启文件夹中&#xff0c;还是创建任务计划程序&#xff0c;都没有很好的实现程序的开机自启效果&#xff0c;而WinSW很好的解决了这个问题…

【数据结构OJ题】复制带随机指针的链表

原题链接&#xff1a;https://leetcode.cn/problems/copy-list-with-random-pointer/description/ 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 此题可以分三步进行&#xff1a; 1. 拷贝链表的每一个结点&#xff0c;拷贝的结点先链接到被拷贝结点…

气象环境监测设备介绍

气象环境监测设备&#xff0c;是用于测量和监测气象参数的设备&#xff0c;可以根据不同功能、环境要求添加不同的传感器设备&#xff0c;主要用于气象领域、环保领域、农业领域等。 接下来&#xff0c;就让我们来介绍一下常见的气象监测设备都有哪些吧。 一、风速/风向监测设…