UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))

信号量原语(PV操作)

  • Dijkstra提出的信号量(Semaphore)概念是并发编程领域迈出的重要一步。信号量是一种特殊的变量,它只能取自然数并且只支持两种操作:等待(wait)和信号(signal)
  • 不过在Linux/Unix中,“等待”和“信号”都已经具有特殊的函数,所以对信号量的这两种操作更常用的称呼是P、V操作。这两个字母来自于荷兰语单词passeren(传递,就好像进入临界区)和vrijgeven(释放,就好像退出临界区)。假设由信号量SV,则对它的P、V操作含义如下:
    • P(SV):如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行
    • V(SV):如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1
  • 信号量的表示:
    • 信号量的取值可以是任何自然数。但最常用的、最简单的信号量是二进制信号量,它只能取0和1这两个值
    • 使用一个普通变量来模拟二进制信号量是行不通的,因为所有高级语言都没有一个原子操作可以同时完成如下两步操作:1.检测变量是否为true/false 2.如果是则再将它设置为true/false

演示案例:

  • 初始化时二进制信号量SV的值为1
  • 此时进程A执行了P(SV)操作将SV减1,则进程B若再执行P(SV)则会被挂起
  • 当A执行完离开关键代码段之后,并执行V(SV)操作将SV加1,则会唤醒进程B开始执行

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第1张图片

一、信号量的概念

  • 信号量与已经介绍过的IPC结构(管道、FIFO、消息队列)不同。它是一个计数器,用于为多个进程提供对共享数据对象的访问
  • 可以用来处理生产者与消费者之间的关系

POSIX信号量

  • POSIX信号量详情点击===>:https://blog.csdn.net/qq_41453285/article/details/90612787

二、信号量的使用

为了获得共享资源,进程需要执行下列操作:

  • ①测试控制该资源的信号量
  • ②若此信号量的值为正数:则进程可以使用该资源。在这种情况下,进程会将信号量值减1,表示它使用了一个资源单位
  • ③若信号量的值为0:则进程进入休眠状态,直至信号量值大于0,进程被唤醒,返回到①步骤

三、信号量的使用特点

  • ①当进程不再使用由一个信号量控制的共享资源时,该信号量值增1(如果此时有进程正在休眠等待此信号量,则唤醒它们)
  • ②常用的信号量形式被称为二元信号量。它控制单个资源,其初始值为1。但是,一般而言,信号量的初值可以是任意一个正值,该值表明有多少共享资源单位可供共享

四、XSI信号量的复杂性

以下3种特性造成了XSI信号量的复杂性:

  • ①信号量并非是单个非负值,而必须定义为含有一个或多个信号量值的集合。当创建信号量时,要指定集合中信号量值的数量
  • ②信号量的创建(semget)是独立于它的初始化(semctl)的。这是一个致命的缺点,因为不能原子地创建一个信号量集合,并且对该集合中的各个信号量值赋初值
  • ③即使没有进程正在使用各种形式的XSI IPC,它们仍然是存在的。有的程序在终止时并没有释放已经分配给它的信号量,所以我们不得不为这种程序担心。后面将要说明的”undo”功能就是处理这种情况的

五、信号量集合结构体(struct  semid_ds)

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 long   sem_nsems; /* No. of semaphores in set */
};
  • 内核为每个信号量集合维护者一个semid_ds结构体,我们所要操作的信号量就包含在这个信号量集合中
  • 信号量的操作大多数也是通过这个信号量集合来操作的
  • struct   ipc_perm结构体见文章:https://blog.csdn.net/qq_41453285/article/details/90552522
  • struct  semid_ds代表一个信号量集合,这个集合中包含很多的信号量
  • 不同的信号量有自己对应的ID和信号量的值(还有其他成员省略未画出)

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第2张图片

六、信号量结构体

  • 信号量集合中的每个信号量由一个无名结构表示,包含以下成员
struct{
    unsigned short  semval;   /* 信号值 */
    unsigned short  semzcnt;  /* # waiting for zero */
    unsigned short  semncnt;  /* # waiting for increase */
    pid_t           sempid;   /*最后一个操作该信号量的进程号*/
};

七、信号量集合的系统限制

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第3张图片

八、信号量集合的使用/创建(semget函数)

#include 
#include 
#include 
int semget(key_t key, int nsems, int flag);

