Linux中的race condition方案

什么时候需要解决race condition问题

名词

常用的几种方法

信号量和互斥锁

LDD3: Chapter 5: Concurrency and Race Conditions 中说得很清楚:

Semaphores are a well-understood concept in computer science. At its core, a sema-
phore is a single integer value combined with a pair of functions that are typically
called P and V. A process wishing to enter a critical section will call P on the relevant
semaphore; if the semaphore’s value is greater than zero, that value is decremented
by one and the process continues. If, instead, the semaphore’s value is 0 (or less), the
process must wait until somebody else releases the semaphore. Unlocking a sema-
phore is accomplished by calling V; this function increments the value of the sema-
phore and, if necessary, wakes up processes that are waiting.

When semaphores are used for mutual exclusion—keeping multiple processes from
running within a critical section simultaneously—their value will be initially set to 1 .
Such a semaphore can be held only by a single process or thread at any given time. A
semaphore used in this mode is sometimes called a mutex, which is, of course, an
abbreviation for “mutual exclusion.” Almost all semaphores found in the Linux ker-
nel are used for mutual exclusion.

读写信号量

正如 读写锁 一节讲的,内核中也有读写信号量 struct rw_semaphore。

Completions

有这类情况:线程A需要等待线程B执行后,才往下执行。使用信号量完全可以实 现上述情形的同步。不过,这种情况下使用信号量又感觉性能上不太好。因为此 时线程A可以确定一定会在信号量上等待,直到线程B执行完后释放信号量。

于是内核引入了Completions。它就是简直的让一个线程告诉另一个线程,事情搞 定了。

Completion是一次性的,用完就完了。如果需要再用,就需要重新初始化一下。

Spinlocks(自旋锁?)

自旋锁的意思是,线程A首先拿到这个锁,然后执行。线程B拿不到这个锁时,就 在那儿等(自旋一会儿),还是占着cpu的(如果是信号量的话,可能它们这时 候就不占用cpu,可以sleep啦。)。注意这里A和B是在不同的cpu上执行的。等A 执行完后,B马上就可以执行。

一般情况下,自旋锁是效率比信号量高,因为一般被锁住的资源会很快被释放的。 但是它也有限制条件。

  1. 获得自旋锁后的操作,一般来说需要是原子操作,它们不能sleep。也就是 说不能在获得了自旋锁后,又放弃了cpu。如果出现这种情况,别的程序来获取 自旋锁时,永远都会“自旋”啦。但是可能导致sleep的操作很多,比如 copy_to_user,kmalloc等等。在使用自旋锁的时候,需要很小心。
  2. 自旋锁的代码中已经处理了抢断。在程序获得了自旋锁后,在执行该程序的 cpu上,抢断会被关闭。使得此时该程序在获得自旋锁后可以一直占着该cpu。
  3. 自旋锁还有接口,可以在获得自旋锁前,关闭该cpu上的中断。目的也是使 得此时该程序在获得自旋锁后可以一直占着该cpu
  4. 还有一个原则是,自旋锁应该尽可能少得的被占用。因为占用时间越多,可 能等待的线程就越多。浪费的cpu也就越多。

总的来说,自旋锁可以在支持抢断式的单核,或者多核系统上使用。在不支持抢 断式的单核设备上不能使用自旋锁。维基百科是这样说的:

显然,单核CPU不适于使用自旋锁,这里的单核CPU指的是单核单线程的CPU,因为,在同一时间只有一个线程是处在运行状态,假设运行线程A发现无法获取锁,只能等待解锁,但因为A自身不挂起,所以那个持有锁的线程B没有办法进入运行状态,只能等到操作系统分给A的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。

自旋锁之前有一个问题,如果核很多,同时等待临界资源的线程也很多,最坏的 情况下,有一个线程总是得不到资源,需要等待很久。于是后面有人给自旋锁加 入了排队机制: Linux 内核的排队自旋锁(FIFO Ticket Spinlock)

问题:一般的锁,如果锁不住,会把cpu还给内核让它调度后面的线程吗?我现在理解应该是会的。

