linux信号量通信

2017-10-23 15:02:34 yishizuofei 阅读数 4458

概念

1、临界资源:同一时刻,只允许一个或有限个进程或线程访问的资源。
例如:
(1)多个人同时用一个笔签字,此时只能有一个人用笔写字,其他人只有等他写完才可以使用这支笔。
(2)若商场试衣间可以有3个试衣间,可以同时供3个人使用,其他人必须等到其中的试衣间没人才能使用。
(3)但是像走廊,不是临界资源,可以同时由多人同时通行。
2、临界区:访问临界资源的代码段。
3、原子操作:不可被分割或中断的操作,操作一旦开始执行,就比执行结束,中途不能被任何原因打断。

例:1:

i = 0;
i++;

当只有一个线程执行这块代码时,执行结束后i的值是1。而如果有两个线程同时执行这块代码时,我们期望的结果是i的值最后变成2。但由于这两句不是原子操作。如果有一个线程还没来得及将修改之后i的值写回内存,而被打断了,这时别的线程就会从内存中读取到原有的值,这两个线程可能同时拿到的i的值为0,等两个线程执行完,i的值不是2而是1。
所谓的原子操作,就是要保证一个线程将i的值修改完之后的结果先写回内存中,然后另一个线程才能访问这个变量,这时线程拿到的值才是正确的。

例2:还有像火车买票过程就是多线程访问票数这个临界资源,必须保证一个线程修改票数的时候,其他线程不能访问,否则就会出现本来已经没票了,却多卖出了一些票的结果。

4、信号量类似于计数器,是一个特殊的变量,值可以改变,但只能取正整数值,并且对它的加1和减1操作是原子操作。如果信号量值为0,那么再进行减1操作时会阻塞。信号量的初始值,代表资源的数量。
作用:控制多个进程对临界资源的访问,使程序在同一个时刻,只有一个进程访问临界资源(进行进程间同步控制)。原理就是控制程序的执行速度。
进程间同步控制:
(1)同步执行:两个或多个进程需要协同执行,进程A的执行需要进程B提供支持
(2)异步执行:进程A和进程B互不干扰,在B给A发送数据前,A不会等待数据到达。
只有B给A发送数据后,A去处理数据。(类似信号)
例1:
多个人要去饮水机接热水,先到先得,其他人需要先等待,直到没人使用,才可以轮到自己。但是要让程序实现等这个操作就比较麻烦,所以可以通过信号量来控制。
例2:
十字路口的红绿灯其实就是起到了信号量的作用,它控制了车辆的行进速度,才能保证中间的那块区域(临界资源)是安全和持续可用的。
信号量1.png

信号量的操作

1、创建:semget函数
函数原型:int semget(key_t key, int num_sems:, int sem_flags);
作用:第一次使用时创建信号量,以后使用时获取信号量。
参数:
key:一个整型值对应内核中一个信号量对象,可以自己指定,不同信号量的key值不一样。不相关的进程可以通过它访问同一个信号量。程序对所有信号量的访问都是间接的,它先提供一个键,再由系统生成一个响应的信号标识符。
num_sems:信号量的数目。
sem_flags:设置一组标志,与open函数的标志非常相似,包括信号量的权限等。IPC_CREAT标志是创建或者使用已有的信号量。
而IPC_CREATE和IPC_EXCL结合使用可以确保创建出的是一个新的、唯一的信号量,如果该信号量已存在,它将返回一个错误。
创建时给出的权限可以是:0600。
2、初始化、删除 :semctl(cmd)
作用:对信号量值进行修改。
函数原型:int semctl(int sem_id, int sem_num, int command, ...);
sem_id:信号量id。
sem_num:信号量的下标,从0开始。
command:具体的操作命令,有SETVAL(设置初值)、IPC_RMID(移除信号量)
最后一个参数可以有也可以没有,如果有的话。它将会是一个union semun结构,包含以下几个成员:

union semun
{
    int val;
    struct semid_ds *buf;
    usigned short *array;
}

一般只使用val这个成员,来为信号量赋初值。当信号量值为0时,进程会阻塞运行。
3、具体操作
+1 V操作 semop
-1 P操作 semop
P V操作是原子操作
函数原型:int semop(int sem_id, struct sembuf *sem_ops,size_t num_sem_ops);
sem_id:信号量的id,用作标识。
sem_ops:指向一个结果体数组的指针,每个数组元素至少包含以下几个成员:

struct sembuf
{
    short sem_num;
    short sem_op;
    short sem_flg;
}

