进程间的通信方式(一)之——共享内存和信号量

目录

一、共享内存

1.1 共享内存概述

1.2 共享内存操作函数

1.2.1 shmget函数(共享内存的创建和获取)

1.2.2 shmat函数(共享内存的映射)

1.2.3 shmdt函数(解除共享内存的映射)

1.2.4 shmctl函数(共享内存控制)

1.3 共享内存操作命令

1.3.1 ipcs -m

1.3.2 ipcrm -m shmid

1.3.3 其他ipcs命令

1.4 共享内存应用实例

1.4.1 共享内存例程

1.4.2 共享内存错误的同步机制 

1.4.3 使用共享内存的优缺点

二、信号量

2.1 信号量概述

2.1.1 多任务编程中互斥、同步、临界资源概念

2.1.2 信号量概念及其工作原理

2.2 信号量操作函数

2.2.1 semget函数(创建或获取信号量)

2.2.2 semop函数(改变信号量值)

2.2.3 semctl函数(控制信号量)

2.3 信号量应用实例

三、共享内存结合信号量应用实例

3.1 例程及运行结果

3.2 浅谈其他进程通信方式


一、共享内存

1.1 共享内存概述

       在Linux系统中,每个进程都有独立的虚拟内存空间,也就是说不同的进程访问同一段虚拟内存地址所得到的数据是不一样的,这是因为不同进程相同的虚拟内存地址会映射到不同的物理内存地址上。但有时候为了让不同进程之间进行通信,需要让不同进程共享相同的物理内存,Linux通过共享内存来实现这个功能。

       共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但其它的多个进程都可以访问,使得多个进程可以访问同一块内存空间。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

1.2 共享内存操作函数

       Linux中提供了一组函数接口用于使用共享内存,分别是shmget、shmat、shmdt、shmctl函数,函数参数直接看难以理解,结合实际应用编程就很简单了,使用它们需包含头文件:

#include<sys/ipc.h>
#include<sys/shm.h>

1.2.1 shmget函数(共享内存的创建和获取)

       shmget函数用来获取或创建共享内存,它的原型为:

int shmget(key_t key, size_t size, int shmflg);

       参数key:共享内存键值,程序需要提供一个参数key(非0整数,unsigned int类型),它有效地为共享内存段命名。

       参数size:需要申请共享内存的大小,以字节为单位。

待求证:

       网上看到过这种说法,在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。但是我在工作中看见的工程代码中开辟共享内存空间并无按此规律,都是按需创建的。还有说实际分配的内存块大小将被扩大到页面大小的整数倍。

       参数shmflg:标识函数的行为及共享内存的权限,其取值IPC_CREAT表示如果不存在就创建;IPC_EXCL表示如果已经存在则返回失败;位或权限位后可以设置共享内存的访问权限,格式和 open()函数的mode参数一样。

shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时创建它的话,可以与IPC_CREAT做或操作。如果是已经存在的,可以使用IPC_CREAT或直接传0。共享内存的权限标志与文件的读写权限一样,举例来说,0666|IPC_CREAT表示全部用户对它可读写,如果共享内存不存在,就创建一个共享内存。总而言之:与IPC_CREAT做或操作即存在就获取,不存在就创建。

       返回值:成功返回共享内存标识符;失败返回-1。

关于shmget参数key和返回值的补充:

       系统建立IPC通讯(消息队列、信号量和共享内存)时必须指定一个键值,也就是调用shmget函数时提供的参数key,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),用于后续的共享内存函数。

       key键值作为实参传入shmget函数有两种方法:

       ①通常情况下,该键值通过ftok函数得到

       使用ftok函数需包含以下头文件,它的原型为:

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

key_t ftok( char * fname, int id ) 

       参数fname:程序员指定的文件名(已经存在的文件名),一般使用当前目录“·”,即:

 key_t key;

 key = ftok(".", 1);     //这样就是将fname设为当前目录。

       参数id:子序号,由程序员提供的一个整数标识符。

       返回值:成功返回key_t值(即IPC 键值),失败返回-1。

       下面是ftok生成key键值然后调用shmget函数创建共享内存例程:

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


int main()
{
    key_t key;
    int shmid;
    char str[40];

    key = ftok(".",27);
    printf("key = 0x%x\n",key);
    if((shmid = shmget(key,128, IPC_CREAT|0666)) == -1)
	{
		perror("m6y: Create Shared Memory Error");
	}
    sprintf(str,"%s","ipcs -m");
    system(str);
    
    return 0;
}

       运行结果:(周立功基于i.MX6ULL处理器M6Y2C运行环境) 

