读取 - 复制 - 更新(RCU)是一种手动内存管理技术,在Linux内核中越来越流行.
是否有可能设计一种使用RCU而不是传统垃圾收集器的语言和VM来回收无法访问的内存?
我正在尝试了解 rcu_read_lock() 同步机制。据我了解,使用的是rcu_read_lock(),这里有多个读线程和一个写线程,读/写相同的数据,在rcu_read_lock()下进行读操作,每个线程复制数据。我写了一个简单的驱动程序来测试这个(read() 和 write() 函数是核心):
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
#include <linux/init.h> /* Needed for the macros */
#include <linux/rcupdate.h>
#include <linux/preempt.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define MY_MAJOR 42
#define MY_MAX_MINORS 5
char buf[] = "0";
struct dev_data
{
struct cdev cdev;
};
struct dev_data devs[MY_MAX_MINORS];
static ssize_t read(struct file *file, char __user *buffer, size_t size, loff_t *offset)
{
rcu_read_lock();
while (1)
{
printk(KERN_INFO "%s", buf);
}
rcu_read_unlock();
return …Run Code Online (Sandbox Code Playgroud) 我正在阅读有关Read-copy-update(RCU)的内容.在SMP的情况下,我不确定我是否正确理解它.据我所知,RCU确保以原子方式执行Update.在例如单链表的情况下,显然可以在一个操作中完成与旧元素的交换,因为它是通过改变指针来完成的.但是如何确保在双向链表的情况下RCU仍将以原子方式执行?给定元素有两个指针(next和prev),因此这个元素的每个更改都需要更改这两个指针.如何确保更改这两个指针将作为原子操作完成?如何在Linux中完成?
rcu_read_lock的实现是禁用抢占和屏障。并且softirq上下文将不会被抢占。因此有必要在softirq上下文中调用rcu_read_lock。障碍重要吗?
(来自LWN上的一篇文章)
1 rcu_read_lock();
2 list_for_each_entry_rcu(p, head, list) {
3 do_something_with(p->a, p->b, p->c);
4 }
5 rcu_read_unlock();
Run Code Online (Sandbox Code Playgroud)
RCU更新操作将执行synchronize_rcu()以断言每个CPU切换上下文,因此每个RCU读取器已完成它的工作.但RCU必须依靠读者不被抢先一步.事实上,LWN接下来说:
虽然这种简单的方法适用于在RCU读取端关键部分禁用抢占的内核,换句话说,对于非CONFIG_PREEMPT和CONFIG_PREEMPT内核,它不适用于CONFIG_PREEMPT_RT实时(-rt)内核.
我理解对于非CONFIG_PREEMPT内核禁用抢占,但为什么CONFIG_PREEMPT内核也可以呢?
任何人都可以解释rcu_dereference()和之间的区别是rcu_dereference_protected()什么?
rcu_dereference()包含障碍代码,rcu_dereference_protected()不包含.
何时使用rcu_dereference()以及何时使用rcu_dereference_protected()?
我提出了一个想法,我试图实现一个无锁堆栈,不依赖于引用计数来解决ABA问题,并且还正确处理内存回收.它在概念上与RCU类似,并且依赖于两个特征:将列表条目标记为已删除,以及跟踪阅读器遍历列表.前者很简单,它只使用指针的LSB.后者是我对实现无限制无锁堆栈的方法的"聪明"尝试.
基本上,当任何线程试图遍历列表时,一个原子计数器(list.entries)会递增.遍历完成后,第二个计数器(list.exits)递增.
节点分配由push处理,释放由pop处理.
推送和弹出操作与天真无锁堆栈实现非常相似,但必须遍历标记为删除的节点才能到达未标记的条目.因此推送基本上就像链表插入一样.
pop操作类似地遍历列表,但它使用atomic_fetch_or将节点标记为在遍历时被删除,直到它到达未标记的节点.
遍历0个或更多标记节点的列表后,弹出的线程将尝试CAS堆栈的头部.至少有一个并发弹出的线程将成功,在此之后,进入堆栈的所有读者将不再看到以前标记的节点.
成功更新列表的线程然后加载原子list.entries,并基本上自旋加载atomic.exits,直到该计数器最终超过list.entries.这应该意味着列表的"旧"版本的所有读者都已完成.然后,该线程只是释放它从列表顶部交换的标记节点列表.
因此,弹出操作的含义应该(我认为)可能没有ABA问题,因为释放的节点不会返回到可用的指针池,直到所有使用它们的并发读取器完成,显然内存回收问题由于同样的原因,处理也是如此.
所以,无论如何,这是理论,但我仍然在实现上摸不着头脑,因为它目前无法正常工作(在多线程情况下).似乎我在免费问题之后得到了一些写作,但是我在查找问题时遇到了麻烦,或者我的假设可能存在缺陷而且无法正常工作.
无论是概念还是调试代码的方法,都会非常感谢任何见解.
这是我当前(损坏的)代码(使用gcc -D_GNU_SOURCE -std = c11 -Wall -O0 -g -pthread -o list list.c编译):
#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <stdio.h>
#include <unistd.h>
#define NUM_THREADS 8
#define NUM_OPS (1024 * 1024)
typedef uint64_t list_data_t;
typedef struct list_node_t {
struct list_node_t * _Atomic next;
list_data_t data;
} list_node_t;
typedef struct {
list_node_t * _Atomic head;
int64_t _Atomic size;
uint64_t _Atomic entries;
uint64_t _Atomic exits;
} list_t; …Run Code Online (Sandbox Code Playgroud)