Linux 下的同步机制

2017-03-10



回想下最初的计算机设计,在单个CPU的情况下,同一时刻只能由一个线程(在LInux下为进程)占用CPU,且2.6之前的Linux内核并不支持内核抢占,当进程在系统地址运行时,能打断当前操作的只有中断,而中断处理完成后发现之前的状态是在内核,就不触发地调度,只有在返回用户空间时,才会触发调度。所以内核中的共享资源在单个CPU的情况下其实不需要考虑同步机制,尽管表面上看起来是多个进程在同时运行,其实那只是调度器以很小的时间粒度,调度各个进程运行的结果,事实上是一个伪并行。但是随着时代的发展,单个处理器根本满足不了人们对性能的需求,多处理器架构才应运而生。这种情况下,多个处理器之间的工作互不干扰,可实现真正的并行。

  但是操作系统只有一个,其中不乏很多全局共享的变量,即使是多CPU也不能同时对其进程操作。然而在多处理器情况下,如果我们不加以防护措施,极有可能两个进程同时对同一变量进行访问,这样就容易造成数据的不同步。这种情况是开发者和用户都无法忍受的。况且,在2.6之后的内核启用了内核抢占,即使进程运行在系统地址空间也有可能被抢占,基于此,内核同步机制便被提出来。

内核中的同步机制又很多,具体由原子操作、信号量、自旋锁、读写者锁,RCU机制等。每种方案都有其优缺点,且适用于不同的应用场景。

原子操作

原子操作在内核中主要保护某个共享变量,防止该变量被同时访问造成数据不同步问题。为此,内核中定义了一系列的API,在内核中定义了atomic_t数据类型,其定义的数据操作都像是一条汇编指令执行,中间不会被中断。atomic_t定义的数据类型和标准数据类型int/short等不兼容,数据的加减不能通过标准运算符,必须通过其本身的API,下面是一些该类型操作的API

static __inline__ void atomic_add(int i, atomic_t * v)
static __inline__ void atomic_sub(int i, atomic_t * v)
static inline int atomic_add_return(int i, atomic_t *v)
static __inline__ long atomic_sub_return(int i, atomic_t * v)

基于上面的基础API,还实现了其他的API,这里就不在列举。

信号量


信号量一般实现互斥操作,但是可以指定处于临界区的进程数目,当规定数目为1时,表示此为互斥信号量。信号量在内核中的结构如下

struct semaphore {
    raw_spinlock_t        lock;
    unsigned int        count;
    struct list_head    wait_list;
};

开头是一个自旋锁,用以保护该数据结构的操作,count指定了信号量关联的资源允许同时访问的进程数目,wait_list是等待访问资源的进程链表。和自旋锁相比,信号量的一个好处允许等待的进程睡眠,而不是一直在轮询请求。所以信号量比较适合于较长的临界区。信号量操作很简单,初始初始化一个信号量,在临界资源前需要down操作以请求获得信号量,执行完毕执行up操作释放资源。

相关代码如下

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);
}
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);
}

对于down操作,首先获取信号量结构的自旋锁,并会关闭当前CPU的中断,然后如果count还大于0,则直接分配资源,count--,否则调用down函数阻塞当前进程,down函数中直接调用了down_common函数。

static inline int __sched __down_common(struct semaphore *sem, long state,
                                long timeout)
{
    struct task_struct *task = current;
    struct semaphore_waiter waiter;

    list_add_tail(&waiter.list, &sem->wait_list);
    waiter.task = task;
    waiter.up = false;

    for (;;) {
        if (signal_pending_state(state, task))
            goto interrupted;
        if (unlikely(timeout <= 0))
            goto timed_out;
        __set_task_state(task, state);
        raw_spin_unlock_irq(&sem->lock);
        timeout = schedule_timeout(timeout);
        raw_spin_lock_irq(&sem->lock);
        if (waiter.up)
            return 0;
    }

 timed_out:
    list_del(&waiter.list);
    return -ETIME;

 interrupted:
    list_del(&waiter.list);
    return -EINTR;
}

首先构建了一个semaphore_waiter结构,插入到信号量结构的等待进程链表中。timeout是一个超时时间,当设置为小于等于0时表示不在此等待资源。通过这些检查后,设置当前进程为TASK_INTERRUPTIBLE状态,表示可被中断唤醒的阻塞。然后开启本地中断表示当前任务告一段落,下面要调用schedule_timeout进程调度。在具体切换进程后,下半部分的代码就是下次被调度的时候执行了。

而对于up操作,首先获取自旋锁,如果当前等待队列为空,则单纯的增加count表示可用资源增加,否则执行_up操作,该函数实现比较简单。首先从等待链表中移除对应节点,设置结构的up信号为true,然后调用wake_up_process函数唤醒执行进程。这样唤醒是吧进程加入就绪链表中,可以被调度器正常调度。

static noinline void __sched __up(struct semaphore *sem)
{
    struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
                        struct semaphore_waiter, list);
    list_del(&waiter->list);
    waiter->up = true;
    wake_up_process(waiter->task);
}

自旋锁

自旋锁恐怕是内核中应用最为广泛的同步机制了,在内核中表现为两个功用:

1、对于数据结构或者变量的保护

2、对于临界区代码的保护

对于自旋锁的操作很简单,其结构spinlock_t,对于自旋锁的操作,根据对临界区的不会要求级别,有多种API可以选择

static inline void spin_lock(spinlock_t *lock)
static inline void spin_unlock(spinlock_t *lock)
static inline void spin_lock_bh(spinlock_t *lock)
static inline void spin_unlock_bh(spinlock_t *lock)
static inline void spin_lock_irq(spinlock_t *lock)
static inline void spin_unlock_irq(spinlock_t *lock)

前面最基础的还是spin_lock,用以获取自旋锁,在具体获取之前会调用preempt_disable禁止内核抢占,所以自旋锁保护的临界代码执行期间会不会被调度。本局临界代码的性质,可以调用spin_lock_bh禁止软中断或者通过调用spin_lock_irq禁止本地CPU的中断。有自旋锁保护的代码不能进入睡眠状态,因为等待获取锁的CPU会一直轮询,不做其他事情,如果在临界区内睡眠,则对CPU性能耗能较大。

通过上面函数获取锁和释放锁主要用于对临界代码的保护,操作本身是一个原子操作。

对于数据结构的保护,自旋锁往往作为一个字段嵌入到数据结构中,在操作具体的结构之前,需要获取锁,操作完毕释放锁。

读写者锁

读写者问题其实就是针对读写操作分别做的处理,可以看到其他的同步机制没有区分读写操作,只要是线程访问,就需要加锁,但是很多资源在不是写操作的情况下,是可以允许多进程访问的。因此为了提高效率,读写者锁就应运而生。读写者锁在执行写操作时,需要加writelock,此时只有一个线程可以进入临界区,而在执行读操作时,加readlock,此时可以允许多个线程进入临界区。适用于读操作明显多于写操作的临界区。

RCU机制

RCU机制是一种较新的内核同步机制,可以提供两种类型的保护:对数据结构和对链表。在内核中应用的相当频繁。

RCU机制使用条件:

  • 对共享资源的访问大部分时间是只读的,写操作相对较少。
  • 在RCU保护的代码范围内,不能进入睡眠。
  • 受保护资源必须通过指针访问。

RCU保护的数据结构,不能反引用其指针,即不能*ptr获取其内容,必须使用其对应的API。同时反引用指针并使用其结果的代码,必须使用rcu_read_lock()和rcu_read_unlock()保护起来。

如果要修改ptr指向的对象,需要先创建一个副本,然后调用rcu_assign_pointer(ptr,new_ptr)进行修改。所以这种情况,受保护的数据结构允许读写并发执行,因为实质上是操作两个结构,只有在对旧的数据结构访问完成后,才会修改指针指向。

 内存和优化屏障

在看内核源码的时候经常看见有barrier()的出现,相当于一堵墙,让编译器在处理完屏障之前的代码之前,不会处理屏障后面的代码。原来为了提高代码的执行效率,编译器都会适当的对代码进行指令重排,一般情况下这种重排不会影响程序功能,但是编译器毕竟不是人,某些对顺序有严格要求的代码,很可能无法被编译器准确识别,比如关闭和启用抢占的代码,这样,如果编译器把核心代码移出关闭抢占区间,那么很可能影响最终结果,因此,这种时候在关闭抢占后应该加上内存屏障,保障不会把后面的代码排到前面来。

时间: 2024-10-26 05:50:45

Linux 下的同步机制的相关文章

Linux 内核的同步机制,第 1 部分 + 第二部分(转)

http://blog.csdn.net/jk198310/article/details/9264721  原文地址: Linux 内核的同步机制,第 1 部分 一. 引言 在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问.尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问.在主流的Linux内核中包含了几乎所有现代的操作系统具有的同步机制,这些同步机制包括:原子操作

