前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文搞懂 | Linux 同步管理(下)

一文搞懂 | Linux 同步管理(下)

作者头像
刘盼
发布2021-10-21 15:04:43
6410
发布2021-10-21 15:04:43
举报
文章被收录于专栏:人人都是极客人人都是极客

上面讲的自旋锁,信号量和互斥锁的实现,都是使用了原子操作指令。由于原子操作会 lock,当线程在多个 CPU 上争抢进入临界区的时候,都会操作那个在多个 CPU 之间共享的数据 lock。CPU 0 操作了 lock,为了数据的一致性,CPU 0 的操作会导致其他 CPU 的 L1 中的 lock 变成 invalid,在随后的来自其他 CPU 对 lock 的访问会导致 L1 cache miss(更准确的说是communication cache miss),必须从下一个 level 的 cache 中获取。

这就会使缓存一致性变得很糟,导致性能下降。所以内核提供一种新的同步方式:RCU(读-复制-更新)。

RCU 解决了什么

RCU 是读写锁的高性能版本,它的核心理念是读者访问的同时,写者可以更新访问对象的副本,但写者需要等待所有读者完成访问之后,才能删除老对象。读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。

RCU 适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是 RCU 发挥作用的最佳场景。

RCU 例子

RCU 常用的接口如下图所示:

API

说明

rcu_read_lock

标记读者进入读端临界区

rcu_read_unlock

标记读者退出临界区

synchronize_rcu

同步RCU,即所有的读者已经完成读端临界区,写者才可以继续下一步操作。由于该函数将阻塞写者,只能在进程上下文中使用

call_rcu

把回调函数 func 注册到RCU回调函数链上,然后立即返回

rcu_assign_pointer

用于RCU指针赋值

rcu_dereference

用于RCU指针取值

list_add_rcu

向RCU注册一个链表结构

list_del_rcu

从RCU移除一个链表结构

为了更好的理解,在剖析 RCU 之前先看一个例子:

代码语言:javascript
复制
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/rcupdate.h>
#include <linux/kthread.h>
#include <linux/delay.h>

struct foo {
        int a;
        struct rcu_head rcu;
};

static struct foo *g_ptr;

static int myrcu_reader_thread1(void *data) //读者线程1
{
        struct foo *p1 = NULL;

        while (1) {
                if(kthread_should_stop())
                        break;
                msleep(20);
                rcu_read_lock();
                mdelay(200);
                p1 = rcu_dereference(g_ptr);
                if (p1) 
                        printk("%s: read a=%d\n", __func__, p1->a);
                rcu_read_unlock();
        }
 
        return 0;
}

static int myrcu_reader_thread2(void *data) //读者线程2
{
        struct foo *p2 = NULL;

        while (1) {
                if(kthread_should_stop())
                        break;
                msleep(30);
                rcu_read_lock();
                mdelay(100);
                p2 = rcu_dereference(g_ptr);
                if (p2)
                        printk("%s: read a=%d\n", __func__, p2->a);
         
                rcu_read_unlock();
        }
 
        return 0;
}

static void myrcu_del(struct rcu_head *rh) //回收处理操作
{
        struct foo *p = container_of(rh, struct foo, rcu);
        printk("%s: a=%d\n", __func__, p->a);
        kfree(p);
}

static int myrcu_writer_thread(void *p) //写者线程
{
        struct foo *old;
        struct foo *new_ptr;
        int value = (unsigned long)p;

        while (1) {
                if(kthread_should_stop())
                        break;
                msleep(250);
                new_ptr = kmalloc(sizeof (struct foo), GFP_KERNEL);
                old = g_ptr;
                *new_ptr = *old;
                new_ptr->a = value;
                rcu_assign_pointer(g_ptr, new_ptr);
                call_rcu(&old->rcu, myrcu_del);
                printk("%s: write to new %d\n", __func__, value);
                value++;
        }

        return 0;
}

static struct task_struct *reader_thread1;
static struct task_struct *reader_thread2;
static struct task_struct *writer_thread;

