Linux内存管理1——虚拟内存空间

1 篇文章 0 订阅
订阅专栏

目录

1.什么是虚拟内存?

 2. 为什么要使用虚拟地址访问内存

3. 虚拟内存空间

 4.Linux进程虚拟内存空间

4.1 32位机器上进程虚拟内存空间分布

4.2 64位机器上进程虚拟内存空间分布

5. 进程虚拟内存空间的管理

5.1内核如何布局进程虚拟空间

5.2 内核如何布局进程虚拟空间

 5.3内核如何管理虚拟内存区域

 5.4 定义虚拟内存区域的访问权限和行为规范

 5.5 关联内存映射中的映射关系

 5.6 针对虚拟内存区域的相关操作

5.7虚拟内存区域在内核中是如何被组织的

 6.程序编译后的二进制文件如何映射到虚拟内存空间

7.内核虚拟内存空间

7.1 32位体系内核虚拟内存空间布局

7.1.1直接映射区

 7.1.2 ZONE_HIGHMEM高端内存

 7.1.3 vmalloc动态映射区

7.1.4 永久映射区

 7.1.5固定映射区

 7.1.6 临时映射区

 7.2 64位体系结构下 Linux 虚拟内存空间整体布局

 8.到底什么是物理内存地址

 8.1 DRAM 芯片的访问

 8.2 CPU如何读写主存

8.3 CPU 从内存读取数据过程

 8.5 CPU向内存写入数据过程

总结​​​​​​​


本文主要从以下5个问题来进行:

1. 那么到底什么是虚拟内存地址 ?

2. Linux 内核为啥要引入虚拟内存而不直接使用物理内存 ?

3. 虚拟内存空间到底长啥样?

4. 内核如何管理虚拟内存?

5. 什么又是物理内存地址 ?如何访问物理内存?

1.什么是虚拟内存?

        联想一下快递投递的过程,其中快递单上填写的地址就是“虚拟地址”的概念,只是为了把快递准确无误地送到客户手里,这个“虚拟地址”只是起到了一个定位的作用。通过收货地址这个虚拟概念将它和现实世界真实存在的城市,小区,街道的地理位置一一映射起来,这样我们就可以通过这个虚拟概念来找到现实世界中的具体地理位置。收货地址是一个虚拟地址,它是人为定义的,而我们的城市,小区,街道是真实存在的,他们的地理位置就是物理地址。

        现在让我们把视角在切换到计算机的世界,在计算机的世界里内存地址用来定义数据在内存中的存储位置的,内存地址也分为虚拟地址和物理地址。而虚拟地址也是人为设计的一个概念,类比我们现实世界中的收货地址,而物理地址则是数据在物理内存中的真实存储位置,类比现实世界中的城市,街道,小区的真实地理位置。接下来看一下虚拟内存到底长什么样。

         32 位虚拟地址的格式为:页目录项(10位)+ 页表项(10位) + 页内偏移(12位)。共 32 位组成的虚拟内存地址。

 2. 为什么要使用虚拟地址访问内存

        在回答大家的这个疑问之前,让我们先来看下,如果在程序中直接使用物理内存地址会发生什么情况?

        假设现在没有虚拟内存地址,我们在程序中对内存的操作全都都是使用物理内存地址,在这种情况下,程序员就需要精确的知道每一个变量在内存中的具体位置,我们需要手动对物理内存进行布局,明确哪些数据存储在内存的哪些位置,除此之外我们还需要考虑为每个进程究竟要分配多少内存?内存紧张的时候该怎么办?如何避免进程与进程之间的地址冲突?等等一系列复杂且琐碎的细节

        如果我们在单进程系统中进行内存管理可能还好,但是多个进程的时候如何避免内存冲突呢?假如现在有a, b, c三个进程,都需要操作i变量,假设变量 i 保存在 0x354 这个物理地址上。这三个进程运行起来之后,同时操作这个 0x354 物理地址,这样这个变量 i 的值不就混乱了吗? 三个进程就会出现变量的地址冲突。如图所示:

        所以在直接操作物理内存的情况下,我们需要知道每一个变量的位置都被安排在了哪里,而且还要注意和多个进程同时运行的时候,不能共用同一个地址,否则就会造成地址冲突。现实中一个程序会有很多的变量和函数,这样一来我们给它们都需要计算一个合理的位置,还不能与其他进程冲突,这就很复杂了。那么我们该如何解决这个问题呢?程序的局部性原理再一次救了我们

程序的局部性原理:时间局部性和空间局部性。时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某块数据被访问,则不久之后该数据可能再次被访问。空间局部性是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。

        从程序局部性原理的描述中我们可以得出这样一个结论:进程在运行之后,对于内存的访问不会一下子就要访问全部的内存,相反进程对于内存的访问会表现出明显的倾向性,更加倾向于访问最近访问过的数据以及热点数据附近的数据

        根据这个结论我们就清楚了,无论一个进程实际可以占用的内存资源有多大,根据程序局部性原理,在某一段时间内,进程真正需要的物理内存其实是很少的一部分,我们只需要为每个进程分配很少的物理内存就可以保证进程的正常执行运转。虚拟内存的引入正是要解决上述的问题,虚拟内存引入之后,进程的视角就会变得非常开阔,每个进程都拥有自己独立的虚拟地址空间,进程与进程之间的虚拟内存地址空间是相互隔离,互不干扰的。每个进程都认为自己独占所有内存空间

        当 CPU 访问进程的虚拟地址时,经过地址翻译硬件将虚拟地址转换成不同的物理地址,这样不同的进程运行的时候,虽然操作的是同一虚拟地址,但其实背后写入的是不同的物理地址,这样就不会冲突了。

3. 虚拟内存空间

(1)在进程运行之前,这些存放在二进制文件中的机器码需要被加载进内存中,而用于存放这些机器码的虚拟内存空间叫做代码段

(2)进程运行需要操作变量:大量的全局变量和静态变量。指定了初始值的全局变量和静态变量在虚拟内存空间中的存储区域我们叫做数据段没有指定初始值的全局变量和静态变量在虚拟内存空间中的存储区域我们叫做 BSS 段

(3)全局变量和静态变量都是在编译期间就确定的。在进程运行期间,有可能会动态申请内存,堆就是用来存放动态申请的内存的

(4)动态链接库中的代码段,数据段,BSS 段,以及通过 mmap 系统调用映射的共享内存区,在虚拟内存空间的存储区域叫做文件映射与匿名映射区

(5)在程序运行的时候总该要调用各种函数,那么该函数对应的局部变量和参数使用栈来存储

如下图所示:

 4.Linux进程虚拟内存空间

4.1 32位机器上进程虚拟内存空间分布

        在 32 位机器上,指针的寻址范围为 2^32,所能表达的虚拟内存空间为 4 GB。所以在 32 位机器上进程的虚拟内存地址范围为:0x0000 0000 - 0xFFFF FFFF。其中用户态虚拟内存空间为 3 GB,虚拟内存地址范围为:0x0000 0000 - 0xC000 000 。内核态虚拟内存空间为 1 GB,虚拟内存地址范围为:0xC000 000 - 0xFFFF FFFF

         0x0000 0000 到 0x0804 8000 这段虚拟内存地址是一段不可访问的保留区,因为在大多数操作系统中,数值比较小的地址通常被认为不是一个合法的地址,这块小地址是不允许访问的。比如在 C 语言中我们通常会将一些无效的指针设置为 NULL,指向这块不允许访问的地址。紧挨着 BSS 段的上边就是我们经常使用到的堆空间,从图中的红色箭头我们可以知道在堆空间中地址的增长方向是从低地址到高地址增长堆空间的上边是一段待分配区域,用于扩展堆空间的使用

4.2 64位机器上进程虚拟内存空间分布

        我们理所应当的会认为在 64 位机器上,指针的寻址范围为 2^64,所能表达的虚拟内存空间为 16 EB 。虚拟内存地址范围为:0x0000 0000 0000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。事实上在目前的 64 位系统下只使用了 48 位来描述虚拟内存空间,寻址范围为 2^48 ,所能表达的虚拟内存空间为 256TB。其中低 128 T 表示用户态虚拟内存空间,虚拟内存地址范围为:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 。高 128 T 表示内核态虚拟内存空间,虚拟内存地址范围为:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF

         如果一个虚拟内存地址的高 16 位全部为 0 ,那么我们就可以直接判断出这是一个用户空间的虚拟内存地址。在高 128T 的内核态虚拟内存空间:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 范围中,所以虚拟内存地址的高 16 位全部为 1 。也就是说内核态的虚拟内存地址的高 16 位全部为 1 ,如果一个试图访问内核的虚拟地址的高 16 位不全为 1 ,则可以快速判断这个访问是非法的

        在代码段跟数据段的中间还有一段不可以读写的保护段,它的作用是防止程序在读写数据段的时候越界访问到代码段,这个保护段可以让越界访问行为直接崩溃,防止它继续往下运行。

5. 进程虚拟内存空间的管理

        在进程描述符 task_struct 结构中,有一个专门描述进程虚拟地址空间的内存描述符 mm_struct 结构,这个结构体中包含了前边几个小节中介绍的进程虚拟内存空间的全部信息。当我们调用 fork() 函数创建进程的时候,表示进程地址空间的 mm_struct 结构会随着进程描述符 task_struct 的创建而创建。do_fork()-->copy_process()-->copy_mm()函数完成了子进程虚拟内存空间 mm_struct 结构的的创建以及初始化

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
    // 子进程虚拟内存空间,父进程虚拟内存空间
    struct mm_struct *mm, *oldmm;
    int retval;

    tsk->min_flt = tsk->maj_flt = 0;
    tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
    tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif

    tsk->mm = NULL;
    tsk->active_mm = NULL;
     // 获取父进程虚拟内存空间
    oldmm = current->mm;
    if (!oldmm)
        return 0;

    /* initialize the new vmacache entries */
    vmacache_flush(tsk);
    // 通过 vfork 或者 clone 系统调用创建出的子进程(线程)和父进程共享虚拟内存空间
    if (clone_flags & CLONE_VM) {
        atomic_inc(&oldmm->mm_users);// 增加父进程虚拟地址空间的引用计数
        mm = oldmm; // 直接将父进程的虚拟内存空间赋值给子进程(线程)线程共享其所属进程的虚拟内存空间
        goto good_mm;
    }

    retval = -ENOMEM;
    mm = dup_mm(tsk);// 如果是 fork 系统调用创建出的子进程,则将父进程的虚拟内存空间以及相关页表拷贝到子进程中的 mm_struct 结构中。
    if (!mm)
        goto fail_nomem;

good_mm:
    tsk->mm = mm;
    tsk->active_mm = mm;// 将拷贝出来的父进程虚拟内存空间 mm_struct 赋值给子进程
    return 0;

fail_nomem:
    return retval;
}

        内核线程和用户态线程的区别就是内核线程没有相关的内存描述符 mm_struct ,内核线程对应的 task_struct 结构中的 mm 域指向 Null,所以内核线程之间调度是不涉及地址空间切换的。 当一个内核线程被调度时,它会发现自己的虚拟地址空间为 Null,虽然它不会访问用户态的内存,但是它会访问内核内存,聪明的内核会将调度之前的上一个用户态进程的虚拟内存空间 mm_struct 直接赋值给内核线程,因为内核线程不会访问用户空间的内存,它仅仅只会访问内核空间的内存,所以直接复用上一个用户态进程的虚拟地址空间就可以避免为内核线程分配 mm_struct 和相关页表的开销,以及避免内核线程之间调度时地址空间的切换开销。

5.1内核如何布局进程虚拟空间

        进程的内存描述符 mm_struct 结构体中的 task_size 变量 定义了用户态地址空间与内核态地址空间之间的分界线

struct mm_struct {
    unsigned long task_size;        /* size of task vm space */
}

