内存管理之内存寻址(笔记)

news2024/11/26 22:19:03

计算机体系结构中的核心问题之一就是如何有效的进行内存寻址。因为所有运算的前提都是要先从内存中取地数据。所以内存寻址技术在一定程度上代表了计算机技术。

1. 图灵机和冯诺依曼体系

图灵机:图灵机是一种通用自动的机器模型,通过二段无线延申的纸带作为存储装置,输入输出和状态转移函数是机器的三要素,这三要素组合变形可成为一切机器的原型,可解决一切图灵机能解决的问题。
图灵机样图(图片来自视频)
冯诺依曼体系结构:图灵机的实现,基于图灵机的数据连续存储和选择读取思想,是目前我们使用的几乎所有机器运行背后的灵魂。
在这里插入图片描述

2. 内存寻址概述

以IntelX86结构为例,因为这是我们最为熟悉的结构之一。

2.1 x86内存寻址的不同时期

  • 石器时期:8位
  • 青铜时期:16位
  • 白银时期:24位
  • 黄金时期:32和64位

2.2 石器时期-8位寻址

在微处理器的是历史上,第一款微处理芯片是4004,由Intel推出的4位机器,之后又推出了一款8位微处理芯片8008。
这个时期没有段的概念,访问内存需要通过绝对地址。程序中的地址必须要进行硬编码,给出具体的物理地址,而且难以重定位。

2.3 青铜时期-16位寻址(段)

8086处理器的时代,引入了段的概念。8086的目标是寻址空间达到了1M,但数据总线只有16位,因此需要分为数个64k的段来进行管理。
段描述了一块有限的内存区域,区域的起始位置存在专门的寄存器(段寄存器)中。
在这里插入图片描述
把16位的段地址,左移4位后再与16位的偏移量相加,获得一个20位的内存地址。
实模式:从16位内存地址到20位实际地址的转换(映射)。

2.4 白银时期-24位寻址(保护模式)

80286的的地址总线增加到了24位,引入了一个全新的理念(保护模式)
保护模式:访问内存不能直接从段寄存器中获得段的起始地址,而是需要进行额外的转换和检查。
保护模式有很多沿用至今的机制: 内存保护, 分页系统, 虚拟内存等. 大部分现今基于x86的操作系统都是在保护模式下运行的.
为了让文章不显的臃肿, 保护模式的相关概念就不过多介绍了, 感兴趣的可以自行搜索, 这里贴几个相关链接:

  • 保护模式汇编系列: https://www.0xffffff.org/2013/10/22/21-x86-asm-1/
  • 《i386体系结构》上:http://kerneltravel.net/blog/2020/i386_1/

2.5 黄金时期-32/64位寻址

以32位CPU80386为例。
Intel选择在段寄存器的基础上构置保护模式,保留段寄存器依然是16位。在保护模式下,段范围不受限于64K,可以达到4GB。
把386以后的处理器称为X86, 这个时候, 保护模式才算是真正的体现出了强大的作用.

3. 分段机制和分页机制

分段和分页这个计算机科班出身的应该都在操作系统课程上学过相关的理论知识, 这里就不多bb了. 如果忘了也没关系, 贴一个链接, 第二章<linux运行的硬件基础>介绍的非常详细, 简单复习一下应该能回忆起主要内容:

  • http://www.kerneltravel.net/book/

简单来说, i386之后的设备, 有三种不同的地址做区分

  • 虚地址(逻辑地址): 机器语言指令用这种地址指定一个操作数的地址或一条指令的地址. 通过分段结构, 将程序分成若干段, 每个虚地址都由一个段和偏移量组成
  • 线性地址: 在32位机器上, 线性地址是一个32位的无符号整数, 可以表达4GB的地址, 通常用16进制表示线性地址0x00000000~0xffffffff.
  • 实地址(物理地址): 内存单元的实际地址, 用于芯片内存单元寻址. 32位机器的物理地址由32位无符号整数表示.

为了更直观的了解分段机制和分页机制, 我们从一个简单的"Hello World"程序说起

#include<stdio.h>
int main(){
    printf("Hello World!\n");
    return 0;
}

