【问题标题】:Why do we need pushcli() before xchg()?为什么我们在 xchg() 之前需要 pushcli()?
【发布时间】:2020-06-15 06:18:18
【问题描述】:

我正在查看自旋锁的this implementation,尤其是获取和释放函数:

void
acquire(struct spinlock *lk)
{
  pushcli(); // disable interrupts to avoid deadlock.
  if(holding(lk))
    panic("acquire");

  // The xchg is atomic.
  // It also serializes, so that reads after acquire are not
  // reordered before it. 
  while(xchg(&lk->locked, 1) != 0)
    ;

  lk->cpu = cpu;
  getcallerpcs(&lk, lk->pcs);
}

// Release the lock.
void
release(struct spinlock *lk)
{
  if(!holding(lk))
    panic("release");

  lk->pcs[0] = 0;
  lk->cpu = 0;

  xchg(&lk->locked, 0);

  popcli();
}

如果 xchg 函数是原子函数,pushcli()popcli() 的用途是什么?原子性不会确保只有一个线程能够在任何实例中更改锁的值并且不会被中断吗?为什么我们必须显式禁用中断以防止死锁?

【问题讨论】:

  • 如果中断处理程序在正常代码持有的情况下尝试获取锁,则可能会发生死锁。中断处理程序将永远在while 循环中旋转,因为它永远无法获得锁,因为主代码执行已暂停,因此没有机会释放它。 (假设这里是单个 cpu 核心。)

标签: c assembly concurrency operating-system spinlock


【解决方案1】:

原子性不会确保在任何实例中只有一个线程能够更改锁的值而不会被中断吗?

是的,但这不是问题。该操作确保线程之间的原子性,但中断处理程序仍然是一个问题。


中断(特别是硬件中断)是异步的,并且可以随时发生,这意味着代码可以在生成中断时执行任何操作。

当 CPU 捕获到中断时,任何正在运行的代码都会“暂停”并进入中断处理程序。只有中断处理程序完成其工作后,其他任何事情都可以正常进行。

现在,假设您的代码需要acquire() 一个自旋锁,函数定义如下:

void acquire(struct spinlock *lk) {
    while(xchg(&lk->locked, 1) != 0)
        ;

    lk->cpu = cpu;
}

如果在上面的while 之后(和release() 之前)的任何时间发生中断,并且中断处理程序需要获取相同的自旋锁,那么它会尝试做同样的事情:调用@再次987654325@,进入while循环。然而,由于自旋锁已经被持有,循环将永远不会退出。中断处理程序将继续旋转以尝试获取永远不会释放的锁,因为根据定义,中断处理程序必须执行到结束,然后 CPU 才能继续执行其他任何操作。这会导致死锁。此时除了关闭电源并重新启动 CPU 外,别无他法。

这就是为什么需要在您显示的代码中禁用中断的原因。

【讨论】:

  • 自旋锁被打破作为在中断服务例程和主线代码之间建立互斥锁的一种方式。但是,如果在持有锁的同时要禁用中断,那么锁需要什么?
  • @supercat 中断绝对不是唯一可以尝试访问资源的东西。需要锁来协调访问同一资源的多个线程。中断会引起麻烦这一事实是次要问题。
  • 禁用内核上的中断将禁用该内核上大多数但不是所有形式的线程切换,并且看起来像一个非常糟糕的代码气味。我认为正确的方法是要求中断处理程序永远不要在互斥锁上自旋锁定,而是进行设置,以便在互斥锁可用时重新触发它们,然后中止它们的操作,除非/直到发生这种情况。跨度>
  • @supercat 很高兴知道,但这不在问题的范围内。
猜你喜欢
  • 1970-01-01
  • 2019-06-09
  • 2012-10-18
  • 1970-01-01
  • 2012-02-23
  • 2014-03-08
  • 1970-01-01
  • 2014-06-18
  • 2017-02-26
相关资源
最近更新 更多