5.2 内核如何布局进程虚拟空间

        前边我们提到,内核中采用了一个叫做内存描述符的 mm_struct 结构体来表示进程虚拟内存空间的全部信息。在本小节中笔者就带大家到 mm_struct 结构体内部去寻找下相关的线索。

        mmap_base 定义内存映射区的起始地址。进程运行时所依赖的动态链接库中的代码段,数据段,BSS 段以及我们调用 mmap 映射出来的一段虚拟内存空间就保存在这个区域。 start_stack 是栈的起始位置在 RBP 寄存器中存储,栈的结束位置也就是栈顶指针 stack pointer 在 RSP 寄存器中存储。在栈中内存地址的增长方向也是由高地址向低地址增长。mm_struct 结构体中的 total_vm 表示在进程虚拟内存空间中总共与物理内存映射的页的总数。当内存吃紧的时候,有些页可以换出到硬盘上,而有些页因为比较重要,不能换出。locked_vm 就是被锁定不能换出的内存页总数,pinned_vm 表示既不能换出,也不能移动的内存页总数。data_vm 表示数据段中映射的内存页数目,exec_vm 是代码段中存放可执行文件的内存页数目,stack_vm 是栈中所映射的内存页数目,这些变量均是表示进程虚拟内存空间中的虚拟内存使用情况。

 5.3内核如何管理虚拟内存区域

        vm_area_struct描述了虚拟内存区域 VMA(virtual memory area),例如代码区、数据段、BSS段、堆等。每个 vm_area_struct 结构对应于虚拟内存空间中的唯一虚拟内存区域 VMA,vm_start 指向了这块虚拟内存区域的起始地址(最低地址),vm_start 本身包含在这块虚拟内存区域内。vm_end 指向了这块虚拟内存区域的结束地址(最高地址),而 vm_end 本身包含在这块虚拟内存区域之外,所以 vm_area_struct 结构描述的是 [vm_start,vm_end) 这样一段左闭右开的虚拟内存区域

 5.4 定义虚拟内存区域的访问权限和行为规范

        vm_page_prot 和 vm_flags 都是用来标记 vm_area_struct 结构表示的这块虚拟内存区域的访问权限和行为规范虚拟内存区域 VMA 由许多的虚拟页 (page) 组成,每个虚拟页需要经过页表的转换才能找到对应的物理页面。页表中关于内存页的访问权限就是由 vm_page_prot 决定的。vm_flags 则偏向于定于整个虚拟内存区域的访问权限以及行为规范。描述的是虚拟内存区域中的整体信息,而不是虚拟内存区域中具体的某个独立页面。它是一个抽象的概念。可以通过 vma->vm_page_prot = vm_get_page_prot(vma->vm_flags) 实现到具体页面访问权限 vm_page_prot 的转换。

        比如代码段这块内存区域的权限是可读,可执行,但是不可写。数据段具有可读可写的权限但是不可执行。堆则具有可读可写,可执行的权限(Java 中的字节码存储在堆中,所以需要可执行权限),栈一般是可读可写的权限,一般很少有可执行权限。而文件映射与匿名映射区存放了共享链接库,所以也需要可执行的权限。

 5.5 关联内存映射中的映射关系

        虚拟内存区域可以映射到物理内存上,也可以映射到文件中,映射到物理内存上我们称之为匿名映射,映射到文件中我们称之为文件映射。当我们调用 malloc 申请内存时,如果申请的是小块内存(低于 128K)则会使用 do_brk() 系统调用通过调整堆中的 brk 指针大小来增加或者回收堆内存。如果申请的是比较大块的内存(超过 128K)时,则会调用 mmap 在上图虚拟内存空间中的文件映射与匿名映射区创建出一块 VMA 内存区域(这里是匿名映射)。这块匿名映射区域就用 struct anon_vma 结构表示。

 5.6 针对虚拟内存区域的相关操作

        struct vm_area_struct 结构中还有一个 vm_ops 用来指向针对虚拟内存区域 VMA 的相关操作的函数指针

struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
    void (*map_pages)(struct vm_area_struct *vma, struct vm_fault *vmf);

    /* notification that a previously read-only page is about to become
     * writable, if an error is returned it will cause a SIGBUS */
    int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

        当指定的虚拟内存区域被加入到进程虚拟内存空间中时,open 函数会被调用;当虚拟内存区域 VMA 从进程虚拟内存空间中被删除时,close 函数会被调用;当进程访问虚拟内存时,访问的页面不在物理内存中,可能是未分配物理内存也可能是被置换到磁盘中,这时就会产生缺页异常,fault 函数就会被调用;当一个只读的页面将要变为可写时,page_mkwrite 函数会被调用;

5.7虚拟内存区域在内核中是如何被组织的

struct vm_area_struct {
       unsigned long vm_start;        /* Our start address within vm_mm. */
       unsigned long vm_end;        /* The first byte after our end address
 within vm_mm. */
      /* linked list of VM areas per task, sorted by address */
      struct vm_area_struct *vm_next, *vm_prev;
      struct rb_node vm_rb;
      struct mm_struct *vm_mm;    /* The address space we belong to. */
      struct list_head anon_vma_chain; /* Serialized by mmap_sem &
page_table_lock */
      struct anon_vma *anon_vma;    /* Serialized by page_table_lock */
      pgprot_t vm_page_prot;        /* Access permissions of this VMA. */
      unsigned long vm_flags;        /* Flags, see mm.h. */
      unsigned long vm_pgoff;        /* Offset (within vm_file) in PAGE_SIZE
 units, *not* PAGE_CACHE_SIZE */
      struct file * vm_file;        /* File we map to (can be NULL). */
      void * vm_private_data;        /* was vm_pte (shared mem) */
      /* Function pointers to deal with this struct. */
      const struct vm_operations_struct *vm_ops;
   }

        在内核中其实是通过一个 struct vm_area_struct 结构的双向链表将虚拟内存空间中的这些虚拟内存区域 VMA 串联起来的。 vm_area_struct 结构中的 vm_next ,vm_prev 指针分别指向 VMA 节点所在双向链表中的后继节点和前驱节点,内核中的这个 VMA 双向链表是有顺序的,所有 VMA 节点按照低地址到高地址的增长方向排序双向链表中的最后一个 VMA 节点的 vm_next 指针指向 NULL,双向链表的头指针存储在内存描述符 struct mm_struct 结构中的 mmap 中,正是这个 mmap 串联起了整个虚拟内存空间中的虚拟内存区域

