栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

Linux学习笔记之进程间通信篇(4)

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Linux学习笔记之进程间通信篇(4)

Linux学习笔记系列文章目录

前置知识篇
1. 进程
2. 线程
进程间通信篇
1. IPC概述
2. 信号
3. 消息传递
4. 同步
5. 共享内存区

文章目录
  • Linux学习笔记系列文章目录
  • 一、前言
  • 二、前置条件
  • 三、本文参考资料
  • 四、正文部分
    • 4.1 同步概述
    • 4.2 POSIX 互斥锁
      • 4.2.1 基本概念
      • 4.2.2 互斥锁与信号量的区别
      • 4.2.3 互斥锁使用
      • 4.2.4 互斥锁与条件变量
      • 4.2.4 相关函数
        • 4.2.4.1 初始化互斥锁
        • 4.2.4.2 获取互斥锁与释放互斥锁
        • 4.2.4.3 销毁互斥锁
        • 4.2.4.4 使用
      • 4.2.5 分段锁
      • 4.2.6 优先级
        • 4.2.6.1 优先级反转
        • 4.2.6.2 优先级继承
    • 4.3 条件变量
      • 4.3.1 基本概念
      • 4.3.2 定时等待和广播
      • 4.3.3 初始化条件变量
      • 4.3.4 属性
    • 4.4 读写锁
    • 4.5 记录上锁
    • 4.6 POSIX信号量
      • 4.6.1 基本概念
      • 4.6.2 有名信号量 / 无名信号量
      • 4.6.3 有名信号量
      • 4.6.4 无名信号量
    • 4.7 system-V信号量
      • 4.7.1 工作原理
      • 4.7.2 Posix信号量 / System V信号量区别
      • 4.7.3 相关函数
        • 4.7.3.1 信号量集信息结构
        • 4.7.3.2 一个信号集的每个成员结构
        • 4.7.3.3 semget()获取函数
        • 4.7.3.4 semop() PV操作函数
        • 4.7.3.5 semctl()属性函数
      • 4.7.4 操作流程
    • 4.8 互斥锁、信号量、条件变量的区别
    • 4.9 一些奇怪的问题
  • 五、总结


一、前言

在介绍完进程间消息传递后,本节介绍一下同步,主要分为互斥锁、信号量、条件变量等。相较于传递信息,同步更多的用在多个进程对于同一个临界资源的使用。


二、前置条件

UB18 + 一点点的基础知识


三、本文参考资料

Unix卷2
百度
野火Linux教程


四、正文部分 4.1 同步概述

在多进程/多进程/线程操作系统环境下,多个进程/线程会同时运行,并且一些进程/线程之间可能存在一定的关联。
多个进程/线程可能为了完成同一个进程/线程会相互协作,这样形成进程/线程之间的同步关系。

而且在不同进程/线程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程/线程之间的互斥关系。
为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,
我们需要一种方法,它可以通过生成并使用令牌来授权, 在任一时刻只能有一个执行进程/线程访问代码的临界区域。

临界区域是指执行数据更新的代码需要独占式地执行。
而本文涉及的信号量、互斥锁等就可以提供这样的一种访问机制,让一个临界区同一时间只有一个进程/线程在访问它。

进程/线程之间的互斥与同步关系存在的根源在于临界资源。
临界资源是在同一个时刻只允许有限个(通常只有一个)进程/线程可以访问(读)或修改(写)的资源,
通常包括硬件资源(处理器、内存、存储器以及其他外围设备等)和软件资源(共享代码段,共享结构和变量等)。

 

4.2 POSIX 互斥锁 4.2.1 基本概念

当不同进程/线程去访问某个临界资源的时候,就需要进行互斥保护,这种互斥保护可以看做是一种锁机制。
在Linux系统中的锁机制是一个比较广泛的概念,而且锁的种类很多,包括互斥锁,文件锁,读写锁等等。

其实信号量也是一种锁,可以使用锁的目的是达到互斥的作用,使得共享资源在同一时间内,只有能有一个进程或者线程对它进行操作。

整个系统拥有很多临界资源,比如某些全局性的文件,硬件接口等,
但整个Linux系统可以完美运行,这无不依赖锁机制,可以说锁机制是Linux整个系统的精髓所在,
Linux内核都是围绕着同步在运转,它决定了进程/线程什么时候可以访问这个临界资源,在多进程和多线程编程中,锁起着极其重要的作用。

