进程间通信-信号量

资源竞争

  • 资源竞争:当多个进程在同时访问共享资源时,会产生资源竞争,最终最导致数据混乱
  • 临界资源:不允许同时有多个进程访问的资源,包括硬件资源(CPU、内存、存储器以及其他外围设备)与软件资源(共享代码段、共享数据结构)
  • 临界区:访问临界资源代码

多进程对 stdout 资源的竞争

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(){
    pid_t cpid;

    cpid = fork();
    if(cpid == -1){
        perror("[Error] fork()");
        exit(EXIT_FAILURE);
    }
    else if(cpid == 0){
        while(1){
            printf("----------------\n");
            printf("C start.\n");
            printf("C end.\n");
            printf("---------------\n");
        }
    }else if(cpid > 0){
        while(1){
            printf("----------------\n");
            printf("P start.\n");
            printf("P end.\n");
            printf("-----------------\n");
        }
        wait(NULL);
    }
    return 0;


}

二、同步与互斥

  • 互斥 :同一时刻只有一个进程访问临界资源
  • 同步: 在互斥的基础上增加了进程对临界资源的访问顺序
  • 进程主要的同步与互斥手段是 信号量

信号量简介

  • 信号量: 由内核维护的整数,其值被限制为大于或等于0
  • 信号量可以执行如下操作:
    • 将信号量设置成一个具体的值
    • 在信号量当前值的基础上加上一个数值
    • 在信号量当前值的基础上减上一个数值
    • 等待信号量的值为 0
      一般信号量分为 二值信号量 与 数信号量
  • 二值信号量 :一般指的是信号量 的值为 1可以理解为只对应一个资源
  • 计数信号量:一般指的是值大于等于2可以理解为对应多个资源
  • 在 Linux 系统中查询信号量使用 ipcs -s

四、创建信号量集合

  • 创建信号量集合调用 semget 函数

函数头文件

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

函数原型

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

函数功能

创建一个信号量集合

函数参数

  • key : 由 ftok() 函数生成
  • nsems : 信号量的数量
  • semflg: 信号量集合的标志
    • IPC CREAT:创建标志
    • IPC EXCL: 与IPC CREAT 标志一起使用,如果信号量集合存在就报错
    • 权限标志

五、初始化信号量

初始化信号量调用 semctl 函数

函数头文件

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

函数原型

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

函数功能

信号集合控制函数,根据 cmd 决定当前函数的功能

函数参数

  • semid : 信号量集合的id
  • semnum:信号量的编号,信号量的编号从 0开始
  • cmd : 命令控制字
    • SETVAL: 设置信号量的值
    • GETVAL: 获取信号量的值
  • ... : 后面是属于可变参参数列表,根据不同的命令有不同的参数
    : 后面是属于可变参参数列表,根据不同的命令有不同的参数

函数返回值
成功: 根据不同的命令有不同的返回值,可以查看帮助文档关于 RETURN 的说明

  • GETNCNT the value of semncnt
  • GETPID the value of sempid
  • GETVAL the value of semval
  • GETZCNT the value of semzcnt
  • All other cmd values return 0 on success
  • 失败: 返回 -1,并设置 errno
    在使用命令时需要使用 union semun 共用体,具体定义如下

    创建一个信号量集合,集合中包含一个信号量,并设置信号量的值为 1
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define SEM_PATHNAME "."
#define SEM_PRO_ID 100

union semun{

    int val;
};
int main(){
    int semid,ret;
    union semun s;
    key_t key = ftok(SEM_PATHNAME,SEM_PRO_ID);
    if(key == -1){
        perror("[ERROR] ftok():");
        exit(EXIT_FAILURE);
    }
    semid = semget(key,2,IPC_CREAT | 0666);
    if(semid == -1){
        perror("[ERROR] semget");
        fprintf(stdout,"%s\n",strerror(errno));
        exit(EXIT_FAILURE);
    }
    s.val = 1;
    ret = semctl(semid,0,SETVAL,s);
    if(ret == -1){
        perror("[ERROR] semctl()");
        exit(EXIT_FAILURE);
    }
    ret = semctl(semid,1,SETVAL,s);
    if(ret == -1){
        perror("[ERROR] setctl()");
        exit(EXIT_FAILURE);
    }
    sleep(10);
    ret = semctl(semid,0,IPC_RMID,NULL);
    if(ret == -1){
        perror("[ERROR] semctl()");
        exit(EXIT_FAILURE);
    }
    return 0;
}

