当前位置: 首页 > news >正文

Linux 内存管理之 Rmap 反向映射

文章目录

  • 一、简介
  • 二、struct page
    • 2.1 mapping 字段
      • 2.1.1 页缓存(Page Cache)
      • 2.1.2 匿名页(Anonymous Pages)
  • 三、匿名页反向映射
    • 3.1 相关结构体
      • 3.1.1 struct vm_area_struct
      • 3.1.2 struct anon_vma
      • 3.1.3 struct anon_vma_chain
    • 3.2 匿名页反向映射全流程
    • 3.3 匿名页反向映射的建立
      • 3.3.1 进程内存分配产生匿名页面
        • 3.3.1.1 anon_vma_prepare
        • 3.3.1.2 alloc_zeroed_user_highpage_movable
        • 3.3.1.3 page_add_new_anon_rmap
          • __page_set_anon_rmap
      • 3.3.2 父进程fork子进程创建匿名页面
  • 四、文件页反向映射
    • 4.1 文件页反向映射全流程
    • 4.2 page_add_file_rmap
  • 参考资料

一、简介

反向映射是内存管理中的一个核心概念,用于高效地通过物理页找到映射了该页的所有虚拟地址(即页表项)。这对于页面回收、迁移、换出等操作至关重要。

反向映射的发展经历了几个阶段,从最初的匿名页反向映射,到后来加入文件页和交换缓存的反向映射支持。

为什么需要反向映射?
在操作系统中,多个进程的虚拟地址可能映射到同一个物理页(例如共享内存、写时复制等)。当内核需要回收一个物理页时,它必须修改所有映射了该页的页表项,使其无效或指向其他位置。如果没有反向映射,内核将不得不遍历所有进程的页表来寻找映射,这是极其低效的。

一个物理页 → 多个虚拟映射:通过 fork()、KSM 等机制,单个物理页可能被多个进程的虚拟地址映射

反向映射的目标是:给定一个物理页,快速找到所有映射了该页的虚拟地址(即页表项),即快速定位所有映射到某个物理页的虚拟地址和进程。。

正向映射:虚拟地址 → 物理页帧(通过页表实现)
正向映射即内存映射,即从虚拟内存到物理内存的映射。

反向映射:物理页帧 → 所有映射它的虚拟地址
反向映射在已知物理页面(page frame,可能是PFN、可能是指向page descriptor的指针,也可能是物理地址,内核有各种宏定义用于在它们之间进行转换)的情况下,找到映射该物理页面的虚拟地址。
由于一个page frame可以在多个进程之间共享,因此反向映射的任务是把分散在各个进程地址空间中的所有的page table entry全部找出来。

如下图所示:
在这里插入图片描述

关键应用场景:
页面回收(回收前需解除所有映射)
内存迁移(迁移前需更新所有页表项)
透明大页分裂(THP split)
NUMA 平衡

比如内存回收:当发生内存回收时,通过反向映射技术在inactive lru中很容易获得物理内存页面并找到所有映射关系进行解映射。

二、struct page