4.2.2 互斥锁与信号量的区别
  • 独占式处理 / 递归访问 / 死锁
    信号量也可以作为锁使用,互斥锁和信号量不同的是,它具有互斥锁所有权、递归访问等特性,常用于实现对临界资源的独占式处理,在任意时刻互斥锁的状态只有两种,开锁或闭锁。

    • 当互斥锁被线程持有时,该互斥锁处于闭锁状态,线程获得互斥锁的所有权。
    • 当该线程释放互斥锁时, 该互斥锁处于开锁状态,线程失去该互斥锁的所有权。

    也就是说,同时只有一个线程能获取互斥锁,
    特别地,持有该互斥锁的线程能够再次获得这个锁而不被阻塞,这就是互斥锁的递归访问,
    这个特性与一般的信号量有很大的不同,在信号量中,由于会不存在可用的信号量,线程递归获取信号量时会发生阻塞,最终形成死锁。

    想要避免死锁,最好遵循以下的规则:

    • 对共享资源操作前一定要获得锁。
    • 完成操作以后一定要释放锁。
    • 尽量短时间地占用锁。
    • 如果有多个锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC。
  • 适用场景
    如果想要实现同步功能(线程与线程间同步),信号量或许是更好的选择,虽然互斥锁也可以用于线程与线程间的同步,但互斥锁更多的是用于保护资源的互斥。

    在信号量中也可以用于保护临界资源,
    当线程获取信号量后才能开始使用该资源,当不需要使用时就释放信号量,如此一来其他线程也能获取到信号量从而可用使用资源。
    但信号量会导致的另一个潜在问题: 当其他线程释放这个信号量的时候,这就不能保证信号量能实现互斥操作了,即存在其他线程释放额外的信号量。

    互斥锁的使用比较单一,并且它是以锁的形式存在,在初始化的时候,互斥锁处于开锁的状态,而当被线程持有的时候则立刻转为闭锁的状态。
    互斥锁更适合于以下场景:

    • 保护临界资源。
    • 线程可能会多次获取互斥锁的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题。
  • 优先级翻转问题
    互斥锁可以降低信号量存在的优先级翻转问题带来的影响。

4.2.3 互斥锁使用

多线程环境下往往存在多个线程竞争同一临界资源的应用场景,互斥锁可被用于对临界资源的保护从而实现独占式访问。

使用互斥锁处理不同线程对临界资源的同步访问时,线程想要获得互斥锁才能访问资源,
如果一旦有线程成功获得了互斥锁, 则互斥锁立即变为闭锁状态,此时其他线程会因为获取不到互斥锁而不能访问该资源,
那么此时线程有2个选择:

  • 扭头就走,不进行等待操作;
  • 或者一直阻塞在这里等待,直到互斥锁被持有线程释放后,线程才能获取互斥锁从而得以访问该临界资源,此时互斥锁再次上锁,

如此一来就可以确保同一时刻只有一个线程正在访问这个临界资源,保证了临界资源操作的安全性。

4.2.4 互斥锁与条件变量

互斥锁用于保护代码临界区,从而保证任何时刻只有一个线程在临界区内执行。
有时候个线程获得某个互斥锁后,发现自己需要等待某个条件变为真。如果是这样,该线程就可以等待在某个条件变量上。

条件变量总是有一个互斥锁与之关联。把调用线程投入睡眠的 pthreadcond wait函数在这么做之前先给所关联的互斥锁解锁,以后某个时刻唤醒该线程前再给该互斥锁上锁。
该条件变量由另外某个线程向它发送信号,而这个发送信号的线程既可以只唤醒个线程( pthread cond signa1),也可以唤醒等待相应条件变为真的所有线程( pthreadcond broadcast)。
互斥锁和条件变量可以静态分配并静态初始化。它们也可以动态分配,那要求动态地初始化它们。
动态初始化允许我们指定进程间共享属性,从而允许在不同进程间共享某个互斥锁或条件变量,其前提是该互斥锁或条件变量必须存放在由这些进程共享的内存区中。

4.2.4 相关函数 4.2.4.1 初始化互斥锁
  • 静态初始化

    在使用互斥锁前需要初始化一个互斥锁,而在POSIX标准中支持互斥锁静态初始化和动态初始化两种方式,
    如果是静态初始化的可以通过以下代码实现(选择其中一句即可):

    pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
    pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
    

    pthread_mutex_t是互斥锁的结构体,其实就是定义一个互斥锁结构,并且将其赋值,代表不同的互斥锁,
    这3种锁的区别主要在于其他未占有互斥锁的线程在获取互斥锁时是否需要阻塞等待:

    PTHREAD_MUTEX_INITIALIZER:
    	表示默认的互斥锁,即快速互斥锁。
    	互斥锁被线程1持有时,此时互斥锁处于闭锁状态, 当线程2尝试获取互斥锁,那么线程2将会阻塞直至持有互斥锁的线程1解锁为止。
    
    PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP:
    	递归互斥锁。
    	互斥锁被线程1持有时,线程2尝试获取互斥锁, 将无法获取成功,并且阻塞等待,
    	而如果是线程1尝试再次获取互斥锁时,将获取成功,并且持有互斥锁的次数加1。
    
    PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP:
    	检错互斥锁。
    	这是快速互斥锁的非阻塞版本,它会立即返回一个错误代码(线程不会阻塞)。
    
  • 动态初始化

    互斥锁动态初始化可以调用pthread_mutex_init()函数,该函数原型如下:

    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
    
    	参数:
    		mutex:
    			初始化互斥锁结构的指针,
    		mutexattr:
    			属性参数,
    			它允许我们设置互斥锁的属性,从而属性控制着互斥锁的行为,
    			如果参数mutexattr为NULL,则使用默认的互斥锁属性,默认属性为快速互斥锁。
    
4.2.4.2 获取互斥锁与释放互斥锁

线程对互斥锁的所有权是独占的,任意时刻互斥锁只能被一个线程持有,
如果互斥锁处于开锁状态, 那么获取该互斥锁的线程将成功获得该互斥锁,并拥有互斥锁的所有权;
而如果互斥锁处于闭锁状态,则根据互斥锁的类型做对应的处理,
默认情况下是快速互斥锁, 获取该互斥锁的线程将无法获得互斥锁,线程将被阻塞,直到互斥锁被释放,当然,如果是同一个线程重复获取互斥锁,也会导致死锁结果。

获取互斥锁有2个函数,mutex参数指定了要操作的互斥锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);
4.2.4.3 销毁互斥锁

当互斥锁不再使用时,可以用它来销毁,mutex参数指定了要销毁的互斥锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
4.2.4.4 使用
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
4.2.5 分段锁
  • 问题场景:
    在多核的情况下,多个线程(任务)是真正的并发执行序列。如果多个线程同时操作某一个全局变量,就一定存在竞争的情况。
    若规定持有线程仅操作数组里的一个对应元素,则此时其余线程必须进行无意义的等待,执行效率太低

  • 解决方法:
    采用分段加锁,也就是对全局变量按照数据索引进行分割,每一段数据分配一把锁。
    至于每一段的数据长度是多少,这需要根据实际的业务场景进行调整,以达到最优的性能。

4.2.6 优先级 4.2.6.1 优先级反转

优先级反转,是指在使用信号量时,可能会出现的这样一种不合理的现象,即:

  • 高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。
  • 但其他中等优先级的任务却能抢到CPU资源。
  • 从现象上来看,好像是中优先级的任务比高优先级任务具有更高的优先权。

具体来说:

  • 当高优先级任务正等待信号量(此信号量被一个低优先级任务拥有着)的时候,
  • 一个介于两个任务优先之间的中等优先级任务开始执行
  • 这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行类似死锁的情形发生。

一个具体的例子:
假定一个进程中有三个线程Thread1(高)、Thread2(中)和Thread3(低),考虑下图的执行情况。

  1. T0时刻,Thread3运行,并获得同步资源SYNCH1;
  2. T1时刻,Thread2开始运行,由于优先级高于Thread3,Thread3被抢占(未释放同步资源SYNCH1),Thread2被调度执行;
  3. T2时刻,Thread1抢占Thread2;
  4. T3时刻,Thread1需要同步资源SYNCH1,但SYNCH1被更低优先级的Thread3所拥有,Thread1被挂起等待该资源
    而此时线程Thread2和Thread3都处于可运行状态,Thread2的优先级大于Thread3的优先级,Thread2被调度执行。
  5. 最终的结果是高优先级的Thread1迟迟无法得到调度,而中优先级的Thread2却能抢到CPU资源。

上述现象中,优先级最高的Thread1要得到调度,不仅需要等Thread3释放同步资源(这个很正常),
而且还需要等待另外一个毫不相关的中优先级线程Thread2执行完成(这个就不合理了),
会导致调度的实时性就很差了。

4.2.6.2 优先级继承

优先级继承就是为了解决优先级反转问题而提出的一种优化机制。
其大致原理是让低优先级线程在获得同步资源的时候(如果有高优先级的线程也需要使用该同步资源时),临时提升其优先级。
以使其能更快的执行并释放同步资源。

释放同步资源后再恢复其原来的优先级。

与上图相比,到了T3时刻,Thread1需要Thread3占用的同步资源SYNCH1,
操作系统检测到这种情况后,就把 Thread3的优先级提高到Thread1的优先级。
此时处于可运行状态的线程Thread2和Thread3中,Thread3的优先级大于Thread2的优先级,Thread3被调度执行。

Thread3执行到T4时刻,释放了同步资源SYNCH1,操作系统恢复了Thread3的优先级,Thread1获得了同步资源SYNCH1,重新进入可执行队列。
处于可运行状态的线程Thread1和Thread2中,Thread1的优先级大于Thread2的优先级,所以Thread1被调度执行。

通过优先级继承机制,可以有效解决优先级反转问题,使优先级最高的Thread1获得执行的时机提前。
 

4.3 条件变量 4.3.1 基本概念

互斥锁用于上锁,条件变量用于等待

pthread_cond_t

int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr)

int pthread_cond_signal(pthread_cond_t *cptr)

注:

  1. pthread_cond_wait
    该函数会原子地执行以下两个动作
    给互斥锁nready.mutex解锁
    把调用线程投入睡眠,直到另外某个线程就本条件变量调用pthread_cond_signal

    pthread_cond_wait在返回前重新给互斥锁nready.mutex上锁
  2. 每个条件变量总是有一个互斥锁与之相关联。
  3. 我们调用pthread_cond_wait等待某个条件为真时,还会指定其条件变量的地址和所关联的互斥锁地址
4.3.2 定时等待和广播

通常pthread_cond_signal只唤醒等待在相应条件变量上的一个线程。
调用pthread_cond_broadcast唤醒阻塞在相应条件变量上的所有线程。
主要用于一写多读的情况

int pthread_cond_broadcast(pthread_cond_t *cptr);
int pthread_cond_timewait(pthread_cond_t *cptr, pthread_mutex_t *mptr, const struct timespec *abstime);
struct timespec {
	
	time_t tv_sec;
	
	long tv_nsec;
}

pthread_cond_timewait允许线程就阻塞时间设置一个限制值。超时未收到信号则返回ETIMEOUT错误。

时间值为绝对时间,而非时间差。即abstime是该函数应返回时刻的系统时间(自UTC时间1970.1.1子时以来流逝的秒及纳秒)
使用绝对时间的好处:若函数过早返回(可能捕获了某个信号),那么同一函数无需修改参数即可再次使用

4.3.3 初始化条件变量

互斥锁动态初始化可以调用pthread_cond_init()函数,该函数原型如下:

int pthread_cond_init(pthread_cond_t *cptr, const pthread_condattr_t *attr);

	参数:
		cptr:
			初始化条件变量的指针,
		attr:
			属性参数,
			它允许我们设置互斥锁的属性,从而属性控制着互斥锁的行为,
			如果参数attr为NULL,则使用默认属性,默认属性为快速互斥锁。
			
int pthread_cond_destroy(pthread_cond_t *cptr);

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
4.3.4 属性

指定互斥锁或条件变量在不同进程间共享,而非只在单个进程内的不同线程间共享。

int pthread_condattr_getshared(pthread_condattr_t *attr, int *valptr)
int pthread_condattr_setshared(pthread_condattr_t *attr, int value)
	value可为PTHREAD_PROCESS_PRIVATE或PTHREAD_PROCESS_SHARED(进程间共享属性)

 

4.4 读写锁

todo
 

4.5 记录上锁

todo
 

4.6 POSIX信号量 4.6.1 基本概念

信号量(Semaphore)是一种实现进程/线程间通信的机制,可以实现进程/线程之间同步或临界资源的互斥访问,常用于协助一组相互竞争的进程/线程来访问临界资源。

信号量本质上是一个计数器,用于协调多进程间对共享数据对象的读取,
它不以传送数据为主要目的, 它主要是用来保护共享资源(信号量也属于临界资源),使得该临界资源在一个时刻只有一个进程独享。

全局变量并不能在进程间共同使用,因为进程间是相互独立的,而且也无法保证引用计数的原子操作,因此使用系统提供的信号量即可。

抽象的来讲,信号量中存在一个非负整数,所有获取它的进程/线程都会将该整数减一(获取它当然是为了使用资源),
当该整数值为零时,所有试图获取它的进程/线程都将处于阻塞状态。

通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。
其值的含义分两种情况:

  • 0:表示没有可用的信号量,进程/线程进入睡眠状态,直至信号量值大于 0。
  • 正值:表示有一个或多个可用的信号量,进程/线程可以使用该资源。进程/线程将信号量值减1, 表示它使用了一个资源单位

对信号量的操作可以分为两个:

  • P 操作:
    如果有可用的资源(信号量值大于0),则占用一个资源(给信号量值减去一,进入临界区代码);
    如果没有可用的资源(信号量值等于0),则被阻塞到,直到系统将资源分配给该进程/线程(进入等待队列, 一直等到资源轮到该进程/线程)。
    这就像你要把车开进停车场之前,先要向保安申请一张停车卡一样, P操作就是申请资源,
    如果申请成功,资源数(空闲的停车位)将会减少一个,如果申请失败,要不在门口等,要不就走人。

  • V 操作:
    如果在该信号量的等待队列中有进程/线程在等待资源,则唤醒一个阻塞的进程/线程。
    如果没有进程/线程等待它, 则释放一个资源(给信号量值加一),就跟你从停车场出去的时候一样,空闲的停车位就会增加一个。

