16.贝博betball网页同步方法

betball贝博app Linux 728 次浏览 没有评论
  • 信号量
  • 信号量和自旋锁的差异
  • 计数信号量和二值信号量
  • 创建和初始化信号量
  • 使用信号量
  • 释放信号量
  • 读-写信号量
  • 互斥体
  • 信号量和互斥体
  • 各类锁的比较
  • 完成变量
  • 原子操作

    贝博betball网页提供了两组原子操作接口,针对整数和位操作,在linux支持的所有体系架构上都实现了这两组接口。

    原子整数操作

    针对整数的原子操作只能对 atomic_t 类型的数据进行处理,没有直接使用int类型。原因:
  • 确保原子操作只与该特殊类型一起使用,保证该类型数据不会传递给任何非原子操作函数。
  • 确保编译器不会对相应的值进行访问优化从而使原子操作最终接受到正确的内存地址而不是一个别名。
  • 不同体系结构上实现原子操作的时候使用该类型可以屏蔽其之间的差异。
  • atomic_t只能当做24位来使用,因为在SPARC架构上原子操作的实现和其他体系结构不同。需要嵌入一个8位的锁。
    原子操作通常是内联函数,通过内联汇编指令来实现。如果某个函数本身就是原子的,往往会被定义成一个宏。
    代码可能有比原子性(一个操作不被中断)更高的要求,比如要求读必须在特定的写之前发生——其实不属于原子性要求,而是顺序性要求,确保多条指令出现在独立的执行线程甚至独立的处理器上仍然可以保持本该的顺序。
    能使用原子操作就不要使用复杂的加锁机制,对于多数体系结构来讲,原子操作与更复杂的同步方法相比较给系统带来的开销小,对于高速缓存行的影响也小。

    64位原子操作

    只是将int换成了long,前缀改为atomic64_t其他功能无异

    原子位操作

    位操作函数是对普通的内存地址进行操作的,参数是一个指针和一个位号。
    对于每一个函数都提供了对应的非原子位函数,名字前缀多两个下划线。如果不需要原子性操作,使用非原子的位函数比原子的位函数可能会执行的更快一些。

    原子位操作的意义

    位操作只是在操作一个bit,看起来原子和非原子操作貌似没有什么差别,因为不存在发生矛盾的可能性。为什么有非原子位操作呢?
    其原因在于,假设同时对一个变量连续设置两个值,那么原子操作一定是按照顺序先设置第一个值再去设置第二个值。
    而非原子操作有可能并没有按照代码描述的过程进行赋值,虽然最终该变量为第二个值,但是有可能根本没有被设置过第一个值。

    自旋锁

    实际项目中临界区可能跨越多个函数,比如先从一个数据结构中移除数据,对其进行格式转换和解析,然后再把它加入另外一个数据结构中。整个执行过程必须是原子的,在数据被更新完毕前不允许有其他代码来读取这些数据。简单的原子操作对此无能为力,需要使用锁来满足要求。
    自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个被已经持有(争用)的自旋锁,那么该线程会一直进行“忙循环-旋转-等待锁可用”的状态。如果锁未被争用,请求锁的执行线程便能立刻得到它继续执行。
    在任意时间自旋锁都可以防止多于一个的执行线程同时进入临界区,同一个锁可以用在多个位置,例如对给定数据的所有访问都可以得到保护和同步。
    一个被争用的自旋锁在等待锁重新可用时自旋(非常浪费处理器时间)是自旋锁的要点,所以自旋锁不应该被长时间持有。自旋锁的初衷就是在短时间内进行轻量级加锁。
    另外还可以采取让请求线程睡眠的方式来处理对锁的争用,在锁重新可用时再唤醒线程从而处理器不用循环等待。但是这回有明显的两次上下文切换开销,与实现自旋锁的少量代码相比,上下文切换需要更多的代码。所以,持有自旋锁的时间最好小于完成两次上下文切换的耗时,要尽可能的短。信号量可以在发生争用时等待的线程投入睡眠而不是旋转。

    自旋锁的实现方法

    自旋锁的实现和体系结构密切相关,往往通过汇编实现。
    自旋锁的基本使用形式:
    DEFINE_SPINLOCK(mr_lock);
    spin_lock(&mr_lock);
    /*临界区*/
    spin_unlock(&mr_lock);
    自旋锁在同一时刻最多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区,这就为多处理器机器提供了防止并发访问所需的保护机制。
    注意自旋锁不可递归!如果试图得到一个自己正持有的锁,必须自旋等待自己释放这个锁,但是因为自己处于自旋忙等待中永远无法释放,于是就被自己锁死了!

    中断处理程序中的使用

    自旋锁可以使用在中断处理程序中,中断处理程序中不能使用信号量,因为它们会导致睡眠。
    中断处理程序中使用自旋锁时一定要在获取锁之前首先禁止当前处理器上的本地中断,否则中断处理程序就会打断正持有锁的贝博betball网页代码,有可能会试图去争用这个已经被持有的自旋锁,这样一来中断处理程序就会自旋,等待该锁重新可用。但是锁的持有者在这个中断处理程序执行完毕之前不可能运行,这就是双重请求死锁
    贝博betball网页提供了禁止中断同时请求锁的接口
  • spin_lock_irqsave(&mr_lock,flags)保存中断的当前状态并禁止本地中断然后再去获得指定的锁
  • spin_unlock_irqrestore(&mr_lock,flags)对指定的锁解锁,然后让中断恢复到加锁前的状态。即使中断最初是被禁止的,代码不会激活中断,会让其继续禁止。
  • 使用锁的目的
    使用锁的时候一定要对症下药,有针对性。要知道**保护的是数据而不是代码**,既然不是对代码加锁,那就一定要用**特定**的锁来保护自己的共享数据,无论何时需要访问共享数据,一定要先保证数据是安全的,而保证数据安全往往就意味着在对数据进行操作之前首先占用恰当的锁完成操作后再去释放他。
    如果确定中断在加锁前是激活的,那就不需要在解锁后恢复中断之前的状态,也就不需要保存,从而可以无条件的在解锁时激活中断。这时使用spin_lock_irq和spin_unlock_irq()效果会好一些。因为贝博betball网页的庞大和复杂性,在贝博betball网页的执行路线上很难搞清楚中断在当前调用点上是不是处于激活状态,所以并不提倡使用spin_lock_irq()方法。

    自旋锁的其他操作

    自旋锁和下半部

    与下半部配合使用时必须小心地使用锁机制,spin_lock_bh()用于获取指定锁同时会禁止所有下半部的执行。spin_unlock_bh()为反操作。
    下半部可以抢占进程上下文中的代码,所以下半部和进程上下文共享数据时必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。
    由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,就必须在获取恰当的锁的同时还要禁止中断。
    因为同类的tasklet不会同时运行,所以对于同类tasklet中的共享数据不需要保护。但是当数据被两个不同种类的tasklet共享时,就需要在访问下半部中的数据前先获得一个普通的自旋锁,这里不需要禁止下半部,因为同一个处理器上不会有tasklet相互抢占的情况。
    软中断无论是否同种类型,如果数据被软中断共享,那么它必须得到锁的保护。即使同类型的两个软中断也可以同时运行在一个系统的多个处理器上,但是同一处理器上的一个软中断不会抢占另一个软中断,因此不必要禁止下半部

    读-写自旋锁

    很多时候锁的用途可以明确的分为“读锁”和“写锁”,如对一个链表既要更新也要检索。
  • 更新(写)链表时,禁止并发读写
  • 读链表时,禁止写,可并发读
  • linux对这种情况提供了专门的 读-写 自旋锁,为读和写提供了不同的锁,一个或多个读任务可以并发的持有读者锁;用于写的锁最多只能被一个写任务持有,且此时不能有并发的读。
    所以读/写锁也可以叫做共享/排斥锁或并发/排斥锁。
    DEFINE_RWLOCK(mr_rwlock);//初始化
    //读者代码
    read_lock(&mr_rwlock);
    /*临界区只读*/
    read_unlock(&mr_rwlock);
    //写者代码
    write_lock(&mr_rwlock);
    /*临界区读写*/
    write_unlock(&mr_rwlock);
    一定要注意读锁不能再上写锁,否则写锁会不断自旋,等待所有的读者释放锁,其中也包括他自己;所以如果确实需要写操作时,就在一开始就请求写入锁。 也就是下述代码不能连续使用。对于不能清晰区分读写的情况,使用一般的自旋锁。
    read_lock(&mr_rwlock);
    write_lock(&mr_rwlock);
    一个进程递归的获取同一读取锁也是安全的,这个特性使得读写自旋锁成为一种有用并常用的优化手段。
    如果中断处理程序中只有读操作而没有写操作,那么就可以混合使用“中断禁止”锁。
    读写锁照顾读比照顾写要多一些,大量的读者会使挂起的写者处于饥饿状态,设计锁时需要注意。
    自旋锁提供了一种快速简单的锁的实现方法,如果加锁时间不长且代码不会睡眠,利用自旋锁是最佳选择。

    信号量

    信号量是一种睡眠锁,如果有一个任务试图获得一个不可用(或者已被占用)的信号量时,信号量会将其推进一个等待队列然后让其睡眠。这时处理器能重获自由执行其他代码。当信号量可用后,处于等待队列中的那个任务被唤醒并获得该信号量。
    信号量可以让处理器不把时间浪费在忙等待上,但是比自旋锁有更大的开销

    信号量和自旋锁的差异

  • 信号量适用于锁会被长时间持有的情况,短时间持有的情况,睡眠,维护等待队列的开销可能会更大。
  • 执行线程在锁被争用时会睡眠,中断上下文不能进行调度,所以只能在进程上下文中才能获取信号量。
  • 可以在持有信号量时睡眠,因为当其他进程试图获得同一信号量时不会因此死锁,只是也去睡眠而已。
  • 占用信号量的同时不能占用自旋锁,因为等待信号量时可能会睡眠,而持有自旋锁时不可以睡眠。
  • 自旋锁在同一时刻最多有一个任务持有它,而信号量同时允许多个持有,数量在声明信号量时指定。
  • 计数信号量和二值信号量

    自旋锁在同一时刻最多有一个任务持有它,而信号量同时允许多个持有,数量在声明信号量时指定。
    计数为1的信号量成为二值信号量或互斥信号量。
    计数为count的信号量允许在一个时刻至多有count个锁持有者,允许多个执行线程同时访问临界区,所以不能用来进行强制互斥。
    贝博betball网页中使用信号量多数都是二值信号量。
    信号量支持两个原子操作P()和V(),来自荷兰语Proberen和Vershogen,分别为测试操作和增加操作。后来的操作系统把两种操作叫做down()和up(),linux遵循此叫法。
    down()操作通过对信号量计数减一来获取一个信号量
  • 如果结果是0或者大于0则获取信号量锁,任务可以进入临界区。
  • 如果结果是负数任务会放入等待队列,处理器开始执行其他任务。
  • 创建和初始化信号量

    信号量的实现和体系结构无关,具体定义在<asm/semaphore.h>
    声明信号量
    struct semaphore name;
    sema_init(&name,count);
    对于互斥信号量可以用快捷宏定义 static DECLARE_MUTEX(name);
    更常见的情况是信号量作为一个大数据结构的一部分动态创建,此时只有指向该动态创建的信号量的间接指针,可以用向name参数直接输入指针进行初始化。
    初始化动态创建的互斥信号量使用 init_MUTEX(sem);

    使用信号量

    down_interruptible()试图获取指定的信号量,如果信号量不可用讲把调度进行置为TASK_INTERRUPTIBLE状态进入睡眠。该状态意味着任务可以被信号唤醒。 如果进程在等待信号量的时候接受到了信号,那么该进程会唤醒,down_interruptible()函数会返回-EINTR
    down()函数会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,进程在等待信号量的时候就不再响应信号了,所以前者应用的更为广泛。
    down_trylock()函数可以尝试以堵塞方式来获取指定的信号量,在信号量被占用时立刻返回非0值,否则返回0并同时获取信号量。

    释放信号量

    up()函数

    读-写信号量

    读-写信号量对于某些场合比普通信号量更有优势,所有的读-写信号量都是互斥信号量
    读写信号量在贝博betball网页中用rw_semaphore结构表示,定义与<linux/rwsem.h>
    所有的读-写锁的睡眠都不会被信号打断,所以只有一个版本的down()操作
    声明: static DECLARE_RWSEM(name); 初始化: init_rwsem(struct rw_semaphore * sem);
    读写信号量也有阻塞方式的trylock函数 down_read_trylock()和down_write_trylock(),都需要指向读-写信号量的指针作为参数。
    读写信号量比读写自旋锁多了一种特有的操作 downgrade_write()可以动态的将写锁转换为读锁

    互斥体

    为了找到一个更简单睡眠锁,贝博betball网页中引入了互斥体(mutex)。互斥体是指任何可以睡眠的强制互斥锁,比如使用计数为1的信号量。
    mutex的简洁性和高效性源自于相比使用信号量更多的受限性。不同于信号量,mutex仅仅实现了dijkstra设计初衷中的最基本的行为,因此mutex的使用场景更严格更定向。
  • 任何时刻中只有一个任务可以持有mutex
  • 给mutex上锁者必须负责再给其解锁-补鞥呢在一个上下文中锁定一个mutex,而在另一个上下文中给它解锁。是的mutex不适合贝博betball网页同用户空间之间复杂交互的同步场景。
  • 不允许递归的上锁解锁
  • 当持有一个mutex时进程不允许退出
  • mutex不能在中断或者下半部中使用
  • mutex只能由官方API管理。
  • 信号量和互斥体

    写代码时除非碰到特殊场合(一般是底层代码)才会需要使用信号量,首选mutex,如果发现不能满足约束条件才选择使用信号量

    各类锁的比较

    完成变量

    贝博betball网页中一个任务需要发出信号通知另一个任务发生了某个特定时间,可以使用完成变量实现,这是实现两个任务同步的简单方法。
    思路上与信号量一样,事实上完成变量仅仅提供了代替信号量的一个简单解决办法。
    完成变量由结构体completion表示,定义于<linux/completion.h>
    完成变量的具体例子可以参考 kernel/sched.c 和 kernel/fork.c
    通常用法是将完成变量作为数据结构中的一项动态创建,完成数据结构初始化工作的贝博betball网页代码将调用wait_for_completion()等待

    发表评论

    邮箱地址不会被公开。

    Go