Description of problem: 请教一个asyncfork的问题。打开以下开关 CONFIG_ASYNC_FORK=y CONFIG_DEBUG_VM=y 然后做asyncfork的并发测试,出现以下bug_on Modules linked in: CPU: 6 PID: 510 Comm: a.out Not tainted 6.6.0+ #238 kernel BUG at include/linux/mm.h:2070! Hardware name: QEMU QEMU Virtual Machine, BIOS 0.0.0 02/06/2015 ... Call trace: copy_pte_range+0x2518/0x6be8 __copy_page_range+0x428/0xdd0 async_fork_fixup_pmd+0x3c4/0xa90 __handle_mm_fault+0x28c/0x950 handle_mm_fault+0x110/0x958 do_page_fault+0x698/0xd70 bugon的代码是: folio_needs_cow_for_dma { VM_BUG_ON(!(raw_read_seqcount(&vma->vm_mm->write_protect_seq) & 1)); ... } 看了一下BUGON的原因,是因为copy_pte_range流程中,没有增加vma->vm_mm->write_protect_seq的信号量保护。 看了一下社区引入write_protect_seq的历史,是为了保护fork流程与pinned流程的并发,防止pinned返回不正确的结果,该信号量是一个只增不减的信号量,初始值为0,fork流程中页表拷贝之前信号量加一,拷贝结束,信号量再加一。如果其二进制值第0位为1,表示信号量为奇数,代表处于fork的页表拷贝流程中。 asyncfork引入之后,导致页表拷贝的触发点变多了,导致这种对应关系(如果为奇数,表示处于页表拷贝流程中,如果是偶数,代表没有处于页表拷贝,pinned功能不受影响)被破坏。 asyncfork实现考虑到了这个问题(点赞!)。代码流程中使用seq_should_lock = false来解决问题,场景是如果当前处于asyncfork的CPR_SLOW模式页表拷贝上下文,并且async_fork_refcnt计数不为1,表示前面已经有进程也处于CPR_SLOW模式页表拷贝上下文,这种情况下认为不再需要进行write_protect_seq的保护了,即不增加write_protect_seq的信号量。 但是上面的修复还是有问题的。进程A首先进入了asyncfork的CPR_SLOW模式页表拷贝上下文,增加了write_protect_seq的信号量,拷贝还未结束时,进程B也进来了(能并发的原因很多地方都是持有mm的读锁)到了这里,发现A已经加过了,进程B就不加了,但是此时如果很快A结束了,B进程还在拷贝,那么此时进程B的页表拷贝就会出现社区修复的问题,与pinned并发会有问题。时序图如下 ------------------------------------------------------------------------------- 进程A 进程B 进程C mode = CPR_SLOW mode = CPR_SLOW add write_protect_seq not add write_protect_seq copy page table sub write_protect_seq get_user_pages_fast() copy page table atomic_read(has_pinned) == 0 page_maybe_dma_pinned() == false atomic_set(has_pinned, 1); pte = pte_wrprotect(pte) ------------------------------------------------------------------------------- 综上所述,主要是两个问题: 1)如何避免开启CONFIG_DEBUG_VM开关之后的bugon问题; 2)目前分析asyncfork没有完全解决write_protect_seq信号量的问题,与user pinned的流程并发还是存在社区描述的问题。 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=57efa1fe5957694fa541c9062de0a127f0b9acb0 这个是基于龙蜥开源代码的分析,如果你们已有修复或者不妥当的地方敬请大佬指出,感谢! How reproducible: 开启asyncfork功能,fork之后,在父进程中进行并发的父进程数据修改,触发COW机制,回退asyncfork的页表 Steps to Reproduce: 1.开启asyncfork控制开关 2.mmap一段4M内存不使用大页,然后写入数据,然后fork() 3.父进程中至少2个线程,并发写访问步骤2的地址,高概率复现该问题 Actual results: 内核panic Expected results: 系统正常运行 Additional info:
(In reply to zhangqilong from comment #0) > Description of problem: > 请教一个asyncfork的问题。打开以下开关 > > CONFIG_ASYNC_FORK=y > CONFIG_DEBUG_VM=y > > 然后做asyncfork的并发测试,出现以下bug_on > Modules linked in: > CPU: 6 PID: 510 Comm: a.out Not tainted 6.6.0+ #238 > kernel BUG at include/linux/mm.h:2070! > Hardware name: QEMU QEMU Virtual Machine, BIOS 0.0.0 02/06/2015 > ... > Call trace: > copy_pte_range+0x2518/0x6be8 > __copy_page_range+0x428/0xdd0 > async_fork_fixup_pmd+0x3c4/0xa90 > __handle_mm_fault+0x28c/0x950 > handle_mm_fault+0x110/0x958 > do_page_fault+0x698/0xd70 > > bugon的代码是: > folio_needs_cow_for_dma > { > VM_BUG_ON(!(raw_read_seqcount(&vma->vm_mm->write_protect_seq) & 1)); > ... > } > > 看了一下BUGON的原因,是因为copy_pte_range流程中,没有增加vma->vm_mm->write_protect_seq的信号量保护。 > > 看了一下社区引入write_protect_seq的历史,是为了保护fork流程与pinned流程的并发,防止pinned返回不正确的结果, > 该信号量是一个只增不减的信号量,初始值为0,fork流程中页表拷贝之前信号量加一,拷贝结束,信号量再加一。 > 如果其二进制值第0位为1,表示信号量为奇数,代表处于fork的页表拷贝流程中。 > > asyncfork引入之后,导致页表拷贝的触发点变多了,导致这种对应关系(如果为奇数,表示处于页表拷贝流程中,如果是偶数,代表没有处于页表拷贝,pinne > d功能不受影响)被破坏。 > > asyncfork实现考虑到了这个问题(点赞!)。代码流程中使用seq_should_lock = > false来解决问题,场景是如果当前处于asyncfork的CPR_SLOW模式页表拷贝上下文,并且async_fork_refcnt计数不为1,表示前面 > 已经有进程也处于CPR_SLOW模式页表拷贝上下文,这种情况下认为不再需要进行write_protect_seq的保护了,即不增加write_protec > t_seq的信号量。 > > 但是上面的修复还是有问题的。 > 进程A首先进入了asyncfork的CPR_SLOW模式页表拷贝上下文,增加了write_protect_seq的信号量,拷贝还未结束时,进程B也进来了( > 能并发的原因很多地方都是持有mm的读锁)到了这里,发现A已经加过了,进程B就不加了,但是此时如果很快A结束了,B进程还在拷贝,那么此时进程B的页表拷贝就会 > 出现社区修复的问题,与pinned并发会有问题。时序图如下 > > ----------------------------------------------------------------------------- > -- > 进程A 进程B 进程C 这里写错了,应该是线程 > mode = CPR_SLOW mode = CPR_SLOW > > add write_protect_seq > > not add write_protect_seq > > copy page table > > sub write_protect_seq > > get_user_pages_fast() > > copy page table > atomic_read(has_pinned) == 0 > page_maybe_dma_pinned() == false > > atomic_set(has_pinned, > 1); > > pte = pte_wrprotect(pte) > ----------------------------------------------------------------------------- > -- > 综上所述,主要是两个问题: > 1)如何避免开启CONFIG_DEBUG_VM开关之后的bugon问题; > 2)目前分析asyncfork没有完全解决write_protect_seq信号量的问题,与user pinned的流程并发还是存在社区描述的问题。 > https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/ > ?id=57efa1fe5957694fa541c9062de0a127f0b9acb0 > > 这个是基于龙蜥开源代码的分析,如果你们已有修复或者不妥当的地方敬请大佬指出,感谢! > > How reproducible: > > 开启asyncfork功能,fork之后,在父进程中进行并发的父进程数据修改,触发COW机制,回退asyncfork的页表 > > > Steps to Reproduce: > 1.开启asyncfork控制开关 > 2.mmap一段4M内存不使用大页,然后写入数据,然后fork() > 3.父进程中至少2个线程,并发写访问步骤2的地址,高概率复现该问题 > > Actual results: > 内核panic > > Expected results: > 系统正常运行 > > Additional info:
(In reply to zhangqilong from comment #0) > Description of problem: > 请教一个asyncfork的问题。打开以下开关 > > CONFIG_ASYNC_FORK=y > CONFIG_DEBUG_VM=y > > 然后做asyncfork的并发测试,出现以下bug_on > Modules linked in: > CPU: 6 PID: 510 Comm: a.out Not tainted 6.6.0+ #238 > kernel BUG at include/linux/mm.h:2070! > Hardware name: QEMU QEMU Virtual Machine, BIOS 0.0.0 02/06/2015 > ... > Call trace: > copy_pte_range+0x2518/0x6be8 > __copy_page_range+0x428/0xdd0 > async_fork_fixup_pmd+0x3c4/0xa90 > __handle_mm_fault+0x28c/0x950 > handle_mm_fault+0x110/0x958 > do_page_fault+0x698/0xd70 > > bugon的代码是: > folio_needs_cow_for_dma > { > VM_BUG_ON(!(raw_read_seqcount(&vma->vm_mm->write_protect_seq) & 1)); > ... > } > > 看了一下BUGON的原因,是因为copy_pte_range流程中,没有增加vma->vm_mm->write_protect_seq的信号量保护。 > > 看了一下社区引入write_protect_seq的历史,是为了保护fork流程与pinned流程的并发,防止pinned返回不正确的结果, > 该信号量是一个只增不减的信号量,初始值为0,fork流程中页表拷贝之前信号量加一,拷贝结束,信号量再加一。 > 如果其二进制值第0位为1,表示信号量为奇数,代表处于fork的页表拷贝流程中。 > > asyncfork引入之后,导致页表拷贝的触发点变多了,导致这种对应关系(如果为奇数,表示处于页表拷贝流程中,如果是偶数,代表没有处于页表拷贝,pinne > d功能不受影响)被破坏。 > > asyncfork实现考虑到了这个问题(点赞!)。代码流程中使用seq_should_lock = > false来解决问题,场景是如果当前处于asyncfork的CPR_SLOW模式页表拷贝上下文,并且async_fork_refcnt计数不为1,表示前面 > 已经有进程也处于CPR_SLOW模式页表拷贝上下文,这种情况下认为不再需要进行write_protect_seq的保护了,即不增加write_protec > t_seq的信号量。 > > 但是上面的修复还是有问题的。 > 进程A首先进入了asyncfork的CPR_SLOW模式页表拷贝上下文,增加了write_protect_seq的信号量,拷贝还未结束时,进程B也进来了( > 能并发的原因很多地方都是持有mm的读锁)到了这里,发现A已经加过了,进程B就不加了,但是此时如果很快A结束了,B进程还在拷贝,那么此时进程B的页表拷贝就会 > 出现社区修复的问题,与pinned并发会有问题。时序图如下 > > ----------------------------------------------------------------------------- > -- > 进程A 进程B 进程C > mode = CPR_SLOW mode = CPR_SLOW > > add write_protect_seq > > not add write_protect_seq > > copy page table > > sub write_protect_seq > > get_user_pages_fast() > > copy page table > atomic_read(has_pinned) == 0 > page_maybe_dma_pinned() == false > > atomic_set(has_pinned, > 1); > > pte = pte_wrprotect(pte) > ----------------------------------------------------------------------------- > -- > 综上所述,主要是两个问题: > 1)如何避免开启CONFIG_DEBUG_VM开关之后的bugon问题; > 2)目前分析asyncfork没有完全解决write_protect_seq信号量的问题,与user pinned的流程并发还是存在社区描述的问题。 看到了以下适配: ->gup_pte_range // 已经获取页表锁 ->if (is_pmd_copied_slow(pmd)) 貌似可以解决这个问题,我再想想。 > https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/ > ?id=57efa1fe5957694fa541c9062de0a127f0b9acb0 > > 这个是基于龙蜥开源代码的分析,如果你们已有修复或者不妥当的地方敬请大佬指出,感谢! > > How reproducible: > > 开启asyncfork功能,fork之后,在父进程中进行并发的父进程数据修改,触发COW机制,回退asyncfork的页表 > > > Steps to Reproduce: > 1.开启asyncfork控制开关 > 2.mmap一段4M内存不使用大页,然后写入数据,然后fork() > 3.父进程中至少2个线程,并发写访问步骤2的地址,高概率复现该问题 > > Actual results: > 内核panic > > Expected results: > 系统正常运行 > > Additional info: