百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Linux随笔 - 这么解释mmap可能好理解些

haoteby 2024-12-18 14:13 6 浏览

写在前面

经常能刷到讲解linux内核相关知识的文章以及课程,大部分是卖课的,给我的感觉就是不太好懂,甚至越讲越不懂,越讲越复杂。我今天思考了一下原因:

  1. 只讲源代码不讲原理。我一直想要搞懂内核,但是随便搜索得来的文章往往不能深入看,发现问题越来越多,好不容易今天看懂了,过几天就忘了,过一个月就全忘了回到起点了;我想根本的原因是,每行代码我都懂,但是不知道为什么这么写。
  2. 不讲历史,只讲结果。任何一个工程,不管大小,都是不断演进的,变化的。我们往往看的是结果,就是他最后的样子,至于为啥会是这样,不清楚,所以后面如果技术发生了变化,升级,新的代码还得重新理解,重新学习,越来越累。

所以,我尝试换一种方式理解Linux内核。就从mmap开始吧。

mmap到底在做什么?

1. 看看man mmap

看官方解释往往是第一步,因为权威,准确。

NAME
mmap, munmap - map or unmap files or devices into memory>

SYNOPSIS
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length); See NOTES for information on feature test macro requirements.

DESCRIPTION
mmap() creates a new mapping in the virtual address space of the calling process. The starting >address for the new mapping is specified in addr. The length argument specifies
the length of the mapping (which must be greater than 0).

看文字mmap的功能就是为当前进程的虚拟内存分配一个新的映射,映射的起始地址是addr长度length。还可以传入一个fd,说明可以将这个虚拟地址绑定到一个文件上。

我的理解

通过看这段文字,我大概脑子里已经有一个大概的思路了。因为我花了很长的时间已经搞懂了这些概念是什么:

  1. 虚拟地址在CPU在OS中到底指的是什么?
  2. 映射这个动作到底指的什么?

如果你清楚上面这两个问题,我来稍微解释下,理解了的可以跳过。

  1. 这里的地址虚拟内存地址。当Linux内核起来后,CPU就不清楚啥是物理地址了(从real mode到long mode),因为它只能接触到虚拟地址。(具体CPU如何将虚拟地址对应到物理地址的寻址过程可以参考:图解CPU的内存管理——彻底讲透分段与分页);
  2. 有了虚拟地址以后,地址这个概念发生了拓展,不再只跟内存一一对应了,它可以代表:各种连接在总线上的设备,特定的寄存器等等,可以代表你想要用mov指令访问的任何位置,任何设备中的数据;
  3. 现在虚拟地址是个资源概念,CPU可以访问到的资源,是个抽象概念了——多一层抽象,构架就多一份灵活性,虚拟地址是个伟大的发明;
  4. 如果我现在说映射指的是将抽象的虚拟地址具体如何绑定到设备中实在的数据的过程,应该好理解些了吧?程序员可以把这个映射理解成——硬件世界的面向对象抽象的过程。用伪代码表示就是:
interface Address {
void access(VirtualAddress address);
}

public class Memory impliment Adress{
  public void access(VirtualAddress address){
  //memory怎么通过虚拟地址访问数据
  }
}

public class File impliment Adress {
public void access(VirtualAddress address){
//如何通过虚拟地址来访问文件的数据
}
}

所以,我的理解是:mmap是个分配物理内存的函数,或者说机制。用户态进程通过调用这个函数向系统申请了一块连续的内存资源。而且,居然还可以传入一个文件,那大概就是利用虚拟地址访问文件资源了吧。大概分成这么几个步骤:

  1. 既然虚拟地址是个资源,尽管它是虚拟的、不存在的抽象概念,但是是资源肯定要分配。所以第一步就是从进程“广袤”的地址空间中找一段大小合适的,没有用过的虚拟地址空间来做后面的映射操作;
  2. 找到以后我要存下来,或者说用一个结构保存下来,后面只要找到这个结构就能操作这段虚拟地址空间;这个结构就是vm_area_struct
  3. 如果要映射到内存,我就去找一个页的物理内存,然后操作页表生成页表项就行了;物理内存维护在slab中,页表维护在task_struct中,登记一下就行了;相当于上面伪代码中Memory.access(virtualAddress)的实现
  4. 如果是要映射到文件,那么就要实现File.access(virtualAddress)接口。