图1.1 创建共享内存运行结果

       ftok是将文件的索引节点号前面加上子序号得到key_t类型的返回值,如本例程中当前文件的索引节点号16进制为 0x060001,我指定的整数标识符为27,换算成16进制为0x1b,所以最后的返回值为0x1b060001,作为shmget创建的共享内存键值。shmget函数只是创建,由于还没有任何进程绑定至该共享内存,所以nattch(当前附加到共享内存段的进程数)计数器值为0。

ftok返回的是根据文件信息(文件索引节点号)和整数标识(id)合成的IPC key键值,从而避免用户使用key值的冲突。id值的意义让一个文件也能生成多个IPC key键值。ftok利用同一文件最多可得到IPC key键值0xff(即256)个,因为ftok只取id值二进制的后8位。

        ②不用ftok函数直接在调用shmget时传入键值

       此种方法直接由程序员给出共享内存键值,最好用16进制表示,因为系统显示就是用的16进制形式。无需调用ftok函数去生成,使用例程如下:

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


int main()
{
    int shmid;
    char str[40];

    if((shmid = shmget((key_t)0x1234,128, IPC_CREAT|0666)) == -1)
	{
		perror("m6y: Create Shared Memory Error");
	}
    sprintf(str,"%s","ipcs -m");
    system(str);
    
    return 0;
}

       运行结果:

图1.2 key键值生成例程结果

       key是共享内存的键值,是共享内存在系统中的编号,不同共享内存的编号不能相同,这一点由程序员保证。shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数,注意两者区别。

1.2.2 shmat函数(共享内存的映射)

       创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间,通俗讲就是用一个指针指向这个共享内存空间。它的原型如下:

void *shmat(int shmid, const void *shmaddr, int shmflg);

       参数shmid:shmid是由shmget函数返回的共享内存标识。

       参数shmaddr:指定共享内存连接到当前进程中的地址位置,即共享内存映射地址,通常为NULL,表示让系统来选择共享内存的地址。

       参数shmflg:共享内存段的访问权限和映射条件,是一组标志位,若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。通常为0表示共享内存具有可读可写权限。

       返回值:成功返回共享内存段映射地址( 相当于这个指针就指向此共享内存 ),并且nattch(当前附加到共享内存段的进程数)计数器加1 ;失败返回-1。返回类型为void*泛指针类型,需强制类型转换为相应的指针类型被接收,然后就可以通过指针访问共享内存啦。

1.2.3 shmdt函数(解除共享内存的映射)

       该函数用于将共享内存从当前进程中分离,相当于shmat函数的反操作。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:

int shmdt(const void *shmaddr);

       参数shmaddr:shmat函数返回的地址。

       返回值:成功返回0,并将nattch计数器减1;出错返回-1。

       将共享内存和当前进程分离( 仅仅是断开联系并不删除共享内存,相当于让之前的指向此共享内存的指针不再指向)。对exit或任何exec族函数的调用都会自动使进程脱离共享内存块。

1.2.4 shmctl函数(共享内存控制)

       shmctl是控制共享内存的函数,其功能不只是删除共享内容,但其它的功能没什么用,所以此处不做介绍,只需知道它是用来删除共享内存即可,原型如下:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

       参数shmid:shmget函数返回的共享内存标识符。

       参数cmd:函数功能的控制,有多种取值,取IPC_RMID表示删除共享内存段。

       参数buf:是一个结构指针,它指向共享内存模式和访问权限的结构,设置为NULL即可。

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

1.3 共享内存操作命令

1.3.1 ipcs -m

       用ipcs -m可以查看系统的共享内存,如下图:

图1.3 ipcs -m命令结果

· Shared Memory Segments:共享内存段

· key:键值

· shmid:共享内存编号

· owner:创建者

· perms:权限

· bytes:大小

· nattch:当前附加到共享内存段的进程数

1.3.2 ipcrm -m shmid

       用ipcrm -m +shmid可以手动删除共享内存,如下图:

图1.4 删除共享内存命令示意

1.3.3 其他ipcs命令

       多进程间通信常用的技术手段包括共享内存、消息队列、信号量等等,Linux系统下自带的ipcs命令是一个极好的工具,可以帮助我们查看当前系统下以上三项的使用情况,从而利于定位多进程通信中出现的通信问题。

       查看系统消息队列信息:ipcs -q

图1.5 ipcs -q命令

· Message Queues:消息队列

· msqid:消息队列ID

       查看系统信号量信息:ipcs -s