信号量操作

  • 信号量可以进行以下操作:
    • 对信号量的值加1
    • 对信号量的值减1
    • 等待信号量的值为0
  • 操作信号量调用 semop 函数

函数头文件

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

函数原型

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

函数功能

信号量操作函数,用于占用信号量、释放信号量、设置信号量等待

函数参数

  • semid :信号量集合id
  • sops : 信号量操作结构体指针,见后面关于 struct sembuf 解释
  • nsops: 操作的信号量的数量

函数返回值

  • 成功 : 返回 0
  • 失败: 返回-1,并设置 errno

struct sembuf 结构体

  • unsigned short sem_num;
    • 信号量编号,从0 开始,在 sem_op 的帮助文档中0
  • short sem_op;
    • 信号量操作
      • -1 : 占用资源
      • +l : 释放资源
      • 0 : 等待资源
  • short sem_flg;
    • 信号量操作标志
      • IPC_NOWAIT:非阻塞,在信号量的值为0时,会立即返回
      • SEM_UNDO: 在进程终止时,会自动释放信号量
        在 semop 函数中关于信号量集合编号的说明

信号量集合删除

  • 信号量集合调用 semctl 函数,设置命令为 IPC_RMID
    • 注意:在使用 IPC_RMID 时,第 三个参数会被忽略,下面是帮助文档中的说明

ret = semctl(semid,IPC_RMID,NULL);

信号量互斥应用

  • 使用信号量解决父子进程对终端的竞争
  • 信号量操作封装
  • sem.h
#ifndef _SEM_H
#define _SEM_H
#endif

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

// 创建信号量合集
extern int sem_create(int nsems,unsigned short values[]);
extern int sem_p(int semid,int semnum);
extern int sem_v(int semid,int semnum);
extern int sem_del(int semid);
extern int sem_get(int semid,int semnum);

  • sem.c
#include<stdio.h>
#include "sem.h"

#define PATHNAME "."
#define SEM_PRO_ID 100
union semun{
    int val;
    unsigned short *array;
};
int sem_create(int nsems,unsigned short values[]){
    int semid, ret;
    key_t key;
    union semun s;
    key = ftok(PATHNAME,SEM_PRO_ID);
    if(key == -1){
        perror("[ERROR] ftok()");
        exit(EXIT_FAILURE);
    }
    semid = semget(key,nsems,IPC_CREAT|0600);
    if(semid == -1){
        perror("[ERRIR] semget()");
        exit(EXIT_FAILURE);
    }
    s.array = values;
    ret = semctl(semid,0,SETALL,s);
    if(ret == -1){
        perror("[ERROR] setctl()");
        exit(EXIT_FAILURE);
    }
    return semid;

}
int sem_p(int semid, int sem_num){
    struct sembuf sops;
    sops.sem_num = sem_num;
    sops.sem_op = -1;
    sops.sem_flg = SEM_UNDO;

    return semop(semid,&sops,1);
}
int sem_v(int semid,int sem_num){
    struct sembuf sops;
    sops.sem_num = sem_num;
    sops.sem_op = 1;
    sops.sem_flg = SEM_UNDO;
    return semop(semid,&sops,1);
}
int sem_del(int semid){
    return semctl(semid,0,IPC_RMID,NULL);
}
int sem_get(int semid,int sem_num){
    union semun2 s;
    int val = semctl(semid,sem_num,GETVAL);
    if(val == -1){
        perror("[ERROR] semctl error");
        exit(EXIT_FAILURE);
    }
    return val;
}

  • main.c
#include<stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include  "sem.h"

