《linux系统编程 —— 7.IPC对象之信号量 》

1、信号量的基本概念

        通俗的来讲,信号量是用来调度进程间通信顺序的一种方法。信号量本质上是一个数字,用来表征一种资源的数量,当多个进程或者线程争夺这些稀缺资源的时候,信号量用来保证他们合理地、秩序地使用这些资源,而不会陷入逻辑谬误之中。

1.1 信号量的分类

在Unix/Linux系统中常用的信号量有三种:

  • IPC信号量组
  • POSIX具名信号量
  • POSIX匿名信号量

        本节课讲解的是第一种IPC信号量组,既然说到它是一个“组”,说明这种机制可以一次性在其内部设置多个信号量,实际上在其内部实现中,IPC信号量组是一个数组,里面包含N个信号量元素,每个元素相当于一个POSIX信号量。

1.2 基本概念

  • 临界资源(critical resources)
    • 多个进程或线程有可能同时访问的资源(变量、链表、文件等等)
  • 临界区(critical zone)
    • 访问这些资源的代码称为临界代码,这些代码区域称为临界区
  • P操作
    • 程序进入临界区之前必须要对资源进行申请,这个动作被称为P操作,这就像你要把车开进停车场之前,先要向保安申请一张停车卡一样,P操作就是申请资源,如果申请成功,资源数将会减少。如果申请失败,要不在门口等,要不走人。
  • V操作
    • 程序离开临界区之后必须要释放相应的资源,这个动作被称为V操作,这就像你把车开出停车场之后,要将停车卡归还给保安一样,V操作就是释放资源,释放资源就是让资源数增加。

        信号量组非常类似于停车场的卡牌,想象一个有N个车位的停车场,每个车位是立体的可升降的,能停n辆车,那么我们可以用一个拥有N个信号量元素,每个信号量元素的初始值等于n的信号量来代表这个停车场的车位资源——某位车主要把他的m辆车开进停车场,如果需要1个车位,那么必须对代表这个车位的信号量元素申请资源,如果n大于等于m,则申请成功,否则不能把车开进去。

2. 函数接口

2.1 创建(或打开)

SEM其他IPC对象类似,首先需要创建(或打开)SEM对象,接口如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

接口说明:

  • key:SEM对象键值
  • nsems:信号量组内的信号量元素个数
  • semflg:创建选项
    • IPC_CREAT:如果key对应的信号量不存在,则创建之
    • IPC_EXCL:如果该key对应的信号量已存在,则报错
    • mode:信号量的访问权限(八进制,如0644)

创建信号量时,还受到以下系统信息的影响:

  1. SEMMNI:系统中信号量的总数最大值。
  2. SEMMSL:每个信号量中信号量元素的个数最大值。
  3. SEMMNS:系统中所有信号量中的信号量元素的总数最大值。

Linux中,以上信息在 /proc/sys/kernel/sem 中可查看,如下

root@ubuntu:/mnt/hgfs/share_file# cat /proc/sys/kernel/sem
32000   1024000000      500     32000
root@ubuntu:/mnt/hgfs/share_file# 

 下面是一个创建或打开一个有两个元素的信号量组

int main() 
{ 
    key_t key = ftok(".", 1); // 创建(若已有则打开)一个包含2个元素的信号量组 
    int id = semget(key, 2, IPC_CREAT|0666); 
} 

2.2 PV操作

        对于信号量而言,最重要的作用就是用来表征对应资源的数量,所谓的P/V操作就是对资源数量进行 +n/-n 操作,既然只是个加减法,那么为什么不使用普通的整型数据呢?原因是:

  • 整型数据的加减操作不具有原子性,即操作可能被中断
  • 普通加减法无法提供阻塞特性,而申请资源不可得时应进入阻塞

        对于原子性再做个简单的解释,即这种资源数量的加减法不能有中间过程,不管是成功还是失败都必须一次性完成。加减法看似简单,但在硬件层面上并非一个原子性操作,而是包含了多个寄存器操作步骤,因此不能作为P/V操作的手段。

因此在SEM对象中,P/V操作的函数接口如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

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

接口说明:

  • 参数
    • semid:SEM对象ID
    • sops:P/V操作结构体sembuf数组
    • nsops:P/V操作结构体数组元素个数
  • 返回值
    • 成功:0
    • 失败:-1

 具体来看一下sembuf结构体都含有哪些信息:

struct sembuf
{
    unsigned short sem_num;  /* 信号量元素序号(数组下标) */
    short		   sem_op;   /* 操作参数 */
    short		   sem_flg;  /* 操作选项 */
};

 下面通过两个例程来认识PV操作。

