【从递归保护锁出发,聊一聊 linux 软中断】

1、递归保护锁

对于函数的递归调用,大家都比较熟悉了,但有些情况下需要防止函数的递归调用,该怎么实现呢?同一线程内,只需要借助一个全局变量即可。

#include 
#include    
#include     
#include     
#include   
#include     
#include 
#include 
#include 
#include 
#include 

struct RecursionGuard
{
    RecursionGuard() : wasLocked(isActive) {
        isActive = true;
    }

    ~RecursionGuard() {
        isActive = wasLocked;
    }

    const bool wasLocked;
    static thread_local bool isActive;
};

thread_local bool RecursionGuard::isActive = false;

void g() {
    std::cout << "enter g" << std::endl;
    if (RecursionGuard::isActive) {
        std::cout << "failed to acquire permission" << std::endl;
        return;
    }
    RecursionGuard guard;
    g();  // 嵌套调用
    std::cout << "exit g" << std::endl;
}

int main() {
    g();
    return 0;
}

上述代码中,函数 g 里嵌套调用了 g,但是由于全局的递归保护锁在当前函数中,嵌套函数 g 里拿不到锁。输出结果如下:

【从递归保护锁出发,聊一聊 linux 软中断】_第1张图片

2、定时器触发的软中断

2.1 无限循环软中断

与函数的无穷递归调用类似,还有一种场景是定时器导致进程陷入软中断中出不来:

int var = 0;

void f(int signo) {
    std::cout << "kernel thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(12));
    var += 10;
    std::cout << "var in kernel thread: " << var << std::endl;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = f;
    if (sigaction(SIGALRM, &sa, nullptr) < 0) {
        std::cout << "sigaction error\n";
    }
    struct itimerval tick;
    memset(&tick, 0, sizeof(tick));
    tick.it_value.tv_sec = 0;
    tick.it_value.tv_usec = 10 * 1000;
    tick.it_interval.tv_sec = 0;
    tick.it_interval.tv_usec = 10 * 1000;
    setitimer(ITIMER_REAL, &tick, nullptr);
    std::cout << "main thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    for (int i = 0; i < 5; i++) {
        var += 1;
        std::cout << "var in main thread: " << var << std::endl;
    }
    return 0;
}

上述代码 main 函数中启动了一个 10ms 的定时器,定时器触发的软中断 handler 是函数 f,f 中再次休眠 12ms,于是,问题来了:前一个 SIGALRM 信号触发的函数 f 还没执行完,下一个定时器信号又到了,所以进程卡死在 handler 里,死循环一直出不来了。

程序输出结果如下:

【从递归保护锁出发,聊一聊 linux 软中断】_第2张图片

2.2 递归保护锁能避免软中断出不来吗?

答案是不能。对于系统调用 sigaction 而言,定时器触发的信号会排队等待处理,也就是说 handler 是阻塞的。而 signal 则是会丢弃定时器触发的信号。

void f(int signo) {
    std::cout << "enter f" << std::endl;
    if (RecursionGuard::isActive) {
        return;
    }
    RecursionGuard guard;
    std::cout << "kernel thread: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(12));
    var += 10;
    std::cout << "var in kernel thread: " << var << std::endl;
}

2.3 软中断中的锁

先看下述代码:

// f1 为主线程函数
void f1() {
    SpinLock(&lock);

    /* 被保护临界区 */

    spinUnlock(&lock);
}

// f2 设置为定时器的 handler
void f2() {
    SpinLock(&lock);

    /* 被保护临界区 */

    SpinLock(&lock);
}

解释下:

  1. 如果 f1 先拿锁,有以下三种情况:

(1) f1 在进程上下文执行

时钟中断到,执行 f2,此时拿锁是拿不到的。因为 f1 处于进程上下文,此时 CPU 已被 f2 抢占,f1 始终无法释放锁,此时发生死锁。

(2) f1 在中断上半部执行(硬中断)

时钟中断到,因为 timer 的中断执行函数在软中断中执行,属于下半部上下文,不能抢占上半部,所以不会发生死锁。

(3) f1 在中断下半部执行(软中断)

时钟中断到,因为 f2 和 f1 同为下半部执行,所以不会抢占 f1,不会死锁。注意,这种情况下也只能使用自旋锁,因为互斥锁会休眠的。

\2. 如果 f2 先拿锁,也有以下三种情况:

(1) f1 在进程上下文执行

因为 f2 在中断上下文,不会被 f1 抢,所以不会发生死锁。

(2) f1 在中断上半部执行(硬中断)

因为 f1 之前没有关中断,f2 在软中断中执行,硬中断可以打断软中断,f2 未释放锁,f1 拿不到锁,发生死锁。

(3) f1 在中断下半部执行(软中断)

时钟中断到,因为 f2 和 f1 同为下半部执行,所以不会抢占 f2,不会死锁。

总结下:中断处理函数只有被执行,才有机会去拿锁,如果连被执行的机会都没有,是不会有抢别人锁的机会的。

下面是在软中断中使用多线程处理的一个示例:

#include 

int var = 0;

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void h() {
    while (lock.test_and_set(std::memory_order_acquire)) {} //获得自旋锁
    var += 10;
    std::cout << "var in kernel thread " << std::this_thread::get_id() << ": " << var << std::endl;
    lock.clear(std::memory_order_release); //释放自旋锁
}

void f(int signo) {
    std::thread t1(h);
    std::thread t2(h);
    t1.join();
    t2.join();
}

