Linux Kernel Synchronization && Mutual Exclusion、Linux Kernel Lock Mechanism Summarize(undone)

目录

1. 同步与互斥
2. 锁定内存总线原子操作
3. 信号量
4. 自旋锁
5. RCU机制
6. PERCPU变量

1. 同步与互斥

在多任务操作系统中,多个进程按照不可预测的顺序进行,因为多个进程之间常常存在相互制约或者相互依赖的关系,这些关系可以被划分为同步和互斥的关系

从本质上来说,同步和互斥也可以理解为进程/线程间同步通信的一种机制,只是这里传递的是一种"争用关系",关于Linux进程间通信和同步、以及不同和互斥的相关知识,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/3867214.html

0x1: 在内核编程中如果不使用锁可能会导致的问题

假设使用一个全局变量printer表示可用的打印机数量,这个变量被初始设置为1,进程按照如下的方法申请使用打印机

int request_printer()
{
    while (printer == 0)
        wait();
    printer--;
    ...
{

这段代码在单线程(在linux下就是单进程,因为linux没有严格的线程概念,linux只有进程)下运行是没有问题的,但是在多线程情况下,会包含着一个隐含的问题,这个问题需要从"--printer"的汇编代码才能看出,在x86平台上,"--printer"指令可能会被编译成如下的汇编指令

# printer--
movl printer %eax
decl %eax
movl %eax printer

由于CPU在每条指令流水阶段(CPU的指令集中的任意一条指令)的最后会进行"中断检查",因此上面的3条汇编指令之间都有可能发生中断,如果将这3条指令看成一个整体,当执行到其中一半的时候发生了中断,就会导致最后的结果不一致性。从本质上来说,要解决这个问题,就需要建立一个"原子操作"的概念,将需要强制一致性的操作(一条指令、或者多条指令)都"封装"到一个原子操作中。

0x2: 内核编程中涉及到的互斥锁机制

1. 锁定内存总线原子操作
2. 信号量
3. 自旋锁
4. RCU机制
5. PERCPU变量

Relevant Link:

深入linux内核架构(中文版).pdf 2.3章 同步与互斥
http://blog.csdn.net/lucien_cc/article/details/7440225

2. 锁定内存总线原子操作

我们继续回到之前提出的那个存在同步问题的case中,这个问题的根源是"printer--"不是原子操作引起的,为了解决这个问题,x86平台提供了dec和inc指令,这两条指令可以直接对内存进行减一或加一的操作而不产生中途的中断

因此上面的指令可以直接编译为decl printer,这样在单CPU系统上,这就是一个原子操作了,但是在多处理器系统上,上述问题依旧存在,因为在CPU内部,decl printer还是由几条微指令组成的,它还是需要首先把printer的值从内存读取到CPU中的寄存器中,然后执行减一操作,最后再写入内存,只不过在这个过程中,CPU不会进行中断检查,因此对单CPU来说,它是原子操作,但是对多CPU系统来说,CPU之间依然存在不同步的问题,脏读、幻读的现象依旧存在。

为此,x86提供了lock前缀,来避免这个问题

# printer--
lock decl printer

lock前缀告诉CPU,在执行当前指令期间锁住内存总线,这样在decl操作的微指令执行期间,如果另外的CPU访问printer,由于得不到总线仲裁的许可,在decl操作完成之前,不会访问到printer内存变量,因此它保证了在多处理器上的原子性

在Linux内核中常用的原子操作有

1. atomic_add
2. atomic_sub
3. atomic_xchg
4. atomic_inc
5. atomic_dec
6. atomic_cmpxchg
7. atomic_min
8. atomic_max
9. atomic_and
10. atomic_or
11. atomic_xor
12. atomic_read

0x1: 内核实现代码分析

/source/arch/x86/include/asm/atomic.h

static inline void atomic_dec(atomic_t *v)
{
    asm volatile(LOCK_PREFIX "decl %0" : "+m" (v->counter));
}

0x2: 使用方法

1. 在多CPU系统中,使用lock前缀保证CPU间的数据一致性
2. 使用由CPU微指令组成的原子操作进行读取、增、减

Relevant Link:

3. 信号量(semaphore)

信号量(semaphore)其实是建立在原子操作的基础上的。从数据结构上角度来看,信号量实际上是一个整数型变量,有两个最基本的原子操作:

1. P(Prolagen)操作:对应于内核中的down()
2. V(Verhogen)操作:对应于内核中的up()
//它们统称PV原语

0x1: 内核实现代码分析

/source/include/linux/semaphore.h

struct semaphore
{
    //1. 信号量
    raw_spinlock_t          lock;

    //2. 等待该信号量的进程个数
    unsigned int            count;

    //3. 该信号量的等待队列
    struct list_head        wait_list;
};

/source/kernel/locking/semaphore.c

down()

void down(struct semaphore *sem)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(sem->count > 0))
        sem->count--;
    else
        __down(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}

up()

void up(struct semaphore *sem)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(list_empty(&sem->wait_list)))
        sem->count++;
    else
        __up(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}