图1.6 ipcs -s命令

· Semaphore Arrays:信号量

· semid:信号量ID

· nsems:信号量集中的信号量数

       系统默认输出信息,显示系统内所有的IPC信息:ipcs -a

图1.7 ipcs -a命令

       还有其他很多ipcs命令,可参考博客:ipcs命令详解 - 马昌伟 - 博客园 (cnblogs.com)

1.4 共享内存应用实例

1.4.1 共享内存例程

       共享内存的应用就是要掌握上面4个函数的使用,纸上得来终觉浅,只有编程实战演练才能熟练应用共享内存,下面就以两个例程(进程)来说明进程间如何通过共享内存进行通信。

       程序shared_memory_test_1.c创建共享内存,并将它连接到自己的地址空间,然后就每隔1s向共享内存写入一个字符,写入10个字符后休眠10s将共享内存和进程分离,代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    int i = 0;
    key_t key;
    int shmid;
    char *shaddr = NULL;

    printf("process 1 running\n");
    srand((unsigned int)time(NULL));        //生成随机数种子
    key = ftok(".",27);
    //创建共享内存
    if(shmid = shmget(key,128, IPC_CREAT|0666) == -1)
    {
        printf("process 1 shmget error\n");
        return 1;
    }
    //连接共享内存
    shaddr = (char*)shmat(shmid, NULL, 0);
	if((int)shaddr == -1)
	{
	    printf("process 1 shmat error\n");
        return 1;
	}
    system("ipcs -m");      //查看共享内存
    for(i=0;i<10;i++)
    {
        *(shaddr+i)='A'+i;          //向共享内存写入数据
        printf("process 1 向共享内存写入数据: %c\n",*(shaddr+i));
        sleep(1);
    }
    printf("process 1 共享内存写入数据完毕~~~\n");
    sleep(10);
    //共享内存和进程分离
    if(shmdt(shaddr) == -1)
    {
        printf("process 1 shmdt error\n");
        return 1;
    }
    printf("process 1 stop running,共享内存和进程分离:\n");
    system("ipcs -m");      //查看共享内存

    return 0;
}

       程序shared_memory_test_2.c根据相同键值获取共享内存并连接,然后休眠随机时间间隔从共享内存读数据,读10次后休眠15s将共享内存和进程分离并删除共享内存,代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    int i = 0;
    key_t key;
    int shmid;
    char *shaddr = NULL;

    printf("process 2 running\n");
    srand((unsigned int)time(NULL));        //生成随机数种子
    key = ftok(".",27);
    //创建共享内存
    if(shmid = shmget(key,128, IPC_CREAT|0666) == -1)
    {
        printf("process 2 shmget error\n");
        return 1;
    }
    sleep(1);
    //连接共享内存
    shaddr = (char*)shmat(shmid, NULL, 0);
	if((int)shaddr == -1)
	{
	    printf("process 2 shmat error\n");
        return 1;
	}
    system("ipcs -m");      //查看共享内存
    for(i=0;i<10;i++)
    {
        printf("process 2 从共享内存读数据:");
        printf("%s\n",shaddr);             
        sleep(rand()%3+1);
    }
    printf("process 2 从共享内存读数据完毕\n");
    sleep(15);
     //共享内存和进程分离
    if(shmdt(shaddr) == -1)
    {
        printf("process 2 shmdt error\n");
        return 1;
    }
    //删除共享内存
    if(shmctl(shmid,IPC_RMID,NULL)==-1)
    {
        printf("process 2 shmctl error\n");
        return 1;
    }
    printf("process 2 stop running,删除共享内存\n");
    system("ipcs -m");

    return 0;
}

程序对共享内存的操作是很快的,很难测试出读/写共享冲突的情况,所以采用sleep语句假设程序操作共享内存需要时间。

       在周立功M6Y2C开发板环境(Linux)下同时运行两个进程,控制台打印结果如下:

共享内存测试例程运行结果

结果分析:

       从运行结果首先可以验证共享内存成功实现了进程间数据交互,进程1往共享内存写入的字符成功在进程2中读取。进程1创建共享内存并连接后通过ipcs -m命令可以看到nattch数为1,表示有一个进程关联到此共享内存;随后进程2获取共享内存并连接后nattch数就变为2了,此时两个进程均已连接到共享内存。

       进程1每间隔1s向共享内存写入一个字符,进程2间隔随机时间从共享内存读取字符串,从运行结果可以看到进程2前几次进行读共享内存数据操作时进程1写数据操作并没有完成,因此读到的是不同结果。然而实际应用中往往是一个进程在往共享内存写入数据全部完成后再由另一个进程去读取,若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。