通过编译, 汇编, 链接, 装在和执行, 最后反汇编

 gcc -S helloworld.c -o helloworld.s // 编译: 编译成汇编文件
 gcc -c helloworld.s -o helloworld.o // 汇编: 汇编成二进制文件
 gcc helloworld.c -o helloworld.out // 链接: 将调用的库进行链接, 输出可执行文件
 ./helloworld.out  // 装载到内存并执行
 objdump -d helloworld.out // 反汇编

有三个问题:

  • 链接以后形成的地址是虚地址还是实地址? 虚地址
  • 装入程序把可执行代码装入到虚拟内存还是物理内存? 虚拟内存
  • cpu访问的是虚地址还是物理地址? 虚地址

下面和下下面将在理论和实践中进行回答.
编译之后形成的虚地址,就是cpu要访问的地址
cpu把虚地址送入MMU(内存管理单元), MMU把虚地址转成物理地址送给存储器

MMU分为两个阶段:

  1. 通过分段机制, 虚拟地址转换为线性地址
  2. 通过分页机制, 线性地址转为物理地址
    在这里插入图片描述

4. Linux中的虚拟地址转化为物理地址

4.1 理论部分

Linux主要采用分页机制来实现虚拟存储管理. Linux分段机制使所有的进程都使用相同的段寄存器, 所有的进程使用同样的线性地址空间
过程可以用一张图来概述: 这张图中很清晰的描述了虚地址是如何转为实地址的, 先通过分段机制转为线性地址, 再通过分页机制转为物理地址.(补充: 在Linux中,段的基地址都为0, 所有程序共享同样的线性空间, 虚地址和线性地址在数值上就相同了)
在这里插入图片描述

在3.10.0版本的内核中, centos7采用了4级分页模式, 在/arch/x86/include/asm/pgtable_types.h文件中可以看到, 共pte,pmd,pud和pgd四部分组成.
分别为

  • pgd(page global directory): 总目录
  • pud(page upper directory): 三级页面
  • pmd(page middle directory): 中间目录
  • pte(page table entry): 页表
    在这里插入图片描述
    另外注意: 由于64位处理器硬件的限制, 它的地址线只有48条, 因此线性地址实际使用的地址只有48位
    在这里插入图片描述
    页面的大小是4k, 每一个页表项的大小是8bit, 整个页表可以映射的空间是256TB(已经很大了)
    另外(Linux4.15内核版本之后, 新增了一个p4d页目录在pgd和pud之间. 这是因为Intel芯片中的mmu提供了5级页表的映射)

4.2 代码实践

寻页机制的代码实践, 需要用到内核提供的一些函数, 因此需要通过编写内核模块的方式实现.
实现思路就是模拟MMU的寻页过程:

  1. 首先在内核中申请一个页面, 利用__get_free_page()函数.
  2. 利用内核提供的函数, 一级级的查询各级页目录
  3. 各级目录组合找到对应的物理地址

注意!!! 新手在服务器或者物理机上直接操作的话一定一定一定要谨慎!!!避免给内核写崩之后重启丢失数据(强烈推荐在虚拟机上搞, 我写崩了好几次内核了, 都需要重启系统, 在虚拟机上试崩了好几次了, 不过有部分原因是我虚拟机内存开小了然后内存越界出错了)

#include<linux/init.h>
#include<linux/module.h>
#include<linux/mm.h> // 内存映射
#include<linux/mm_types.h>
#include<linux/sched.h>
// #include<linux/export.h>
#include<asm/pgtable.h> // 多级页表项

/*
    在内核中先申请一个页面,
    利用内核提供的函数, 
    利用寻页步骤一步步查询各级页目录,
    最终找到所对应的物理地址.

    等价于手动模拟MMU单元的寻页过程
*/

static unsigned long cr0, cr3;
static unsigned long vaddr = 0;