0x2: 使用方法

1. 将信号量的值减1
2. 如果信号量的值小于0,则进入等待状态,否则继续执行。访问完资源之后,线程释放信号量,进行如下操作
3. 将信号量的值加1
4. 如果信号量的值小于1,唤醒一个等待中的线程

Relevant Link:

4. 自旋锁

当需要对成块的代码进行"原子化串行操作",即保证一个代码块的原子性,最好的办法就是在一个代码块执行前关闭中断,在代码块结束之后再开启中断,在x86中

1. 关闭中断:通过cli指令清除标志寄存器中的中断允许位,即关闭中断
2. 开启中断:通过sti指令开启中断

在关闭中断期间,CPU每执行完一条指令,不会进行中断检查,由于关闭中断后,系统的外部设备可能得不到响应,因此这就要求进程关中断的时间必须是短暂的,并且在关闭中断期间,不能使系统进入睡眠状态(因为睡眠的调度也是通过中断完成的)

通过关中断的方式可以使CPU在执行某些"临界"代码块中免受干扰,但是cli只能关闭当前CPU的中断,但是在多CPU系统中,还是无法避免CPU间脏读、幻读的问题,虽然可以通过信号量的方式来防止其他CPU在同时访问同一个数据,但是由于以下原因,信号量显得不太合适

1. 信号量引起的进程切换消耗相对较大,由于这类"临界"代码的特征是执行时间非常短暂,也就是说CPU执行信号量进程切换的消耗远远大于"临界"代码本身执行的消耗。
所以,在这种情况下,让另一个CPU进入"忙等待"状态,直到临界代码操作完成

2. 由于信号量可能引起进程切换,但是在某些环境下,是不允许进程切换的,例如
    1) 中断环境中是不允许进程切换的,否则会引起panic

因此在这种情况下,当一个进程不能进入信号量包裹的临界区(PV)时,最好的办法是让CPU进入忙等待状态,即使用自旋锁

1. 设置一个锁变量,用来保护临界区代码
2. 当CPU进入临界区之前,检查锁的状态
3. 如果已经上锁,则当前CPU(或者是单CPU模拟出的多线程)执行一个"空循环"反复检测锁的状态,直到其他的CPU(或者是单CPU模拟出的其他线程)
4. 由于在测试锁的期间,CPU处于忙等待状态的"自旋"状态,因此把这种机制称为自旋锁

0x1: 内核实现代码分析

\linux-3.15.5\include\linux\spinlock_types.h

typedef struct raw_spinlock
{
    arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
    unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned int magic, owner_cpu;
    void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} raw_spinlock_t;

在使用自旋锁的时候,首先需要使用spin_lock_init来初始化,spin_lock_init是一个宏

\linux-3.15.5\include\linux\spinlock.h

# define raw_spin_lock_init(lock)                    do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)

申请和释放锁分别由spin_lock_irq()、spin_unlock_irq()完成

/source/include/linux/spinlock_api_smp.h

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
    //关闭本地CPU的IRQ中断响应
    local_irq_disable();

    /*
    关闭进程抢占,由于中断或系统调用之后,可能会调度其他的进程运行(例如当前进程的时间片用完,或者有一个拥有更高优先级的进程已经进入就绪状态),preempt_disable关闭调度器这个功能,从而保证当前进程在执行临界区代码的过程中不会被其他进程干扰
    */
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);

    /*
    关闭中断,这是一个宏,经过层层翻译后最终调用一条汇编指令
    /source/arch/x86/include/asm/irqflags.h
    static inline void native_irq_disable(void)
    {
        asm volatile("cli": : :"memory");
    }
    */
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

我们知道,在单CPU系统中,进入临界区代码只需要关闭中断就可以了,但是在多CPU(或者在单CPU模拟的多线程)的系统中,则需要测试自旋锁的状态

/source/arch/x86/include/asm/spinlock.h