sem_num是信号量的下标,sem_op是信号量一次操作总需要改变的数值,+1是v操作,-1是p操作,sem_flg通常设置为SEM_UNDO,表示操作系统会跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量,防止其他进程一直处于等待状态。
num_sem_ops:指的是结构数组的元素个数。

4、信号量的命令操作
查看:ipcs -s
删除:ipcrm -s semid
semid指的是信号量的id,可以同过查看命令看到。

注意:
(1)内核对象和键值对:
内核对象:系统内核中,创建、控制和销毁的一个对象或变量。
键值对:key-value,通过key获取值。

(2)可以封装信号的操作

sem_get();  //获取信号量
sem_p();    //-1操作
sem_v();    //+1操作
sem_del();  //删除操作

具体实现:
sem.h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sem.h>
#include <unistd.h>

struct semun
{
    int val;
};

void sem_init();

void sem_p();

void sem_v();

void sem_destroy();

sem.c

#include "sem.h"

static int semid = 0;

void sem_init()
{
    semid = semget((key_t)1234,1,IPC_CREAT | IPC_EXCL| 0600);
    if(semid == -1)
    {
        semid = semget((key_t)1234,1,IPC_CREAT | 0600);
        if(semid == -1)
        {
            printf("semget error");
        }
    }
    else
    {
        union semun a;//a传值
        a.val = 1;
        if(semctl(semid,0,SETVAL,a)==-1)//0代表信号量下表
        {
            perror("semctl init error");
        }
    }
}

void sem_p()
{
    struct sembuf buf;
    buf.sem_num = 0;//信号量下标
    buf.sem_op = -1;//p操作
    buf.sem_flg = SEM_UNDO;
    if(semop(semid,&buf,1)==-1)
    {
        perror("p error");
    }
}

void sem_v()
{
    struct sembuf buf;
    buf.sem_num = 0;
    buf.sem_op = 1;
    buf.sem_flg = SEM_UNDO;//设置在进程出现错误时信号量值自动恢复,防止一个进程占着信号量
    if(semop(semid,&buf,1)==-1)//1表示操作数,sembuf的数量
    {
        perror("v error");
    }
}

void sem_destroy()
{
    if(semctl(semid,0,IPC_RMID)==-1)//0代表信号量集
    {
        perror("semctl destroy error");
    }
}

信号量的使用举例

例1:模拟对打印机的使用,同一时刻只能一个人打印,printf代表打印,打印a开始,打印第二个a结束打印,打印a时不能打印b,可以出现4个a或4个b的情况,不能出现交错打印的情况。

思路:a先访问打印机时,b中的p操作阻塞住,当a执行完,进行v操作,b执行p操作,此时a中的p操作阻塞,当b执行完,执行v操作,a中的p操作就不会阻塞。
代码:
a.c

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include "sem.h"

int main()
{
    sem_init();
    int i = 0;
    for(;i<10;++i)
    {
        sem_p();
        printf("A");
        fflush(stdout);

        int n = rand()%3;
        sleep(n);

        printf("A");
        fflush(stdout);

        sem_v();
        n = rand()%3;
        sleep(n);
    }
}

b.c

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include "sem.h"

int main()
{
    sem_init();
    int i = 0;
    for(;i<10;++i)
    {
        sem_p();
        printf("B");
        fflush(stdout);

        int n = rand()%3;
        sleep(n);

        printf("B");
        fflush(stdout);

        sem_v();
        n = rand()%3;//产生随机睡眠时间
        sleep(n);
    }
    sleep(10);
    sem_destroy();
}

输出结果:
进程同步之信号量.png

2013-08-24 00:12:54 ljianhui 阅读数 152880
这篇文章将讲述别一种进程间通信的机制——信号量。注意请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物。有关信号的更多内容,可以阅读我的另一篇文章:Linux进程间通信——使用信号。下面就进入信号量的讲解。

一、什么是信号量
为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

二、信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

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

三、Linux的信号量机制
Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

1、semget函数
它的作用是创建一个新信号量或取得一个已有信号量,原型为:
int semget(key_t key, int num_sems, int sem_flags);
第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。

第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。

第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

semget函数成功返回一个相应信号标识符(非零),失败返回-1.

2、semop函数
它的作用是改变信号量的值,原型为:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:
struct sembuf{
    short sem_num;//除非使用一组信号量,否则它为0
    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                    //一个是+1,即V(发送信号)操作。
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                    //并在进程没有释放该信号量而终止时,操作系统释放信号量
};
3、semctl函数
该函数用来直接控制信号量信息,它的原型为:
int semctl(int sem_id, int sem_num, int command, ...);
如果有第四个参数,它通常是一个union semum结构,定义如下:
union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
};
前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

四、进程使用信号量通信
下面使用一个例子来说明进程间如何使用信号量来进行通信,这个例子是两个相同的程序同时向屏幕输出数据,我们可以看到如何使用信号量来使两个进程协调工作,使同一时间只有一个进程可以向屏幕输出数据。注意,如果程序是第一次被调用(为了区分,第一次调用程序时带一个要输出到屏幕中的字符作为一个参数),则需要调用set_semvalue函数初始化信号并将message字符设置为传递给程序的参数的第一个字符,同时第一个启动的进程还负责信号量的删除工作。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

在main函数中调用semget来创建一个信号量,该函数将返回一个信号量标识符,保存于全局变量sem_id中,然后以后的函数就使用这个标识符来访问信号量。

源文件为seml.c,代码如下:
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>

union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};

static int sem_id = 0;

static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();

int main(int argc, char *argv[])
{
	char message = 'X';
	int i = 0;

	//创建信号量
	sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

	if(argc > 1)
	{
		//程序第一次被调用,初始化信号量
		if(!set_semvalue())
		{
			fprintf(stderr, "Failed to initialize semaphore\n");
			exit(EXIT_FAILURE);
		}
		//设置要输出到屏幕中的信息,即其参数的第一个字符
		message = argv[1][0];
		sleep(2);
	}
	for(i = 0; i < 10; ++i)
	{
		//进入临界区
		if(!semaphore_p())
			exit(EXIT_FAILURE);
		//向屏幕中输出数据
		printf("%c", message);
		//清理缓冲区,然后休眠随机时间
		fflush(stdout);
		sleep(rand() % 3);
		//离开临界区前再一次向屏幕输出数据
		printf("%c", message);
		fflush(stdout);
		//离开临界区,休眠随机时间后继续循环
		if(!semaphore_v())
			exit(EXIT_FAILURE);
		sleep(rand() % 2);
	}

	sleep(10);
	printf("\n%d - finished\n", getpid());

	if(argc > 1)
	{
		//如果程序是第一次被调用,则在退出前删除信号量
		sleep(3);
		del_semvalue();
	}
	exit(EXIT_SUCCESS);
}

static int set_semvalue()
{
	//用于初始化信号量,在使用信号量前必须这样做
	union semun sem_union;

	sem_union.val = 1;
	if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
		return 0;
	return 1;
}

static void del_semvalue()
{
	//删除信号量
	union semun sem_union;

	if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
		fprintf(stderr, "Failed to delete semaphore\n");
}

static int semaphore_p()
{
	//对信号量做减1操作,即等待P(sv)
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;//P()
	sem_b.sem_flg = SEM_UNDO;
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_p failed\n");
		return 0;
	}
	return 1;
}

static int semaphore_v()
{
	//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;//V()
	sem_b.sem_flg = SEM_UNDO;
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_v failed\n");
		return 0;
	}
	return 1;
}
运行结果如下:


注:这个程序的临界区为main函数for循环不的semaphore_p和semaphore_v函数中间的代码。

例子分析 :同时运行一个程序的两个实例,注意第一次运行时,要加上一个字符作为参数,例如本例中的字符‘O’,它用于区分是否为第一次调用,同时这个字符输出到屏幕中。因为每个程序都在其进入临界区后和离开临界区前打印一个字符,所以每个字符都应该成对出现,正如你看到的上图的输出那样。在main函数中循环中我们可以看到,每次进程要访问stdout(标准输出),即要输出字符时,每次都要检查信号量是否可用(即stdout有没有正在被其他进程使用)。所以,当一个进程A在调用函数semaphore_p进入了临界区,输出字符后,调用sleep时,另一个进程B可能想访问stdout,但是信号量的P请求操作失败,只能挂起自己的执行,当进程A调用函数semaphore_v离开了临界区,进程B马上被恢复执行。然后进程A和进程B就这样一直循环了10次。

五、对比例子——进程间的资源竞争
看了上面的例子,你可能还不是很明白,不过没关系,下面我就以另一个例子来说明一下,它实现的功能与前面的例子一样,运行方式也一样,都是两个相同的进程,同时向stdout中输出字符,只是没有使用信号量,两个进程在互相竞争stdout。它的代码非常简单,文件名为normalprint.c,代码如下:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	char message = 'X';
	int i = 0;	
	if(argc > 1)
		message = argv[1][0];
	for(i = 0; i < 10; ++i)
	{
		printf("%c", message);
		fflush(stdout);
		sleep(rand() % 3);
		printf("%c", message);
		fflush(stdout);
		sleep(rand() % 2);
	}
	sleep(10);
	printf("\n%d - finished\n", getpid());
	exit(EXIT_SUCCESS);
}
运行结果如下:


例子分析
从上面的输出结果,我们可以看到字符‘X’和‘O’并不像前面的例子那样,总是成对出现,因为当第一个进程A输出了字符后,调用sleep休眠时,另一个进程B立即输出并休眠,而进程A醒来时,再继续执行输出,同样的进程B也是如此。所以输出的字符就是不成对的出现。这两个进程在竞争stdout这一共同的资源。通过两个例子的对比,我想信号量的意义和使用应该比较清楚了。

六、信号量的总结
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。

2016-03-16 17:01:01 Xiejingfa 阅读数 2418

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50904927

前面我们在介绍共享内存(传送门: 【Linux进程间通信】 - 共享内存)这种进程间通信时方式时提到,使用共享内存通信时需要使用同步机制来控制多个进程对同一内存区的读写操作。今天我们就来讲述一种常用的多进程同步机制 – 信号量。


1、什么是信号量?

在多进程环境下,为了防止多个进程同时访问一个公共资源而出现问题,需要一种方法来协调各个进程,保证它们能够合理地使用公共资源。信号量就是这样一种机制。信号量可以用来保证两个或多个临界区代码不被并发调用。具体过程是:一个进程在执行临界区代码前,需要获取一个信号量,一旦该临界区代码执行完毕,那么该进程必须释放信号量。其它想进入该临界区代码的进程也必须等待直到获取到一个信号量。信号量分为单值信号量和多值信号量,单值信号量只能被一个进程获取,多值信号量可以被多个进程获取。

上面的介绍好像有点抽象,我们举一个停车场的例子来详细说明一下。假设有一个停车场,只有两个车位(即信号量的初始值为2,表示有两个同样的公共资源)。某个时刻,有三辆车同时来到停车场(即三个并发进程)。由于停车场只有两个空的停车位,所以保安只能让先来的前两辆车进入停车场(即获得信号量),第三辆车只能等待(未能获得信号量,一直等待)。等了一会儿后,有一辆车离开了(释放信号量),这时停车场有一个空车位,第三辆车就可以进入停车场了(获取信号量,进入临界区)。

对比上面这个例子,信号量是一个特殊的非负整数,表示某类公共资源的数量。如果一个进程占用了该资源,会将信号量的值做减1操作,相反,一个进程使用完毕该资源则对信号量做加1操作,如果该信号量的数值为0,则所有试图使用该类资源的进程都必须等待直到信号量值不为0。上面这种加1操作和减1操作都是原子操作,前者又称为P操作,后者又称为V操作。

关于信号量,一定要弄明白:信号量是一种“锁”的概念,它本身不具备数据传输的功能,而是通过对进程访问资源时进行同步控制来实现进程间通信。

2、信号量PV操作

信号量只有两种操作,P操作和V操作。具体如下:

  • P操作:如果信号量的值大于0,则做减1操作,表示进程获得该资源的使用权。否则进程进入休眠状态直到信号量的值大于0后再被唤醒。
  • V操作:表示该进程不再使用信号量控制的资源,对信号量做加1操作。如果此时有其它进程在等待该信号量,则唤醒它们。

为了保证信号量能有效工作,信号量的测试以及加1和减1操作都必须是原子性的。所以,信号量通常都是在内核中实现的。

3、信号量的相关操作

在Linux中,对信号量进行操作的主要有semget、semop、semctl三个函数,它们都被定义在<sys/sem.h>头文件中。值得注意的是,Linux在实现上可以同时对多个信号量进行控制,这些成组的信号量又称为信号量集。

在介绍这些函数之前,我们先来讲述一下信号量在内核中的表示。

内核为每个信号量集维护这样一个semid_ds结构(摘自网络):

struct semid_ds
{
    struct ipc_perm sem_perm;  /* operation permission struct */
    struct sem *sem_base;      /* ptr to first semaphore in set */
    unsigned short sem_nsems;  /* numbers of semaphores in set */
    time_t sem_otime;          /* last semop time */
    time_t sem_ctime;          /* last change time */
    ...
};

每个信号量又由一个sem结构表示(摘自网络):

struct sem
{
    unsigned short semval;   /* semaphore value, always >= 0 */
    pid_t sempid;            /* pid for last operation */
    unsigned short semncnt;  /* number of processes awaiting semval > vurrval */
    unsigned short semzcnt;  /* number of processes awaiting semval = 0 */
    ...
};

下面具体介绍一下这些函数。

3.1、创建或打开一个信号量集

使用semget函数可以创建或打开一个信号量集,函数原型如下:

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

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

如果操作成功,semget函数返回信号量集的标识符ID,否则返回-1。各个参数的含义具体如下:

(1)第一个参数key和第三个参数semflg