P操作:

        P操作的含义类似于申请使用空间,当公共空间有余量空间存在时,P操作才能申请成功,才能使得程序执行下去,否则将阻塞等待。

示例代码如下:

    struct sembuf op;
    op.sem_num = 0;     
    op.sem_op = -1;
    op.sem_flg = 0;
    semop(sem_id, &op, 1);  // 应用P或V操作

         其中op.sem_num = 0,代表是对第1组信号量做操作,0类似数组下标。 op.sem_op = -1;意味着要申请一个空间。op.sem_flg = 0;这个0是默认值,意味着默认不使用任何特殊选项,你也可以设置为IPC_NOWAIT标志来表示不进行阻塞操作。

         那么知道了这个特性后我们便可以封装函数。我们通过函数参数的设定可以更好的理解P操作做了什么事情:

/**
 * @description: 对信号量进行P操作
 * @param {int}  sem_id  信号量对象ID
 * @param {int}  whitch_group    你要操作的是第几组信号量
 * @param {int}  require_size    你需要申请多少空间
 * @param {int}  group_size      信号量组的大小
 * @return {int} 成功返回0,失败返回-1
 */
int P(int sem_id,int whitch_group,int require_size,int group_size)    
{
    struct sembuf op[group_size];
    op[whitch_group].sem_num = whitch_group;     
    op[whitch_group].sem_op = -require_size;
    op[whitch_group].sem_flg = 0;
    return semop(sem_id, &op[whitch_group], group_size);  // 应用P或V操作
}

V操作:

        有了申请空间自然就要释放空间,当公共空间有内存被占用时,可以进行V操作进行释放空间,那么要注意的是,释放的空间不会超过初始值,也就是说如果你用semop函数规定了初始空间就是1,即使你进行十次释放操作,空间还是1。

    struct sembuf op;
    op.sem_num = 0;     
    op.sem_op = +1;
    op.sem_flg = 0;
    semop(sem_id, &op, 1);  // 应用P或V操作

和P操作同理,+1代表释放资源。

下面通过封装函数来理解

/**
 * @description: 对信号量进行P操作 
 * @param {int} sem_id          信号量对象ID
 * @param {int} whitch_group    你要操作的是第几组信号量
 * @param {int} free_size       你要释放多少空间
 * @param {int}  group_size     信号量组的大小
 * @return {int} 成功返回0,失败返回-1
 */
int V(int sem_id,int whitch_group,int free_size,int group_size)  
{
    struct sembuf op[group_size];
    op[whitch_group].sem_num = whitch_group;
    op[whitch_group].sem_op =  free_size;
    op[whitch_group].sem_flg = 0;
    return semop(sem_id, &op[whitch_group], group_size);  // 应用P或V操作
}

 

案例:

创建两个进程shm1与shm2,要求实现 shm1中对共享内存进行scanf写操作,shm2中对共享内存进行printf读操作。

例程1:使用1个信号量。

shm1.c文件

/*
 * @Author: Fu Zhuoyue
 * @Date: 2023-07-14 10:44:25
 * @LastEditors: Fu Zhuoyue
 * @LastEditTime: 2023-07-16 23:18:25
 * @Description: 
 * @FilePath: /share_file/系统编程/5.共享内存/shm1.c
 */
#include "../syshead.h"
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 */
};
/**
 * @description: 对信号量进行P操作
 * @param {int}  sem_id  信号量对象ID
 * @param {int}  whitch_group    你要操作的是第几组信号量
 * @param {int}  require_size    你需要申请多少空间
 * @param {int}  group_size      信号量组的大小
 * @return {int} 成功返回0,失败返回-1
 */
int P(int sem_id,int whitch_group,int require_size,int group_size)    
{
    struct sembuf op[group_size];
    op[whitch_group].sem_num = whitch_group;     
    op[whitch_group].sem_op = -require_size;
    op[whitch_group].sem_flg = 0;
    return semop(sem_id, &op[whitch_group], group_size);  // 应用P或V操作
}

/**
 * @description: 对信号量进行P操作 
 * @param {int} sem_id          信号量对象ID
 * @param {int} whitch_group    你要操作的是第几组信号量
 * @param {int} free_size       你要释放多少空间
 * @param {int}  group_size     信号量组的大小
 * @return {int} 成功返回0,失败返回-1
 */
int V(int sem_id,int whitch_group,int free_size,int group_size)  
{
    struct sembuf op[group_size];
    op[whitch_group].sem_num = whitch_group;
    op[whitch_group].sem_op =  free_size;
    op[whitch_group].sem_flg = 0;
    return semop(sem_id, &op[whitch_group], group_size);  // 应用P或V操作
}

int main()
{
    key_t key = ftok(".",566);

    if(key == - 1)                  //创建对象键值
    {
        perror("ftok");
        return -1;
    }
    printf("key = %d\n",key);

    int shm_id = shmget(key,1024,IPC_CREAT|0666);       //创建共享内存
    if( shm_id == -1)
    {
        perror("shmget");
        return -1;
    }

    int sem_id = semget(key, 1, IPC_CREAT | 0666);  //第二个参数是信号量的个数,如果是信号量数组就填数组元素个数

    union semun a;
    a.val = 1;          // 假设将信号量元素初始值定为1
    semctl(sem_id, 0, SETVAL, a);

    char *p = shmat(shm_id, NULL, 0);       //申请获取共享内存地址
    if( p == (void *) -1)
    {
        perror("shmat");
    }
        
    while(1)
    {
        printf("请输入写入共享内存的消息:\n");
        scanf("%s",p);
        V(sem_id,0,1,1);
    }
    shmdt(p);
    shmctl(shm_id, IPC_RMID, NULL);
    semctl(sem_id, 0, IPC_RMID, 0);
}

shm2.c文件

/*
 * @Author: Fu Zhuoyue
 * @Date: 2023-07-14 10:44:25
 * @LastEditors: Fu Zhuoyue
 * @LastEditTime: 2023-07-16 23:18:05
 * @Description: 
 * @FilePath: /share_file/系统编程/5.共享内存/shm2.c
 */
#include "../syshead.h"

struct sembuf op;
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 */
};

/**
 * @description: 对信号量进行P操作
 * @param {int}  sem_id  信号量对象ID
 * @param {int}  whitch_group    你要操作的是第几组信号量
 * @param {int}  require_size    你需要申请多少空间
 * @param {int}  group_size      信号量组的大小
 * @return {int} 成功返回0,失败返回-1
 */
int P(int sem_id,int whitch_group,int require_size,int group_size)    
{
    struct sembuf op[group_size];
    op[whitch_group].sem_num = whitch_group;     
    op[whitch_group].sem_op = -require_size;
    op[whitch_group].sem_flg = 0;
    return semop(sem_id, &op[whitch_group], group_size);  // 应用P或V操作
}

/**
 * @description: 对信号量进行P操作 
 * @param {int} sem_id          信号量对象ID
 * @param {int} whitch_group    你要操作的是第几组信号量
 * @param {int} free_size       你要释放多少空间
 * @param {int}  group_size     信号量组的大小
 * @return {int} 成功返回0,失败返回-1
 */
int V(int sem_id,int whitch_group,int free_size,int group_size)  
{
    struct sembuf op[group_size];
    op[whitch_group].sem_num = whitch_group;
    op[whitch_group].sem_op =  free_size;
    op[whitch_group].sem_flg = 0;
    return semop(sem_id, &op[whitch_group], group_size);  // 应用P或V操作
}

int main()
{
    key_t key = ftok(".",566);

    if(key == - 1)                  //创建对象键值
    {
        perror("ftok");
        return -1;
    }
    printf("key = %d\n",key);

    int shm_id = shmget(key,1024,IPC_CREAT|0666);       //创建共享内存
    if( shm_id == -1)
    {
        perror("shmget");
        return -1;
    }

    int sem_id = semget(key, 1, IPC_CREAT | 0666);  //第二个参数是信号量的个数,如果是信号量数组就填数组元素个数

    union semun a;
    a.val = 1;          // 假设将信号量元素初始值定为1
    semctl(sem_id, 0, SETVAL, a);   

    char *p = shmat(shm_id, NULL, 0);       //申请获取共享内存地址
    if( p == (void *) -1)
    {
        perror("shmat");
    }

    while(1)
    {
        P(sem_id,0,1,1);   
        printf("从共享内存中读出的信息是:\n");
        printf("%s\n",p);
    }
    shmdt(p);
    shmctl(shm_id, IPC_RMID, NULL);
    semctl(sem_id, 0, IPC_RMID, 0);
}

        我们通过semop函数申请了的初始空间为1,在shm1.c文件中可以看到,我们在scanf操作完成后会为信号量资源释放1个空间,紧接着while循环里会再次等待用户输入。而此时在shm2.c的while循环中,先进行P操作即申请了1个空间,使得当前剩余空间为0,在进行下一轮while循环时会由于可用空间不足无法申请空间而产生阻塞,避免了不断printf数据的现象发生,直到在shm1中scanf输入完成后,进行V操作释放空间为止。才会允许shm2中对P操作的阻塞解除,进行printf。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值