Linux编程--文件原子 *** 作

Linux编程--文件原子 *** 作,第1张

当多个进程同时访问一个文件的时候,普通的write/read在执行的时候,无法保证 *** 作原子性,可能会导致文件被污染,达不到预期的结果。

任何一个需要多个函数调用的 *** 作都不可能是原子 *** 作,因为在两个函数调用间,内核可能会将进程挂起执行另外的进程。

如果想要避免这种情况的话,则需要使用pread/pwrite函数

ssize_t pread(int fd ,void *buffer ,size_t size,off_t offset)

返回真正读取到的字节数,offset是指的从文件开始位置起的offset个字节数开始读。其余的参数与read无异。

PS:

pread是无法中断的原子 *** 作,无法中断它的定位和读取 *** 作

pread读取过后的文件偏移量不会发生改变

同理pwrite也是一样的

而在文件创建的时候也是一样的,当需要做文件创建同步的时候,我们需要在O_CREATE的时候,加上O_EXCL标志位,当已经创建过的话,会返回fd,否则返回错误

int dup( int filedes):

传入一个文件描述符,返回当前可用的最小文件描述符。

int dup2(int filedes,int filedes2):

传入文件描述符,以及新的文件描述符,如果新的文件描述符所指向的文件已经打开,则会强行将其关闭后,将该文件描述符指向到已存在的文件描述符。

如果filedes和filedes2指向同一个文件,则不做任何处理,直接返回filedes2,不会关闭文件

新返回回来的filedes2会共享filedes的文件状态标识,文件偏移量等等信息。因为它们的文件指针会指向文件表的同一个位置。只是fd不一样而已。

Linux内核设计与实现 十、内核同步方法

手把手教Linux驱动5-自旋锁、信号量、互斥体概述

== 基础概念: ==

并发 :多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行

竞态 :并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竟态状态。

临界资源 :多个进程访问的资源

临界区 :多个进程访问的代码段

== 并发场合: ==

1、单CPU之间进程间的并发 :时间片轮转,调度进程。 A进程访问打印机,时间片用完,OS调度B进程访问打印机。

2、单cpu上进程和中断之间并发 :CPU必须停止当前进程的执行中断

3、多cpu之间

4、单CPU上中断之间的并发

== 使用偏向: ==

==信号量用于进程之间的同步,进程在信号量保护的临界区代码里面是可以睡眠的(需要进行进程调度),这是与自旋锁最大的区别。==

信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。它负责协调各个进程,以保证他们能够正确、合理的使用公共资源。它和spin lock最大的不同之处就是:无法获取信号量的进程可以睡眠,因此会导致系统调度。

1、==用于进程与进程之间的同步==

2、==允许多个进程进入临界区代码执行,临界区代码允许睡眠;==

3、信号量本质是==基于调度器的==,在UP和SMP下没有区别;进程获取不到信号量将陷入休眠,并让出CPU;

4、不支持进程和中断之间的同步

5、==进程调度也是会消耗系统资源的,如果一个int型共享变量就需要使用信号量,将极大的浪费系统资源==

6、信号量可以用于多个线程,用于资源的计数(有多种状态)

==信号量加锁以及解锁过程:==

sema_init(&sp->dead_sem, 0)/ 初始化 /

down(&sema)

临界区代码

up(&sema)

==信号量定义:==

==信号量初始化:==

==dowm函数实现:==

==up函数实现:==

信号量一般可以用来标记可用资源的个数。

举2个生活中的例子:

==dowm函数实现原理解析:==

(1)down

判断sem->count是否 >0,大于0则说明系统资源够用,分配一个给该进程,否则进入__down(sem)

(2)__down

调用__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT)其中TASK_UNINTERRUPTIBLE=2代表进入睡眠,且不可以打断;MAX_SCHEDULE_TIMEOUT休眠最长LONG_MAX时间;

(3)list_add_tail(&waiter.list, &sem->wait_list)

把当前进程加入到sem->wait_list中;

(3)先解锁后加锁

进入__down_common前已经加锁了,先把解锁,调用schedule_timeout(timeout),当waiter.up=1后跳出for循环;退出函数之前再加锁;

Linux内核ARM构架中原子变量的底层实现研究

rk3288 原子 *** 作和原子位 *** 作