参数key表示所要创建或打开的信号量集对应的键值,参数semflg指明了调用函数的操作类型。这两个参数跟【Linux进程间通信】 - 共享内存一文中介绍的shmget函数一致,这里就不再赘述。

(2)、第二个参数nsems

参数nsems表示创建的信号量集中包含的信号量的个数,通常被设置为1。这个参数只有在创建一个新的信号量集时才有效。

当semget成功创建一个新的信号量集时,它所关联的semid_ds结构被初始化。

3.2、信号量集的操作

使用semop函数可以操作一个信号量集,原型如下:

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

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

如果操作成功,semop函数返回0,否则返回-1。各个参数的含义具体如下:

(1)、第一个参数semid

参数semid表示需要操作的信号量集的标识符ID,也就是通过semget函数返回的标识符ID。

(2)、第二个参数sops

参数sops用来说明需要执行的操作。其定义如下:

struct sembuf
{
    unsigned short sem_num;
    short sem_op;
    short sem_flg;
};

在sembuf结构中,sem_num表示该信号量集中的某一个信号量(即将要操作的信号量)。参数sem_op表示要执行的操作,参数sem_flg说明了函数semop的行为,取值通常为IPC_NOWAIT和SEM_UNDO。参数sem_op不同值对应的操作如下:

1、如果sem_op > 0时,表示进程对资源使用完毕,释放所占用的资源并将sem_op加到信号量的值上,此时可用资源数量增加。

2、如果sem_op = 0时,表示调用进程阻塞直到信号量相应的值为0。
此时需要分情况讨论:
如果当前信号量的值已经为0,函数立即返回。
如果当前信号量的值不为0,则由sem_flg参数决定函数动作:
a、如果sem_flg被指定为IPC_NOWAIT,则出错返回EAGAIN。
b、如果sem_flg未指定为IPC_NOWAIT,则该信号量semzcnt值加1,调用进程进入休眠状态,直到下列事情发生:
i、此信号量为0,则对semzcnt值减1,表示调用进程已经结束等待;
ii、此信号量被删除,函数出错返回EIDRM;
iii、进程捕获到一个信号并从信号处理函数返回。在这种情况下semzcnt值减1,函数出错返回EINTR。

3、如果sem_op < 0 时,表示调用进程请求|sem_op|(绝对值)数量的资源。此时需要分情况讨论:
如果相应的资源数可以满足请求,则将信号量的值减去semp_op的绝对值,函数成功返回。
如果相应的资源不能满足要求,则由sem_flg参数决定函数动作:
a、如果sem_flg被指定为IPC_NOWAIT,则函数出错返回EAGAIN。
b、如果sem_flg未被指定为IPC_NOWAIT,则该信号量semncnt值加1,调用进程进入休眠状态,直到下列事情发生:
i、当相应的资源数量可以满足要求,则将信号量的值减去semp_op的绝对值,函数成功返回。
ii、此信号量被删除,函数出错返回EIDRM;
iii、进程捕获到一个信号并从信号处理函数返回。在这种情况下semncnt值减1,函数出错返回EINTR。

(3)、第三个参数nops

参数nops表示sops数组的元素个数。

3.3、信号量集的控制

共享内存有自己的控制函数shmctl,信号量集也有自己的专用控制函数,原型如下:

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

 int semctl(int semid, int semnum, int cmd, union senun arg);

如果操作成功,semctl返回所请求的值(依据cmd参数而定),否则返回-1。各参数的含义如下:

(1)、第一个参数semid

参数semid表示需要操作的信号量集的标识符ID,也就是通过semget函数返回的标识符ID。

(2)、第二个参数semnum

参数semnum和前面介绍的sembuf结构中的sem_num字段含义一致。

(3)、第四个参数arg

这里有必要先介绍一下第四个参数,第四个参数arg是可选的,是否使用取决参数cmd。

semun结构定义如下:

union semun 
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

(4)、第三个参数cmd

cmd用来指定下面命令中的一个:

cmd取值 含义
GETALL 获取semid信号集中信号量的个数并赋值给semun.array
SETALL 用semun.array的值设置信号量集中信号量的个数
GETVAL 获取semid信号量集中semnum指定信号量的值semval
SETVAL 用semun.val的值设置信号量集中semnum指定信号量的semval
GETNCNT 获取semnum指定信号量的semncnt值
GETZCNT 获取semnum指定信号量的semzcnt值
IPC_STAT 取该信号集的semid_ds结构,并存放在semun.buf字段中
IPC_SET 按照semun.buf的值设置该信号量集中的sem_perm.uid、sem_perm.gid和sem_perm.mode字段
IPC_RMID 删除信号量集,这种删除是立即发生的
GETPID 获取semnum指定信号量的sempid值

