注册 登录
电子工程世界-论坛 返回首页 EEWORLD首页 频道 EE大学堂 下载中心 Datasheet 专题
oxlm_1的个人空间 https://home.eeworld.com.cn/space-uid-1076641.html [收藏] [复制] [分享] [RSS]
日志

《Linux内核深度解析》-- 内核互斥技术

已有 73 次阅读2025-1-26 11:39 |个人分类:读书笔记

        这一章也是和笨叔的书有共同点的地方,共同点在于,都是在讲内核上怎么实现并发状态下的数据正确性。但是呢,也有明显的不同点,笨叔的讲解增加侧重于实现的底层逻辑,因此看笨叔书籍时,会明显发现笨叔对于各种互斥技术细节的分析十分多,如原子操作,内存屏障,自旋锁,信号量,MCS锁,互斥锁,读写锁,读写信号量,RCU实现等。《Linux内核深度解析》刚好反过来,对于笨叔提到的各种实现,这本书并没有详细提及,仅仅是提及了怎么使用,在什么场景下使用,因此要想了解这些锁的详细实现,还是得跟着笨叔的那本书去看,如果仅仅想知道怎么使用,看这边书就够了。
       由于这一章节书里侧重点与笨叔的那本书不一致,因此这里所总结的侧重点也以笨叔书籍中未提及的部分为主。
实时互斥锁
        互斥锁存在一个比较大的问题,优先级反转,什么意思呢,就是两个进程都要申请互斥锁,但是呢,低优先级的线程先拿到了锁,这个时候高优先级的任务就不得不进入睡眠状态等待低优先级线程释放锁。这本身不算问题,问题是,如果这个时候有优先级介于两个进程中的第三个进程要执行任务了,此时低优先级的任务不得不让出时间片,导致了原先排队等锁的进程(优先级比第三个优先级高的进程)不得不花更长的时间等待锁释放。
        实时互斥锁就是为了解决这么个问题的,他的实现思路其实也挺简单,就是在低优先级进程拿到锁并执行的时候,如果有高优先级的进程在申请锁,那就临时将低优先级进程的优先级提高到申请锁进程的优先级,这样保证了低优先级任务执行期间,不会被介于两个进程优先级之间的进程打断,这样就保证了高优先级的进程能更早的拿到锁。
实时互斥锁的接口也挺简单,就是在互斥锁的基础上增加rt_ 的头,即:
DEFINE_RT_MUTEX(mutexname); // 静态申明实时互斥锁
rt_mutex_init(mutex); // 动态定义实时互斥锁
void rt_mutex_lock(struct rt_mutex *lock); // 申请锁,若申请不到,则深度睡眠
int rt_mutex_lock_interruptible(struct rt_mutex *lock); // 申请锁,若申请不到,则轻度睡眠
int rt_mutex_timed_lock(struct rt_mutex *lock, struct hrtimer_sleeper *timeout); // 申请锁,若申请不到,则睡眠等待一段时间
int rt_mutex_trylock(struct rt_mutex *lock); // 尝试获得锁,成功返回1,失败返回0
void rt_mutex_unlock(struct rt_mutex *lock); // 释放锁

 

顺序锁
        读写锁有个问题,就是如果有很多读者时,写着很难拿到锁,这个时候就成了消息难发出,读者一直在等待的情况。为了解决这个问题,内核实现了两种方式的解决方案,一个是排队自旋锁,这个笨叔的讲解更详细。另一个就是顺序锁。
        顺序锁的逻辑如下:写着在写完数据后会更新序列号(序列号加1),读者发现序列号发生改变,则去读取数据。
提供自旋锁和序列号的顺序锁
seqlock_t seqlock;

//  初始化方法
DEFINE_SEQLOCK(x)
seplock_init(x)

// 写者写数据的方法
write_seqlock(&seqlock);
// 写数据
write_sequnlock(&seqlock);

// write_seqlock()变体
write_seqlock_bh() // 申请写锁并禁止当前处理器的软中断
write_seqlock_irq() // 申请写锁并禁止当前处理器的硬中断
write_seqlock_irqsave() // 申请写锁,保存但钱处理器的硬件中断状态并禁用处理器的硬中断

// 顺序读者读数据方法
unsigned int seq;
do {
    seq = read_seqbegin(&seqlock);
    
    //  读数据
} while(read_seqretry(&seqlock,seq));

// 读者读数据方法
read_seqlock_excl(&seqlock);
// 读数据
read_sequnlock_excl(&seqlock);

//read_seqlock_excl的变体
read_seqlock_excl_bh() //申请自旋锁,并禁止当前处理器的软中断
read_seqlock_excl_irq() //申请自旋锁,并禁止当前处理器的硬中断
read_seqlock_excl_irqsave() //申请自旋锁,保存当前处理器的硬中断状态,并禁用当前处理器的硬中断

// 写着空闲时,读者成为顺序读者,写这些,读者成为持锁着操作
db {
    read_seqbegin_or_lock(&seqlock, &seq);
    
    // 读数据
} while (need_seqretry(&seqlock, seq));

done_seqretry(&seqlock, seq);

//read_seqbegin_or_lock 变体
read_seqbegin_or_lock_irqsave() // 附加功能同read_seqlock_excl_irqsave(),都是在原有功能的基础上,
                                // 保存当前处理器的硬件中断状态,并禁用当前处理器的中断

 

只提供序列号的顺序锁
// 定义方法
seqcount_t x = SEQCNT_ZERQ(x);
seqcount_init(s);

