进程间通信------信号量

     以下用到的消息队列中的相关内容,均可在这里找到:https://blog.csdn.net/sandmm112/article/details/79936107

        信号量是进程间通信的有一种方式。之前有提到共享内存是不带同步与互斥机制的,这样在多个进程同时对共享内存写入数据时,会导致数据错乱。而信号量就是确保多个进程在对共享内存这样的共享资源同时读写时,使之实现互斥与同步的,下面,在具体介绍一下互斥与同步的概念。

互斥:当多个进程竞争同一资源时,这些资源一次只能用于一个进程,所以这些资源只能互斥使用。比如说两个进程都要向显示器上输出内容,但同一时刻显示器只能被一个进程使用,此时,显示器就叫做临界资源或互斥资源。这两个进程之间的关系就叫做互斥。而两个进程中涉及到临界资源的代码段就叫做临界区。

同步:多个进程在执行顺序上存在着遵循着一定的关系对于某临界资源来说,只能一个进程使用完了,另一个进程才能使用。

一,信号量

        信号量本质上可以理解为一个计数器,记录临界资源的个数的计数器。比如说上面的显示器资源只有一个,那么此时记录显示器资源的信号量的值即为1。

        称信号量值为1的为二元信号量,也叫互斥锁。

信号量的P操作:       

        假设有一个二元信号量即它维护的临界资源个数只有一个。当一个进程要使用临界资源(如显示器资源)时,对记录该临界资源的信号量的值减1。当另一进程也要使用该临界资源时,因此临界资源的数目此时为0,所以该进程必须挂起等待,将该进程的PCB放入等待队列中。

信号量的V操作

        当进程使用完临界资源时,信号量加1。如果此时在等待队列中有等待的进程,就唤醒该进程将临界资源分配给他使用。

        通过以上描述得知,一个信号量不仅包含临界资源的数目,还必须维护一个队列。所以,信号量的结构体伪代码如下:

struct semaphore
{
    int value;//信号量的值,即它维护的临界资源的数目
    pointer_PCB queue;//等待队列
}

        当多个进程要使用同一临界资源时,都要向维护该资源的信号量进行申请,所以,多个进程都必须能看到这个信号量资源,所以信号量也是一个临界资源。

        信号量想要维护临界资源使其满足互斥与同步机制,而信号量本身又是临界资源,所以它自己必须得满足互斥与同步,才能维护它的临界资源。所以,对信号量的操作必须是原子的。

原子操作:访问资源时要么全做,即把一整套流程全部做完,要么不做,不会有做到一半就停下或被打扰的状态。

        因此,上述信号量的P-V操作都必须是原子的(此时,临界资源的数目不一定为1),它们可分别概括为:

P-操作:

P(s)//s为信号量维护的临界资源
{
    s.value--;
    if(s.value <= 0)
    {
        //使申请临界资源的进程挂起等待,它的PCB放入等待队列s.queue中  
    }
}

V-操作:

V(s)
{
    s.value++;
    if(s.value > 0)
    {
         //唤醒等待队列s.queue中的进程,给它分配临界资源,使其状态变为就绪态
    }
}

二,信号量集

        维护一种临界资源需要一个信号量,那维护多种临界资源就需要多个信号量。多种信号量组成一个信号量集。信号量集可以看做是计数器的个数,信号量值可看作计数器的个数。信号量集是以数组形式进行组织的,以下标来提取各个信号,数组元素表示信号量的值即临界资源的数目。

        在对信号量进行操作时,都是以信号量集为单位进行的。比如要对显示器资源进行维护,此时就需要一个信号量集,该集中只有一个信号量,所以其下标为0即可。

        以下为信号量集的操作函数:

1. 创建和访问一个信号量集

int semget(key_t key,int nsems,int semflg);//头文件:,,

参数:

        key:由ftok函数返回的key值,与消息队列中的用法相同。

        nsems:信号量集中的信号两个数

        semflg:可选参数为IPC_CREAT和IPC_EXCL,用法与消息队列中的相同。

返回值:成功返回一个非负整数,即该信号量集的标识符,失败返回-1。

        信号量集创建好后,操作系统会为其维护一个数据结构semid_ds,内容与消息队列的结构体msqid_ds类似。 

2. 控制信号量集

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