举个例子,就是两个进程/线程共享信号量sem,sem可用信号量的数值为1,一旦其中一个进程/线程执行了P(sem)操作, 它将得到信号量,并可以进入临界区,使sem减1。
而第二个进程/线程将被阻止进入临界区,因为当它试图执行P(sem)操作时, sem为0,
它会被挂起以等待第一个进程/线程离开临界区域并执行V(sem)操作释放了信号量,这时第二个进程/线程就可以恢复执行。

4.6.2 有名信号量 / 无名信号量

在POSIX标准中,信号量分两种,一种是无名信号量(基于内存),一种是有名信号量。
单个进程中使用POSIX 信号量时,无名信号量更简单,多个进程间使用 POSIX信号量时,有名信号量更简单。
有名信号量总是能够在不同进程间,共享基于内存的信号量则必须在创建时指定成是否在进程间共享。
这两类信号量的持续性也有差别:
有名信号量至少有随内核的持续性(要求创建一个文件),基于内存的信号量则具有随进程的持续性(直接保存在内存)。

4.6.3 有名信号量

如果要在Linux中使用信号量同步,需要包含头文件 semaphore.h 。

有名信号量其实是一个文件,它的名字由类似 “sem.[信号量名字]” 这样的字符串组成,注意看文件名前面有 “sem.” , 它是一个特殊的信号量文件,
在创建成功之后,系统会将其放置在 /dev/shm 路径下,

不同的进程间只要约定好一个相同的信号量文件名字,就可以访问到对应的有名信号量, 并且借助信号量来进行同步或者互斥操作,
需要注意的是,有名信号量是一个文件,在进程退出之后它们并不会自动消失, 而需要手工删除并释放资源。

  • 文件路径
    虽然Posix信号量是由可能与文件系统中的路径对应的名字来标识的,但是并不要求它们真正存放在文件系内的某个文件中。
    举例来说,嵌入式实时系统可能使用这样的名字来标识信号量,但是真正的信号量值却存放在内核中的某个地方。
    然而,如果信号量的实现用到了映射文件,那么信号量的真正值确实出现在某个文件中,而该文件是映射到所有让信号量打开着的进程的地址空间的。

当终止只有信号量锁的程序时,该信号量值并不改变,即当持有某个信号量锁的进程没有释放它就终止时,内核并不给该信号量解锁

  • 相关函数
    打开/创建一个有名信号量

    sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
    	
    	参数:
    		name:打开或者创建信号量的名字。
    		oflag:当指定的文件不存在时,可以指定 O_CREATE 或者 O_EXEL进行创建操作,如果指定为0,后两个参数可省略,否则后面两个参数需要带上。
    		mode:数字表示的文件读写权限,如果信号量已经存在,本参数会被忽略。
    		value:信号量初始的值,这这个参数只有在新创建的时候才需要设置,如果信号量已经存在,本参数会被忽略
    	返回值:
    		返回值是一个sem_t类型的指针,它指向已经创建/打开的信号量,后续的函数都通过改信号量指针去访问对应的信号量。
    

    等待(获取)信号量:

    int sem_wait(sem_t *sem);
    
    	如果信号量的值大于0,将信号量的值减1,立即返回。
    	如果信号量的值为0, 则进程/线程阻塞。相当于P操作。
    	成功返回0,失败返回-1。
    

    等待信号量:如果指定信号量的计数器为0,那么直接返回EAGAIN错误,而不是阻塞等待。

    int sem_trywait(sem_t *sem);
    

    释放信号量:让信号量的值加1,相当于V操作。成功返回0,失败返回-1。

    int sem_post(sem_t *sem);
    

    用于关闭一个信号量:

    • 这表示当前进程/线程取消对信号量的使用,它的 作用仅在当前进程/线程, 其他进程/线程依然可以使用该信号量,
      同时当进程结束的时候,无论是正常退出还是信号中断退出的进程, 内核都会主动调用该函数去关闭进程使用的信号量,
      即使从此以后都没有其他进程/线程在使用这个信号量了,内核也会维持这个信号量(随内核持续)。
    int sem_close(sem_t *sem);
    

    主动删除一个信号量,直接删除指定名字的信号量文件。

    int sem_unlink(const char *name);
    
4.6.4 无名信号量

无名信号量的操作与有名信号量差不多,但它不使用文件系统标识,直接存在程序运行的内存中,不同进程之间不能访问,不能用于不同进程之间相互访问。
同样的一个父进程初始化一个信号量, 然后 fork其副本得到的是该信号量的副本,这两个信号量之间并不存在关系。

真正的持续性取决于存放信号量的内存区的类型,即内存有效,信号量存在

fork出来的子进程通常不共享父进程的内存空间,子进程是在父进程内存空间的副本上启动的