struct mm_struct {
      struct vm_area_struct *mmap;        /* list of VMAs */
}

        每个虚拟内存区域 VMA 中又通过 struct vm_area_struct 中的 vm_mm 指针指向了所属的虚拟内存空间 mm_struct

        

        在进程虚拟内存空间中包含的内存区域 VMA 比较多的情况下,使用红黑树查找特定虚拟内存区域的时间复杂度是 O( logN ) ,可以显著减少查找所需的时间。所以在内核中,同样的内存区域 vm_area_struct 会有两种组织形式,一种是双向链表用于高效的遍历,另一种就是红黑树用于高效的查找。每个 VMA 区域都是红黑树中的一个节点,通过 struct vm_area_struct 结构中的 vm_rb 将自己连接到红黑树中。而红黑树中的根节点存储在内存描述符 struct mm_struct 中的 mm_rb 中: 

struct mm_struct {
        struct vm_area_struct *mmap;        /* list of VMAs */
        struct rb_root mm_rb;

 6.程序编译后的二进制文件如何映射到虚拟内存空间

        进程的虚拟内存空间 mm_struct 以及这些虚拟内存区域 vm_area_struct 是如何被创建并初始化的呢?

        我们写的程序代码编译之后会生成一个 ELF 格式的二进制文件,这个 ELF 格式的二进制文件中的布局和我们前边讲的虚拟内存空间中的布局类似,也是一段一段的,每一段包含了不同的元数据。磁盘文件中的段我们叫做 Section,内存中的段我们叫做 Segment,也就是内存区域

        磁盘文件中的这些 Section 会在进程运行之前加载到内存中并映射到内存中的 Segment。通常是多个 Section 映射到一个 Segment。比如磁盘文件中的 .text,.rodata 等一些只读的 Section,会被映射到内存的一个只读可执行的 Segment 里(代码段)。而 .data,.bss 等一些可读写的 Section,则会被映射到内存的一个具有读写权限的 Segment 里(数据段,BSS 段)

        那么这些 ELF 格式的二进制文件中的 Section 是如何加载并映射进虚拟内存空间的呢?

        内核中完成这个映射过程的函数是 load_elf_binary ,这个函数的作用很大,加载内核的是它,启动第一个用户态进程 init 的是它,fork 完了以后,调用 exec 运行一个二进制程序的也是它。当 exec 运行一个二进制程序的时候,除了解析 ELF 的格式之外,另外一个重要的事情就是建立上述提到的内存映射。

static int load_elf_binary(struct linux_binprm *bprm)
{
    // 设置虚拟内存空间中的内存映射区域起始地址 mmap_base
       setup_new_exec(bprm);

    /* Do this so that we can load the interpreter, if need be.  We will
       change some of these later */
    // 创建并初始化栈对应的 vm_area_struct 结构。
    // 设置 mm->start_stack 就是栈的起始地址也就是栈底,并将 mm->arg_start 是指向栈底的。   
    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                 executable_stack);
     // 将二进制文件中的代码部分映射到虚拟内存空间中
    error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
                elf_prot, elf_flags, total_size);
    // 创建并初始化堆对应的的 vm_area_struct 结构
    // 设置 current->mm->start_brk = current->mm->brk,设置堆的起始地址 start_brk,结束地址 brk。起初两者相等表示堆是空的
    retval = set_brk(elf_bss, elf_brk);
     // 将进程依赖的动态链接库 .so 文件映射到虚拟内存空间中的内存映射区域
    elf_entry = load_elf_interp(&loc->interp_elf_ex,
                        interpreter,
                        &interp_map_addr,
                        load_bias, interp_elf_phdata);
    // 初始化内存描述符 mm_struct
    current->mm->end_code = end_code;
    current->mm->start_code = start_code;
    current->mm->start_data = start_data;
    current->mm->end_data = end_data;
    current->mm->start_stack = bprm->p;
}

7.内核虚拟内存空间

        不同进程之间的虚拟内存空间是相互隔离的,彼此之间相互独立,相互感知不到其他进程的存在。使得进程以为自己拥有所有的内存资源。

         而内核态虚拟内存空间是所有进程共享的,不同进程进入内核态之后看到的虚拟内存空间全部是一样的。

7.1 32位体系内核虚拟内存空间布局

        本小节我们主要关注 0xC000 000 - 0xFFFF FFFF 这段虚拟内存地址区域也就是内核虚拟内存空间的布局情况。

