标签:
在前面,我们学习了传统的进程间通信方式——无名管道(pipe)、有名管道(fifo)和信号(signal)。
下面我们来学习 System V IPC 对象:
1、共享内存(share memory);
2、信号灯(semaohore);
3、消息队列(message queue);
IPC对象是活动在内核级别的一种进程间通信的工具。存在的IPC对象通过它的标识符来引用和访问,这个标识符是一个非负整数,它唯一的标识了一个IPC对象,这个IPC对象可以是消息队列或信号量或共享存储器中的任意一种类型。
Linux系统中标识符被声明成整数,所以可能存在的最大标识符为65535。当时对于IPC对象,其标识符(id)与文件描述符有所不同,使用open函数打开一个文件时,返回的文件描述符的值为当前进程最小可用的文件描述符数组的下标。IPC对象是系统全局的流水号,其删除或创建时相应的标识符的值会不断增加到最大的值,归零循环分配使用。
IPC的标识符只解决了内部访问一个IPC对象的问题,如何让多个进程都访问某一个特定的IPC对象还需要一个外部键(key),一个IPC对象都与一个键相关联。这样就解决了多进程在一个IPC对象上汇合的问题。IPC对象时需要指定一个键值,类型为key_t,在<sys/types.h>中定义为一个长整型。键值到标识符的转换是由系统内核来维护的,这里调用IPC对象的创建函数(semget msgget shmget )实现key 值到 id 的转换。
从上图中我们可以看到得到这个键值 key 有两种方法:
1)通用方法:调用ftok()函数
函数ftok可以使用两个参数生成一个键值,函数原型如下:
函数中参数path是一个文件名。函数中进行的操作是,取该文件的stat结构的st_dev成员和st_ino成员的部分值,然后与参数ID的第八位结合起来生成一个键值。由于只是使用st_dew和st_ino的部分值,所以会丢失信息,不排除两个不同文件使用同一个ID,得到同样键值的情况。
系统为每一个IPC对象保存一个ipc_perm结构体,该结构说明了IPC对象的权限和所有者,每一个版本的内核各有不用的ipc_perm结构成员。
文件<sys/ipc.h> 中对其定义:
2)父子进程之间:
Key 为IPC_PRIVATE,父子进程之间key值为IPC_PRIVATE。
当有了一个IPC对象的键值,如何让多个进程知道这个键,可以有多种实现的办法:
1) 、使用文件来做中间的通道,创建IPC对象进程,使用键IPC_PRIVATE成功建立IPC对象之后,将返回的标识符存储在一个文件中。其他进程通过读取这个标识符来引用IPC对象通信。
2)、定义一个多个进程都认可的键,每个进程使用这个键来引用IPC对象,值得注意的是,创建IPC对象的进程中,创建IPC对象时如果该键值已经与一个IPC对象结合,则应该删除该IPC对象,再创建一个新的IPC对象。
3)、多进程通信中,对于指定键引用一个IPC对象而言,可能不具有拓展性,并且在该键值已经被一个IPC对象结合的情况下。所以必须删除这个存在对象之后再建立一个新的。这有可能影响到其他正在使用这个对象的进程。函数ftok可以在一定程度上解决这个问题。
但IPC对象存在一些问题,主要集中在以下几点:
1)、过于繁杂的编程接口,比起使用其他通信方式,IPC所要求的代码量要明显增多。
2)、IPC不使用通用的文件系统,这也是饱受指责的原因。所以不能使用标准I/O操作函数来读写IPC对象。为此不得不新增加一些函数来支持必要的一些操作(例如msgget msgrev msgctl等)并且对于不同类型的IPC对象都有一系列特定的操作函数。由于IPC不使用文件描述符,所以不能使用多路I/O监控函数select及poll函数来操作IPC对象。
3)、缺少的资源回收机制。由于IPC对象在使用过程中并不保存引用计数,所以当出现一个进程创建了IPC对象然后退出时,则这个对象只有在出现后面几种情况才会被释放或者删除,即由某一个进程读出消息,或者IPC的所有者或超级用户删除了这个对象。这也是IPC相对于管道或FIFO所欠缺的资源回收机制。
下面是文件对象和IPC对象的对比
一、共享内存
共享内存可以说是Linux 下最快速、最有效的进程间通信方式。两个不同进程A 、B 共享内存的意思是,同一块物理内存被映射到进程A 、B 各自的进程地址空间,进程A 可以即时看到进程B 对共享内存中数据的更新;反之,进程B 也可以即时看到进程A对共享内存中数据的更新。
这里简单说下映射的概念:
Linux系统会为每个进程分配 4GB 的虚拟地址空间,一定情况下,需要将虚拟内存转换成物理内存,这就需要内存映射。为什么我们需要使用虚拟地址呢?最主要的原因是不同PC的物理内存会不一样,如果直接使用物理地址,会造成程序的移植性很差,另外虚拟地址访问内存有以下优势:
1、程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
2、程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
3、不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
进程可用的虚拟地址范围称为该进程的“虚拟地址空间”。每个用户模式进程都有其各自的专用虚拟地址空间。对于 32 位进程,虚拟地址空间通常为 4 GB,范围从 0x00000000 至 0xFFFFFFFF。
1、共享内存的概念
共享内存从字面意义解释就是多个进程可以把一段内存映射到自己的进程空间,以此来实现数据的共享及传输,这也是所有进程间通信方式最快的一种,共享内存是存在于内核级别的一种资源。
在Shell 中可以使用ipcs 命令查看当前系统IPC中的状态,在文件系统中/proc目录下有对其描述的相应文件
ipcs -m ,其中 -m 是 memory 的意思 。
在系统内核为一个进程分配内存地址时,通过分页机制可以让一个进程的物理地址不连续,同时也可以让一段内存同时分配给不同的进程。共享内存机制就是通过该原理实现的,共享内存机制只是提供数据的传送,如何控制服务器端和客户端的读写操作互斥,这就需要一些其他的辅助工具,例如信号量。
采用共享内存通信的一个显而易见的好处就是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户控件进行四次数据的拷贝,而共享内存只拷贝两次数据:一次从输入文件到共享区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,知道通信完毕为止,这样,数据内同一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在接触映射时才写回文件的。因此,采用共享内存的通信方式效率是最高的。
共享内存最大不足之处在意,由于多个进程对同一块内存区域具有访问的权限,各个进程之间的同步问题显得尤为重要。必须控制同一时刻只有一个进程对共享内存区域写入数据,否则会造成数据的混乱。同步控制问题可以由信号量来解决;
对于每一个共享存储段,内核会为其维护一个shmid_ds类型的结构体,其定义在头文件<sys/shm.h>中,其定义如下:
2、共享内存的相关操作
1)创建或打开共享内存
要使用共享内存,首先要创建一个共享内存区域,创建共享内存的函数调用如下:
所需头文件 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> |
函数原型 | int shmget(key_t, int size ,int shmflg ); |
函数参数 |
Key:IPC_PRIVATE 或 ftok 的返回值 size:共享内存区大小 shmflag :同open函数的权限位,也可用8进制表示法 |
函数返回值 |
成功:共享内存段标识符 出错:-1 |
shmget函数除了可以用于创建一个新的共享内存外,也可以用于打开一个已存在的共享内存。其中,参数key表示索要创建或打开的共享内存的键值。size表示共享内存区域的大小,只在创建一个新的共享内存时生效。参数shmflag 表示调用函数的操作类型,也可用于设置共享内存的访问权限,两者通过逻辑或表示.参数key 和 flag 决定了调用函数 shmget 的作用,相应的约定如下:
1)当 key 为 IPC_PRIVATE 时,创建一个新的共享内存,此时参数 flag 的取值无效;
2)当 key 不为 IPC_PRIVATE时,且flag 设置了IPC_CREAT 位,而没有设置 IPC_EXCL 位,则执行操作由key取值决定。如果key 为内核中每个已存在的共享内存的键值。则执行打开这个键的操作;反之,则执行创建共享内存的操作;
3)当 key 不为 IPC_PRIVATE时,且flag 设置了IPC_CREAT 位和 IPC_EXCL 位,则只执行创建共享内存的操作。参数key的取值应与内核中已存在的任何共享内存的键值都不相同,否则函数调用失败,返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开共享内存的函数就可以了(即将flag 设置为 IPC_CREAT,而不设置IPC_EXCL);
2)附加
当一个共享内存创建或打开后,某个进程如果要使用该共享内存则必须将此内存区附加到它的地址空间,附加操作的系统调用函数如下:
所需头文件 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> |
函数原型 | void *shmat (int shmid, const void *shaddr, int shmflg); |
函数参数 |
shmid :要映射的共享内存区标示符 shmaddr:将共享内存映射到指定地址(若为NULL,则表示由系统自动完成映射) shmflg:默认0:共享内存只读 |
函数返回值 |
成功:映射后的地址 出错:-1 |
参数shmid 指定要引入的共享内存,参数 addr 和 flag 组合说明要引入的地址值,通常将 shmaddr 设置为NULL ,shmflag为0;
3)分离
当进程对共享内存段的操作完成后,应调用 shmdt 函数,作用是将指定的共享内存段从当前进程空间中脱离出去,函数原型如下:
所需头文件 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> |
函数原型 | int shmdt(const void *shmaddr); |
函数参数 |
shmaddr:共享内存映射后的地址 |
函数返回值 |
成功:0 出错:-1 |
此函数仅用于将共享内存区域与进程的地址空间分离,并不删除共享内存本身。参数addr是调用 shmat 函数时的返回值。
4)共享内存的控制
由于共享内存这一特殊的资源类型,使它不同于普通的文件,因此,系统需要为其提供专有的操作函数,其函数原型如下:
所需头文件 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> |
函数原型 | int shmctl(int shmid, int cmd, struct shmid_ds *buf); |
函数参数 |
shmid:要操作的共享内存标示符 cmd: IPC_STAT (获取对象属性) IPC_SET(设置对象属性) IPC_RMID(删除对象) buf:指定IPC_STAT/ IPC_SET 时用以保存/设置属性 |
函数返回值 |
成功:0 出错:-1 |
下面是一个实例,两个进程间实现共享内存进行通信:
执行结果如下:
通过结果,不断的调用 system ,可以看到共享内存区的变化;
二、信号量
信号灯(semaphore),也叫信号量,它是不同进程间或一个给定进程内部不同线程间同步的机制。
信号灯种类:1)posix 有名信号灯 2)posix 基于内存的信号灯(无名信号灯) 3)System V 信号灯 (IPC对象);
信号灯:
1)二值信号灯:值为 0 或 1。与互斥锁类似,资源可用时值为1,不可用时值为 0;
2)计数信号灯:值在 0 到 n 之间。用来统计资源,其值代表可用资源数;
等待操作时等待信号灯的值变为大于0,然后将其减一;而释放操作则相反,用来唤醒等待资源的进程或者线程;
事实上,在信号量的实际应用中,是不能单独定义一个信号量的,而只能定义一个信号量集,其中包含一组信号量,同一信号量集中的信号量使用同一个引用ID,这样的设置是为了多个资源和同步操作的需要。每个信号量集都有一个与之对应结构,一种记录了信号量集的各种信息,该结构的定义如下:
sem结构记录一个信号量的信息,其定义如下:
下面是信号量操作有关的函数调用:
函数说明:
在Linux 系统中,使用信号量通常分为以下几个步骤:
1)创建信号量或获得系统已存在的信号量,此时需要调用 semget() 函数。不同进程通过使用同一个信号键值来获得同一个信号量;
2)初始化信号量,此时使用 senctl() 函数的 SETVAL 操作。当使用二维信号量时,通常将信号量初始化为1;
3)进行信号量的PV操作,此时调用 semop() 函数。这一步是实现进程之间的同步和互斥的核心工作部分;
4)如果不需要信号量,则从系统中删除它,此时使用semctl() 函数的IPC_RMID 操作。此时需要注意,在程序中不应该出现对已经被删除的信号量的操作;
下面是具体说明:
1、创建或打开信号量集
使用函数 semget 可以创建或者获得一个信号量集ID,函数原型如下:
所需头文件 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
函数原型 | int semget(key_t key, int nsems, int semflg ); |
函数参数 |
key :和信号灯集关联的key 值 nsems:信号灯集中包含的信号灯数目 semflg:信号灯集的访问权限,通常为IPC_CREAT|0666 |
函数返回值 |
成功:信号灯集ID 出错:-1 |
此函数可以用于创建或打开一个信号量集。其中,参数key 表示要创建或打开的信号量集对于的键值。参数 nsems 表示创建的信号量集中包含的信号量的个数,此参数只在创建一个新的信号量集时有效。参数flag表示调用函数的操作类型,也可以用于设置信号量集的访问权限,两者通过逻辑或表示。调用函数semget 的作用由参数key和flag 决定。
另外,当semget 成功创建一个新的信号量集时,它相应的semid_ds结构被初始化。ipc_perm 结构中成员被设置成相应的值 ,sem_nsems设置为函数参数nsems的值,sem_otime被设置为0,sem_ctime 设置为系统当前时间。
2、对信号量集的操作
函数semop 用以操作一个信号量集,函数原型如下:
所需头文件 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
函数原型 | int semop(int semid,struct sembuf *opsptr,size_t nops); |
函数参数 |
semid:信号灯集ID struct sembuf 结构体每一个元素表示一个操作; nops:要操作的信号灯的个数 |
函数返回值 |
成功:0 出错:-1 |
结构体sembuf 用来说明所要执行的操作,其定义如下:
3、信号量集的控制
和共享内存的控制一样,信号量集也有自己的专属控制函数 semctl ,函数原型如下:
所需头文件 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
函数原型 | int semctl(int semid, int semnum, int cmd, union semun arg); |
函数参数 |
semid:信号灯集ID semnum:要修改的信号灯编号 cmd :GETVAL:获取信号灯的值 SETVAL:设置信号灯的值 IPC_RMID:从系统中删除信号灯集合 |
函数返回值 |
成功:0 出错:-1 |
参数cmd 定义函数所要进行的操作,其取值及表达的意义与参数arg 的设置有关,最后一个参数arg 是一个联合体(union),其定义如下:
下面是个应用实例:
执行结果如下:
标签:
原文地址:http://blog.csdn.net/qq_21593899/article/details/51711365