/*  get_pgtable_macro():
    打印页机制中的一些重要参数, 例如:
    CR3寄存器的值, 通过read_cr3_pa函数获取
*/
static void get_pgtable_macro(void){
    cr0 = read_cr0();
    // cr3 = read_cr3_pa();
    cr3 = read_cr3();
    
    //  _SHIFT的宏是指示线性地址中 相应字段所能映射区域大小的对数
    //  PAGE_SHIFT指page offset字段所能映射区域大小的对数(映射的是一个页面的大小)
    //      一个页面大小是4k(1<<12)
    printk("cr0 = 0x%lx, cr3 = 0x%lx\n", cr0, cr3);
    printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
    // printk("P4D_SHIFT = %d\n", P4D_SHIFT);
    printk("PUD_SHIFT = %d\n", PUD_SHIFT);
    printk("PMD_SHIFT = %d\n", PMD_SHIFT);
    printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);

    //PTRS_PER_x 这些宏是用来指示相应页目录表中项的个数
    printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
    // printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D);
    printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
    printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
    printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);

    // page_mask 页内偏移掩码, 屏蔽page offset字段
    // 为了方便寻页时进行位运算
    printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}
static unsigned long vaddr2paddr(unsigned long vaddr){
    pgd_t *pgd;
    // p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    unsigned long paddr = 0;
    unsigned long page_addr = 0;
    unsigned long page_offset = 0;
    
    pgd = pgd_offset(current->mm, vaddr);
    printk("pdg_val = 0x%lx, pgd_index = %lu\n", pgd_val(*pgd), pgd_index(vaddr));
    if (pgd_none(*pgd)){
        printk("not mapped in pgd\n");
        return -1;
    }

    // p4d = p4d_offset(pgd, vaddr);
    // printk("p4d_val = 0x%lx, p4d_index = %lu\n", p4d_val(*p4d), p4d_index(vaddr));
    // if (p4d_none(*p4d)){
    //     printk("not mapped in p4d\n");
    //     return -1;
    // }

    // pud = pud_offset(p4d, vaddr);
    pud = pud_offset(pgd, vaddr);
    printk("pud_val = 0x%lx, pud_index = %lu\n", pud_val(*pud), pud_index(vaddr));
    if (pud_none(*pud)){
        printk("not mapped in pud\n");
        return -1;
    }

    pmd = pmd_offset(pud, vaddr);
    printk("pmd_val = 0x%lx, pmd_index = %lu\n", pmd_val(*pmd), pmd_index(vaddr));
    if (pmd_none(*pmd)){
        printk("not mapped in pmd\n");
        return -1;
    }

    pte = pte_offset_kernel(pmd, vaddr);
    printk("pte_val = 0x%lx, pte_index = %lu\n", pte_val(*pte), pte_index(vaddr));
    if (pte_none(*pte)){
        printk("not mapped in pte\n");
        return -1;
    }

    page_addr = native_pte_val(*pte) & PAGE_MASK;
    page_offset = vaddr & ~PAGE_MASK;
    paddr = page_addr | page_offset;
    printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
    printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);
    return paddr;
}

static int __init v2p_init(void){
    unsigned long vaddr = 0;
    printk("vaddr to paddr module is running...\n");
    get_pgtable_macro();
    printk("\n");
    // vaddr = __get_free_page(GFP_KERNEL);
    vaddr = __get_free_page(___GFP_HIGHMEM);
    if (vaddr == 0){
        printk("__get_free_page failed..\n");
        return 0;
    }
    sprintf((char*)vaddr, "hello world from kernel\n");
    printk("get_page_vaddr = 0x%lx\n", vaddr);
    vaddr2paddr(vaddr);
    return 0;
}
static void __exit v2p_exit(void){
    printk("vaddr to paddr module is leaving..\n");
    free_page(vaddr);
}

module_init(v2p_init);
module_exit(v2p_exit);

MODULE_LICENSE("GPL");

Makefile文件内容

obj-m:= paging_lowmem.o
PWD:= $(shell pwd)
LINUX_KERNEL_PATH := /usr/src/kernels/$(shell uname -r)
all:
	make -C $(LINUX_KERNEL_PATH) M=$(PWD) modules
clean:
	@rm -rf *.o *.mod.c *.mod.o *.ko *.order *.symvers .*.cmd .tmp_versions

然后就是内核编程的老操作了:

  • 通过make命令进行编译,生成paging_lowmem.ko文件
  • 之后通过insmod paging_lowmem.ko命令装载
  • 通过lsmod命令查看已装在的模块列表,通过dmesg命令查看printk输出的日志
  • 最后通过remod paging_lowmem命令卸载

每个人的输出结果可能不同, 这里给出我的输出结果提供一下参考, 顺便方便下面的细节介绍给出示例:

[17024.193831] vaddr to paddr module is running...
[17024.193879] cr0 = 0x80050033, cr3 = 0x3a1a4000
[17024.193880] PGDIR_SHIFT = 39
[17024.193881] PUD_SHIFT = 30
[17024.193882] PMD_SHIFT = 21
[17024.193883] PAGE_SHIFT = 12
[17024.193884] PTRS_PER_PGD = 512
[17024.193885] PTRS_PER_PUD = 512
[17024.193886] PTRS_PER_PMD = 512
[17024.193887] PTRS_PER_PTE = 512
[17024.193887] PAGE_MASK = 0xfffffffffffff000

[17024.193891] get_page_vaddr = 0xffff8caaf497b000
[17024.193893] pdg_val = 0x26aa6067, pgd_index = 281
[17024.193894] pud_val = 0x26aa7067, pud_index = 171
[17024.193895] pmd_val = 0x3498b063, pmd_index = 420
[17024.193896] pte_val = 0x800000003497b063, pte_index = 379
[17024.193897] page_addr = 800000003497b000, page_offset = 0
[17024.193898] vaddr = ffff8caaf497b000, paddr = 800000003497b000
[17038.643975] vaddr to paddr module is leaving..

4.3 细节介绍

