filemap_fault à__filemap_get_folio àfilemap_get_pages àfilemap_add_folioà__folio_set_locked(设置page的PG_locked)

filemap_read_folioàfolio_wait_locked_killableàfolio_wait_bit_killable(folio, PG_locked) –> folio_wait_bit_common(等IO完成PG_locked被清除)

mpage_read_end_ioàfolio_mark_uptodateàfolio_unlock(IO完成时会标记page的PG_update,同时清除PG_locked)。

简单来说,当发生文件页pagefault,所需内存不在文件缓存时,会分配page页面,发起IO读操作,但这里IO读操作仅下发IO读请求,还不能保证page中已经读取到所需内容,因此会在下发IO读请求前设置了PG_locked标记,当IO完成时,会清除PG_locked标记。这也就是为什么可以在systrace中看到blockio黄条时一般会blocked reson为folio_wait_bit_killable的原因。

同理,匿名页发生page fault时也是类似流程,只是如果使用zram时,没有实际的IO而已。整个过程同样也是由PG_locked控制页面等待及完成读取(解压缩)的并发过程。

2.2lru_lock

Linux内核在内存回收是使用LRU(Last Recent Used)算法,即最近最少使用算法。在内存回收时扫描active和inactive LRU链表进行。lruvec结构体中有一个自旋锁保护LRU链表的操作过程的并发问题。

struct lruvec {struct list_head lists[NR_LRU_LISTS];/* per lruvec lru_lock for memcg */spinlock_t lru_lock;}

shrink_inactive_list和shrink_active_list等典型的操作LRU的过程都需要持有此锁。下面以shrink_inactive_list为例,列举了锁的使用过程。

2.3mmap_lock

Linux内核用vma表示进程地址空间,进程地址的访问受mmap_lock锁保护。mmap_lock是定义在mm_struct中的读写信号量成员。

struct mm_struct {…struct rw_semaphore mmap_lock;…}

mmap_lock保护进程虚拟地址vma rbtree、vma list、vma flags等。进程发生page fault缺页,mmap, mprotect等访问vma的操作时,可能会持有该锁。

获取mmap_lock写锁一般以下典型接口:

mmap_write_trylock

mmap_write_lock_killable

mmap_write_lock

mmap_write_lock_nested

我们以mmap_write_lock_killable为例看下具体的API实现:

#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)static inline int __down_write_killable(struct rw_semaphore *sem){return __down_write_common(sem, TASK_KILLABLE);}int __sched down_write_killable(struct rw_semaphore *sem){might_sleep;rwsem_acquire(&sem->dep_map, 0, 0, _RET_IP_);if (LOCK_CONTENDED_RETURN(sem, __down_write_trylock,__down_write_killable)) {rwsem_release(&sem->dep_map, _RET_IP_);return -EINTR;}return 0;}static inline int mmap_write_lock_killable(struct mm_struct *mm){int ret;__mmap_lock_trace_start_locking(mm, true);ret = down_write_killable(&mm->mmap_lock);__mmap_lock_trace_acquire_returned(mm, true, ret == 0);return ret;}

从上面可以看出down_write_killable获取mmap_lock过程可能会sleep,因此调用者需要注意不能使用在不可睡眠上下文。如果成功获取到锁,返回0,如果获取不到锁(锁已经被其它线程持有),会设置当前进程为TASK_KILLABLE状态,TASK_KILLABLE状态其实就是可杀的D状态,从宏定义可以到它是TASK_WAKEKILL | TASK_UNINTERRUPTIBLE的组合。

在systrace中经常可以看到这几个函数的block reson的紫色的uninterruptable sleep的D状态。

mprotect可以用来修改一段指定内存区域的保护属性。由于它会修改进程vma区域flag,因此,为了处理并发问题,需要mmap_lock的保护。我们以mprotect为例,分析mmap_lock的使用。

内核中的 mmap_lock(在5.15之前的内核版本中称为 mmap_sem)锁,实际上是一个读写锁,可以支持并发的多线程读访问。然而,在某些情况下,即使需要获取读取者(reader)读锁,也需要等待,这时该锁实际上就变成了互斥锁。这种情况常见于下面两种场景:

1)线程1持有写锁,线程2尝试获取读锁。

线程1持有写锁时,线程2需要等待直到线程1释放写锁。这种行为确保了在有写者的情况下,其他线程(包括读者)将被阻塞。这种等待直到写锁释放的模式类似于互斥锁的行为。

2) 线程1持有读锁,线程2尝试获取写锁,后续线程3也尝试获取读锁:

线程1持有读锁时,线程2尝试获取写锁而处于等待状态。由于写者优先级高于读者,因此线程3即使尝试获取读锁也会处于等待状态,直到写锁被释放。这保证了在需要修改共享数据时,写者优先级最高,而后续读者需要等待写锁释放后才能获取读锁。

因此,即使是一个读写锁,在特定条件下也可能会表现出类似于互斥锁的行为,以保证对共享资源操作的正确性和一致性。

2.4anon_vma->rwsem

Linux内核内存紧张时会进行内存回收。内存回收通过反向映射rmap机制找到page所有映射的vma并进行解除映射。对匿名页而言,page找到vma的路径一般如下:page->av(anon_vma)->avc(anon_vma_chain)->vma,其中avc起到桥梁作用。

anon_vma简称av, 用于管理匿名页vma, 当匿名页需要解映射时需要先找到av,再通过av进行查找处理。struct anon_vma_chain,简称avc。主要用于链接vma和av。

这几个重要数据结构的关系如下图:

anon_vma定义了一组红黑树,vma中数据结构维护了avc,当需要访问av中的红黑树数据和vma中的avc时,需要锁保护。

anon_vma->rwsem是定义在anon vma数据结构中的读写信号量。

struct anon_vma {struct anon_vma *root; /* Root of this anon_vma tree */struct rw_semaphore rwsem; /* W: modification, R: walking the list */…}

获取锁的接口主要是anon_vma_lock_write和anon_vma_lock_read。当然也有带try类型的。这里不赘述。

static inline void __sched __down_write(struct rw_semaphore *sem){rwbase_write_lock(&sem->rwbase, TASK_UNINTERRUPTIBLE);}static inline void anon_vma_lock_write(struct anon_vma *anon_vma){down_write(&anon_vma->root->rwsem);}static inline void anon_vma_lock_read(struct anon_vma *anon_vma){down_read(&anon_vma->root->rwsem);}

可以看到,如果anon_vma的rwsem没有获取成功(已有其它线程持有)。当前进程会设置为TASK_UNINTERRUPTIBLE。这也是systrace中我们有时可能看到block reason为anon_vma_lock_write的D状态(uninterruptable sleep)的原因。

典型的在fork线程时dup_mmapàanon_vma_forkàanon_vma_lock_write持写锁路径:

内存回收对匿名页进行反向映射是需要持读锁的典型路径:

static struct anon_vma *rmap_walk_anon_lock(struct folio *folio,struct rmap_walk_control *rwc){struct anon_vma *anon_vma;if (rwc->anon_lock)return rwc->anon_lock(folio, rwc);anon_vma = folio_anon_vma(folio);if (!anon_vma)return NULL;if (anon_vma_trylock_read(anon_vma))goto out;if (rwc->try_lock) {anon_vma = NULL;rwc->contended = true;goto out;}//获取读锁anon_vma_lock_read(anon_vma);out:return anon_vma;}static void rmap_walk_anon(struct folio *folio,struct rmap_walk_control *rwc, bool locked){struct anon_vma *anon_vma;pgoff_t pgoff_start, pgoff_end;struct anon_vma_chain *avc;if (locked) {anon_vma = folio_anon_vma(folio);/* anon_vma disappear under us? */VM_BUG_ON_FOLIO(!anon_vma, folio);} else {//获取读锁anon_vma = rmap_walk_anon_lock(folio, rwc);}if (!anon_vma)return;//遍历找VMA过程需要锁保护pgoff_start = folio_pgoff(folio);pgoff_end = pgoff_start + folio_nr_pages(folio) - 1;anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root,pgoff_start, pgoff_end) {struct vm_area_struct *vma = avc->vma;unsigned long address = vma_address(&folio->page, vma);VM_BUG_ON_VMA(address == -EFAULT, vma);cond_resched;if (rwc->invalid_vma && rwc->invalid_vma(vma, rwc->arg))continue;if (!rwc->rmap_one(folio, vma, address, rwc->arg))break;if (rwc->done && rwc->done(folio))break;}if (!locked)//释放锁anon_vma_unlock_read(anon_vma);}

2.5mapping->i_mmap_rwsem

上一节介绍了匿名页反向映射需要的锁,同样,文件页在反向映射也需要对应的锁,它就是mapping->i_mmap_rwsem。page结构体中有mapping成员。借助此成员,可以方便快捷地找到对应匿名页或文件页。在page->mapping不为0的情况下,如果第0位为0,说明该页为匿名页,此mmaping指高一个anon_vma结构变量;如果第0位不为0,则mapping指向一个每个file一个的address_space地址空间结构变量。

struct address_space结构体中的i_mmap 是一个优先搜索树,关联了这个文件 page cache 页的 vm_area_struct 就挂在这棵树上。当文件页反向映射时,需要通过page--->i_mmap--->vma这个查找顺序并解除对应映射。

struct address_space结构体中定义了i_mmap_rwsem读写信号量锁,用于保护i_mmap。

struct address_space {struct inode *host;struct xarray i_pages;struct rw_semaphore invalidate_lock;gfp_t gfp_mask;atomic_t i_mmap_writable;#ifdef CONFIG_READ_ONLY_THP_FOR_FS/* number of thp, only for non-shmem files */atomic_t nr_thps;#endifstruct rb_root_cached i_mmap;unsigned long nrpages;pgoff_t writeback_index;const struct address_space_operations *a_ops;unsigned long flags;errseq_t wb_err;spinlock_t i_private_lock;struct list_head i_private_list;struct rw_semaphore i_mmap_rwsem;void * i_private_data;} __attribute__((aligned(sizeof(long)))) __randomize_layout;

典型的获取i_mmap_rwsem锁的接口是i_mmap_lock_write和i_mmap_lock_read。获取写锁的路径举例如下:

内存回收对文件页进行反向映射是需要持读锁的典型路径:

static void rmap_walk_file(struct folio *folio,struct rmap_walk_control *rwc, bool locked){struct address_space *mapping = folio_mapping(folio);pgoff_t pgoff_start, pgoff_end;struct vm_area_struct *vma;/** The page lock not only makes sure that page->mapping cannot* suddenly be NULLified by truncation, it makes sure that the* structure at mapping cannot be freed and reused yet,* so we can safely take mapping->i_mmap_rwsem.*/VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);if (!mapping)return;pgoff_start = folio_pgoff(folio);pgoff_end = pgoff_start + folio_nr_pages(folio) - 1;if (!locked) {if (i_mmap_trylock_read(mapping))goto lookup;if (rwc->try_lock) {rwc->contended = true;return;}//持读锁i_mmap_lock_read(mapping);}lookup://遍历i_mmap,需锁保护vma_interval_tree_foreach(vma, &mapping->i_mmap,pgoff_start, pgoff_end) {unsigned long address = vma_address(&folio->page, vma);VM_BUG_ON_VMA(address == -EFAULT, vma);cond_resched;if (rwc->invalid_vma && rwc->invalid_vma(vma, rwc->arg))continue;if (!rwc->rmap_one(folio, vma, address, rwc->arg))goto done;if (rwc->done && rwc->done(folio))goto done;}done:if (!locked)//释放锁i_mmap_unlock_read(mapping);}

2.6shrinker_rwsem

6.9内核shrinker_rwsem已经被改为mutex了。这里呈现原来的shrinker_rwsem使用及问题,因此这部分基于6.6内核进行分析。后续的案例会分析6.9内核针对这个锁的优化改动。

shrinker是一把全局的读写信号量锁。

DECLARE_RWSEM(shrinker_rwsem)

shrinker_rwsem锁主要用于保护shrinker_list链表。驱动可能会注册相应的shinker以便在内存回收时被回调进行shrinker其内存。在注册时会将对应的shrinker加到全局的shrinker->list上。在内存紧张进行内存回收时会遍历shrinker->list的shrinker进行回调。因此为处理并发问题,需要锁保护。shrinker->rwsem主要就是确保这两个过程的并发。

获取写锁的典型路径:

void register_shrinker_prepared(struct shrinker *shrinker){down_write(&shrinker_rwsem);list_add_tail(&shrinker->list, &shrinker_list);shrinker->flags |= SHRINKER_REGISTERED;shrinker_debugfs_add(shrinker);up_write(&shrinker_rwsem);}

获读锁的典型路径:

static unsigned long shrink_slab(gfp_t gfp_mask, int nid,struct mem_cgroup *memcg,int priority){unsigned long ret, freed = 0;struct shrinker *shrinker;/** The root memcg might be allocated even though memcg is disabled* via "cgroup_disable=memory" boot parameter. This could make* mem_cgroup_is_root return false, then just run memcg slab* shrink, but skip global shrink. This may result in premature* oom.*/if (!mem_cgroup_disabled && !mem_cgroup_is_root(memcg))return shrink_slab_memcg(gfp_mask, nid, memcg, priority);//获取shrinker_rwsem读锁if (!down_read_trylock(&shrinker_rwsem))goto out;//遍历shrinker_list进行回调,需要锁保护list_for_each_entry(shrinker, &shrinker_list, list) {struct shrink_control sc = {.gfp_mask = gfp_mask,.nid = nid,.memcg = memcg,};ret = do_shrink_slab(&sc, shrinker, priority);if (ret == SHRINK_EMPTY)ret = 0;freed += ret;/** Bail out if someone want to register a new shrinker to* prevent the registration from being stalled for long periods* by parallel ongoing shrinking.*/if (rwsem_is_contended(&shrinker_rwsem)) {freed = freed ? : 1;break;}}//放锁up_read(&shrinker_rwsem);out:cond_resched;return freed;}

3典型优化案例总结3.1Linux开源社区优化案例3.1.1案例1:per memcg lru_lock优化1

自内核引入memcg以来,系统单一的lru list已经分成了每个内存组一个lru list,由每个内存组单独管理自己的 lru lists。但per memcg lru lock却没有同时引入,导致lru锁在不同memcg上的竞争十分激烈。社区中per-memcg LRU lock的补丁集为每个memcg引入了lru lock,来解决这个存在已久的问题。

阿里巴巴的Alex Shi在2020年提交了一组patchset优化此问题,在linux5.11合入主线。

通过这组patchset的修改,获得了62%的性能提升。

这个改动主要是是通过大锁化小锁。

优化的核心如下:

1)在lruvec增加了per memcg的spinlock_t类型的lru_lock成员。

2)对给定的page,可以获取对应memcg 的lruvec的lru_lock并返回对应lruvec。

3)原来需要获取node的lru_lock的地方,换成了获取memcg的lru_lock。

3.1.2案例2:mmap_lock优化之IO fault path 路径锁优化2

在内核中经常发生优先级翻转的问题。例如一个进程在遍历/proc/pid节点下的任何信息,可能会获取mmap_lock读锁,但如果一个低优先级任务获取了mmap_lock写锁,或正在执行耗时操作,这对高优先级进程的性能明显产生了影响。

Josef Bacik在2018年提交了一组patchset, 在page fault发生IO耗时操作路径上尽快释放mmap_lock锁,相关处理借助page fault现成的retry机制完成。在linux5.1内核进入主线。

此组patchset几个关键修改点:

1)增加page fault的cached_page,retry时handle_mm_fault_cacheable直接处理