int main(){
    pid_t cpid;

    int semid;
    unsigned short values[] = {1};
    semid = sem_create(1,values);
    if(semid == -1){
        perror("[ERROR] sem_create()");
        exit(EXIT_FAILURE);
    }
    cpid = fork();
    if(cpid == -1){
        perror("[ERROR] fork()");
        exit(EXIT_FAILURE);
    }
    else if(cpid == 0){
        while(1){
            sem_p(semid,0);
            printf("-----------\n");
            printf("C start \n");
            sleep(1);
            printf("C end \n");
            printf("-----------\n");
            sem_v(semid,0);
        }
    }else if(cpid > 0){
        while(1){
            sem_p(semid,0);
            printf("-----------\n");
            printf("P start \n");
            sleep(1);
            printf("P end \n");
            printf("------------\n");
            sem_v(semid,0);
        }
    }
    return 0;
}

信号量同步

需求

  • 创建父子进程,输出“ABA”字符串,具体需求如下
    • 父进程 输出A
    • 子进程 输出 B
    • 父进程 输出 A,输出换行
    • 能够循环输出“ABA”字符

实现思路

通过创建一个信号量集合,包含 2个信号量,一个信号量 编号为 0 (SEM_CONTROL_P) 控制父进程的运行与暂停,一个信号量 编号为 1 (SEM_CONTROL_C) 控制子进程的运行与暂停

  • 信号初始化
    • SEM_CONTROL_P :初始化为 1
    • SEM_CONTROL_C :初始化为 0
  • 子进程:
    • 占用 SEM_CONTROL_C,此时子进程阻塞
    • 当父进程释放 SEM_CONTROL_C 时,子进程输出 B,释放 SEM_CONTROL_P
    • 循环占用 SEM_CONTROL_C,由于之前已经占用,此时进入子进程阻塞,等待父进程释放SEM_CONTROL_C
  • 父进程:
    • 占用 SEM_CONTROL_P此时父进程正常运行,输出 A
    • 释放 SEM_CONTROL_C,占用 SME_CONTROL_P,此时父进程阻塞,子进程继续执行
    • 当子进程输出 B 之后,释放 SEM_CONTROL_P父进程继续执行,输出 A
    • 父进程释放 SEM_CONTROL_P 循环结束
#include<stdio.h>
#include "sem.h"
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define SEM_CONTROL_P 0
#define SEM_CONTROL_C 1
int main(){
    pid_t cpid;
    int semid;
    unsigned  short values[] = {1,0};
    semid = sem_create(2,values);
    if(semid == -1){
        perror("[ERROR] sem_create");
        exit(EXIT_FAILURE);
    }
    cpid = fork();
    if(cpid == -1){
        perror("[ERROR] fork()");
        exit(EXIT_FAILURE);
    }
    else if(cpid == 0){
        while(1){
            if(sem_p(semid,SEM_CONTROL_C) == -1){
                perror("[ERROR] sem_p");
                exit(EXIT_FAILURE);
            }
            printf("B");
            fflush(stdout);
            sem_v(semid,SEM_CONTROL_P);
        }
    }
    else if(cpid > 0){
        while(1){
            sem_p(semid,SEM_CONTROL_P);
            printf("A");
            fflush(stdout);
            sem_v(semid,SEM_CONTROL_C);
            sem_p(semid,SEM_CONTROL_P);
            printf("A");
            fflush(stdout);
            sem_v(semid,SEM_CONTROL_P);
            putchar('\n');
            sleep(0.1);
        }
        wait(NULL);
    }
    return 0;
}

对于SEM_UNDO来说,内核记录的信息是跟进程相关的。一个进程在P操作的时候对应该进程的UNDO计数就多一个,V操作的时候那么计数就减一。在设置SEM_UNDO的时候一定要注意使用的场景,否则就会导致Numerical result out of range错误。
1)如果P\V操作都是在一个进程中完成,就可以设置该标志, 但要注意P\V操作时要同时设置,否则也会导致计数值一直增加而溢出;
2)如果一个进程做P操作,另外一个进程做V操作,就不能设置该标志,因为对单一的进程来说UNDO计数会一直增加而溢出,计数的上限是32767(和信号量的最大值相同)

posted @ 2023-04-04 18:27  shubin  阅读(24)  评论(0编辑  收藏  举报