9 进程间通信
对应APUE第十五章——进程间通信。
进程间通信(IPC,InterProcess Communication)分为:
PIPE(管道)
Socket(套接字)
XSI(System V)
消息队列
信号量数组
共享内存
这些手段都是用于进程间通讯的,只有进程间通讯才需要借助第三方机制,线程之间的通讯是不需要借助第三方机制的,因为线程之间的地址空间是共享的。
9.1 管道
管道是UNIX系统IPC的最古老形式,所有UNIX系统都提供此种通信机制,管道有以下两种局限性。
1.历史上,它们是半双工的(即数据只能在一个方向上流动),现在,某些系统提供全双工管道。
2.管道只能在具有公共祖先的两个进程之间使用。通常,一个管道由一个进程创建,在进程调用fork之后,这个管道就能在父进程和子进程之间使用了。
尽管有这两种局限性,半双工管道仍是最常用的IPC形式。每当在管道中键入一个命令序列,让shell执行时,shell都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相连接。
管道分为命名管道(FIFO)和匿名管道(PIPE),无论是哪种管道,都是由内核帮你创建和维护的。
9.1.1 匿名管道
匿名管道是通过调用pipe函数创建的。
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd 是一个数组,表示管道的两端文件描述符,pipefd[0] 端作为读端,pipefd[1] 端作为写端。
pipe产生的是匿名管道,在磁盘的任何位置上找不到这个管道文件,而且匿名管道只能用于具有亲缘关系的进程之间通信。一般情况有亲缘关系的进程之间使用管道进行通信时,会把自己不用的一端文件描述符关闭。
注意:子进程打开的文件描述符与父进程的一致。
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define BUFSIZE 1024
void main(void) {
int pd[2];
char buf[BUFSIZE];
pid_t pid;
int len;
// 创建匿名管道
if(pipe(pd) < 0) {
perror("pipe()");
exit(1);
}
// 创建子进程
pid = fork();
if(pid < 0) {
perror("fork()");
exit(1);
}
if(pid == 0) { // 子进程 读取管道数据
// 关闭写端
close(pd[1]);
// 从管道中读取数据,如果子进程比父进程先被调度会阻塞等待数据写入
len = read(pd[0], buf, BUFSIZE);
puts(buf);
// 管道是fork之前创建的,父子进程里都有一份,所以退出之前要确保管道两端都关闭
close(pd[0]);
exit(0);
} else { // 父进程 向管道写入数据
// 关闭读端
close(pd[0]);
write(pd[1], "Hello!", 6);
close(pd[1]);
// 等待子进程退出
wait(NULL);
exit(0);
}
// never reached
exit(0);
}
10.1.2 命名管道
mkfifo函数用于创建命名管道,作用与匿名管道相同,不过可以在不同的进程之间使用,相当于对一个普通文件进行读写操作就可以了。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname:管道文件的路径和文件名。
mode:创建管道文件的权限。该mode还需要和umask做并运算来确定最后的管道文件权限。
返回值:成功返回 0,失败返回 -1 并设置 errno
当用mkfifo创建FIFO时,要用open来打开它。
FIFO有以下两种用途:
shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件
客户进程-服务器进程应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程二者之间传递数据。
可以使用命令,来创建管道文件:
mkfifo filename
代码示例1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define PATHNAME "/tmp/myfifo"
#define BUFSIZE 1024
int main (void) {
pid_t pid;
int fd = -1;
char buf[BUFSIZE] = "";
// 创建一个命名管道,用 ls -l 命令查看这个管道文件的属性
if (mkfifo(PATHNAME, 0644) < 0) {
perror("mkfifo()");
exit(1);
}
fflush(NULL);
pid = fork();
if (pid < 0) {
perror("fork()");
exit(1);
}
if (pid > 0) { // 父进程
pid = fork(); // 再次产生一个子进程
if (pid < 0) {
perror("fork()");
exit(1);
}
if (pid > 0) { // 父进程
// 两个子进程都创建完之后父进程直接退出,使两个子进程不具有亲缘关系。
exit(0);
}
/* 子进程2 */
/* 像操作普通文件一样对这个管道进行 open(2)、read(2)、write(2)、close(2) */
fd = open(PATHNAME, O_RDWR);
if (fd < 0) {
perror("open()");
exit(1);
}
read(fd, buf, BUFSIZE); // 读管道,如果没有数据会阻塞
printf("%s", buf);
write(fd, " World!", 8);
close(fd);
exit(0);
} else { // 子进程1
fd = open(PATHNAME, O_RDWR);
if (fd < 0) {
perror("open()");
exit(1);
}
write(fd, "Hello", 6);
sleep(1); // 刚写完管道不要马上读,等第二个进程读取完并且写入新数据之后再读。
read(fd, buf, BUFSIZE);
close(fd);
puts(buf);
// 肯定是这个进程最后退出,所以把管道文件删除,不然下次再创建的时候会报文件已存在的错误
remove(PATHNAME);
exit(0);
}
return 0;
}
子进程1在写完管道之后要先休眠,等待子进程2从管道的另一端把数据读入并写入新的数据,子进程1再醒来读出管道的内容。如果子进程1不休眠而是在写完之后马上读管道,很可能在它写完管道之后子进程2还没来得及调度,它自己就又把管道里的数据读出来了,这样不仅读出来的不是子进程2写入的数据,还会导致子进程2永远阻塞在 read ,因为管道中不会再有数据写入。
可能的过程:
子进程2 read 阻塞 --> 子进程1写Hello --> 子进程1休眠 -->
子进程2读Hello --> 子进程1 read 阻塞 --> 子进程2写 World! --> 子进程1读 World!
代码示例2
comm.h
#include<string.h>
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#define MY_FIFO "./fifo"
server.c
#include"comm.h"
#define BUFSIZE 1024
int main() {
umask(0);
if(mkfifo(MY_FIFO, 0666) < 0) { //创建命名管道
perror("mkfifo");
return 1;
}
// 只需要文件操作即可
int fd = open(MY_FIFO, O_RDONLY);
if(fd < 0) {
perror("open");
return 2;
}
// 业务逻辑
while(1) {
char buffer[BUFSIZE];
ssize_t s = read(fd, buffer, BUFSIZE-1);
if(s > 0) {
buffer[s] = 0;
printf("client-> %s\n", buffer);
}else if(s == 0) {
printf("client quit...\n");
break;
}else {
perror("read");
break;
}
}
close(fd);
return 0;
}
client.c
#include "comm.h"
#define BUFSIZE 1024
int main() {
int fd = open(MY_FIFO, O_WRONLY);
if(fd < 0) {
perror("open");
return 1;
}
// 业务逻辑
while(1) {
printf("请输入-> ");
fflush(stdout);
char buffer[BUFSIZE];
ssize_t s = read(0, buffer, BUFSIZE-1);
if(s > 0) {
buffer[s-1] = 0;
write(fd, buffer, strlen(buffer));
}
}
return 0;
}
执行结果:
[root@zoukangcheng proc]# ./server
client-> 1
client-> Hello World!
[root@zoukangcheng proc]# ./client
请输入-> 1
请输入-> Hello World!
请输入->
9.2 XSI IPC
XSI IPC函数是紧密地基于System V的IPC函数的。
system V:同一主机内的进程间通信方案,在OS层面专门为进程间通信设计的方案。
system V标准下的三种通信方式:
共享内存
消息队列
信号量
9.2.1 相关命令
ipcs命令可以查看 XSI IPC 的使用情况。
ipcrm命令可以删除指定的 XSI IPC。
[root@zoukangcheng proc]# ipcs
------ Message Queues -------- # 消息队列
key msqid owner perms used-bytes messages
------ Shared Memory Segments -------- # 共享内存
key shmid owner perms bytes nattch status
------ Semaphore Arrays -------- # 信号量数组
key semid owner perms nsems
通过 ipcs 命令可以看出来,命令的输出结果分为三个部分,第一部分是系统中当前开辟的共享内存(shm),第二部分是信号量数组(sem),第三部分是消息队列(msg)。
可以看到,不论是哪一部分,都有一列叫做key,使用 XSI IPC 通信的进程就是通过同一个 key 值操作同一个共享资源的。这个 key 是一个正整数,与文件描述符不同的是,生成一个新 key 值时它不采用当前可用数值中的最小值,而是类似生成进程 ID 的方式,key 值连续的加 1,直至达到一个整数的最大正值,然后再回转到 0 从头开始累加。
不同IPC的特征比较
9.2.2 标识符和键
每个内核中的IPC结构(消息队列、信号量或共享存储)都用一个非负整数的标识符(identifier)加以引用。例如,要向一个消息队列发送消息或者从一个消息队列取消息,只需要知道其队列标识符。标识符是IPC对象的内部名,为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。为此,每个 IPC对象都与一个键(key)相关联,将这个键作为该对象的外部名。
无论何时创建IPC结构(通过调用msgget、semget或shmget 创建),都应指定一个键。这个键的数据类型是基本系统数据类型key_t,通常在头文件<sys/types.h>中被定义为长整型。这个键由内核变换成标识符。
有多种方法使客户进程和服务器进程在同一IPC结构上汇聚。
方法1:服务器进程可以指定键IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(如一个文件)以便客户进程取用。键IPC_PRIVATE保证服务器进程创建一个新IPC结构。这种技术的缺点是:文件系统操作需要服务器进程将整型标识符写到文件中,此后客户进程又要读这个文件取得此标识符。IPC_PRIVATE键也可用于父进程子关系。父进程指定IPC_PRIVATE创建一个新IPC结构,所返回的标识符可供fork后的子进程使用。接着,子进程又可将此标识符作为exec函数的一个参数传给一个新程序。
方法2:可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的IPC结构。这种方法的问题是该键可能已与一个IPC结构相结合,在此情况下,get 函数(msgget、semget 或shmget)出错返回。服务器进程必须处理这一错误,删除已存在的IPC结构,然后试着再创建它。
方法3:客户进程和服务器进程认同一个路径名和项目ID(项目ID是0~255之间的字符值),接着,调用函数ftok将这两个值变换为一个键。然后在方法2中使用此键。ftok提供的唯一服务就是由一个路径名和项目ID产生一个键。
#include <sys/ipc.h>
key_t ftok(const chat *path, int id);
path参数必须引用一个现有的文件。当产生键时,只使用id参数的低8位。对于不同文件的两个路径名,如果使用同一项目ID,可能产生相同的键。
3个get 函数(msgget、semget和shmget)都有两个类似的参数:一个key和一个整型flag。在创建新的IPC结构(通常由服务器进程创建)时,如果key是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要指明 flag 的IPC_CREAT标志位。为了引用一个现有队列(通常由客户进程创建),key必须等于队列创建时指明的key的值,并且IPC_CREAT必须不被指明。
注意,决不能指定IPC_PRIVATE作为键来引用一个现有队列,因为这个特殊的键值总是用于创建一个新队列。
如果希望创建一个新的IPC结构,而且要确保没有引用具有同一标识符的一个现有IPC结构,那么必须在flag中同时指定IPC_CREAT和IPC_EXCL位。这样做了以后,如果IPC结构已经存在就会造成出错,返回EEXTST。
9.2.3 消息队列
消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。
msg、sem 和 shm 都有一系列函数遵循下面的命名规则:
xxxget():创建或引用,将key转换为标识符id
xxxop():相关操作
xxxctl():其它的控制或销毁
每个队列都有一个msqid_ds结构体与之关联:
struct msqid_ds {
struct ipc_perm msg_perm; // 保存ipc权限信息的结构体
msgqnum_t msg_qnum;
msglen_t msg_qbytes;
pid_t msg_lspid;
pid_t msg_lrpid;
// ...
}
相关系统调用
// msgget - get a System V message queue identifier
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// 函数的作用是创建或引用一个消息队列,消息队列是双工的,两边都可以读写。
int msgget(key_t key, int msgflg);
key:IPC内核标识符的外部方案实现,拥有相同 key 的双方才可以通信。key 值必须是唯一的,ftok 函数可以用于获取 key
msgflg:特殊要求,没有写0
返回:非负的队列ID或出错-1
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// 将 msgp 指向的结构体存放到 msgid 的消息队列中,这段空间有 msgz 个字节大小,msgz 的值要减掉强制的成员 mtype 的大小(sizeof(long))。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 从 msgid 这个消息队列中取出 msgp 结构体数据,msgp 的大小是 msgsz,msgflg 是特殊要求,没有特殊要求可以写 0。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
// msgtyp可以指定接收哪一种信息“
// 0: 返回队列的第一个信息
// >0: 返回队列中消息类型为 type 的第一个消息。
// <0: 返回队列中消息类型值小于等于 type 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
/* msgp 指向的结构体的成员定义要类似 msgbuf 这个结构体,第一个成员必须是 long 类型的 mtype,并且必须是 > 0 的值 */
struct msgbuf {
long mtype; /* 消息类型,必须 > 0 */
char mtext[512]; /* 消息数据字段 */
};
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// msqctl函数对队列执行多种操作。它和另外两个与信号量及共享存储有关的函数(semctl 和shmctl)都是XSI IPC的类似于ioctl的函数(亦即垃圾桶函数)。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
cmd参数指定对msqid指定的队列要执行的命令:
IPC_STAT:取此队列的msqid_ds结构,并将它存放在buf指向的结构中。
IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所有数据。这种删除立即生效。仍在使用这一消息队列的其他进程在它们下一次试图对此队列进行操作时,将得到EIDRM错误。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。
代码示例
proto.h:定义双方都需要使用的数据或对象
#ifndef __PROTO_H__
#define __PROTO_H__
#define NAMESIZE 32
/* 通讯双方生成 key 值共同使用的文件 */
#define KEYPATH "/usr/local/linux_c/proc/key"
/* 通讯双方生成 key 值共同使用的 salt 值:0~255之间的字符值 */
#define KEYPROJ 'a'
/* 消息类型,只要是大于 0 的合法整数即可 */
#define MSGTYPE 10
/* 通讯双方约定的协议 */
struct msg_st {
long mtype; // 消息类型
char name[NAMESIZE]; // 消息数据
int math; // 其他信息
int chinese;
};
#endif
snder.c:客户端
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include "proto.h"
int main() {
key_t key;
int msgid;
struct msg_st sbuf;
// 设置随机数种子
srand(time(NULL));
// 用与接收方相同的文件和 salt 生成一样的key,这样才可以通讯
key = ftok(KEYPATH, KEYPROJ);
if(key < 0) {
perror("ftok()");
exit(1);
}
// 取得消息队列
msgid = msgget(key, 0);
if(msgid < 0) {
perror("msgget()");
exit(1);
}
// 为要发送的结构体赋值
sbuf.mtype = MSGTYPE;
strcpy(sbuf.name, "Alan");
sbuf.math = rand() % 100;
sbuf.chinese = rand() % 100;
// 发送结构体
if(msgsnd(msgid, &sbuf, sizeof(sbuf)-sizeof(long), 0) < 0) {
perror("msgsnd()");
exit(1);
}
puts("ok!");
// 消息队列不是发送方创建的,所以发送方不用负责销毁
exit(0);
}
rcver.c:服务器
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "proto.h"
int main() {
key_t key;
int msgid;
struct msg_st rbuf;
// 通过文件和字符 'a' 生成唯一的 key,注意文件必须是真实存在的
key = ftok(KEYPATH,KEYPROJ);
if(key < 0) {
perror("ftok()");
exit(1);
}
// 接收端应该先启动,所以消息队列由接收端创建
msgid = msgget(key, IPC_CREAT|0600);
if(msgid < 0) {
perror("msgget()");
exit(1);
}
// 不停的接收消息
while(1) {
// 没有消息的时候会阻塞等待
if(msgrcv(msgid, &rbuf, sizeof(rbuf)-sizeof(long), 0, 0) < 0) {
perror("msgrcv");
exit(1);
}
/* 用结构体中强制添加的成员判断消息类型,
* 当然在这个例子中只有一种消息类型,所以不判断也可以。
* 如果包含多种消息类型这里可以写一组 switch...case 结构
*/
if(rbuf.mtype == MSGTYPE) {
printf("Name = %s\n",rbuf.name);
printf("Math = %d\n",rbuf.math);
printf("Chinese = %d\n",rbuf.chinese);
}
}
/* 谁创建谁销毁。
* 当然这个程序是无法正常结束的,只能通过信号杀死。
* 使用信号杀死之后大家可以用 ipcs(1) 命令查看一下,消息队列应该是没有被销毁的,
* 大家可以使用上面我们提到的 ipcrm(1) 命令把它删掉。
*/
msgctl(msgid, IPC_RMID, NULL);
exit(0);
}
执行结果:
[root@zoukangcheng proc]# ./rcver
Name = Alan
Math = 81
Chinese = 36
[root@zoukangcheng proc]# ./snder
ok!
使用命令查看ipc并销毁:
[root@zoukangcheng proc]# ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
0x610125ef 0 root 600 0 0
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
[root@zoukangcheng proc]# ipcrm -q 0
[root@zoukangcheng proc]# ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
9.2.4 信号量
信号量与已经介绍过的IPC(管道、FIFO以及消息列队)不同。它是一个计数器,用于为多个进程提供对共享数据对象的访问。
为了获得共享资源,进程需要执行下列操作:
测试控制该资源的信号量。
若此信号量的值为正,则进程可以使用该资源。在这种情况下,进程会将信号量值减1,表示它使用了一个资源单位。
否则,若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0。进程被唤醒后,它返回至步骤1。
当进程不再使用由一个信号量控制的共享资源时,该信号量值增1。如果有进程正在休眠等待此信号量,则唤醒它们。
为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。
常用的信号最形式被称为二元信号量(binary semaphore)。它控制单个资源,其初始值为1。但是,一般而言,信号量的初值可以是任意一个正值,该值表明有多少个共享资源单位可供共享应用。
相关系统调用
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 创建或获得一个信号量id
int semget(key_t key, int nsems, int semflg);
key:具有亲缘关系的进程之间可以使用一个匿名的 key 值,key 使用宏 IPC_PRIVATE 即可。
nsems:表示有多少个信号。信号量实际上是一个计数器,所以如果设置为 1 可以用来模拟互斥量。
semflg:IPC_CREAT 表示创建信号量,同时需要按位或一个权限,如果是匿名 IPC 则无需指定这个宏,直接给权限就行了。
成功返回 sem ID,失败返回 -1 并设置 errno。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 用来控制或销毁信号量
int semctl(int semid, int semnum, int cmd, ...);
semid:信号量id
semnum:信号量数组的下标
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 操作信号量
int semop(int semid, struct sembuf *sops, unsigned nsops);
struct sembuf {
unsigned short sem_num; /* 对第几个资源(数组下标)操作 */
short sem_op; /* 取几个资源写负数几(不要写减等于),归还几个资源就写正数几 */
short sem_flg; /* 特殊要求 */
};
sops:结构体数组起始位置;
nsops:结构体数组长度;
返回值:成功返回0,失败返回-1并设置 errno。
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#define PROCNUM 20
#define FNAME "/tmp/out"
#define BUFSIZE 1024
// 多个函数都要使用这个信号量 ID,所以定义为全局变量
static int semid;
static void P(void) {
struct sembuf op;
op.sem_num = 0; // 只有一个资源,所以数组下标是 0
op.sem_op = -1; // 取一个资源就减1
op.sem_flg = 0; // 没有特殊要求
while(semop(semid,&op,1) < 0) {
// 出现假错就重试
if(errno != EINTR && errno != EAGAIN) {
perror("semop()");
exit(1);
}
}
}
static void V(void) {
struct sembuf op;
op.sem_num = 0;
op.sem_op = 1; // 归还一个资源
op.sem_flg = 0;
while(semop(semid,&op,1) < 0) {
if(errno != EINTR && errno != EAGAIN) {
perror("semop()");
exit(1);
}
}
}
static void func_add() {
FILE *fp;
char buf[BUFSIZE];
fp = fopen(FNAME,"r+");
if(fp == NULL) {
perror("fopen()");
exit(1);
}
// 先取得信号量在操作文件,取不到就阻塞等待,避免发生竞争
P();
fgets(buf,BUFSIZE,fp);
rewind(fp);
sleep(1); // 调试用,为了放大竞争,更容易看出来互斥量发挥了作用
fprintf(fp,"%d\n",atoi(buf)+1);
fflush(fp);
// 操作结束之后归还信号量,让其它进程可以取得信号量
V();
fclose(fp);
return ;
}
int main() {
int i;
pid_t pid;
// 在具有亲缘关系的进程之间使用,所以设置为 IPC_PRIVATE 即可。
// 另外想要实现互斥量的效果,所以信号量数量设置为 1 个即可。
semid = semget(IPC_PRIVATE, 1, 0600);
if(semid < 0) {
perror("semget()");
exit(1);
}
// 将 union semun.val 的值设置为 1
if(semctl(semid, 0, SETVAL, 1) < 0) {
perror("semctl()");
exit(1);
}
// 创建 20 个子进程
for(i = 0 ; i < PROCNUM ; i++) {
pid = fork();
if(pid < 0) {
perror("fork()");
exit(1);
}
if(pid == 0) { // child
func_add();
exit(0);
}
}
for(i = 0 ; i < PROCNUM ; i++)
wait(NULL);
// 销毁信号量
semctl(semid,0,IPC_RMID);
exit(0);
}
9.2.5 共享存储
共享存储允许两个或多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC。
使用共享存储时要掌握的唯一窍门是,在多个进程之间同步访问一个给定的存储区。若服务器进程正在将数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去取这些数据。通常,信号量用于同步共享存储访问。(不过正如前节最后部分所述,也可以用记录锁或互斥量。)
我们已经看到了共享存储的一种形式,就是在多个进程将同一个文件映射到它们的地址空间的时候。XSI共享存储和内存映射的文件的不同之处在于,前者没有相关的文件。XSI共享存储段是内存的匿名段。
相关系统调用
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);78 int shmdt(const void *shmaddr);
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
// 申请的共享内存大小,单位是字节
#define MEMSIZE 1024
int main() {
char *str;
pid_t pid;
int shmid;
// 有亲缘关系的进程 key 参数可以使用 IPC_PRIVATE 宏,并且创建共享内存 shmflg 参数不需要使用 IPC_CREAT 宏
shmid = shmget(IPC_PRIVATE, MEMSIZE, 0600);
if(shmid < 0) {
perror("shmget()");
exit(1);
}
pid = fork();
if(pid < 0) {
perror("fork()");
exit(1);
}
if(pid == 0) { // 子进程
// 关联共享内存
str = shmat(shmid,NULL,0);
if(str == (void *)-1) {
perror("shmat()");
exit(1);
}
// 向共享内存写入数据
strcpy(str,"Hello!");
// 分离共享内存
shmdt(str);
// 无需释放共享内存
exit(0);
}
else { // 父进程
// 等待子进程结束再运行,因为需要读取子进程写入共享内存的数据
wait(NULL);
// 关联共享内存
str = shmat(shmid,NULL,0);
if(str == (void *)-1) {
perror("shmat()");
exit(1);
}
// 直接把共享内存中的数据打印出来
puts(str);
// 分离共享内存
shmdt(str);
// 释放共享内存
shmctl(shmid,IPC_RMID,NULL);
exit(0);
}
exit(0);
}