[原创] 《奔跑吧Linux内核2:调试与案例分析》1-并发与同步

qiao---   2024-3-18 17:52 楼主

        感谢eeworld和吧奔跑吧社区给我这次阅读此书的机会。在收到书后也是看了一下此书的第一章,第一章主要讲解的是Linux的并发与同步。虽然并发与同步是两个相关但又不同的概念。但是在计算机科学中,它们经常一起讨论,因为在处理多任务和多线程时,需要考虑如何实现和协调好并发和同步,而本书的第一章主要就是围绕这个来讲的。在Linux内核中提供了多种并发访问的保护机制,像原子操作、自旋锁、信号量、互斥锁、读写锁、RCU等,下面我将一一介绍这几种方式,并且有选择性的编写一些测试程序。

 

1.原子操作

原子操作是保证指令以原子地方式执行,执行过程不被打断。例如简单的i++指令,在多处理器架构或者是单处理器架构上会发生并发访问,这时候我们必须保证i++的原子性,因为一个简单i++操作过程是通过“读--修改--回写”来完成的,如果不保证他的原子性,可能在执行读的时候就被其他的处理器或者中断给抢占了这个资源,这样就打不到我们需要的效果,所以针对这样的指令我们必须“原子地”(不间断地)完成“读--修改--回写”。通过阅读此书我总结了一些原子操作的优点是开销小,而缺点是对于复杂的数据结构不适用。

 

2.自旋锁

如果临界区只有一个变量,那么原子变量可以解决问题,但是绝大多数情况临界区是有一个数据操作的集合,类似于“read--modify--write”的操作,这个时候用原子操作就不合适,这也是我上面所说的原子操作的缺点。而自旋锁就可以很好的解决这个问题,自旋锁在同一时刻只能被一个内核代码路径持有,如果另一个内核代码路径试图获取已经被持有的的自旋锁,那么该内核代码路径需要一直忙等待,直到自旋锁持有者释放该锁。如果该锁没有被其他内核代码路径持有(争用)那么就可以立即获得该锁。

自旋锁特点:

  1. 操作系统中锁的机制分为两种,一种是忙等待,一种是睡眠等待,自旋锁属于前者,当无法获取该锁时会不断尝试,直到获取为止。
  2. 同一时刻只有一个代码路径可获得该锁。
  3. 临界区中执行时间不能过长,不然外面忙等待的CPU比较浪费,特别的临界区中不能睡眠。
  4. 自旋锁可以再中断上下文中使用。

注意点:

使用自旋锁保护临界区的时候,不能发生中断,不论是硬中断还是软中断。不然假设一个代码段获取了自旋锁,此时发生了一个硬中断,也要获取自旋锁,由于自旋锁被抢占,所以中断只能处于忙等待状态。此时就导致了死锁的发生,自旋锁持有者因为被中断打断而不能尽快释放锁。而中断处理程序一直在忙等待该锁。对于这种情况LINUX内核有一个自旋锁的变体可以解决该情况spin_lock_irq(),它在获取锁之前关闭了本地处理器中断。

测试程序:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/freezer.h>
#include <linux/delay.h>

static DEFINE_SPINLOCK(hack_spinA);//定义一个自旋锁hack_spinA
static struct page *page;//定义一个指向页面结构的指针变量
static struct task_struct *lock_thread;//定义一个指向任务结构的指针变量

static int nest_lock(void)
{
 int order = 5;

 spin_lock(&hack_spinA);
 page = alloc_pages(GFP_KERNEL, order);
 if (!page) {
    printk("cannot alloc pages\n");
    return -ENOMEM;
 }

 spin_lock(&hack_spinA);  //不释放自旋锁,第二次尝试抢占锁
 msleep(10);
 __free_pages(page, order);
 spin_unlock(&hack_spinA);
 spin_unlock(&hack_spinA);

 return 0;
}

static int lockdep_thread(void *nothing)
{
 set_freezable();
 set_user_nice(current, 0);

 while (!kthread_should_stop()) {
    msleep(10);
    nest_lock();
 }
 return 0;
}

static int __init my_init(void)
{
    lock_thread = kthread_run(lockdep_thread, NULL, "lockdep_test");
    if (IS_ERR(lock_thread)) {
        printk("create kthread fail\n");
        return PTR_ERR(lock_thread);
    }

    return 0;
}
static void __exit my_exit(void)
{
 kthread_stop(lock_thread);
}
MODULE_LICENSE("GPL");
module_init(my_init);
module_exit(my_exit);

 

 

3.MCS锁

MCS锁是自旋锁的优化方案,目的是解决排队自旋锁(FIFO)CPU高速缓存行颠簸的情况。MCS的算法思想是每个锁的申请者只能在本地CPU上自旋,而不是全局变量上。在Linux内核中由一个CPU的链表来管理。下面是申请MCS锁的流程图:

image.png      

 

4.信号量和互斥锁

信号量允许系统进程进入睡眠状态,简单来说,信号量是一个计数器,它支持两个操作原语:P操作和V操作。信号量是在并行处理环境中多个处理器访问某个公共资源进行保护的机制。互斥锁用于互斥操作。学过实时操作系统的对这里应该很熟悉,这里我就不多介绍了。

信号量特点:

  1. 允许进程进入睡眠状态,也就是睡眠等待。
  2. 只要资源够,可多个进程同时操作信号量。

5.RCU

RCU是为了解决前面几个同步操作中多CPU争用共享的变量让高速缓存一致性变得很糟的问题。比如以读写信号量,而RCU机制需要实现的目标是读者线程没有同步开销,或者同步开销很小可以忽略不计,不需要额外加锁,不需要原子操作指令和内存屏障指令,即可以畅通无阻的访问,然后把需要同步的任务交给线程。这样读开销少了,整体性能就提高了。

 

 

总结:通过本期的阅读更加深刻理解了Linux系统并发与同步的关系,更加熟悉了Linux系统的同步操作,也学习到了一些新的同步操作,比如RCU等。

回复评论 (2)

Linux系统的同步操作是个学习重点,,

点赞  2024-3-19 07:45
引用: Jacktang 发表于 2024-3-19 07:45 Linux系统的同步操作是个学习重点,,

是的


点赞  2024-3-25 09:08
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复