Linux内核中的锁——知识点

简介:


 系统中的锁,说简单点就是为了保护共享资源,从而更好的实现系统并发。本文对内核中的相关常用锁进行了介绍以及部分使用。

1.   信号量

第一个经验法则是设计驱动时在任何可能的时候记住避免共享的资源。

全局变量远远不是共享数据的唯一方式

信号量Semaphores是一个单个整型值, 结合有一对函数, 典型地称为 P 和 V 信号量用作互斥,阻止多个进程同时在同一个临界区内运行 -- 它们的值将初始化为 1. 这样的信号量在任何给定时间只能由一个单个进程或者线程持有. 以这种模式使用的信号量有时称为一个mutex互斥锁。

内核中实现

Linux 内核提供了信号量实现, 内核代码必须包含 <asm/semaphore.h>. 相关的类型是 struct semaphore; 实际可以用 几种方法来声明和初始化. 一种是直接创建一个, 接着使用 sema_init 来设定它:

sema_init(struct semaphore *sem, int val)

其中val是初始化的值。

如果是互斥锁,val就设置为1 即可。

获取信号量可以使用:

down_interruptible,获取信号量除非中断打断。

down_trylock,尝试获取信号量但不等待。

down,尝试获取信号量,如果没有获取则等待直到获取。

down对应的方法是up函数。

up(struct semaphore *sem)

            除了互斥锁外,还有读写信号量,这样读写可以有更好的保障。

一个 rwsem 允许一个读者或者不限数目的读者来持有信号量。写者有优先权; 当一个写者试图进入临界区, 就不会允许读者进入直到所有的写者完成了它们的工作. 这个实现可能导致读者饥饿 -- 读者被长时间拒绝存取。 所以, rwsem 最好用在很少请求写的时候, 并且写者只占用短时间。

 

 

2.   completions机制

  如果信号量有很多竞争,性能会受损并且加锁方案需要重新审视.在调用down 的线程将几乎是一直不得不等待.在一些情况中, 信号可能在调用 up 的进程用完它之前消失.

在2.4.7 内核中增加了 "completion" 接口 ,一个轻量级机制: 允许一个线程告诉另一个线程工作已经完成. 为使用 completion,代码必须包含 <linux/completion.h>

通过DECLARE_COMPLETION(work)来静态创建。

也可以动态创建,通过init_completion来初始化。

init_completion - Initialize a dynamically allocated completion

使用中进程可以调用wait_for_completion来等待,这是一个不可中断的等待,也不能杀死了。

     另一方面,complete和complete_all可以唤醒wait_for_completion的进程。

complete 只唤醒一个等待的线程, 而 complete_all 允许所有都继续.  

  completion 机制的典型使用是在模块退出时。当模块准备被清理时, exit函数告知线程退出并且等待结束。内核包含一个特殊的函数complete_and_exit给线程使用.

void complete_and_exit(struct completion *comp, long code)

{

        if (comp)

                complete(comp);

 

        do_exit(code);

}

代码实例   

使用实例如下,创建一个字符设备,关联一个读操作和写操作。

#include <linux/module.h>

#include <linux/init.h>

#include <linux/sched.h>  /* current and everything */

#include <linux/kernel.h> /* printk() */

#include <linux/fs.h>     /* everything... */

#include <linux/types.h>  /* size_t */

#include <linux/completion.h>

 

MODULE_LICENSE("Dual BSD/GPL");

static int complete_major = 0;

DECLARE_COMPLETION(comp);

 

ssize_t complete_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)

{

        printk(KERN_DEBUG "process %i (%s) going to sleep\n",

                        current->pid, current->comm);

        wait_for_completion(&comp);

        printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);

        return 0; /* EOF */

}

 

ssize_t complete_write (struct file *filp, const char __user *buf, size_t count,

                loff_t *pos)

{

        printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",

                        current->pid, current->comm);

        complete(&comp);

        return count; /* succeed, to avoid retrial */

}

 

 

struct file_operations complete_fops = {

        .owner = THIS_MODULE,

        .read =  complete_read,

        .write = complete_write,

};

int complete_init(void)

