IPC主题二:信号量

时间:2021-09-13 20:37:53
一、1、信号量的概念:
     信号量是一个特殊的变量,它的本质是一种数据操作锁,它本身不具有素具交换的功能,而是通过控制其他的通信

源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的斥、

同步等功能。程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操

作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整

数的信号量被称为通用信号量。这里主要讨论二进制信号量。

2、使用信号量的原因:

     为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使

用令牌来授权,在任一时刻只能有一个执行线程访问 代码的临界区域。临界区域是指执行数据更新的代码需要独占式

地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量

是用来调协进程对共享资源的访问的。其*享内存的使用就要用到信号量

     当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用。大于0,资源可以请

求,等于0,无资源可用,进程会进入睡眠状态直到资源可用。当进程不再使用一个信号量控制的共享资源是,信号

量的值+1,对信号量的值进行增减操作均为原子操作,这是由于信号量的主要作用是维护资源的互斥或多进程的同步

访问。而在信号量的创建及初始化上,不能保证操作均为原子性。

3、信号量的工作原理:

   由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,

使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离

开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

4、Linux的信号量机制:

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进

行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

1)semget函数

它的作用是创建一个新信号量或取得一个已有信号量,原型为: int  semget (key_t key, int nsem, int oflag) ;

返回值是一个称为信号量标识符的整数,semop和semctl函数将使用它。第一个参数key是整数值(唯一非零),不

相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,

程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有

semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使

用相同的key值,key将负责协调工作。

第二个参数nsems指定需要的信号量数目,它的值几乎总是1 (若用于访问一个已存在的集合,那就可以把该参数指

定为0)

第三个参数oflags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值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,使操作系统跟踪信号,

                    //并在进程没有释放该信号量而终止时,操作系统释放信号量 

};  


semop操作中:sembuf结构的sem_flg成员可以为0、IPC_NOWAIT、SEM_UNDO 。为SEM_UNDO时,它将


使操作系统跟踪当前进程对这个信号量的修改情况如果这个进程在没有释放该信号量的情况下终止,操作系统


将自动释放该进程持有的。


sembuf结构的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;//IPC_STAT IPC_SET  使用缓冲区

    unsigned  short  *arry;//GETALL,SETALL使用数组

    struct seminfo *_buf;//IPC_INFO(Linux特有)使用缓冲区

};  

前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个

SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第

一次使用前对它进行设置。

IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

注意:该数组没有定义在任何系统头文件中,因此得用户自己声明。

5、ipc指令

ipcs -s  查看信号量

ipcrm -s  删除信号量

6、 进程使用信号量通信

     下面使用一个例子来说明进程间如何使用信号量来进行通信,这个例子是两个相同的程序同时向屏幕输出数据,

我们可以看到如何使用信号量来使两个进程协调工作,使同一时间只有一个进程可以向屏幕输出数据。注意,如果程

序是第一次被调用(为了区分,第一次调用程序时带一个要输出到屏幕中的字符作为一个参数),则需要调用

set_semvalue函数初始化信号并将message字符设置为传递给程序的参数的第一个字符,同时第一个启动的进程还

负责信号量的删除工作。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程

序时引发问题,而且信号量是一种有限的资源。