原子变量适用于只共享一个int型变量;

1、原子 *** 作是指不被打断的 *** 作,即它是最小的执行单位。

2、最简单的原子 *** 作就是一条条的汇编指令(不包括一些伪指令,伪指令会被汇编器解释成多条汇编指令)

==常见函数:==

==以atomic_inc为例介绍实现过程==

在Linux内核文件archarmincludeasmatomic.h中。 执行atomic_read、atomic_set这些 *** 作都只需要一条汇编指令,所以它们本身就是不可打断的。 需要特别研究的是atomic_inc、atomic_dec这类读出、修改、写回的函数。

所以atomic_add的原型是下面这个宏:

atomic_add等效于:

result(%0) tmp(%1) (v->counter)(%2) (&v->counter)(%3) i(%4)

注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中 *** 作。如果出现上下文切换,切换机制会做寄存器上下文保护。

(1)ldrex %0, [%3]

意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %0, %0, %4

result = result + i

(3)strex %1, %0, [%3]

意思是将result保存到&v->counter指向的内存中, 此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。

(4) teq %1, #0

测试strex是否成功(tmp == 0 ??)

(5)bne 1b

如果发现strex失败,从(1)再次执行。

Spinlock 是内核中提供的一种比较常见的锁机制,==自旋锁是“原地等待”的方式解决资源冲突的==,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源),一般应用在==中断上下文==。

1、spinlock是一种死等机制

2、信号量可以允许多个执行单元进入,spinlock不行,一次只能允许一个执行单元获取锁,并且进入临界区,其他执行单元都是在门口不断的死等

3、由于不休眠,因此spinlock可以应用在中断上下文中;

4、由于spinlock死等的特性,因此临界区执行代码尽可能的短;

==spinlock加锁以及解锁过程:==

spin_lock(&devices_lock)

临界区代码

spin_unlock(&devices_lock)

==spinlock初始化==

==进程和进程之间同步==

==本地软中断之间同步==

==本地硬中断之间同步==

==本地硬中断之间同步并且保存本地中断状态==

==尝试获取锁==

== arch_spinlock_t结构体定义如下: ==

== arch_spin_lock的实现如下: ==

lockval(%0) newval(%1) tmp(%2) &lock->slock(%3) 1 <<TICKET_SHIFT(%4)

(1)ldrex %0, [%3]

把lock->slock的值赋值给lockval;并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %1, %0, %4

newval =lockval +(1<<16)相当于next+1;

(3)strex %2, %1, [%3]

newval =lockval +(1<<16)相当于next+1;

意思是将newval保存到 &lock->slock指向的内存中, 此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。

(4) teq %2, #0

测试strex是否成功

(5)bne 1b

如果发现strex失败,从(1)再次执行。

通过上面的分析,可知关键在于strex的 *** 作是否成功的判断上。而这个就归功于ARM的Exclusive monitors和ldrex/strex指令的机制。

(6)while (lockval.tickets.next != lockval.tickets.owner)

如何lockval.tickets的next和owner是否相等。相同则跳出while循环,否则在循环内等待判断;

* (7)wfe()和smp_mb() 最终调用#define barrier() asm volatile ("": : :"memory") *

阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。

== arch_spin_unlock的实现如下: ==

退出锁时:tickets.owner++

== 出现死锁的情况: ==

1、拥有自旋锁的进程A在内核态阻塞了,内核调度B进程,碰巧B进程也要获得自旋锁,此时B只能自旋转。 而此时抢占已经关闭,(单核)不会调度A进程了,B永远自旋,产生死锁。

2、进程A拥有自旋锁,中断到来,CPU执行中断函数,中断处理函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,只能自旋,产生死锁。

== 如何避免死锁: ==

1、如果中断处理函数中也要获得自旋锁,那么驱动程序需要在拥有自旋锁时禁止中断;

2、自旋锁必须在可能的最短时间内拥有

3、避免某个获得锁的函数调用其他同样试图获取这个锁的函数,否则代码就会死锁;不论是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁,如果试图这么做,系统将挂起;

4、锁的顺序规则(a) 按同样的顺序获得锁;b) 如果必须获得一个局部锁和一个属于内核更中心位置的锁,则应该首先获取自己的局部锁 c) 如果我们拥有信号量和自旋锁的组合,则必须首先获得信号量;在拥有自旋锁时调用down(可导致休眠)是个严重的错误的;)

== rw(read/write)spinlock: ==

加锁逻辑:

1、假设临界区内没有任何的thread,这个时候任何的读线程和写线程都可以键入

2、假设临界区内有一个读线程,这时候信赖的read线程可以任意进入,但是写线程不能进入;

3、假设临界区有一个写线程,这时候任何的读、写线程都不可以进入;

4、假设临界区内有一个或者多个读线程,写线程不可以进入临界区,但是写线程也无法阻止后续的读线程继续进去,要等到临界区所有的读线程都结束了,才可以进入,可见:==rw(read/write)spinlock更加有利于读线程;==

== seqlock(顺序锁): ==

加锁逻辑:

1、假设临界区内没有任何的thread,这个时候任何的读线程和写线程都可以键入

2、假设临界区内没有写线程的情况下,read线程可以任意进入;

3、假设临界区有一个写线程,这时候任何的读、写线程都不可以进入;

4、假设临界区内只有read线程的情况下,写线程可以理解执行,不会等待,可见:==seqlock(顺序锁)更加有利于写线程;==

读写速度 CPU >一级缓存 >二级缓存 >内存 ,因此某一个CPU0的lock修改了,其他的CPU的lock就会失效;那么其他CPU就会依次去L1 L2和主存中读取lock值,一旦其他CPU去读取了主存,就存在系统性能降低的风险;

mutex用于互斥 *** 作。

互斥体只能用于一个线程,资源只有两种状态(占用或者空闲)

1、mutex的语义相对于信号量要简单轻便一些,在锁争用激烈的测试场景下,mutex比信号量执行速度更快,可扩展

性更好,

2、另外mutex数据结构的定义比信号量小、

3、同一时刻只有一个线程可以持有mutex

4、不允许递归地加锁和解锁

5、当进程持有mutex时,进程不可以退出。

• mutex必须使用官方API来初始化。

• mutex可以睡眠,所以不允许在中断处理程序或者中断下半部中使用,例如tasklet、定时器等

==常见 *** 作:==

struct mutex mutex_1

mutex_init(&mutex_1)

mutex_lock(&mutex_1)

临界区代码;

mutex_unlock(&mutex_1)

==常见函数:==

=

linux中关于原子 *** 作

2016年08月02日

原子 *** 作:就是在执行某一 *** 作时不被打断。

linux原子 *** 作问题来源于中断、进程的抢占以及多核smp系统中程序的并发执行。

对于临界区的 *** 作可以加锁来保证原子性,对于全局变量或静态变量 *** 作则需要依赖于硬件平台的原子变量 *** 作。

因此原子 *** 作有两类:一类是各种临界区的锁,一类是 *** 作原子变量的函数。

对于arm来说,单条汇编指令都是原子的,多核smp也是,因为有总线仲裁所以cpu可以单独占用总线直到指令结束,多核系统中的原子 *** 作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子 *** 作时,其他CPU核必须停止对内存 *** 作或者不对指定的内存进行 *** 作,这样才能避免数据竞争问题。但是对于load update store这个过程可能被中断、抢占,所以arm指令集有增加了ldrex/strex这样的实现load update store的原子指令。

但是linux种对于c/c++程序(一条c编译成多条汇编),由于上述提到的原因不能保证原子性,因此linux提供了一套函数来 *** 作全局变量或静态变量。

