Linux——进程间通信(信号量)

前言

信号量与其他的进程间通信方式不太相同,它主要提供对进程间共享资源的访问机制,进程会根据它判定是否能够访问某些共享资源,同时进程也可以修改该标志。
除了用于访问控制外,还可以用于进程同步。

概念补充:

  1. 临界资源: 同一时刻只能一个进程(线程)访问的资源。
    例如:一块物理内存(共享内存),显示终端,打印机。

  2. 临界区:
    程序中访问临界资源的代码区域。

  3. 原子操作: 不能被中断的操作。
    例如: i++不是原子操作,它的完成需要三步操作,在任何一步可以被中断。
    ①Mov eax ptr i //将i的地址值存放到eax寄存器中
    ②Add eax 1 //寄存器+1保存
    ③Mov ptr i eax //将eax寄存器的值保存到i的地址空间中去。

  4. P,V操作 都是原子操作
    ——P操作 -1操作,临界区之前 ——获取资源
    ——V操作 +1操作,临界区之后——释放资源

信号量:

——可以抽象地理解为是十字路口的交通信号灯。
为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。
是一个特殊的计数器,来完成多进程环境下进程执行的同步控制。
信号量是一个值,

  • 当它大于0时,记录的是临界资源的个数,此时对信号量执行P操作不会阻塞,
  • 当它的值为0时,执行p操作会被阻塞,直到另一个进程对该信号量执行V操作。

进程需要同步执行的场景:
1.间接制约关系:多个进程竞争使用同一个临界资源。
2.直接制约关系:一个进程为另一个进程提供服务。

信号量就是在一个叫做互斥区的门口放一个盒子,盒子里面装着固定数量的小球,每个线程过来的时候,都从盒子里面摸走一个小球,然后去互斥区里面浪(?),浪开心了出来的时候,再把小球放回盒子里。如果一个线程走过来一摸盒子,得,一个球都没了,不拿球不让进啊,那就只能站在门口等一个线程出来放回来一个球,再进去。这样由于小球的数量是固定的,那么互斥区里面的最大线程数量就是固定的,不会出现一下进去太多线程把互斥区给挤爆了的情况。这是用信号量做并发量限制。
另外一些情况下,小球是一次性的,线程拿走一个进了门,就把小球扔掉了,这样用着用着小球就没了,不过有另外一些线程(一般叫做生产者)会时不时过来往盒子里再放几个球,这样就可以有新的线程(一般叫做消费者)进去了,放一个球进一个线程,这是信号量做同步功能。主线程是生产者,通过sem_post往盒子里放小球(信号量加一),而其他线程是消费者,通过sem_wait从盒子里拿小球(信号量减一),如果遇到盒子里一个小球都没有(信号量为0),就会开始等待信号量不为0,然后拿走一个小球(信号量减一)再继续。
本质上来说信号量就是那个盒子,以及“摸不到球就不让进”这个机制。

二元信号量:

信号量的值只可能是0,1,解决直接制约关系。
二元信号量类似于互斥锁。区别在于:信号量可以被任意线程获取并释放,即信号量的获取和释放可以由两个线程完成,而互斥量要求谁获取谁释放,其他线程释放是无效操作。 即同时只能被一个线程获取。

整型信号量

信号量取值是整数,它可以被多个线程同时获得,直到信号量的值变为0。

维护信号量状态的是Linux内核操作系统而不是用户进程。从头文件linux/include/linux/semaphore.h 中看到内核用来维护信号量状态的各个结构的定义。信号量是一个数据集合,用户可以单独使用这一集合的每个元素。要调用的第一个函数是semget,用以获得一个信号量ID。Linux2.6.26下定义的信号量结构体:

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

从以上信号量的定义中,可以看到信号量底层使用到了spin lock的锁定机制,这个spinlock主要用来确保对count成员的原子性的操作(count–)和测试(count > 0)。
在保证原子操作的前提下,先测试count是否大于0,如果是说明可以获得信号量,这种情况下需要先将count–,以确保别的进程能否获得该信号量,然后函数返回,其调用者开始进入临界区。如果没有获得信号量,当前进程利用struct semaphore 中wait_list加入等待队列,开始睡眠,在__down_interruptible()函数中,会构造一个struct semaphore_waiter类型的变量,struct semaphore_waiter定义如下:

struct semaphore_waiter {
	struct list_head list;
	struct task_struct *task;
	int up;
};

将当前进程赋给task,并利用其list成员将该变量的节点加入到以sem中的wait_list为头部的一个列表中,假设有多个进程在sem上调用down_interruptible,则sem的wait_list上形成的队列如下图:
在这里插入图片描述
将一个进程阻塞,一般的经过是先把进程放到等待队列中,接着改变进程的状态,比如设为TASK_INTERRUPTIBLE,然后调用调度函数schedule(),后者将会把当前进程从cpu的运行队列中摘下

信号量的存放
每个进程都拥有自己独立的4G虚拟地址空间,但每个进程都有3G相互独立的用户空间,代码中的任何变量都是在3G的用户空间中,因此信号量不能存放在这里,但进程的1G内核空间是所有进程共用的,因此可以将信号量存放在这个共用的内核空间中来完成进程之间的同步控制

信号量的操作:操作信号量的内核对象
1.创建或者获取信号灯的一个内核对象

  • 若第一次访问(无论哪个进程),则需要创建;
  • 若不是第一次访问,则直接获取。

作用是创建一个新信号量或取得一个已有信号量,原型为:

int semget((key_t)key , int nsems , int flag)

一个内核对象记录了很多信号量的属性信息,却只维护一个信号量,很不划算,因此该方法创建或获取的是一个信号量集合(数组)

  • key:是一个信号标识,唯一标识一个信号量集,若多个进程使用同一个信号量集,要求在调用semget方法时的key值相同
  • nsems:在创建时使用,用来执行创建的信号量集中信号量的个数
  • flag:指定操作权限,同时可以通过设置IPC_CREAT,来指明本次需要创建的信号量集
  • 返回值:失败返回-1,成功返回内核对象的ID值,用于后续的其他方法

2.改变信号量的值,原型为:

int	semop(int semid,struct sembuf *buf,int buflength)
  • semid:semget方法返回的id值。
  • buf是一个sembuf类型的数组首地址
  • buflength:数组的元素个数
  • 返回值:成功返回0,失败返回-1

struct sembuf的结构

struct sembuf{
    short sem_num;//除非使用一组信号量,否则它为0,对应信号集中的一个信号量
    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                    //一个是+1,即V(发送信号)操作。
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                    //并在进程没有释放该信号量而终止时,操作系统释放信号量

该函数用来直接控制信号量信息,它的原型为

 int semctl(int semid, int sem_num, int cmd, ...);
  • semid指定信号量集
  • sen_num 信号量集合中的下标,指定对哪个信号量进行操作,一般来说只有一个信号量,也就是为 0
  • cmd 指定操作类型,一般有如 GETVAL , IPC_RMID, SETVAL, SETVALL 等操作
  • arg用于设置或返回信号量信息
    在这里插入图片描述
    SETVAL:用来把信号量初始化为一个已知的值
    IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
    -

封装在这里插入图片描述

#include<assert.h>
#include<unistd.h>
#include<sys/sem.h>
#include<sys/types.h>
#include<sys/ipc.h>

typedef union SemUn
{
	int val;
}SemUn;

//initVal数组保存需要创建的每个信号量的初始值
//nsems:指定数组的大小,指定信号量的个数
//key就是用户标识
int SemGet(int val, int initVal[],int nsems);

int SemP(int semid,int index);

int SemV(int semid,int index);

int SemDel(int semid);
#include"sem.h"

int SemGet(int key, int initVal[],int nsems)
{
	//先获取一次
	int semid = semget((key_t)key,0,0664);

	if(semid == -1)//获取失败
	{
		//创建信号量集合
		semid = semget((key_t)key,nsems,0664 | IPC_CREAT);
		if(semid == -1)reurn -1;

		//对信号量集合中的所有信号量进行初始化
		int i = 0;
		for(;i < nsems ;i++)
		{
			SemUn arg;
			arg.val = initVal[i];

			semctl(semid,i,SETVAL,arg);
		}
	}
	return semid;
}

int SemP(int semid,int index)
{
	struct sembuf buf;

	buf.sem_num = index;
	buf.sem_op = -1;
	buf.sem__flg = SEM_UNDO;

	return semop(semid,&buf,1);
}
int SemV(int semid,int index)
{
	struct sembuf buf;

	buf.sem_num = index;
	buf.sem_op = 1;
	buf.sem_flag = SEM_UNDO;

	return semop(semid,&buf,1);

}
int SemDel(int semid)
{
	return semctl(semid,0,IPC_RMID);
}

在这里插入图片描述
1.4 信号量与线程互斥锁区别(转)
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在sem_wait的时候,就阻塞在那里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”
也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或 者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。
此外,信号量的作用是用于线程/进程同步,而互斥锁是用于线程的互斥,这是两者的根本区别,
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

两者之间的区别:
作用域
信号量: 进程间或线程间(linux仅线程间)
互斥锁: 线程间
上锁时
信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value值加一。一句话,信号量的value>=0。
互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源。如果没有锁,获得资源成功,否则进行阻塞等待资源可用。一句话,线程互斥锁的vlaue可以为负数。
应用场景
信号量: 用于线程/进程间同步,不单单用于锁住某一资源,还可能是某些流程或计算过程。
互斥锁: 用于线程互斥锁定某资源,锁住某一资源,同一时间内仅指定线程可以访问该资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值