Linux下信号量的读写操作原理 (linux 读写信号量)

信号量是一种用于多进程或多线程之间协调共享资源的机制。在Linux系统中,信号量是由内核提供的一种进程间通信的方式。它可以控制进程或线程之间的竞争条件,避免死锁和竞态等问题。在本文中,我们将深入探讨。

一、信号量的概念

信号量是一个计数器,用于协调进程或线程之间对共享资源的访问。它包含两个基本操作:PV操作和初始化操作。其中,PV操作分为P操作和V操作,它们分别用于申请和释放信号量资源,可以理解为锁住和解锁资源。

信号量的初始化操作在进程或线程的创建时进行,它用于初始化信号量的计数器。在Linux系统中,信号量的初始化操作是使用semget()函数完成的。其具体用法如下:

int semget(key_t key, int nsems, int sem);

其中,key表示信号量的标识符,nsems表示信号量数量,sem表示信号量的创建标志,如指定IPC_CREAT可以创建信号量,指定IPC_EXCL表示如果信号量已存在则返回错误。该函数返回一个信号量的标识符。

二、信号量的读写操作

在Linux系统中,信号量的读写操作是通过semop()函数实现的。semop()函数用于对一个信号量集进行操作。其具体用法如下:

int semop(int semid, struct sembuf *sops, size_t nsops);

其中,semid表示信号量的标识符,sops表示信号量操作数组,nsops表示操作的数量。信号量操作数组包含了每次对信号量执行的操作,其中每一个操作由一个sembuf结构体表示,该结构体包含三个参数:

struct sembuf {

ushort_t sem_num; /* 信号量中的信号量编号 */

short_t sem_op; /* 信号量操作:P操作(-1)或V操作(+1) */

short_t sem_; /* 信号量操作标志 */

};

信号量的读写操作是通过P操作和V操作来实现的。

1)P操作

P操作用于申请信号量资源。当多个进程或线程同时竞争一个资源时,如果其中一个申请成功,则其它申请者必须等待。在Linux系统中,P操作是通过semop()函数中的sem_op参数设置为-1来实现的。

当一个进程或线程执行P操作时,它会向内核发出一个信号量请求,如果此时信号量计数器的值大于0,则立即将信号量计数器减1,并返回操作成功;否则将进程或线程的状态设置为等待,并等待其他进程或线程释放信号量。当其他进程或线程释放信号量后,该进程或线程再次执行P操作。

在Linux系统中,如果已经有一个进程或线程执行了P操作,并且该信号量已被锁定,则其他进程或线程执行P操作时会进入睡眠状态,等待其它进程或线程释放信号量。如果该信号量已被锁定,则进程或线程会一直等待,直到信号量被释放。当信号量被释放后,内核会向所有等待该信号量的进程或线程发送信号,以唤醒它们。

2)V操作

V操作用于释放信号量资源。在Linux系统中,V操作是通过semop()函数中的sem_op参数设置为1来实现的。

当一个进程或线程执行V操作时,它会将信号量计数器加1,并检查是否有其它进程或线程在等待该信号量。如果没有,则返回操作成功;否则将找到其中一个等待该信号量的进程或线程,并将其状态设置为就绪。内核会在适当的时候调度该进程或线程,并执行它们的剩余操作。

在Linux系统中,V操作的执行过程是原子性的,即一旦信号量计数器被加1,进程或线程就会立即释放该信号量,而不会被其它进程或线程打断。因此,V操作可以保证信号量的原子性,避免了竞态等问题。

三、

本文主要介绍了。信号量是一种用于协调进程或线程之间对共享资源的访问的机制,它包含P操作和V操作两个基本操作,用于申请和释放信号量资源。在Linux系统中,信号量的读写操作是通过semop()函数实现的。当多个进程或线程同时竞争一个资源时,如果其中一个申请成功,则其它申请者必须等待。如果该信号量已被锁定,则进程或线程会一直等待,直到信号量被释放。V操作的执行过程是原子性的,即一旦信号量计数器被加1,进程或线程就会立即释放该信号量,而不会被其它进程或线程打断。信号量的读写操作可以避免死锁和竞态等问题,确保共享资源的安全性和可靠性。

相关问题拓展阅读:

linux内核同步问题

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

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

==

基础概念:

==

并发

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

竞态

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

临界资源

:多个进程访问的资源

临界区

:多个进程访问的代码段

==

并发场合:

==

1、单CPU之间进程间的并发

:时间片轮转,调度进程。 A进程访问打印机,时间片用完,OS调度B进程访问打印机。

2、单cpu上进程和中断之间并发

:CPU必须停止当前进程的执行中断;

3、多cpu之间

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

==

使用偏向:

==

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

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

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

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

3、信号量本质是==基于调度器的==,在UP和P下没有区别;进程获取不到信号量将陷入休眠,并让出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内核文件archarmincludeaatomic.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,

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

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

result = result + i

(3)strex %1, %0,

意思是将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 slock的值赋值给lockval;并且(分别在Local monitor和Global monitor中)设置独占标志。

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

newval =lockval +(1slock指向的内存中,

此时 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()和p_mb() 最终调用#define barrier()

a

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)

==常见函数:==

=

2、linux 共享内存 可不可以不加锁呢? 系统有两个进程,一个负责写入,一个负责读取

Linux

共享内存

可以不用加锁,不过需要一种机制来标记共享内存的读写状态;

也就是说要让两个进程知道:

1)负责游或写入的进程,必须知道当前共享内存是否可以写入,上一次的写入内容是否有被负责读取的进程读走;

2)负责读取的进程,必须知道当前共享内存是否需要读取,防止重复读取。

一般的这种标记机制是通过以下方式来简单实现:

1)通过读写锁来控制;

2)共享内存上设置一个地方,专门存放当前共享内存的稿手读写键磨嫌状态;

能.并且是”要”加锁.可以使用信号量加锁.

返回列表

上一篇:linux 设置用户uid

关于linux 读写信号量的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。


数据运维技术 » Linux下信号量的读写操作原理 (linux 读写信号量)