7.1.1直接映射区

        在总共大小 1G 的内核虚拟内存空间中,位于最前边有一块 896M 大小的区域,我们称之为直接映射区或者线性映射区地址范围为 3G -- 3G + 896m 。之所以这块 896M 大小的区域称为直接映射区或者线性映射区,是因为这块连续的虚拟内存地址会映射到 0 - 896M 这块连续的物理内存上。也就是说 3G -- 3G + 896m 这块 896M 大小的虚拟内存会直接映射到 0 - 896M 这块 896M 大小的物理内存上,这块区域中的虚拟内存地址直接减去 0xC000 0000 (3G) 就得到了物理内存地址。所以我们称这块区域为直接映射区

        大家这里只需要记得内核态虚拟内存空间的前 896M 区域是直接映射到物理内存中的前 896M 区域中的,直接映射区中的映射关系是一比一映射。映射关系是固定的不会改变。明白了这个关系之后,我们接下来就看一下这块直接映射区域在物理内存中究竟存的是什么内容。通过命令"cat /proc/iomem"命令可以查看具体物理内存布局情况。

        在这段 896M 大小的物理内存中,前 1M 已经在系统启动的时候被系统占用,1M 之后的物理内存存放的是内核代码段,数据段,BSS 段(这些信息起初存放在 ELF格式的二进制文件中,在系统启动的时候被加载进内存)。当我们使用 fork 系统调用创建进程的时候,内核会创建一系列进程相关的描述符,比如之前提到的进程的核心数据结构 task_struct,进程的内存空间描述符 mm_struct,以及虚拟内存区域描述符 vm_area_struct 等。这些进程相关的数据结构也会存放在物理内存前 896M 的这段区域中,当然也会被直接映射至内核态虚拟内存空间中的 3G -- 3G + 896m 这段直接映射区域中。当进程被创建完毕之后,在内核运行的过程中,会涉及内核栈的分配,内核会为每个进程分配一个固定大小的内核栈(一般是两个页大小,依赖具体的体系结构),每个进程的整个调用链必须放在自己的内核栈中,内核栈也是分配在直接映射区

        我们都知道内核对物理内存的管理都是以页为最小单位来管理的,每页默认 4K 大小,理想状况下任何种类的数据页都可以存放在任何页框中,没有什么限制。比如:存放内核数据,用户数据,缓冲磁盘数据等。但是实际的计算机体系结构受到硬件方面的限制制约,间接导致限制了页框的使用方式。比如在 X86 体系结构下,ISA 总线的 DMA (直接内存存取)控制器,只能对内存的前16M 进行寻址,这就导致了 ISA 设备不能在整个 32 位地址空间中执行 DMA,只能使用物理内存的前 16M 进行 DMA 操作。

        因此直接映射区的前 16M 专门让内核用来为 DMA 分配内存,这块 16M 大小的内存区域我们称之为 ZONE_DMA。而直接映射区中剩下的部分也就是从 16M 到 896M(不包含 896M)这段区域,我们称之为 ZONE_NORMAL。从字面意义上我们可以了解到,这块区域包含的就是正常的页框(使用没有任何限制)。

 7.1.2 ZONE_HIGHMEM高端内存

        而物理内存 896M 以上的区域被内核划分为 ZONE_HIGHMEM 区域,我们称之为高端内存。 本例中我们的物理内存假设为 4G,高端内存区域为 4G - 896M = 3200M,那么这块 3200M 大小的 ZONE_HIGHMEM 区域该如何映射到内核虚拟内存空间中呢? 由于内核虚拟内存空间中的前 896M 虚拟内存已经被直接映射区所占用,而在 32 体系结构下内核虚拟内存空间总共也就 1G 的大小,这样一来内核剩余可用的虚拟内存空间就变为了 1G - 896M = 128M。 显然物理内存中 3200M 大小的 ZONE_HIGHMEM 区域无法继续通过直接映射的方式映射到这 128M 大小的虚拟内存空间中这样一来物理内存中的 ZONE_HIGHMEM 区域就只能采用动态映射的方式映射到 128M 大小的内核虚拟内存空间中,也就是说只能动态的一部分一部分的分批映射,先映射正在使用的这部分,使用完毕解除映射,接着映射其他部分。

        内核虚拟内存空间中的 3G + 896M 这块地址在内核中定义为 high_memory,high_memory 往上有一段 8M 大小的内存空洞。空洞范围为:high_memory 到 VMALLOC_START 。 VMALLOC_START 定义在内核源码 /arch/x86/include/asm/pgtable_32_areas.h 文件中:

#define VMALLOC_OFFSET    (8 * 1024 * 1024)
#define VMALLOC_START    ((unsigned long)high_memory + VMALLOC_OFFSET)

 7.1.3 vmalloc动态映射区

        接下来 VMALLOC_START 到 VMALLOC_END 之间的这块区域成为动态映射区。采用动态映射的方式映射物理内存中的高端内存。

        和用户态进程使用 malloc 申请内存一样,在这块动态映射区内核是使用 vmalloc 进行内存分配。由于之前介绍的动态映射的原因,vmalloc 分配的内存在虚拟内存上是连续的,但是物理内存是不连续的。通过页表来建立物理内存与虚拟内存之间的映射关系,从而可以将不连续的物理内存映射到连续的虚拟内存上。(由于 vmalloc 获得的物理内存页是不连续的,因此它只能将这些物理内存页一个一个地进行映射,在性能开销上会比直接映射大得多。)

7.1.4 永久映射区

        而在 PKMAP_BASE 到 FIXADDR_START 之间的这段空间称为永久映射区。在内核的这段虚拟地址空间中允许建立与物理高端内存的长期映射关系。比如内核通过 alloc_pages() 函数在物理内存的高端内存中申请获取到的物理内存页,这些物理内存页可以通过调用 kmap 映射到永久映射区中

#ifdef CONFIG_X86_PAE
#define LAST_PKMAP 512
#else
#define LAST_PKMAP 1024
#endif

#define PKMAP_BASE ((FIXADDR_START - PAGE_SIZE * (LAST_PKMAP + 1))    \
            & PMD_MASK)

 7.1.5固定映射区

        内核虚拟内存空间中的下一个区域为固定映射区,区域范围为:FIXADDR_START 到 FIXADDR_TOP。 FIXADDR_START 和 FIXADDR_TOP 定义在内核源码 /arch/x86/include/asm/fixmap.h 文件中:

extern unsigned long __FIXADDR_TOP; //0xFFFF F000
#define FIXADDR_TOP    ((unsigned long)__FIXADDR_TOP)
#define FIXADDR_START        (FIXADDR_TOP - FIXADDR_SIZE)

        在内核虚拟内存空间的直接映射区中,直接映射区中的虚拟内存地址与物理内存前 896M 的空间的映射关系都是预设好的,一比一映射。 在固定映射区中的虚拟内存地址可以自由映射到物理内存的高端地址上,但是与动态映射区以及永久映射区不同的是,在固定映射区中虚拟地址是固定的,而被映射的物理地址是可以改变的。也就是说,有些虚拟地址在编译的时候就固定下来了,是在内核启动过程中被确定的,而这些虚拟地址对应的物理地址不是固定的。采用固定虚拟地址的好处是它相当于一个指针常量(常量的值在编译时确定),指向物理地址,如果虚拟地址不固定,则相当于一个指针变量。 那为什么会有固定映射这个概念呢 ? 比如:在内核的启动过程中,有些模块需要使用虚拟内存并映射到指定的物理地址上,而且这些模块也没有办法等待完整的内存管理模块初始化之后再进行地址映射。因此,内核固定分配了一些虚拟地址,这些地址有固定的用途,使用该地址的模块在初始化的时候,将这些固定分配的虚拟地址映射到指定的物理地址上去。

 7.1.6 临时映射区

        在内核虚拟内存空间中的最后一块区域为临时映射区应用在把物理地址临时映射到虚拟地址上。使用 kmap_atomic 将缓存页临时映射到内核空间的一段虚拟地址上,这段虚拟地址就位于内核虚拟内存空间中的临时映射区上,然后将用户空间缓存区中的待写入数据通过这段映射的虚拟地址拷贝到 page cache 中的相应缓存页中。由于是临时映射,所以在拷贝完成之后,调用 kunmap_atomic 将这段映射再解除掉

 32 位体系结构 Linux 的整个虚拟内存空间的布局:

 7.2 64位体系结构下 Linux 虚拟内存空间整体布局

        因此在 64 位体系下的内核虚拟内存空间与物理内存的映射就变得非常简单,由于虚拟内存空间足够的大,即便是内核要访问全部的物理内存,直接映射就可以了,不在需要用到《7.1.2 ZONE_HIGHMEM 高端内存》小节中介绍的高端内存那种动态映射方式。

 8.到底什么是物理内存地址

        聊完了虚拟内存,我们接着聊一下物理内存,我们平时所称的内存也叫随机访问存储器( random-access memory )也叫 RAM 。而 RAM 分为两类: 一类是静态 RAM( SRAM ),这类 SRAM 用于 CPU 高速缓存 L1Cache,L2Cache,L3Cache。其特点是访问速度快,访问速度为 1 - 30 个时钟周期,但是容量小,造价高。另一类则是动态 RAM ( DRAM ),这类 DRAM 用于我们常说的主存上,其特点的是访问速度慢(相对高速缓存),访问速度为 50 - 200 个时钟周期,但是容量大,造价便宜些(相对高速缓存)

        内存由一个一个的存储器模块(memory module)组成,它们插在主板的扩展槽上。常见的存储器模块通常以 64 位为单位( 8 个字节)传输数据到存储控制器上或者从存储控制器传出数。据。而 DRAM 芯片就包装在存储器模块中,每个存储器模块中包含 8 个 DRAM 芯片,依次编号为 0 - 7

        而每一个 DRAM 芯片的存储结构是一个二维矩阵,二维矩阵中存储的元素我们称为超单元(supercell),每个 supercell 大小为一个字节(8 bit)。每个 supercell 都由一个坐标地址(i,j)。 i 表示二维矩阵中的行地址,在计算机中行地址称为 RAS (row access strobe,行访问选通脉冲)。 j 表示二维矩阵中的列地址,在计算机中列地址称为 CAS (column access strobe,列访问选通脉冲)。

        DRAM 芯片中的信息通过引脚流入流出 DRAM 芯片。每个引脚携带 1 bit的信号。 图中 DRAM 芯片包含了两个地址引脚( addr ),因为我们要通过 RAS,CAS 来定位要获取的 supercell 。还有 8 个数据引脚(data),因为 DRAM 芯片的 IO 单位为一个字节(8 bit),所以需要 8 个 data 引脚从 DRAM 芯片传入传出数据

 8.1 DRAM 芯片的访问

        我们现在就以读取上图中坐标地址为(2,2)的 supercell 为例,来说明访问 DRAM 芯片的过程:

(1)首先存储控制器将行地址 RAS = 2 通过地址引脚发送给 DRAM 芯片。

(2)DRAM 芯片根据 RAS = 2 将二维矩阵中的第二行的全部内容拷贝到内部行缓冲区中。

(3)接下来存储控制器会通过地址引脚发送 CAS = 2 到 DRAM 芯片中。

(4)DRAM芯片从内部行缓冲区中根据 CAS = 2 拷贝出第二列的 supercell 并通过数据引脚发送给存储控制器。

 8.2 CPU如何读写主存

        本小节我们来介绍下 CPU 是如何访问内存的:

        CPU 与内存之间的数据交互是通过总线(bus)完成的,而数据在总线上的传送是通过一系列的步骤完成的,这些步骤称为总线事务(bus transaction)。 其中数据从内存传送到 CPU 称之为读事务(read transaction),数据从 CPU 传送到内存称之为写事务(write transaction)。 总线上传输的信号包括:地址信号,数据信号,控制信号。其中控制总线上传输的控制信号可以同步事务,并能够标识出当前正在被执行的事务信息

(1) 当前这个事务是到内存的?还是到磁盘的?或者是到其他 IO 设备的?

(2) 这个事务是读还是写?

(3) 总线上传输的地址信号(物理内存地址),还是数据信号(数据)?

        IO bridge 负责将系统总线上的电子信号转换成存储总线上的电子信号。IO bridge 也会将系统总线和存储总线连接到IO总线(磁盘等IO设备)上。这里我们看到 IO bridge 其实起的作用就是转换不同总线上的电子信号。

8.3 CPU 从内存读取数据过程

        假设 CPU 现在需要将物理内存地址为 A 的内容加载到寄存器中进行运算。(CPU 只会访问虚拟内存,在操作总线之前,需要把虚拟内存地址转换为物理内存地址,总线上传输的都是物理内存地址,这里省略了虚拟地址到物理地址的转换)

        存储控制器会将物理内存地址转换为 DRAM 芯片中 supercell 在二维矩阵中的坐标地址(RAS,CAS)。并将这个坐标地址发送给对应的存储器模块。随后存储器模块会将 RAS 和 CAS 广播到存储器模块中的所有 DRAM 芯片。依次通过 (RAS,CAS) 从 DRAM0 到 DRAM7 读取到相应的 supercell 。我们知道一个 supercell 存储了一个字节( 8 bit ) 数据,这里我们从 DRAM0 到 DRAM7 依次读取到了 8 个 supercell 也就是 8 个字节,然后将这 8 个字节返回给存储控制器,由存储控制器将数据放到存储总线上

        CPU 总是以 word size 为单位从内存中读取数据,在 64 位处理器中的 word size 为 8 个字节。64 位的内存每次只能吞吐 8 个字节。

 8.5 CPU向内存写入数据过程

(1)CPU 将要写入的物理内存地址 A 放入系统总线上。

(2)通过 IO bridge 的信号转换,将物理内存地址 A 传递到存储总线上。

(3)存储控制器感受到存储总线上的地址信号,将物理内存地址 A 从存储总线上读取出来,并等待数据的到达。

        CPU 将寄存器中的数据拷贝到系统总线上,通过 IO bridge 的信号转换,将数据传递到存储总线上。存储控制器感受到存储总线上的数据信号,将数据从存储总线上读取出来。存储控制器通过内存地址 A 定位到具体的存储器模块,最后将数据写入存储器模块中的 8 个 DRAM 芯片中。

