进程通信之信号量

一,信号量简介

信号量⼜称为信号灯,主要是⽤来保护共享资源,使得资源在⼀个时刻只有⼀个进程(线程)所拥有
它的本质就是⼀个整数计数器,⽤来为多个进程共享的数据结构提供受控访问。信号量⽤于实现进程间或指定线程的不同线程间的互斥与同步,而不是⽤于存储进程间通信数据,它是为配合其他通信⼿段⽽存在的。
Linux提供两种信号量:
1.内核信号量,由内核控制路径使⽤
2.⽤户态进程使⽤的信号量,这种信号量⼜分为POSIX信号量和SYSTEMV信号量。
libc库实现了POSIX和System V两种接⼝标准的信号量API。这⾥主要讨论SystemV接⼝的信号量API。
相关概念:
临界资源:是⼀次仅允许⼀个进程使⽤的共享资源。
原⼦操作:原⼦操作是不可分割的,在执⾏完毕之前不会被任何其它任务或事件中断。
信号量是⼀个特殊的变量,程序对其访问都是原⼦操作。

二,信号量操作

信号量初始化的值的⼤⼩⼀般⽤于表示可⽤资源的数;如果初始化为1,则称之⼆值信号量(⼆元信号量),⼆值信号量的功能就有点像互斥锁,使⽤⽐较常⻅。
当它的值⼤于0时,表示当前可⽤资源的数量;当它的值⼩于0时,其绝对值表示等待使⽤该资源的进程个数。内核会负责维护信号量的值,并确保其值不⼩于0。
信号量只能进⾏两种操作即P操作和V操作
P操作是:
(1)S减1;
(2)若S减1后仍⼤于或等于零,则进程继续执⾏;
(3)若S减1后⼩于零,则该进程被阻塞  
V操作是:
(1)S加1;
(2)若相加结果⼤于等于零,则进程继续执⾏;
(3)若相加结果⼩于零,则该进程被阻塞。
使⽤PV操作实现进程互斥时应该注意:
1.每个程序中⽤户实现互斥的P、V操作必须成对出现,先做P操作,进临界区,后做V操作,出临界区。若有多个分⽀,要认真检查其成对性。
2.P、V操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环。
3.互斥信号量的初值⼀般为1

三,信号量API介绍

信号量创建函数:

说明:
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引⽤⼀个现有的集合,则将num_sems指定为 0。

操作信号量:

信号量操作相关的数据结构:
struct sembuf //定义好的,可以直接拿来使⽤
{
short sem_num; // 信号量集中信号的编号,0代表第⼀个信号
short sem_op; //// 信号量在⼀次操作中需要改变的数据,通常是两个数,⼀个
-1,即P(等待)操作,
// ⼀个是+1,即V(发送信号)操作。
short sem_flg;
};
sem_op: sem_op >0,表示V操作,使得信号量+sem_op,
        若sem_op <0,表示p操作,使得信号量-sem_op
        若sem_op =0,调⽤进程进⼊阻塞,直到信号量为0,
        若 sem_flg 值为 IPC_NOWAIT,不会阻塞,返回EAGAIN错误
sem_flg: 若设为IPC_NOWAIT,信号量会以⾮阻塞⽅式进⾏
        通常设为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量⽽终⽌时,操作 系统释放信号量

控制信号量函数:

信号量常⽤控制指令:
IPC_SET 设置信号量struct semid_ds信息
IPC_STAT 获取信号量struct semid_ds信息
IPC_INFO 获取信号量struct seminfo信息
IPC_RMID 删除信号量集
SETVAL 通过公⽤体中val设置信号量集中单个信号的值
GETPID 获取信号量拥有者的pid的值;
GETNCNT: 获取等待信号量的值递增的进程数;
GETZCNT: 获取等待信号量的值递减的进程数;
GETVAL 获取信号量集中单个信号的值,放⼊共⽤体val
SETVAL 通过公⽤体中val设置信号量集中单个信号的值
GETALL 获取信号量集中所有信号的值,放⼊公⽤体数组中
SETALL 通过公⽤体数组设置信号量集中所有信号的值
常用的主要有:
SETVAL:⽤来把信号量初始化为⼀个已知的值。这个值通过union semun中的val成员设置。 其作⽤是在信号量第⼀次使⽤之前对它进⾏设置。
IPC_RMID:⽤于删除⼀个⽆需继续使⽤的信号量标志符。
信号量控制相关的结构体:
对于semctl函数,只有当command取某些特定的值的时候,才会使⽤到第4个参数,第四个参数⼀般传的是个union semun sem_un 联合体, 主要⽤来对信号量集中特定的信号量进⾏初始,因为参数信号量参数多,所以采⽤联合体的形式减少内存。
union semun
{
int val; /* Value for SETVAL */
struct semid_ds *buf; /*⽤于获取IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */ 是对信号量集的操作
struct seminfo *__buf; /* Buffer for IPC_INFO */获取信号集的信息
};
array指向⼀个数组,当cmd==SETALL时,就根据arg.array来将信号量集的所有值都赋值;当cmd ==GETALL时,就将信号量集的所有值返回到arg.array指定的数组中。
//SETVAL⼀次初始化⼀个信号量
int semid=semget(125,2,IPC_CREAT|0666); //创建2个信号量。
semctl(semid,0,SETVAL,5); //设置第1个信号量的值为 5
semctl(semid,1,SETVAL,3); //设置第1个信号量的值为 3
int val1 = semctl(semid, 0, GETVAL); //获取第 0 个信号量的值
int val2 = semctl(semid, 1, GETVAL); //获取第 1 个信号量的值
printf("val1=%d,val2=%d\n",val1,val2); //5,3
//SETALL初始化多个信号量
int semid=semget(127,3,IPC_CREAT|0666);
unsigned short sem_arr[3] = {2, 5, 8};
semctl(semid,0,SETALL,sem_arr);// 注意:此时第⼆个参数被忽略
//读取多个信号量
unsigned short temp[3]={0};
semctl(semid, 0, GETALL, temp);// 注意:这时候第⼆个参数被忽略
for(int i=0;i<3;i++)
printf("val=%d\n",temp[i]);
注意:信号量⽤完要删除

四,信号量的使用步骤

1>>创建⼀个新信号量或取得⼀个已有信号量
int semget(key_t key, int num_sems, int sem_flags) ;
2>>初始化信号量(可以⽤SETVAL命令)
int semctl(int sem_id, int sem_num, int command, ...) ;
3>>操作信号量(PV操作)
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops) ;
4>>删除信号量(可以⽤IPC_RMID命令)
int semctl(int sem_id, int sem_num, int command, ...) ;

样例程序:

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

#define SEM_KEY 1234    // 信号量 key
#define LOOP_NUM 5

// 封装 P 操作
void P(int semid) {
    struct sembuf sb;
    sb.sem_num = 0;   // 信号量编号(信号量集里第0个)
    sb.sem_op = -1;   // P操作:减1
    sb.sem_flg = 0;
    semop(semid, &sb, 1);
}

// 封装 V 操作
void V(int semid) {
    struct sembuf sb;
    sb.sem_num = 0;   // 信号量编号
    sb.sem_op = 1;    // V操作:加1
    sb.sem_flg = 0;
    semop(semid, &sb, 1);
}

int main() {
    int semid;
    pid_t pid;

    // 1. 创建一个信号量集合,包含1个信号量
    semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }

    // 2. 初始化信号量的值为1(互斥锁)
    if (semctl(semid, 0, SETVAL, 1) == -1) {
        perror("semctl");
        exit(1);
    }

    // 初始化文件
    FILE *fp = fopen("count.txt", "w+");
    if (!fp) {
        perror("fopen");
        exit(1);
    }
    fprintf(fp, "0\n");
    fclose(fp);

    // 3. 创建子进程
    pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    }

    for (int i = 0; i < LOOP_NUM; i++) {
        // P操作:进入临界区
        P(semid);

        // ---- 临界区 ----
        fp = fopen("count.txt", "r+");
        int count;
        fscanf(fp, "%d", &count);
        rewind(fp);
        fprintf(fp, "%d\n", count + 1);
        fclose(fp);
        printf("进程 %d 修改 count: %d -> %d\n", getpid(), count, count + 1);
        // ---- 临界区 ----

        // V操作:退出临界区
        V(semid);

        sleep(1);
    }

    // 父进程等待子进程
    if (pid > 0) {
        wait(NULL);
        // 删除信号量
        semctl(semid, 0, IPC_RMID);
        printf("最终结果写在 count.txt 里\n");
    }

    return 0;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值