欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 手游 > 《linux2.4内存管理》第 4 章 进程地址空间

《linux2.4内存管理》第 4 章 进程地址空间

2025/9/10 3:05:42 来源:https://blog.csdn.net/m0_52043808/article/details/148607104  浏览:    关键词:《linux2.4内存管理》第 4 章 进程地址空间

虚拟内存的重要优势是让每个进程拥有专属虚拟地址空间,可经操作系统映射到物理内存。

内核空间与用户空间分配差异 :内核空间分配不受 CPU 运行进程影响,能快速满足,具全局性(vmaalloc() 除外,因其易引发页表同步问题)。进程通过页表项指针指向只读全局全零页面在其线性地址空间预留空间,写操作触发缺页中断,系统分配、初始化新全零页面并标记可写,新页面外观与原全零页面一致。

用户空间特性:用户空间非固定不变,每次上下文切换(除 4.3 节提到的 TLB 切换)后内容可能变化。内核需捕获用户空间异常并定位错误,相关内容在 5.5 节讨论。

章节内容规划:先探讨线性地址空间构成及各部分用途,再讨论进程相关结构及其配置、初始化与释放。接着介绍进程地址空间私有区域创建及相关函数,涉及异常处理、缺页中断等。最后阐述内核与用户空间间数据正确拷贝方式。

4.1 线性地址空间

从用户视角看,地址空间是平坦的线性地址空间,但内核视角下不同。地址空间分两部分:随上下文切换改变的用户空间部分,以及保持不变的内核空间部分,二者分界由PAGE_OFFSET决定,在x86中其值为0xC0000000 ,这意味着有3GB空间供用户使用,内核可映射剩余1GB空间。

系统为载入内核,需保留从PAGE_OFFSET开始的8MB空间(两个PGD定位的内存大小),内核映象在内核页表初始化时放入此8MB空间,之后是供UMA体系结构使用的mem_map数组 。mem_map数组通常位于16MB位置,为避免使用ZONE_DMA ,其在不同体系结构中各部分位置分散。

PAGE_OFFSETVMALLOC_START - VMALLOC_OFFSET是物理内存映射区域,大小由可用RAM决定,通过页表项映射物理内存到PAGE_OFFSET开始的虚拟地址,为防止边界错误,在物理内存映射和vmalloc地址空间间有大小为VMALLOC_OFFSET的空隙 ,如32MB的x86系统中,VMALLOC_START等于PAGE_OFFSET + 0x2000000 + 0x00800000

在内小内存系统,vmalloc可在连续虚拟地址空间表示非连续内存分配,减去2个页面空隙后供vmalloc使用;大内存系统中,vmalloc区域更大,减去2个页面空隙后还引入2个区域,第1个是从PKMAP_BASEPKMAP_ENDkmap()kmap()用于将高端内存页面映射到低端内存;第2个是从FIXADDR_STARTFIXADDR_TOP的固定虚拟地址映射区域,供编译时需知道虚拟地址的子系统使用,如高级可编程中断控制器(APIC) ,在x86中静态地址为0xFFFFE000 ,大小通过编译时的__FIXADDR_SIZE计算。

vmallockmap及固定映射区域限制了ZONE_NORMAL大小,运行中的内核需这些函数,在地址空间顶端至少保留VMALLOC_RESERVE大小区域,x86中为128MB ,这使得ZONE_NORMAL通常只有896MB ,vmalloc区域由线性地址空间上端1GB减去保留的128MB所得。

🌍 思考:线性地址空间和虚拟地址空间有什么关系?

本质上讲,线性地址空间和进程的虚拟地址空间是一种映射关系中的不同视角,它们共同体现了现代操作系统中虚拟内存机制的两个核心角色:统一抽象实际执行

视角

线性地址空间

进程虚拟地址空间

所属层次

操作系统内核层

用户进程视角

结构特点

是连续的一段虚拟地址范围,通常是所有进程共享的逻辑视图,分为用户区和内核区

是每个进程独立的一组地址映射,用户认为是从0开始的扁平空间

管理单位

内核通过页表映射线性地址到物理地址

用户通过系统调用访问虚拟地址,最终由页表转换为物理地址

地址有效性

对于CPU来说,是访问内存的第一层翻译结果

对于进程来说,是访问内存的抽象接口

举个例子说明:

  • 每个用户进程都认为自己的地址从 0x00000000 开始,一直到 0xBFFFFFFF(假设3GB),这是它的虚拟地址空间
  • 当进程执行某条访问地址 0x08048000 的指令时,CPU通过段机制或现代CPU的扁平模型直接得到线性地址0x08048000
  • 然后通过页表将这个线性地址转换为对应的物理地址。

💡 线性地址是 CPU 统一访问的中间形式,虚拟地址则是进程对内存的抽象认知。

📌 PAGE_OFFSET 的意义

在 x86 架构中,内核定义 PAGE_OFFSET = 0xC0000000,这意味着:

  • 虚拟地址 0x000000000xBFFFFFFF:每个进程私有,称为用户空间
  • 虚拟地址 0xC00000000xFFFFFFFF:系统统一分配给内核空间,所有进程共享。

这段内核空间也属于线性地址空间的一部分,由内核统一管理,用于映射物理内存、vmalloc分配、IO映射等用途。

虚拟地址空间是操作系统给每个进程看的抽象世界;

线性地址空间是 CPU 实际执行指令时理解的逻辑世界;

二者的转换通过段表(如果使用段机制)+ 页表共同完成。

🧠 再思考:

这套机制的核心价值在于:

  • 隔离性:每个进程拥有独立的虚拟空间。
  • 共享性:通过映射同一物理页实现进程间共享或内核访问。
  • 灵活性:内核可通过线性地址映射物理资源,不暴露底层细节。

需要我再配一张结构图说明关系吗?

4.2 地址空间的管理

管理结构

  • 进程可用地址空间由mm_struct管理。每个进程地址空间包含多个按页面对齐的内存区域,这些区域不重叠,代表地址集合,包含用于保护或其他目的且相互关联的页面。
  • 内存区域由struct vm_area_struct管理。一个区域可表示malloc()使用的进程堆、内存映射文件(如共享库)、mmap()分配的匿名内存区域等。区域中的页面可能未分配、已分配或常驻内存且可被交换出去。
  • 若区域是文件映射,其vm_file字段会被设置,通过查看vm_file→f_dentry→d_inode→i_mapping ,可获取该区域代表的地址空间内容,此地址空间包含与文件系统相关的特定信息,用于磁盘上基于页面的操作。

相关系统调用(表4.1 内存区域相关的系统调用 )

fork():创建具有新地址空间的进程,页面标记为写时复制(COW),页面中断前由两个进程共享,出错时为进程复制COW页面,有时也被称为使一个COW页面失效。

clone():允许创建共享上下文的新进程(Linux中线程实现方式)。若未设置CLONE_VM位,clone()将创建新地址空间,作用和fork()一样 。

mmap():在进程线性地址空间中创建一个新区域。

mremap():重映射一个内存区域或改变其大小。若虚拟地址空间对映射不可用,移走该区域,除非该移动操作被调用者所禁止。 mummap():销毁部分或所有的区域。若已解除映射的区域位于已存在区域中间,该存在区域就分裂成两个单独的区域。

shmat():关联共享内存段到进程地址空间。

shmdt():从地址空间移除共享内存段。

execve():载入一个新的可执行文件,替换当前的地址空间。

exit():销毁一个地址空间和所有的区域。

🌍 思考:这些系统调用和内存区域有什么关系?

这些系统调用与内存区域(vm_area_struct)之间的关系可以概括为:

在 Linux 中,一个进程的虚拟地址空间被组织为一组不重叠的内存区域(vm_area_struct 结构体管理),这些区域由 mm_struct 统一管理。

系统调用提供了创建、修改、映射或销毁这些内存区域的机制,因此它们实质上是对地址空间数据结构的动态管理操作

🔄 系统调用和内存区域的关系:

系统调用

作用

vm_area_struct 的关系

fork()

创建新进程

拷贝 mm_structvm_area_struct 指针共享,但物理页设置为 COW

clone()

创建线程或新进程

若设置 CLONE_VM,多个线程共享同一 mm_struct;否则行为如 fork()

mmap()

创建新的内存区域

分配新的 vm_area_struct,插入到 mm_struct 的区域列表或红黑树中

mremap()

调整区域大小

扩展/移动原 vm_area_struct,或者新建一个区域替换原来的

munmap()

销毁区域

删除一个或多个 vm_area_struct,并释放相应的页表映射

shmat()

映射共享内存段

创建一个新的 vm_area_struct,并设置其 vm_file 指向共享内存文件描述符

shmdt()

解除共享内存映射

删除对应 vm_area_struct,解除映射

execve()

加载新程序

销毁当前所有 vm_area_struct,重新创建用于代码段、堆栈、库的区域

exit()

退出进程

销毁整个 mm_struct 及其所有 vm_area_struct,释放所有虚拟内存资源

🗂️ 数据结构字符图(mm_structvm_area_struct 的关系)

进程 task_struct|--> mm (指向 mm_struct)||--> mmap ------------------> [vm_area_struct] (代码段)|                             start: 0x08040000|                             end:   0x08050000|--> [vm_area_struct] (数据段)|     start: 0x08050000|     end:   0x08060000||--> [vm_area_struct] (堆 heap,由 malloc() 等分配)|     start: 0x08060000|     end:   0x08100000||--> [vm_area_struct] (mmap 文件映射,如共享库)|     start: 0x40000000|     end:   0x40100000||--> [vm_area_struct] (栈 stack)start: 0xbfffe000end:   0xc0000000其他字段:||--> pgd      -----> 页表|--> map_count: 区域数量|--> total_vm: 总虚拟内存页数|--> flags: 内存策略|--> mm_rb: 所有 vm_area_struct 的红黑树(用于快速查找)

说明:mmap 是一个链表头,连接所有 vm_area_struct;同时它们也存在于 mm_rb 这棵红黑树中用于快速定位虚拟地址所在的区域。

总结

系统调用是用户态操作虚拟地址空间中内存区域(vm_area_struct)的接口,每个调用对应着创建、修改、销毁或映射某些区域。而 mm_struct 是这些区域的统一容器,维护它们的组织结构(链表 + 红黑树)。

4.3 进程地址空间描述符

概述
进程地址空间由mm_struct结构描述,一个进程只有一个mm_struct结构,且在进程用户空间中由多个线程共享,线程通过是否指向同一个mm_struct判定。内核线程一般不需要mm_struct,因为它们不会发生缺页中断或访问用户空间,仅vmalloc空间的缺页中断是例外,且task_struct→mm字段总为NULL 。某些任务如引导空闲任务,mm_struct永不设置。

延迟TLB技术
因TLB刷新开销大,在像PPC这样的体系结构中,未访问用户空间的进程进行TLB刷新无意义,Linux采用“延迟TLB”技术避免。通过借用前个任务的mm_struct,放入task_struct→active_mm ,避免调用switch_mm()刷新TLB 。进入延迟TLB时,SMP上调用enter_lazy_tlb()防止mm_struct被SMP处理器共享,UP机器上是个空操作;进程退出且等待被父进程回收时,调用start_lazy_tlb()函数。

mm_struct结构及字段含义

定义在<linux/sched.h> ,结构包含众多字段:

struct mm_struct {struct vm_area_struct *mmap;rb_root_t mm_rb;struct vm_area_struct *mmap_cache;pgd_t * pgd;atomic_t mm_users;atomic_t mm_count;int map_count;struct rw_semaphore mmap_sem;spinlock_t page_table_lock;struct list_head mmlist;unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;unsigned long rss, total_vm, locked_vm;unsigned long def_flags;unsigned long cpu_vm_mask;unsigned long swap_address;unsigned dumpable:1;mm_context_t context;
};
- **字段含义**:- `mmap` :地址空间中所有VMA(虚拟内存区域)的链表首部。- `mm_rb` :VMA排列在链表中且存于红黑树以加快查找,该字段表示树的根部。- `mmap_cache` :最后一次通过`find_vma()`找到的VMA存放处。- `pgd` :全局目录表的起始地址。- `mm_users` :访问用户空间部分的用户计数值。- `mm_count` :匿名用户计数值,数值1代表真实用户。- `map_count` :正被使用的vma数量。- `mmap_sem` :读写保护锁,通过`down_read()`获取信号量,写操作需`down_write()` ,更新VMA链表后获取`page_table_lock`锁。- `page_table_lock` :保护`mm_struct`中大部分字段,防止驻留集大小(RSS)计数和VMA被修改。- `mmlist` :所有`mm_struct`结构通过它链接在一起。- `start_code`等 :代码段、数据段、堆、栈、命令行参数、环境变量区域的起始和结束地址。- `rss` :驻留集大小,进程常驻内存页面数,全局零页面不包括在内。- `total_vm` :进程中所有vma区域的内存空间总和。- `locked_vm` :内存中被锁住的常驻页面数。- `def_flags` :只有`VM_LOCKED`一种可能值,指定默认情况下映射是否上锁。- `cpu_vm_mask` :代表SMP系统中所有CPU的掩码值,用于判定内部处理器中断(IPI)时CPU的TLB刷新操作。 - `swap_address` :换出整个进程时,记录最后一次被换出的地址。- `dumpable` :由`prctl()`设置,跟踪进程时有用。- `context` :跟体系结构相关的MMU上下文。 