linux下磁盘管理机制--RAID

RAID(Redundant Array Of Independent Disks):独立磁盘冗余阵列.RAID的最初出现的目的是为了解决中小型企业因经费原因使用不起SCSCI硬盘,而不得不使用像IDE较廉价的磁盘情况下,将多块IDE磁盘通过某种机制组合起来,使得IDE磁盘在一定程度上提高读写性能的一种机制.当然,现在也可以将SCSCI类的磁盘也可以做成RAID来提高磁盘的读写性能. 一.RAID的级别 RAID机制通过级别来RAID级别来定义磁盘的组合方式.常见的级别有:RAID0,RAID1

2017-2018-1 20155222 《信息安全系统设计基础》第10周 Linux下的IPC机制

2017-2018-1 20155222 <信息安全系统设计基础>第10周 Linux下的IPC机制 IPC机制 在linux下的多个进程间的通信机制叫做IPC(Inter-Process Communication),它是多个进程之间相互沟通的一种方法.在linux下有多种进程间通信的方法:半双工管道.命名管道.消息队列.信号.信号量.共享内存.内存映射文件,套接字等等.使用这些机制可以为linux下的网络服务器开发提供灵活而又坚固的框架. 以上内容引用自CSDN 共享内存 共享内存是在多个

linux下磁盘管理机制--LVM

当我们用传统分区方法使用磁盘时,当出现分区大小不够用的时候,通常只能添加添加一个更大的磁盘,重新创建分区来扩展空间.但是,这样只能是将原来的磁盘下线,换上新的磁盘,在将原始数据写入,在实际的生产过程中是不允许的.此时就需要使用逻辑卷LVM这种磁盘分区管理了. 逻辑卷是将硬盘空间重新"分割"成大小相等的块(PE)组成的PV放到一个容器(VG)中,当需要可以随时向这个容器中取出这样的块,来实现动态调整磁盘空间大小.当然新添加的块不会改变原来的文件系统,而且原磁盘也不用下线. 下面说明逻辑卷

linux下六大IPC机制【转】

转自http://blog.sina.com.cn/s/blog_587c016a0100nfeq.html linux下进程间通信IPC的几种主要手段简介: 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信:信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身:linux除了支持Un

Linux下线程同步的几种方法

Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量和信号量. 一.互斥锁(mutex) 锁机制是同一时刻只允许一个线程执行一个关键部分的代码.  1. 初始化锁 int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr); 其中参数 mutexattr 用于指定锁的属性(见下),如果为NULL则使用缺省属性. 互斥锁的属性在创建锁的时候指定,在LinuxThreads实

linux下文件同步利器rsync

rsync rsync是linux下的数据备份工具,支持远程同步.本地复制. 这是一篇rsync简单的使用文章,很多rsync的认识不足,更多的rsync知识请 到rsync官网研读:https://rsync.samba.org/how-rsync-works.html rsyrsync是系统自带的(至少2.6内核是这样的),如果不是自己编译的内核应该是自带. 检查一个安装 rpm -qa | grep rsync 如果没有安装,自己下载rpm包或者使用yum安装,这里就不演示. 配置rsyn

Linux内核的同步机制---自旋锁

自旋锁的思考:http://bbs.chinaunix.net/thread-2333160-1-1.html 近期在看宋宝华的<设备驱动开发具体解释>第二版.看到自旋锁的部分,有些疑惑.所以来请教下大家. 以下是我參考一些网络上的资料得出的一些想法,不知正确与否.记录下来大家讨论下: (1) linux上的自旋锁有三种实现: 1. 在单cpu.不可抢占内核中,自旋锁为空操作. 2. 在单cpu,可抢占内核中,自旋锁实现为"禁止内核抢占".并不实现"自旋"

linux下 signal信号机制的透彻分析与各种实例讲解

转自:http://blog.sina.com.cn/s/blog_636a55070101vs2d.html 转自:http://blog.csdn.net/tiany524/article/details/17048069 首先感谢上述两位博主的详细讲解. 虽然内容有点长,但是分析的很全面,各种实例应用基本都考虑到了. 本文将从以下几个方面来阐述信号: (1)信号的基本知识 (2)信号生命周期与处理过程分析 (3) 基本的信号处理函数 (4) 保护临界区不被中断 (5) 信号的继承与执行 (