struct page {union {struct {	/* Page cache and anonymous pages *//* See page-flags.h for PAGE_MAPPING_FLAGS */struct address_space *mapping;  //file page cachepgoff_t index;		/* Our offset within mapping. */};}union {		/* This union is 4 bytes in size. *//** If the page can be mapped to userspace, encodes the number* of times this page is referenced by a page table.*/atomic_t _mapcount;};
}

物理页面描述符struct page中与反向映射有关的两个关键成员是mapping和__mapcount:
(1)mapping:字段 mapping 用于区分匿名页面和基于文件映射的页面,如果该字段的最低位被置位了,那么该字段包含的是指向 anon_vma 结构(用于匿名页面)的指针;否则,该字段包含指向 address_space 结构的指针(用于基于文件映射的页面)。

mapping:表示页面所指向的地址空间。内核中的地址空间通常有两个不同的地址空间,—个用于文件映射页面,如在读取文件时,地址空间用于将文件的内容数据与装载数据的存储介质区关联起来;另—个用于匿名映射。内核使用一个简单直接的方式实现了“一个指针,两种用途”,mapping成员的最低两位用于判断是否指向匿名映射或KSM页面的地址空间。如果指向匿名页面,那么mapping成员指向匿名页面的地址空间数据结构anon_vma。

mapping等于NULL,表示该page frame不再内存中,而是被swap out到磁盘去了。
mapping不为NULL,且第一位置位,该页为匿名页,mapping指向ano_vma结构。
mapping不为NULL,且第一位为0,该页为文件页,mapping指向address_space结构。

(2)index成员指向了该page在整个vm_area_struct中的偏移。

(3)__mapcount:_mapcount表示共享该物理页面的页表现数目,即有多少个进程页表的pte映射到该物理页面。该值初始值为-1,每增减一个pte映射该值+1
即该 page 映射了多少个进程的虚拟内存空间,一个 page 可以被多个进程映射。

2.1 mapping 字段

mapping 字段的双重用途:
struct page 中的 mapping 字段是一个联合体(union),它有两种用途:
对于文件映射页(file-backed pages),它指向文件的 address_space 结构体。
对于匿名页,它包含一个指向 anon_vma 结构体的指针,以及一些标志位。

// v5.15/source/include/linux/page-flags.h/** On an anonymous page mapped into a user virtual memory area,* page->mapping points to its anon_vma, not to a struct address_space;* with the PAGE_MAPPING_ANON bit set to distinguish it.  See rmap.h.** On an anonymous page in a VM_MERGEABLE area, if CONFIG_KSM is enabled,* the PAGE_MAPPING_MOVABLE bit may be set along with the PAGE_MAPPING_ANON* bit; and then page->mapping points, not to an anon_vma, but to a private* structure which KSM associates with that merged page.  See ksm.h.** PAGE_MAPPING_KSM without PAGE_MAPPING_ANON is used for non-lru movable* page and then page->mapping points a struct address_space.** Please note that, confusingly, "page_mapping" refers to the inode* address_space which maps the page from disk; whereas "page_mapped"* refers to user virtual address space into which the page is mapped.*/
#define PAGE_MAPPING_ANON	0x1
#define PAGE_MAPPING_MOVABLE	0x2
#define PAGE_MAPPING_KSM	(PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)
#define PAGE_MAPPING_FLAGS	(PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)

在 Linux 内核中,struct page 的 mapping 字段是一个巧妙的设计,它通过指针与标志位复用的方式来高效存储不同类型的映射信息:
对于文件映射页:mapping 直接指向文件的 struct address_space
对于匿名页:mapping 低几位存储标志位(如 PAGE_MAPPING_ANON),剩余高位存储 struct anon_vma 的指针。
这种设计允许内核通过简单的位运算同时存储类型信息和指针,无需额外字段,节省了内存空间。

这种指针与标志位复用的设计主要出于两个目的:
节省内存:内核中 struct page 实例数量庞大(通常数百万个),每个实例节省几个字节就能显著减少内存占用
提高性能:通过位运算直接提取信息,避免了条件判断和函数调用的开销

这种设计依赖于两个重要前提:
地址对齐:现代系统的内存地址通常是按页对齐的(例如 4KB 对齐),这意味着指针的低几位总是 0(例如 4KB 对齐时,低 12 位为 0)
标志位占用低位:内核利用了指针低几位永远为 0 的特性,将标志位存储在这些空闲位中
通过这种方式,内核在不使用额外内存的情况下,实现了类型信息与指针的共存。

struct page 的 mapping 字段在页缓存(Page Cache)和匿名页(Anonymous Pages)中的双重作用:

2.1.1 页缓存(Page Cache)

struct address_space是用于管理文件系统中的文件页缓存(page cache):

struct address_space *page_mapping(struct page *page)
{struct address_space *mapping;//获取复合页(compound page)的头页page = compound_head(page);......//匿名页过滤mapping = page->mapping;if ((unsigned long)mapping & PAGE_MAPPING_ANON)return NULL;//清除低2位标志(PAGE_MAPPING_FLAGS = 0x3)return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
}
EXPORT_SYMBOL(page_mapping);

page_mapping() 用于安全获取与 struct page 关联的 address_space 指针,主要服务于:
文件系统页缓存管理
页面回收(page reclaim)
内存迁移(migration)
反向映射(reverse mapping)

2.1.2 匿名页(Anonymous Pages)

static inline void *__page_rmapping(struct page *page)
{unsigned long mapping;mapping = (unsigned long)page->mapping;//先对 PAGE_MAPPING_FLAGS 取反(得到一个高地址部分全为 1,标志位部分全为 0 的掩码)//然后将这个掩码与 mapping 进行按位与操作,从而清除标志位,只保留高地址部分的指针值//对于匿名页:低几位存储标志位,高地址部分存储 anon_vma 结构体指针mapping &= ~PAGE_MAPPING_FLAGS;//返回anon_vma 结构体(对于匿名页)return (void *)mapping;
}struct anon_vma *page_anon_vma(struct page *page)
{unsigned long mapping;// 1. 处理复合页(compound page)的情况// 复合页是由多个连续物理页组成的大页,这里获取其头部页page = compound_head(page);// 2. 获取 page 结构体中的 mapping 字段并转换为无符号长整型mapping = (unsigned long)page->mapping;// 3. 检查 mapping 字段的标志位,判断是否为匿名页if ((mapping & PAGE_MAPPING_FLAGS) != PAGE_MAPPING_ANON)return NULL;// 4. 如果是匿名页,则调用 __page_rmapping() 获取其 anon_vma 指针return __page_rmapping(page);
}

(1)复合页处理 (compound_head())
Linux 内核支持将多个连续的物理页合并为一个 “复合页”(Compound Page),用于支持大页(Huge Pages)等特性。每个复合页有一个 “头部页”(head page)和多个 “尾部页”(tail pages),只有头部页包含完整的元数据。compound_head() 函数用于获取复合页的头部页。

(2)mapping 字段的双重用途:
struct page 中的 mapping 字段是一个联合体(union),它有两种可能的用途:
对于文件映射页(file-backed pages),它指向文件的 address_space 结构体
对于匿名页,它包含一个指向 anon_vma 结构体的指针。

通过将 mapping 转换为无符号长整型并与 PAGE_MAPPING_FLAGS 掩码进行按位与操作,可以提取出标志位,从而判断该页是否为匿名页。

标志位检查 (PAGE_MAPPING_ANON)
PAGE_MAPPING_FLAGS 是一个预定义的掩码,用于提取 mapping 字段中的标志位部分。PAGE_MAPPING_ANON 是一个特定的标志值,表示该页是匿名页。如果标志位匹配,则说明该页是匿名页,可以继续处理;否则返回 NULL。

(3)标志位检查 (PAGE_MAPPING_ANON)
PAGE_MAPPING_FLAGS 是一个预定义的掩码,用于提取 mapping 字段中的标志位部分。PAGE_MAPPING_ANON 是一个特定的标志值,表示该页是匿名页。如果标志位匹配,则说明该页是匿名页,可以继续处理;否则返回 NULL。

(4)获取反向映射数据结构 (__page_rmapping())
如果确认是匿名页,则调用 __page_rmapping() 函数来获取该页的 anon_vma 指针。这个函数通常会通过一些内部机制(如指针运算或间接查找)从 mapping 字段中提取出实际的 anon_vma 结构体地址。

page_anon_vma() 用于安全获取与匿名页关联的 anon_vma 结构指针,主要服务于:
反向映射(RMAP)操作
页面回收时的匿名页处理
COW(Copy-On-Write)机制
KSM(Kernel Samepage Merging)