初始化与分配

  • mm_init() :初始化mm结构,设置字段初始值,分配PGD,初始化自旋锁等。
  • allocate_mm() :从slab分配器中分配一个mm_struct
  • mm_alloc() :从slab中分配mm_struct ,并调用mm_init()初始化。

复制与销毁

  • copy_mm() :为新任务复制所需mm_struct的完美副本,fork过程中用到。
  • exit_mmap() :遍历mm_struct结构,解除所有与其关联的VMA映射。
  • free_mm() :返回mm结构给slab分配器。

分配、初始化与销毁描述符

分配描述符:Allocate_mm()是预处理宏,从slab分配器分配mm_structmm_alloc()从slab分配后调用mm_init()初始化。

初始化描述符:系统中第一个mm_struct通过init_mm()初始化,后续mm_struct以其为模板,通过copy_mm()复制创建,copy_mm()调用init_mm()初始化与具体进程相关字段。第一个mm_struct编译时通过宏INIT_MM()静态设置:

#define INIT_MM(name) \
{ \.mm_rb = RB_ROOT, \.pgd = swapper_pg_dir, \.mm_users = ATOMIC_INIT(2), \.mm_count = ATOMIC_INIT(1), \.mmap_sem = __RWSEM_INITIALIZER(name.mmap_sem), \.page_table_lock = SPIN_LOCK_UNLOCKED, \.mmlist = LIST_HEAD_INIT(name.mmlist), \
}

销毁描述符:新用户通过atomic_inc(&mm→mm_users)增加使用计数,mmput()减少计数。mm_users变为0时,通过exit_mmap()释放所有映射区域和页表;mm_count通过mmdrop()减1 ,变为0时,mm_struct被释放。

🌍 思考:线程通过是否指向同一个mm_struct判定?这句话是否精准?

在 Linux 中,如果多个线程共享地址空间,它们的 task_struct 中的 mm 字段会指向同一个 mm_struct,这表示它们确实是线程(共享虚拟地址空间)。但不精准的地方在于:

判断线程不能仅靠 mm_struct 是否相同,因为:

  • 内核线程mm 字段是 NULL,但它们也是线程。
  • 创建进程时(如 clone)可以控制是否共享 mm_struct,这时 CLONE_VM 标志才是决定是否共享地址空间(是否共享 mm_struct)的关键。

更准确的判断线程方式是:

  • 判断是否为同一个“线程组”的成员(即共享 tgid);
  • 或者是否是通过 clone(CLONE_VM | CLONE_THREAD) 创建的;
  • 更底层,可以通过 mm_struct(地址空间)+ signal_struct(信号处理)+ tgid(线程组 ID)共同判断是否是“线程”关系。

精准说法应为:

线程通常共享同一个 mm_struct,以实现共享虚拟地址空间,但判断是否为线程应基于是否在同一线程组(共享 tgid),以及是否共享 mm_struct、files_struct、signal_struct 等资源,由 clone 系统调用的参数决定。

示例:Linux 中线程 vs 进程的区别

| 属| 属性 | 进程(fork) | 线程(clone with CLONE_VM | CLONE_THREAD) |
|--------------|--------------|-----------------------------|
| mm_struct | 不共享 | 共享 |
| files_struct | 不共享 | 可共享 |
| signal_struct | 不共享 | 共享 |
| tgid | 不同 | 相同 |
| 地址空间 | 不同 | 相同 |

4.4 内存区域

区域表示与特性
进程地址空间一般只用部分分离区域,由 vm_area_struct 表示,区域间不交叉,代表相同属性和用途的地址集合,如只读共享库所在区域。进程已映射区域可在 /proc/PID/maps 查看(PID 为进程号) 。

vm_struct 结构声明

<linux/mm.h> 中声明,包含众多字段:

基本地址相关vm_start(起始地址)、vm_end(结束地址) 。

链接与查找相关vm_next按地址空间次序链接 VMA 链表)vm_rb(用于红黑树存储 VMA 加速查找) 。

保护与属性相关vm_page_prot(PTE 保护标志位)、vm_flags(保护和属性标志位,如 VM_READ 可读取、VM_WRITE 可写入等,具体见内存区域标志位表 ) 。

共享与操作相关vm_next_share(链接文件映射的 VMA 共享区域)、vm_pprev_share(辅助指针)、vm_ops(指向磁盘同步操作函数指针 ) 。

文件映射相关vm_file(指向被映射文件指针)、vm_pgoff(被映射文件里对齐页面偏移) 。

其他vm_raend(预读窗口结束地址)、vm_private_data(设备驱动私有数据存储)。

区域管理结构优势
所有区域按地址排序由 vm_next 链接成链表,查找空闲区间遍历链表即可。缺页中断时搜索指定区域,链表操作频繁,所以引入红黑树,平均搜索时间为 O(logN) ,红黑树节点地址左小右大,能快速定位区域。

4.4.1 内存区域的操作
  1. 操作函数及结构声明
    VMA 提供 open()close()nopage() 三个操作函数,通过 vm_operations_struct 结构体的 vma->vm_ops 提供这些操作。该结构体在 <linux/mm.h> 中声明:
struct vm_operations_struct {void (*open)(struct vm_area_struct * area);void (*close)(struct vm_area_struct * area);struct page * (*nopage)(struct vm_area_struct * area,unsigned long address,int unused);
};
  1. 函数调用时机及作用
    • open()close() :创建或删除区域时系统调用,如 system V 和 system v 的共享区域打开或关闭时,会执行额外操作,像 system V 中 open() 回调函数会递增共享段 VMA 数量。
    • nopage() :在发生缺页中断时,do_no_page() 会调用此回调函数。它负责定位页面在高速缓存中的位置,或分配新页面并填充请求的数据,然后返回该页面 。
  1. 通用文件映射操作
    多数被映射文件会用到 generic_file_vm_opsvm_operations_struct ,它只注册了名为 filemap_nopage()nopage() 函数,在 mm/filemap.c 中声明:
static struct vm_operations_struct generic_file_vm_ops = {.nopage = filemap_nopage,
};

🌍 思考:VMA 链表是什么?有什么用?

🌍 思考:VMA 链表是什么?有什么用?

VMA 链表(Virtual Memory Area list)是指每个进程的地址空间中,用于管理虚拟内存区域(VMA)的链表结构,每个节点是一个 struct vm_area_struct,描述一段具有相同属性的连续虚拟地址范围。

📌 什么是 VMA?

VMA(虚拟内存区域)代表一段具有相同属性的虚拟地址区间,比如:

  • 一段程序代码(可执行、只读)
  • 堆区(malloc分配的内存)
  • 栈区(函数调用栈)
  • mmap 映射的文件或匿名内存
  • 共享内存(shm)

一个进程的虚拟地址空间,通常由多个这样的区域拼接构成。

📌 VMA 链表是什么?

在 Linux 中,每个进程的地址空间由 mm_struct 管理,其中有一个成员:

struct vm_area_struct *mmap;

指向该进程的VMA 链表头部。每个 vm_area_struct 结构体中又有一个vm_next 指针,用于将多个 VMA 串联起来形成链表。此外还有一个红黑树 mm->mm_rb 来加速地址查找(比如页错误处理时用)。

VMA 链表的作用:

用途

描述

管理内存区域

每个 VMA 表示一段具有统一访问权限和用途的虚拟地址区间

页错误处理

当访问某地址页错误时,内核遍历(或红黑树查找)VMA 链表以确认访问是否合法,并分配物理页

权限控制

每个 VMA 有访问权限标志(如只读、可执行、不可写),内核依此判断是否允许读写执行

内存映射文件支持

VMA 可映射到文件,系统据此在访问时加载磁盘文件内容

内存回收

munmap()exit() 时,系统遍历 VMA 链表释放相关资源

             +--------------------+
mm_struct -> | mmap (VMA 链表头)  |------++--------------------+      |↓                   ↓+----------------------+   +-----------------------+| vm_area_struct #1    |-->| vm_area_struct #2     |---> ...| start = 0x08048000   |   | start = 0x40000000    || end   = 0x08050000   |   | end   = 0x40001000    || flags = r-xp         |   | flags = rw-p          || file  = /bin/bash    |   | anonymous mmap        |+----------------------+   +-----------------------+

总结:VMA 链表是 Linux 内核用来描述和管理进程虚拟内存布局的核心数据结构。每个 VMA 是一个虚拟内存段,VMA 链表是这些段的有序集合。内核通过它来实现权限管理、内存分配、页错误处理、文件映射等核心功能。

4.4.2 有后援文件/设备的内存区域

address_space 结构声明及字段
在有后援文件的区域中,vm_file引出address_space结构,其在 <linux/fs.h> 中声明,包含与文件系统相关信息:

struct address_space {struct list_head clean_pages; // 不需后援存储器同步的干净页面链表struct list_head dirty_pages; // 需要后援存储器同步的脏页面链表struct list_head locked_pages; // 在内存中被锁住的页面链表unsigned long nrpages; // 地址空间中正在被使用且常驻内存的页面数struct address_space_operations *a_ops; // 操作函数集struct inode *host; // 文件的索引节点struct vm_area_struct *i_mmap; // 使用 address_space 的私有映射链表struct vm_area_struct *i_mmap_shared; // 该地址空间中共享映射的 VMA 链表spinlock_t i_shared_lock; // 保护此结构的自旋锁int gfp_mask; // 调用 __alloc_pages() 所要用到的掩码
};

address_space_operations结构声明及字段
内存管理器需定期将信息写回磁盘,通过 a_ops 结构(address_space_operations 类型)调用相关函数,在 <linux/fs.h> 中声明:

struct address_space_operations {int (*writepage)(struct page *); // 把一个页面写到磁盘,写操作由具体文件系统代码完成int (*readpage)(struct file *, struct page *); // 从磁盘读一个页面int (*sync_page)(struct page *); // 同步一个脏页面到磁盘int (*prepare_write)(struct file *, struct page *, unsigned, unsigned); // 准备写操作,保证文件系统日志最新等int (*commit_write)(struct file *, struct page *, unsigned, unsigned); // 提交写操作,将数据提交给磁盘int (*bmap)(struct address_space *, long); // 映射磁盘块,与具体文件系统有关int (*flushpage)(struct page *, unsigned long); // 释放页面之前处理等待该页面的 I/O 操作int (*releasepage)(struct page *, int); // 释放页面之前刷新相关缓冲区int (*direct_IO)(int, struct inode *, struct kiobuf *, unsigned long, int); // 对索引节点执行直接 I/O 时使用int (*direct_fileIO)(struct file *, struct kiobuf *, unsigned long, int); // 对 struct file 进行直接 I/Ovoid (*removepage)(struct page *); // 页面从页面高速缓存中移除时使用
};
4.4.3 创建内存区域
  1. 系统调用流程
    在 x86 中,通过系统调用 mmap() 为进程创建新内存区域 。mmap() 会调用 sysy_mmap2()sysy_mmap2() 进一步调用 do_mmap2() ,这三个函数使用相同参数 。do_mmap2() 负责获取 do_mmap_pgoff() 所需参数,do_mmap_pgoff() 是在体系结构中创建新区域的主要函数。
  2. do_mmap2() 操作
    • 首先清空 flags 参数中的 MAP_DENYWRITEMAP_EXECUTABLE 位,因为 Linux 用不到这两个标志位。
    • 若映射文件,do_mmap2() 通过文件描述符查找对应的 struct file,并在调用 do_mmap_pgoff() 前获得 mmap_struct→mmap 信号量。
  1. do_mmap_pgoff() 操作
    • 合法性检查:检查文件和设备被映射时,相应文件系统和设备的操作函数是否有效;检查映射大小是否与页面对齐,确保不在内核空间创建映射,且映射大小不超过 pgoff 范围和进程映射区域上限。
    • 地址空间查找:若系统提供 get_unmapped_area() 函数(基于文件系统和设备 ),则调用它,否则使用 arch_get_unmapped_area() 函数找出内存映射所需的空闲线性地址空间。
    • 标志位处理:获得 VM 标志位,并根据文件存取权限对其进行检查。
    • 区域修正与分配:若映射处有旧区域,系统会修正以便新映射使用;从分配器里分配一个 vm_area_struct,并填充其各个字段 。
    • 链接与调用:把新的 VMA 链接到链表中;调用与文件系统或设备相关的 mmap() 函数 。
    • 返回:更新数据并返回。

4.4.4 查找已映射内存区域

在处理缺页中断等场景下,常需查找给定地址所属的虚拟内存区域(VMA),相关操作由以下函数完成:

  1. find_vma()
    函数原型:struct vm_area_struct *find_vma(struct mm_struct * mm, unsigned long addr)
    功能:查找涉及给定地址的 VMA 。若该区域不存在,将返回离请求地址最近的 VMA 。执行时先检查 mmap_cache 所代表的 VMA(mmap_cache 是上一次调用 find_vma() 返回的结果 ),若不是目标 VMA ,则遍历 mm_rb 存放的红黑树。若给定地址不包含在任何一个 VMA 中,函数返回离给定地址最近的 VMA ,调用此函数后需检查返回的 VMA 是否包含给定的地址。
  2. find_vma_prev()
    函数原型:struct vm_area_struct *find_vma_prev(struct mm_struct * mm, unsigned long addr, struct vm_area_struct **pprev)
    功能:和 find_vma() 类似,但返回指向目标 VMA 的前一个 VMA 的指针。该函数一般在判断两个 VMA 能否合并以及删除一个内存区域时使用 ,常需调用 find_vma_prepare()
  3. find_vma_prepare()
    函数原型:struct vm_area_struct *find_vma_prepare(struct mm_struct * mm, unsigned long addr, struct vm_area_struct **pprev, rb_node_t **rb_link, rb_node_t **rb_parent)
    功能:和 find_vma() 类似,且还会在链表中查找运行中的 VMA,如同红黑树节点插入操作一样 。
  4. find_vma_intersection()
    函数原型:struct vm_area_struct *find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
    功能:返回与给定地址范围相交的 VMA 。主要在扩展一个内存区域且调用 do_brk() 过程中使用,用于确保扩展的区域不会与原有的区域相交 。
  5. vma_merge()
    函数原型:int vma_merge(struct mm_struct * mm, struct vm_area_struct * prev, rb_node_t *rb_parent, unsigned long addr, unsigned long end, unsigned long vm_flags)
    功能:尝试扩展一个补胎的 VMA 以覆盖新的地址范围。若 VMA 不能向前拓展,会检查接下来的 VMA 是否可向后拓展以覆盖该地址范围,区域可能在无文件/设备映射且许可匹配时合并掉 。
  6. get_unmapped_area()
    函数原型:unsigned long get_unmapped_area(struct file * file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags)
    功能:返回足以覆盖所请求内存大小的空闲内存区域地址,主要在创建新的 VMA 时使用 。
  7. insert_vm_area()
    函数原型:void insert_vm_area(struct mm_struct *, struct vm_area_struct *)
    功能:将一个新的 VMA 插入到线性地址空间中 。
4.4.5 查找空闲内存区域

映射内存时,需先获取足够大的空闲区域,get_unmapped_area() 函数用于此目的,其参数及作用如下:

  • struct file:表示映射的文件或设备。
  • pgoff:表示偏移量。
  • address:表示请求区域的起始地址。
  • length:表示请求区域的长度 。
  • flags:表示此区域的保护标志位。

若映射设备(如视频卡),需先调用 f_op→get_unmapped_area(),因为设备或文件有额外操作要求,通用代码无法满足,比如映射地址须对齐到特殊虚拟地址。若无特殊要求,系统调用体系结构相关函数 arch_get_unmapped_area() ,若体系结构未提供,则调用 mm/mmap.c 中通用版本函数。其调用关系为:get_unmapped_areaarch_get_unmapped_areafind_vma

4.4.6 插入内存区域

插入新区域主要函数是 insert_vm_struct() ,调用流程如下:

  1. 先调用 find_vma_prepare() 找到新区域在两个 VMA 间的位置及在红黑树中的正确节点。
  2. 然后调用 vma_link() 将新区域链接到 VMA 链表。

在 Linux 中,insert_vm_struct() 不增加 mm_structmap_count 值 ,与之功能相似但常用的 __insert_vm_struct 会增加该值。

链接函数有 vma_link()__vma_link()vma_link() 用于无加锁情况,调用 __insert_vm_struct 前需确保所需锁已获取;若是文件映射,还需锁住文件,然后 __vma_link() 将 VMA 添加到相关链表。

__vma_link() 分三步:

  • 第一步__vma_link_list() ,把新区域插入单向 VMA 线性链表,若前驱为 NULL,则成为红黑树根节点。
  • 第二步__vma_link_rb() ,把新区域插入 VMA 红黑树。
  • 第三步__vma_link_file() ,处理文件共享映射,通过 vm_pprev_sharevm_next_share 把 VMA 插入 VMA 链表。

4.4.7 合并邻接区域
  • 目的与函数:Linux 用 merge_segments() 或等价的 vma_merge() 合并相邻内存区域,以减少 VMA 数量,因大量映射操作(如 sys_mprotect() )会创建众多映射,合并开销大,映射多耗时久。
  • 应用场景vma_merge()sys_mmap() 映射匿名区域(可合并 )及 do_brk() 给区域扩展并合并新分配区域时使用。条件是文件和权限匹配,且在无文件和设备映射、两区域权限相同下,区域可扩展。其他场景如 sys_mprotect() 权限一致时判断是否合并,move_vma() 中可能将相似区域移到一起。

