linux:进程间通信之信号量(semop & semget & semctl & P & V)

1.信号量怎样实现同步互斥?

信号量并不是让进程间能够直接的发送字符串数据,而是通过自身计数器的性质,来完成进程之间的同步互斥(通过使用信号量来完成令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域,临界区域是指执行数据更新的代码需要独占式的执行,而信号量则是让一个临界区同一时间只有一个线程在访问他,即就是说信号量是用来协调进程对共享资源的访问的一种手段)

ps:信号量其实就是一个计数器

2.同步互斥

(1)互斥:有些资源不能进行共享的访问,需要互斥的访问来保证数据的完整性,进程间的这种关系为进程的互斥
(2)临界资源:需要进行互斥访问的资源就是临界资源
(3)临界区:某一区域的代码在进行数据更新时,只能被独占式的进行(涉及到临界资源的代码段就是临界区)

3.信号量

(1)信号量的使用场景

信号量可以使用在同步中也可以在异步中使用
互斥:P、V在同一进程
同步:P、V不在同一进程

(2)信号量值含义

S>0—>S表示现在有多少个资源可以使用
S<0—>|S|的值是现在等待进程的个数
S=0—>现在已经没有资源可用了,也没有进程在等待

ps:注意system V版本的信号量不会小于0,当可用资源的个数等于0时,就会阻塞,直到某个资源被释放,才会在进行相关操作

(3)P、V的含义

P—>P表示申请资源,此时可用资源的个数会减少一个
V—>V表示释放资源,此时可用资源的个数会增加一个

ps:注意区分pv量,pv量有时表示信号量,有时是某网站被用户在一天中总访问的次数,此时的PV是page view,简称pv

4.信号量结构体伪代码


struct semaphore
{
 int value;
 pointer_PCB queue;
}

5.P、V原语

(1)申请资源

P(s)
{
   s.value=s.value--;//如果s<0那就会阻塞等待,直到有资源释放
   if(s.value<0)
    {
      该进程状态置为等待状态
      将该进程的PCB插入相应的等待队列s.queue
    }
 }

(2)释放资源

V(s)
{
   s.value=value++;//只有value<=0,才会有等待队列,否则不会有等待队列,故当  value<=0,唤醒等待队列就好
   if(s.value<=0)
    {
      唤醒相应等待队列s.queue中的一个等待队列
      改变其状态为就绪状态
      将其插入就绪队列
    }
 }

6.相关函数

(1)semget()

功能:创建和访问一个信号集
头文件:#include<sys/sem.h>

int semget(key_t key,int nsems,int semflag);
参数说明:
   key:信号集的名字
   ps:key是ftok()函数的返回值
   nsems:信号集中信号量的个数
          ps:此参数的值总是1
   semflag:由9个权限构成

返回值:
    成功:返回一个非负整数,此整数就是信号集的标识码
    失败:返回-1

(2)semctl()

功能:用于控制信号集
头文件:#include<sys/sem.h>

int semctl(int semid,int semnum,int cmd,......)
参数说明:
     semid:是semget函数的返回值,此参数是信号集的标识码
     semnum:信号集中信号量的序号(即将要进行操作的信号量的编号)
            ps:第一个信号量的索引为0,此参数没有太大的意义
     cmd:对信号集将要采取的操作

返回值:
    成功:返回0
    失败:返回-1


cmd:对信号集将要采取的操作
以下是cmd常用的操作:

(1)IPC_RMID:删除信号集
(2)SETVAL:设置信号集中的信号量的计数值
(3)GETVAL:获取信号集中的信号量的计数值

对于该函数,只有当cmd取某些特定的值的时候才会用到第4个参数,第4个参数通常是一个union semnum结构

union semnum
{
  int val;
  struct semid_ds*buf;
  struct short*array;
 }
对于第4个参数,当cmd等于SETVAL时,会用到union sennum结构中的成员val

(3)semop()

功能:对信号量进行操作
头文件:#include<sys/sem.h>

int semop(int semid,struct sembuf*sops,unsigned nsops);
参数说明:
    semid:semid:是semget函数的返回值,此参数是信号集的标识码
    sops:是一个指针,指向结构体sembuf
    nsops:信号量的个数

sembuf结构体

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

说明:
  sem_num:是信号量在信号集中的索引,0代表第一个信号量,1代表第二个信号量
  sem_op:是对信号量在进行P/V操作时进行加减的数值,一般只会用到两个值:
     (1)P操作:-1,进行P操作就是-1,等待信号量变得可用
     (2)V操作:+1,进行V操作就是+1,发出信号量变得可用
  sem_flg:是操作标志,有两个取值:IPC_NOWAIT、IPC_UNDO

7.实例

sem.h

#include<stdio.h>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<sys/wait.h>

#define PATHNAME "."
#define PROJ_ID 0x6666

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

