首页
关于
Search
1
2023 年保研经验贴 [计算机-末九Rank50%-入北计清深-北计直博]
324 阅读
2
Linux Kernel-THP阅读分析[Hang up]
223 阅读
3
FreeBSD Kernel-编译环境搭建
198 阅读
4
Linux Kernel-编译调试环境搭建
151 阅读
5
CSAPP 1. 计算机系统漫游
139 阅读
内核
源码分析
阅读笔记
Rust
语法
习题
读书笔记
深入理解计算机系统
技术之外
开发
rcore-arm
登录
Search
yyi
累计撰写
46
篇文章
累计收到
0
条评论
首页
栏目
内核
源码分析
阅读笔记
Rust
语法
习题
读书笔记
深入理解计算机系统
技术之外
开发
rcore-arm
页面
关于
搜索到
12
篇与
的结果
2024-03-11
Zswap4BSD接入 BSD Swap
这一步是比较艰巨的,上一篇文章里虽然看上去都是fanyi的锅,实际上问题是我都是处理一些迁移好的依赖(by linuxkpi),而fanyi需要弄一个没有的依赖(没有那么好接,需要读一下源码)到了我们这,Linux和FreeBSD的swap区别还是蛮大,我将会首先梳理Linux中是如何在swap之前先调用frontswap的,再梳理FreeBSD的swap到底是怎么运转的,最后再选择一种合理的方式(尽量照抄Linux)把frontswap接入到 BSD 中。Frontswap的一探究竟Frontswap作为一个中间层,为优化方式提供了一种接口,使得页面在真正换出到swap设备之前可以先进入frontswap,如果frontswap成功,则不必再换出。从下层接口看起struct frontswap_ops { void (*init)(unsigned); /* this swap type was just swapon'ed */ int (*store)(unsigned, pgoff_t, struct page *); /* store a page */ int (*load)(unsigned, pgoff_t, struct page *); /* load a page */ void (*invalidate_page)(unsigned, pgoff_t); /* page no longer needed */ void (*invalidate_area)(unsigned); /* swap type just swapoff'ed */ };这里没加参数名字,但是那个unsigned都是type, pgoff_t 都是offset就好了。现在frontswap只有一个driver,即zswap,所以我们都会结合zswap来说。在我理解,一个type对应的是一个swap设备,或者说,我们每次做swapon系统调用的时候,那个对应的文件就是一个设备,当成一个逻辑设备就好了(毕竟万物皆文件嘛)。在这种基础上,我们就很好理解了,frontswap要求driver可以区分不同的设备,根据不同的设备初始化、插入、加载和删除页面。以及可以失效一整个设备的所有页面。上层接口extern void frontswap_init(unsigned type, unsigned long *map); extern int __frontswap_store(struct page *page); extern int __frontswap_load(struct page *page); extern void __frontswap_invalidate_page(unsigned, pgoff_t); extern void __frontswap_invalidate_area(unsigned);相对应的,frontswap也为上层提供了一系列接口,其中参数大差不差,store和load会提取page中的private字段,最后拆成type和offset,再塞给下层,这可能是我们要在过程中关注的一点,一个页面在什么时候会获得这个private,它是怎么跟pid和虚拟内存的offset对应上的?我们在FreeBSD里如何把swap的vm_object和offset 跟这个private对应起来?我们好像没有那么多地方能存放数据。总不能把zswap改成二重索引的树套树。除此之外,frontswap还在对应swap设备的控制结构swap_info_struct中留下了一个unsigned long *类型的参数map,用于中间层校验当前的page是否在frontswap中存在,及相应的是否有效上层接口到下层接口1. Driver的注册在做zswap的时候我们已经很明显的看到了,frontswap用register函数注册相关的控制结构int frontswap_register_ops(const struct frontswap_ops *ops) { if (frontswap_ops) return -EINVAL; frontswap_ops = ops; static_branch_inc(&frontswap_enabled_key); return 0; }注册的过程伴随着swap设备的swapon,但是存在一种情况即backend driver还没准备好,我们就要往里写了,所以frontswap的每个操作都会在操作前首先检测注册情况2. initvoid frontswap_init(unsigned type, unsigned long *map) { struct swap_info_struct *sis = swap_info[type]; VM_BUG_ON(sis == NULL); if (WARN_ON(!map)) return; frontswap_map_set(sis, map); if (!frontswap_enabled()) return; frontswap_ops->init(type); }这个map是个bitmap,用来判断一个页面是不是在frontswap里,初始化的时候,要新建一个map赋给这个设备的swap_info3. test & set & clear听起来就像bit操作,实际上也确实是,这两个函数根据给定的swap_info和offset,把map的第offset位返回/置1/清除4. store & load终于到了比较核心的地方int __frontswap_store(struct page *page) { int ret = -1; swp_entry_t entry = { .val = page_private(page), }; int type = swp_type(entry); struct swap_info_struct *sis = swap_info[type]; pgoff_t offset = swp_offset(entry); if (__frontswap_test(sis, offset)) { __frontswap_clear(sis, offset); frontswap_ops->invalidate_page(type, offset); } ret = frontswap_ops->store(type, offset, page); if (ret == 0) { __frontswap_set(sis, offset); inc_frontswap_succ_stores(); } else { inc_frontswap_failed_stores(); } return ret; } 就和我们之前说的一样,首先从private中拆出来private字段,拆成type和offset,验证当前页面是否在frontswap中已经出现了,如果出现,首先干掉,然后再把页面塞进去int __frontswap_load(struct page *page) { int ret = -1; swp_entry_t entry = { .val = page_private(page), }; int type = swp_type(entry); struct swap_info_struct *sis = swap_info[type]; pgoff_t offset = swp_offset(entry); if (!__frontswap_test(sis, offset)) return -1; ret = frontswap_ops->load(type, offset, page); if (ret == 0) inc_frontswap_loads(); return ret; }load也差不多,这边如果检测到不在,那就直接返回失败,否则load一下。至于后面的两个invalid,我们等到他们被调用的时候再看好了。那它在哪被调用呢?别急,我们先去看看Linux的Swap吧Linux Swap的浅述swap应该是大家都听过的概念,废话不多说,不管是linux还是unix,只有匿名页面在需要内存回收的时候,需要放在swap分区中,以便重新访问的时候调入内存,不然只能OOM Kill了,而以文件做backend的页面可能是不需要换出的(可读写页面可能会分情况,比如出现了CoW之后再设置为可以换出的页面)什么时候发生swap?周期性检查,保证内存有充足空间:mm/vmscan.c - kswapd()直接内存回收,当内存不够用的时候: mm/page_alloc.c - __alloc_pages_slowpath我们关心的第一个问题:swp_entry_t 从哪里来,又到哪里去来看一张图swp_entry就是一个long,和一个PTE等长,正好可以放进一个页表作为一个页表项,当swap out的时候,linux在回收物理页面之前先修改这个PTE,这样下次访问到这个PTE的时候就会触发一个Page Fault,再通过这个swp_entry_t找到原来这个页面在swap中的位置,进而换出,再把这个表项修改成真的PTE。PTE的如下三种情况全为0:该页不属于进程的地址空间,或相应的页框尚未分配给进程。最低位为0,其余位不全为0:该页现在在swap分区中。最低位为1:present位为1,表示该页在内存中。直接转换。至于FreeBSD怎么办,我们一会再想,反正现在是知道这个entry是咋来的了swap是怎么调用frontswap的?失败了怎么办?什么时候initSYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags) { // ... if (IS_ENABLED(CONFIG_FRONTSWAP)) frontswap_map = kvcalloc(BITS_TO_LONGS(maxpages), sizeof(long), GFP_KERNEL); // ... enable_swap_info(p, prio, swap_map, cluster_info, frontswap_map); // ... } static void enable_swap_info(struct swap_info_struct *p, int prio, unsigned char *swap_map, struct swap_cluster_info *cluster_info, unsigned long *frontswap_map) { if (IS_ENABLED(CONFIG_FRONTSWAP)) frontswap_init(p->type, frontswap_map); }swapon的时候,这个调用链路实在太明确啦,就不说了,因此我们大概也是要找到FreeBSD对应的Swapon,幸运的是,这个应该不是很难。什么时候Store肯定是先Store再load啦,我们先看Store好了。在mm/page_io.c下int swap_writepage(struct page *page, struct writeback_control *wbc) { // some other code if (frontswap_store(&folio->page) == 0) { folio_start_writeback(folio); folio_unlock(folio); folio_end_writeback(folio); return 0; } __swap_writepage(&folio->page, wbc); return 0; }如果走frontswap成功,就不走后面的write_page了,调用start_writeback,然后结束流程。然而这块很显然出现了一个问题,folio_xx_writeback是用来干啥的呢?按理说,我们是在swap之前就把该unmap的已经unmap掉了,最核心的写入部分也搞完了,应该只能有点收尾工作,不能有writeback啊?这个问题稍后再看,我们把frontswap看完什么时候load读取的部分是+号位有点长void swap_readpage(struct page *page, bool synchronous, struct swap_iocb **plug) { struct swap_info_struct *sis = page_swap_info(page); bool workingset = PageWorkingset(page); unsigned long pflags; bool in_thrashing; VM_BUG_ON_PAGE(!PageSwapCache(page) && !synchronous, page); VM_BUG_ON_PAGE(!PageLocked(page), page); VM_BUG_ON_PAGE(PageUptodate(page), page); /* * Count submission time as memory stall and delay. When the device * is congested, or the submitting cgroup IO-throttled, submission * can be a significant part of overall IO time. */ if (workingset) { delayacct_thrashing_start(&in_thrashing); psi_memstall_enter(&pflags); } delayacct_swapin_start(); if (frontswap_load(page) == 0) { SetPageUptodate(page); unlock_page(page); } else if (data_race(sis->flags & SWP_FS_OPS)) { swap_readpage_fs(page, plug); } else if (synchronous || (sis->flags & SWP_SYNCHRONOUS_IO)) { swap_readpage_bdev_sync(page, sis); } else { swap_readpage_bdev_async(page, sis); } if (workingset) { delayacct_thrashing_end(&in_thrashing); psi_memstall_leave(&pflags); } delayacct_swapin_end(); }别看这里多了几个if else,其实store只是把这几个if-else包了一层,如果失败自动回归原流程,好像也没啥需要特别注意的。至于range_free(invalidate)什么的,我们倒是不着急,先把核心做出来吧.
2024年03月11日
25 阅读
0 评论
0 点赞
2024-02-20
FreeBSD Arch Handbook阅读笔记 #7 VM
https://docs.freebsd.org/en/books/arch-handbook/vm/7.1 vm_page_tvm_page_t 管理物理内存,每个vm_page_t对应一个物理页面,vm_page_t属于不同双向链表队列。主要包括活动、非活动、缓存或空闲。缓存和空闲的分页队列更加复杂,会根据L1和L2 Cache的大小分成多个队列。申请新页面的时候也会从Cache的角度来申请对齐的页面。页面通过引用计数持有,通过繁忙计数锁定。VM system通过LRU来对页面进行降级。对于Weird类型的页面,不在队列里,通常是载有一个页表。空闲链表里的页面是真正空闲的,而Cache链表里的页面随时可被重用。当进程访问一个不在页表里的页面时,如果页面在页面队列里,页错误就会导致更少的开销。否则只能从外存读入了。pageout守护进程负责:动态调整分页队列、维持各个队列页面合理比例维持脏页面和干净页面的细分(清洗脏页、升降级)VM会尝试在产生合理的页错误量级的前提下帮助决策何时换出和清洗页面。7.2 vm_object_tFreeBSD实现了一个通用内存对象,这个对象的backend可以是:TypesunbackedSwap-backedPhysical device backedFile-backedVM对象可以被遮蔽,具体可以看我的另一篇笔记https://yirannn.com/source_code/vm_sys.html比较常见的应用就是cow一个vm_page_t同时只能和一个vm_object关联。而不同实例的内存共享由遮蔽特性提供7.3 struct buf对于vnode作为back的vm对象,比如file,对脏页信息的维护和vm对脏页信息的维护是相互独立的。在写回脏页时,vm需要在写回前清除dirty标记。另外文件也需要将文件映射到KVM才能进行操作,这种结构被称为文件系统缓冲区,一般是struct buf,文件系统需要对VM进行操作时,把这个vm对象转换为一个buf,然后把buf中的页面映射在KVM里。同理,磁盘IO也是先把vm_object映射到buf,再在buf上发出IO。7.4 vm_map_t, vm_entry_tFreeBSD通过vm_map_t结构把object和虚拟内存范围相关联,页表通过vm_map_t/vm_entry_t/vm_object_t 直接合成。这一段我其实没太读懂,但是我感觉笔者意思是一个物理页不光是和一个vm_object联系,同时也和页表项联系。但是对于同个object的同个页面,对应的vm_page_t都相同7.5 KVM 内存映射FreeBSD用KVM保存内核结构,其中最大的就是7.3中提到的buf。FreeBSD不会把所有物理内存都映射到KVM,主要利用区域分配器管理KVM
2024年02月20日
37 阅读
0 评论
0 点赞
2024-02-13
部署一个基于VMWare的FreeBSD工作环境
概述在开发FreeBSD Kernel的过程中,发现基于Linux交叉编译的方法实在是过于不便,大量FreeBSD的脚本都是为了BSD自己准备的,因此搭建一个FreeBSD的开发环境,以便未来减少阻力。该环境至少应该能支持:基础的代码浏览与修改功能基础的代码提示、补全与Linter完备的编译环境测试编译产生的内核的虚拟机在这里,编译环境应该是相对好解决的,我们需要自己找到代码提示补全和虚拟机的方案。FreeBSD直接下载Index of /releases/VM-IMAGES/13.2-RELEASE/amd64/Latest/,然后新建虚拟机加载即可如果用vmware,安个tools先pkg install open-vm-toolsGUI换个源先FreeBSD pkg 源使用帮助 ‒ USTC Mirror Help 文档qcow出现的问题vmdk也一样,需要给虚拟磁盘扩容下先https://developer.aliyun.com/article/758140?spm=a2c6h.12873581.0.0.34342784yoZBWHpkg install xorg xfce startxfce4共享文件夹挂载很坑,这里只提供vmware的在 /boot/loader.conf加入fusefs_load="YES"在 /etc/fstab加入.host:/ /root/mnt/hgfs fusefs rw,mountprog=/usr/local/bin/vmhgfs-fuse 0 0安装toolspkg install open-vm-tools mkdir -p /mnt/hgfs rebootreboot之后可以执行kldstat这里应该有个fusefs.ko就对了此时可以看看/mnt/hgfs下有没有内容了(当然前提是Vmware里配了)到此为止,可以通过共享文件夹的方式进行编译了(但是灰常慢)为FreeBSD配置SSH修改/etc/inetd.conf,把ssh tcp前面的注释干掉修改/etc/rc.conf 加入 sshd_enable="YES"修改/etc/ssh/sshd_config把 PasswordAuthentication 的 no改为yes,注释去掉,允许PAM登录【可选的,我没开其实】把 PermitRootLogin no 改为 yes,注释去掉【可选的,我不是生产环境,用root方便】把 PubkeyAuthentication 注释去掉,允许通过key登录,然后自己配个authorized_keys查看虚拟机ip:用passwd改个密码此时就可以ssh上来啦配置sudo安装个sudo、创建个用户pkg install sudo adduser添加到wheel组,为了可以su到rootpw group mod wheel -m yyi设置sudoersvisudo这行注释去掉测试一下即可配置zsh+ohmyzsh我的这些shell基本上都是一样的ohmyzsh+powerlevel10k,不赘述了配置vscode-remote[失败了]Vscode 的 ssh-remote不支持freebsd,但是配置一下并不难启用一下freebsd的linux兼容模式sudo sysrc linux_enable="YES" sudo service linux start sudo pkg install linux_base-c7vscode-remote最后也没成功,因为remote一定要在ssh后面加个命令(即使我在config中已经制定了remote命令)还是选择git同步吧。最后记得打个快照
2024年02月13日
39 阅读
0 评论
0 点赞
2023-10-06
FreeBSD Kernel-VM System
FreeBSD Kernel-VM System1. VM Objects每个用户进程都看到一个单独的、私有的、连续的VM地址空间。该空间包含了不同种类的内存对象。程序的代码段和数据段实际上是一个内存映射的文件,代码段只读,数据段写时复制(copy-on-write), BSS段按需分配,并且填充 0 (demand zero page fill)。任意文件也可以被映射到地址空间,如共享库。对于一个写时复制的基本页,当它被加载到虚拟内存空间中,只初始化内存映射,并在之后直接返回程序的二进制段。允许VM系统可以释放或者重用这个页面,并在晚些时候把它再加载回来。当进程修改这些数据时,VM系统必须为进程提供一份私有的复制。因为这个私有复制已经被修改过了,VM系统可能不会释放它,因为以后无法恢复。当发生fork系统调用时,复杂性会进一步增加,两个进程都有自己的私有地址空间。显然,在fork发生时制作完整的内存副本是非常不优雅的,FreeBSD通过分层的VM对象模型管理这些内容。原始的程序文件是最低的VM对象层。当进程启动后,VM系统创建一个对象层AA表示可以从物理介质调入调出的文件页面。从磁盘调入是正常操作,但是我们往往不想覆写可执行程序,因此VM系统创建一个由Swap提供物理支持的对象层B当第一次发生写操作时,在B中创建一个新的页面,该页面的内容从A初始化而来,该页面可以调度到swap设备上。当程序 fork 时,VM系统创建两个新的对象层:C1和C2,其中C1是父进程、C2是子进程在此情况下,当B中的一个页面被父进程修改,则该进程触发一个COW错误,并在C1中复制这个页面,并把原始的页面留存在B中。当子进程修改同一个页面时,也会触发一个COW错误,并在C2中复制这个页面。此时原始的页面在B中已经完全隐去了,当B不代表真实文件时,可能会被销毁,然而FreeBSD没有做这种优化。假设此时,子进程执行了exec(),它的当前地址空间会被代表新文件的新地址空间替换,此时C2被破坏。此时B只有一个子层,并且对B的访问全部都会通过C1,因此B和C1会折叠在一起,同时存在与B和C1的页面会在这种过程中被删除。这种模型带来了一些问题,第一个是VM对象栈很深,会导致触发错误的时候扫描时间。第二个问题是可以能会在对象栈深处遇到死页面、不可访问的页面。(凭我理解,死页应该是永远不会被访问到的页面,比如当进程A执行完毕,进程B永远不会访问到数据A')。FreeBSD 提供了一种名为 All Shadowed Case 的特殊优化来解决这种过深层栈导致的问题。如果C1或C2导致的COW错误已经足以完全隐藏B中的层,以C1为例,我们就可以让C1跳过B,变为C1-> A和C2->B->A,此时B只有一个子层引用(C2),B和C2可以被折叠到一起,表现得结果就是B被完全删除。
2023年10月06日
73 阅读
0 评论
0 点赞
2023-09-09
Linux Kernel-Zpool 接口阅读
Linux Kernel-Zpool 接口阅读最近还是在做zswap的迁移,需要整理一下zpool的接口功能,以决定是迁移过去一个现成的还是自己实现一个简单的。1 Zpool作用Zpool只是一个接口层,由zbud、zsmalloc等进行具体实现。Zpool提供了一个内存池,用以存放被压缩的内存。因为它是一个接口,其实我们只需要简单的关注注册数据结构,就能了解到它的一些关键作用。2 Driver Structstruct zpool_driver { char *type; struct module *owner; atomic_t refcount; struct list_head list; void *(*create)(const char *name, gfp_t gfp, const struct zpool_ops *ops, struct zpool *zpool); void (*destroy)(void *pool); bool malloc_support_movable; int (*malloc)(void *pool, size_t size, gfp_t gfp, unsigned long *handle); void (*free)(void *pool, unsigned long handle); int (*shrink)(void *pool, unsigned int pages, unsigned int *reclaimed); bool sleep_mapped; void *(*map)(void *pool, unsigned long handle, enum zpool_mapmode mm); void (*unmap)(void *pool, unsigned long handle); u64 (*total_size)(void *pool); };refcount、list的作用都是字面意思。Type是当前driver的名字,索引driver的时候会用到。下面的函数是zpool后端提供给接口的实现,我们会在下一节里介绍其作用。3 暴露接口3.1 zpool_register_drivervoid zpool_register_driver(struct zpool_driver *driver); int zpool_unregister_driver(struct zpool_driver *driver)该函数暴露给所有zpool的实现,由他们在启动时注册。该函数初始化了引用计数、将被注册的driver加入到zpool的链表中。与其相对应的是zpool_unregister_driver,注销driver该接口评估为不需要迁移。3.2 zpool_get_driverstatic struct zpool_driver *zpool_get_driver(const char *type) static void zpool_put_driver(struct zpool_driver *driver)给定一个zpool驱动名字,返回对应的zpool_driver。并且增加对应driver的引用计数。与其相对应的是zpool_put_driver该接口评估为不需要迁移。3.3 zpool_has_pool、zpool_create_pool、zpool_destry_poolbool zpool_has_pool(char *type) struct zpool *zpool_create_pool(const char *type, const char *name, gfp_t gfp, const struct zpool_ops *ops) void zpool_destroy_pool(struct zpool *zpool)分别承担检测给定的driver是否可用、创建一个指定类型的zpool和销毁一个已经存在的zpool。该接口评估为不需要迁移3.4 zpool_malloc_support_movablebool zpool_malloc_support_movable(struct zpool *zpool)返回driver是否支持迁移的字段,评估为不需要迁移。3.5 driver实现函数3.5.1 zpool_malloc、zpool_free、zpool_shrinkint zpool_malloc(struct zpool *zpool, size_t size, gfp_t gfp, unsigned long *handle) void zpool_free(struct zpool *zpool, unsigned long handle) int zpool_shrink(struct zpool *zpool, unsigned int pages, unsigned int *reclaimed)在给定的zpool中申请大小为size的空间,并把申请得到的句柄放在handle中在给定的zpool中释放给定的handle的空间。zpool_shrink可能未被实现,如果实现,尝试驱逐当前正在使用的句柄。3.5.2 zpool_map_handle、zpool_unmap_handlevoid *zpool_map_handle(struct zpool *zpool, unsigned long handle, enum zpool_mapmode mapmode) void zpool_unmap_handle(struct zpool *zpool, unsigned long handle)根据给定的pool和handle,映射到连续的地址上取消映射,解锁相关的锁3.5.3 zpool_get_total_sizeu64 zpool_get_total_size(struct zpool *zpool)获取zpool所占用的内存空间
2023年09月09日
106 阅读
0 评论
0 点赞
1
2
3