很多函数都是第一次使用,在这里简单介绍一下:
注意内核版本!

  • 一些库函数, 内核模块编程的<linux/init.h>, <linux/module.h>和<linux/export.h>, 内存映射的<linux/mm.h>, <linux/mm_types.h>和<asm/pgtable.h>, 最后是用于进程管理的<linux/sched.h>. 这些库的功能上网就能轻松查到, 点进去也能看到源码, 也可以看看源码中的注释. 就不多介绍了.
  • 一些寄存器: read_cr0()来源于嵌入汇编指令的native_read_cr0(void)函数,该函数被定义在/usr/src/kernels/{version}/arch/x86/include/asm/special_insns.h中,细节实现如下所示:
    extern unsigned long __force_order;
    static inline unsigned long native_read_cr0(void){
    	unsigned long val;
    	asm volatile("mov %%cr0,%0\n\t" : "=r" (val), "=m" (__force_order));
    	return val;
    }
    
    cr0和cr3属于控制寄存器: cr0用来描述处理器的操作模式和状态控制; cr3用来描述当前进程的页目录表物理内存基地址.
    内核在创建一个进程的时候就会给它分配一个页全局目录, 在进程描述符task_struct结构体中有一个指向mm_struct结构的指针mm, 这个mm_struct结构就是用来描述进程的虚拟地址空间的. mm_struct结构中有一个变量pgd就是用来保存该进程的页全局目录(物理)地址的.
    在进程切换的时候, 操作系统通过访问task_struct结构, 再访问mm_struct结构,最终找到pgd字段,取得新进程的页全局目录的地址, 填充到cr3寄存器中, 就完成了页表的切换.
  • 一些宏:
    • XX_SHIFT等宏是指线性地址中相应字段能映射区域大小的对数, (例: 其中PAGE_SHIFT指page offset字段所映射区域大小的对数. 一个页面大小是4k, 就是1<<12, 因此PAGE_SHIFT的值为12), 这里隔9位是一个页面, 符合我们前面的分页理论.
    • PTRS_PER_XX等宏是指相应页目录表中项的个数, (例PMD中, 21-39共9位, (1<<9)=512, 因此值是512)
    • PAGR_MASK宏是指页内偏移掩码, 用来屏蔽page offset字段的(后12位的页内4K空间)
  • 一些寻址函数:
    • xxx_offset()这些函数是通过上级页表来寻找下级页表, 通过与上级页表和宏进行位运算得到的. 需要留意的是currect->mm这个参数, 其实就是获取当前进程的mm_struct.
    • xxx_index()这些函数描述的是该地址是该页表的第几项
  • 从cr3中获得pgd, 然后获得pud, pmd最终获得pte, 获得这些页表的线性地址之后, 通过位运算获得物理地址了
    1. 先将pte和PAGE_MASK进行&运算, 获得其高48位, 得到了页框的物理地址page_addr
    2. 取出页偏移量: 将PAGE_MASK按位取反(获得低12位) 后与vaddr进行&操作
    3. 将页框物理地址和页内偏移量通过 | 拼接起来, 得到物理地址paddr
  • 输出案例分析
    • 申请到的虚拟地址(线性地址)是get_page_vaddr = 0xffff8caaf497b000
    • 将其转成二进制, 然后将对应的索引字段转成十进制和十六进制查看. 这里由于其每个页目录项有8B,因此需要左偏4位才能得到该索引在物理地址中的偏移量.
      字段二进制十进制十六进制
      vaddr1111 1111 1111 1111 1000 1100 1010 1010 1111 0100 1001 0111 1011 0000 0000 0000/0xffff8caaf497b000
      PGD1000 1100 1281*8B8c0
      PUD010 1010 11171*8B558
      PMD11 0100 100420*8Bd20
      PTE1 0111 1011379*8Bbd8
    • 依次查找了这个虚拟地址的pgd, pud, pmd和pte和对应的页内偏移:
      1. 获取pgd页表项的物理地址: 从cr3中获取基地址0x3a1a4000, 也就是pgd的起始地址然后加上PGD的偏移量8c0组成一个新的物理地址: 0x3a1a48c0, 这个物理地址就是pgd页表中项的物理地址(这个项当然就是pgd页表中的项了, 内容就是下一级页表(pud)的物理地址)
      2. 获取pud页表项的物理地址: 查找pgd物理地址中的内容, 0x0000 0000 26aa 6067, 这个数据就是下级页表(pud)的物理地址.同理加上偏移量0x558组成一个一个新的物理地址0x26aa6558就是pud页表中项的地址了, 其中包含的内容就是下一级页表(pmd)的物理地址
      3. 获取pmd页表项的物理地址: 查找pud页表项中的数据: 0x26aa 7067, 加上pmd的偏移量0xd20组成一个新的物理地址: 0x26aa 7d20, 这个物理地址就是pmd页表项的物理地址了, 其中保存的是下一级pte的物理地址.
      4. 获取pte的的物理地址: 基地址0x3498b063+偏移0xbd8组成的0x3498bbd8就是pte的物理地址了.其中内容就是页的实际物理地址0x800000003497b063. 一页是4k, 所以该页的实际物理地址是0x800000003497b000.
      • 需要留意的是, 物理地址的后三位是无效的, 因为需要与页内偏移做拼接, 这后三位一般是0x067或0x063, 是用来描述相应的页目录项或者页表的属性. 在做地址加减的时候置0即可.
      • 还可以留意到,其实这个实际物理地址和虚拟地址在低位很相似, 尝试了几次, 发现虚拟地址都是物理地址左偏一个0xffff000000000000, 其实也很容易理解, 毕竟前几位不存在麻. 中间的几位就是中间的各级页表的页内偏移.
    • Linux中提供了多种访问物理地址的方法, 包括mmap()物理内存映射, ioremap()+iounmap()物理到虚拟地址的映射, 或者通过dd if=/dev/mem的方式, 都可以通过物理地址进行访问. 感兴趣的可以自行查看.

参考资料

  • 学堂在线-Linux内核分析与应用:https://next.xuetangx.com/course/XIYOU08091001441/14767915
  • 《i386体系结构》上和下:http://kerneltravel.net/blog/2020/i386_1/
  • 深入分析LINUX内核源码:http://kerneltravel.net/book/
  • chatgpt

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

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

相关文章

C++Primer——第一讲