2)重新设计文件映射page fault路径,以便在可能执行 IO 或长时间阻塞的任何时间点释放 mmap sem(mmap_lock)

新增加maybe_unlock_mmap_for_io,在可能执行IO操作时调用,可能会释放mmap_sem锁。

耗时的IO还没有完成,释放mmap_sem锁,返回VM_FAULT_RETRY重载获取锁再进入此路径。

3.1.3案例3:mmap_lock优化之SPF优化3

SPF是Speculative page-fault handling的缩写,中文是投机性缺页。它的主要思路是先尝试不需要获取mmap_sem(mmap_lock)进行page fault操作,能进行这个的前提是在整个page fault过程vma没有发生变化,一旦发生变化,这个操作就是白干的,需要重载获取mmap_sem进行正常操作。

SPF改动最早出现在2010年左右,由Peter Zijlstra提交handle page fault without holding the mm semaphore的patchset,但由于还存在一些问题,直到2019年IBM的Laurent Dufour修复了相关问题,重启了这项工作。但此patchset一直没能merge进内核主线。

据描述,优化效果如下:

在Android平台进行测试,应用程序启动时间平均提升6%,一些大型应用程序(100 个线程以上)启动时间提高了20%。

Dufour提交patset如下:

关键修改点简介:

1)引入vma vm_sequence计数,当vma发生变化时,进行计数。在VMA修改的地方引入seqlock, 在进入临界区前调用vm_write_begin, 出临界区再调用vm_write_end, 两者都会对vma->vm_sequence增加计数。因此如果发生发VMA修改,可以通过获取vma->vm_sequence来判断。