4.4.8 重映射和移动内存区域
  • 扩展与收缩:系统调用 mremap() 扩展或收缩现有区域,由 sys_mremap() 实现。扩展时可能移动区域,未指定 MREMAP_FIXED 标志位时可移动。
  • 移动操作do_mremap() 先调用 get_unmapped_area() 找容纳扩展映射的空闲区域,再用 move_vma() 移动旧 VMA 。move_vma() 检查新区域能否与相邻 VMA 合并,不能则创建新 VMA 并分配 PTE,再用 move_page_tables() 复制旧映射页表项,最后用 zap_page_range() 处理旧映射页面(交换或删除 )。
4.4.9 对内存区域上锁
  • 系统调用:Linux 通过 mlock()(由 sys_lock() 实现 )锁住给定地址范围内存,sys_lock() 为地址范围创建 VMA 并设 VM_LOCKED 标志位,再调用 make_pages_present() 确保页在内存。mlockall()(由 sys_mlockall() 实现 )功能类似但影响进程所有 VMA ,二者依赖 do_mlock() 处理查找受影响 VMA 等工作。
  • 限制条件:VMA 页对齐,待锁定地址范围也需页对齐(向上取整 );不能超系统管理员设置的 RLIMIT_MLOCK 进程限制;每个进程每次只能锁住一半物理内存。

4.4.10 对区域解锁
  • 解锁函数:系统调用 munlock()munlockall() 分别用于对内存区域解锁,由 sys_munlock()sys_munlockall() 实现 。
  • 实现原理:相较于上锁函数,解锁函数简单,依赖 do_mmap() 来修整区域,无需过多检查 。
4.4.11 上锁后修整区域

影响及修正函数:上锁或解锁时,VMA 会在 4 个方面受影响,需 mlock_fixup() 修正 。

  • 当影响到所有 VMA 时,调用 mlock_fixup_all() 进行修正。
  • 被锁住区域地址起始处,由 mlock_fixup_start() 处理,可能需分配新 VMA 映射新区域。
  • 被锁住区域地址结束处,由 mlock_fixup_end() 处理。
  • mlock_fixup_middle() 处理映射区域中间部分,可能需分配 2 个新 VMA 。

注意事项:创建上锁的 VMA 时不合并,解锁后也不能合并。已上锁区域的进程没必要再次锁住同一区域,因为合并和分裂区域会消耗处理器计算资源 。

4.4.12 删除内存区域

删除函数及步骤do_munmap() 负责删除内存区域,操作分三部分。

  • 第一部分是修整红黑树,为保证红黑树有序,要删除的 VMA 先添加到 free 链表,再用 rb_erase() 从红黑树中移除,若区域后续有变动,会以新地址重新添加到系统。
  • 第二部分是释放和对应区域相关的页面和页表项,通过遍历 free 指向的 VMA 链表,调用 remove_shared_vm_struct() 共享映射,部分删除时用 unmap_fixup 处理,还会调用 zap_page_range() 删掉相关页面 。
  • 第三部分是如果生成了空洞就修整区域,最后调用 free_pgtables() 释放相关页表项,若页表项使用比例低则不释放,因释放代价高且可能再次被使用。
4.4.13 删除所有的内存区域

进程退出处理:进程退出时,exit_mmap() 函数负责删除与 mm_struct 相关联的所有 VMA 。在遍历 VMA 链表前刷新 CPU 高速缓存,依次删除每个 VMA 并释放相关页面,之后刷新 TLB 并删除页表项 。

4.5 异常处理

异常关注重点:在虚拟内存(VM)中,重点关注因页面中断产生的异常,而非除数为零等错误 。

错误引用情况

  1. 情况一:进程通过系统调用向内核传递无效指针,内核需安全处理,一般检查地址是否低于PAGE_OFFSET.
  2. 情况二:内核使用 copy_from_user()copy_to_user() 读写用户空间数据时可能引发异常.
  3. 异常表机制:编译时,链接器在代码段的 __ex_table 创建异常表,从 __start_ex_table 开始,到__stop_ex_table 结束 。表中每个表项类型为 exception_table_entry,由可执行点和修整子程序组成。产生异常且缺页中断处理程序无法处理时,会调用 search_exception_table() 检查是否为引起中断的指令提供了修整子程序,若系统支持,还会搜索每个模块的异常表。若在异常表中找到对应异常地址,将返回修整子程序位置并执行 。

4.6 缺页中断

问题背景与技术:进程线性地址空间页面不必常驻内存,Linux 采用请求调页技术解决非常驻页面问题。从后援存储器调页时,swapin_readahead() 会预取页面,若运气不佳,刚要用到的页面可能仅一次机会靠近交换区,因此 Linux 采用适合应用程序的预约式换页策略 。

缺页中断类型

  • 主缺页中断:费时从磁盘读取数据时产生。
  • 次缺页中断:也称轻微缺页中断,通过物理页面分配器分配页面帧等简单操作可解决 。Linux 通过 task_struct→maj_flttask_struct→min_flt 统计数目。

缺页中断原因及动作

异常

类型

动作

线性区有效但页面没分配

次要

通过物理页面分配器分配一个页面帧

线性区无效但是可扩展,如堆栈

次要

扩展线性区并分配一页

页面被交换但在交换高速缓存中

次要

从交换高速缓存中删除并分配给进程

页面被交换至后援存储器

主要

通过 PTE 中的信息查找页面并从磁盘读到内存中

写只读页面

次要

如果是 COW 页,就复制一页,标志为可写的并分配给进程,如果是写异常,就发送 SIGSEGV 信号

线性区无效或者没有访问权限

错误

给进程发送 SIGSEGV 信号

异常发生在内核地址空间

次要

如果异常发生在 vmaloc,就更新当前进程页表,相对于 init_mm 的主内核页表。这是惟一的有效发生内核页面异常的情况

异常发生在内核模态用户空间

错误

如果发生异常,表明内核不能从用户空间正确地复制数据,这是非常严重的内核 Bug

4. 处理函数:每种体系结构注册处理缺页中断函数(如 do_page_fault() )。handle_mm_fault() 是与体系结构无关的顶层函数,处理后援存储器中的缺页中断、执行写时复制(COW)等 。返回值含义:1 表示次缺页中断,2 表示主缺页中断,0 表示发送 SIG - BUS 错误,其他值激活内存溢出处理子程序 。

4.6.1 处理缺页中断