共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。

1.4.2 共享内存错误的同步机制 

       一个进程在往共享内存写数据的时候别的进程不能去读写共享内存数据,我们可能会想到在数据结构中定义一个变量作为可读或可写的标志,例如非0表示可读,0表示可写,写完将标志进行加1操作,读完将标志进行减1操作。乍看之下,它似乎行得通,但这不是原子操作,这种做法是不安全的,如果当标志为0时有两个进程同时访问共享内存,都对其进行写操作,显然不行。

       原子操作就是: 不可中断的一个或者一系列操作, 也就是不会被线程调度机制打断的操作, 运行期间不会有任何的上下文切换(context switch)。

为什么关注原子操作?
1. 如果确定某个操作是原子的, 就不用为了去保护这个操作而加上会耗费昂贵性能开销的锁。
2. 借助原子操作可以实现互斥锁(mutex)。
3. 借助互斥锁, 可以实现让更多的操作变成原子操作。

       要想让程序安全地执行,就要有一种进程同步的进制,保证在进入临界区的操作是原子操作。例如,可以使用信号量来进行进程的同步,因为信号量的操作都是原子性的。

1.4.3 使用共享内存的优缺点

       优点:使用共享内存进行进程间通信非常方便,函数接口简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。因为直接在内存上操作,所以共享内存是最快的进程间通信方式。

       缺点:共享内存没有提供同步的机制,程序员必须提供自己的同步措施,例如在数据被写入之前不允许进程从共享内存中读取、不允许两个进程同时向同一个共享内存地址写入数据等。

二、信号量

2.1 信号量概述

2.1.1 多任务编程中互斥、同步、临界资源概念

       互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
       同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

       临界资源:可以简单的理解为在某一个时刻只能由一个进程或线程进行操作的资源,这里的资源可以是一段代码、一个变量或某种硬件资源。信号量的值大于或等于0时表示可供并发进程使用的资源实体数;小于0时代表正在等待使用临界资源的进程数。

2.1.2 信号量概念及其工作原理

       为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的

       信号量本质上是一个计数器,用于协调多个进程(包括但不限于父子进程)对共享数据对象的读写。与其它进程间通信方式不大相同,它不以传送数据为目的,主要是用来保护共享资源(信号量、消息队列、socket连接等),保证共享资源在一个时刻只有一个进程独享。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时进程也可以修改该标志,除了用于共享资源的访问控制外,还可用于进程同步,特别是对临界资源的访问的同步。它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源,因此,主要作为进程间以及同一个进程内不同线程之间的同步手段

       信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许进程对它进行等待信号和发送信号操作。最简单的信号量是取值0和1的二元信号量,这是信号量最常见的形式,叫做二进制信号量。通用信号量(可以取多个正整数值)和信号量集方面的知识比较复杂,应用场景也比较少,本文只介绍二元信号量。

信号量的工作原理:

       编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量(sem)值大于0时,则可以访问,否则将阻塞。它是一个特殊变量,只允许对它进行等待和发送信号这两种操作,即P和V,一次P操作使信号量减1,一次V操作使信号量加1。它们的行为是这样的:

       P操作:等待,如果sem的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行,进入阻塞等待,这说明P操作可能会阻塞,只有sem大于0时P才不会对进程阻塞。
       V操作:发送信号,如果有其他进程因等待sem而被挂起,就让它恢复运行,如果没有进程因等待sem而挂起,就给它加1,这表明V操作不会阻塞

       举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

       P操作是用在进入临界区之前,V操作是用在离开临界区之后,这两个操作一般是成对出现的。

信号量应用场景:信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源(比如上文说的共享内存)的访问,可以用来保证两个或多个关键代码段不被并发调用。

信号量用于互斥逻辑图:

图2.1 信号量用于互斥逻辑图

信号量用于同步逻辑图:

图2.2 信号量用于同步逻辑图

注意:

不要把信号量与信号混淆起来,它们是不同的两种事物。信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。

2.2 信号量操作函数

       Linux提供了一组精心设计的信号量接口来对信号量进行操作,所有的Linux信号量函数都是针对成组的通用信号量进行操作,而不只是针对一个二进制信号量。但是在绝大多数情况下,使用一个单个信号量就足够了,所以在这里只讨论单个信号量的使用。使用它们需要包含头文件:

#include <sys/sem.h>