这是个高层的理解,也是设计的初衷。因为有了虚拟地址就可以做到mmap,也只有虚拟地址才能做到mmap这么灵活的构架设计。这就是所谓的机制与策略的分离构架思想。因为:

  • Linux是个通用的操作系统,未来要接入的设备五花八门,接口形式也不同,要怎么设计一套足够灵活构架解决这个问题呢?虚拟内存提供了答案。
  • 虚拟内存挡在CPU与外部设备之间,对CPU屏蔽了外部设备与数据访问的复杂度,用统一的方式去访问所有的设备——也就是CPU指提供访问机制,no more no less。简单来说就是,CPU只提供接口,不提供具体实现
  • 而设备的复杂度由设备的制造商去解决,制造商根据不同CPU构架访问资源的接口,实现自己的access实现,并将实现注入到OS中去就行了。这就是策略由提供商来做,也只能由制造商来做才能繁荣整个生态;
  • 现在大家明白什么是驱动程序了吗?很简单,就是根据CPU/OS提供的接口,规范,机制来实现自己的策略,然后注入到OS中去跑的过程。

下面看看是不是这么回事,我们现在看看关键的内核代码。

内核mmap的代码

浅看一下,理解意思就行,内核的代码嵌套比较深,其实了解原理后,只要抓住关键点就行了。

首先就是要找到虚拟内存的接口定义

  • vm_area_struct其实就是虚拟内存的class对象,在其中定义了一个跟access十分类似的字段:
struct vm_area_struct {
    /* The first cache line has the info for VMA tree walking. */

    unsigned long vm_start;     /* Our start address within vm_mm. */
    unsigned long vm_end;       /* The first byte after our end address
                       within vm_mm. */

    ......

    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops; //就是这个字段

    .......
} __randomize_layout;

这个字段就是了const struct vm_operations_struct *vm_ops;,我们展开看看:

struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    /* Called any time before splitting to check if it's allowed */
    int (*may_split)(struct vm_area_struct *area, unsigned long addr);
    int (*mremap)(struct vm_area_struct *area, unsigned long flags);
    /*
     * Called by mprotect() to make driver-specific permission
     * checks before mprotect() is finalised.   The VMA must not
     * be modified.  Returns 0 if eprotect() can proceed.
     */
    int (*mprotect)(struct vm_area_struct *vma, unsigned long start,
            unsigned long end, unsigned long newflags);
    vm_fault_t (*fault)(struct vm_fault *vmf);
    vm_fault_t (*huge_fault)(struct vm_fault *vmf,
            enum page_entry_size pe_size);
    void (*map_pages)(struct vm_fault *vmf,
            pgoff_t start_pgoff, pgoff_t end_pgoff);
    ........... //后面还有就不贴了
};

可以看到很多接口——C语言就是函数指针。这个就是抽象类。我们可以看到有个叫做fault的接口函数,这个函数就是x86中断中“第14名”,大名鼎鼎的pagefault exception的处理点了。(要理解Linux内核,必须先了解CPU,要了解CPU只要了解内存怎么管理,中断怎么处理其实就够了,一点点题外话)

看到这里其实我们就能猜测这个过程了:
1、在
mmap系统调用中linux其实不用分配实际的物理内存,只要给进程分配一段资源——一段没有映射的虚拟地址空间——vma结构;
2、对vma结构做初始化,主要是为CPU机制——pagefault exception——准备具体的实现类——映射文件还是映射内存;设置到这里就可以了,因为COW(copy on write)机制会把物理内存的分配推迟到最后一刻——中断发生的时候;
3、在pagefault exception中肯定会做两个事情,也只需要做两个事情:
1. 根据中断进程,找到发生中断的虚拟内存——vma 结构;
2. 调用vma->fault接口进行处理就行了。

我们先看内核代码,看看pagefault exception是否真的是这么处理的。(先证实第3点猜测)
先看看pagefault的入口:

/*
address就是引发pagefault中断处的虚拟地址
regs是用户态进程的CPU上下文
*/
void do_page_fault(unsigned long address, struct pt_regs *regs)
{
    struct vm_area_struct *vma = NULL;
    struct task_struct *tsk = current;
    struct mm_struct *mm = tsk->mm;
    int sig, si_code = SEGV_MAPERR;
    unsigned int write = 0, exec = 0, mask;
    vm_fault_t fault = VM_FAULT_SIGSEGV;    /* handle_mm_fault() output */
    unsigned int flags;         /* handle_mm_fault() input */

    ........ //这里就是根据引发中断的地址找到对应的vma结构
    vma = find_vma(mm, address);

        ......... //找到vma以后就开始调用vma相应的处理方法了
    fault = handle_mm_fault(vma, address, flags, regs);

    .........
}

最后调用 __do_fault函数处理。

static vm_fault_t __do_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret;
    if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) {
        vmf->prealloc_pte = pte_alloc_one(vma->vm_mm);
        if (!vmf->prealloc_pte)
            return VM_FAULT_OOM;
        smp_wmb(); /* See comment in __pte_alloc() */
    }
        //这里就开始调用fault了。跟我们猜测一致。
    ret = vma->vm_ops->fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
                VM_FAULT_DONE_COW)))
        return ret;