2)新增__handle_speculative_fault接口,page fault时会先尝试__handle_speculative_fault。Spf处理如果vma的vm_sequence在整个处理过程没有发生计数,即vma没有被修改。可以不需要mmap_sem完成page faut,如果发生变生,返回VM_FAULT_RETRY重来一次获取mmap_sem的操作。

3)需要对应体系结构支持,如arm64在处理page fault前,先走SPF流程,如果成功直接完成page faut, 如果不成功,就走原来的持mmap_sem流程。

3.1.4案例4:mmap_lock优化之PVL优化4

mmap_sem锁最大的问题是一个进程级别的大锁,锁保护的范围太大,导致本应该可以并发进行的一些操作,无法进行并行。例如,线程1 page fault访问的vma1,线程2 mmap操作的是vma2,这两个vma不一样,本来完全可以进行并发,但由于都被mmap_sem锁保护导致无法并行。

Google的Suren Baghdasaryan在2023年提交一组patchset,引入了per vma lock(PVL),通过per vma lock保护vma的修改,不同vma访问可以并发进行。在linux6.4进入主线。

通过per vma lock, 一些benchmark测试显示,收益稍差于SPF,可以达到SPF约75%效果

Performance benchmarks show similar although slightly smaller benefits aswith SPF patchset (~75% of SPF benefits). Still, with lower complexitythis approach might be more desirable.

另外在Android上一些多线程并发的APP(约100个线程)的启动时间最大可以优化20%

Patchset提交如下:

关键修改点简介如下:

1)每个描述VMA(用vm_area_struct描述)里面实际增加了一个vm_lock_seq成员和lock锁。struct mm_struct新增加 mm_lock_seq成员。当对VMA进行写时,会增加vm_lock_seq计数,通过vm_lock_seq和mm_lock_seq的比对,可以判断是否有人正在持写锁写VMA。

2)封装了vma_write_lock和vma_read_try_lock接口。

vma_write_lock写锁获取由接口,它首先会取vma->lock锁,然后向vma->vm_lock_seq中写入vma->vm_mm->mm_lock_seq。就马上释放vma-> lock锁了。

进程级mm->mm_lock_seq会在vma_write_unlock_mm的时候增加1。

vma读锁获取由接口vma_read_trylock 完成,它判断vma->vm_lock_seq是否与vma->vm_mm->mm_lock_seq相等,如果相等,表示有人获取写锁。否则拿到vma->lock,与写锁不同的是,在这个过程一直持有vma->lock锁,直到完成时通过vma_read_unlock释放。

2) mmap写修改地方,由vma_write_lock标记。

3)try VMA lock-based page fault handling first

lock_vma_under_rcu调用尝试获取vma读锁。如果vma seq 和mm seq相等,表明有人正在进行写操作,回退使用mmap_lock。否则获取vma的读锁进行page fault,完成后释放。

这个优化是典型的大锁化小锁的优化。总结一下,通过此优化后的场景锁的使用情况:

1)、当thread1处理vma1需要持write锁时,需要持mm->mmap_lock;此时当thread2处理vma2需要持read锁时,因为mm->mm_lock_seq与vma2里保存的vma>vm_lock_seq不同,所以thread2处理vma2不需要持mm->mmap_lock,因此也不需要等待thread1去释放mm->mmap_lock,从而thread1和thread2可以并行处理;同理:与此同时,thread3处理vma3、thread4处理vma4需要持读锁的场景也不需要。

2)、当thread1处理vma1需要持write锁时,同时thread2处理vma1也需要持read锁时,两者就转换成去持mm->mmap_lock读写锁逻辑,必须等待thread1写完成或者thread2读完成,才能唤醒另外的thread执行。

第一点优化了更大程度增加了同一地址空间不同vma并行处理能力,在处理同一vma时转换成原来的处理方式,也没有不会带来任何额外性能损耗。

3.1.5案例5:mmap_lock优化之fault around优化5

mmap_lock锁竞争最大的是发生在page fault,如果可以减少page fault,就可以从源头上减少对mmap的竞争。2016年Vinayak Menon就在内核对fault_around_bytes提交了可配置的patch.

通过把fault_around_bytes配置来优化性能,但也需要平衡内存压力。此参数默认此参数是64K。Vinayak Menon提供的一些测试结果可以参考如下:

Patchset参数提交列举如下:

它优化的思路是在文件页缺页时会把当次附近地址的一起建立映射(大小可按需求配置),它只会对本身存在page cache的提前建立映射,并不会产生page cache。

3.1.6案例6:mmap_lock优化之unmap优化6

当上层在用户态调用free时,底层调用munmap,但是munmap代价并不小,因为它需要持mmap_sem的write锁后解除所有的PTE映射并释放回内存伙伴系统。这个代价会随着VMA区域长短增大成线性增加关系。例如unmap 320GB 需要约 18 秒。

阿里巴巴的yang.shi在2018年提交一组分段unmmap的patchset。

关键修改很简单,就是对大于HPAGE_PUD_SIZE(1GB)的,每次unmmap HPAGE_PUD_SIZE大小,然后判断一下mmap_sem是否有waiter,并且当前处于可睡眠上下文,主动释放mmap_sem并让出CPU,然后下一次重新获得CPU运行时再次获取mmap_sem继续进行unmap。收益如下:

此pathset未进入主线。

大部分人相比munmap更喜欢使用madvise_dontneed调用, 它不需要持mmap_sem的写锁,只需要持mmap_sem的读锁。因此进程中其它线程发生缺页时,仍然可以并发处理,相对而言性能优于munmap。由于madvise_dontneed并不是立刻释放虚拟地址,它也有一些缺陷,可能会导致虚拟地址紧张(特别是对于32位应用)。

3.1.7案例7:rmap路径的锁性能优化7

在系统内存紧张的场景同时有进程在持续处理它们的vma工作如fork, mmap,munmap等行为时,rmap反向映射路径上的锁如i_mmap_rwsem和anon_vma->root->rwsem会存在明显的竞争。它可能会使内存回收路径性能较差或者卡住。在一些观察中,可以看到kswapd等待这些锁的耗时在300ms以上, 最坏的情况下,可以达到1s以上,这使得其它一些进程进入direct recalim, 当然,进入direct reclaim也会卡在这些锁上面。

2022年minchan提交了一组patchset优化种情况的问题,优化补丁已在5.19合入主线。合入补丁后,可以看到在rmap路径上的平均耗时大幅下降。

patchset提交如下:

关键修改简介:

1)封装了i_mmap_rwsem和anon_vma->root->rwsem锁的trylock_read接口。

2)在rmap时先进行trylock,在trylock成功时,按原来流程处理 。trylock不成功时,表明锁存在竞争,设置rwc->contended为true然后返回。

3)在folio_check_references如果referenced_ptes为-1即rmap的锁存在竞争,trylock不成功,跳过此page的处理。

3.1.8案例8:shrinker lockless优化8

shrinker_rw是一把全局的读写锁,用于保护一些操作例如shrink slab, shrinker registration和shrinker的unregistration等。这些都容易存在性能问题。例如:

1)当系统内存压力大且同时系统文件系统存在mount和unmount时,shinrk slab会受到影响(down_read_trylock失败)

2)如果一个shrinker被blocked住(例如上面1描述的情况且一个写者来临,写者会被blocked,然后所有shriner相关操作也会被blocked)

例如,当一个driver进行register一个shrinker时,它需要写锁,如果shirnk slab在内回收时,由于runnable或D状态等长时间没释放此锁,那么register的进程需要长时间等待。同样,如果一个进程在unreginer一个shrinker时,由于runnable较长时间没有释放锁,shrink slab由于获取不到锁,内存回收也明显的受到影响。

字节跳动的zhengqi在2023年09月提交了一组patchset,借助refcout计数和RCU将全局shinker锁优化为无锁操作。patchset在linux6.7合入主线。

patchset提交如下:

合入patch后,可以看到性能明显提升,收益数据如下:

关键修改简介如下:

1)封装了shrinker_alloc、shrinker_register、shrinker_free接口。

2)改造原来直接调用register_shrinker的地方。

3)Shrinker新增refcount、struct completion done、struct rcu_head rcu成员。

Refcount会在shrinker Registered的时候拥有初始值1,然后一些查找操作将被通过shrinker_try_get允许使用它。然后在unregistration的时候Refcount会进行计数的减少,当其为0时,会通过异步的RCU来释放shrinker。

在原来获取shrinker读写锁的地方,现在先通过获取shrinker_try_get增加shrinker->refcount引用计数,完成shrinker操作后通过shrinker_put减少shrinker->refcount引用计数。

当shrinker->refcount引用计数减少时,如果到达到0,即没有进程在使用,就设置shrinker->done complete操作。

shrinker->done条件满足,即没有进程在使用shrinker了,通过rcu cb进行删除释放。

4)在确认没有reader, shrinker_rwsem替换为mutex锁

4.优化演进方向总结

通过上面案例,可以看到在内存锁(操作系统锁的方向也一样)在优化的演进和方向上主要是以下优化方向:

1)lock less无锁操作。要想优化锁的性能问题,最根本的改善办法就是无锁操作。可以参考上面案例中的shrinker lockless案例和SPF案例。

2)缩小临界区,尽早释放锁。如果锁无法避免,需要避免长时间持锁的临界区,可以考虑先释放锁,完成耗时操作后,再次获取锁完成此过程。可以参考IO fault path 路径锁优化和munmap优化的案例。

3)大锁化小锁。把大锁拆成小锁,减少锁竞争的粒度。可以参考PVL优化案例和per memcg lru lock优化案例。

4)降低锁等待的影响。在锁竞争激烈时,尽量不让等待锁影响关键路径的性能。可以参考上面rmap路径的锁性能优化的案例。

5)减少持锁次数。通过一些提前操作的方式或一次性多操作的方式,或从业务源头减少相关持锁操作方式,降低持锁次数。可以参考上面fault around案例。

5.参考资料

1.https://patchwork.kernel.org/project/linux-mm/cover/1604566549-62481-1-git-send-email-alex.shi@linux.alibaba.com/

2.https://patchwork.kernel.org/project/linux-mm/cover/20180925153011.15311-1-josef@toxicpanda.com/

3.https://patchwork.kernel.org/project/linux-mm/cover/20190416134522.17540-1-ldufour@linux.ibm.com/

4.https://patchwork.kernel.org/project/linux-mm/cover/20230109205336.3665937-1-surenb@google.com/

5.https://lore.kernel.org/all/20160422141716.GD7336@node.shutemov.name/T/

6.https://lore.kernel.org/lkml/f88deb20-bcce-939f-53a6-1061c39a9f6c@linux.alibaba.com/

7.https://patchwork.kernel.org/project/linux-mm/patch/20220510215423.164547-1-minchan@kernel.org/

8.https://patchwork.kernel.org/project/linux-mm/patch/20230911094444.68966-43-zhengqi.arch@bytedance.com/

9.https://elixir.bootlin.com/linux/v6.9.7/C/ident/返回搜狐,查看更多