Linux Device Drivers, Third Edition 上说得很清楚:

an atomic context is simply a state where multiple steps must be
performed without any sort of concurrent access. What that means, with
regard to sleeping, is that your driver cannot sleep while holding a
spinlock, seqlock, or RCU lock. You also cannot sleep if you have
disabled interrupts. It is legal to sleep while holding a semaphore,
but you should look very carefully at any code that does so. If code
sleeps while holding a sema- phore, any other thread waiting for that
semaphore also sleeps. So any sleeps that happen while holding
semaphores should be short, and you should convince your- self that,
by holding the semaphore, you are not blocking the process that will
even- tually wake you up.
在得到一个信号量后,是可以sleep的,也就是放弃cpu。其它等待该信号量的线 程也会sleep。其它比如自旋锁,是不行的。

读写自旋锁

读写信号量 一样,也有读写自旋锁。

读写锁

临界资源,有多个读和一个写时,这是比较有用的。没有写的线程时,多个读可 以同时进行。有写的线程时,读和写都必须等待。

条件变量 (这个是不是就是上面的信号量?)

在《UNIX环境高级编程》一书中,条件变量就是对应的P,V操作吧。一个线程V出 来一个资源,就可以唤醒一个等待的线程来P它。条件变量和互斥锁一起使用, 可以实现线程间的同步。

无锁算法

可以通过一些设计优良的算法来避免使用锁。比如 circular buffer(循环队 列)。它用于生产消费模型,不过只有一个reader和一个writer。writer不停得 往队尾写数据,reader不停地从队首读数据。仔细编写地话,这样的一个模型就可以不用锁。

原子操作

原子操作是意思是把对临界资源的操作全部放入api接口内部实现,这样就不需 要加锁了。Linux实现方式依赖于平台,每个平台都不一样,有些平台是直接用 汇编语言实现的,有些平台则是通过关中断的方法来实现的(如何通过关中断来实现这里我不太清楚)。

当需要保护的数据比较少的时候,使用原子操作是比较好的。一般情况下,就是 一些整型数等等。原子操作提供了一些比如加1减1,读取等的简单接口。

位操作

我理解位操作(读,写)都可以直接只由一条机器指令执行。所以就不存在race condition?如果是多核也不存在吗?

使用位操作,可以实现自旋锁,比如:

/* try to set lock */
while (test_and_set_bit(nr, addr) != 0)
wait_for_a_while( );
/* do your work */
/* release lock, and check... */
if (test_and_clear_bit(nr, addr) = = 0)
something_went_wrong( ); /* already released: error */

seqlocks

这个比较好玩。读的线程read不管三七二十一直接先读数据,读完后,看这个数 据在它读的时候是否被writer动过,如果动过,丢弃当前读的重新再读一次。看 起来像这样:

unsigned int seq;
do {
seq = read_seqbegin(&the_lock);
/* Do what you need to do */
} while read_seqretry(&the_lock, seq);

其中,seq就是来查看是否数据被writer动过的。seqlock的read还有关中断的接 口。writer写的时候需要拿一个锁--自旋锁。所以自旋锁相关的限制对它也有。

seqlock一般锁的是简单,比较一致的数据。 它不能锁有指针的数据。 因为如果writer正在修改数据,谁也不知道reader读出来的指针是指向哪里的。

Read-Copy-Update

  1. 用在读很常见,写少见的情况。
  2. reader除了关闭抢断,啥也不管,读就是了,不过在读的操作,必须是原子 的。
  3. 数据全是通过指针访问。
  4. 申请新的空间,把旧的指针拷贝过来。
  5. 在新申请的空间中更新指针。这个时候,新的reader就直接访问新申请的空间的指针了。
  6. writer还需要等所有访问old数据的reader访问完成后,释放old数据。

参考资料

  1. Anatomy of Linux synchronization methods
  2. 维基百科: 自旋锁
  3. Linux 内核的排队自旋锁(FIFO Ticket Spinlock)
  4. TODO: Ticket spinlocks

如果你觉得本文不错,欢迎 donate