参数:

        semid:由semget返回的信号量集的标识符

        semnum:对信号量集中的哪个信号量进行操作,即为信号量所在的下标

        cmd:对信号量做何种操作,取值如下:

            IPC_STAT,IPC_SET,IPC_RMID这三种与消息队列中的用法相同,最后一个参数也与消息队列的最后一个参数相对应

            SETVAL:初始化信号量集,即设置信号量集中的信号量的计数值

            GETVAL:获取信号量集中的信号量的计数值

            cmd取后两个值时,最后一个参数为一联合体变量(这里我们只关注val的值):

union semun
{
    int val;//信号量的初始值
    struct semid_ds* buf;
    unsigned short* array;
    struct seminfo* _buf;
}

3. 对信号量进行操作

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

参数:

        semid:由semget返回的信号量集的标识符

        sops:指向一个结构体struct sembuf的指针

struct sembuf
{
    short sem_num;//某个信号量所处的下标
    short sem_op;//对信号量所采取的操作,-1为P操作,1为V操作
    short sem_flg;//取0表示没资源时阻塞等待,取IPC_NOWAIT表示没资源不等待
}

        nsops:信号量的个数

以下通过代码来说明信号量的作用:

        当编写如下代码,想要循环实现一个进程输出AA,另一进程输出BB,即“AA BB AA BB...”这种形式时:

int main()
{
    pid_t pid = fork();
    if(pid < 0)
    {
        ERR_EXIT("fork error");
    }
    else if(pid == 0)//子进程输出AA
    {
        while(1)
        {
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A ");                                                                                                             
            fflush(stdout);
            usleep(125455);
        }
    }
    else//父进程输出BB
    {
        while(1)
        {
            printf("B");
            fflush(stdout);
            usleep(123456);
            printf("B ");
            fflush(stdout);
            usleep(125455);
        }
        wait(NULL);
    }
    return 0;
}   

        运行结果如下:

[admin@localhost Semaphore]$ ./sem 
BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BA^C

        可以看到,结果并没有达到我们预期的效果,那是因为当父进程向显示器输出一个B后,显示器又被子进程使用输出A,而不是一次使父进程使用完即输出BB之后再给子进程使用输出AA。此时显示器是临界资源,父子进程要竞争使用它,我们希望父子进程可以互斥的使用它,即输出AA后在输出BB,此时就需要使用信号量来维护显示器资源使其实现互斥与同步。

封装信号量的操作函数:

头文件:

#pragma once                                                                                                                          

#include
#include
#include
#include
#include
#include
#include
//异常退出宏函数
#define ERR_EXIT(m)\
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }while(0)

#define PATHNAME "."
#define PROJ_ID 0X6666
union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};
//创建一个信号量集
int CreateSem(int nsem);
//获取一个信号量集
int GetSem(int nsem);
//初始化信号量集
int InitSem(int semid,int semnum,int value);
//对信号量集进行P操作
int P_Op(int semid,int who);
//对信号量集进行V操作
int V_Op(int semid,int who);
//删除信号量集
int DestorySem(int semid,int nsem); 

封装函数:

#include "comm.h"                                                                                                                     
//创建或访问一个信号量集
static int CommSem(int nsem,int semflags)
{
    key_t key = ftok(PATHNAME,PROJ_ID);//生成key值
    if(key < 0)//生成出错
    {
        ERR_EXIT("ftok error");
    }
    //创建或访问一个信号量集
    int semid = semget(key,nsem,semflags);
    if(semid < 0)//获取或访问出错
    {   
        ERR_EXIT("semget error");
    }   
    return semid;//返回创建或获取的信号量集标识符
}
//创建一个信号量集
int CreateSem(int nsem)
{
    return CommSem(nsem,IPC_CREAT|IPC_EXCL|0666);
}
//获取一个信号量集
int GetSem(int nsem)
{
    return CommSem(nsem,IPC_CREAT);
}
//初始化信号量集
int InitSem(int semid,int semnum,int value)
{
    union semun _un;//联合体变量
    _un.val = value;
    int ret = semctl(semid,semnum,SETVAL,_un);//初始化信号量集
    if(ret < 0)//初始化失败
    {
        printf("init error\n");
        return -1;
    }
    return 0;
}
//对信号量集进行P-V原语操作
static int CommOp(int semid,int who,int flag)
{
    struct sembuf sem;
    sem.sem_num = who;
    sem.sem_op = flag;
    sem.sem_flg = 0;
    int ret =  semop(semid,&sem,1);
    if(ret < 0)
    {
        printf("semop error\n");
        return -1;
    }                                                                                                                                 
    return 0;
}
//对信号量集进行P操作
int P_Op(int semid,int who)
{
    return CommOp(semid,who,-1);
}
//对信号量集进行V操作
int V_Op(int semid,int who)
{
    return CommOp(semid,who,1);
}
//删除信号量集
int DestorySem(int semid,int nsem)
{
    int ret = semctl(semid,nsem,IPC_RMID);
    if(ret < 0)
    {
        printf("delete error\n");
        return -1;
    }
    return 0;
}                  