总结

        本文我们从虚拟内存地址开始聊起,一直到物理内存地址结束,包含的信息量还是比较大的。首先笔者通过一个进程的运行实例为大家引出了内核引入虚拟内存空间的目的及其需要解决的问题。

        在我们有了虚拟内存空间的概念之后,笔者又近一步为大家介绍了内核如何划分用户态虚拟内存空间和内核态虚拟内存空间,并在次基础之上分别从 32 位体系结构和 64 位体系结构的角度详细阐述了 Linux 虚拟内存空间的整体布局分布。

  • 我们可以通过 cat /proc/pid/maps 或者 pmap pid 命令来查看进程用户态虚拟内存空间的实际分布。
  • 还可以通过 cat /proc/iomem 命令来查看进程内核态虚拟内存空间的的实际分布。

        在我们清楚了 Linux 虚拟内存空间的整体布局分布之后,笔者又介绍了 Linux 内核如何对分布在虚拟内存空间中的各个虚拟内存区域进行管理,以及每个虚拟内存区域的作用。在这个过程中还介绍了相关的内核数据结构,近一步从内核源码实现角度加深大家对虚拟内存空间的理解。

        最后笔者介绍了物理内存的结构,以及 CPU 如何通过物理内存地址来读写内存中的数据。

2020-10-12
u014426028的博客
10-12 585
linux中mmap系统调用原理分析与实现 1、mmap系统调用(功能) void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , off_t offset ) 内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用read,write等操作。 2、mmap系统调用(参数) 1)addr: 指定映射的起...
深入理解Linux虚拟内存管理
weixin_45337360的博客
05-05 752
类比快递的收货地址和真实地址位置,首先,收货地址是一个虚拟地址,它是人为定义的,而我们的城市,小区,街道是真实存在的,他们的地理位置就是物理地址。然后,我们切回计算机世界,在计算机里面,内存地址是用来定义数据在内存中存储位置的,内存地址也分为虚拟地址和物理地址。同样,这个虚拟地址也是人为定义的,类比我们现实世界的收货地址,而物理地址就是数据在物理内存中真实存储的位置,类比我们的城市,小区,街道的地理位置。说完定义后,现在开始讲一下虚拟内存地址到底长什么样。
操作系统:详解物理内存与虚拟内存,用户空间与内核态空间
weixin_44440311的博客
06-14 2814
针对这个操作:我们的用户在写读数据时,会去向内核态申请,想要读取内核的数据,而内核数据要去等待驱动程序从硬件上读取数据,当从磁盘上加载到数据之后,内核会将数据写入到内核的缓冲区中,然后再将数据拷贝到用户态的buffer中,然后再返回给应用程序,整体而言,速度慢,就是这个原因,为了加速,我们希望read也好,还是wait for data也最好都不要等待,或者时间尽量的短。反之,当一个进程需要访问物理内存中没有被装入的页时,操作系统就会将这些页从磁盘上读取到物理内存中,供该进程进行访问和使用。
高级OS(八) - Linux内存映射
zxq997997的博客
03-10 413
高级OS(八) - Linux内存映射一.题目二.解答1.进入Linux 内核(4.19)源代码, 给出task_struct 结构,mm_struct 结构和 vm_area_struct 结构的关系图,这样的数据结构设计对你有什么启发?每个数据结构至少分析2个以上字段2.对实验中涉及的相关内核数据结构、函数进行深入分析,并画出流程图。至少分析6个数据结构和6个函数 一.题目 查看学堂在线《Linux内核分析与应用》的4.5~4.7节的视频,完成其实验代码,截图,讨论,两人一组。 进入Linux 内核(
进程管理(九)--创建进程内存管理copy_mm
奇小葩
05-18 1379
对于进程,除了0号进程,其他的所有进程(无论是内核线程还是普通线程)都是通过fork出来的,而创建进程是在内核中完成的 要么在内核空间直接创建出所谓的内核线程 要么是应用空间通过fork/clone/vfork这样的系统调用进入内核,再内核空间创建 同上一章,我们完成的分析了fork的整个过程,fork分为两部分,一部分是初始化进程控制块,另外一部分是进程管理部分。本章的重点学习以下内容 子进程如何构建自己的内存管理 父子进程如何共享地址空间 写时复制如何发生 1. 写时复制技术 在传统的unix操
Linux学习心得——内存管理方法
09-06
1. Linux 内核管理哪些物理空间;arm 版本的4G虚拟地址是如何组织的; 2. 内核空间如何获得虚拟/物理空间,并使用它们; 3. 用户空间如何获得虚拟/物理空间,并使用它们; 4. 内核空间和用户空间如何交互; 5. 整体...
疯狂内核之——Linux虚拟内存
05-30
1.4 Linux内存布局 21 1.5 内核空间和用户空间 23 1.5.1 初始化临时内核页表 24 1.5.2 永久内核页表的初始化 32 1.5.3 第一次进入用户空间 41 1.5.4 内核映射机制实例 44 1.6 固定映射的线性地址 48 1.7 高端内存...
内存管理内存管理内存管理
04-04
如您所见,brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。 实现一个简单的分配程序 如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 ...
操作系统(内存管理)
09-20
文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半...
linux获取进程首地址,Linux内核设计与实现(13)--进程地址空间
weixin_33785291的博客
05-02 666
上一节讲了内核如何管理物理内存,其实内核除了管理本身的内存外,还必须管理用户空间中进程的内存,这就是进程地址空间,也就是系统中每个用户空间进程所看到的内存。Linux采用虚拟内存技术,系统中所有进程之间以虚拟方式共享内存,对一个进程而言,可以访问整个系统的所有物理内存,其拥有地址空间也可以远远大于系统物理内存。1.地址空间进程地址空间由进程可寻址的虚拟内存组成。每个进程都有一个32bit或64bi...
Linux——虚拟内存空间分布
Snfiltration
12-30 1292
Linux——虚拟内存空间分布虚拟地址空间分布图用户空间内存空间分布描述 虚拟地址空间分布图 用户空间内存空间分布描述 系统内的程序分为程序段和数据段,具体又可细分为一下几个部分: text段-代码段 text段存放程序代码,运行前就已经确定(编译时确定),通常为只读,可以直接在ROM或Flash中执行,无需加载到RAM。 rodata段(read-only-data)-常量区 rodata段存储常量数据,比如程序中定义为const的全局变量,#define定义的常量,以及诸如“Hello W
分段 分页 虚拟内存空间 逻辑地址 物理地址
心之所向
03-11 8220
一、虚拟内存空间虚拟内存空间是系统的一种技术,当程序被载入内存时,运用虚拟内存空间技术让程序误认为自己目前独占电脑内存,能够占用电脑所有的内存,访问所有内存地址。 以32位操作系统为例: 32位系统程序的指针为32位(4字节),2^32 = 4GB,也就是说指针可以取值的方法有2^32种,可以访问2^32地址。这也就为什么有种说法:32位系统支持装最高4g内存。当程序载入内存后,系统为程序赋予4
Linux虚拟内存空间分布
qq_18144747的博客
03-03 3807
1.程序和进程 问题:程序和进程各是什么? 程序只是一段可以执行的代码文件,通俗讲在 linux 上就是一个可执行文件。当一个程序运 行时就被称为进程,即进程是运行状态的程序。 程序存储了一系列文件信息,这些信息描述了如何在运行时创建一个进程,包含了下面的内容: 二进制格式标识:  描述可执行文件的元信息,内核利用该信息解释文件中的其他信息 机器语言指令:   对程...
两篇很好的虚拟内存区域管理方面的…
-第二月-的专栏
08-27 886
今天本来想写一篇文章总结一下linux进程空间映射,特别是跟vm_area有关的内容。记录一下进程空间是怎么组织的,vm_area_struct的设计意图,以及mm/mmap.c中所定义的对它的操作,等等。 不过没想到偶然间找到了两篇非常棒的文章,正是想要的,而且质量相当高,乍看很有惊艳的感觉。我就不再重造车轮了,收藏起来,在这里记录一下: 与虚拟内存区域有关的操作 与虚拟内存区域有关的操作
虚拟内存(现代操作系统),分段
qq_36553387的博客
06-13 991
虚拟内存 虚拟内存的基本思想:每个程序拥有自己的地址空间,这个空间被分割成多个块,每个块称作一页或页面。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。 分页 当程序引用了一组内存地址,当程序执行指令。 mov REG,1000 时,它把地址为1000的内存单元的内容复制到REG中。地址可
linux mmap 使用
bruce_wang_janet的专栏
07-03 1588
一.前言mmap的具体实现以前在学习内核时学习过,但是对于其中的很多函数是一知半解的,有些只能根据其函数名来猜测其具体的功能,在本文中,一起来重新深入理解其具体的实现。二.mmap的用户层应用void *mmap(void *start,size_t leng
struct vm_area_struct内核数据结构
山庄来客的专栏
06-02 1119
Linux内核中,关于虚存管理的最基本的管理单元应该是struct vm_area_struct了,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。   下面是struct vm_area_struct结构体的定义: /* * This struct defines a memory VMM memory area. There is o
fork后子进程与父进程的内存关系
igaozh的专栏
09-17 1978
  fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。于是起初我就感到奇怪,子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?!原来在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆...
Linux内存管理 用户空间
最新发布
05-13
Linux内存管理中,用户空间是指进程可以访问的地址空间,用于存储进程的代码、数据和堆栈等。用户空间的大小通常是由具体的硬件架构和操作系统版本等因素决定的,一般情况下,用户空间大小在3GB~4GB之间。 用户空间...

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
写文章

热门文章

  • 遗传算法(GA)解决MTSP问题及Matlab代码 5978
  • 禁忌搜索算法(tabu search)解决TSP及其Matlab代码 5916
  • 粒子群优化算法解决TSP问题及Matlab代码 5102
  • 本人研究方向:多机器人任务分配的简单介绍 2495
  • Linux内存管理1——虚拟内存空间 1490

分类专栏

  • Linux内核理论 7篇
  • Linux内存管理篇 1篇
  • Linux驱动实践分析 9篇
  • 算法 5篇

最新评论

  • 遗传算法(GA)解决MTSP问题及Matlab代码

    小灵蛇: 如果只知道每个城市之间的距离该如何求解呢

  • 遗传算法(GA)解决MTSP问题及Matlab代码

    m0_55701018: 机器人数量为1就是TSP问题,一条路径一个圈,那位兄弟想要的是多条路径多个圈,路径交点唯一且在出发点

  • 遗传算法(GA)解决MTSP问题及Matlab代码

    m0_74393838: 机器人数量为一就行了啊

  • 遗传算法(GA)解决MTSP问题及Matlab代码

    m0_74393838: 写的太复杂了,根本看不懂

  • Linux platform 设备驱动实验

    So_shine: 第一个图画的比较形象

大家在看

  • 用什么仪器可以看到普通陶瓷杯碗,怎么看清不锈钢碗里的硬币仪器穿透 103
  • 代码随想录训练营 day36,37 | Leetcode406 根据身高重建队列、LeetCode452 用最少数量的箭引爆气、LeetCode56 合并区间、LeetCode738 单调递增的数字
  • 基于Ubuntu的nfs文件共享服务器搭建 794
  • 不可避免的网络攻击致 2000 家公司损失 4000 亿美元

最新文章

  • Linux I2C 驱动实验
  • Linux platform 设备驱动实验
  • Linux中断实验
2023年18篇
2022年3篇
2021年5篇

目录

目录

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小吴伴学者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或 充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

深圳SEO优化公司黄南英文网站建设报价兰州营销网站公司西乡外贸网站制作公司南宁网络营销哪家好安庆外贸网站制作报价雅安企业网站建设报价岳阳SEO按天计费价格濮阳网站优化按天计费十堰网站优化按天扣费推荐濮阳企业网站改版推荐山南seo报价鞍山关键词按天收费推荐营口网站开发公司济源建站德州网站推广系统和田网站制作设计多少钱淮安关键词按天计费推荐绍兴网站推广工具报价平凉网站开发多少钱包头至尊标王哪家好临沂如何制作网站公司湛江网站推广方案报价延边企业网站设计民治SEO按天计费推荐天门网站设计模板价格晋中阿里店铺托管哪家好南京至尊标王公司果洛外贸网站设计价格海口网站优化价格洛阳网站改版哪家好歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

深圳SEO优化公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化