一.整型原子 *** 作定义于#include<asm/atomic.h>分为 定义,获取,加减,测试,返回。void atomic_set(atomic_t *v,int i)    //设置原子变量v的值为iatomic_t v = ATOMIC_INIT(0)    //定义原子变量v,并初始化为0atomic_read(atomic_t* v)     //返回原子变量v的值void atomic_add(int i, atomic_t* v)     //原子变量v增加ivoid atomic_sub(int i, atomic_t* v)   void atomic_inc(atomic_t* v)    //原子变量增加1void atomic_dec(atomic_t* v)     int atomic_inc_and_test(atomic_t* v)       //先自增1,然后测试其值是否为0,若为0,则返回true,否则返回falseint atomic_dec_and_test(atomic_t* v)       int atomic_sub_and_test(int i, atomic_t* v)     //先减i,然后测试其值是否为0,若为0,则返回true,否则返回false注意:只有自加,没有加 *** 作int atomic_add_return(int i, atomic_t* v)  //v的值加i后返回新的值int atomic_sub_return(int i, atomic_t* v) int atomic_inc_return(atomic_t* v)    //v的值自增1后返回新的值int atomic_dec_return(atomic_t* v)    二.位原子 *** 作定义于#include<asm/bitops.h>分为 设置,清除,改变,测试void set_bit(int nr, volatile void* addr)       //设置地址addr的第nr位,所谓设置位,就是把位写为1void clear_bit(int nr, volatile void* addr)     //清除地址addr的第nr位,所谓清除位,就是把位写为0void change_bit(int nr, volatile void* addr)    //把地址addr的第nr位反转int test_bit(int nr, volatile void* addr)   //返回地址addr的第nr位int test_and_set_bit(int nr, volatile void* addr)    //测试并设置位若addr的第nr位非0,则返回true若addr的第nr位为0,则返回falseint test_and_clear_bit(int nr, volatile void* addr)    //测试并清除位int test_and_change_bit(int nr, volatile void* addr)    //测试并反转位上述 *** 作等同于先执行test_bit(nr,voidaddr)然后在执行xxx_bit(nr,voidaddr)

举个简单例子:为了实现设备只能被一个进程打开,从而避免竞态的出现static atomic_t scull_available = ATOMIC_INIT(1)      //init atomic在scull_open 函数和scull_close函数中:int scull_open(struct inode *inode, struct file *filp){    struct scull_dev *dev         // device information    dev = container_of(inode->i_cdev, struct scull_dev, cdev)    filp->private_data = dev         // for other methods     if(!atomic_dec_and_test(&scull_available)){        atomic_inc(&scull_available)        return -EBUSY    }    return 0        // success }int scull_release(struct inode *inode, struct file *filp){    atomic_inc(&scull_available)    return 0}

假设原子变量的底层实现是由一个汇编指令实现的,这个原子性必然有保障。但是如果原子变量的实现是由多条指令组合而成的,那么对于SMP和中断的介入会不会有什么影响呢?我在看ARM的原子变量 *** 作实现的时候,发现其是由多条汇编指令(ldrex/strex)实现的。在参考了别的书籍和资料后,发现大部分书中对这两条指令的描诉都是说他们是支持在SMP系统中实现多核共享内存的互斥访问。但在UP系统中使用,如果ldrex/strex和之间发生了中断,并在中断中也用ldrex/strex *** 作了同一个原子变量会不会有问题呢?就这个问题,我认真看了一下内核的ARM原子变量源码和ARM官方对于ldrex/strex的功能解释,总结如下:

一、ARM构架的原子变量实现结构

对于ARM构架的原子变量实现源码位于:arch/arm/include/asm/atomic.h

其主要的实现代码分为ARMv6以上(含v6)构架的实现和ARMv6版本以下的实现。

该文件的主要结构如下:

#if __LINUX_ARM_ARCH__ >= 6

......(通过ldrex/strex指令的汇编实现)

#else /* ARM_ARCH_6 */

#ifdef CONFIG_SMP

#error SMP not supported on pre-ARMv6 CPUs

#endif

......(通过关闭CPU中断的C语言实现)

#endif /* __LINUX_ARM_ARCH__ */

......

#ifndef CONFIG_GENERIC_ATOMIC64

......(通过ldrexd/strexd指令的汇编实现的64bit原子变量的访问)

#else /* !CONFIG_GENERIC_ATOMIC64 */

#include <asm-generic/atomic64.h>

#endif

#include <asm-generic/atomic-long.h>

这样的安排是依据ARM核心指令集版本的实现来做的:

(1)在ARMv6以上(含v6)构架有了多核的CPU,为了在多核之间同步数据和控制并发,ARM在内存访问上增加了独占监测(Exclusive monitors)机制(一种简单的状态机),并增加了相关的ldrex/strex指令。请先阅读以下参考资料(关键在于理解local monitor和Global monitor):

1.2.2. Exclusive monitors

4.2.12. LDREX 和 STREX

(2)对于ARMv6以前的构架不可能有多核CPU,所以对于变量的原子访问只需要关闭本CPU中断即可保证原子性。

