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 > ))
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 <= ))
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 ;
} 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()的出现,相当于一堵墙,让编译器在处理完屏障之前的代码之前,不会处理屏障后面的代码。原来为了提高代码的执行效率,编译器都会适当的对代码进行指令重排,一般情况下这种重排不会影响程序功能,但是编译器毕竟不是人,某些对顺序有严格要求的代码,很可能无法被编译器准确识别,比如关闭和启用抢占的代码,这样,如果编译器把核心代码移出关闭抢占区间,那么很可能影响最终结果,因此,这种时候在关闭抢占后应该加上内存屏障,保障不会把后面的代码排到前面来。
Linux 下的同步机制的更多相关文章
- linux下数据同步、回写机制分析
一.前言在linux2.6.32之前,linux下数据同步是基于pdflush线程机制来实现的,在linux2.6.32以上的版本,内核彻底删掉了pdflush机制,改为了基于per-bdi线程来实现 ...
- 2017-2018-1 20155222 《信息安全系统设计基础》第10周 Linux下的IPC机制
2017-2018-1 20155222 <信息安全系统设计基础>第10周 Linux下的IPC机制 IPC机制 在linux下的多个进程间的通信机制叫做IPC(Inter-Process ...
- Linux下的IPC机制
Linux下的IPC机制 IPC(Inter-Process Communication)是多个进程之间相互沟通的一种方法.在linux下有多种进程间通信的方法. 共享内存 Linux内存共享有多种, ...
- linux下的同步与互斥
linux下的同步与互斥 谈到linux的并发,必然涉及到线程之间的同步和互斥,linux主要为我们提供了几种实现线程间同步互斥的 机制,本文主要介绍互斥锁,条件变量和信号量.互斥锁和条件变量包含在p ...
- linux下epoll实现机制
linux下epoll实现机制 原作者:陶辉 链接:http://blog.csdn.net/russell_tao/article/details/7160071 先简单回顾下如何使用C库封装的se ...
- linux下六大IPC机制【转】
转自http://blog.sina.com.cn/s/blog_587c016a0100nfeq.html linux下进程间通信IPC的几种主要手段简介: 管道(Pipe)及有名管道(named ...
- Linux内核的同步机制
本文详细的介绍了Linux内核中的同步机制:原子操作.信号量.读写信号量和自旋锁的API,使用要求以及一些典型示例 一.引言 在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程 ...
- Linux下线程同步的几种方法
Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量和信号量. 一.互斥锁(mutex) 锁机制是同一时刻只允许一个线程执行一个关键部分的代码. 1. 初始化锁 int pthrea ...
- linux内核级同步机制--futex
在面试中关于多线程同步,你必须要思考的问题 一文中,我们知道glibc的pthread_cond_timedwait底层是用linux futex机制实现的. 理想的同步机制应该是没有锁冲突时在用户态 ...
随机推荐
- mysql 从一个表中查询插入另一个表
insert into dnt_userfields (uid,realname ) select uid,nickname from discuz.dnt_users where uid>72 ...
- c#第一个程序-计算平方根
上课教的内容.做笔记了. using System; using System.Collections.Generic; using System.ComponentModel; using Syst ...
- 多个 label checkbox 组合 显示在同一个水平线上[前提Bootstrap框架]
<th align="left" valign="middle"> <label class="checkbox inline fo ...
- [android] AndroidManifest.xml - 【 manifest -> application】
语法: <application android:allowTaskReparenting=["true" | "false"] android:back ...
- 转载 Python导入模块的几种姿势
作为一名新手Python程序员,你首先需要学习的内容之一就是如何导入模块或包.但是我注意到,那些许多年来不时使用Python的人并不是都知道Python的导入机制其实非常灵活.在本文中,我们将探讨以下 ...
- php -- 设计模式 之 单例模式
实现单例的条件:三私一公 三私:私有化构造方法:不让外部创建对象 私有化克隆方法:不让外部克隆对象 私有静态属性:保存已经产生的对象 一公:公共静态方法:在类内部创建对象 实例: <?php / ...
- 一条SQL语句查询两表中两个字段
首先描述问题,student表中有字段startID,endID.garde表中的ID需要对应student表中的startID或者student表中的endID才能查出grade表中的name字段, ...
- freemarker2 指令
if,else,elseif 指令 <#if x==1> x is 1 </#if> <#if==1> x is 1 <#else> x is not ...
- C#------SortedLIst键值对的使用方法
方法: SortedList sf = new SortedList(); sf.Add(, "广州"); sf.Add(, "江门"); sf.Add(, & ...
- open() 函数以 a+ 模式打开文件
这种模式打开文件,可读可写,从文件顶部读取内容,从文件底部追加内容,文件不存在则自动创建 [root@localhost ~]$ cat 1.txt aaa bbb ccc In [1]: data ...