	if (PageAnon(page)) {struct anon_vma *page__anon_vma = page_anon_vma(page);}

三、匿名页反向映射

3.1 相关结构体

3.1.1 struct vm_area_struct

// v5.15/source/include/linux/mm_types.hstruct vm_area_struct {/** A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma* list, after a COW of one of the file pages.	A MAP_SHARED vma* can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack* or brk vma (with NULL file) can only be in an anon_vma list.*/struct list_head anon_vma_chain; /* Serialized by mmap_lock &* page_table_lock */struct anon_vma *anon_vma;	/* Serialized by page_table_lock */
}

anon_vma_chain通过链表链接了vma。
vma则会有指针指向自己的anon_vma。

内核在匿名页面创建需要建立反向映射的钩子,即建立相关的数据结构。有两个重要的数据结构:struct anon_vma 和 struct anon_vma_chain。

3.1.2 struct anon_vma

// v5.15/source/include/linux/rmap.h/** The anon_vma heads a list of private "related" vmas, to scan if* an anonymous page pointing to this anon_vma needs to be unmapped:* the vmas on the list will be related by forking, or by splitting.** Since vmas come and go as they are split and merged (particularly* in mprotect), the mapping field of an anonymous page cannot point* directly to a vma: instead it points to an anon_vma, on whose list* the related vmas can be easily linked or unlinked.** After unlinking the last vma on the list, we must garbage collect* the anon_vma object itself: we're guaranteed no page can be* pointing to this anon_vma once its vma list is empty.*/
struct anon_vma {struct anon_vma *root;		/* Root of this anon_vma tree */......struct anon_vma *parent;	/* Parent of this anon_vma */....../* Interval tree of private "related" vmas */struct rb_root_cached rb_root;
};

struct anon_vma:匿名线性区描述符,每个匿名vma都会有一个这个结构。

page数据结构中的mapping成员指向匿名页面的anon_vma数据结构。

对于一个页框,若该页为匿名页,则其struct page中的mapping指向 anon_vma。

anon_vma结构体用于管理匿名页对应的所有VMAs:
匿名页找到对应的anon_vma,然后再遍历AV的rb_root查询到该页框所有的anon_vma_chain,然后通过anon_vma_chain获取对应的VMA。

在这里插入图片描述
anon_vma为匿名页提供一个 “映射集合” 管理单元,每个匿名页通过 page->mapping 关联到一个 AV。内部通过 rb_root 红黑树存储所有与该匿名页相关的 AVC 节点,实现对多进程映射关系的快速插入、删除和查询。

anon_vma 是匿名页反向映射的核心数据结构,主要解决:
匿名页生命周期管理:跟踪所有映射同一物理页的 VMA(Virtual Memory Area)
COW (Copy-On-Write) 优化:在 fork/mprotect 时高效复制映射关系
内存回收支持:快速找到所有映射页面的进程,以便解除映射或迁移

3.1.3 struct anon_vma_chain

// v5.15/source/include/linux/rmap.h/** The copy-on-write semantics of fork mean that an anon_vma* can become associated with multiple processes. Furthermore,* each child process will have its own anon_vma, where new* pages for that process are instantiated.** This structure allows us to find the anon_vmas associated* with a VMA, or the VMAs associated with an anon_vma.* The "same_vma" list contains the anon_vma_chains linking* all the anon_vmas associated with this VMA.* The "rb" field indexes on an interval tree the anon_vma_chains* which link all the VMAs associated with this anon_vma.*/
struct anon_vma_chain {struct vm_area_struct *vma;struct anon_vma *anon_vma;struct list_head same_vma;   /* locked by mmap_lock & page_table_lock */struct rb_node rb;			/* locked by anon_vma->rwsem */......
};

anon_vma_chain 是连接虚拟内存区域(VMA)和匿名页反向映射结构(anon_vma)的桥梁。

在这里插入图片描述

通过same_vma链表节点,将anon_vma_chain添加到vma->anon_vma_chain链表中;
same_vma 中存储的 anon_vma_chain 对应的 VMA 全都是一样的,链表结构 same_vma 存储了进程相应虚拟内存区域 VMA 中所包含的所有匿名页。
即所有指向相同VMA的 anon_vma_chain 会被链接到一个链表中,链表头就是VMA的anon_vma_chain成员。

通过rb红黑树节点,将anon_vma_chain添加到anon_vma->rb_root的红黑树中;
列表元素 anon_vma_chain 中的 anon_vma 是不一样的。
而一个anon_vma 会管理若干的anon_vma_chain (及管理若干的VMA),所有相关的anon_vma_chain (即VMA(其子进程或者孙进程))都挂入红黑树,根节点就是anon_vma 的rb_root成员。
每个 anon_vma_chain 对应一个映射该物理页的 VMA。

代码如下所示:

static void anon_vma_chain_link(struct vm_area_struct *vma,struct anon_vma_chain *avc,struct anon_vma *anon_vma)
{avc->vma = vma;				// AVC 指向所属 VMAavc->anon_vma = anon_vma;		// AVC 指向所属 anon_vmalist_add(&avc->same_vma, &vma->anon_vma_chain);		//加入 VMA 的链表anon_vma_interval_tree_insert(avc, &anon_vma->rb_root);  //插入 anon_vma 的红黑树
}

在这里插入图片描述

如下图所示:
在这里插入图片描述
在这里插入图片描述
页框与page结构对应,page结构中的mapping字段指向anon_vma,从而可以通过RMAP机制去找到与之关联的VMA;

page找到VMA的路径一般如下:page->anon_vma->anon_vma_chain->vm_area_struct,其中anon_vma_chain起到桥梁作用,至于为何需要anon_vma_chain,主要考虑当父进程和多个子进程同时拥有共同的page时的查询效率。

作为 AV 与 vma 之间的 “桥梁”,每个 AVC 同时关联一个 AV 和一个 vma:
通过 avc->anon_vma 指向所属的 AV;
通过 avc->vma 指向对应的虚拟内存区域(vma)。

同时,vma 会通过 vma->anon_vma_chain 维护一个 AVC 链表:同一 vma 可能映射多个匿名页,因此会关联多个 AVC,这些 AVC 以链表形式串联,便于 vma 快速遍历自身关联的所有 AV。

3.2 匿名页反向映射全流程

关键数据结构关系:

struct page {union {struct {	/* Page cache and anonymous pages */struct address_space *mapping;  	//file page cache//struct anon_vma *anon_vma;      	// anonymous pages(带PAGE_MAPPING_ANON标记)pgoff_t index;						/* Our offset within mapping. */};}
}struct vm_area_struct {struct list_head anon_vma_chain;   		//通过链表链接该VMA中所包含的所有anon_vma_chainstruct anon_vma *anon_vma;				//指向自己所属的anon_vma数据结构
}struct anon_vma {struct anon_vma *root;		/* Root of this anon_vma tree */struct anon_vma *parent;	/* Parent of this anon_vma *//* Interval tree of private "related" vmas */struct rb_root_cached rb_root;    // 红黑树管理anon_vma_chain
};struct anon_vma_chain {struct vm_area_struct *vma;   // 指向自己所属的关联的进程虚拟内存空间struct anon_vma *anon_vma;    // 指向自己所属的anon_vma数据结构struct list_head same_vma;    //链表节点,通常把anon_vma_chain添加到vma->anon_vma_chain链表中struct rb_node rb;			 //红黑树节点,通常把anon_vma_chain添加到anon_vma->rb_root的红黑树
};

(1) 物理页到struct page

// 通过PFN获取page
struct page *pfn_to_page(unsigned long pfn) 

(2)获取anon_vma

struct anon_vma *anon_vma = page_anon_vma(page);
static inline struct anon_vma *page_anon_vma(struct page *page)
{if (((unsigned long)page->mapping & PAGE_MAPPING_ANON) == 0)return NULL;return (struct anon_vma *)(page->mapping & ~PAGE_MAPPING_FLAGS);
}

(3)遍历红黑树anon_vma_chain,然后获取vm_area_struct。

anon_vma_lock_read(anon_vma);  // 加读锁
struct anon_vma_chain *avc;
struct rb_node *rb_node;
for (rb_node = rb_first_cached(&anon_vma->rb_root); rb_node; rb_node = rb_next(rb_node)) {avc = rb_entry(rb_node, struct anon_vma_chain, rb);vma = avc->vma;// 计算虚拟地址unsigned long vaddr = vma->vm_start + (page->index << PAGE_SHIFT);struct task_struct *task = vma->vm_mm->owner;pid_t pid = task_pid_nr(task);// 处理映射关系(我们自定义逻辑)handle_mapping(vma, pid, vaddr);
}
anon_vma_unlock_read(anon_vma);  // 释放锁

(1)通过物理地址获取物理页描述符 struct page
物理内存被划分为固定大小的页框(如 4KB),每个页框由 struct page 结构体描述,包含页的状态、映射关系等核心信息。
获取方式:通过物理地址计算页框号(pfn = phys_addr >> PAGE_SHIFT),再通过 pfn_to_page(pfn) 宏将页框号转换为 struct page 指针。
作用:struct page 是连接物理页与虚拟地址映射的枢纽,其成员 mapping 和 index 是反向映射的关键。

(2)利用 page->mapping 获取匿名映射数据(anon_vma)
对于匿名页(如进程堆、栈等无文件关联的内存),page->mapping 指向 struct anon_vma 结构体(简称 AV),而非文件映射中的 address_space。
struct anon_vma 的核心作用:管理所有映射了该匿名页的虚拟内存区域(vma),通过红黑树组织这些映射关系,实现高效查找。
关键成员:rb_root(红黑树的根节点),用于存储 struct anon_vma_chain(简称 AVC)节点,每个 AVC 对应一个 vma 与匿名页的映射关系。

(3)遍历 anon_vma->rb_root 红黑树,处理每个 anon_vma_chain(AVC)
红黑树 anon_vma->rb_root 中的每个节点是 struct anon_vma_chain(AVC),它是连接 anon_vma 与 vma 的桥梁。

解析 anon_vma_chain(AVC)数据
struct anon_vma_chain 包含两个核心成员:
vma:指向映射该匿名页的 struct vm_area_struct(VMA),即进程的虚拟内存区域(描述虚拟地址范围、权限等)。
rb_node:红黑树节点,用于将 AVC 链接到 anon_vma 的红黑树中。

通过遍历红黑树的每个 AVC 节点,可执行以下操作:
获取 VMA 与虚拟地址:
利用 AVC->vma 得到对应的 VMA,VMA 中包含虚拟地址范围(vm_start、vm_end)和所属进程(vma->vm_mm->owner,即 task_struct)。

结合 page->index 计算该物理页在 VMA 中的虚拟地址:vaddr = vma->vm_start + (page->index << PAGE_SHIFT)。
其中 page->index 是该物理页在 VMA 中的偏移量(以页为单位)。

获取进程 PID:通过 vma->vm_mm->owner->pid 从 VMA 所属的内存描述符(mm_struct)中获取进程 PID。

(4)遍历结果:获取所有映射该物理页的进程与虚拟地址
通过遍历 anon_vma 的红黑树,处理每个 AVC 节点,最终可收集到:
所有映射该匿名页的进程 PID(通过 vma->vm_mm->owner)。
每个进程中对应的虚拟地址(通过 vma->vm_start + page->index * PAGE_SIZE)。
对应的 VMA 信息(如虚拟地址范围、访问权限等)。

总结:匿名页反向映射的核心逻辑
匿名页的反向映射通过 物理页(page)→ 匿名映射管理(anon_vma)→ 映射链(anon_vma_chain)→ 虚拟内存区域(vma)→ 进程(task_struct) 的链路,实现了从物理页到所有映射它的虚拟地址及进程的追踪。

如下图所示:
在这里插入图片描述
图片来源:https://blog.csdn.net/u010923083/article/details/116456497

3.3 匿名页反向映射的建立

3.3.1 进程内存分配产生匿名页面

进程为自己的进程地址空间VMA分配物理内存时,通常会产生匿名页面。例如:

malloc() → 用户进程写内存 → 内核发生缺页异常 → do_anonymous_page()

用户态进程访问虚拟内存的时候,发现没有映射到物理内存,页表也没有创建过,才触发缺页异常。进入内核调用 do_page_fault,一直调用到 __handle_mm_fault,既然原来没有创建过页表,于是,__handle_mm_fault 调用 pud_alloc 和 pmd_alloc,来创建相应的页目录项,最后调用 handle_pte_fault 来创建页表项。

do_page_fault()-->__handle_mm_fault()-->handle_pte_fault ()-->do_anonymous_page()
handle_pte_fault()
{//如果 PTE,也就是页表项,从来没有出现过,那就是新映射的页if (!vmf->pte) {//如果是匿名页,应该映射到一个物理内存页,调用 do_anonymous_pageif (vma_is_anonymous(vmf->vma))return do_anonymous_page(vmf);
}
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;struct page *page;/* Allocate our own private page. */anon_vma_prepare(vma)page = alloc_zeroed_user_highpage_movable(vma, vmf->address);page_add_new_anon_rmap(page, vma, vmf->address, false);
}
3.3.1.1 anon_vma_prepare
static inline int anon_vma_prepare(struct vm_area_struct *vma)
{if (likely(vma->anon_vma))return 0;return __anon_vma_prepare(vma);
}
/*** __anon_vma_prepare - attach an anon_vma to a memory region* @vma: the memory region in question** This makes sure the memory mapping described by 'vma' has* an 'anon_vma' attached to it, so that we can associate the* anonymous pages mapped into it with that anon_vma.** The common case will be that we already have one, which* is handled inline by anon_vma_prepare(). But if* not we either need to find an adjacent mapping that we* can re-use the anon_vma from (very common when the only* reason for splitting a vma has been mprotect()), or we* allocate a new one.** Anon-vma allocations are very subtle, because we may have* optimistically looked up an anon_vma in page_lock_anon_vma_read()* and that may actually touch the rwsem even in the newly* allocated vma (it depends on RCU to make sure that the* anon_vma isn't actually destroyed).** As a result, we need to do proper anon_vma locking even* for the new allocation. At the same time, we do not want* to do any locking for the common case of already having* an anon_vma.** This must be called with the mmap_lock held for reading.*/
int __anon_vma_prepare(struct vm_area_struct *vma)
{struct mm_struct *mm = vma->vm_mm;struct anon_vma *anon_vma, *allocated;struct anon_vma_chain *avc;//1. 分配 AVC 结构avc = anon_vma_chain_alloc(GFP_KERNEL);if (!avc)goto out_enomem;//2. 查找可合并的 anon_vmaanon_vma = find_mergeable_anon_vma(vma);allocated = NULL;if (!anon_vma) {//若找不到可复用的 AV,则分配一个新的 anon_vma 结构。anon_vma = anon_vma_alloc();}//3. 锁定并设置 VMA 的 anon_vmaanon_vma_lock_write(anon_vma);/* page_table_lock to protect against threads */spin_lock(&mm->page_table_lock);if (likely(!vma->anon_vma)) {vma->anon_vma = anon_vma;//将 AVC 同时加入 VMA 的链表和 AV 的红黑树。anon_vma_chain_link(vma, avc, anon_vma);/* vma reference or self-parent link for new root */anon_vma->degree++;allocated = NULL;avc = NULL;}return 0;
}

__anon_vma_prepare函数的作用是确保一个虚拟内存区域(VMA, vm_area_struct)关联到一个 anon_vma 结构,用于管理匿名页(Anonymous Pages)的逆向映射(Reverse Mapping)。

为给定的 VMA 准备 anon_vma 结构,确保后续分配的匿名页能够正确建立反向映射关系。

(1)分配 AVC 结构

avc = anon_vma_chain_alloc(GFP_KERNEL);
if (!avc)goto out_enomem;
static inline struct anon_vma_chain *anon_vma_chain_alloc(gfp_t gfp)
{return kmem_cache_alloc(anon_vma_chain_cachep, gfp);
}

分配一个 anon_vma_chain 结构,用于后续连接 VMA 和 AV。

(2)查找可合并的 anon_vma

anon_vma = find_mergeable_anon_vma(vma);
allocated = NULL;
if (!anon_vma) {anon_vma = anon_vma_alloc();if (unlikely(!anon_vma))goto out_enomem_free_avc;allocated = anon_vma;
}

a:find_mergeable_anon_vma(vma):尝试查找相邻 VMA 中可复用的 anon_vma(例如因 mprotect() 分割的 VMA 可能共享同一个 AV)。

/** find_mergeable_anon_vma is used by anon_vma_prepare, to check* neighbouring vmas for a suitable anon_vma, before it goes off* to allocate a new anon_vma.  It checks because a repetitive* sequence of mprotects and faults may otherwise lead to distinct* anon_vmas being allocated, preventing vma merge in subsequent* mprotect.*/
struct anon_vma *find_mergeable_anon_vma(struct vm_area_struct *vma)
{struct anon_vma *anon_vma = NULL;/* Try next first. *///优先检查后向相邻 VMA(vm_next)if (vma->vm_next) {//:判断该相邻 VMA 的 anon_vma 是否可以复用。anon_vma = reusable_anon_vma(vma->vm_next, vma, vma->vm_next);if (anon_vma)return anon_vma;}/* Try prev next. *///检查前向相邻 VMA(vm_prev)if (vma->vm_prev)//检查前一个 VMA 的 anon_vma 是否可以复用。anon_vma = reusable_anon_vma(vma->vm_prev, vma->vm_prev, vma);/** We might reach here with anon_vma == NULL if we can't find* any reusable anon_vma.* There's no absolute need to look only at touching neighbours:* we could search further afield for "compatible" anon_vmas.* But it would probably just be a waste of time searching,* or lead to too many vmas hanging off the same anon_vma.* We're trying to allow mprotect remerging later on,* not trying to minimize memory used for anon_vmas.*/return anon_vma;
}

该函数用于在分配新的 anon_vma 之前,检查当前 VMA(vm_area_struct)的相邻 VMA,看看是否可以复用它们关联的 anon_vma。

此vma能与前后的vma进行合并,系统就不会为此vma创建anon_vma,而是这两个vma共用一个anon_vma,但是会创建一个anon_vma_chain,如下:
在这里插入图片描述
图片来自于:https://www.cnblogs.com/tolimit/p/5398552.html

这种情况,如果新的vma能够与前后相似vma进行合并,则不会为这个新的vma创建anon_vma结构,而是将此新的vma的anon_vma指向能够合并的那个vma的anon_vma。不过内核会为这个新的vma建立一个anon_vma_chain,链入这个新的vma中,并加入到新的vma所指的anon_vma的红黑树中。在这种情况中,匿名页的反向映射就能够找到新的vma。

为什么优先检查 vm_next?
内存局部性:在大多数情况下,mprotect 或 mmap 操作更倾向于向后扩展内存,因此 vm_next 更有可能共享相同的 anon_vma。

anon_vma 复用条件:
两个 VMA 必须属于同一个进程(mm_struct)。
它们的 anon_vma 必须未被其他进程共享(否则需要 COW 处理)。

static struct anon_vma *reusable_anon_vma(struct vm_area_struct *old, struct vm_area_struct *a, struct vm_area_struct *b)
{//检查两个VMA(a和b)是否兼容。if (anon_vma_compatible(a, b)) {struct anon_vma *anon_vma = READ_ONCE(old->anon_vma);//检查anon_vma是否可复用if (anon_vma && list_is_singular(&old->anon_vma_chain))return anon_vma;}return NULL;
}

该函数用于检查给定的VMA(old)的anon_vma是否可以安全地被新的VMA(a或b)复用,以避免不必要的anon_vma分配。

list_is_singular(&old->anon_vma_chain):检查old的anon_vma_chain是否只包含一个节点。
意义:如果anon_vma_chain是单例(singular),说明old的anon_vma未被其他进程共享(即未经过fork),可以安全复用。

避免共享anon_vma的复杂情况:
如果anon_vma_chain包含多个节点,说明anon_vma已被多个进程共享(例如通过fork)。
这种情况下复用anon_vma可能导致写时复制(COW)问题,因此必须分配新的anon_vma。

b:若找不到可复用的 AV,则分配一个新的 anon_vma 结构。

static inline struct anon_vma *anon_vma_alloc(void)
{struct anon_vma *anon_vma;anon_vma = kmem_cache_alloc(anon_vma_cachep, GFP_KERNEL);if (anon_vma) {atomic_set(&anon_vma->refcount, 1);anon_vma->degree = 1;	/* Reference for first vma */anon_vma->parent = anon_vma;/** Initialise the anon_vma root to point to itself. If called* from fork, the root will be reset to the parents anon_vma.*/anon_vma->root = anon_vma;}return anon_vma;
}

在这里插入图片描述
图片来自于:https://www.cnblogs.com/tolimit/p/5398552.html

之后再次访问此vma中不属于已经映射好的页的其他地址时,就不需要再次为此vma创建anon_vma和anon_vma_chain结构了。

(3)锁定并设置 VMA 的 anon_vma

anon_vma_lock_write(anon_vma);
spin_lock(&mm->page_table_lock);
if (likely(!vma->anon_vma)) {vma->anon_vma = anon_vma;anon_vma_chain_link(vma, avc, anon_vma);anon_vma->degree++;allocated = NULL;avc = NULL;
}
spin_unlock(&mm->page_table_lock);
anon_vma_unlock_write(anon_vma);

设置逻辑:
若 VMA 尚未关联 AV(vma->anon_vma == NULL),则:
将新 AV 赋值给 vma->anon_vma。
通过 anon_vma_chain_link() 将 AVC 同时加入 VMA 的链表和 AV 的红黑树。
增加 AV 的引用计数 degree,表示有新的 VMA 关联到该 AV。

3.3.1.2 alloc_zeroed_user_highpage_movable
// v5.15/source/arch/x86/include/asm/page.h#define alloc_zeroed_user_highpage_movable(vma, vaddr) \alloc_page_vma(GFP_HIGHUSER_MOVABLE | __GFP_ZERO, vma, vaddr)

物理页分配:分配一个用户态物理页,从伙伴系统中申请一个匿名页,并初始化为全零。

3.3.1.3 page_add_new_anon_rmap
/*** page_add_new_anon_rmap - add pte mapping to a new anonymous page* @page:	the page to add the mapping to* @vma:	the vm area in which the mapping is added* @address:	the user virtual address mapped* @compound:	charge the page as compound or small page** Same as page_add_anon_rmap but must only be called on *new* pages.* This means the inc-and-test can be bypassed.* Page does not have to be locked.*/
void page_add_new_anon_rmap(struct page *page,struct vm_area_struct *vma, unsigned long address, bool compound)
{//如果是复合页(Compound Page,如THP),计算子页数量;否则为1。int nr = compound ? thp_nr_pages(page) : 1;//标记页面为SwapBacked__SetPageSwapBacked(page);if (compound) {/* increment count (starts at -1) */atomic_set(compound_mapcount_ptr(page), 0);} else {// 普通页处理/* increment count (starts at -1) */atomic_set(&page->_mapcount, 0);}//更新匿名页统计__mod_lruvec_page_state(page, NR_ANON_MAPPED, nr);//设置匿名页反向映射__page_set_anon_rmap(page, vma, address, 1);
}

该函数用于将新分配的匿名页添加到反向映射(Reverse Mapping, RMAP)系统中,是匿名页内存管理的核心操作之一。其主要作用:
建立反向映射:关联匿名页与对应的VMA(虚拟内存区域)。
更新计数状态:维护页的_mapcount、LRU状态等。
支持大页(THP):透明大页的特殊处理。

(1)标记页为交换支持(Swap-Backed)

__SetPageSwapBacked(page);

标记该物理页为 “可交换”(swap-backed),即可以被交换到磁盘。
匿名页默认支持交换,与文件映射页(如 mmap 的文件)区分开来。

(2)建立反向映射关系

__page_set_anon_rmap(page, vma, address, 1);

核心操作:调用 __page_set_anon_rmap 建立物理页与 VMA 的反向映射关系:
设置 page->mapping 指向 VMA 的 anon_vma(AV)。
设置 page->index 为虚拟地址在 VMA 中的偏移量(以页为单位)。
将物理页添加到 AV 的红黑树中(通过 anon_vma_chain),使该物理页与 VMA 关联。

(3)为什么_mapcount初始值为-1?
-1:表示页未被映射。
0:表示被1个进程映射。
N:被N+1个进程映射(如共享内存)。

__page_set_anon_rmap
/*** __page_set_anon_rmap - set up new anonymous rmap* @page:	Page or Hugepage to add to rmap* @vma:	VM area to add page to.* @address:	User virtual address of the mapping	* @exclusive:	the page is exclusively owned by the current process*/
static void __page_set_anon_rmap(struct page *page,struct vm_area_struct *vma, unsigned long address, int exclusive)
{//获取 VMA 的匿名内存映射对象(anon_vma)struct anon_vma *anon_vma = vma->anon_vma;BUG_ON(!anon_vma);//如果页面已经是匿名页面,则直接返回if (PageAnon(page))return;/** If the page isn't exclusively mapped into this vma,* we must use the _oldest_ possible anon_vma for the* page mapping!*///1(独占):新分配的匿名页,仅属于当前进程(如do_anonymous_page分配的页)。if (!exclusive)//0(共享):页可能被多个进程共享(如fork后的COW页),需使用anon_vma层次结构的根节点(root)统一管理。anon_vma = anon_vma->root;/** page_idle does a lockless/optimistic rmap scan on page->mapping.* Make sure the compiler doesn't split the stores of anon_vma and* the PAGE_MAPPING_ANON type identifier, otherwise the rmap code* could mistake the mapping for a struct address_space and crash.*///设置匿名页标识//通过将指针值加上PAGE_MAPPING_ANON(为0x1),利用指针的最低有效位作为标记位。//这样mapping字段既能存储anon_vma地址,又能标识页类型(匿名页 vs. 文件页)。anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;WRITE_ONCE(page->mapping, (struct address_space *) anon_vma);//计算虚拟地址在VMA中的线性页偏移page->index = linear_page_index(vma, address);
}

该函数是匿名页反向映射(Reverse Mapping, RMAP)的核心底层实现,负责将匿名页与对应的虚拟内存区域(VMA)关联起来。其核心功能包括:
设置匿名页标识:通过mapping字段标记页面为匿名页(PAGE_MAPPING_ANON)。
关联anon_vma:将页面的mapping指向VMA所属的anon_vma结构。
记录虚拟地址偏移:通过index字段保存地址在VMA中的位置。

内存屏障的重要性:
代码注释中提到的 page_idle 函数会通过 page->mapping 进行无锁的乐观反向映射扫描
通过 WRITE_ONCE 和特殊指针标记,确保对 page->mapping 的写入是原子的,防止其他代码误判

共享页面的处理:
当页面被多个进程共享时(non-exclusive),使用 anon_vma->root 确保所有进程都能通过根对象找到这个页面
这是处理共享匿名内存(如 fork 后的父子进程共享内存)的关键机制

特殊指针标记:
将 PAGE_MAPPING_ANON 作为指针偏移量,创建一个特殊的 struct address_space* 指针
这种设计允许内核通过检查指针值来区分不同类型的映射(文件映射 vs 匿名映射)

static inline pgoff_t linear_page_index(struct vm_area_struct *vma,unsigned long address)
{pgoff_t pgoff;if (unlikely(is_vm_hugetlb_page(vma)))return linear_hugepage_index(vma, address);pgoff = (address - vma->vm_start) >> PAGE_SHIFT;pgoff += vma->vm_pgoff;return pgoff;
}

该函数用于计算给定虚拟地址 (address) 在虚拟内存区域 (vma) 中的线性页索引(pgoff_t 类型),即确定该地址在 VMA 所映射的文件或匿名内存中的页偏移量。

普通页面的页偏移量计算:

pgoff = (address - vma->vm_start) >> PAGE_SHIFT;
pgoff += vma->vm_pgoff;

计算虚拟地址相对于 VMA 起始地址的偏移量(address - vma->vm_start)
将字节偏移量右移 PAGE_SHIFT 位,转换为页偏移量(因为 PAGE_SHIFT 通常是 12,表示 4KB 页面)
将结果加上 VMA 的起始页偏移量(vma->vm_pgoff),得到最终的页索引。

vm_pgoff:表示 VMA 映射的文件或设备内存的起始页号(对匿名页通常为 0)。
如下图所示:
在这里插入图片描述

3.3.2 父进程fork子进程创建匿名页面

父进程通过fork系统调用创建子进程时,子进程会复制父进程的进程地址空间VMA数据结构作为自己的进程地址空间,并且会复制父进程的PTE页表项内容到子进程的页表中,实现父子进程共享页表。

多个不同子进程中的虚拟页面会同时映射到同一个物理页面,另外多个不相干进程虚拟页面也可以通过KSM机制映射到同一个物理页面。

四、文件页反向映射

4.1 文件页反向映射全流程

同匿名页一样,可能会有多个进程的VMA同时共享一个文件映射页。而进程文件页的反向映射是通过一个与文件相关的结构address_space来进行维护的。

文件页的反向映射通常通过mapping和index来关联到文件的页缓存。当内核需要找到某个物理页对应的文件时,可以通过mapping找到对应的address_space,然后通过index计算出该页在文件中的位置。

struct page {union {struct {	/* Page cache and anonymous pages *//* See page-flags.h for PAGE_MAPPING_FLAGS */struct address_space *mapping;  //file page cachepgoff_t index;		/* Our offset within mapping. */};}union {		/* This union is 4 bytes in size. *//** If the page can be mapped to userspace, encodes the number* of times this page is referenced by a page table.*/atomic_t _mapcount;};
}

page->index:表示该页在映射的文件中的偏移量(以页为单位,即page index)。注意, page->index是页偏移,而不是字节偏移。它表示该页在文件中的第几个页(从0开始)。

struct address_space {struct inode		*host;......struct rb_root_cached	i_mmap;
}

每个文件对应一个 address_space
i_mmap:区间树 (Interval Tree),i_mmap 存储的是所有映射该文件的 vm_area_struct(VMA)结构。一个文件可能会被映射到多个进程的多个VMA中,所有的这些VMA都被挂入到i_mmap指向的区间树 (Interval Tree)中。

struct vm_area_struct {unsigned long vm_start;		/* Our start address within vm_mm. */......struct mm_struct *vm_mm;	/* The address space we belong to. */....../* Information about our backing store: */unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE......
}

vma->vm_pgoff:表示该VMA(虚拟内存区域)在映射的文件中的起始页偏移(同样以页为单位)。即VMA映射的文件部分从文件的第vma->vm_pgoff页开始。

计算页在VMA中的页内偏移:对于给定的页,如果它属于这个VMA,那么它在VMA中的页内偏移为:page->index - vma->vm_pgoff。

计算虚拟地址:该页在VMA中的虚拟地址为:vma->vm_start + (page->index - vma->vm_pgoff) * PAGE_SIZE。
在这里插入图片描述

页表项(PTE):有了虚拟地址(virtual address)和地址空间(vma->vm_mm),我们可以通过查询页表来找到对应的PTE。

当需要查找映射某个文件页的所有进程PTE时:

  1. 通过page->mapping获取address_space
  2. 获取页在文件中的偏移pgoff = page->index
  3. 遍历address_space->i_mmap中的所有VMA(使用vma_interval_tree_foreach宏)。
  4. 对于每个VMA,计算该页在VMA中的虚拟地址:address = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT)
  5. 检查address是否在[vma->vm_start, vma->vm_end)之间。如果不是,跳过。
  6. 如果地址有效,那么我们就得到了该页在进程地址空间中的虚拟地址,以及所属的mm_struct(即vma->vm_mm)。
  7. 然后,通过该虚拟地址和mm_struct,我们可以查询页表找到PTE。

在这里插入图片描述
如下图所示:
在这里插入图片描述

获取页表项 (PTE):
(1)确定文件内偏移
page->index: 文件内的页偏移(以页为单位)
vma->vm_pgoff: VMA 在文件中的起始页偏移

(2)计算 VMA 内的页偏移

vma_page_offset = page->index - vma->vm_pgoff

(3)计算虚拟地址

virtual_address = vma->vm_start + (vma_page_offset << PAGE_SHIFT)= vma->vm_start + (page->index - vma->vm_pgoff) * PAGE_SIZE

(4) 获取页表项 (PTE)

pte = lookup_pte(vma->vm_mm, virtual_address)

4.2 page_add_file_rmap

文件页反向映射通过page_add_file_rmap来完成:

/*** page_add_file_rmap - add pte mapping to a file page* @page: the page to add the mapping to* @compound: charge the page as compound or small page** The caller needs to hold the pte lock.*/
void page_add_file_rmap(struct page *page, bool compound)
{int i, nr = 1; // nr 用于记录需要增加统计的页数(通常是1,大页时可能更多)// 如果 compound 为 true,但 page 不是透明大页,则触发内核 BUG。VM_BUG_ON_PAGE(compound && !PageTransHuge(page), page);// 获取此页所属内存控制组(memcg)的锁,确保对 memcg 统计的修改是原子的。lock_page_memcg(page);// 如果 compound 为 true 且 page 确实是一个透明大页 (THP)if (compound && PageTransHuge(page)) {int nr_pages = thp_nr_pages(page); // 获取大页包含的页数(通常是 512 个 4KB 页)// 遍历大页中的每一个子页for (i = 0, nr = 0; i < nr_pages; i++) {// 增加子页的 _mapcount。如果增加前 _mapcount 是 -1(表示之前无映射),则 atomic_inc_and_test 返回 true。// nr 记录有多少个子页是从“无映射”状态变为“有映射”状态的。if (atomic_inc_and_test(&page[i]._mapcount))nr++;}// 增加大页的复合映射计数。如果增加后计数不为 0,说明之前已有映射,跳转到 out。if (!atomic_inc_and_test(compound_mapcount_ptr(page)))goto out;// 根据页是否被标记为可换出(SwapBacked),更新不同的 LRU 统计。// NR_SHMEM_PMDMAPPED: 共享内存大页映射数// NR_FILE_PMDMAPPED: 文件页大页映射数if (PageSwapBacked(page))__mod_lruvec_page_state(page, NR_SHMEM_PMDMAPPED, nr_pages);else__mod_lruvec_page_state(page, NR_FILE_PMDMAPPED, nr_pages);}// 【处理普通页或非 compound 情况】else {// 【处理复合页但非大页的情况】如果 page 是复合页(如普通大页)且有映射(page_mapping(page) 不为空)if (PageTransCompound(page) && page_mapping(page)) {struct page *head = compound_head(page); // 获取复合页的头页// 调试警告:如果子页被锁定但头页未被锁定,可能有问题。VM_WARN_ON_ONCE(!PageLocked(page));// 【关键标志】设置头页的 DoubleMap 标志。// 这个标志用于优化,表示该复合页被映射了,避免在某些操作(如 munmap)中重复遍历所有子页。SetPageDoubleMap(head);// 如果子页被 mlock 锁定,则清除头页的 mlock 标志(? 这行逻辑可能需要结合上下文理解,通常 mlock 是针对整个复合页的)if (PageMlocked(page))clear_page_mlock(head);}// 【增加普通页的映射计数】// 如果增加后 _mapcount 不为 0,说明之前已有映射,跳转到 out。if (!atomic_inc_and_test(&page->_mapcount))goto out;// nr 保持为 1(初始值)}// 增加“已映射文件页”的全局统计。// 这里只在 _mapcount 从 -1 变为 0 时才增加(即从“无映射”到“首次有映射”)。// nr 的值在大页分支中可能大于 1,在普通页分支中为 1。__mod_lruvec_page_state(page, NR_FILE_MAPPED, nr);out:// 释放之前获取的 memcg 锁。unlock_page_memcg(page);
}

page_add_file_rmap 函数是 Linux 内核内存管理子系统中的一个核心函数。它的主要作用是增加一个文件页(file-backed page)的映射计数。

当一个进程通过 mmap 或其他方式将一个文件映射到其虚拟地址空间时,内核需要记录这个映射关系。page_add_file_rmap 就是在这个过程中被调用的,它会:
(1)增加 page->_mapcount:这个计数器记录了有多少个不同的虚拟地址(PTEs)映射到了这个物理页框。每次增加一个映射,这个计数就加一。
(2)更新内存统计信息:将该页计入全局的“已映射文件页”统计中(NR_FILE_MAPPED)。
(3)处理大页(THP)和特殊标志:对于透明大页(Transparent Huge Page, THP)或共享内存页,进行额外的计数和标志设置。

调用该函数时,调用者必须持有 PTE 锁(page table entry lock)。这是为了保证在修改页表项(PTE)和更新页的映射计数(_mapcount)这两个操作之间的原子性,防止竞态条件。

核心思想:维护物理页与虚拟地址映射关系的“引用计数”,以便在后续操作(如页面回收、写时复制)时,内核能知道这个页被多少个地方使用。

参考资料

Linux 5.15

https://www.cnblogs.com/LoyenWang/p/12164683.html
https://zhuanlan.zhihu.com/p/627558618
https://blog.csdn.net/u010923083/article/details/116456497
https://blog.csdn.net/u012489236/article/details/114734823
https://www.zhihu.com/question/60110786
http://www.wowotech.net/memory_management/reverse_mapping.html
https://zhuanlan.zhihu.com/p/564867734
https://www.cnblogs.com/arnoldlu/p/8335483.html

http://www.dtcms.com/a/309375.html

相关文章:

  • 每天一点跑步运动小知识
  • 使用gcc代替v语言的tcc编译器提高编译后二进制文件执行速度
  • 分布在背侧海马体CA1区域的位置细胞(place cells)对NLP中的深层语义分析的积极影响和启示
  • Ⅹ—6.计算机二级综合题23---26套
  • CIFAR10实战
  • gitlab+jenkins的ci/cd部署
  • 报错[Vue warn]: Failed to resolve directive: else如何解决?
  • PyTorch分布式训练:从入门到精通
  • 什么是CI/CD?
  • python学智能算法(三十))|SVM-KKT条件的数学理解
  • 测试平台如何重塑CI/CD流程中的质量协作新范式
  • LLM Prompt与开源模型资源(1)提示词工程介绍
  • 全新发布|知影-API风险监测系统V3.3,AI赋能定义数据接口安全新坐标
  • HTML无尽射击小游戏包含源码,纯HTML+CSS+JS
  • Redis 中 ZipList 的级联更新问题
  • Dockerfile详解 笔记250801
  • fingerprintjs/botd爬虫监听
  • Ajax笔记
  • SD-WAN在煤矿机械设备工厂智能化转型中的应用与网络架构优化
  • ansible.cfg 配置文件的常见配置项及其说明
  • AI量化模型解析黄金3300关口博弈:市场聚焦“非农数据”的GRU-RNN混合架构推演
  • 【立体标定】圆形标定板标定python实现
  • MySQL学习从零开始--第六部分
  • PyTorch 分布式训练全解析:从原理到实践
  • 数据仓库、数据湖与湖仓一体技术笔记
  • 第三章 网络安全基础(一)
  • OPENGLPG第九版学习 - 纹理与帧缓存 part2
  • linux中posix消息队列的使用记录
  • Java与Kotlin中“==“、“====“区别
  • 解锁 Grok-4 —— 技术架构、核心能力与API获取指南