剖析脏牛3_-proc-self-mem是怎么实现的

robots

 

测试程序

int fd;
struct stat st;
void *mem;

void processMem(void)
{
    int f = open("/proc/self/mem", O_RDWR);
    lseek(f, mem, SEEK_SET);
    write(f, "AAA", 3);
}

int main(void)
{
    fd = open("./test", O_RDONLY);
    fstat(fd, &st);
    mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

    processMem();
}

 

sys_write()

  • 由于我们是通过write读写文件的, 因此先进入write的系统调用处理函数看一下
  • sys_write()获取一些参数后调用vfs_write()进行真正的写入

 

vfs_write()

  • 这是虚拟文件系统提供的通用的文件写入操作, 本身就是一个__vfs_write()的包裹函数

 

__vfs_write()

  • 这个函数主要就是根据文件对象调用其内部的write()方法

  • /proc/self/mem这个文件对象来说, 会调用mem_write()函数

 

mem_write()

  • mem_write()会调用mem_rw()

 

mem_rw()

  • mem_rw()首先根据/proc/self/mem这个文件对象的私有数据区域, 找到其映射的是哪一个虚拟地址空间, 然后在内核中申请了一个临时页作为内核缓冲区

  • 接着通过一个循环写入count长数据, copy_from_user把数据搬运到内核缓冲区中, 再调用access_remote_vm()写入虚拟地址空间中

  • 注意:
    • 假如进程A陷入write系统调用, 由于惰性TLB, 内核线程使用页表就是A的页表, 也就是说当前页表的用户部分是进程A的, 内核部分是所有内核内核同步的
    • copy_from_user()与copy_to_user()是在当前页表的用户部分与内核部分中进行读写操作, 其本质上就是memcpy(), 对于MMU来说和用户态的memcpy()没啥区别, 对于内核来说, 由于页表的用户部分是可变的, 所以要保证页表的用户部分属于要写入的进程
    • 而进程A可能会读写/proc/B/mem, 因此需要调用access_remote_vm()去读写别的进程的虚拟地址空间, 而不再是本进程的地址空间了

 

access_remote_vm()

  • 是__access_remote_vm()的包裹函数

 

__access_remote_vm()

  • 主要分为两部分, 首先调用get_user_pages_remote()把页面锁定在内存中, 从而可以直接访问

  • 然后根据锁定的页对象page找到其所处的内存地址maddr, 然后使用copy_to_user_page()进行写入工作

  • 这个函数的核心就在与怎么把别的进程的页面锁定在内存中的, 因此get_user_pages_remote()的实现

 

get_user_pages_remote()

  • 函数位于mm/gup.c中, 就是一个对于__get_user_pages_locked()的包装函数

 

__get_user_pages_locked()

  • 由于locked设置为NULL, 因此get_user_pages_locked()设置flags, 调用get_user_pages()就直接返回了, 不会进入VM_FAULT_RETRY的逻辑

 

__get_user_pages()

  • 首先进行一些简单的参数检查

然后通过一个do{…}while(nr_pages)循环, 遍历所有需要锁定的页, 处理一个页之前, 先找到所属的VMA

  • __get_user_pages()最核心的部分, 就是下面这个循环, follow_page_mask()判断对应页是否满足foll_flags要求, faultin_page()负责处理错误, 会一直循环到对应页满足foll_flags的要求

  • 处理完这个页之后, 记录结果, 然后处理下一个页

 

follow_page_mask()

  • 先是一些基本的变量声明

  • 然后就是跟踪四级页目录:pgd=>pud=>pmd, 如果对应表项为none, 则返回no_page_table()表示出错, 最后进入follow_page_pte()跟踪pte

 

follow_page_pte()

  • 对于大多数普通页来说follow_page_pte()会检查页不存在和页不可写入两种缺页异常, 然后调用vm_normal_page()根据pte找到对应的页描述符page

  • 找到页描述符后, 会根据flags进行一些操作, 然后返回page, 在这里flags = 0x2017, 也就是如下标志
    • FOLL_WRITE 0x01 : 需要进行写入
    • FOLL_TOUCH 0x02 : 标记一下页面被访问过
    • FOLL_GET 0x04 : 获取页面的引用, 从而让页面锁定在内存中
    • FOLL_FORCE 0x10 : 强制写入只读内存区
    • FOLL_REMOTE 0x2000 : 要访问的不是当前任务的内存空间

 

faultin_page()

  • faultpage()会把flags中的FOLL标志转为handlemm_fault()使用的FAULT标志, 然后调用handle_mm_fault()处理

  • handle_mm_fault()前一个文章已经分析过, 由于FORCE标志, mem可以写入进程只读内存区, 因此会进行进入do_wp_page(), 把只读页复制一份, 替换原有的页
  • 但是与缺页异常的COW不同, 这片VMA自身就是只读的, 因此在COW之后设置PTE时, PTE只有Dirty标志, 而没有RW标志

  • 当do_page_page()处理完毕后, 会返回VM_FAULT_WRITE标志, 表示这个页可以进行写入

  • faultin_page()结束部分是最具有trick的一个地方: COW一个只读页的结果任然是一个只读页, 如果flags有FOLL_WRITE标志, 那么follow_page_mask()会因为写权限问题再次失败, 但此时作为一个复制过来的页可以安全的写入, 所以要去掉FOLL_WRITE标志

 