再对上述代码进行如下改进:

#include "comm.h"
int main()
{                                                                                                                                     
    //创建一个信号量集
    int semid = CreateSem(1);//信号量集中只有一个信号量
    //初始化信号量集
    InitSem(semid,0,1);//初始化信号量集中的0号下标信号量值为1
    pid_t pid = fork();//创建子进程
    if(pid < 0)
    {
        ERR_EXIT("fork error");
    }
    else if(pid == 0)//子进程
    {
        while(1)
        {
            P_Op(semid,0);//子进程申请显示器资源,如果没有,则等待,如果有,则使用,此时其他进程不能使用
            printf("A");//向显示器上输出A
            fflush(stdout);
            usleep(123456);
            printf("A ");//在输出A
            fflush(stdout);
            usleep(125455);
            V_Op(semid,0);//归还显示器资源,归还后,其他进程才可使用
        }
    }
    else//父进程
    {
        while(1)
        {                                                                                                                             
            P_Op(semid,0);   
            printf("B");
            fflush(stdout);
            usleep(123456);
            printf("B ");
            fflush(stdout);
            usleep(125455);
            V_Op(semid,0);
        }
        wait(NULL);
    }
    DestorySem(semid,0);//删除信号量集
    return 0;
}

再次运行时,可看到如下结果:

[admin@localhost Semaphore]$ ./sem 
BB AA BB AA BB AA BB AA BB AA BB AA BB AA BB A^C

        可以看到,已达到我们预期的效果,说明信号量集可以实现对临界资源的互斥与同步。

        当再次运行该程序时,会出现下面情况:

[admin@localhost Semaphore]$ ./sem 
semget error: File exists

        这是因为信号量集的生命周期随内核,而不随进程。当上述进程按ctrl+c异常终止时,并没有执行信号量销毁函数,所以该信号量集还存在,此时可以通过如下命令进行删除:

[admin@localhost Semaphore]$ ipcs -s  //查看信号量集

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     
0x66023b61 262146     admin      666        1         
0x66023c51 294915     admin      666        1         
0x66023b50 360452     admin      666        1         

[admin@localhost Semaphore]$ ipcrm -s 360452  //根据信号量集的标识符删除指定信号量集

        再次运行时,便可正常运行。

注意:消息队列,共享内存,信号量三种System V IPC资源的生命周期都是随内核的,必须进行删除。除非重启。

三,将上述的信号量的P-V操作封装成动静态库

关于动静态的详细操作见:https://blog.csdn.net/sandmm112/article/details/79718221

1. 封装为静态库

        首先编译封装函数comm.c

[admin@localhost Semaphore]$ ls
comm.c  comm.h  LIB.a  LIB.so  makefile  sem  sem.c
[admin@localhost Semaphore]$ gcc -c comm.c -o comm.o
[admin@localhost Semaphore]$ ls
comm.c  comm.h  comm.o  LIB.a  LIB.so  makefile  sem  sem.c

        在将comm.o文件打包成静态库:

[admin@localhost Semaphore]$ ls
comm.c  comm.h  comm.o  LIB.a  LIB.so  makefile  sem  sem.c
[admin@localhost Semaphore]$ ar -rc -o libcomm.a comm.o
[admin@localhost Semaphore]$ ls
comm.c  comm.h  comm.o  LIB.a  libcomm.a  LIB.so  makefile  sem  sem.c

        编译sem.c文件并链接上述生成的静态库:

[admin@localhost LIB.a]$ ls
comm.h  libcomm.a  sem.c
[admin@localhost LIB.a]$ gcc sem.c -L. -lcomm
[admin@localhost LIB.a]$ ls
a.out  comm.h  libcomm.a  sem.c

        运行可执行程序a.out:

[admin@localhost LIB.a]$ ./a.out 
BB AA BB AA BB AA BB AA BB ^C

        将静态库删除后,只要不删除可执行文件a.out,该执行文件会一直生效。因为静态链接已经将comm.c的二进制代码链接进a.out中。

2. 封装为动态库

        编译comm.c文件。此时要加-fPIC选项生成与位置无关码

[admin@localhost Semaphore]$ gcc -fPIC -c comm.c -o comm.o
[admin@localhost Semaphore]$ ls
comm.c  comm.h  comm.o  LIB.a  LIB.so  makefile  sem  sem.c

        将comm.o文件打包生成动态库

[admin@localhost Semaphore]$ gcc -shared -o libcomm.so comm.o
[admin@localhost Semaphore]$ ls
comm.c  comm.h  comm.o  LIB.a  libcomm.so  LIB.so  makefile  sem  sem.c

        编译sem.c文件并链接动态库

[admin@localhost LIB.so]$ ls
comm.h  libcomm.so  sem.c
[admin@localhost LIB.so]$ gcc sem.c -L. -lcomm -o sem
[admin@localhost LIB.so]$ ls
comm.h  libcomm.so  sem  sem.c

        运行sem文件,因为是动态链接,所以直接运行会因为找不到动态库而出错。

[admin@localhost LIB.so]$ ./sem 
./sem: error while loading shared libraries: libcomm.so: cannot open shared object file: No such file or directory

        因此,运行时还需要指定动态库的路径,这里用设置环境变量的方法指定路径。

[admin@localhost LIB.so]$ export LD_LIBRARY_PATH=.
[admin@localhost LIB.so]$ ./sem 
BB AA BB AA BB AA BB AA BB AA BB AA BB AA BB AA BB AA BB AA BB AA BB AA B^C

        所以,即使生成了可执行程序sem,但动态库还不能删除,因为每次运行时都要链接该动态库。

四,生产者消费者原理

        在文章开头已经详细介绍过互斥的概念。要研究生产者与消费者原理还必须理解同步的概念,下面具体说明同步的概念:

进程互斥与同步的基本概念

        进程互斥与同步机制的主要任务是,对多个相关进程在执行次序上进行协调,使并发执行的诸进程间能按照一定的规则(或时序)共享系统资源,并能很好的相互合作,从而使程序能够正确执行。

        在多个进程共享资源或为完成某一任务而相互合作时,可能存在以下两种形式的制约关系:

(1)间接相互制约关系(互斥)

        多个程序并发执行时,要对临界资源进行共享,所以只能互斥的访问临界资源,这些进程间就形成了间接制约关系。

(2)直接相互制约关系(同步)

        多个进程间未完成某一共同任务时必须合作进行。比如一个进程用于输入数据,另一进程用于计算数据,只有输入进程拿到数据之后,计算进程才可进行计算,也就是说输入进程完成后才可进行计算进程,即输入进程直接制约着计算进程。

        由于上述两种制约关系的存在,必须对临界资源的访问和进程的执行次序进行协调,保证各进程按次序执行,这就是进程的互斥与同步概念。

生产者消费者问题

        该问题就是一个著名的进程同步问题。它描述的是:一群生产者进程正在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程和消费者进程能够并发执行。在两者之间设置了一个具有n个缓冲区的缓冲池,生产者进程将生产的产品放入缓冲区中,消费者进程从缓冲区中取走产品进行消费。

        生产者在投放产品时,消费者不能去取产品。消费者在取产品时,生产者不能投放产品。即对缓冲区必须互斥的使用。

        只有生产者往缓冲区中投放了产品,消费者才可取走消费,而不能在空缓冲区中取走产品。同理,当缓冲区满了的时候只有消费者进程取走产品后,生产者进程才可继续投放产品,而不能对满了的缓冲区投放产品。所以生产者进程与消费者进程之间必须同步

        所以,可以利用信号量实现对临界资源(缓冲区)的维护。

你可能感兴趣的:(Linux,信号量,动静态库,同步与互斥,消费者与生产者)