当异常处理程序确定是有效内存区域内的有效缺页中断时,会调用 handle_mm_fault() 函数(与体系结构无关 ),其处理流程如下:

  1. 页表项检查与处理
    • 调用 pte_present() 检查页表项(PTE)标志位,确定页面是否在内存中,再调用 pte_none() 检查 PTE 是否已分配。
    • 若 PTE 未分配(pte_none() 返回 true ),调用 do_no_page() 处理请求页面的分配;若页面已交换到磁盘,调用 do_swap_page() 处理请求换页。特殊情况(在 12.4 节讨论 )下,换出页面属于虚拟文件时,由 do_mmap_pgoff() 处理 。
  1. 写页面判断与处理:检查 PTE 是否写保护,若是,调用 do_wp_page() 处理写时复制(COW)页面。写时复制页面指多个进程共享一页(常为父子进程 ),直到某进程写操作时才为其分配并复制单独页面,可通过页面所在区域 VMA 标志位可写但相应 PTE 不可写来识别 。若不是写时复制页面,检查其标志是否为脏。
  2. 页面读取状态检查:确定页面是否已读取,某些无 3 级页表的体系结构中,建立 PTE 并标志为新即可 。若请求的页表项不存在,会先分配页表项,再调用 handle_pte_fault()

4.6.2 请求页面分配

在进程首次访问页面时,需分配页面,通常由 do_no_page() 函数填充数据。若父 VMA 的 vm_ops 提供 nopage() 函数,就调用它填充数据,这对内存映射设备(如视频卡 )很关键。

  1. 处理匿名页面
    • vm_area_struct→vm_ops 字段未填充或无 nopage() 函数,调用 do_anonymous_page() 处理匿名访问。
    • 第一次读:匿名页面无数据,系统一般用全零页 empty_zero_page 映射 PTE 且写保护,进程写时会引发缺页中断,mem_init() 负责将全局零页面归零。
    • 第一次写:调用 alloc_page() 分配由 clear_user_highpage() 用零填充的空闲页。分配成功,mm_struct 中 Resident Set Size(RSS)字段递增,部分体系结构中,页面插入进程空间时调用 flush_page_to_ram() 保证高速缓存一致性,页面插入 LRU 链表,最后更新进程页表反映新映射。
  1. 处理文件/设备映射页
    若页面被文件或设备映射,VMA 中 vm_operation_struct 提供 nopage() 函数。文件映射时,filemap_nopage() 分配页面并从磁盘读取相应数据;虚文件映射时,使用 shmem_nopage() 。设备驱动程序提供各自 nopage() 函数。
  2. 返回页面处理
    返回页面时,先检查分配是否成功,失败则返回错误。接着检查提前 COW 失效是否发生(向页面写且受管 VMA 无 VM_SHARED 标志时发生,指分配新页面并在减少 nopage() 返回页面引用计数前交叉复制数据 )。利用 pte_none() 检查确保使用的 PTE 不在页表中,SMP 环境中,若两个异常几乎同时发生且自旋锁未完全被异常获得,需立即检查。无竞争条件时,给 PTE 赋值,更新统计数据,调用体系结构相关钩子函数保证高速缓存一致性 。

4.6.3 请求换页
  1. 换页函数及原理do_swap_page() 函数负责将已交换至后援存储器的页面读入内存 。通过页表项(PTE)信息查找交换出去的页面,因页面可能被多个进程共享,一般先放至交换高速缓存,不能立即交换出去 。
  2. 反向映射技术(RMAP):2.5.x 后期版本和定制的 2.4.x 补丁引入 RMAP ,通过它,页面所映射的 PTE 组成链表,方便反向查找进程页表,避免查找所有进程页表浪费时间 。
  3. 交换高速缓存及处理:发生缺页中断时,若页面在交换高速缓存中,只需简单增加页面计数,放入进程页表并统计缺页中断发生次数 。
  4. 磁盘页面读取:若页面仅存于磁盘,Linux 调用 swapin_readahead() 读取该页面及其后续若干页面,读取页面数量由 mm/swap.cpage_cluster 变量决定 。内存小于 16MB 的机器,该值初始化为 2 或 3 ,除碰到坏或空的交换表项,通常读取数量是 page_cluster
4.6.4 写时复制(COW)页
  • 技术背景:以往创建进程时,将父进程地址空间完全复制给子进程耗时,因大量内容可能需从后援存储器交换入内存。为避免开销,Linux 采用写时复制(COW)技术。
  • 实现原理:创建进程时,将父子进程的页表项(PTE)设为只读,进程写操作时引发缺页中断。Linux 识别 COW 页(PTE 写保护但 VMA 区域可写 ),通过 do_wp_page() 函数将复制页赋值给写进程,必要时为页面保留新交换插槽。采用此技术,创建进程时只需复制页表项。

4.7 复制到用户空间/从用户空间复制

安全问题与处理机制:直接访问进程地址空间内存不安全,因难以快速检测页面是否常驻内存。在 x86 中,地址无效时 MMU 抛出缺页中断,由缺页中断处理子程序捕获处理。__copy_user 捕获无效地址异常,调用 search_exception_table() 确定修整代码位置 。

函数

功能

unsigned long copy_from_user(void *to, const void *from, unsigned long n)

从用户地址复制 n 个字节到内核地址空间

unsigned long copy_to_user(void *to, const void *from, unsigned long n)

从内核地址复制 n 个字节到用户地址空间

void copy_user_page(void *to, void *from, unsigned long address)

复制数据到用户空间的匿名页面或 COW 页面,可通过内核虚拟地址避免 D - cache 别名问题

void clear_user_page(void *page, unsigned long address)

清零用户空间页面

void get_user(void *to, void *from)

从用户空间复制一个整型值到内核空间

void put_user(void *from, void *to)

从内核空间复制一个整型值到用户空间

long strncpy_from_user(char *dst, const char *src, long count)

从用户空间复制最长字节末尾 NULL 终结字符串到内核空间

long strlen_user(const char *s, long n)

返回包含终结符 NULL 在内的用户空间字符串长度,最大值为 n

int access_ok(int type, unsigned long addr, unsigned long size)

检查用户空间内存块是否合法,不合法返回 0