__get_user_pages()第一次循环

  • 第一次follow_page_mask()时, 由于VMA没有建立映射,因此对应页表项为空, follow_page_mask()会因为pmd_none(*pmd)而失败, 第一次进入faultin_page()
  • faultin_page()沿着下面的调用流程
    • __handle_mm_fault()负责分配各级页表项, 然后调用handle_pte_fault()
    • handle_pte_fault()发现是映射到文件, 但整个PTE为none的情况, 会调用do_fault()处理
    • do_fault()发现需要写入私有文件映射的内存区就会调用do_cow_fault()进行写时复制
faultin_page()
    handle_mm_fault()
        __handle_mm_fault()
            handle_pte_fault()
                do_fault()
                    do_cow_fault()
                        alloc_page_vma()
                        __do_fault()
                        do_set_pte()
  • do_cow_fault()的流程如下
    • 首先调用alloc_page_vma()分配一个新页
    • 然后调用__do_fault()需要找address对应的原始页的描述符
    • 然后调用copy_user_highpage()把原始页的内容复制到新页中
      • 新旧页都被映射到内核地址空间中, 因此复制的时候直接memcpy()就可以
    • 最后调用do_set_pte()设置页表的PTE, 建立反向映射

  • do_set_pte()流程如下, 在本测试程序中, 由于进行COW的VMA区域不可写入, 因此得到的COW页只有脏标志, 没有可写标志
    • 注意这里的set_pte_at(), 会把描述此物理页的pte写入到vma->vm_mm这个地址空间的页表中, 也就是让其他用户进程的虚拟内存映射到这个物理页中.
    • 与此同时, 由于要要进行写入等工作, 内核地址空间也映射到这个物理页. 要注意区分两种映射

 

__get_user_pages()第二次循环

  • 分配到COW页之后, 会再次进入follow_page_mask()进行检查, 由于PTE不可写入, 但是flags中设置了FOLL_WRITE标志, 因此会再次失败

  • 本次faultin_page()沿着下面路径进行
    • 由于要进行写入操作, 并且对应页存在, 因此handle_pte_fault()会调用do_wp_page()进行写时复制
faultin_page()
    handle_mm_fault()
        __handle_mm_fault()
            handle_pte_fault()
                do_wp_page()
                    wp_page_reuse()
                        maybe_mkwrite(pte_mkdirty(entry), vma);
                        return VM_FAULT_WRITE;
  • do_wp_page()流程如下
    • 调用vm_normal_page() 根据address找到对应的页描述符
    • 如果发现是匿名页, 并且此页只有一个引用, 那么会调用wp_page_reuse()直接重用这个页.
    • 第一次faultin_page()时进入do_cow_fault(), 就已经专门复制了一页, 因此会直接进入wp_page_reuse() 重用这个页

  • wp_page_reuse()主要就是设置PTE, 然后返回VM_FAULT_WRITE
    • 注意由于这片VMA不可写入,因此PTE任然没有RW标志,

  • 最后handle_mm_fault()返回到faultin_page()中时, 由于返回了VM_FAULT_WRITE标志, 表示可以写入, 因此会去掉flags中的FOLL_WRITE标志, 不再检查写入权限

 

__get_user_pages()第三次循环

  • 再次进入follow_page_mask(), 由于之前去掉了FOLL_WRITE标志, 因此不会检查PTE有没有写入权限, 从而通过follow_page_mask()返回对应的页

  • 之后会沿着路径返回: get_user_pages() -> get_user_pages_locked() -> get_user_page_remote() -> __access_remote_vm()
  • __access_remote_vm()锁定页面后, 先调用kmap把页面映射到内核地址空间中, 再调用copy_to_user_page()完成从内核缓冲区到对应页面的写入

 

总结

  • 如果/proc/self/mem文件的权限为rw_, 那么通过读写这个文件会强制修改一个一个进程的内存区域,(FORCE标志), 即使这片内存区域只读. 又要有写入的效果, 又不能真的写入原来的只读页, 因此需要先复制原来的只读页, 然后由内核进行写入.
  • 有别于可写入内存的缺页异常, 只读内存区的页进行COW之后得到的仍然是只读页. 如果设置了FOLL_WRITE标志, 那么follow_page_mask()要求对应PTE可写入. 但COW得到的是只读页, 为了避免死循环, 所以在COW之后需要去掉FOLL_WRITE的标志, 表示不用检查可写入权限了
  • 只读页是如何进行写入的? 只读只是相对于用户地址来说的, 如果使用用户地址进行写入, 那么MMU在页表中找到的PTE是只读的, 这个没问题. 同时这个物理页也被映射到内核地址空间中, 如果使用内核地址进行写入, 那么MMU在页表中找到的PTE则是可读可写的.
(完)