代码如下:
#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()  
{    
    struct sembuf sem_b;  
    sem_b.sem_num = 0;  
    sem_b.sem_op = -1;  
    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()  
{   
    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;  
}  

运行结果如下:
IPC主题二:信号量


7、信号量的总结
   信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信

号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行

线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。信号量本身就

是一个计数器,用于衡量临界资源的数据,它属于进程间通信的一种,但不以传送数据为目的,而以协调、同步、互

斥、多进程或多线程之间为目的。信号量它必须保证被不同的进程看到,这点可以通过system V的IPC解决;对于信

号量,我们使用的二元信号量用来保护资源的,二元信号量本身就是一种临界资源,有时一种计数器。

二、1、 SEM_UNDO

SEM_UNDO是semun中的sem_flg成员的标志位,设定该标志位主要是用来异常退出进程时进行调整,操作系统所设置的调整如下: 
由于信号量的生命周期是“随系统”,即如果我们创建了信号量集合,但是没有对其进行删除,那么信号量便会一直存在在操作系统中,直到操作系统关闭或者用户使用命令删除为止,而如果我们为信号量设定了SEM_UNDO标志,如果sem_op的值小于0(即此时调用进程占有临界区域),当该进程终止时,内核都会检验进程是否还有尚未处理的信号量调整值,如果有则进行相应的调整。 
 当操作信号量(semop)时,sem_flg可以设置SEM_UNDO标识;SEM_UNDO用于将修改的信号量值在进程正常退出(调用exit退出或main执行完)或异常退出(如段异常、除0异常、收到KILL信号等)时归还给信号量。

2、回滚

mywait用于对信号等进可回滚的P操作,可回滚的意思是,当对信号灯进行P操作的进程因为某些意外或者有意识的

退出(死亡)后,信号等将自动恢复到操作前的状态,例如某个进程因为做了P操作阻塞了,在阻塞过程中被人人为

的kill掉,该进程死后,系统讲自动的对该信号量做一次V操作以保持平衡,这样的好处是在进行互斥操作的多进程程

序不会因为某个进程只做P操作而未做V操作就退出后,发生死锁行为。

myuwait 是对信号灯进行不可回滚的P操作,使用起来要各位小心。否则容易发生死锁

需要注意的是多某个信号灯变量做可回滚的P操作,就必须使用可回滚的V操作,就是说如果对某个信号灯用mywait

操作,就必须使用mysignal做V操作,而不能使用myusignal,并且其他地方要对该信号灯变量做P,V操作,都必须使

用mywait  和  mysignal ,mywait  和 myuwait不能对同一个信号等变量混合使用,否则系统会发生不可预测的错

误。

三、我们在父进程中用fork创建子进程,然后父进程向屏幕上打印AA,子进程向屏幕上打印BB,观察在设置信号量和没有设置信号量时所出现的不同的情况,案例源代码如下:

头文件
#ifndef __SEM_H__
#define __SEM_H__
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define PATHNAME "."
#define PROJID 0x6666
union semun
{  
int val;
struct semid_ds *buf;  
unsignedshort *array;
struct seminfo *__buf;  
};
int CreatSem(int nsems); 
int InitSem(int semid);
int GetSemID();
int P(int semid,int which);
int V(int semid,int which);
int DestorySem(int semid);
#endif //__SEM_H__


源文件:
#include"MySem.h"
int InitSem(int semid)
{  
union semun un;  
un.val=1;  
int ret = semctl(semid,0,SETVAL,un);  
if(ret < 0)
{  
perror("semctl ...\n");  
return -1;  
}  
return0;
}
staticint CommSem(int nsems,int flags)
{  
key_t _k=ftok(PATHNAME,PROJID);  
if(_k < 0)
{  
perror("ftok error..\n");  
return -1;  
}  
int semid=semget(_k,nsems, flags);  
if(semid < 0)
{  
perror("semget error..\n");  
return -2;  
}  
return semid;
}
int CreatSem(int nsems)
{
return CommSem(nsems,IPC_CREAT | IPC_EXCL |0666);
}
int GetSemID()
{  
return CommSem(0,0);
}
int SemOp(int semid,int op,int num)
{  
struct sembuf buf;  
buf.sem_op=op;  
buf.sem_num=num;  
buf.sem_flg=0;  
int ret = semop(semid,&buf,1);  
if(ret < 0)
{  
perror("Semop..\n");  
return -1;  
}  
return0;
}
int P(int semid,int which)
{
return SemOp(semid,-1,which);
}
int V(int semid,int which)
{  
return SemOp(semid,1,which);
}
int DestorySem(int semid)
{
int ret = semctl(semid,0,IPC_RMID); 
if(ret < 0)
{
perror("semctl..\n");
return0;
}



测试文件:
#include "MySem.h"int main()
{
int semid=CreatSem(10);  
printf("Semid:%d\n",semid);
InitSem(semid); 
pid_t id =fork();
if(id==0)
{  
while(1)
{  
P(semid,0);  
usleep(5300); 
printf("A ");  
fflush(stdout);  
usleep(5000);
printf("A "); 
usleep(10000);  
fflush(stdout);  
V(semid,0);  
}
}  
else
{  
while(1)
{  
P(semid,0); 
usleep(10300);
printf("B ");  
fflush(stdout);  
usleep(10000);  
printf("B ");  
usleep(10000);  
fflush(stdout); 
V(semid,0);
}  
DestorySem(semid); 
return0;
}

输出结果:
IPC主题二:信号量