static __always_inline void __ticket_spin_lock(raw_spinlock_t *lock)
{
    short inc = 0x0100;

    asm volatile (
        //锁住内存总线
        LOCK_PREFIX "xaddw %w0, %1\n"
        "1:\t"

        //如果lock->lock == 0,则说明成功获取到了这个自旋锁,跳转到标号2,成功退出
        "cmpb %h0, %b0\n\t"
        "je 2f\n\t"

        //否则进入"自旋状态",执行nop指令,并测试lock->slock是否为0
        "rep ; nop\n\t"
        "movb %1, %b0\n\t"
        /* don‘t need lfence here, because loads are in-order */
        "jmp 1b\n"
        "2:"
        : "+Q" (inc), "+m" (lock->slock)
        :
        : "memory", "cc");
}

/*
在单CPU下该函数展开为一个空操作
*/

从内核代码中可以看出,当一个CPU获取自旋锁失败时,这个CPU就在循环中做"无用功",等待其他CPU(或者单CPU模拟的多线程)释放自旋锁,所以,要求持有自旋锁的时间必须尽可能短暂,并且持有自旋锁时不能进入睡眠状态

在单CPU系统中,spin_unlock_irq()只需要打开中断就可以了,在多CPU系统中,spin_unlock_irq()还需要把spinlock设置为开锁状态(即释放自旋锁的持有)

0x2: 使用方法

在使用自旋锁的时候,spin_lock_irq()和spin_unlock_irq()保护临界区代码不受干扰,即在申请自旋锁的时候需要关闭中断,释放自旋锁的时候又要开启中断,这里存在一个潜在的问题

1. 某段代码在调用spin_lock_irq()之前需要关闭中断,之后获取了某个自旋锁,退出临界区后,再释放自旋锁
2. 在释放自旋锁时,spin_unlock_irq()开启了中断,然而该段代码此时可能根本不允许开启中断
3. 因此我们需要在调用申请自旋锁时保存当时的中断许可状态,在释放自旋锁时恢复之前的状态,而不是盲目地开启中断

spin_lock_irqsave、spin_unlock_irqrestore就是用来完成这个工作的

/source/include/linux/spinlock.h

/*
由于中断许可位位于CPU的标志寄存器中,因此spin_lock_irqsave()在获取自旋锁之后,把标志寄存器的值保存到flags中,而spin_unlock_irqrestore()在释放自旋锁之后,根据flags恢复标志寄存器
*/
#define spin_lock_irqsave(lock, flags)                          do {                                    raw_spin_lock_irqsave(spinlock_check(lock), flags);        } while (0)

static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
{
    raw_spin_unlock_irqrestore(&lock->rlock, flags);
}

0x3: 代码示例

spin_lock_irqsave(...);
//some code to do somethings
spin_unlock_irqrestore(...);

Relevant Link:

http://blog.csdn.net/hustyangju/article/details/40391815
http://www.ibm.com/developerworks/cn/linux/l-cn-spinlock/
http://www.searchtb.com/2011/01/pthreads-mutex-vs-pthread-spinlock.html
http://blog.csdn.net/zhanglei4214/article/details/6837697

5. RCU机制

在实际应用中,对于某些关键数据结构而言,读取操作的次数远远超过修改操作的次数,典型的例子就是内核中的路由表。在读取操作次数远大于写入操作的情况下,自旋锁带来了不必要的消耗,为了解决这个问题,Linux内核专门设计了RCU机制,它的基本原理就是读操作不加锁,写操作必须加锁

Relevant Link:

深入linux内核架构(中文版).pdf 35 page

6. PERCPU变量

Relevant Link:

深入linux内核架构(中文版).pdf 39 page

Copyright (c) 2014 LittleHann All rights reserved

 

时间: 2024-10-28 20:49:27

Linux Kernel Synchronization && Mutual Exclusion、Linux Kernel Lock Mechanism Summarize(undone)的相关文章

Linux Communication Mechanism Summarize(undone)

目录 1. Linux通信机制分类简介 2. Inter-Process Communication (IPC) mechanisms: 进程间通信机制 3. 多线程并行中的阻塞和同步 4. Ring3和Ring0的通信机制 5. 远程网络通信 1. Linux通信机制简介 在开始学习Linux下的通信机制之前,我们先来给通信机制下一个定义,即明白什么是通信机制?为什么要存在通信机制? 0x1: Linux通信目的 1. 数据传输: 一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节

Linux发行版介绍、Linux系统基础使用入门、Linux命令帮助、Linux基础命令

