Linux--进程间通信之信号量(system V版本的信号量)

引入信号量之前,我们先要了解四个基本概念,什么是临界资源与临界区,什么是同步与互斥。

临界资源与临界区的概念

1.临界资源:对于某些共享资源,一次仅允许一个进程访问,这个资源就称为临界资源。
2.临界区:访问临界资源的那些代码称为临界区。

同步与互斥

1.互斥:对于某种资源,如果有一个进程正在访问该资源,则其它的进程必须等待,当那个进程访问完成后其它进程才能访问。
2.同步:某些进程的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。
最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

信号量

1.本质:信号量本质上是一个表示临界资源数目的一个计数器。
2.底层:一个计数器加上一个等待队列。

struct semaphore
 {
       int value;    //计数器
       pointer_PCB queue;   //等待队列
}

3.信号量里有一个特殊的信号量:二元信号量
(1)二元信号量的值非0即1。
(2)二元信号量的本质就是一把互斥锁。
4.在进程间通信里,信号量本身并不具备传输数据的能力,而是通过控制其它的通信资源来实现进程间通信的同步与互斥。

信号量值的含义

若用S表示信号量
(1)S > 0 ,表示剩余临界资源的数目
(2)S = 0,表示没有可用资源,也没有等待进程
(3)S < 0,|S|表示等待进程的数目

注意:这里的S表示的信号量是通用的信号量,但是对于system V版本的信号量来说,信号量没有负值,当信号量的值等于0的时候,需要申请该资源的进程挂起等待。

信号量常见的操作

1.信号量常见的操作有P操作和V操作
(1)P操作
对该信号量的值减1,以表明对应申请得到资源。
(2)V操作
对该信号量的值加1,以表明该用户归还申请得到的资源。
2.因为信号量是用来实现同步与互斥的,所以对信号量的操作也必须是原子的。
3.P,V操作具有原子性,因为信号量能被不同进程看到,信号量本身也是一种临界资源。

信号量集相关的API

  • 因为对于system V版本的信号量来说,创建信号量是以信号量集的方式创建的

1.创建信号量集
(1)通过semget函数

       #include <sys/sem.h>
       int semget(key_t key, int nsems, int semflg);

(2)参数解析:
key:通过ftok函数产生一个唯一标识信号量集的标识符
nsems:由于该函数是创建一个信号量集,一个集合必须知道元素的个数,这里的nsems指的就是创建的信号量集中信号量的个数。
semflg:与消息队列和共享内存里的用法一致,当为IPC_CREAT表示不存在就创建存在就打开,当同时使用IPC_CREAT和IPC_EXCL表示不存在就创建,存在就失败。
(3)使用示例:

//创建包含num个信号量的信号量集,返回值为信号量集唯一标识semid
int  SemCreate(int num)
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0)
    {   
        perror("ftok");
        return -1;
    }   

    int semid = semget(key,num,fIPC_CREAT | IPC_EXCL | 0666);
    if(semid < 0)
    {   
        perror("semget");
        return -1;
    }   
    return semid;
}

2.删除信号量集
(1)通过semctl函数

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

(2)参数解析:
semid:信号量集的唯一标识,表示对哪个信号量集执行相关操作
semnum:表示信号量集中某个信号量的下标(序号)
cmd:执行的相关命令,当cmd为IPC_RMID时,第二个参数无效(因为对整个信号量集都删除,对哪个信号量标号也就不关心了),表明删除信号量集
(3)使用示例:

int DestroySem(int semid)
{
    //因为删除信号量集,所以第二隔参数任意
    int ret = semctl(semid,0,IPC_RMID);
}

3.拿到信号量集
(1)通过semget函数

#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

(2)将semflag参数设置为IPC_CREATE就表示拿到该信号量集
4.初始化信号量
(1)通过函数semctl

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

(2)将参数cmd设置为SETVAL就表示对信号量集semid的下标序号为semnum的信号量进行初始化;并且此时后面的变长参数就起作用了,我们必须还要填写后面的参数。并且需要自己定义如下的联合体:

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) */
 };

(3)使用示例:

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

//表示对信号量集semid的序号为index的信号量进行初始化,初始化值为value
int InitSem(int semid,int index,int value)
{
    union semun sem;
    sem.val = value;  //初始值
    int ret = semctl(semid,index,SETVAL,sem);
    if(ret < 0)
    {                                                                                                                                   
        perror("semctl");
        return -1;
    }   
    return 0;
}

5.获得某个信号量的值
(1)通过semctl函数
(2)只用将参数cmd位置设置为GETVAL
(3)使用示例:

//获得信号量集semid的序号为index的信号量的值,并且把值的结果保存在value里面
void GetVal(int semid,int index,int* value)
{
    union semun sem;                                                                                                                    
    *value= semctl(semid,index,GETVAL,sem);
}

6.P操作
(1)对信号量值进行减1;
(2)通过semop函数

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

(3)semop函数相关参数解析:
semid:信号量集的唯一标识
sops:结构体指针,指向一个sembuf的结构体

//sembuf结构体包含下面内容:

unsigned short sem_num;  /* semaphore number */
short          sem_op;   /* semaphore operation */
short          sem_flg;  /* operation flags */

(4)当执行P操作时,sem_num指定信号量的编号,sem_op指明对信号量的操作,这里是-1,sem_flg为0;
(5)编写的P操作代码:

//index为信号量的编号
int P(int semid, int index)
{
    struct sembuf sem;
    sem.sem_num = index;
    sem.sem_op = -1;
    sem.sem_flg = 0;  //这个也必须填写,否则会出现错误
    int ret = semop(semid,&sem,1);  //1表示对1个信号量进行操作
    if(ret < 0 )
    {
        perror("P");
        return -1;
    }                                                                                                                                   
    return 0;
}

7.V操作
(1)对信号量的值进行加1;
(2)通过semop函数,与P操作类似,只是将sops指向结构体sembuf里面的sem_op设置为1,就表示对信号量进行加1操作。
(3)V操作代码:

int V(int semid,int index)
{
    struct sembuf sem;
    sem.sem_num = index;
    sem.sem_op = 1;
    sem.sem_flg = 0;
    int ret = semop(semid,&sem,1);
    if(ret < 0 )
    {
        perror("semop");
        return -1;
    }
    return 0;
}      
信号量的应用
  • 通过fork创建一个进程,在while循环里面,子进程输出AA(一次输出一个A,输出两次),父进程输出BB(一次输出一个B,输出两次),每次输出中间会有时间的间隔,看采用PV操作前后的输出变化(因为两个进程都往屏幕上输出,显示器只有一个,显示器就是一个临界资源)

comm.h

#ifndef __COMM_H__                                                                                                                      
#define __COMM_H__
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/sem.h>

#define PATHNAME "."
#define PROJ_ID 1
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) */
};

//信号量集中信号量的个数为num
int CommSem(int num,int flags);

//创建信号量集
int SemCreate(int num);

int GetSem();

//销毁信号量集
int DestroySem(int semid);

//对信号量集中下标为index的信号量进行初始化,初始时值为value
int InitSem(int semid,int index,int value);

//获得某个信号量的值
void GetVal(int semid,int index,int* value);

//p操作,对信号量减1
int P(int semid,int index);

//v操作,对信号量加1
int V(int semid,int index);
#endif              

comm.c

#include"comm.h"                                                                                                                        

int CommSem(int num,int flags)
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }

    int semid = semget(key,num,flags);
    if(semid < 0)
    {
        perror("semget");
        return -1;
    }
    return semid;
}

//创建包含num个信号量的信号量集
int SemCreate(int num)
{
    return CommSem(num,IPC_CREAT | IPC_EXCL | 0666);
}

int GetSem(int num)
{
    return CommSem(num,IPC_CREAT);
}

//删除信号量集
int DestroySem(int semid)
{
    //因为删除信号量集,所以第二隔参数任意
    int ret = semctl(semid,0,IPC_RMID);
}

int InitSem(int semid,int index,int value)
{                                                                                                                                       
    union semun sem;
    sem.val = value;  //初始值
    int ret = semctl(semid,index,SETVAL,sem);
    if(ret < 0)
    {
        perror("semctl");
        return -1;
    }
    return 0;
}

void GetVal(int semid,int index,int* value)
{
    union semun sem;
    *value= semctl(semid,index,GETVAL,sem);
}

//index为信号量的编号
int P(int semid, int index)
{
    struct sembuf sem;                                                                                                                  
    sem.sem_num = index;
    sem.sem_op = -1;
    sem.sem_flg = 0;  //这个也必须填写,否则会出现错误
    int ret = semop(semid,&sem,1);  //1表示对1个信号量进行操作
    if(ret < 0 )
    {
        perror("P");
        return -1;
    }
    return 0;
}

int V(int semid,int index)
{
    struct sembuf sem;
    sem.sem_num = index;
    sem.sem_op = 1;
    sem.sem_flg = 0;
    int ret = semop(semid,&sem,1);
    if(ret < 0 )
    {
        perror("semop");
        return -1;
    }                                                                                                                                   
    return 0;
}

test.c

#include"comm.h"                                                                                                                        
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
    int semid = SemCreate(1);
    //信号量集必须初始化
    InitSem(semid,0,1);
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork");
        return -1;
    }
    else if(pid == 0)
    {
        //child
        while(1)
        {
            P(semid,0);
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A ");
            fflush(stdout);
            usleep(345678);
            V(semid,0);
        }

    }
    else
    {
        //parent
        while(1)
        {
            P(semid,0);
            printf("B");
            fflush(stdout);
            usleep(579932);
            printf("B ");
            fflush(stdout);
            usleep(237531);
            V(semid,0);
        }
        wait(NULL);
    }

    DestroySem(semid);
    return 0;
}                           

运行结果:(如果加入信号量就会输出连续的两个A或两个B,如果不加信号量去控制临界资源就会使得两个A之间夹杂着B)
这里写图片描述

猜你喜欢

转载自blog.csdn.net/xu1105775448/article/details/80748088