//返回值:成功返回信号量ID;出错返回-1
  • 功能:此函数可以用来创建一个信号量集合,或者引用一个已存在的信号量集合
  • 注意:此函数是用来创建一个信号量集合(struct  semid_ds),而不是创建单个信号量

参数:

  • key参数:该信号量集合对应的key值
  • nsems参数:如果是创建新信号量集合,那么nsems代表新信号量集合中的信号量的数目。如果是引用一个现有的信号量集合,那么此参数指定为0
  • flag参数:创建/使用信号量集合时的标志

key参数与flag参数如何设置,详情见文章==>:https://blog.csdn.net/qq_41453285/article/details/90552522

如果此函数用来创建一个信号量集合:

如果semget函数用来创建一个新集合,那么内核会自动把新信号量集合的struct semid_ds结构体做以下初始化:

  • 初始化ipc_perm结构体(该结构体中的mode成员按semget函数的flag参数中的相应权限位设置)
  • sem_otime设置为0
  • sem_ctime设置为当前时间
  • sem_qnsems设置为semget函数的nsems参数的值

九、信号量集合设置函数(semctl)

#include 
#include 
#include 
int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);

//返回值:见下
  • 功能:此函数可以对信号量集合/信号量进行多种不同的操作(得到信号量的信息值、删除信号量、改变信号量值等等操作)

semid参数

  • 信号量集合ID

semnum参数:

  • 此参数用来指定该信号量集合中的某一特定信号量成员,也就是信号量对应的ID。(semid值在0~nsems-1之间,包括0和nsems-1(nsems就是创建信号量集合semget函数的参数2))

cmd参数:

  • 此参数指定下列10种命令中的一种。其中有5种命令是针对一个特定的信号量值的(特定的信号量值由semnum参数指定)

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第4张图片

arg参数:

  • 此参数是可选的,是否使用取决于所请求的命令,如果使用该参数,则其类型是union semun,它是多个命令特定参数的联合
  • 重点:如果应用程序想使用此联合体,必须自己定义。(但是在FreeBSD8.0中,此联合已经由定义)
union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO (Linux-specific) */
};

返回值:

  • cmd参数为GET_XXX类型的(除去GETALL命令,因为其将值存储在参数arg中,而不是返回):
    • 函数执行成功:函数的返回值为对应的值(见上图)
    • 函数执行失败:返回-1
  • cmd参数为其他类型的(包含GETALL命令):
    • 函数执行成功:返回0
    • 函数执行失败:返回-1

十、semop函数

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

//返回值:成功返回0;出错返回-1
  • 功能:此函数用来对参数2数组中所指定的信号量进行操作
  • semop函数具有原子性,它或者执行sops中的所有操作,或者一个也不执行

semid参数:

  • 信号量集合ID

sops参数:

  • 此参数是一个指针,指向一个由struct  sembuf结构表示的信号量操作数组
  • 数组中的每个sembuf结构体对应一个信号量ID,以及对该信号量ID进行操作的标志
struct sembuf{
    unsigned short sem_num;  /*信号量ID*/
    short          sem_op;   /* semaphore operation */
    short          sem_flg;  /* operation flags */
};

sem_flg成员如下:

  • 默认值填0
  • SEM_UNDO :进程退出后,该进程对sem进行的操作将被撤销(例如对信号量值进行加1或减1操作,则进程退出后这些操作都撤销)

nsops参数:

  • 对应于参数2的sops数组的元素个数

如何使用:

  • 对信号量集合中每个信号成员的操作由struct sembuf的sem_op成员的值决定。此值可以是负值、0、正值
  • 备注:下面提到的“undo”标志对应于struct sembuf的sem_flg成员的SEM_UNDO位

一、如果sem_op是正值

  • 作用:释放sem_num对应的信号量的资源(所以信号量的值增加)。sem_op的值增加到sem_num对应的信号量上
  • 如果指定了undo标志:则也从该进程的此信号量调整值中减去sem_op

二、如果sem_op是负值

  • 作用:获取sem_num对应的信号量的资源(所以信号量的值减少)
    • ①如果该信号量的值>=sem_op的绝对值:从信号量值中减去sem_op的绝对值。这能保证信号量的结果值>=0。(如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上)
    • ②如果该信号量的值则适用下列条件:

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第5张图片

三、如果sem_op是0

  • 作用:这表示调用进程希望“等待”到该信号量值变为0
  • 如果信号量值当前是0:则此函数立即返回
  • 如果信号量值非0:则使用下列条件:

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第6张图片

十一、exit时信号量的调整

  • 在进程终止时,它占用了经由信号量分配的资源,那么就会成为一个问题
  • 无论何时只要为信号量操作指定了SEM_UNDO标志,然后分配资源(sem_op值小于0),那么内核就会记住对于该特定信号量,分配给调用进程多少资源(sem_op的绝对值)
  • 当进程终止时,不论资源还是不自愿,内核都将检验该进程是否还有尚未处理的信号量调整值。如果有,则按调整值对相应信号量值进行处理
  • 如果用带SETVAL或SETALL命令的semctl函数设置一个信号量值,则在所有进程中,该信号量的调整值都将设置为0

十二、信号量与记录锁和互斥量的时间比较

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第7张图片

十三、演示案例

程序实现的功能:

  • shm_a程序创建信号量集合,集合中只有一个信号量,该信号量的值为1,然后阻塞等待该信号量的值变为0
  • 紧接着shm_b程序引用shm_a程序创建的信号量集合,将信号量的值变为0
  • 信号量的值变为0之后,shm_a程序阻塞返回,销毁该信号量集合

代码实现

//shm_a.c

#include 
#include 
#include 
#include 


#define FLAG     IPC_CREAT|0777
#define SEMID    100
#define FILENAME "./sem.txt"

key_t createKey(char *name,int id);

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

int main()
{
    key_t key;
    int semId;
    key=createKey(FILENAME,SEMID);

    //创建信号量集合,集合中只有1个信号量(ID为0)
    if((semId=semget(key,1,FLAG))==-1){
        printf("A semget error\n");
        exit(EXIT_FAILURE);
    }

    //设置信号量集合中ID为0的信号量的值为1
    union semun arg;
    arg.val=1;
    if(semctl(semId,0,SETVAL,arg)==-1){
        printf("A semctl error\n");
        exit(EXIT_FAILURE);
    }

    //获得当前信号量集合中ID为0的信号量值
    int semval;
    if((semval=semctl(semId,0,GETVAL,NULL))==-1){
        printf("A semctl error\n");
        exit(EXIT_FAILURE);
    }
    printf("Current semval:%d\n",semval);
    
    printf("Wait for the semval to change to 0...\n");
    //等待信号量集合中ID为0的信号量值变为0
    struct sembuf buff;
    buff.sem_num=0;
    buff.sem_op=0;
    buff.sem_flg=0;
    if(semop(semId,&buff,1)==-1){
        printf("A semop error\n");
        exit(EXIT_FAILURE);
    }

    //销毁信号量集合
    if(semctl(semId,0,IPC_RMID,NULL)==-1){
        printf("A semctl error\n");
        exit(EXIT_FAILURE);
    }

    printf("End...\n");
    exit(EXIT_SUCCESS);
}

key_t createKey(char *name,int id)
{
    key_t key;
    if((key=ftok(name,id))==-1){
        printf("A ftok error\n");
        exit(EXIT_FAILURE);
    }
    return key;
}

 

//shm_b.c

#include 
#include 
#include 
#include 
#include 

#define FLAG     0777
#define FILENAME "./sem.txt"
#define SEMID    100

key_t createKey(char* filename,int id);

int main()
{
    int semId;
    key_t key;
    key=createKey(FILENAME,SEMID);

    //引用信号量集合
    if((semId=semget(key,0,FLAG))==-1){
        printf("B semget error\n");
        exit(EXIT_FAILURE);
    }

    printf("Change semval to 0...\n");

    //将信号量集合中信号量ID为0的信号量值变为0
    struct sembuf buff;
    buff.sem_num=0;
    buff.sem_op=-1;
    buff.sem_flg=0;
    if(semop(semId,&buff,1)==-1){
        printf("B semop error\n");
        exit(EXIT_FAILURE);
    }

    printf("End...\n");
    exit(EXIT_SUCCESS);
}

key_t createKey(char* filename,int id)
{
    key_t key;
    if((key=ftok(filename,id))==-1){
        printf("B ftok error\n");
        exit(EXIT_FAILURE);
    }
    return key;
}

演示结果:

  •  运行shm_a之后,程序创建了一个信号量集合,集合中只有一个信号量,信号量的值为0。程序中打印了信号量的值并且阻塞等待信号量的值变为0

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第8张图片

  • 紧接着运行程序shm_b,该程序将引用到shm_a程序创建的信号量集合中,并且将集合中的信号量的值变为0。可以看到左侧的程序返回了

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第9张图片