4、示例演示

下面用一个例子来演示一下信号量如何进行同步控制。

sem_test.c代码如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>

union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};


int sem_id = -1;

struct sembuf sem_p = {0, -1, 0};
struct sembuf sem_v = {0, 1, 0};

// P操作
static int P()
{
    return semop(sem_id, &sem_p, 1);
}

// V操作
static int V()
{
    return semop(sem_id, &sem_v, 1);
}

// 打印当前时间,供调试用
static void printCurrentTime()
{
    time_t timep;
    time(&timep);
    printf("%s\n", ctime(&timep));
}

int main(int argc, char *argv[])
{
    // 打开一个信号量集,该信号量集只有一个信号量
    sem_id = semget((key_t) 1234, 1, IPC_CREAT | 666);
    if (sem_id == -1)
    {
        perror("semget error!");    
        exit(1);
    }

    // 设置信号量的初始值
    if (argc > 1)
    {
        union semun semopt;
        semopt.val = 1;
        int res = semctl(sem_id, 0, SETVAL, semopt);
        if (res == -1)
        {
            printf("semctl error!\n");
            exit(1);
        }
    }

    // 准备进入临界区
    printf("before critical section...\n");
    printCurrentTime();
    if (P() == -1)
    {
        printf("P error!\n");
        exit(1);
    }

    // 在临界区里面
    printf("in critical section...\n");
    printCurrentTime();

    sleep(20);

    // 离开临界区
    if (V() == -1)
    {
        printf("V error!\n");
        exit(1);
    }

    printf("after critical section...\n");
    printCurrentTime();
}

第一次运行程序的时候,我们需要初始化信号量的值为1,所以需要以./sem_test 1命令(参数随意,只要保证有两个以上参数即可)运行程序,我们称其为程序A。此时,输出如下:

Wed Mar 16 20:01:52 2016

in critical section...
Wed Mar 16 20:01:52 2016

这时候程序A休眠20秒。

接下来,我们打开另一个终端,输入./sem_test命令运行同一个程序,我们称其为程序B。此时输出如下:

before critical section...
Wed Mar 16 20:01:57 2016

可以看到程序B没有获得信号量,一直阻塞到程序A释放掉信号量。

程序A的完整打印信息如下:

before critical section...
Wed Mar 16 20:01:52 2016

in critical section...
Wed Mar 16 20:01:52 2016

after critical section...
Wed Mar 16 20:02:12 2016

程序B的完整打印信息如下:

before critical section...
Wed Mar 16 20:01:57 2016

in critical section...
Wed Mar 16 20:02:12 2016

after critical section...
Wed Mar 16 20:02:32 2016

参考:《Unix环境高级编程》

2018-04-19 11:44:35 dove1202ly 阅读数 1570

在Linux中支持System V 进程通信的手段有三种:消息队列(Message queue)、信号量(Semaphore)、共享内存(Shared memory)。消息队列点击打开链接、共享内存点击打开链接,今天我们主要来看信号量。。。。

在看信号量之前,我们先来看几个概念

进程互斥:由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程竞争使用这些资源,进程的这种关系为进程的互斥。也就是说一个资源每次只能被一个进程访问。

进程同步:在访问资源的时候,以某种特定顺序的方式区访问资源,也就是多个进程需要相互配合共同完成一项资源。

临界资源(互斥资源):系统中某些资源一次只允许一个进程使用。

临界区:在进程中涉及到互斥资源的程序段。

原子性:不可被中断的操作,通俗点就是一件事情只会有两种情况,要么是做了,要么就没做。

信号量是什么

(1)信号量本质上是一个具有原子性的计数器,用来描述临界资源的,不能用全局变量count加加减减替换(因为他没有原子性)

(2)信号量以保护临界资源为目的,但他本身也是个临界资源;他控制多个进程对共享资源的访问,通常描述临界资源当中,临界资源的数量,常常被当做锁来使用,防止一个进程访问另外一个进程正在使用的资源

(3)其中最简单的信号量=1,也叫做二元信号量(互斥锁),可控制单个资源,只能取0和1;信号量>=2为多远信号量

为什么要使用信号量

其实还是要拿临界资源来说,进程间通信时,可能会出现多个进程同时区访问同一临界资源,那这个时候就会出现让谁来访问的问题,为了防止出现这样的矛盾,我们需要一种方法,控制在任意时刻只能有一个执行线程来访问临界区,而信号量就是解决这种矛盾的一种机制,信号量就是用来协调进程对共享资源的访问的。

信号量的特点

