云计算百科
云计算领域专业知识百科平台

Linux内存管理中锁使用分析及典型优化案例总结

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流程如下:

3d03102eea4dbc975418fa1384427d25.png

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为例,列举了锁的使用过程。

8f6b967743fdbc6cfa9977ce00f6af57.png

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的使用。

2fe70ab1cde61df70b76f3befb779e7e.png

内核中的 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。

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

67a55131641f19d3f9dae5201dd6d8d6.jpeg

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

赞(0)
未经允许不得转载:网硕互联帮助中心 » Linux内存管理中锁使用分析及典型优化案例总结
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!