介绍
在Linux中二进制的程序从磁盘加载到内存,运行起来后用户态是使用pid来唯一标识进程,对于内核都是以task_struct表示。二进制程序中的数据段、代码段、堆都能提现在task_struct中。每一个进程都有自己的虚拟地址空间,虚拟地址空间包含几种区域,具体参照如下
在内核中进程分配内存时候并非立马给定虚拟内存对应的物理内存,而是分配虚拟内存的使用权。只有当进程真正访问申请的虚拟内存才会分配物理页帧并建立页表映射。就如下面的代码malloc仅仅是在当前的进程的地址空间内分配虚拟内存的使用权,分配物理页帧是在memset函数访问虚拟内存的时候。
void *ptr=malloc(sizeof(int));
memset(ptr,0,sizeof(int));
进程虚拟地址空间
之前聊过task_struct用来表示内核中的进程或者线程,在task_struct中有一个进程内存空间的描述符,用来描述进程的内部虚拟空间布局。这个结构非常大,我们会从task_struct->mm_struct->vm_area_struct从上往下的顺序简单介绍下
// 内核中用来表示进程或者线程的数据结构
struct task_struct {// 进程的内存空间描述符struct mm_struct *active_mm;
};// 进程的虚拟内存空间描述符号
struct mm_struct {struct {// 进程是使用的所有虚拟内存的链表struct vm_area_struct *mmap; /* list of VMAs */// 链表中的节点组成的红黑树struct rb_root mm_rb;// 当前进程最大的虚拟地址空间大小unsigned long task_size; /* size of task vm space */// 页表的物理地址pgd_t * pgd;// 二进制代码的虚拟内存区域是从start_code到end_code来表示// 初始化区域虚拟内存用start_data和end_data来表示unsigned long start_code, end_code, start_data, end_data;// 动态变化的堆虚拟内存区域是从start_brk到brkunsigned long start_brk, brk, start_stack;// 参数列表的虚拟内存区域是从arg_start到arg_end// 环境变量的虚拟内促区域是从env_start到env_endunsigned long arg_start, arg_end, env_start, env_end;} __randomize_layout;
};// 用来表示各个虚拟内存区域的结构
struct vm_area_struct {// 虚拟内存的起始地址unsigned long vm_start; /* Our start address within vm_mm. */// 虚拟内存的结束地址unsigned long vm_end; /* The first byte after our end addresswithin vm_mm. */// 进程所使用的各个虚拟内存区域通过vm_prev和vm_next链表链接起来struct vm_area_struct *vm_next, *vm_prev;// 当查找虚拟地址存在于哪个区域时链表性能显然不行,通过vm_rb构建的红黑树查找struct rb_node vm_rb;// 指向属于哪一个mm_struct结构用来表示从属关系struct mm_struct *vm_mm; /* The address space we belong to. */// 内存区域的标记pgprot_t vm_page_prot;unsigned long vm_flags; /* Flags, see mm.h. */// vma的操作函数const struct vm_operations_struct *vm_ops;// 文件映射的偏移量unsigned long vm_pgoff; // 如果是文件映射vm_file则是表示对应的文件指针struct file * vm_file; /* File we map to (can be NULL). */void * vm_private_data; /* was vm_pte (shared mem) */} __randomize_layout;
相关视频推荐
2024,彻底搞懂计算机的底层原理,linux内核源码分析教程,六大模块全面分析(内存管理、进程管理、设备驱动、网络协议栈、文件系统、中断管理及基础)https://www.bilibili.com/video/BV1GT4y1t7Hs/
免费学习地址:Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)
需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
内存操作
这里涉及到的是文件映射和堆内存分配两种的情况
文件映射
用户态的文件映射是通过mmap系统调用进行实现,它可以绕靠文件系统的过程,利用内存指针快速访问文件数据。mmap新的系统调用对应的是内核中ksys_mmap_pgoff.
// mmap的系统调用的实现,底层是调用ksys_mmap_pgoff的函数
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, off)
{if (off & ~PAGE_MASK)return -EINVAL;return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}
// ksys_mmap_pgoff的具体定义如下
unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,unsigned long prot, unsigned long flags,unsigned long fd, unsigned long pgoff)
{struct file *file = NULL;unsigned long retval;// 匿名文件映射,设置映射的文件if (!(flags & MAP_ANONYMOUS)) {audit_mmap_fd(fd, flags);file = fget(fd);// 大页方式} else if (flags & MAP_HUGETLB) {struct ucounts *ucounts = NULL;struct hstate *hs;hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,VM_NORESERVE,&ucounts, HUGETLB_ANONHUGE_INODE,(flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);}// 最核心的映射函数实现retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);return retval;
}// 调用底层的do_mmap函数实现映射
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flag, unsigned long pgoff)
{ret = do_mmap(file, addr, len, prot, flag, pgoff, &populate,&uf);return ret;
}// 最底层的文件映射的实现
unsigned long do_mmap(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flags, unsigned long pgoff,unsigned long *populate, struct list_head *uf)
{struct mm_struct *mm = current->mm;vm_flags_t vm_flags;int pkey = 0;// 在线性区间找到未被使用并且足够大的地址空间addr = get_unmapped_area(file, addr, len, pgoff, flags);// 传入prot和flags设置vm_flagsvm_flags = calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;// 如果是文件映射,则通过find_inode找到inode并检查文件if (file) {struct inode *inode = file_inode(file);}addr = mmap_region(file, addr, len, vm_flags, pgoff, uf){// 检查虚拟地址空间容量限制if(!may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)){}// 检查是否有当前的vma有重叠如果有则进行munmap操作munmap_vma_range(mm, addr, len, &prev, &rb_link, &rb_parent, uf);// 与现有的vma进行合并vma = vma_merge(mm, prev, addr, addr + len, vm_flags,NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);if (vma) {goto out; }// 申请新的vm_area_struct结构vma = vm_area_alloc(mm);// 将新的vm_area_struct插入到mm_struct中的链表、红黑树以及对应文件的地址空间上的adress_space->i_mmap或者address_space->i_mmap_nolinear中vma_link(mm, vma, prev, rb_link, rb_parent);}return addr;
}
堆内存
在用户态申请内存和释放内存通过malloc/free库函数进行,它们的底层还是通过SYSCALL_DEFINE1(brk, unsigned long, brk)系统调用来完成。堆内存的扩大可以通过SYSCALL_DEFINE1(brk, unsigned long, brk)进行,如果需要缩小的空间则通过do_munmap实现。如果分配空间大于128KB(glibc源码中定义的MMAP_THRESHOLD),malloc使用sys_mmap2实现内存申请。不论是malloc还是calloc申请的是线性虚拟地址而非物理地址,连续的空间也是指的虚拟地址空间的连续。
// brk系统调用的实现
SYSCALL_DEFINE1(brk, unsigned long, brk)
{origbrk = mm->brk;// 检查资源的限制if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,mm->end_data, mm->start_data))goto out;// page的对齐newbrk = PAGE_ALIGN(brk);oldbrk = PAGE_ALIGN(mm->brk);if (oldbrk == newbrk) {mm->brk = brk;goto success;}// 如果是是释放操作则执行__do_munmap调整指针的位置if (brk <= mm->brk) {int ret;mm->brk = brk;ret = __do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true);goto success;}// 对应malloc的实现,申请新的vm_area_struct、插入到mm_struct中的list和rb树中// do_b rk_flags可以理解是mmap简单版的实现if (do_brk_flags(oldbrk, newbrk-oldbrk, 0, &uf) < 0)goto out;mm->brk = brk;success:populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;if (downgraded)mmap_read_unlock(mm);elsemmap_write_unlock(mm);userfaultfd_unmap_complete(mm, &uf);if (populate)mm_populate(oldbrk, newbrk - oldbrk);return brk;out:mmap_write_unlock(mm);return origbrk;
}