static int __init my_test_init(void)
{
        int value = 5;

        printk("figo: my module init\n");
        g_ptr = kzalloc(sizeof (struct foo), GFP_KERNEL);

        reader_thread1 = kthread_run(myrcu_reader_thread1, NULL, "rcu_reader1");
        reader_thread2 = kthread_run(myrcu_reader_thread2, NULL, "rcu_reader2");
        writer_thread = kthread_run(myrcu_writer_thread, (void *)(unsigned long)value, "rcu_writer");

        return 0;
}
static void __exit my_test_exit(void)
{
        printk("goodbye\n");
        kthread_stop(reader_thread1);
        kthread_stop(reader_thread2);
        kthread_stop(writer_thread);
        if (g_ptr)
                kfree(g_ptr);
}
MODULE_LICENSE("GPL");
module_init(my_test_init);
module_exit(my_test_exit);

执行结果是:

myrcu_reader_thread2: read a=0 myrcu_reader_thread1: read a=0 myrcu_reader_thread2: read a=0 myrcu_writer_thread: write to new 5 myrcu_reader_thread2: read a=5 myrcu_reader_thread1: read a=5 myrcu_del: a=0

RCU 原理

可以用下面一张图来总结,当写线程 myrcu_writer_thread 写完后,会更新到另外两个读线程 myrcu_reader_thread1 和 myrcu_reader_thread2。读线程像是订阅者,一旦写线程对临界区有更新,写线程就像发布者一样通知到订阅者那里,如下图所示。

写者在拷贝副本修改后进行 update 时,首先把旧的临界资源数据移除(Removal);然后把旧的数据进行回收(Reclamation)。结合 API 实现就是,首先使用 rcu_assign_pointer 来移除旧的指针指向,指向更新后的临界资源;然后使用 synchronize_rcucall_rcu 来启动 Reclaimer,对旧的临界资源进行回收(其中 synchronize_rcu 表示同步等待回收,call_rcu 表示异步回收)。

为了确保没有读者正在访问要回收的临界资源,Reclaimer 需要等待所有的读者退出临界区,这个等待的时间叫做宽限期(Grace Period)。

Grace Period

中间的黄色部分代表的就是 Grace Period,中文叫做宽限期,从 Removal 到 Reclamation,中间就隔了一个宽限期,只有当宽限期结束后,才会触发回收的工作。宽限期的结束代表着 Reader 都已经退出了临界区,因此回收工作也就是安全的操作了。

宽限期是否结束,与 CPU 的执行状态检测有关,也就是检测静止状态 Quiescent Status。

Quiescent Status

Quiescent Status,用于描述 CPU 的执行状态。当某个 CPU 正在访问 RCU 保护的临界区时,认为是活动的状态,而当它离开了临界区后,则认为它是静止的状态。当所有的 CPU 都至少经历过一次 Quiescent Status 后,宽限期将结束并触发回收工作。

因为 rcu_read_lock 和 rcu_read_unlock 分别是关闭抢占和打开抢占,如下所示:

代码语言:javascript
复制
static inline void __rcu_read_lock(void)
{
 preempt_disable();
}
代码语言:javascript
复制
static inline void __rcu_read_unlock(void)
{
 preempt_enable();
}

所以发生抢占,就说明不在 rcu_read_lock 和 rcu_read_unlock 之间,即已经完成访问或者还未开始访问。

Linux 同步方式的总结

机制

等待机制

优缺

场景

原子操作

无;ldrex 与 strex 实现内存独占访问

性能相当高;场景受限

资源计数

自旋锁

忙等待;唯一持有

多处理器下性能优异;临界区时间长会浪费

中断上下文

信号量

睡眠等待(阻塞);多数持有

相对灵活,适用于复杂情况;耗时长

情况复杂且耗时长的情景;比如内核与用户空间的交互

互斥锁

睡眠等待(阻塞);优先自旋等待;唯一持有

较信号量高效,适用于复杂场景;存在若干限制条件

满足使用条件下,互斥锁优先于信号量

RCU

绝大部分为读而只有极少部分为写的情况下,它是非常高效的;但延后释放内存会造成内存开销,写者阻塞比较严重

读多写少的情况下,对内存消耗不敏感的情况下,满足 RCU 条件的情况下,优先于读写锁使用;对于动态分配数据结构这类引用计数的机制,也有高性能的表现。

5T技术资源大放送!包括但不限于:C/C++,Arm, Linux,Android,人工智能,单片机,树莓派,等等。在上面的【人人都是极客】公众号内回复「peter」,即可免费获取!!

记得点击分享、赞和在看,给我充点儿电吧

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 人人都是极客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • RCU 解决了什么
  • RCU 例子
  • RCU 原理
    • Grace Period
      • Quiescent Status
      • Linux 同步方式的总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档