..................

    return ret;
}

看到这里ret = vma->vm_ops->fault(vmf);,确实调了fault来处理缺页,这个fault其实是个接口,是不是很像多态?所以说,面相对象是个概念,任何语言都可以实现的。

嗯嗯,非常好!跟我的猜测是一致的。现在就是要确认1,2两个地方了,具体分配页表是在缺页中断处,mmap系统调用就是实现多态函数的绑定咯,我们看看。回到mmap系统调用处开始找。

SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
        unsigned long, prot, unsigned long, flags,
        unsigned long, fd, unsigned long, off)
{
    long error;
    error = -EINVAL;
    if (off & ~PAGE_MASK)
        goto out;

    error = ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
out:
    return error;
}

再找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)
{
    ............\\前面都是校验

    retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
out_fput:
    if (file)
        fput(file);
    return retval;
}

进入vm_mmap_pgoff函数,再到do_mmap函数,linux代码嵌套是很深的。

/*
 * 这个函数完成了file->vma的绑定。
 */
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)
{
    ............

    /* Obtain the address to map to. we verify (or select) it and ensure
     * that it represents a valid section of the address space.
     */

  //获取一个没有映射的起始地址。应该是4k对齐的地址
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr;

............................ 
//实际绑定的函数
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
    ..............
    return addr;
}

实际的绑定函数是mmap_region,怎么绑定的呢?

unsigned long mmap_region(struct file *file, unsigned long addr,
        unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
        struct list_head *uf)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev, *merge;
    ......................
    /*
     * 这里会拿到address对应的vma对象
     */
    vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
            NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
    if (vma)
        goto out;

    /*
          也可能在这里拿到address对应的vma对象。不管在哪里拿到vma,到这里肯定拿到了。
     */
    vma = vm_area_alloc(mm);
    if (!vma) {
        error = -ENOMEM;
        goto unacct_error;
    }
        //vma起始位置
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_flags = vm_flags;
    vma->vm_page_prot = vm_get_page_prot(vm_flags);
    vma->vm_pgoff = pgoff;

    .....
//vma跟file绑定
        vma->vm_file = get_file(file);

//这里就是完成绑定的地方了!
        error = call_mmap(file, vma);
.............................
}

可以看到call_mmapyou两个参数filevma,可见这个函数就是将两个对象绑定起来的地方了:

static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
    return file->f_op->mmap(file, vma);
}

这里会调用file描述符中的mmap(file,vma)函数完成绑定。如果我们用的文件系统是ext4则应该去找找这个文件系统的mmap函数的实现。

const struct file_operations ext4_file_operations = {
    .llseek     = ext4_llseek,
    .read_iter  = ext4_file_read_iter,
    .write_iter = ext4_file_write_iter,
    .iopoll     = iomap_dio_iopoll,
    .unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl   = ext4_compat_ioctl,
#endif
//在这里定义
    .mmap       = ext4_file_mmap,
    .mmap_supported_flags = MAP_SYNC,
    .open       = ext4_file_open,
    .release    = ext4_release_file,
    .fsync      = ext4_sync_file,
    .get_unmapped_area = thp_get_unmapped_area,
    .splice_read    = generic_file_splice_read,
    .splice_write   = iter_file_splice_write,
    .fallocate  = ext4_fallocate,
};

ext4_file_operations就是file->f_op,可见文件系统也是用机制策略分离的构架建立的。任何文件系统,都要实现struct file_operations接口。上面的file->f_op->mmap(file, vma);调用的就是ext4_file_mmap(file,vma)

static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
    .....
        vma->vm_ops = &ext4_file_vm_ops;
        .....
    return 0;
}

这里我们就看到了对于新找到的vma,如果是映射到文件,会把vma的vm_ops抽象接口改成文件定制的vm_ops——ext4_file_vm_ops

static const struct vm_operations_struct ext4_file_vm_ops = {
    .fault      = ext4_filemap_fault,
    .map_pages  = filemap_map_pages,
    .page_mkwrite   = ext4_page_mkwrite,
};

我们可以看到,对于ext4文件系统的vm_ops接口的实现有fault。至此,我们就找到了mmap到文件的地方了,到时候pagefault发生的时候,会执行ext4_filemap_fault函数进行物理内存映射,步骤是将vma对应的虚拟内存地址映射到物理地址,然后这个物理地址填充上相应文件的内容。是不是很容易理解了。

接着找找将匿名映射,也就是将vma映射到普通的物理内存。