2.2.1 semget函数(创建或获取信号量)

       semget函数用来创建一个新信号量或取得一个已有信号量,不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。它的原型如下:

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

        参数key:信号量键值,与上述共享内存键值含义相同,是信号量在系统中的编号,不同信号量的编号不能相同,这一点由程序员保证。

       参数nsems:要创建的信号集包含的信号个数,该参数只在创建信号量集时有效,几乎总是取值为1;如果只是打开信号集,把nsems设置为0即可。一旦创建了该信号量, 就不能更改其信号量个数,只要不删除该信号量,重新调用该函数创建该键值的信号量,该函数只是返回以前创建的值,不会重新创建。

        参数semflg:标识函数的行为及信号量的权限,可取值:

      IPC_CREAT:创建信号量。当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。

        IPC_EXCL:检测信号量是否存在。当semflg取IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

        位或权限位:信号量位或权限位后可以设置信号量的访问权限,格式和open函数的mode一样,但可执行权限未使用。

        返回值:执行成功返回一个信号集的标志符,失败返回-1。

2.2.2 semop函数(改变信号量值)

       信号量的值与相应资源的使用情况有关,当它的值大于0时,表示当前可用资源的数量,当它的值小于0时,其绝对值表示等待使用该资源的进程的个数。信号量的值仅能由PV操作来改变,在Linux下,PV操作通过调用函数semop实现。该函数有两个功能:①等待信号量的值变为1,如果等待成功,立即把信号量的值置为0,这个过程也称之为等待锁;②把信号量的值置为1,这个过程也称之为释放锁。它的原型如下:

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

       参数semid:semget函数返回的信号量标识符;  

       参数sops:是一个struct sembuf结构类型的数组指针,指向进行操作的结构体数组首地址;

       参数nsops:是操作信号量的个数,即操作信号量的结构体数组中元素的个数,设置它的值为1(只对一个信号量的操作)。

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

       结构sembuf来说明所要执行的操作,其定义如下:

struct sembuf{
  unsigned short sem_num;//信号在信号集中的索引,0代表第一个信号,1代表第二个信号  
  short sem_op;
  short sem_flg;
}

sembuf结构中:

       sem_num是相对应的信号量集中的某一个资源,所以其值是一个从0到相应的信号量集的资源总数之间的整数。除非使用一组信号,否则它的取值一般为0(sembuf结构体数组的第一个成员)。

       sem_op的值是一个整数,是信号量在一次操作中需要改变的数值(可以是非1的数值)。通常只会用到两个值:-1—P操作,+1—V操作。

       对信号量最基本的操作就是进行PV操作,而信号量正是通过semop函数和sembuf结构体的数据结构来进行PV操作的。当sembuf的第二个数据结构sem_op设置为负数时,是对它进行P操作,即减1操作;当设置为正数时,就是进行V操作,即加1操作。

       P操作负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。操作为:申请一个空闲资源(把信号量减1),若成功,则退出;若失败,则该进程被阻塞。

       V操作负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。

       sem_flg说明函数semop的行为——信号操作标志,可能的选择有两种: 

       IPC_NOWAIT:在对信号量的操作不能执行的情况下,semop()不会阻塞,使函数立即返回,同时设定错误信息。
       SEM_UNDO :程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。

       通常被设置为SEM_UNDO,它将使得操作系统跟着当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。

       示例用法:

       P操作:等待信号量的值变为1,如果等待成功,立即把信号量的值置为0

struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);

       V操作:把信号量的值置为1

struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);

2.2.3 semctl函数(控制信号量)

       该函数用来控制信号量(常用于设置信号量的初始值和销毁信号量),它的原型如下:

int semctl(int semid, int sem_num, int command, ...);

       参数semid:semget函数返回的信号量标识符;

       参数sem_num:跟semop函数sembuf结构中的sem_num一样,是信号量集数组上的下标,表示某一个信号量,单个信号操作填0;

       参数command:信号量操作的命令种类,常用的有两个:

       IPC_RMID:销毁信号量,不需要第四个参数;

       SETVAL:用来把信号量初始化为一个已知的值,信号量成功创建后,需要设置初始值,这个值由第四个参数决定,通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。第四参数是一个自定义的共同体,如下:

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

       semctl()函数可能有3个参数,也可能有4个参数,参数的个数由cmd决定,第4个参数为联合体。

int semctl(int semid,int sem_num,int command,union semun);

       返回值:调用失败返回-1;成功返回0和其他代表不同含义的非负值。

       示例用法:

       ①销毁信号量

semctl(semid,0,IPC_RMID);

       ②初始化信号量的值为1,信号量可用。