重制CPrimer 目录 一、第一个程序 二、代码 二、题目 前言 我们会从一个C程序开始&#xff0c;这里默认您已经安装了Dev-C或其他的IDE软件。 一、第一个程序 下面这串代码是可以输出“Hello world”的代码。 #include<bits/stdc.h> using namespace std; int main(){…

Arduino+ESP8266 MCU开发板 ----带你开发DHT11温湿度开发项目

目录 PC调试过程如图 手机APP可在各大商场APP中下载 手机APP调试结果/效果如图 ESP8266 MUC介绍 ESP8266 MUC主要特点&#xff1a; 步1&#xff1a;下载Arduino&#xff0c;本次不多做说明&#xff0c;本次使用的arduino软件为老版本的&#xff0c;新版本有关的问题本人…

统计学的假设检验

假设检验的核心其实就是反证法。反证法是数学中的一个概念&#xff0c;就是你要证明一个结论是正确的&#xff0c;那么先假设这个结论是错误的&#xff0c;然后以这个结论是错误的为前提条件进行推理&#xff0c;推理出来的结果与假设条件矛盾&#xff0c;这个时候就说明这个假…

总结881

学习目标&#xff1a; 月目标&#xff1a;5月&#xff08;1800基础部分&#xff0c;背诵15篇短文&#xff09; 周目标&#xff1a;1800高等数学部分并完成错题记录&#xff0c;英语背3篇文章并回诵 每日必复习&#xff08;5分钟&#xff09; 前天错题纠错&#xff0c;线代部…

Solidity拓展:数据类型的转换

1.数据类型隐式转换 (自动) 同一类型之间的转换:由低长度转换为高长度int8-int16-int32int256,但int不能自动转换成uint&#xff0c;因为放不下负数所以直接不让转换,且 int8 不能转换成 uint256 &#xff08;因为 uint256 不能涵盖某些值&#xff0c;例如&#xff0c; -1&…

Android解决xutils数据库kotlin添加List数组问题

Android解决xutils数据库kotlin添加List数组问题 前言&#xff1a; 上一篇我们讲解了xutils中数据库版本升级的使用和问题&#xff0c;这篇博客讲解xutils中数据库添加list数据的问题&#xff0c;这个库真的是很强大&#xff0c;但是数据库的使用真不友好&#xff0c;添加一个…

从零开始手搓一个STM32与机智云的小项目——硬件介绍

文章目录 前言硬件简介选型1.主控2.电源3.电机驱动4.舵机驱动5.USB转TTL6.其他模块 原理图绘制1.STM32最小系统1.电源输入2.晶振选择3.复位电路4.BOOT选择电路5.下载电路 2.电源部分及与PC通信部分3.功能模块的实现1.串口2.定时器输入捕获与输出比较3.硬件SPI4.ADC5.温湿度传感…

学校食堂明厨亮灶 yolov8

学校食堂明厨亮灶可以yolov8网络模型技术&#xff0c;学校食堂明厨亮灶通过对厨师的穿戴情况行为举止等进行监测。YOLOv8 算法的核心特性和改动可以归结为如下&#xff1a;提供了一个全新的 SOTA 模型&#xff0c;包括 P5 640 和 P6 1280 分辨率的目标检测网络和基于 YOLACT 的…

C++环形缓冲区设计与实现:从原理到应用的全方位解析

C环形缓冲区设计与实现&#xff1a;从原理到应用的全方位解析 一、环形缓冲区基础理论解析&#xff08;Basic Theory of Circular Buffer&#xff09;1.1 环形缓冲区的定义与作用&#xff08;Definition and Function of Circular Buffer&#xff09;1.2 环形缓冲区的基本原理&…

SAP-MM-内向外向交货单

1、内向&外向交货单概念 外向交货&#xff08;outbound delivery&#xff09;是用在客户与企业之间的交货单&#xff0c;而内向交货&#xff08;inbound delivery&#xff09;则是用在供应商与企业之间的交货单&#xff1b;换言之&#xff0c;外向交货多用于SD 模块&#…

基于MAX-10 FPGA 超声波测距模块HC_SR04

