进程间通信(IPC)介绍
进程间通信的本质就是让两个毫不相关的进程看到一份共同的资源,大概意思就是实现不同进程间的传播或交换信息
进程间通信的主要方式有管道,消息队列,共享内存,信号量,Socket,Streams等,这篇博客主要详细讲解前四种通信方式。(因为后面两个还没学,嘿嘿)
一、管道
管道是UNIX中最古老的进程间通信方式
我们把从一个进程连接到另一个进程的一个数据流称为“管道”
管道又分为匿名管道和命名管道两种
匿名管道
1、特点
- 管道是半双工的,只支持单向通信,如果需要全双工通信,就需要建立起两个管道
- 管道只支持有血缘关系的进程间通信,如父子进程和兄弟进程
- 管道的生命周期随进程,进程终止管道就会被释放
- 管道提供面向字节流的通信
2、原型
#include <unistd.h>
int pipe(int fd[2]);
管道创建成功后返回两个文件描述符,用fd[2]数组接收,fd[0]为读,fd[1]为写
返回值:成功返回0,失败返回错误代码
3、图解
父进程刚创建出子进程时,调用pipe函数返回的文件描述符都是打开的,父子进程通信时就要关闭对应的描述符,如父进程从管道读数据,用的是fd[0],就要关掉它的fd[1],子进程则相反
代码演示
int main()
{
pid_t pid;
int file[2];//用于接收文件描述符
pipe(file);//创建管道
char buf[256] = {0};
pid = fork();
if(pid < 0)
{
perror("fork");
}else if(pid == 0)
{
//子进程进行写操作,关闭文件描述符fd[0]
close(file[0]);
write(file[1],"hello father",strlen("hello father"));
}else
{
//父进程进行读操作,关闭文件描述符fd[1]
close(file[1]);
read(file[0],buf,sizeof(buf));
}
printf("%s\n",buf);
}
因为在运行a.out文件时是敲了回车的,所以会自动换一次行
命名管道
也成为FIFO,是一种文件类型
1、特点
- 支持任意两个进程间的通信
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
2、与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建并打开
- 唯一的区别就是打开和创建的方式不同,一但这些工作完成之后,它们具有相同的语义
3、原型
#include <sys/stat.h>
int mkfifo(const char *filename,mode_t mode);
成功返回0,出错返回-1
其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。
4、例子:用命名管道实现文件拷贝
writer.c
int main()
{
//创建命名管道,权限是644
mkfifo("pipe",0644);
//test 文件已在当前目录下创建,以只读方式打开
int input = open("test",O_RDONLY);
if(input == -1)
{
perror("open");
}
//以只写方式打开命名管道
int output = open("pipe",O_WRONLY);
if(output == -1)
{
perror("open");
}
char out[1024];
int n;
while((n = read(input,out,sizeof(out))) > 0)
{
//向管道写数据
write(output,out,n);
}
close(input);
close(output);
return 0;
}
reader.c
int main()
{
int output;
//创建一个文件,并以只写方式打开它
//目的是将test文件中的内容拷贝到test.bak中
output = open("test.bak",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(output == -1)
{
perror("open");
}
int input;
input = open("pipe",O_RDONLY);
if(input == -1)
{
perror("open");
}
char buf[1024];
int n;
while((n = read(input,buf,sizeof(buf))) > 0)
{
write(output,buf,n);
}
close(input);
close(output);
unlink("pipe");
return 0;
}
test文件内容
test.bak 文件内容显示拷贝成功
二、消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
1、特点
- 消息队列提供了一个从一个进程向另外一个进程发送一个数据块(有类型数据块)的方法
消息队里的生命周期随内核,进程终止后消息队列也不会销毁
ipcs -q 查看当前的消息队列 ipcrm -q + 消息队列ID 销毁消息队列,也可以通过在函数内调用msgctl函数清除消息队列
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
- 消息队列也有管道一样的不足,即每个消息队列的最大长度是有上限的
(MSGMAX)
,每个消息队列的总的字节数是由上线的(MSGMNB)
,系统上消息队列的总数也有一个上限(MSGMNI)
。
2、原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/msg.h>
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// key:消息队列的名字,下面的例子中将具体体现使用方法
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// msqid:消息队列的标识符,即队列ID
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
3、例子:用两个终端实现两个前台进程的通信
common.h
#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 0X6667
#define SERVER_TYPE 1
#define CLIENT_TYPE 2
struct msgbuf{
long mtype;
char mtext[1024];
};
int CreateMsgQueue();
int GetMsgQueue();
int DestroyMsgQueue(int msgid);
int SendMsg(int msgid, int who, char *msg);
int RecvMsg(int msgid, int recvType, char out[]);
common.c
#include "common.h"
int CommonMsgQueue(int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);//ftok函数可以生成key
if(key < 0)
{
perror("ftok");
return -1;
}
int msgid = msgget(key,flags);
if(msgid < 0)
{
perror("msgget");
return -1;
}
return msgid;
}
int CreateMsgQueue()//创建一个消息队列
{
//flag由9个权限标志构成
//IPC_CREAT|IPC_EXCL一起用表示如果没有该消息队列,则创建一个新的,并返回队列ID
return CommonMsgQueue(IPC_CREAT|IPC_EXCL|0666);
}
int GetMsgQueue()//获得一个消息队列
{
//返回已创建的消息队列ID
return CommonMsgQueue(IPC_CREAT);
}
int DestroyMsgQueue(int msgid)//销毁一个消息队列
{
if (msgctl(msgid,IPC_RMID,NULL) < 0 )
{
perror("msgctl");
return -1;
}
return 0;
}
int SendMsg(int msgid, int who, char *msg)//往消息队列中发送数据
{
struct msgbuf buf;
buf.mtype = who;
strcpy(buf.mtext,msg);
if(msgsnd(msgid,(void*)&buf,sizeof(buf.mtext),0) < 0)
{
perror("msgsnd");
return -1;
}
return 0;
}
int RecvMsg(int msgid, int recvType, char out[])//从消息队列中读取数据
{
struct msgbuf buf;
if((msgrcv(msgid,(void *)&buf,sizeof(buf.mtext),recvType,0)) < 0)
{
perror("msgrcv");
return -1;
}
strcpy(out,buf.mtext);
return 0;
}
server.c
#include "common.h"
int main()
{
int msgid = CreateMsgQueue();
char buf[1024];
while(1)
{
buf[0] = 0;
RecvMsg(msgid,CLIENT_TYPE,buf);
printf("Client say# %s \n",buf);
printf("Please Enter# ");
fflush(stdout);//刷新缓冲区
ssize_t s = read(0,buf,sizeof(buf));//从标准输出读取数据到buf中
if(s > 0)
{
buf[s-1] = 0;
SendMsg(msgid,SERVER_TYPE,buf);
printf("send done,wait for client:..\n");
}
if(strcmp(buf,"exit") == 0)
{
break;
}
}
DestroyMsgQueue(msgid);
return 0;
}
client.c
#include "common.h"
int main()
{
int msgid = GetMsgQueue();
char buf[1024];
while(1)
{
buf[0] = 0;
printf("Please Enter# ");
fflush(stdout);//刷新缓冲区
ssize_t s = read(0,buf,sizeof(buf));//从标准输出读取数据到buf中
if(s > 0)
{
buf[s-1] = 0;
SendMsg(msgid,CLIENT_TYPE,buf);
printf("send done, wait recv...\n");
}
RecvMsg(msgid,SERVER_TYPE,buf);
printf("Server say# %s \n",buf);
if(strcmp(buf,"exit") == 0)
{
return 0;
}
}
return 0;
}
三、共享内存
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
1、特点
- 共享内存是最快的IPC形式,因为进程是直接对内存进行读取的
共享内存的生命周期随内核
ipcs -m 查看共享内存 ipcrm -m + 共享内存ID 手动销毁共享内存或者通过调用shmctl函数来销毁
信号量和共享内存通常放在一起使用,信号量用来同步对共享内存的访问
2、图解
3、原型
#include <sys/shm.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。
当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。
shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。
4、例子:一个进程往共享内存中写数据,一个进程往出读数据
commo.h
#pragma once
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 0X6666
int CreateShm(int size);
int DestroyShm(int shmid);
int GetShm(int size);
common.c
#include "common.h"
//与创建消息队列很类似,就不做介绍了
int CommonShm(int size, int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int shmid = 0;
if((shmid = shmget(key,size,flags)) < 0)
{
perror("shmget");
return -2;
}
return shmid;
}
int CreateShm(int size)
{
return CommonShm(size,IPC_CREAT|IPC_EXCL|0666);
}
int DestroyShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,NULL) < 0)
{
return -1;
}
return 0;
}
int GetShm(int size)
{
return CommonShm(size,IPC_CREAT);
}
server.c
#include "common.h"
int main()
{
int shmid = CreateShm(4096);
char *addr = shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i++<26)
{
printf("client# %s\n",addr);
sleep(1);
}
//将共享内存段与当前进程脱离
shmdt(addr);
sleep(2);
DestroyShm(shmid);
return 0;
}
client.c
#include "common.h"
int main()
{
int shmid = GetShm(4096);
sleep(1);
char *addr =shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i<26)
{
//每隔1秒写一个字母进去
addr[i] = 'A'+i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
DestroyShm(shmid);
return 0;
}
client只负责写数据,所以没有输出结果
信号量
信号量主要作用于同步和互斥,而不是存储进程间通信数据
1、特点
信号量本质上是一个计数器,记录了临界资源的数目
临界资源:系统中有些资源一次只允许一个进程访问,被称为临界资源,也可被当做互斥资源 临界区: 在进程中涉及到互斥资源的程序段叫临界区
- 信号量基于操作系统的P,V操作,且P,V操作是原子性的
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
2、原型
简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。
在semop函数中,sembuf结构的定义如下:
struct sembuf
{
short sem_num; // 信号量组中对应的序号,0~sem_nums-1
short sem_op; // 信号量值在一次操作中的改变量
short sem_flg; // IPC_NOWAIT, SEM_UNDO
}
其中 sem_op
是一次操作中的信号量的改变量:
- 若
sem_op > 0
,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。 若
sem_op < 0
,请求 sem_op 的绝对值的资源。如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
当相应的资源数不能满足请求时,这个操作与
sem_flg
有关。sem_flg
指定IPC_NOWAIT
,则semop函数出错返回EAGAIN。sem_flg
没有指定IPC_NOWAIT
,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
- 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
- 此信号量被删除,函数smeop出错返回EIDRM;
- 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
- 若
sem_op == 0
,进程阻塞直到信号量的相应值为0:
- 当信号量已经为0,函数立即返回。
- 如果信号量的值不为0,则依据
sem_flg
决定函数动作:
sem_flg
指定IPC_NOWAIT,则出错返回EAGAIN。sem_flg
没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
- 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
- 此信号量被删除,函数smeop出错返回EIDRM;
- 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR
在semctl
函数中的命令有多种,这里就说两个常用的:
SETVAL
:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。IPC_RMID
:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。3、例子:父进程与子进程之间同步打印字母,使其成对出现
commn.h
#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define PATHNAME "."
#define PROJ_ID 0X6666
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
int CreateSem(int nums);
int InitSem(int semid, int nums, int initVal);
int GetSem(int nums);
int P(int semid, int who);
int V(int semid, int who);
int DestroySem(int semid);
common.c
#include "common.h"
static int CommonSemSet(int nums, int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int semid = semget(key,nums,flags);
if(semid < 0)
{
perror("semget");
return -2;
}
return semid;
}
//创建信号量集
int CreateSem(int nums)
{
return CommonSemSet(nums,IPC_CREAT|IPC_EXCL|0666);
}
//初始化信号量集
int InitSem(int semid, int nums, int initVal)
{
union semun un;
un.val = initVal;
if(semctl(semid,nums,SETVAL,un) < 0)
{
perror("semctl");
return -1;
}
return 0;
}
//获取已创建好的信号量集
int GetSem(int nums)
{
return CommonSemSet(nums,IPC_CREAT);
}
static int CommPV(int semid, int who, int op)
{
struct sembuf sf;
sf.sem_flg = 0;
sf.sem_op = op;
sf.sem_num = who;
if(semop(semid,&sf,1)<0)
{
perror("semop");
return -1;
}
return 0;
}
//申请资源,相当于对信号量进行减操作
int P(int semid, int who)
{
return CommPV(semid,who,-1);
}
//释放资源,相当于对信号量进行加操作
int V(int semid, int who)
{
return CommPV(semid,who,1);
}
//销毁刚创建的信号量集
int DestroySem(int semid)
{
if(semctl(semid, 0 ,IPC_RMID) < 0)
{
perror("semctl");
return -1;
}
return 0
}
test_sem.c
#include "common.h"
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int semid = CreateSem(1);
InitSem(semid, 0 ,1);
pid_t pid = fork();
int i = 20;
if(pid < 0)
{
perror("fork");
return 0;
}else if(pid == 0)
{
//子进程
int _semid = GetSem(0);
//通过P,V操作实现两个进程的互斥
//在屏幕上打印成对出现的AB
while(i--)
{
P(_semid, 0);
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(321456);
V(_semid,0);
}
}
else{
//父进程
while(i--)
{
P(semid,0);
printf("B");
fflush(stdout);
usleep(223456);
printf("B ");
fflush(stdout);
usleep(121456);
V(semid,0);
}
wait(NULL);
}
DestroySem(semid);
return 0;
}
运行结果(加上PV操作),AB成对出现,父子进程实现同步,那去掉PV之后呢
去掉PV之后现象如下,可以看出父子进程没有实现同步
五种进程间通信方式总结
- 管道:只支持有血缘关系的进程间通信,且生命周期随进程
- FIFO:支持任意进程间通信,创建和打开方式与管道不同,其他同管道一样
- 消息队列:存在系统限制,而且有时候需要手动删除,在读一条消息的时候还要注意上一条消息没有读完的情况
- 共享内存:速度最快的IPC方式,但要注意保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
- 信号量:不能用来传递复杂消息,只能用来互斥与同步