copy_from_user()实现copy_from_user() 依据编译时复制数据量是否确定,调用 __constant_copy_from_user()__generic_copy_from_user() 。通用复制函数最终调用 <asm - i386/uaccess.h> 里的 __copy_user_zeroing(),该函数包含三部分:一是汇编器计算实际复制字节数,页面不在内存时发缺页中断,地址有效时进行数据换操作;二是修整代码;三是 __ex_table,将第一部分指令映射到第二部分,通过连接器复制到内核异常处理表 。若读到无效地址,执行 do_page_fault() ,调用 search_exception_table() 查找 EIP,跳过异常发生地方,跳转到修整代码处,修整代码将 0 复制到保留内核空间,修整寄存器并返回,使内核能安全访问用户空间并让 MMU 处理异常,其他访问用户空间函数遵循类似模式 。

这些函数主要用于Linux内核中处理用户空间内核空间之间的数据交换,以及确保这些操作的安全性和正确性。它们在内核开发中非常重要,尤其是在驱动程序或需要访问用户空间数据的场景中。

copy_from_user的实现与异常处理

copy_from_user的实现展示了Linux内核如何安全地处理用户空间数据访问:

实现机制

    • 根据复制数据量是否为编译时常量,选择调用__constant_copy_from_user(固定大小)或__generic_copy_from_user(动态大小)。
    • 底层依赖__copy_user_zeroing(定义在<asm-i386/uaccess.h>中),包含三部分:
      1. 汇编计算与复制:计算实际复制字节数,触发缺页中断(若页面不在内存)或执行数据复制。
      2. 修整代码:处理异常情况(如无效地址),将未复制部分填充为0。
      3. 异常表(__ex_table):将复制指令映射到修整代码,记录在内核异常处理表中。

异常处理

    • 若访问无效地址,触发页面错误(do_page_fault)。
    • 内核调用search_exception_table查找异常指令指针(EIP),跳转到修整代码。
    • 修整代码将剩余字节置0,调整寄存器状态,返回未复制的字节数。

安全性:通过MMU(内存管理单元)和异常表机制,内核能安全访问用户空间,处理页面错误或非法地址访问。

这些函数是Linux内核与用户空间交互的核心工具:

安全数据传输:在用户空间和内核空间之间安全地传递数据,防止非法访问或越界。

内存管理:处理页面级操作(如清零、复制页面),支持内存分配和虚拟化。

异常处理:通过异常表和修整代码,确保内核在访问无效用户空间地址时不会崩溃。

驱动开发:设备驱动程序常使用这些函数读取用户输入或返回处理结果。

系统调用:系统调用实现中广泛使用这些函数来传递参数和结果。

注意事项:这些函数运行在内核态,需要确保用户空间地址的合法性(通过access_ok)。它们通过异常处理机制(如__ex_table)确保内核稳定性,防止用户空间的错误地址导致内核崩溃。开发者在使用这些函数时需仔细检查返回值(如copy_from_user返回的未复制字节数),以处理潜在的错误。

🌍 思考:用户空间和内核空间交换是指虚拟地址空间的 内核地址空间的数据 和 用户地址空间的数据 互相拷贝吗?

用户空间和内核空间之间的数据交换确实是指虚拟地址空间内核地址空间的数据和用户地址空间的数据互相拷贝的过程。

用户空间与内核空间的虚拟地址空间

现代操作系统(如Linux)中,每个进程都有自己的虚拟地址空间

  • 用户空间:进程运行用户态代码(如应用程序)的地址范围,通常是虚拟地址的低地址部分(例如在32位系统中为0到3GB)。
  • 内核空间:操作系统内核运行的地址范围,通常是高地址部分(例如在32位系统中为3GB到4GB),所有进程共享同一内核空间。

用户空间和内核空间的隔离通过硬件内存保护机制(如MMU,内存管理单元)实现,用户态代码无法直接访问内核空间的地址。

2. 数据交换的本质

  • 用户空间到内核空间:当用户程序通过系统调用(如readwrite)或设备驱动与内核交互时,需要将用户空间的数据(存储在用户态虚拟地址中)复制到内核空间。例如,用户程序传递一个缓冲区给内核,内核需要读取这些数据。
  • 内核空间到用户空间:内核处理完成后,可能需要将结果(如读取的文件内容、设备状态)复制到用户空间的缓冲区,供用户程序使用。
  • 这种数据交换是通过虚拟地址进行的,内核和用户空间的虚拟地址指向不同的物理内存区域,复制过程由内核提供的函数(如copy_from_usercopy_to_user)完成。

3. 为什么需要复制?

  • 全性和隔离:用户空间和内核空间的隔离确保用户程序无法直接访问或修改内核数据,防止恶意代码破坏系统。直接访问对方地址空间会导致安全漏洞。
  • 地址转换:用户空间和内核空间的虚拟地址不同,指向的物理内存也不同。复制过程通过MMU将数据从一个虚拟地址空间的物理内存移动到另一个。
  • 异常处理:用户提供的地址可能无效(例如未映射的页面或越界访问)。复制函数(如copy_from_user)包含异常处理机制,确保内核在访问用户空间地址时不会崩溃。

4. 复制的实现

函数如copy_from_usercopy_to_user负责在用户空间和内核空间之间复制数据:

    • copy_from_user:从用户空间的虚拟地址读取数据,复制到内核分配的内存(内核空间的虚拟地址)。
    • copy_to_user:将内核空间的数据复制到用户空间的虚拟地址。

这些函数会:

    • 检查用户空间地址的有效性(通过access_ok)。
    • 处理页面错误(通过异常表和修整代码)。
    • 完成实际的数据复制(通常通过汇编优化,确保高效)。
  • 例如,copy_from_user(to, from, n)会将用户空间地址fromn字节数据复制到内核空间地址to,并返回未复制的字节数(若为0表示成功)。

具体例子

  • 用户空间到内核空间:用户程序调用write系统调用,传递一个缓冲区(用户空间虚拟地址)。内核通过copy_from_user将缓冲区内容复制到内核内存,然后写入设备(如磁盘)。
  • 内核空间到用户空间:用户程序调用read系统调用,内核从设备读取数据到内核缓冲区,然后通过copy_to_user将数据复制到用户程序提供的缓冲区(用户空间虚拟地址)。

关键点

虚拟地址的角色:用户空间和内核空间的虚拟地址是分开的,复制过程本质上是将数据从一个虚拟地址空间的物理内存映射移动到另一个虚拟地址空间的物理内存映射。

内核的控制:所有复制操作都在内核态执行,内核通过MMU和页表管理地址转换和访问权限。

安全性:复制函数确保即使用户提供无效地址,内核也能安全处理(通过触发缺页中断或返回错误)。

1

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词