1技术背景
锁在Linux内存管理中起着非常重要的作用。一方面,锁在内存管理中保护了多线程的临界区并发处理; 另一方面,内存管理各种锁的使用在一些场景也会表现出性能问题。本文针对内核内存管理中典型的锁进行介绍及典型优化案例总结。
本文分析基于Linux内核6.9版本(部分为低版本内核,会特别说明)。
2内存管理中的锁
2.1PG_Locked
Linux内核物理内存使用page进行管理,page的使用需要考虑并发处理,在内核中借助PG_locked实现,当page被标记了PG_locked时表明page已经被锁定,正在使用中,不要修改。page结构体中定义了flag可以表示是否被锁定。
struct page { unsigned long flags; /* Atomic flags, some possibly * updated asynchronously */ … } |
内核中使用lock_page来对page上锁。接口如下:
static inline bool folio_trylock(struct folio *folio) { return likely(!test_and_set_bit_lock(PG_locked, folio_flags(folio, 0))); } void __folio_lock(struct folio *folio) { folio_wait_bit_common(folio, PG_locked, TASK_UNINTERRUPTIBLE, EXCLUSIVE); } static inline void lock_page(struct page *page) { struct folio *folio; might_sleep(); folio = page_folio(page); if (!folio_trylock(folio)) __folio_lock(folio); } |
从上面代码可以看到,lock_page是可能存在睡眠的,因此,不要在不可睡眠的上下文使用。它会尝试对page设置PG_locked标记,如果page的PG_locked已经被置位,也就是此时有人正在访问此page,会通过__folio_lock设置uninterruptable sleep状态等待PG_locked标记被清除。
以典型的filemap_fault为例,使用PG_locked流程如下:
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_UNINTERRUPTI |
评论前必须登录!
注册