// 写数据方法
spin_lock(&mylock);
write_seqcount_begin(&sc);
// 写数据
write_seqcount_end(&sc);
spin_unlock(&mylock);

// 读数据方法
seqcount_t sc;
unsigned int seq;

do {
    seq = read_seqcount_begin(&sc);
    // 读数据
} while(read_seqcount_retry(&sc));

 

内核抢占
        内核抢占是指进程在内核态中运行时可被其他进程抢占,此功能需在内核编译前打开宏CONFIG_PREEMPT来支持。
preempt_disable() // 禁用内核抢占入口
preempt_enable() // 抢占计数减1,若减到0,则重新调度进程
preempt_enable_no_resched() // 抢占计数减1,若减到0,不重新调度进程

 

进程和软中断互斥
        若进程和软中断可能同时访问同一对象,那么进程和软中断需要互斥,此时进程需关闭软中断。
local_bh_disable() // 禁止本处理器的软中断
local_bh_enable() // 使能本处理器的软中断

 

进程和硬中断互斥
        若进程和硬中断访问同一对象,那么进程和硬中断需要互斥,此时进程需关闭硬中断。
// 禁止本处理器的硬中断,不同点为save接口会将当前硬件中断状态保存至flags中后再去禁用硬中断
local_irq_disable()
local_irq_save(flags)

// 使能本处理器的硬中断 
local_irq_enable()
local_irq_restore(flags)

        需要注意的是,local_irq_disable和local_irq_enable不能嵌套使用,而local_irq_save和local_irq_restore可以嵌套使用。

每处理器变量
        为了避免处理器之间互斥和缓存同步的问题,在多处理器的系统中,增加了每处理器变量这么个工具,其使用方法如下:
// 静态申明每处理器变量
DEFINE_PER_CPU(type, name) // 声明普通每处理器变量
DEFINE_PER_CPU_FIRST(type, name) // 定义必须在每处理器变量集合中最先出现的变量
DEFINE_PER_CPU_ALIGNED(type, name) // 定义和处理器缓存对齐的每处理器变量
DEFINE_PER_CPU_PAGE_ALIGNED(type, name) // 定义和页长度对齐的每处理器变量
DEFINE_PER_CPU_READ_MOSTLY(type, name)  // 定义以读为主的每处理器变量

// 若要每处理器变量能被其他内核模块使用,需要导出至符号表,接口如下:
EXPORT_PER_CPU_SYMBOL(var)   // 允许任何内核模块引用
EXPORT_PER_CPU_SYMBOL_GPL(var) // 只允许使用GPL许可的内核模块使用

// 动态每处理器变量
void __percpu *__alloc_percpu_gfp(size_t size, size_t align, gfp_t gfp)
{
    return pcpu_alloc(size, align, false, gfp);
}
void __percpu *__alloc_percpu(size_t size, size_t align)
{
    return pcpu_alloc(size, align, false, GFP_KERNEL);
}
#define alloc_percpu_gfp(type, gfp)                 \
    (typeof(type) __percpu *)__alloc_percpu_gfp(sizeof(type),   \
                        __alignof__(type), gfp)
#define alloc_percpu(type)                      \
    (typeof(type) __percpu *)__alloc_percpu(sizeof(type),       \
                        __alignof__(type))

//访问每处理器变量
#define VERIFY_PERCPU_PTR(__p)                      \
({                                  \
    __verify_pcpu_ptr(__p);                     \
    (typeof(*(__p)) __kernel __force *)(__p);           \
})

#define per_cpu_ptr(ptr, cpu)   ({ (void)(cpu); VERIFY_PERCPU_PTR(ptr); })
#define raw_cpu_ptr(ptr)    per_cpu_ptr(ptr, 0)
#define this_cpu_ptr(ptr)   raw_cpu_ptr(ptr) // 本质上为基地址加当前处理器的偏移值

#define get_cpu_var(var)                        \
(*({                                    \
    preempt_disable();                      \
    this_cpu_ptr(&var);                     \
}))

#define get_cpu_ptr(var)                        \
({                                  \
    preempt_disable();                      \
    this_cpu_ptr(var);                      \
})

// 每处理器计数器
// 动态初始化每处理器计数器
percpu_counter_init(fbc, value, gfp) // fbc: 计数器地址 value:初始值 gfp:标志位
// 计数累加到每处理器计数器
void percpu_counter_add(struct percpu_counter *fbc, s64 amount)

// 读取近似计数值入口
s64 percpu_counter_read(struct percpu_counter *fbc)
s64 percpu_counter_read_positive(struct percpu_counter *fbc) // 如果是负数,则返回0

//读取准确计数值入口
s64 percpu_counter_sum(struct percpu_counter *fbc)
s64 percpu_counter_sum_positive(struct percpu_counter *fbc) // 如果是负数,则返回0

 

死锁检测工具lockdep
        这可能属于这一章实战中分析问题最实用的工具了,开启此功能检测需要在内核配置时启用以下配置:
  1. CONIFIG_PROVE_LOCKING或CONDIF_DEBUG_LOCK_ALLOC:前一个是允许内核报告死锁问题,后一个是检查内核是否错误地释放被持有的锁。
  2. CONFIG_DEBUG_LOKING_API_SELFTESTS:在内核初始化过程中运行的小程序,检查调式机制是否能发现常见的锁缺陷
            由于这部分属于这章为数不多的分析到实现原理的部分,最好单独拉出来看,因此晚点配合看完源码后统一输出。

本文来自论坛,点击查看完整帖子内容。

评论 (0 个评论)

facelist doodle 涂鸦板

您需要登录后才可以评论 登录 | 注册

热门文章