📄 ch05s07.html
字号:
<html xmlns:cf="http://docbook.sourceforge.net/xmlns/chunkfast/1.0"><head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>5.7. 加锁的各种选择</title><link rel="stylesheet" href="docbook.css" type="text/css"><meta name="generator" content="DocBook XSL Stylesheets V1.69.0"><link rel="start" href="index.html" title="Linux 设备驱动 Edition 3"><link rel="up" href="ch05.html" title="第 5 章 并发和竞争情况"><link rel="prev" href="ch05s06.html" title="5.6. 锁陷阱"><link rel="next" href="ch05s08.html" title="5.8. 快速参考"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">5.7. 加锁的各种选择</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch05s06.html">上一页</a> </td><th width="60%" align="center">第 5 章 并发和竞争情况</th><td width="20%" align="right"> <a accesskey="n" href="ch05s08.html">下一页</a></td></tr></table><hr></div><div class="sect1" lang="zh-cn"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="AlternativestoLocking.sect"></a>5.7. 加锁的各种选择</h2></div></div></div><p>Linux 内核提供了不少有力的加锁原语能够用来使内核避免被自己绊倒. 但是, 如同我们已见到的, 一个加锁机制的设计和实现不是没有缺陷. 常常对于旗标和自旋锁没有选择; 它们可能是唯一的方法来正确地完成工作. 然而, 有些情况, 可以建立原子的存取而不用完整的加锁. 本节看一下做事情的其他方法.</p><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="LockFreeAlgorithms.sect"></a>5.7.1. 不加锁算法</h3></div></div></div><p>有时, 你可以重新打造你的算法来完全避免加锁的需要. 许多读者/写者情况 -- 如果只有一个写者 -- 常常能够在这个方式下工作. 如果写者小心使数据结构的视图, 由读者所见的, 是一直一致的, 有可能创建一个不加锁的数据结构.</p><p>常常可以对无锁的生产者/消费者任务有用的数据结构是环形缓存. 这个算法包含一个生产者安放数据到一个数组的尾端, 而消费者从另一端移走数据. 当到达数组末端, 生产者绕回到开始. 因此一个环形缓存需要一个数组和 2 个索引值来跟踪下一个新值放到哪里, 以及哪个值在下一次应当从缓存中移走.</p><p>当小心地实现了, 一个环形缓存在没有多个生产者或消费者时不需要加锁. 生产者是唯一允许修改写索引和它所指向的数组位置的线程. 只要写者在更新写索引之前存储一个新值到缓存中, 读者将一直看到一个一致的视图. 读者, 轮换地, 是唯一存取读索引和它指向的值的线程. 加一点小心到确保 2 个指针不相互覆盖, 生产者和消费者可以并发存取缓存而没有竞争情况.</p><p>图<a href="ch05s07.html#ldd3-5-1.fig" title="图 5.1. 环形缓冲">环形缓冲</a>展示了在几个填充状态的环形缓存. 这个缓存被定义成一个空情况由读写指针相同来指示, 而满情况发生在写指针紧跟在读指针后面的时候(小心解决绕回!). 当小心地编程, 这个缓存能够不必加锁地使用.</p><div class="figure"><a name="ldd3-5-1.fig"></a><p class="title"><b>图 5.1. 环形缓冲</b></p><div><img src="images/snagitldd3/ldd3-5-1.png" alt="环形缓冲"></div></div><p>在设备驱动中环形缓存出现相当多. 网络适配器, 特别地, 常常使用环形缓存来与处理器交换数据(报文). 注意, 对于 2.6.10, 有一个通用的环形缓存实现在内核中可用; 如何使用它的信息看 <linux/kfifo.h>.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="AtomicVariables.sect"></a>5.7.2. 原子变量</h3></div></div></div><p>有时, 一个共享资源是一个简单的整数值. 假设你的驱动维护一个共享变量 n_op, 它告知有多少设备操作目前未完成. 正常地, 即便一个简单的操作例如:</p><pre class="programlisting">n_op++; </pre><p>可能需要加锁. 某些处理器可能以原子的方式进行那种递减, 但是你不能依赖它. 但是一个完整的加锁体制对于一个简单的整数值看来过分了. 对于这样的情况, 内核提供了一个原子整数类型称为 atomic_t, 定义在 <asm/atomic.h>.</p><p>一个 atomic_t 持有一个 int 值在所有支持的体系上. 但是, 因为这个类型在某些处理器上的工作方式, 整个整数范围可能不是都可用的; 因此, 你不应当指望一个 atomic_t 持有多于 24 位. 下面的操作为这个类型定义并且保证对于一个 SMP 计算机的所有处理器来说是原子的. 操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令.</p><div class="variablelist"><dl><dt><span class="term"><span>void atomic_set(atomic_t *v, int i);</span></span></dt><dd></dd><dt><span class="term"><span>atomic_t v = ATOMIC_INIT(0);</span></span></dt><dd><p>设置原子变量 v 为整数值 i. 你也可在编译时使用宏定义 ATOMIC_INIT 初始化原子值.</p></dd><dt><span class="term"><span>int atomic_read(atomic_t *v);</span></span></dt><dd><p>返回 v 的当前值.</p></dd><dt><span class="term"><span>void atomic_add(int i, atomic_t *v);</span></span></dt><dd><p>由 v 指向的原子变量加 i. 返回值是 void, 因为有一个额外的开销来返回新值, 并且大部分时间不需要知道它.</p></dd><dt><span class="term"><span>void atomic_sub(int i, atomic_t *v);</span></span></dt><dd><p>从 *v 减去 i.</p></dd><dt><span class="term"><span>void atomic_inc(atomic_t *v);</span></span></dt><dd></dd><dt><span class="term"><span>void atomic_dec(atomic_t *v);</span></span></dt><dd><p>递增或递减一个原子变量.</p></dd><dt><span class="term"><span>int atomic_inc_and_test(atomic_t *v);</span></span></dt><dd></dd><dt><span class="term"><span>int atomic_dec_and_test(atomic_t *v);</span></span></dt><dd></dd><dt><span class="term"><span>int atomic_sub_and_test(int i, atomic_t *v);</span></span></dt><dd><p>进行一个特定的操作并且测试结果; 如果, 在操作后, 原子值是 0, 那么返回值是真; 否则, 它是假. 注意没有 atomic_add_and_test.</p></dd><dt><span class="term"><span>int atomic_add_negative(int i, atomic_t *v);</span></span></dt><dd><p>加整数变量 i 到 v. 如果结果是负值返回值是真, 否则为假.</p></dd><dt><span class="term"><span>int atomic_add_return(int i, atomic_t *v);</span></span></dt><dd></dd><dt><span class="term"><span>int atomic_sub_return(int i, atomic_t *v);</span></span></dt><dd></dd><dt><span class="term"><span>int atomic_inc_return(atomic_t *v);</span></span></dt><dd></dd><dt><span class="term"><span>int atomic_dec_return(atomic_t *v);</span></span></dt><dd><p>就像 atomic_add 和其类似函数, 除了它们返回原子变量的新值给调用者.</p></dd></dl></div><p>如同它们说过的, atomic_t 数据项必须通过这些函数存取. 如果你传递一个原子项给一个期望一个整数参数的函数, 你会得到一个编译错误.</p><p>你还应当记住, atomic_t 值只在当被置疑的量真正是原子的时候才起作用. 需要多个 atomic_t 变量的操作仍然需要某种其他种类的加锁. 考虑一下下面的代码:</p><pre class="programlisting">atomic_sub(amount, &first_atomic); atomic_add(amount, &second_atomic);</pre><p>从第一个原子值中减去 amount, 但是还没有加到第二个时, 存在一段时间. 如果事情的这个状态可能产生麻烦给可能在这 2 个操作之间运行的代码, 某种加锁必须采用.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="BitOperations.sect"></a>5.7.3. 位操作</h3></div></div></div><p>atomic_t 类型在进行整数算术时是不错的. 但是, 它无法工作的好, 当你需要以原子方式操作单个位时. 为此, 内核提供了一套函数来原子地修改或测试单个位. 因为整个操作在单步内发生, 没有中断(或者其他处理器)能干扰.</p><p>原子位操作非常快, 因为它们使用单个机器指令来进行操作, 而在任何时候低层平台做的时候不用禁止中断. 函数是体系依赖的并且在 <asm/bitops.h> 中声明. 它们保证是原子的, 即便在 SMP 计算机上, 并且对于跨处理器保持一致是有用的.</p><p>不幸的是, 键入这些函数中的数据也是体系依赖的. nr 参数(描述要操作哪个位)常常定义为 int, 但是在几个体系中是 unsigned long. 要修改的地址常常是一个 unsigned long 指针, 但是几个体系使用 void * 代替.</p><p>各种位操作是:</p><div class="variablelist"><dl><dt><span class="term"><span>void set_bit(nr, void *addr);</span></span></dt><dd><p>设置第 nr 位在 addr 指向的数据项中.</p></dd><dt><span class="term"><span>void clear_bit(nr, void *addr);</span></span></dt><dd><p>清除指定位在 addr 处的无符号长型数据. 它的语义与 set_bit 的相反.</p></dd><dt><span class="term"><span>void change_bit(nr, void *addr);</span></span></dt><dd><p>翻转这个位.</p></dd><dt><span class="term"><span>test_bit(nr, void *addr);</span></span></dt><dd><p>这个函数是唯一一个不需要是原子的位操作; 它简单地返回这个位的当前值.</p></dd><dt><span class="term"><span>int test_and_set_bit(nr, void *addr);</span></span></dt><dd></dd><dt><span class="term"><span>int test_and_clear_bit(nr, void *addr);</span></span></dt><dd></dd>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -