System V IPC -- 信号量

System V IPC – 信号量

信号量不同于其它IPC,它本身是不携带消息数据的,它的功能主要实现进程的同步的。上一篇讲解共享内存的时候说过,多个进程会出现同时操作共享内存的情况,多个进程访问临界资源可能会造成错误,所以需要互斥的访问,那么信号量恰好可以实现对共享内存的互斥访问!(信号量和共享内存是一对好兄弟)

信号量是由内核维护的一个整数,一般该值 ≥ 0;信号量正是由内核维护的一个整数,所以对信号量的操作是原子操作,是由内核保证的!

信号量最简单的理解:可以理解成是资源的剩余数量。如体育馆上有3个篮球场、2个乒乓球场。此时为了表示两种资源需要创建两个信号量sem1、sem2。早上体育馆开馆时,球场都是空闲的,所以两个信号量为了表示球场的数量分别初始化成:sem1 = 3, sem2 = 2;当有人来打乒乓球时,需要占用一个乒乓球场,那么代表乒乓球场的sem2需要 -1。此时还剩下一个乒乓球场,倘若此时又来了人来打乒乓球,所以需要再占用一个乒乓球场,那么sem2需要 -1,此时sem2的值为0,表示已经没有乒乓球场这个资源了,所以再来人打乒乓球,肯定是打不了了。假如有一伙打球的人打累了,退场了,那么sem2的值需要 +1,此时sem2的值为1。

通过上面的例子,动态的说明了信号量值的作用,信号量代表某个资源,信号量的值代表该资源的剩余数量。

某些靓仔、靓女看到这里可能会有一个疑问,为啥不直接使用一个变量来代表资源的数量?对一个变量做简单的加减操作时,虽然代码只有简单的一行,但实际上是需要很多条汇编指令来完成的,并不是一个原子操作(不可以被分割,执行过程中不可以被打断)。

示例:

//add.c
#include <stdio.h>

int main(int argc, char **argv)
{
    int a = 0;
    a++;
    return 0;
}

[wy@wy ~/textCode/text/FIFO]$ gcc add.c -S //生成.s文件

//add.s
.file	"add.c"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -20(%rbp)
	movq	%rsi, -32(%rbp)
	movl	$0, -4(%rbp)
	addl	$1, -4(%rbp)
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

可以看到简单一个+1操作有好几条汇编指令(当然,你不需要浪费时间去读懂这些汇编指令的含义,知道对一个变量简单的+1不是原子操作就够了)

  1. 信号量集的打开或创建
    int semget(key_t key, int nsems, int semflg);
    //参数2:创建的信号量的个数,需要>0
    

    需要注意的是,此时只是创建了信号量,此时信号量还没有值,所以还不能直接使用

    可以通过ipcs -s查看是否创建成功,也可以使用ipcrm -S/s 删除信号量集

    [wy@wy ~/textCode/text/ipc/sem]$ ipcs -s
    
    --------- 信号量数组 -----------
    键          semid      拥有者   权限    nsems     
    0x00000080 32768        wy     660      1
    
  2. 信号量的控制
    int semctl(int semid, int semnum, int cmd, ...);
    //参数2:需要操作的信号量(哪一个信号量)
    //参数3:操作,可以通过SETVAL(对一个信号量设置)、SETALL(对所有信号量设置,此时参数semnum需要填成0) 对信号量的值进行设置
    //可变长参数:根据不同的cmd,有不同的填法,如果需要填第四个参数,它被定义成一个union,如下
    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(Linux-specific) */
    };
    //semun中的结构体定义不在详细说明,详情可以查看man帮助
    

    对信号量设置初始值的时候,如果初始值大于1,又称为计数信号量;如果初始值等于1,称为二元信号量,该信号量的作用显然适用于互斥操作(特殊的同步)!

  3. 信号量的操作
    int semop(int semid, struct sembuf *sops, size_t nsops);
    //sops:指向结构体数组的指针,结构体中包含了需要执行操作
    struct sembuf {
        unsigned short sem_num;  /* semaphore number */
        short          sem_op;   /* semaphore operation */
        short          sem_flg;  /* operation flags */
    };
    //nsops:指定数组的大小(至少为1)
    //sem_flg标记,除了可以设置为IPC_NOWAIT(不阻塞)外,还可以设置SEM_UNDO(回退操作),
    //如果当前两个进程访问临界资源采用二进制信号量,其中一个进程执行完信号量值减一变为0后,
    //突然意外终止了,造成信号量的值一直为0,那么另一个进程需要访问临界资源的时候,需要判断
    //信号量的值,发现值一直为零,所以使没办法访问临界资源的,所以如果使用SEM_UNDO
    //标志位后,当某个进程意外退出时,可以回退信号量在被该进程使用之前的值!
    

    信号量的操作在大学教材中常用的说法是P、V;这两个字母可不是某两个英文单词的首字母,而是荷兰词汇的首字母。因为提出信号量的概念的大佬是荷兰计算机科学家Dijkstra提出的。

  4. 代码示例:

    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    #include <sys/shm.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    
    /* 
     * 本程序实现两个进程操作共享内存的一个整型值,使用信号量实现两个进程
     * 互斥的访问共享内存
     */
    
    #define N 10000000 /* 小目标:1000w */
    
    int main(int argc, char **argv)
    {
        int shmid = shmget(100, 8, IPC_CREAT|0660);
    
        int *p = (int*)shmat(shmid, NULL, 0);
        memset(p, 0, 8);
        
        int semid = semget(128, 1, IPC_CREAT|0660);
        //对一号信号量(数组下标从0开始,所以我写的是0)的值设置为1
        semctl(semid, 0, SETVAL, 1);
        
        struct sembuf P, V;
        memset(&P, 0, sizeof(P));
        memset(&V, 0, sizeof(V));
    
        //定义P,V结构体的操作
        P.sem_num = 0;
        P.sem_op = -1;
        P.sem_flg = 0;
    
        V.sem_num = 0;
        V.sem_op = 1;
        V.sem_flg = 0;
    
        if(0 == fork()){
            for(int i = 0; i < N; ++i){
                //执行P操作,使信号量值-1,变成0
                semop(semid, &P, 1);
                (*p)++;
    
                //执行V操作,使信号的值+1
                semop(semid, &V, 1);
            }
            exit(0);
        }
    
        else{
            for(int i = 0; i < N; ++i){
                //执行P操作,使信号量值-1,变成0
                semop(semid, &P, 1);
                (*p)++;
    
                //执行V操作,使信号的值+1
                semop(semid, &V, 1);
            }
            wait(NULL);
            printf("*p = %d\n", *p);
        }
    
        shmdt(p);
    
        return 0;
    }
    运行结果:
    *p = 20000000
    

    可以得到正确的结果!

  5. 最后不推荐使用system V的信号量实现同步,可以使用锁或是使用posix的信号量替代

三种system V的高级IPC就全部讲解完毕了,总结如下,哈哈!

消息队列共享内存信号量
创建msgget()shmget()semget()
控制msgctl()shmctl()semctl()
其它发消息:msgsnd()
收消息:msgrcv()
附加到本进程:shmat()
解除附加:shmdt()
控制:semop()
查看ipcs -qipcs -mipcs -m
查看限制ipcs -lqipcs -lmipcs -ls
命令删除
小写后面+id
大写后面+key
ipcrm -q/Qipcrm -m/Mipcrm -s/S

本人能力有限,如有错误望各位大佬不吝指正,原创不易,转载请注明出处

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值