union semun sem_union;
sem_union.val = 1;
semctl(semid,0,SETVAL,sem_union);

2.3 信号量应用实例

       虽然上述函数调用看似很复杂,但是我们可以用这些接口函数来创建一些简单接口用于分别实现P、V操组以及设置信号量、删除信号量,然后用这些简单的接口来进行信号量相关操作。

       进程1创建信号量并初始化为1使之可用,然后进行P操作,若获得锁后休眠10s再进行V操作释放锁,代码如下:

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

union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};
int set_semvalue(int,int);
void del_sem(int);
int sem_p(int);
int sem_v(int);

int main()
{
    int sem_id;

    printf("process 1 running\n");
    if(sem_id = semget((key_t)0x1111,1,0640|IPC_CREAT) == -1)       //创建信号量
    {
        printf("process 1 semget error\n");
        return -1;
    }
    if(!set_semvalue(sem_id,1))     //初始化信号量使其可用
    {
        printf("process 1 failed to init semaphore\n");
		exit(EXIT_FAILURE);
    }
    printf("process 1 P 操作\n");
	if(!sem_p(sem_id))      //P操作
	{
		printf("process 1 sem_p error\n");
		return -1;
	}
	printf("process 1 get semaphore:持有锁\n");

	int i = 0;
	for(i=0;i<10;i++)
	{
		sleep(1);
		printf("process 1 sleep %d s\n",i+1);
	}
    printf("process 1 V 操作\n");
    if(!sem_v(sem_id))
    {
        printf("process 1 sem_v error\n");
        return -1;
    }
    printf("process 1 put semaphore:释放锁\n");
    
    return 0;
}

//用于初始化信号量,在使用信号量前必须这样做
int set_semvalue(int semid,int init_val)
{
	union semun sem_union;

	sem_union.val = init_val;
	if (semctl(semid, 0, SETVAL, sem_union) == -1)
		return 0;
	return 1;
}

//删除信号量
void del_sem(int semid)
{
	union semun sem_union;

	if (semctl(semid, 0, IPC_RMID, sem_union) == -1)
		fprintf(stderr, "Failed to delete semaphore\n");
}

//对信号量做减1操作,即等待—P操作
int sem_p(int semid)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;
	sem_b.sem_flg = SEM_UNDO;
	if (semop(semid, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_p failed\n");
		return 0;
	}
	// while (semop(semid, &sem_b, 1) == -1)
	// 	if (errno != EINTR)
	// 		return 0;
	return 1;
}

//这是一个释放操作,它使信号量变为可用,即发送信号—V操作
int sem_v(int semid)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;
	sem_b.sem_flg = SEM_UNDO;
	if (semop(semid, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_v failed\n");
		return 0;
	}
	return 1;
}

       进程2获取信号量后同样进行P操作,获得锁后再释放锁,最后删除信号量,代码如下:

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

union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};
int set_semvalue(int,int);
void del_sem(int);
int sem_p(int);
int sem_v(int);

int main()
{
    int sem_id;

    printf("process 2 running\n");
    if(sem_id = semget((key_t)0x1111,1,0640|IPC_CREAT) == -1)       //获取信号量
    {
        printf("process 2 semget error\n");
        return -1;
    }
    sleep(1);
    system("ipcs -s");
    printf("process 2 P 操作\n");
    if(!sem_p(sem_id))      //P操作
    {
        printf("process 2 sem_p error\n");
    }
    printf("process 2 get semaphore:持有锁\n");
    sleep(1);
    printf("process 2 V 操作\n");
    if(!sem_v(sem_id))
    {
        printf("process 2 sem_v error\n");
        return -1;
    }
    printf("process 2 put semaphore:释放锁\n");
    del_sem(sem_id);
    printf("process 2 stop running,删除信号量\n");
    system("ipcs -s");
    
    return 0;
}

//用于初始化信号量,在使用信号量前必须这样做
int set_semvalue(int semid,int init_val)
{
	union semun sem_union;

	sem_union.val = init_val;
	if (semctl(semid, 0, SETVAL, sem_union) == -1)
		return 0;
	return 1;
}

//删除信号量
void del_sem(int semid)
{
	union semun sem_union;

	if (semctl(semid, 0, IPC_RMID, sem_union) == -1)
		fprintf(stderr, "Failed to delete semaphore\n");
}

//对信号量做减1操作,即等待—P操作
int sem_p(int semid)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;
	sem_b.sem_flg = SEM_UNDO;
	if (semop(semid, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_p failed\n");
		return 0;
	}
	// while (semop(semid, &sem_b, 1) == -1)
	// 	if (errno != EINTR)
	// 		return 0;
	return 1;
}