对于(2),非常好理解。

但是(1)情况,我还是要通过源码的分析才认同这种代码,以下我仅仅分析最具有代表性的atomic_add源码,其他的API原理都一样。如果读者还不熟悉C内嵌汇编的格式,请参考《ARM GCC 内嵌汇编手册》

二、内核对于ARM构架的atomic_add源码分析

/*

* ARMv6 UP 和 SMP 安全原子 *** 作。 我们是用独占载入和

* 独占存储来保证这些 *** 作的原子性。我们可能会通过循环

* 来保证成功更新变量。

*/

static inline void atomic_add(int i, atomic_t *v)

{

unsigned long tmp

int result

__asm__ __volatile__("@ atomic_add\n"

"1: ldrex %0, [%3]\n"

" add %0, %0, %4\n"

" strex %1, %0, [%3]\n"

" teq %1, #0\n"

" bne 1b"

: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)

: "r" (&v->counter), "Ir" (i)

: "cc")

}

源码分析:

注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中 *** 作。如果出现上下文切换,切换机制会做寄存器上下文保护。

(1)ldrex %0, [%3]

意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %0, %0, %4

result = result + i

(3)strex %1, %0, [%3]

意思是将result保存到&v->counter指向的内存中,此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。

(4) teq %1, #0

测试strex是否成功(tmp == 0 ??)

(5)bne 1b

如果发现strex失败,从(1)再次执行。

通过上面的分析,可知关键在于strex的 *** 作是否成功的判断上。而这个就归功于ARM的Exclusive monitors和ldrex/strex指令的机制。以下通过可能的情况分析ldrex/strex指令机制。(请阅读时参考4.2.12. LDREX 和 STREX)

1、UP系统或SMP系统中变量为非CPU间共享访问的情况

此情况下,仅有一个CPU可能访问变量,此时仅有Local monitor需要关注。

假设CPU执行到(2)的时候,来了一个中断,并在中断里使用ldrex/strex *** 作了同一个原子变量。则情况如下图所示:

A:处理器标记一个物理地址,但访问尚未完毕

B:再次标记此物理地址访问尚未完毕(与A重复)

C:进行存储 *** 作,清除以上标记,返回0( *** 作成功)

D:不会进行存储 *** 作,并返回1( *** 作失败)

也就是说,中断例程里的 *** 作会成功,被中断的 *** 作会失败重试。

2、SMP系统中变量为CPU间共享访问的情况

此情况下,需要两个CPU间的互斥访问,此时ldrex/strex指令会同时关注Local monitor和Global monitor。

(i)两个CPU同时访问同个原子变量(ldrex/strex指令会关注Global monitor。)

A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。

B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。

C:没有标记为CPU0独占访问,不会进行存储,并返回1( *** 作失败)。

D:已被标记为CPU1独占访问,进行存储并清除独占访问标记,并返回0( *** 作成功)。

也就是说,后执行ldrex *** 作的CPU会成功。

(ii)同一个CPU因为中断,“嵌套”访问同个原子变量(ldrex/strex指令会关注Local monito)

A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。

B:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。

C:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0( *** 作成功)。

D:没有标记为CPU0独占访问,不会进行存储,并返回1( *** 作失败)。

也就是说,中断例程里的 *** 作会成功,被中断的 *** 作会失败重试。

(iii)两个CPU同时访问同个原子变量,并同时有CPU因中断“嵌套”访问改原子变量(ldrex/strex指令会同时关注Local monitor和Global monitor)

虽然对于人来说,这种情况比较BT。但是在飞速运行的CPU来说,BT的事情随时都可能发生。

A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。

B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。

C:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。

D:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0( *** 作成功)。

E:没有标记为CPU1独占访问,不会进行存储,并返回1( *** 作失败)。

F:没有标记为CPU0独占访问,不会进行存储,并返回1( *** 作失败)。

当然还有其他许多复杂的可能,也可以通过ldrex/strex指令的机制分析出来。从上面列举的分析中,我们可以看出:ldrex/strex可以保证在任何情况下(包括被中断)的访问原子性。所以内核中ARM构架中的原子 *** 作是可以信任的。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/tougao/6077372.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-03-13
下一篇 2023-03-13

发表评论

登录后才能评论

评论列表(0条)

保存