(1)同消息队列一样,生命周期随内核,当进程结束的时候,信号来那个还没有被销毁;

(2)信号量并非是单个非负值,而必须定义为含有一个或多个信号量值的集合,当创建信号量的时候,需要制定集合当中信号量的数量

(3)信号量的创建和初始化不能同时进行,因为操作不具有原子性,由于不互斥,可能会导致创建后未初始化就被另一个进程获取。

信号量的P、V操作---->必须保证操作的原子性

(1)信号量:互斥---->P、V在同一进程中

                       同步---->P、V不在同一进程中

(2)信号量值含义:S>0:S表示可用资源的个数

                                 S=0:S表示无可用资源,无等待进程

                                  S<0:|S|表示等待队列中进程个数

为了更好理解信号量以及P、V操作,看下面的伪代码

信号量本质上是一个计数器
struct semaphore
{
   int count;//计数
   pointer_PCB queue;//等待队列
}

P操作

P(s)//申请资源
{
   s.count=s.count--;//申请成功,资源减1
   if(s.count<0)
   {
       //该进程状态置为等待状态
       //将该进程的PCB插入相应的等待队列s.queue末尾
   }
}

V操作

V(s)
{
   s.count=s.cout++;//释放资源
   if(s.count<=0)
   {
       //唤醒相应等待队列s.queue中等待的一个进程
       //改变其状态为就绪态
       //并将其插入就绪队列
   }
}

信号集结构

struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned short sem_nsems; /* No. of semaphores in set */
};

相关函数

//头文件
#include<sys/sem.h>
//1.ftok--->获取key值
#include<sys/types.h>
#include<sys/ipc.h>
key_t ftok(const char* pathname,int id)//pathname:路径名  id:项目id,非0整数(只有低8位有效)
//2.semget函数
int semget(key_t key,int nsems,int semflg);
//参数:key-->信号集的名字
//nsems:信号集中信号量的个数
//semflg:由九个权限构成,用法和创建文件时使用的mode模式一样
//返回值:成功返回一个非负整数,即该信号集的标识码;失败返回-1
//semctl函数
int semctl(int semid,int semnum,int cmd,...);
//参数:
//semid:mesget返回的信号集标识码
//semnum:信号集中信号量的序号
//cmd:将要采取的动作(有三个取值)
//最后一个参数根据命令不同而不同
//返回值:成功返回0;失败返回-1

第四个参数是可选的,如果使用该参数,则他的类型是semun,它是一个联合

union semun
{
   int val;
   struct semid_ds *buf;
   unsigned short *array;
   struct seminfo *_buf;
}
//4semop函数
int semop(int semid,struct sembuf* sops,unsigned nsops);
//参数:
//semid:semget函数返回值(信号量的标识码)
//sops:是指向一个结构数值的指针
//nsops:信号量的个数
//返回值:成功返回0,失败返回-1

【sembuf结构体】

sembuf结构体:
struct sembuf {
short sem_num;//信号量的编号0,1,.....nsems-1
short sem_op;//信号量一次PV操作时加减的数值(一个是“-1”,也就是P操作,等待信号量变得可用;另一个是“+1”,也就是我们的V操作,发出信号量已经变得可用)
short sem_flg;//两个取值是IPC_NOWAIT或SEM_UND
};

【信号量实例】

下面所示例子是利用信号量,实现了父子进程的互斥

如果使用了P/V操作会出现如下情况


如果未使用P/V操作呢?


【实现代码】

Makefile


comm.h

 #ifndef __COMM_H_
 #define __COMM_H_
 #include<stdio.h>
 #include<sys/types.h>
 #include<sys/ipc.h>
 #include<sys/sem.h>
 
 #define PATHNAME "."
 #define PROJ_ID 0x4444                                                                                                    
 union semun{
     int val;
     struct semid_ds *buf;
     unsigned short *array;
     struct seminfo *_buf;
 };
 int createsemset(int nums);//创建sem
 int initsem(int semid,int nums,int initval);
 int getsemset(int nums);
 int P(int semid,int who);//申请资源
 int V(int semid,int who);//释放资源
 int destroysemset(int semid);//销毁资源
 
 #endif //__COMM_H_