int createSemSet(int nums);//创建信号量,此时的nums是创建几个信号量,一般为1
int getSemSet(int nums);//打开一个信号量,此时的nums是打开几号信号量
//这两个参数同名是因为,打开与创建的操作基本相同,为了把相同的操作封装成一个函数,便于操作

int destroySemSet(int semid);//销毁信号集
int initSem(int semid,int nums,int initVal);//初始化信号量
//semid是信号集的标识码,
//nums是信号量的序号,此参数是对那个信号量进行操作,
//initVal是要进行初始化的那个信号量的值

int P(int semid,int who);//semid是信号量的标识码,who表示要对那个信号量进行P操作
int V(int semid,int who);//semid是信号量的标识码,who表示要对那个信号量进行V操作

sem.c

#include"sem.h"
static int commSemSet(int nums,int flag)//static表示此函数只能在此文件中使用
{
    //获得key
    key_t key=ftok(PATHNAME,PROJ_ID);
    if(key<0)
    {
        perror("ftok");
        exit(1);
    }
    //创建信号集
    int semid=semget(key,nums,flag);
    if(semid<0)
    {
        perror("semget");
        exit(1);
    }
    return semid;
}
int createSemSet(int nums)
{
    return commSemSet(nums,IPC_CREAT|IPC_EXCL|0666);
}
int getSemSet(int nums)
{
    return commSemSet(nums,0);
}
int initSem(int semid,int nums,int initVal)
{
    union semun _un;
    _un.val=initVal;
    if(semctl(semid,nums,SETVAL,_un)<0)
    {
        perror("semctl");
        exit(1);
    }
    return 0;
}
static int commPV(int semid,int who,int op)//who:是对那个信号量进行操作,op:是进行什么操作
{
    struct sembuf s;
    s.sem_num=who;
    s.sem_op=op;
    s.sem_flg=0;
    if(semop(semid,&s,1)<0)
    {
        perror("semop");
        exit(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)
    {
        perror("semctl");
        exit(1);
    }
    return 0;
}

test.c

#include"sem.h"
int main()
{
    //创建信号集
    int semid=createSemSet(1);//创建一个信号量,此信号量的索引为0
    //初始化信号量
    initSem(semid,0,1);//把索引为0的信号量初始化为1
    //创建两个进程
    pid_t pid=fork();
    if(pid<0)
    {
        perror("fork");
        exit(1);
    }
    if(pid==0)
    {
        //打开信号集
        int semid=getSemSet(0);//打开索引为0的信号量,索引为0的信号量就是父进程创建的信号量
        while(1)
        {
            //申请资源
            P(semid,0);//对信号集semid中的索引为0的信号量进行P操作
            printf("A");
            fflush(stdout);//因为printf中没有换行符,所以输出缓冲区中的A不会输出,故使用fflush函数刷新输出缓冲区
            usleep(100000);
            printf("A");
            fflush(stdout);
            usleep(100000);
            printf("A");
            fflush(stdout);
            usleep(100000);
            //释放资源
            V(semid,0);//释放刚才申请的资源
        }
    }
    if(pid>0)
    {
        while(1)
        {
            //申请资源
            P(semid,0);
            printf("B");
            fflush(stdout);
            usleep(100000);
            printf("B");
            fflush(stdout);
            usleep(100000);
            printf("B");
            fflush(stdout);
            usleep(100000);
            //释放资源
            V(semid,0);
        }
        wait(NULL);
    }
    destroySemSet(semid);//销毁信号集
    return 0;
}

运行结果:
这里写图片描述

当程序正在执行的时候,使用Ctrl +c终止进程(异常终止)
这里写图片描述
信号量的生命周期随内核,如果没有手动删除的话,那就会一直存在,当第一次异常终止进程后,内核中就会存在一个消息队列,所以程序第二次在执行时,就会出现文件存在,手动删除信号量后,再次执行就可以正常执行了

当不使用信号量时,两个进程同时打印,此时显示器成为了临界资源,会出现以下的情况:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
    pid_t pid=fork();
    if(pid<0)
    {
        perror("frok");
        exit(1);
    }
    while(1)
    {
        if(pid==0)
        {
            printf("A");
            fflush(stdout);
            usleep(100000);
            printf("A");
            fflush(stdout);
            usleep(100000);
        }
        if(pid>0)
        {
            printf("B"); 
            fflush(stdout);
            usleep(100000);
            printf("B"); 
            fflush(stdout);
            usleep(100000);
        }
    }
    return 0;
}

运行结果:
这里写图片描述
解析:A和B会乱序的打印,谁先打印不确定,不会得到我们想要的结果,故不互斥的访问临界资源会导致数据错乱

猜你喜欢

转载自blog.csdn.net/dangzhangjing97/article/details/80157963
今日推荐