相关函数:

  • 初始化信号量:

    int sem_init(sem_t *sem, int pshared, unsigned int value);
    		
    		参数:
    			sem:
    				初始化的信号量,不要对已初始化的信号量sem再做sem_init操作,会发生不可预知的问题。
    			pshared:
    				此信号量是在进程间共享还是线程间共享,
    				由于目前Linux 还没有实现进程间共享无名信号量,所以这个值只能够取0,表示这个信号量是当前进程的局部信号量。
    			value:
    				信号量的初始值。
    				
    		返回值:
    			成功返回0,失败返回-1。实际作用的返回值是sem入参
    

    注:
    由于没有存在O_CREATE标志,故需要小心只调用一次,重复调用时结果未定义
    sem_open返回一个指向某个sem_t变量的指针,该变量由sem_open函数分配并初始化
    sem_init的sem参数是一个指向某个sem_t变量的指针,该变量由调用者分配,然后由调用者分配,然后由sem_init函数初始化

  • 销毁信号量
    只有用sem_init初始化的信号量才能用sem_destroy()函数销毁:

    int sem_destroy(sem_t *sem);
    
  • 其余同理:

    int sem_wait(sem_t *sem);
    int sem_trywait(sem_t *sem);
    int sem_post(sem_t *sem);
    

 

4.7 system-V信号量 4.7.1 工作原理

由内核维护

由于信号量只能进行两种操作:等待和发送信号,即P操作和V操作,锁行为就是P操作,解锁就是V操作,可以直接理解为P操作是申请资源,V操作是释放资源。

PV操作是计算机操作系统需要提供的基本功能之一,它们的行为是这样的:

  • P 操作:如果有可用的资源(信号量值大于0),则占用一个资源(给信号量值减去一,进入临界区代码);
    如果没有可用的资源(信号量值等于 0),则阻塞,直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
    这就像你要把车开进停车场之前,先要向保安申请一张停车卡一样,P 操作就是申请资源,
    如果申请成功, 资源数(空闲的停车位)将会减少一个,如果申请失败,要不在门口等,要不就走人。

  • V 操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞的进程。
    如果没有进程等待它, 则释放一个资源(给信号量值加一),就跟你从停车场出去的时候一样,空闲的停车位就会增加一个。

举个例子,就是两个进程共享信号量sem,sem可用信号量的数值为1(资源数为1),一旦其中一个进程执行了P操作,它将得到信号量, 并可以进入临界区,使sem减1。
而第二个进程将被阻止进入临界区,因为当它试图执行P操作时,sem为0,它会被挂起以等待第一个进程离开临界区域并执行V操作释放了信号量,这时第二个进程就可以恢复执行。

在信号量进行PV操作时都为原子操作(因为它需要保护临界资源)。

注:原子操作:单指令的操作称为原子的,单条指令的执行是不会被打断的

简单来说就是内核可以对这个信号量(计数器)做加减操作,并且操作时遵守一些基本操作原则,
即:对计数器做加操作立即返回,

做减操作要检查计数器当前值是否可减?(这个计数器的值要大于1), 如果是则进行减操作;否则将进程将阻塞等待,直到系统中有进程对该信号量进行P操作。

4.7.2 Posix信号量 / System V信号量区别
  1. System V信号量由一组值构成。
    当指定应用到某个信号量集的一组信号量操作时,要么所有操作都执行,要么一个操作都不执行。
  2. 可应用到一个信号量集的每个成员的操作有三种:测试其值是否为0、往其值加一个整数以及从其值中减掉一个整数(假设结果值仍然非负)。
    Posix信号量所允许的操作只是将其值加1或减1(假设结果值仍然非负)
  3. 创建一个 System V信号量集需要技巧,因为创建该集合并随后初始化其各个值需要两个操作,从而可能导致竞争状态
  4. System V信号量提供“复旧”特性,该特性保证在进程终止时逆转某个信号量操作(SEM_UNDO(还原)标志)
4.7.3 相关函数 4.7.3.1 信号量集信息结构

struct semid_ds {
	struct ipc_perm sem_perm;
	
	struct sem     *sem_base;
	
	ushort          sem_nsems;
	time_t          sem_otime;
	time_t          sem_ctime;
};
4.7.3.2 一个信号集的每个成员结构
struct sem {
	
	ushort_t       semval;
	
	short             sempid;
	
	ushort_t       semncnt;
	
	ushort_t       semzcnt;
}
4.7.3.3 semget()获取函数