//这是一个释放操作,它使信号量变为可用,即发送信号—V操作
int sem_v(int semid)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;
	sem_b.sem_flg = SEM_UNDO;
	if (semop(semid, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_v failed\n");
		return 0;
	}
	return 1;
}

       运行结果如下:

信号量测试例程运行结果

       从运行结果可以看到在进程1休眠10s过程中进程2已经执行P操作,但由于此时信号量的值为0,这导致进程2阻塞,直至进程1进行V操作释放锁后,进程2等到了信号量的值变为1获得锁后才得以执行后续代码。

我看到过下面这种写法:

while (semop(semid, &sem_b, 1) == -1)

这应该是多此一举的行为,P操作如果没有获得锁会阻塞进程,等待锁释放可用,所以不需要循环进行P操作来强行人为阻塞程序的运行。

三、共享内存结合信号量应用实例

3.1 例程及运行结果

       共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的,但是共享内存没有提供同步的机制,因此它往往与其它通信机制,比如结合信号量来使用,以实现进程间的同步和通信。

       上文共享内存应用实例中进程1向共享内存写入数据操作没有完成,进程2就开始从共享内存读取数据,导致前几次读取的结果都不是完整的,这是因为共享内存没有提供同步机制。掌握信号量有关知识以后,很容易就能够针对上文共享内存例程进行改进,引入信号量来实现对共享内存的互斥访问。

写端例程:

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


union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};
int set_semvalue(int,int);
void del_sem(int);
int sem_p(int);
int sem_v(int);

int main()
{
    int i = 0;
    // int sec;
    key_t key;
    int shmid;
    int sem_id;
    char *shaddr = NULL;

    printf("process 1 running\n");
    srand((unsigned int)time(NULL));        //生成随机数种子
    key = ftok(".",27);
    //创建共享内存
    if(shmid = shmget(key,128, IPC_CREAT|0666) == -1)
    {
        printf("process 1 shmget error\n");
        return -1;
    }
    //连接共享内存
    shaddr = (char*)shmat(shmid, NULL, 0);
	if((int)shaddr == -1)
	{
	    printf("process 1 shmat error\n");
        return -1;
	}
    system("ipcs -m");      //查看共享内存
    if(sem_id = semget((key_t)0x1111,1,0640|IPC_CREAT) == -1)       //创建信号量
    {
        printf("process 1 semget error\n");
        return -1;
    }
    if(!set_semvalue(sem_id,1))     //初始化信号量使其可用
    {
        printf("process 1 failed to init semaphore\n");
		exit(EXIT_FAILURE);
    }
    system("ipcs -s");      //查看信号量
    printf("process 1 P 操作\n");
    if(!sem_p(sem_id))      //P操作
	{
		printf("process 1 sem_p error\n");
		return -1;
	}
	printf("process 1 get semaphore:持有锁,开始往共享内存写数据\n");
    for(i=0;i<10;i++)
    {
        *(shaddr+i)='A'+i;
        printf("process 1 向共享内存写入数据: %c\n",*(shaddr+i));           
        sleep(1);
    }
    printf("process 1 向共享内存写入数据完毕,V 操作\n");
    for(i=0;i<10;i++)
	{
		sleep(1);
		printf("process 1 sleep %d s\n",i+1);
	}
    if(!sem_v(sem_id))
    {
        printf("process 1 sem_v error\n");
        return -1;
    }
    printf("process 1 put semaphore:释放锁\n");
    //共享内存和进程分离
    if(shmdt(shaddr) == -1)
    {
        printf("process 1 shmdt error\n");
        return -1;
    }
    printf("process 1 stop running,解除共享内存映射\n");
    system("ipcs -m");      //查看共享内存

    return 0;
}

//用于初始化信号量,在使用信号量前必须这样做
int set_semvalue(int semid,int init_val)
{
	union semun sem_union;

	sem_union.val = init_val;
	if (semctl(semid, 0, SETVAL, sem_union) == -1)
		return 0;
	return 1;
}

//删除信号量
void del_sem(int semid)
{
	union semun sem_union;

	if (semctl(semid, 0, IPC_RMID, sem_union) == -1)
		fprintf(stderr, "Failed to delete semaphore\n");
}

//对信号量做减1操作,即等待—P操作
int sem_p(int semid)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;
	sem_b.sem_flg = SEM_UNDO;
	if (semop(semid, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_p failed\n");
		return 0;
	}
	// while (semop(semid, &sem_b, 1) == -1)
	// 	if (errno != EINTR)
	// 		return 0;
	return 1;
}