comm.c

   #include"comm.h"
   static int commsemset(int nums,int flags)
   {
       key_t key=ftok(PATHNAME,PROJ_ID);
       if(key<0){
           printf("ftok error\n");
           return -1;
       }
       int semid=semget(key,nums,flags);
      if(semid<0){
          printf("semget error\n");
          return -2;
      }
      return semid;
  }
  int createsemset(int nums)
  {
      return commsemset(nums,IPC_CREAT|IPC_EXCL|0666);
  }
  int getsemset(int nums)
  {                                                                                                                         
       return commsemset(nums,IPC_CREAT);
  }
  int initsem(int semid,int nums,int initval)
  {
      union semun _un;
      _un.val=initval;
      if(semctl(semid,nums,SETVAL,_un)<0){
          printf("semctl error\n");
          return -1;
      }
      return 0;
  }
  static int commPV(int semid,int who,int op)
  {
      struct sembuf _buf;
      _buf.sem_num=who;
      _buf.sem_op=op;
      _buf.sem_flg=0;
      if(semop(semid,&_buf,1)<0){
         printf("semop error\n");
          return -1;
      }
      return 0;
  }
  int P(int semid ,int who)
  {
      return commPV(semid,who,-1);
  }
  int V(int semid,int who)
  {   
     return commPV(semid,who,1);
  }
  int destroysemset(int semid)
  {
      if(semctl(semid,0,IPC_RMID)<0){
          printf("semctl error");
          return -1;
      }
 }                      

【测试代码】test.c

   #include "comm.h"
   int main()
   {
       int semid=createsemset(1);
       initsem(semid,0,1);
       pid_t id=fork();
       if(id==0){
           int ssemid=getsemset(0);
           while(1){
              //P(ssemid,0);
              printf("A");
              fflush(stdout);
              usleep(101000);
              printf("A ");
              fflush(stdout);
              usleep(100000);
              //V(ssemid,0);
          }
      }else{
          while(1)
          {
              //P(semid,0);
              printf("B");
              fflush(stdout);
              usleep(100100);
              printf("B ");
              fflush(stdout);
              usleep(120000);
              //V(semid,0);                                                                                                 
          }
          wait(NULL);
      }
      destroysemset(semid);
      return 0;
  }

【封装】

1、使用gcc将上述文件生成静态库


【注】在第四步时,链接时-l参数后不加空格制定所需链接的库(库名虽然是libmysem.a,但是这里只需要给出-lmysem);

         -L指定库路径,这里的-L.(指的是当前路径);

         其中目标文件生成后,静态库删除,程序照样可以运行


2、使用gcc生成动态库


【注】

使用ldd test命令查看test使用动态库,如果libmysemshared无法找到,直接执行test就会出现错误。

解决方法:首先使用export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH将当前目录加入LD_LIBRARY_PATH变量中。再次运行ldd test

另一种编译test.c并链接libmymysemshared.so的方式如下(该方式通过./libmymysemshared.so直接指定使用当前目录下的libmymysemshared.so文件)

----->命令:gcc -o test test.c ./libmymysemshared.so

至此,我们已经了解了在Linux中支持System V 进程通信的三种手段。






2018-08-26 19:42:23 Chiang2018 阅读数 539

信号量通信机制主要用于实现进程间的同步,避免并发访问共享资源。其过程与数据结构与消息队列很相似。
1、使用函数semget()创建信号量集合:

// from /usr/include/sys/sem.h
int semget(key_t key,int sems,int flag);

函数第一个参数为ftok()产生的键值,第二个为创建的信号量个数,第三个表示信号量集合的权限及属性。同消息队列:

//from /usr/include/bit/ipc.h
#define  IPC_CREAT  00001000  //如果key值不存在则创建
#define  IPC_EXCL   00002000  //如果key存在,则返回失败
#define  IPC_NOWAIT 00004000  //如果需要等待时,直接返回错误

2、使用函数semctl信号量集合或信号量:

//from /usr/include/sys/sem.h
int semctl(int semid,int semnum,int cmd,...);

函数第1个参数为semget返回的信号量集合id,第2个参数为操作信号量时,要操作的信号量的编号 。第3个参数为要执行的操作,如果是要操作整个信号量集合的话,其值包括:

//from /usr/include/linux/ipc.h
#define  IPC_RMID  0    // 立即删除消息队列
#define  IPC_SET   1    // 设置buf中的消息队列属性
#define  IPC_STAT  2    // 获取消息队列的属性并保存在buf中
#define  IPC_INFO  3    // 获取限制信息

如果只是操作单个信号量,其选项包括:

#define GETPID  11  //获取信号拥有者的pid
此时,第2个参数应为0,第4个参数无效,如果执行成功,将返回该进程的pid,否则返回-1#define  GETVAL 12  //返回获取某个信号量的值
此时,第2个参数为某信号量的编号

#define  SETVAL 16  //设置某信号量的值
此时,第2个参数为某信号量的编号,第4个参数为要设置的值

#define SETALL  17  //设置所有信号量的值
此时,第2个参数为0,第4个参数为欲设置的信号量值的数组首地址

linux信号量使用

阅读数 972