十四、演示案例(验证“SEM_UNDO”参数)

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define FILENAME "."
#define PROJID   0x6666

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

//设置一个key值并返回
static key_t get_Key(const char* filename,int proj_id);
//创建或引用一个信号量集合
static int   get_semid(key_t key,int nsems,int flag);
//设置信号量ID为semnum的semval为value
static void  set_semvalue(int semid,int semnum,int value);
//得到信号量ID为semnum的semval
static int   get_semvalue(int semid,int semnum);
//释放一个信号量ID为semnum的semval(semval+1)
static void  semaphore_v(int semid,int semnum);
//销毁信号量集合
static void destory_sem(int semid);

int main()
{
    int sem_id,sem_value;
    key_t key;
    pid_t pid;
    key=get_Key(FILENAME,PROJID);
    
    if((pid=fork())<0){
        perror("fork");
        exit(EXIT_FAILURE);
    }else if(pid==0){
        printf("I am child:\n");
        
        //创建一个信号量集合
        sem_id=get_semid(key,1,IPC_CREAT|0777);
        printf("    create sem\n");
        
        //设置信号量ID为0的semval为5
        set_semvalue(sem_id,0,5);
        printf("    set semval is 5\n");
         
        //得到信号量ID为0的semval
        sem_value=get_semvalue(sem_id,0);
        printf("    current semval is %d\n",sem_value);
        
        //将信号量ID为0的semval加1
        semaphore_v(sem_id,0);
        printf("    add 1 to semval\n");

        //得到信号量ID为0的semval
        sem_value=get_semvalue(sem_id,0);
        printf("    current semval is %d\n",sem_value);

    }else{
        wait(NULL);
        printf("I am father:\n");
        
        //引用子进程的信号量集合
        sem_id=get_semid(key,1,0);
        printf("    get child sem\n");
        
        //得到信号量ID为0的semval
        sem_value=get_semvalue(sem_id,0);
        printf("    current semval is %d\n",sem_value);
        
        //释放信号量集合
        destory_sem(sem_id);
        printf("    destory sem\n");
    }
    
    exit(0);
}

static key_t get_Key(const char* filename,int proj_id)
{
    key_t key;
    if((key=ftok(filename,proj_id))==-1){
        perror("ftok");
        exit(EXIT_FAILURE);
    }
    return key;
}
static int get_semid(key_t key,int nsems,int flag)
{
    int sem_id;
    if((sem_id=semget(key,nsems,flag))==-1){
        perror("semget");
        exit(EXIT_FAILURE);
    }
    return sem_id;
}
static void  set_semvalue(int semid,int semnum,int value)
{
    union semun arg;
    arg.val=5;
    if(semctl(semid,semnum,SETVAL,arg)==-1){
        perror("semctl");
        exit(EXIT_FAILURE);
    }
}
static int   get_semvalue(int semid,int semnum)
{
    int sem_value;
    if((sem_value=semctl(semid,semnum,GETVAL))==-1){
        perror("semctl");
        exit(EXIT_FAILURE);
    }
    return sem_value;
}
static void  semaphore_v(int semid,int semnum)
{
    struct sembuf sem_temp;
    sem_temp.sem_num=semnum;
    sem_temp.sem_op=1;
    sem_temp.sem_flg=0;
    if(semop(semid,&sem_temp,1)==-1){
        perror("semop");
        exit(EXIT_FAILURE);
    }
}
static void destory_sem(int semid)
{
    if(semctl(semid,0,IPC_RMID)==-1){
        perror("semctl");
        exit(EXIT_FAILURE);
    }
}

演示结果

  • 上面的代码是:子进程使用semop函数对信号量ID为0的信号量值进行加1操作,父进程使用信号时,信号量的值是被子进程加1后的值(因为struct  sembuf结构体的sem_flg成员没有指定SEM_UNDO参数)

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第10张图片

  • 如果修改上面的代码,使用semop函数时,struct  sembuf结构体的sem_flg成员指定了SEM_UNDO参数,则会产生下面这种情况(因为如果使用SEM_UNDO,进程对该信号量的所有操作都会撤销,所以父进程得到的信号量的值是没有变的)

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第11张图片

UNIX(编程-进程间通信):08---XSI IPC之信号量(struct semid_ds、附信号量原语(PV操作))_第12张图片

你可能感兴趣的:(UNIX(编程-进程间通信))