//这是一个释放操作,它使信号量变为可用,即发送信号—V操作
int sem_v(int semid)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;
	sem_b.sem_flg = SEM_UNDO;
	if (semop(semid, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_v failed\n");
		return 0;
	}
	return 1;
}

读端例程:

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


union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};
int set_semvalue(int,int);
void del_sem(int);
int sem_p(int);
int sem_v(int);

int main()
{
    int i = 0;
    key_t key;
    int shmid;
    int sem_id;
    char *shaddr = NULL;

    printf("process 2 running\n");
    key = ftok(".",27);
    //创建共享内存
    if(shmid = shmget(key,128, IPC_CREAT|0666) == -1)
    {
        printf("process 2 shmget error\n");
        return -1;
    }
    sleep(1);
    //连接共享内存
    shaddr = (char*)shmat(shmid, NULL, 0);
	if((int)shaddr == -1)
	{
	    printf("process 2 shmat error\n");
        return -1;
	}
    system("ipcs -m");      //查看共享内存
    if(sem_id = semget((key_t)0x1111,1,0640|IPC_CREAT) == -1)       //获取信号量
    {
        printf("process 2 semget error\n");
        return -1;
    }
    sleep(1);
    printf("process 2 P 操作\n");
    if(!sem_p(sem_id))      //P操作
    {
        printf("process 2 sem_p error\n");
        return -1;
    }
	printf("process 2 get semaphore:持有锁,开始从共享内存读数据\n");
    printf("process 2 从共享内存读数据:%s\n",shaddr);
    printf("process 2 从享内存读数据完毕,V 操作\n");
    if(!sem_v(sem_id))
    {
        printf("process 1 sem_v error\n");
        return -1;
    }
    printf("process 2 put semaphore:释放锁\n");
    sleep(20);
     //共享内存和进程分离
    if(shmdt(shaddr) == -1)
    {
        printf("process 2 shmdt error\n");
        return -1;
    }
    printf("process 2 解除共享内存映射\n");
    //删除共享内存
    if(shmctl(shmid,IPC_RMID,NULL)==-1)
    {
        printf("process 2 shmctl error\n");
        return 1;
    }
    del_sem(sem_id);
    printf("process 2 stop running,删除共享内存和信号量\n");
    system("ipcs -m");
    system("ipcs -s");

    return 0;
}

//用于初始化信号量,在使用信号量前必须这样做
int set_semvalue(int semid,int init_val)
{
	union semun sem_union;

	sem_union.val = init_val;
	if (semctl(semid, 0, SETVAL, sem_union) == -1)
		return 0;
	return 1;
}

//删除信号量
void del_sem(int semid)
{
	union semun sem_union;

	if (semctl(semid, 0, IPC_RMID, sem_union) == -1)
		fprintf(stderr, "Failed to delete semaphore\n");
}

//对信号量做减1操作,即等待—P操作
int sem_p(int semid)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;
	sem_b.sem_flg = SEM_UNDO;
	if (semop(semid, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_p failed\n");
		return 0;
	}
	// while (semop(semid, &sem_b, 1) == -1)
	// 	if (errno != EINTR)
	// 		return 0;
	return 1;
}

//这是一个释放操作,它使信号量变为可用,即发送信号—V操作
int sem_v(int semid)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;
	sem_b.sem_flg = SEM_UNDO;
	if (semop(semid, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_v failed\n");
		return 0;
	}
	return 1;
}

运行结果:

共享内存+信号量例程运行结果

       此例程成功实现了进程1在进行向共享内存写数据操作时,不论休眠多久只要没有V操作释放锁,进程2就一直阻塞不能从共享内存中读数据,直至进程1进行V操作释放锁后才读取到了完整的字符串。

3.2 浅谈其他进程通信方式

进程通信的方式有:

        1)无名管道(pipe)及有名管道(named pipe):无名管道可用于具有父进程和子进程之间的通信。有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

        2)信号(signal):信号用于通知接收进程有某种事件发生

        3)消息队列(message):消息队列是消息的链接表,进程可以向队列中添加消息,其它的进程则可以读走队列中的消息。

        4)共享内存:使得多个进程可以访问同一块内存空间。

        5)信号量(semaphore):主要作为进程之间对共享资源加锁的手段。

        6)套接字(socket):可用于不同机器之间的进程间通信,说白了就是网络通信。

       其中管道和消息队列已经没什么应用价值,了解即可,其他都是非常重要且常见的,尤其是信号和socket

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值