System V IPC(Interprocess Communication)是在Unix/Linux操作系统中实现进程间通信的机制。它包括三种通信方式:消息队列(Message Queue)、共享内存(Shared Memory)和信号量(Semaphore)。它有一个显著特点,即它的具体实例在内核中是以对象的形式出现的,称之为IPC对象。每个IPC对象在系统内核中都有唯一的标识符,通过标识符内核可以正确的引用指定的IPC对象。
Linux 系统编程
1、ftok()函数
IPC对象在程序中是通过关键字(key)访问的,要访问同一个IPC对象,客户端与服务器也必须使用同一个关键字,而ftok()就能够构造不冲突的新关键字,同时保证客户端与服务器的关键字相同。
参数 pathname(路径名必须存在且可自定义)与 proj_id 的低8位共同产生一个key值。
这里补充一下 ipcs 命令
即在终端显示系统内核的 IPC 对象状况
ipcs -q 只显示消息队列
ipcs -m 只显示共享内存
ipcs -s 只显示信号量
ipcrm 表示强制删除系统已存在的 IPC 对象
2、消息队列(Message Queues)
消息队列是一种在进程间传递数据的方式,它允许进程将数据发送到队列尾部,并将其从队列头部接收。消息队列是 System V IPC 中最简单的一种通信机制。它允许进程通过调用系统调用函数msgget()
、msgsnd()
和msgrcv()
来创建新的消息队列、将消息发送到队列中,以及从队列中读取消息。
(1)msgget() 创建或打开队列
- 参数:
- key :为消息的键值,用来标志多个进程所访问的消息队列是否为同一个队列。其值可被设置为IPC_PRIVATE(0) 或由 ftok() 函数的返回值
- msgflg :用来设置标志位属性,可指定为IPC_CREAT、IPC_EXCL,同时还必须指定权限,与文件的mode一样
- 返回值:为信息队列的标识符,其错误时返回 -1
- 其 errno =
- EACCESS (permission denied)
- EEXIST (Queue exists, cannot create)
- EIDRM (Queue is marked for deletion)
- ENOENT (Queue does not exist)
- ENOMEM (Not enough memory to create queue)
- ENOSPC (Maximum queue limit exceeded)
- 其 errno =
对于 msgflg 而言,如果单独使用 IPC_CREAT 标志,msgget()函数要么返回一个已经存在的消息队列对象的标识符,要么返回一个新建立的消息队列对象的标识符。如果将 IPC_CREAT 和 IPC_EXCL标志一起使用,msgget()将返回一个新建的消息对象的标识符,或者返回 -1 ,如果消息队列对象已存在。IPC_EXCL 标志本身并没有太大的意义,但和 IPC_CREAT 标志一起使用可以用来保证所得的消息队列对象是新创建的而不是打开的已有的对象。
int msgget(key, IPC_CREAT | IPC_EXCL | 0664);
(2)msgsnd() 发送消息 、msgrcv() 接收信息
-
参数:
-
msqid:消息队列标识符
-
msgp:添加(或保存)消息队列的消息,指向msgbuf的指针,其mtype表示消息的类型,用来在读取时可进行选择性读取,类似于一个标志。要注意的是它可以是一个数组、结构体、字符串等,可自定义
struct msgbuf { long mtype; char mtext[1]; .....可自定义 };
-
msgsz:消息正文mtext的大小,一般可用
sizoef(msgbuf) - sizeof(mtype)
表示 -
msgtyp:消息类型,选择性读取则依赖该函数:
msgtyp 功能 0 读取消息队列的第一条消息 >0 读取消息队列中消息类型等于msgtyp中的第一条消息 <0 读取消息队列中不小于msgtyp绝对值且类型最小的第一条消息 - msgflg:设置发送时的属性,当为0时,表示无法发送消息时阻塞直到可发送消息为止;当设置为IPC_NOWAIT时,即不进行阻塞,直接返回
-
(3)msgctl() 消息的控制
- 参数:
- msqid:消息队列的标识符
- cmd:设定消息队列的控制
- buf:描述符消息队列的各种属性消息
参数cmd用来设定函数执行何种操作,如下:
- IPC_STAT,则获取消息队列的属性信息
- IPC_SET,设置消息队列的属性消息,即通过第三个参数设置消息队列属性
- IPC_RMID,则删除消息队列,第三个参数可赋值为NULL
下面将通过两个实例来运用上述消息队列的基本接口
一、实现两个终端的消息交互,类似于聊天功能(子进程发送消息,父进程接收消息)
#include<string.h>
#include <stdio.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#define N 128
#define errlog(msg) do{perror(msg); return -1;}while(0)
#define SIZE sizeof(struct msgbuf) - sizeof(long)
#define TYPE1 100
#define TYPE2 200
struct msgbuf{
long mtype;
char buf[N];
};
int main()
{
key_t key;
if((key = ftok(".", 'a')) < 0) errlog("frok error");
int msqid;
struct msgbuf msg_snd, msg_rcv;
if((msqid = msgget(key, IPC_CREAT|IPC_EXCL|0664)) < 0)
{
if(errno != EEXIST)
errlog("msgget error");
else
msqid = msgget(key, 0664);
}
pid_t pid = fork();
if(pid < 0) errlog("pid error");
else if(pid == 0){
while(1){
msg_snd.mtype = TYPE1; //第二个终端只需改为TYPE2
fgets(msg_snd.buf, N, stdin);
msg_snd.buf[strlen(msg_snd.buf) - 1] = '\0';
msgsnd(msqid, &msg_snd, SIZE, 0);
if(strncmp(msg_snd.buf, "quit", 4) == 0){
kill(getppid(), SIGKILL);
break;
}
}
}else{
while(1)
{
msgrcv(msqid, &msg_rcv, SIZE, TYPE2, 0); //第二个终端只需改为TYPE1
if(strncmp(msg_rcv.buf, "quit", 4) == 0){
kill(pid, SIGKILL);
goto err;
}
printf("msg_b: %s\n", msg_rcv.buf);
}
}
return 0;
err:
msgctl(msqid, IPC_RMID, NULL);
}
二、msgtool,一个交互式的消息队列使用工具
msgtool 的命令行语法 ........待更.........
3、共享内存(Shared Memory)
共享内存是一种允许多个进程共享同一块内存区域的机制,它可以提高进程之间的通信效率。在创建共享内存时,需要指定一个共享内存标识符(shmid),它是一个唯一的整数。共享内存的大小由进程自行定义,可以通过调用系统调用函数shmat将共享内存附加到进程地址空间中。在使用共享内存时,进程可以直接访问共享内存中的数据,而不需要进行数据拷贝操作。同时,由于多个进程可以同时访问同一块内存区域,因此需要使用互斥机制来保证数据的一致性。可以使用信号量作为互斥机制,来控制对共享内存的访问。
(1)创建或阿凯共享内存段:
#include<sys/types.h>
#include<sys/shm.h>
int shmget(key_t key, size_t size, int shmflg;
- 参数
- key:键值,可以被设置为ftok()函数的返回值,也可以被设置为IPC_PRIVATE,与消息队列一致
- size:用于设置创建内存段的大小
- shmflg:与msgget()函数一致,可以被设置为 IPC_CREAT|IPC_EXCL|mode
- 返回共享内存的标识符
(2)将共享内存段映射到进程的虚拟地址空间上
#include<sys/types.h>
#include<sys/shm.h>
void *shmat(int shmid, const void* shmaddr, int shmflg);
- 参数:
- shmid:共享内存段的标识符
- shmaddr:一般设置为空NULL,表示系统会自动在进程的虚拟地址空间选择一块合适的区域与共享内存段建立映射关系
- shmflg:设置为0,表示共享内存段区域可读写;当设置为SHM_RDONLY,表示只可读;
- 函数执行成功时,返回与物理地址建立映射关系的进程虚拟地址(进程操作的地址)
(3)将共享内存段与进程的虚拟地址空间映射关系断开
#include<sys/types.h>
#include<sys/shm.h
int shmdt(const void* shmaddr); //shnaddr 为断开后的寻地址
(4)共享内存放入控制
#include<sys/types.h>
#include<sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
- 参数:
- shmid:共享内存的标识符
- cmd:实现对共享内存的控制
- buf:指向的结构体描述共享内存的属性,设置为IPC_STAT,表示获取共享内存的属性保持到第三个参数中;设置IPC_SET,表现通过第三个参数来设置共享内存属性;设置IPC_RMID,表示删除共享内存段,此时第三个参数为NULL
由于共享内存在通信时不可单独使用,因此下面不会同时操作共享内存,而是用进程向共享内存中写入,再从另一个进程进行读取
写入共享内存
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <errno.h>
#define errlog(msg) do{perror(msg); return -1;}while(0)
struct shmbuf{
int a, b;
};
int main()
{
key_t key;
if((key = ftok(".", 'a')) < 0) errlog("ftok error");
int shmid;
struct shmbuf *shm;
if((shmid = shmget(key, 512, IPC_CREAT|IPC_EXCL|0664)) < 0) {
if(errno != EEXIST)
errlog("shmget error");
else
shmid = shmget(key, 512, 0664);
}
if((shm = shmat(shmid, NULL, 0)) > 0) printf("shm: %p\n", shm);
shm->a = 1, shm->b = 1000;
if(shmdt(shm) < 0)errlog("shmdt error");
system("ipcs -m");
return 0;
}
读入共享内存
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <errno.h>
#define errlog(msg) do{perror(msg); return -1;}while(0)
struct shmbuf{
int a, b;
};
int main()
{
key_t key;
if((key = ftok(".", 'a')) < 0) errlog("ftok error");
int shmid;
struct shmbuf *shm;
if((shmid = shmget(key, 512, IPC_CREAT|IPC_EXCL|0664)) < 0) {
if(errno != EEXIST)
errlog("shmget error");
else
shmid = shmget(key, 512, 0664);
}
if((shm = shmat(shmid, NULL, 0)) > 0) printf("shm: %p\n", shm);
printf("a = %d, b = %d\n", shm->a, shm->b);
if(shmdt(shm) < 0)errlog("shmdt error");
shmctl(shmid, IPC_RMID, NULL);
system("ipcs -m");
return 0;
}
这里使用了自定义结构体指针shm接收映射出来shmat()函数的返回值。由于shmat()函数的返回值为与物理地址建立连接的虚拟地址,因此shm接收返回值后,则可以理解为该结构体在虚拟地址空间上。因此进程只需要操作该结构体即可完成通信。如下所示:
4、信号量(Semaphore)
共享内存作为进程间最高效的通信机制,其缺陷也很明显,为了保证进程在访问同一块内存区域而不会产生竞态,需要与同步互斥机制配合使用。信号量是一种用于进程同步和互斥的机制,它允许多个进程访问共享资源。每个信号量都有一个整数值,进程可以执行P操作(申请),将信号量的值减1,或执行V操作(释放),将信号量的值加1。当信号量的值为0时,P操作将被阻塞,直到值变为非零。
信号量集的操作包括创建或打开信号量集,信号量集的控制(初始化、删除)PV操作等
(1)创建或打开信号量集
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semget(key_t key, int nsems, int semflg);
- 参数:
- key:同上
- nsems:设置本次信号量集中信号的个数
- semflg:设置为 IPC_CREAT|IPC_EXCL,同上
- 返回值信号量标识符
(2)信号量的控制(初始化、删除)
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
- 参数
- semid:信号量集标识符
- semnum:操作的信号量的编号,从0开始
- cmd:对信号量的操作,第四个参数的有无,由cmd决定
- …:共用体semun及其成员semid_ds
- 当cmd设置为 IPC_STAT时,获取信号量的属性并保存在第三个参数的共用体的成员struct semid_ds中;
- 当cmd设置为IPC_SET时,设置信号量的属性;
- 当cmd设置为IPC_RMID时,则删除编号为semnum的信号量
- 当cmd设置为SETVAL时,通过第四个参数的成员val设置编号为semnum的信号量的初始值
- 当cmd设置为GETVAL时,获取编号semnum的信号量的值
(3)PV操作(申请信号量、释放信号量)
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semop(int semid, struct sembuf* sops, unsigned nsops); //实现信号量的申请及释放
- 参数
- semid:信号量标识符
- nsops:本次操作信号量的个数
- sopx:指向一个结构体,用来设置信号量的PV操作,如下
- sem_op用来设置PV操作。如果sem_op大于0,则信号量增加,即释放信号量,一般设置为1,表示释放信号量,值加1;若sem_op小于0,则信号量减少,一般设置为-1,表示申请信号量,值减1
- sem_flg:可以被设置为0 或 SEM_UNDO。设置SEM_UNDO表示进程没释放信号量而退出时,系统自动释放该进程中为释放的信号量
信号量编程测试
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#define errlog(msg) do{perror(msg); return -1;}while(0)
union semun{
int val;
};
int main()
{
key_t key = ftok(".", 'q');
if(key < 0) errlog("ftok error");
int semid;
if((semid = semget(key, 2, IPC_CREAT|IPC_EXCL|0664)) < 0) {
if(errno != EEXIST)
errlog("semget error");
else
semid = semget(key, 2, 0664);
}
union semun semun;
struct sembuf sem;
semun.val = 1;
semctl(semid, 0, SETVAL, semun);
semun.val = 1;
semctl(semid, 1, SETVAL, semun);
sem.sem_num = 0;
sem.sem_op = -1;
sem.sem_flg = 0;
semop(semid, &sem, 1);
sem.sem_num = 1;
sem.sem_op = 1;
sem.sem_flg = 0;
semop(semid, &sem, 1);
int retval;
retval = semctl(semid, 0, GETVAL);
printf("No.0 retval = %d\n", retval);
retval = semctl(semid, 1, GETVAL);
printf("No.1 retval = %d\n", retval);
semctl(semid, 0, IPC_RMID);
semctl(semid, 1, IPC_RMID);
return 0;
}
运行结果如下,可以看出信号量集合中的两个信号量,执行申请、释放成功
5、简单实验(待更)
以上介绍了共享内存和信号量的机制,我们可以结合两者来做个简单的实验
参考书籍:《Linux系统编程》