文章目录 一、介绍超声波测距模块HC_SR04二、模块框图三、模块编写1. 测距信号源2. 距离计算3. 数码管模块4. 顶层模块 四、实验现象总结 一、介绍超声波测距模块HC_SR04 HC-SR04是一种基于超声波的测距模块。该模块向前15度内发送超声波并接收回响&#xff0c;通过发出超声波…

第一章:简单的C程序设计基础

一、C语言词汇 在C语言中使用的词汇分为&#xff1a;关键字、标识符、常量、运算符、分隔符、注释符等。 1.1关键字 1.2标识符 在程序中使用的变量名或函数名等统称为标识符&#xff1b;标识符的命名规则如下&#xff1a; &#xff08;C语言区分大小写&#xff09; 不能是关…

一个简单的基于C/S模型的TCP通信实例

1 TCP协议 1.1 概念 TCP是一种面向连接的、可靠的协议&#xff0c;有点像打电话&#xff0c;双方拿起电话互通身份之后就建立了连接&#xff0c;然后说话就行了&#xff0c;这边说的话那边保证听得到&#xff0c;并且是按说话的顺序听到的&#xff0c;说完话挂机断开连接。也…

2023 华为 Datacom-HCIE 真题题库 08--含解析

单项选择 1.[试题编号&#xff1a;190385] &#xff08;单选题&#xff09;以下关于BGP/MPLSIPVPN路由交互的描述&#xff0c;错误的是哪一项? A、PE与CE之间交互的是IPv4路由信息 B、出口PE可以通过BGP、IGP或静态路由的方式向远端CE发送IPv4路由 C、入口PE将从CE接收到的I…

数组(1)

文章目录 目录1. 一维数组的创建和初始化1.1 一维数组的创建1.2 一维数组的初始化 2. 一维数组的使用3. 一维数组在内存中的存储4. 二维数组的创建和初始化4.1 二维数组的创建4.2 二维数组的初始化 5. 二维数组的使用6. 二维数组在内存中的存储7. 数组越界8. 数组作为函数参数 …

chatgpt赋能python:Python自动填表单:提高工作效率的好帮手

Python 自动填表单&#xff1a;提高工作效率的好帮手 在现代社会中&#xff0c;表单已成为我们日常工作中不可或缺的一部分。填表单虽然看似简单&#xff0c;但是时间一长&#xff0c;不仅会影响工作效率&#xff0c;还会带来心理负担。幸运的是&#xff0c;Python 自动填表单…

Linux-0.11 boot目录bootsect.s详解

Linux-0.11 boot目录bootsect.s详解 模块简介 bootsect.s是磁盘启动的引导程序&#xff0c;其概括起来就是代码的搬运工&#xff0c;将代码搬到合适的位置。下图是对搬运过程的概括&#xff0c;可以有个印象&#xff0c;后面将详细讲解。 bootsect.s主要做了如下的三件事: 搬…

doris---Rollup

Rollup 3.5.1基本概念 通过建表语句创建出来的表称为 Base 表&#xff08;Base Table,基表&#xff09; 在 Base 表之上&#xff0c;我们可以创建任意多个 ROLLUP 表。这些 ROLLUP 的数据是基于 Base 表产生的&#xff0c;并且在物理上是独立存储的。 Rollup表的好处&#xff…

C#调用FreeSpire.PDF获取PDF文档中使用的字体

除了图片之外&#xff0c;电子文件中使用的字体都必须要在本机中安装才能正常查看文字&#xff08;word缺少字体的话会自动使用相似或默认字体&#xff09;&#xff0c;要想知道电子文件中使用的字体&#xff0c;可以将电子文件转换为PDF文件&#xff08;如果是打印成PDF的话&a…

chatgpt赋能python:Python收集数据在SEO中的重要性

Python 收集数据在 SEO 中的重要性 随着互联网的发展&#xff0c;搜索引擎对于用户获取信息的重要性日益增加。SEO&#xff08;搜索引擎优化&#xff09;一直是每个网站必须考虑的问题。Python 收集数据在 SEO 中可以发挥重要的作用&#xff0c;帮助网站提高排名。下面我们来详…