后来我翻了下代码发现其实我想多了,Linux对于匿名映射,是没有填充fault函数的......do_fault直接从slab中找空闲页面就行了。这里是证据:

static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
    return !vma->vm_ops;
}

如果是匿名映射的话vma->vm_ops是空的。
到这里就结束了,结论就是,mmap确实是根据不同的映射条件将虚拟内存空间映射到不同的资源上来统一访问的。

总结

我感觉,单纯阅读Linux源代码其实对开发帮助有限,而且对一般的非内核开发人员,一段时间不用,就会忘记,但是如果你理解了代码的机制,知道了Linux为什么要这么写,你可能长时间的记住,并且运用到自己的工作中,学习Linux其实就是要学习它的工程经验。

相关推荐

单点登录(SSO)解决方案介绍(单点登录概念)

一、单点登录的介绍单点登录(SingleSignOn),简称为SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系...

系统登录的三种方式,哪一种更安全?

登录是一个高频的动作,笔者抓住这一个小点,分析了系统登录的几种方式和对应的场景。今天谈谈登录。登录即用户输入用户名和密码登录进系统中。B端系统,对于登录的业务场景有两种(可能不止,目前遇到过这两种):...

到底什么是单点登录(SSO)?(什么叫做单点登录)

什么是单点登录?单点登录(SingleSign-On,简称SSO)是一种集中式的身份验证和授权机制,用户只需在一处输入一次凭证(例如用户名和密码)就可以访问多个相关但独立的软件系统。在数字化时代,...

5年稳如老狗的单点登录系统,到底是怎么搞出来的?

说到单点登录(SingleSign-On,简称SSO),大家的第一反应可能是——啊不就是登录一次,能到处串门儿嘛?别说,还真差不多,就是这么个意思。但真要搭一套好用、耐造、还能扛住公司里各种奇奇怪...

这些负载均衡都解决哪些问题?服务、网关、NGINX?

在微服务项目中,有服务的负载均衡、网关的负载均衡、Nginx的负载均衡,这几个负载均衡分别用来解决什么问题呢?一、服务的负载均衡先抛出一个问题:...

Nginx负载均衡最全详解(4大算法原理机制)

Nginx在大型网站架构很重要,也是大厂重点考察方向,今天我就重点来详解Nginx负载均衡@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》里面。Nginx负载均衡N...

负载均衡 Nginx Session 一致性(nginx 负载均衡 会话保持)

HTTPS请求跳转...

监控Oracle Cloud负载均衡器:Applications Manager释放最佳性能

设想你正在运营一个受欢迎的在线学习平台,在考试前的高峰期,平台流量激增。全球的学生同时登录,观看视频、提交作业和参加测试。如果OracleCloud负载均衡器不能高效地分配流量,或者后端服务器难...

Nginx负载均衡:nginx.conf配置文件说明!

大家好,欢迎来到程序视点!我是你们的老朋友.小二!在此记录下Nginx服务器nginx.conf负载均衡的配置文件说明,部分注释收集与网络.关于nginx.conf基本的配置,请查看上一篇文章!Ng...

Java高可用系统架构中的负载均衡策略

Java高可用系统架构中的负载均衡策略在现代的分布式系统中,负载均衡策略是构建高可用系统的基石。Java开发者需要深刻理解这些策略,以便打造稳定且高效的系统。接下来,让我们一起揭开负载均衡的神秘面纱。...

深入对比Nginx、LVS和HAProxy,选择最合适负载均衡方案!

关注...

Spring Boot3 客户端负载均衡全解析:从原理到实战

在当今互联网大厂后端技术开发的激烈竞争环境中,构建高效、稳定的微服务架构是核心诉求。其中,SpringBoot3作为热门开发框架,其客户端负载均衡功能对于提升系统性能、保障服务稳定性起着关键作用。...

MySql高可用集群MySQL Router负载均衡读写分离

名词解释MGR:MysqlGroupReplication组复制,多台MySQL服务器在同一组中会自动保持同步状态,当某台服务器故障时,整个复制组依然可以保持正常并对外提供服务。...

性能测试之tomcat+nginx负载均衡(nginx tomcat)

nginxtomcat配置准备工作:两个tomcat执行命令cp-rapache-tomcat-8.5.56apache-tomcat-8.5.56_2修改被复制的tomcat2下con...

win10/11双网卡链路聚合叠加负载均衡提升网速解决网卡网速瓶颈!

双网卡链路聚合一种网络配置技术,通过将多个物理网卡绑定在一起,形成一个逻辑上的网络接口,以提高网络的可靠性、可用性和性能。这种技术通常用于服务器和网络设备中,以实现负载均衡、冗余和高可用性。本机环境:...