{

        int result;

 

        /*

         * Register your major, and accept a dynamic number

         */

        result = register_chrdev(complete_major, "complete", &complete_fops);

        if (result < 0)

                return result;

        if (complete_major == 0)

                complete_major = result; /* dynamic */

        return 0;

}

void complete_cleanup(void)

{

        unregister_chrdev(complete_major, "complete");

}

module_init(complete_init);

module_exit(complete_cleanup);

3.   自旋锁

内核中大部分加锁通过自旋锁来完成。自旋锁用在不能睡眠的代码中,例如中断处理。

自旋锁概念上简单,是一个互斥设备,有2个值:"上锁"和"解锁".要想获取一个特殊锁的代码,需要测试相关的位. 如果锁是可用的, 这个"上锁"位被置位并且代码继续进入临界区.如果这个锁已经被别人获得, 代码进入一个紧凑的循环中反复检查这个锁, 直到它变为可用。

"测试并置位"操作必须以原子方式进行, 以便只有一个线程能够获得锁

自旋锁是设计用在多处理器系统上,如果一个非抢占的单处理器系统进入一 个锁上的自旋, 它将永远自旋; 没有其他的线程再能够获得 CPU 来释放这个锁. 在没有打开抢占的单处理器系统上自旋锁操作被优化为什么不作,如果是支持抢占的单处理系统也是正确加锁的。

内核实现

自旋锁的核心规则是任何代码必须在持有自旋锁时, 是原子性的,不能睡眠(其实而很多内核函数可能睡眠)。

      持有自旋锁时禁止中断( 只在本地 CPU )

            自旋锁必须一直是尽可能短时间的持有

自旋锁原语要求的包含文件是 <linux/spinlock.h>

实际所的类型是spinlock_t.像其他数据结构一样, 一个自旋锁必须初始化,初始化函数spin_lock_init

            获取自旋锁的函数是spin_lock。

            释放自旋锁是spin_unlock函数。

            此外还有三对获取自旋锁函数如下:

            spin_lock_irqsave/ spin_unlock_irqrestore:在获得锁之前,禁止本地CPU中断,中断状态保存在flags.

            spin_lock_irq/spin_unlock_irq进入之前中断时开启的,不用保存中断状态,用完继续开启中断即可。

            spin_lock_bh/spin_unlock_bh禁用软中断。

            另外还有非阻塞的自旋锁操作:spin_trylock, spin_trylock_bh

读写自旋锁

读写锁有一个类型 rwlock_t, 在<linux/spinlokc.h> 中定义 ,类似信号量中的读写信号量.

            使用rwlock_init来初始化。操作和spin_lock基本类似。

read_lock/read_unlock

read_lock_irqsave/read_unlock_irqsave

read_lock_irq/read_unlock_irq

read_lock_bh/read_unlock_bh

write_lock/write_unlock

write_trylock

write_lock_irqsave/write_unlock_irqsave

write_lock_irq/write_unlock_irq

write_lock_bh/write_unlock_bh

读写锁会引起读饥饿,要在多读少写场景使用。

 

4.   其他锁

因为加锁机制是实现存在缺陷,有些情况不需要加锁。例如环形缓冲区,在网络适配器中的使用。

还可以使用原子变量atomic_t来替代一个完整的加锁体制。对一个变量实现加锁显得有些过分,原子变量的操作如下。

void atomic_set(atomic_t *v, int i);

atomic_t v = ATOMIC_INIT(0);

int atomic_read(atomic_t *v);

void atomic_add(int i, atomic_t *v);

void atomic_sub(int i, atomic_t *v);

void atomic_inc(atomic_t *v);

void atomic_dec(atomic_t *v);

……

     atomic_t在整数算术时是不错的,但是如果要以原子方式操作可能就不行了。

     原子位操作非常快, 因为它们使用单个机器指令来进行操作, 而在任何时候低层平台做的 时候不用禁止中断,因为中断来不及中断原子位操作。如:

void set_bit(nr, void *addr);

void clear_bit(nr, void *addr);

void change_bit(nr, void *addr);

     不过在新代码中,还是建议使用自旋锁,至少别人知道是在做什么。

seqlock锁

spin_lock对于临界区是不做区分的。而读写锁是对临界区做读写区分,写进程进入时需要等待读进程退出临界区。为了保护写进程的优先权,并使得写进程可以更快的获得锁,引入了顺序锁。