int main() {
    std::cout << "main thread: " << std::this_thread::get_id() << std::endl;
    struct sigaction sa;
    sa.sa_handler = f;
    if (sigaction(SIGALRM, &sa, nullptr) < 0) {
        std::cout << "sigaction error\n";
    }
    struct itimerval tick;
    memset(&tick, 0, sizeof(tick));
    tick.it_value.tv_sec = 0;
    tick.it_value.tv_usec = 1 * 1000;
    tick.it_interval.tv_sec = 0;
    tick.it_interval.tv_usec = 2 * 1000;
    setitimer(ITIMER_REAL, &tick, nullptr);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    return 0;
}

程序输出结果如下:

【从递归保护锁出发,聊一聊 linux 软中断】_第3张图片

注意:在多个子线程力启多个定时器是没有效果的。

2.4 中断没有优先级,也不能嵌套

触发中断时,需要用到栈。 中断 A 正在处理的过程中,假设又发生了中断 B,那么在栈里要保存 A 的现场,然后处理 B。在处理 B 的过程中又发生了中断 C,那么在栈里要保存B的现场,然后处理 C。 如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。所以,为了防止这种情况发生,也是为了简单化中断的处理,在 Linux 系统上中断无法嵌套:即当前中断 A 没处理完之前,不会响应另一个中断 B(即使它的优先级更高)。

3、其他示例代码

代码 1:

class SpinMutex {
public:
    SpinMutex() = default;
    SpinMutex(const SpinMutex&) = delete;
    SpinMutex& operator=(const SpinMutex&) = delete;
    void lock() {
        while(flag.test_and_set(std::memory_order_acquire));
    }
    void unlock() {
        flag.clear(std::memory_order_release);
    }

private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
};

int var = 0;
SpinMutex spin_mtx;
void f() {
   // std::lock_guard lock(spin_mtx);
    var += 1;
    std::cout << "var in kernel thread " << std::this_thread::get_id() << ": " << var << std::endl;
}

void g() {
   // std::lock_guard lock(spin_mtx);
    var += 1;
    std::cout << "var in main thread " << std::this_thread::get_id() << ": " << var << std::endl;
}
void h(int signo) {
    f();
}

int main() {
    struct sigaction sa;
    sa.sa_handler = h;
    if (sigaction(SIGALRM, &sa, nullptr) < 0) {
        std::cout << "sigaction error\n";
    }
    struct itimerval tick;
    memset(&tick, 0, sizeof(tick));
    tick.it_value.tv_sec = 0;
    tick.it_value.tv_usec = 1 * 1000;
    tick.it_interval.tv_sec = 0;
    tick.it_interval.tv_usec = 1 * 1000;
    setitimer(ITIMER_REAL, &tick, nullptr);
    //std::this_thread::sleep_for(std::chrono::milliseconds(10));
    for (int i = 0; i < 100000; i++) {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        g();
    }
    return 0;
}

【从递归保护锁出发,聊一聊 linux 软中断】_第4张图片

很明显,内核软中断和用户态线程都操作了全局变量 var,var 没有得到保护。

代码 2:

int var = 0;
SpinMutex spin_mtx;
void f() {
    std::lock_guard<SpinMutex> lock(spin_mtx);
    var += 1;
    std::cout << "var in kernel thread " << std::this_thread::get_id() << ": " << var << std::endl;
}

void g() {
    std::lock_guard<SpinMutex> lock(spin_mtx);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    var += 1;
    std::cout << "var in main thread " << std::this_thread::get_id() << ": " << var << std::endl;
}
void h(int signo) {
    f();
}

int main() {
    struct sigaction sa;
    sa.sa_handler = h;
    if (sigaction(SIGALRM, &sa, nullptr) < 0) {
        std::cout << "sigaction error\n";
    }
    struct itimerval tick;
    memset(&tick, 0, sizeof(tick));
    tick.it_value.tv_sec = 0;
    tick.it_value.tv_usec = 1 * 1000;
    tick.it_interval.tv_sec = 0;
    tick.it_interval.tv_usec = 1 * 1000;
    setitimer(ITIMER_REAL, &tick, nullptr);
    for (int i = 0; i < 100000; i++) {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        g();
    }
    return 0;
}

【从递归保护锁出发,聊一聊 linux 软中断】_第5张图片

虽说代码 2 中加了自旋锁,但是分别在内核软中断和用户态线程中,还是会造成进程卡死。有没办法解决该问题呢?借助一个标记 flag 就可以,请看代码 3:

代码 3:

int var = 0;
int flag = 0;
void f() {
    if (flag == 1) {
        return;
    }
    var += 1;
    std::cout << "var in kernel thread " << std::this_thread::get_id() << ": " << var << std::endl;
}

void g() {
    flag = 1;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    var += 1;
    std::cout << "var in main thread " << std::this_thread::get_id() << ": " << var << std::endl;
    flag = 0;
}
void h(int signo) {
    f();
}

int main() {
    struct sigaction sa;
    sa.sa_handler = h;
    if (sigaction(SIGALRM, &sa, nullptr) < 0) {
        std::cout << "sigaction error\n";
    }
    struct itimerval tick;
    memset(&tick, 0, sizeof(tick));
    tick.it_value.tv_sec = 0;
    tick.it_value.tv_usec = 1 * 1000;
    tick.it_interval.tv_sec = 0;
    tick.it_interval.tv_usec = 1 * 1000;
    setitimer(ITIMER_REAL, &tick, nullptr);
    for (int i = 0; i < 100000; i++) {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        g();
    }
    return 0;
}

更多细节及代码示例,持续更新中。。。

你可能感兴趣的:(Linux,linux,运维,服务器)