semget()函数的功能是创建或者获取一个已经创建的信号量(不执行初始化)。
函数原型如下:

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

	参数:
		key:
			与消息队列一样的是,参数key用来标识系统内的信号量,如果指定的key已经存在,则意味着打开这个信号量,这时nsems参数指定为0,semflg参数也指定为0。
			特别地,可以使用IPC_PRIVATE创建一个没有key的信号量。
		nsems:
			本参数用于在创建信号量的时候,表示可用的信号量数目,创建完则不可改变。若只是访问已存在,则设为0
			可用信号量为1,但不影响获取或释放多个
		semflg:
			semflg参数用来指定标志位,与消息队列中的类似。
			主要有IPC_CREAT,IPC_EXCL和权限mode,
			其中使用IPC_CREAT标志创建新的信号量,即使该信号量已经存在(具有同一个键值的信号量已在系统中存在),也不会出错。
			如果同时使用IPC_EXCL标志可以创建一个新的唯一的信号量,此时如果该信号量已经存在, 该函数会返回出错。
	
	返回值:
		成功则返回对应的信号量标识符semid
		失败则返回-1。

注:

  • 当创建一个新信号量时
    sem_otime设为0,sem_ctime设为当前时间
    uid和cuid设为调用进程的有效用户id,gid和cgid设为有效组id
    oflag参数中的读写权限为存入sem_perm.mode
    sem_nsems设为nsems参数值
    与该集合中每个信号量关联的各个sem结构并不初始化。这些结构是在以SET_VAL或SETALL命令调用semctl时初始化的

  • 创建信号量时,还受到以下系统信息的影响:
    SEMMNI:系统中信号量总数的最大值。
    SEMMSL:每个信号量中信号量元素个数的最大值。
    SEMMNS:系统中所有信号量中的信号量元素总数的最大值。

    在Linux系统中,以上信息可通过命令 ipcs -l 查看

4.7.3.4 semop() PV操作函数
int semop(int semid, struct sembuf *sops, size_t nsops);

	参数:
		semid:
			System V信号量的标识符,用来标识一个信号量。
		sops:
			是指向一个struct sembuf结构体数组的指针,该数组是一个信号量操作数组。
			原型如下:
				struct sembuf
				{
					
					unsigned short int sem_num;   
					
					short int sem_op;         
					   
					
					
					
					
					
					short int sem_flg;           
				};
		nsops:表示上面sops数组的数量,如只有一个sops数组,nsops就设置为1

注:

  • sem_op可能是一个数组,存放多种信号量的操作,故选用sem_num进行指定

  • 对信号量的操作,>0, =0, <0

    • sem_op 等于 0 (相较于Posix信号量新增部分),表示进程要阻塞等待,直至信号量当前值semval 变为 0
    • sem_op 大于 0,表示进程对资源使用完毕,交回该资源,即对该信号量执行V操作
      交回的资源数由sem_op决定,系统会把sem_op的值加到该信号量的信号量当前值semval上
      特别地,如果sem_flag指定了SEM_UNDO(还原)标志,则从该进程的此 信号量调整值semadj(进程终止仿佛从未运行过)中减去sem_op
    • sem_op 小于 0,表示进程希望使用资源,对该信号量执行P操作
      当信号量当前值semval 大于或者等于 -sem_op时,semval减掉sem_op的绝对值, 为该进程分配对应数目的资源
      特别地,如果指定SEM_UNDO,则sem_op的绝对值也加到该进程的此信号量调整值上
      当semval 小于 -sem_op时,相应信号量的等待进程数量就加1,调用进程被阻塞
      直到semval 大于或者等于 -sem_op 时,调用进程被唤醒,执行相应的P操作
  • 信号量调整值

    • 其实就是指定信号量针对某个特定进程的调整值。
      只有sembuf结构的sem_flag指定为SEM_UNDO后,信号量调整值才会随着sem_op而更新。

    • 讲简单一点:对某个进程,在指定SEM_UNDO后,
      对信号量的当前值的修改都会反应到信号量调整值上,
      当该进程终止的时候,内核会根据信号量调整值重新恢复信号量之前的值,
      SEM_UNDO操作可以防止进程退出时没有释放信号量导致的死锁。

  • 信号量操作
    Posix信号量只允许-1和+1两个操作
    System V信号量允许信号量的值变化不光是1,且允许等待信号量的值变为0

4.7.3.5 semctl()属性函数

semctl函数主要是对信号量集的一系列控制操作,根据操作命令cmd的不同,执行不同的操作,第四个参数是可选的。原型如下:

int semctl(int semid, int semnum, int cmd, ...);

	参数:
		semid:
			System V信号量的标识符;
		semnum:
			表示信号量集中的第semnum个信号量。它的取值范围: 0 ~ nsems-1 。
		cmd:
			操作命令,主要有以下命令:
				IPC_STAT:获取此信号量集合的semid_ds结构,存放在第四个参数的buf中。
				IPC_SET:通过第四个参数的buf来设定信号量集相关联的semid_ds中信号量集合权限为sem_perm中的uid,gid,mode。
				IPC_RMID:从系统中删除该信号量集合。
				GETVAL:返回第semnum个信号量的值。
				SETVAL:设置第semnum个信号量的值,该值由第四个参数中的val指定。
				GETPID:返回第semnum个信号量的sempid,最后一个操作的pid。
				GETNCNT:返回第semnum个信号量的semncnt。等待semval变为大于当前值的线程数。
				GETZCNT:返回第semnum个信号量的semzcnt。等待semval变为0的线程数。
				GETALL:去信号量集合中所有信号量的值,将结果存放到的array所指向的数组。
				SETALL:按arg.array所指向的数组中的值,设置集合中所有信号量的值。
				
		第四个参数是可选的:
			如果使用该参数,该参数的类型为 union semun,它是多个特定命令的联合体,具体如下:
				union semun {
					
					int val;    
					
					
					struct semid_ds *buf;    
					
					
					unsigned short  *array;  
					
					
					struct seminfo  *__buf;  
				};
4.7.4 操作流程

semget创建信号量 -->semctl(sem_id, 0, SETVAL, sem_union)初始化资源数 --> semop执行PV操作 --> semctl删除信号量
 

4.8 互斥锁、信号量、条件变量的区别
  • 互斥锁必须总是由锁住它的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行
  • 任何线程都可以挂出一个信号(如由0增1),即使当时没有线程在等待该信号量值变为正数也没有关系
  • 互斥锁要么被锁住,要么被解开(二值状态,类似于二值信号量)
  • 既然信号量有一个与之关联的状态(计数值),那么信号量挂出操作总是被记住。
  • 然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号量将丢失。
  • 互斥锁是为上锁优化的,条件变量是为等待优化的,信号量既可以用于上锁,也可以用于等待,因而可能导致更多的开销和更高的复杂性
     
4.9 一些奇怪的问题
  • 在获得锁的情况下重新初始化这把锁,会有什么现象?
    通过实验可以得到在获得锁的情况下重新初始化该锁,能够正常重新获得锁并释放锁,但下一线程就不能再次获得该锁了

    1. 刚开始所有线程都在等同一个锁,此时每个线程内存放的锁都是指向lock1内存
    2. 当第一个进程拿到指向lock1的锁后,重新初始化锁的时候,这时mutex变量已经指向lock2内存了,并且此时释放掉的锁也是lock2内存的,
    3. 最终指向lock1的锁还是有第一个线程所持有,但其余两个线程所等待的锁还是指向lock1的锁,
    4. 最终导致挂起
  • 多次解锁会有问题吗?
    不会

 


五、总结
  • Posix互斥锁
    持有该互斥锁的线程能够再次获得这个锁而不被阻塞,这就是互斥锁的递归访问
    互斥锁可以降低信号量存在的优先级翻转问题带来的影响。

  • 条件变量
    条件变量主要用于唤醒在等待某个互斥锁的线程
    有时候一个线程获得某个互斥锁后,发现自己需要等待某个条件变为真。如果是这样,该线程就可以等待在某个条件变量上
    条件变量总是有一个互斥锁与之关联。把调用线程投入睡眠的 pthreadcond wait函数在这么做之前先给所关联的互斥锁解锁,以后某个时刻唤醒该线程前再给该互斥锁上锁。

  • 读写锁

  • 记录上锁

  • 信号量
    信号量本质是一个计数器,主要用于保护共享资源,防止多进程对一个资源同时进行读写
    P操作为上锁也就是申请资源的行为,V操作为解锁也就是释放资源的行为,PV操作在执行时均视为原子操作,不会被打断

    • Posix信号量
      分为无名信号量(基于内存)与有名信号量
      有名信号量至少有随内核的持续性(要求创建一个文件),基于内存的信号量则具有随进程的持续性(直接保存在内存)。

      • 有名信号量
        当持有某个信号量锁的进程没有释放它就终止时,内核并不给该信号量解锁
        当进程结束的时候,无论是正常退出还是信号中断退出的进程,内核都会主动调用sem_close函数去关闭进程使用的信号量,该函数仅作用于当前进程或线程

      • 无名信号量
        fork其副本得到的是该信号量的副本,这两个信号量之间并不存在关系
        内存有效,信号量存在

    • System-V信号量
      System V信号量由一组值构成。当指定应用到某个信号量集的一组信号量操作时,要么所有操作都执行,要么一个操作都不执行。
      Posix信号量允许的操作只是将其值加1或减1(假设结果值仍然非负)
      System V信号量则支持查询其值是否为0
      创建一个 System V信号量集需要技巧(semget只创建,不初始化),因为创建该集合并随后初始化其各个值需要两个操作,从而可能导致竞争状态
      System V信号量提供“复旧”特性,该特性保证在进程终止时逆转某个信号量操作(SEM_UNDO(还原)标志)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/963222.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号