顺序锁的思想是:对某一个共享数据读取的时候不加锁,写的时候加锁。在读取者和写入者之间引入变量sequence,读取者在读取之前读取sequence, 读取之后再次读取此值,如果不相同,则说明本次读取操作过程中数据发生了更新,需要重新读取。而对于写进程在写入数据的时候就需要更新sequence的值。

初始化可以如下:

seqlock_t lock1 = SEQLOCK_UNLOCKED;//静态初始化

seqlock_t lock2;

seqlock_init(&lock2);//动态初始化

读之前调用函数:read_seqbegin,读完继续调用read_seqretry。

     如果是写则是:write_seqlock,写完调用write_sequnlock

     考虑到中断影响,读写都有irqsave,irq,bh版本。

RCU

读取-拷贝-更新(RCU) 是一个高级的互斥方法,当数据结构需要改变, 写线程做一个拷贝, 改变这个拷贝, 接着使相关的指针对准新的版本.RCU 的代码应当包含 <linux/rcupdate.h>使用一个 RCU-保护的数据结构的代码应当用 rcu_read_lock 和 rcu_read_unlock 调用将它的引用包含起来.

 

 

 

 

 

 

目录
相关文章
|
3天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
1天前
|
缓存 网络协议 Linux
Linux操作系统内核
Linux操作系统内核 1、进程管理: 进程调度 进程创建与销毁 进程间通信 2、内存管理: 内存分配与回收 虚拟内存管理 缓存管理 3、驱动管理: 设备驱动程序接口 硬件抽象层 中断处理 4、文件和网络管理: 文件系统管理 网络协议栈 网络安全及防火墙管理
18 4
|
3天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
5天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
28 4
|
6天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
8天前
|
缓存 运维 Linux
深入探索Linux内核:CPU拓扑结构探测
【10月更文挑战第18天】在现代计算机系统中,CPU的拓扑结构对性能优化和资源管理至关重要。了解CPU的核心、线程、NUMA节点等信息,可以帮助开发者和系统管理员更好地调优应用程序和系统配置。本文将深入探讨如何在Linux内核中探测CPU拓扑结构,介绍相关工具和方法。
10 0
|
6天前
|
缓存 算法 Linux
Linux内核中的内存管理机制深度剖析####
【10月更文挑战第28天】 本文深入探讨了Linux操作系统的心脏——内核,聚焦其内存管理机制的奥秘。不同于传统摘要的概述方式,本文将以一次虚拟的内存分配请求为引子,逐步揭开Linux如何高效、安全地管理着从微小嵌入式设备到庞大数据中心数以千计程序的内存需求。通过这段旅程,读者将直观感受到Linux内存管理的精妙设计与强大能力,以及它是如何在复杂多变的环境中保持系统稳定与性能优化的。 ####
13 0
|
网络协议 NoSQL Linux
阿里云 Linux 内核优化实战(sysctl.conf 和 ulimits )
一、sysctl.conf优化Linux系统内核参数的配置文件为 /etc/sysctl.conf 和 /etc/sysctl.d/ 目录。其读取顺序为: /etc/sysctl.d/ 下面的文件按照字母排序;然后读取 /etc/sysctl.conf 。
8573 1
|
6月前
|
机器学习/深度学习 人工智能 负载均衡
深度解析:Linux内核调度策略的演变与优化
【5月更文挑战第30天】 随着计算技术的不断进步,操作系统的性能调优成为了提升计算机系统效率的关键。在众多操作系统中,Linux因其开源和高度可定制性而备受青睐。本文将深入剖析Linux操作系统的内核调度策略,追溯其历史演变过程,并重点探讨近年来为适应多核处理器和实时性要求而产生的调度策略优化。通过分析比较不同的调度算法,如CFS(完全公平调度器)、实时调度类和批处理作业的调度需求,本文旨在为系统管理员和开发者提供对Linux调度机制深层次理解,同时指出未来可能的发展趋势。
|
3月前
|
存储 安全 Linux
在Linux中,内核调优配置文件名字有哪些?举例几个内核需要优化的参数配置?
在Linux中,内核调优配置文件名字有哪些?举例几个内核需要优化的参数配置?