计算机打的基础知识:CPU(运算器.控制器).memory.I/O(输入设备.输出设备) 程序运行模式: 用户空间:user space,us (可执行普通指令) 内核空间:system space (可执行特权指令) POS:Postable Operating System 可移植操作系统 POSIX 可移植操作系统规范 运行程序格式: Windows:EXE,库文件dll(dynamic link library 动态链接库) Linux:ELF,库文件so(shared object 共

【Linux基础入门】1、Linux系统简介

1.1 Linux为何物? Linux 就是一个操作系统,就像你多少已经了解的 Windows(xp,7,8,10)和 Mac OS .这里简单介绍一下操作系统在整个计算机系统中的角色.我们的应用体系主要由四层构成,分别为:硬件(最大层).内核.系统调用和应用程序,我们的 Linux 也就是系统调用和内核那两层.当然直观地看,我们使用的操作系统还包含一些在其上运行的应用程序,比如文本编辑器.浏览器.电子邮件等. 1.2 Linux历史简介 操作系统始于二十世纪五十年代,当时的操作系统能运行批处理

Linux 入门记录:六、Linux 硬件相关概念(硬盘、磁盘、磁道、柱面、磁头、扇区、分区、MBR、GPT)

一.硬盘 硬盘的功能相当简单但很重要,它负责记录系统所需要的各种数据.硬盘记录数据有两个方面,一个是硬件方面的存储原理和结构,另外一方面则是软件方面的数据和文件系统.硬盘的主要行为就是数据的存放和取出. 构成硬盘的基本元件,主要可以分为实际存储数据用的磁盘(Plat Platter).读取数据用的磁头(Head).帮助磁头搜寻读取位置的马达等等,这些元件构成一个能够在不同磁盘上的任意位置读取数据的结构体. 二.磁盘(Plat Platter) 磁盘是硬盘中真正用来存储信息的部分,外观上如同圆盘一

Linux 入门记录:八、Linux 文件系统

一.文件系统 操作系统通过文件系统管理文件及数据,磁盘或分区需要创建文件系统之后,才能被操作系统所用,创建文件系统的过程又称之为格式化.没有文件系统的设备又称之为裸设备(raw),某些环境会需要裸设备,例如安装 Oracle 时会需要裸设备. 常见的文件系统类型 常见的文件系统有 fat32.NTFS.ext2.ext3.ext4.xfs.HFS 等.其中,fat32 和 NTFS 是 Windows 中的文件系统,ext2.ext3.ext4.xfs.HFS 是 Linux 中的文件系统.NT

Linux基础(05)、Linux进阶命令

目录 一.进阶命令 二.系统命令 三.压缩和归档 3.1.归档 3.2.压缩 3.3.归档并压缩 归档.接档:tar -cf.tar -tvf 压缩.解压:gzip.gunzip 归档并压缩:tar -czf 接档并解压:tar -xzf 一.进阶命令 find //列出当前目录以及子目录(包括隐藏文件) | //管道符,作用:前一个命令输出结果,作为后一个命令的输入 grep //文本搜索工具,例:gerp bash for99.sh 在for99.sh中搜索带有bash的行 //ll | g

Linux 入门记录:十三、Linux 扩展权限

一.默认权限 每一个终端都有一个 umask 属性,是用来确定新建文件或目录的默认权限的权限“掩码”(mask 有“掩码”的含义,至于 u,后面说). Linux 中一般有默认的权限掩码,使用命令 umask 用以查看或设置: umask 0022 带权限值是设置,否则是查看 一般地,普通用户(uid 为 500 以上)的权限掩码认为 0002,root 用户(uid 为 0)的权限掩码默认为 0022. 如果新建了一个文件或目录,那么这个文件或目录的默认权限等于 文件或目录的默认最高权限 &

Linux 入门记录:二、Linux 文件系统基本结构

一.树状目录结构 Linux 文件系统是一个倒置的单根树状结构.文件系统的根为"/":文件名严格区分大小写:路径使用"/"分割(Windows 中使用"\"). 树状目录结构图如下(引用自菜鸟教程): 以下是对上图中各个目录的解释(引用自菜鸟教程,略作修改): 目录 描述 /bin Binary 的缩写,这个目录存放着最经常使用的命令 /boot 存放着 Linux 启动时的一些核心文件,包括一些连接文件和镜像文件 /dev Device 的缩写

Linux 入门记录:十、Linux 下获取帮助

一.获取帮助 Linux 提供了极为详细的帮助工具和文档,通过查阅相关文档,可以大大减少需要记忆的东西并提高效率. 二.--help参数 几乎所有命令都可以使用 -h 或 --help 参数获取命令的详细信息. 三.man命令 man 命令使 Linux 中最为常见的帮助命令,将要获取帮助的命令作为参数执行 man 命令,就可以获取相应的文档帮助. man 文档分为很多类型: 类型 描述 1 用户命令 2 内核系统调用 3 库函数 4 特殊文件设备 5 文件格式和规范 6 游戏 7 规范.标准和