IPC:进程间通信(Inter Process Communication)的缩写
** System V IPC 通信机制下,所有创建的共享内存、消息队列、信号灯集,即使创建好暂时不用,共享内存、消息队列、信号灯集仍会一直存在于内核中,因此在不使用时,要及时删除共享内存、消息队列、信号灯集,以防止内存泄漏。**
IPC对象是基于系统内核,实现进程间通信的桥梁(通道)。
共享内存(share memory)
消息队列(message queque)
信号灯集(semaphore set)
IPC对象基于不同的类型,都有唯一的标识符ID(shmid、msqid、semid)来引用和访问每个IPC对象。IPC对象标识符是IPC对象的内部名字(标识符),用一个IPC标示符来标识和引用一个IPC对象。
IPC的标识符只解决了内部访问一个IPC对象的问题,如何让多个进程都访问某一个特定的IPC对象还需要一个外部键(key),每一个IPC对象都与一个键相关联。这样就解决了多进程在一个IPC对象上汇合的问题。
创建一个IPC对象时需要指定一个键值,键值到标识符的转换是由系统内核来维护的。
(1)ftok创建key函数(file to key)
#include
#include
key_t ftok(const char *pathname, int proj_id);
(2)返回值
成功:返回合法的key值
失败: 返回 -1
(3)参数
1)pathname:文件/目录的路径。文件/目录路径需保持不可改变,不可更改位置和不可删除重建。
i节点号:在linux系统下,文件、目录都有一个节点号且是唯一的标识。(用 ls - i:查看索引节点号)
2)proj_id:id是子序号,虽然是int类型,但是只使用8bits(取值范围1-255)。
(4)key键值生成的规则
key键值生成的规则:key是根据文件/目录的i节点号和proj_id的子序号,取该文件的stat结构的st_dev成员和st_ino成员的部分值,然后与参数proj_id的低八位结合起来生成一个键值。
系统为每一个IPC对象都保存一个ipc_perm结构体,该结构说明了IPC对象的权限和所有者,每一个版本的内核各有不用的ipc_perm结构成员。
查看详细的定义参阅<sys/ipc.h>:
struct ipc_perm {
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
unsigned short mode;
unsigned short seq;
};
当调用IPC对象的创建函数(shmget msgget semget )时,会根据ftok生成的key 去创建一个IPC对象,并返回IPC标识符(shmid、msqid、semid),并会对ipc_perm结构的每一个域赋值。
(1)ipcs (-a):查看IPC对象
linux@ubuntu:~$ ipcs
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 65536 linux 777 6920592 2 dest
0x00000000 32769 linux 777 183936 2 dest
0x00000000 98306 linux 777 27852 2 dest
0x00000000 131075 linux 777 19272 2 dest
0x00000000 163844 linux 777 20856 2 dest
------ Semaphore Arrays --------
key semid owner perms nsems
------ Message Queues --------
key msqid owner perms used-bytes messages
ipcs输出的信息中的key 以及shmid,key标识的是IPC对象的外键,shmid标识的IPC对象的标识符。owner标识的是IPC所属的用户,perms标识权限。
(2)ipcrm:删除ipc对象
ipcrm [ [-q msqid] [-m shmid] [-s semid]
[-Q msgkey] [-M shmkey] [-S semkey] ... ]
ipcrm -m shmid号/rm -M key值:删除共享内存
ipcrm -q msqid号/rm -Q key值:删除消息队列
ipcrm -s semid号/rm -S key值:删除信号灯
(3)ipcs -l:查看每块共享内存、消息队列、信号灯集的大小
ipcs -l
或 cat /proc/sys/kernel/shmmax
linux@ubuntu:~/test$ ipcs -l
------ Shared Memory Limits --------
max number of segments = 4096
max seg size (kbytes) = 32768
max total shared memory (kbytes) = 8388608
min seg size (bytes) = 1
------ Semaphore Limits --------
max number of arrays = 128
max semaphores per array = 250
max semaphores system wide = 32000
max ops per semop call = 32
semaphore max value = 32767
------ Messages Limits --------
max queues system wide = 988
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384
注意:(1)当共享内存删除时,shmctl(shmid, IPC_RMID, NULL);只有当 nattch的值为 0 时,没有attach的进程时,才是完全删除共享内存。
(1)共享内存允许系统内两个或多个进程共享同一块内存空间,并且数据不用在客户进程和服务器进程间复制,因此共享内存是通信速度最快的一种IPC。
(2)共享内存在内核空间创建,可被进程由内核映射到用户空间访问,使用灵活
(3)由于多个进程可同时访问共享内存,因此共享内存需要同步和互斥机制配合使用。
(4)共享内存创建之后,一直存在于内核中,直到被删除或系统关闭;
(5)共享内存和管道不一样,读取后,内容仍在其共享内存中。
(6)共享内存不用时,一定要释放,防止内存泄漏。
共享内存的机制:一个进程在系统中申请开辟了一块共享内存空间,然后使用这个共享内存空间的各个进程分别打开这个共享内存空间,并将这个内存空间映射到自己的进程空间上,这样各个进程就可以共同使用这个共享内存空间,就如同使用自己进程地址空间的内存一样。
要实现共享内存空间,内核做了许多工作:比如给每个共享内存块分发一个“身份证”、允许用户进程将共享内存映射到各自的地址空间上、在进程提出申请以后将共享内存和进程地址空间脱离,并在适当的时候讲共享内存删除,让其回到可以被创建的状态。
(1)创建/打开共享内存
(2)映射共享内存
(3)读写共享内存
(4)撤销共享内存映射
(5)删除共享内存对象
用户利用共享内存实现进程间的通信,实际上就是使用内核提供的API完成对共享内存的
建立shmget、映射shmat、断开shmdt、删除shmctl等。当建立并映射成功以后,进程间就能通过共享内存实现数据的交互。
共享内存函数由shmget、shmat、shmdt、shmctl四个函数组成.
函数功能:创建或者打开一个键值是key的共享内存。
#include
#include
int shmget(key_t key, size_t size, int shmflg);
参数:
(1)key:两种形式
IPC_PRIVATE:创建一个私有的共享内存。PC_PRIVATE操作时,共享内存的key值都一样,都是0。
key键值(ftok生成key):创建一个和key关联的共享内存
(2)size:创建共享内存的大小
(3)shmflg :共享内存标志位,一般写:IPC_CREAT|0666
返回值:
RETURN VALUE
A valid segment identifier, shmid, is returned on success, -1 on error.
成功:返回共享内存的shmid
失败:返回 -1
注:使用ftok来创建key值,只要key值是一样的,用户空间的进程通过这个函数打开,则会对内核的同一个IPC对象操作。
映射共享内存是把指定的共享内存映射到进程的用户空间地址上,用于访问。即连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,之后可像本地空间一样访问映射后的共享内存。
void *shmat(int shmid, const void *shmaddr, int shmflg);
一般用法:
shmat(shmid, NULL, 0);
参数:
1)shmid:要映射的共享内存对象
2)shmaddr:指定共享内存映射在进程用户内存空间的位置,
默认NULL:让系统内核自动决定合适的内存地址位置。
3)shmflg(映射标志位):
默认为0:表示共享内存可读写
SHM_RDONLY:表示共享内存只读
返回值:
RETURN VALUE
On success shmat() returns the address of the attached shared memory segment;
on error (void *) -1 is returned, and errno is set to indicate the cause of the error.
成功:返回共享内存的映射地址shm_addr.
失败:(void *)-1
shmdt函数功能:断开用户进程与共享内存连接,结束此进程访问此片共享内存。
int shmdt(const void *shmaddr);
参数:
shmaddr:连接的共享内存映射后的首地址
返回值:
RETURN VALUE
On success shmdt() returns 0; on error -1 is returned,
and errno is set to indicate the cause of the error.
成功:0
失败:-1
注:
(1)调用打开同一块共享内存的各个进程,但是会映射到各自的用户空间,各个进程的用户空间哪里空闲着,系统就会随机映射到哪里,所以说:对于同一块共享内存,不同进程的用户空间下映射的地址是不一样,这是一个很正常的现象。
(2)不使用共享内存时应撤销(断开)映射, 进程结束时自动撤销。
shmctl()函数功能:执行cmd指定的控制操作来管理共享内存,包括:删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
一般用法:删除共享内存
shmctl(shmid,IPC_RMID, NULL);
参数:
(1)shmid:要管理的共享内存对象
(2)cmd:要执行的操作
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中。
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode
复制到共享内存的shmid_ds结构内。
IPC_RMID:删除这片共享内存
(3)buf:保存或设置共享内存属性的地址;一般设置为空 NULL。
buf是指向shmid_ds结构的指针,该结构在
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
返回值:
成功:0
出错:-1
错误原因存于error中:
EACCESS:参数cmd为IPC_STAT,无权限读取该共享内存
EFAULT:参数buf指向无效的内存地址
EIDRM:标识符为shmid的共享内存已被删除
EINVAL:无效的参数cmd或shmid
EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
“消息队列”是在消息的传输过程中保存消息的容器,先进先出。
(1)“消息”是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本“字符串”;也可以更复杂,可能包含嵌入对象。
(2)消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。“消息队列管理器”将消息从它的源中寄送到它的目标时充当中间桥梁。队列的主要作用是提供路由,并保证消息的传递。如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。
(1)消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的。
(2)消息队列是随内核持续的,只有在内核重起或者显示删除一个消息队列时,该消息队列才会真正被删除。因此系统中记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核中,系统中的所有消息队列都可以在结构msg_ids中找到访问入口。
(3)消息队列就是一个消息的链表。每个消息队列都有一个队列头,用结构struct msg_queue来描述。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID。用户可以访问这些信息,也可以设置其中的某些信息。
(1)消息队列也是System V IPC对象的一种类型;
(2)消息队列由消息队列ID:msqid来唯一标识
(3)消息队列就是一个消息的链表,用户可以在消息队列中添加消息、读取消息等
(4)消息队列可以按照类型来发送/接收消息
函数功能:创建一个新的或打开一个已经存在的消息队列MQ,此消息队列与ftok生成的key相对应。
SYNOPSIS
#include
#include
#include
int msgget(key_t key, int msgflg);
一般用法:
msqid = msgget(key, IPC_CREAT | 0666);
参数:
(1)key:两种形式
IPC_PRIVATE:创建一个私有的共享内存。PC_PRIVATE操作时,共享内存的key值都一样,都是0。
key键值(ftok生成key):创建一个和key关联的共享内存.
(2)msgflg:消息队列的建立标志和存取权限。
IPC_CREAT:如果内核中没有此队列,则创建它。
IPC_EXCL:当和IPC_CREAT一起使用时,如果队列已经存在,则失败返回-1。
注意:
(1)如果单独使用IPC_CREAT,则msgget()要么返回一个新创建的消息队列的标识符,要么返回具有相同关键字值的队列的标识符。
(2)如果IPC_EXCL和IPC_CREAT一起使用,则msgget()要么创建一个新的消息队列,要么如果队列已经存在则返回一个失败值-1。
(3)IPC_EXCL单独使用是没有用处的。
返回值:
成功:返回消息队列ID:msqid
失败:-1 并设置errno
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
一般用法:
#define MSGSIXE (sizeof(struct msgbuf_t) - sizeof(long))
struct msgbuf_t msgbuf;
msgsnd(msqid, &msgbuf, MSGSIZE, 0);
参数:
(1)msqid:msgget的返回值消息队列的ID,msqid
(2)msgp:指向消息结构体的缓冲区地址的指针,用来暂时存储发送和接收的消息。
消息结构体的缓冲区两部分组成:消息类型和消息正文。
消息结构体 struct msgbuf:
struct msgbuf
{
long mtype; 消息类型,必须 > 0; 信息队列结构体必须从消息类型作为首部
char mtext[1]; 消息正文,可改为自定义正文大小NUM:char text[NUM];
};
(3)msgsize:消息的正文长度(单位字节),注意:不包括消息缓冲区buf中的消息类型(long mtype)的长度。从总长度中减4字节。
(4)msgflg:控制函数行为的标志位,两种形式:
1)0:表示阻塞,调用msgsnd如果消息队列已经发送满了,则阻塞当前进程,直到消息发送成功。
2)IPC_NOWAIT:非阻塞,如果消息队列满了,则不阻塞,直接返回-1;如果消息队列为空,则返回一个ENOMSG,并将控制权交回调用函数的进程。
返回值:
成功: 0
失败:-1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsize, long msgtype, int msgflg);
一般用法:
msgrcv(msqid, &msgbuf, MSGSIZE, msgtype, 0);
参数:
1)msgtype:接收时,过滤筛选的消息类型。这个不是消息类型mtype,而是根据msgtype取值筛选mtype类型的消息。
while(1){msgrcv()}连续接收消息时:
msgtyp(为 0) = 0 :接收消息队列的第一个消息(接收所有的消息的第一个)。
msgtyp(正数) > 0 :接收类型为数值为msgtype = mtype的第一个消息。
msgtyp(负数) < 0 :接收类型为 mtype <= 绝对值|msgtype| 所有类型的消息。
2)msgflg:
(1)0:阻塞,表示msgrcv()函数阻塞直到接收到一条相应类型的消息为止。
(2)MSG_EXCEPT:设置为接收除了msgtype筛选的消息。一定要宏定义: #define MSG_EXCEPT 020000
(3)IPC_NOWAIT:非阻塞,若消息队列中没有相对应类型的消息可以接受,则立即返回。
(4)MSG_NOERROR:如果函数取得的消息长度大于msgsz,将只返回msgsz 长度的信息,剩下的部分被丢弃了。
返回值:
成功:返回实际从消息队列读出的字符的个数
失败:-1
一般msgrcv接收完消息,要使用 msgctl(msqid, IPC_RMID, NULL);移除内核中的消息队列。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
一般用法:删除消息队列
msgctl(msqid, IPC_RMID, NULL);
参数:
cmd:要执行的操作, IPC_RMID / IPC_STAT / IPC_SET
1)IPC_RMID:从系统内核中移除消息队列。
2)IPC_STAT:读取消息队列的数据结构msqid_ds,并将其存储在msgbuf指定的地址中。
3)IPC_SET:设置消息队列的数据结构msqid_ds中的ipc_perm元素的值。这个值取自msgbuf参数。
buf:存放消息队列属性的地址,一般写 NULL:表示默认不修改消息队列属性。
返回值:
成功:0
失败:- 1,并设置errno
程序:接收消息,并用signal函数ctrl + c 移除消息队列和结束进程。
1 #include <stdio.h>
2 #include <string.h>
3 #include <sys/types.h>
4 #include <sys/ipc.h>
5 #include <sys/msg.h>
6 #include <signal.h>
7
8 #define MSGSIZE (sizeof(struct msgbuf_t) - sizeof(long))//宏定义消息正文长度:msgsize
9 //#define MSG_EXCEPT 020000
10
11 typedef void(* sighandler_t)(int);//重定义一个sighandler_t函数指针类型
12
13 struct msgbuf_t
14 {
15 long mtype; /* message type, must be > 0 */
16 char mtext[32]; /* message data */
17 };
18
19
20 sighandler_t oldhandler;//oldhandler记录指定信号老的行为
21 int msqid;//消息队列标识符,此进程因为信号处理函数用到msqid,故定义成全局
22
23 void myhandler(int sig)
24 {
25 if(sig == SIGINT)
26 {
//signal(sig, oldhandler);//激活默认的SIGINT的行为
27 msgctl(msqid, IPC_RMID, NULL);//移除消息队列
28 printf("i have remove MQ\n");
29 signal(sig, oldhandler);//启动默认方式,结束进程
30 }
31 }
32
33 int main(int argc, const char *argv[])
34 {
35 key_t key;
36 struct msgbuf_t msgbuf;//定义一个消息队列缓冲区
37 int ret, ret_len;
38
39 key = ftok(".", 100);//生成ipc对象关联的key键值
40 if(key < 0)
41 {
42 perror("ftok fail");
43 return -1;
44 }
45
46 msqid = msgget(key, IPC_CREAT | 0666);//打开消息队列
47 if(msqid < 0)
48 {
49 perror("msgget fail");
50 return -1;
51 }
52
53 oldhandler = signal(SIGINT, myhandler);//向内核注册一个signal
54 if(oldhandler == SIG_ERR)//注意:signal的错误返回值是一个信号宏:SIG_ERR
55 {
56 perror("signal fail");
57 return -1;
58 }
59
60 while(1)//连续接收消息
61 {
62 ret_len = msgrcv(msqid, &msgbuf, MSGSIZE, 1, 0);//接收信息类型为1的消息
63 if(ret_len == -1)
64 {
65 perror("msgrcv fail");
66 return -1;
67 }
68
69 printf("msgtype = %d, msgbuf = %s\n", (int)msgbuf.mtype, msgbuf.mtext);
70 }
71
72 return 0;
73 }
linux@ubuntu:~/test$ ./rcv
msgtype = 2, msgbuf = hello world
msgtype = 2, msgbuf = how are you
^Ci have remove MQ
msgrcv fail: Interrupted system call
信号灯也叫信号量,用于进程/线程同步(顺序相关)或互斥(独占性)的机制
Posix 无名信号灯(线程:sem_init 、 sem_wait 、 sem_post)
Posix 有名信号灯
System V 信号灯
(1) System V 信号灯是一个或多个计数信号灯的集合
(2)同时可以操作信号灯集合中的多个信号灯
(3)在申请多个资源时,可以避免死锁
SYNOPSIS
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
一般用法:
semid = semget(key, 2(sem_num), IPC_CREAT | 0666);
RETURN VALUE
If successful, the return value will be the semaphore set identifier:semid,
otherwise -1 is returned, with errno indicating the error.
参数:
nsems:信号灯集合中包含的计数信号灯总个数。
semflg:信号灯集标志位:IPC_CREAT|0666 、IPC_EXCL
int semctl(int semid, int semnum, int cmd, union semun);
一般用法:
semctl(semid, READ(0) / WRITE(1), SETVAL, mysemun);
(一)参数:
(1)semnum:指定操作信号灯集中的信号灯:从 0 开始编号…即:第一个信号灯编号为0.
一般写成宏定义方式:
#define READ 0
#define WRITE 1
(2)cmd:执行的操作
SETVAL (初始化信号灯)
IPC_RMID (删除信号灯)
(3)union semun:union semun mysemun;
不同的信号灯编号根据需求设置不同的值:mysemun.val = 0或者1(设置信号灯的初值)
注意:对于semun联合体,最好自己定义,否则GCC编译器可能会报“semun大小未知”。
union semun
{
int val; 设置信号灯的初值(SETVAL初始化操作),一般写:0,1
struct semid_ds * buf; IPC_STAT、IPC_SET 使用缓存区
unsigned short * array; GETALL,、SETALL 使用的数组
struct seminfo * __buf; IPC_INFO(Linux特有) 缓存区
};
(二)返回值
成功: 0
失败:-1
RETURN VALUE
On failure semctl() returns -1 with errno indicating the error.
All other cmd values return 0 on success.
int semop(int semid, struct sembuf *sops, unsigned nsops);
一般用法:
struct sembuf sops;
sops.sem_num = sem_no;(信号灯编号)
sops.sem_op = -1;(-1:P操作,1:V操作)
sops.sem_flg = 0;(阻塞)
semop(semid, &sops, 1);
返回值:
成功:0
失败:-1,并设置errno
(一)参数
(1) struct sembuf *sops:信号灯的操作属性结构体
sops.sem_num = sem_no;
sops.sem_op = -1;
sops.sem_flg = 0;
struct sembuf
{
unsigned short sem_num; 信号灯编号:semid
short sem_op; -1:P操作 1:V操作
short sem_flg; 0 (阻塞等待) / IPC_NOWAIT(不阻塞)
};
(2)unsigned nsops:要操作的信号灯的个数,一般写1:操作一个信号灯。
int semctl(int semid, int semnum, int cmd, union semun);
一般用法:
semctl(semid, semnum, IPC_RMID, semun);
程序:两个进程通过共享内存,利用信号灯实现两个进程的同步操作,一个从键盘输入,另一个读取内容。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/ipc.h>
4 #include <sys/shm.h>
5 #include <sys/sem.h>
6 #include <unistd.h>
7
8 #define SHM_SIZE 256
9 #define READ 0
10 #define WRITE 1
11
12 union semun
13 {
14 int val;
15 };
16
17 int P_operation(int sem_no, int semid)
18 {
19 struct sembuf sops;
20 sops.sem_num = sem_no;
21 sops.sem_op = -1;
22 sops.sem_flg = 0;
23 semop(semid, &sops, 1);
24
25 return 0;
26 }
27
28 int V_operation(int sem_no, int semid)
29 {
30 struct sembuf sops;
31 sops.sem_num = sem_no;
32 sops.sem_op = 1;
33 sops.sem_flg = 0;
34 semop(semid, &sops, 1);
35
36 return 0;
37 }
39 int main(int argc, const char *argv[])
40 {
41 key_t key;
42 int shmid;
43 int semid;
44 char * shmaddr;
45 union semun mysemun;
46 pid_t pid;
47
48 key = ftok(".", 100);
49
50 shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
51 shmaddr = shmat(shmid, NULL, 0);
52
53 semid = semget(key, 2, IPC_CREAT | 0666);
54 mysemun.val = 0;
55 semctl(semid, READ, SETVAL, mysemun);
56 mysemun.val = 1;
57 semctl(semid, WRITE, SETVAL, mysemun);
58
59 pid = fork();
60 if(pid < 0)
61 {
62 perror("fork");
63 return -1;
64 }
65 else if(pid == 0)//READ
66 {
67 while(1)
68 {
69 P_operation(READ, semid);
70 printf("shm = %s\n", shmaddr);
71 V_operation(WRITE, semid);
72 }
73 }
74 else//WRITE
75 {
76 while(1)
77 {
78 P_operation(WRITE, semid);
79 fgets(shmaddr, 20, stdin);
80 V_operation(READ, semid);
81 }
82 }
83
84 return 0;
85 }
~
linux@ubuntu:~/test$ ./a.out
hello world
shm = hello world
^C
linux@ubuntu:~/test$
既可用于进程间通信,一般也可用于多台计算机服务器和客户端的网络通信。