进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
一般而言,进程都拥有自己独立的地址空间,并通过页表映射物理内存,彼此之间应相互独立,那么想要在不同的进程之间传播和交换信息就需要通过一些特殊的方法,其核心的思路即为:使不同的进程看到一份公共的资源。其中主要方法包括:管道、消息队列、信号量、共享内存等。
管道
管道,分匿名管道与命名管道两种,两者的区别主要在于前者只适用于有血缘关系的进程之间,后者则没有这种约束。管道通过文件描述符表的方式,创建出两个进程的文件描述符表中都能访问到的一个文件,并使用写文件的方式实现进程间通信。
进程1创建管道
进程2获取到相同的文件描述符中管道的读写端地址
两进程各自关闭端口其中之一使其一收一发
特点:
1. 生命周期随进程,当进程终止时,文件描述符随之销毁;
2. 只能实现单项通信,在通信时应一收一发。
3. 是面向数据流的通信;
4. 自带同步,互斥和控制机制,解决了数据读写的二义性;
匿名管道
匿名管道中调用pipe开辟管道,并fork创建子进程,子进程即会得到一份相同的文件描述符表的拷贝,其中的文件地址相同,即可访问一片公共的空间。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int _pipe[2];
int ret = pipe(_pipe);
if(ret==-1)
{
perror("pipe");
return -1;
}
pid_t id=fork();
if(id<0)
{
perror("fork");
return -1;
}
else if(id==0)//child
{
close(_pipe[0]);
char masg[]="i am child";
while(1)
{
sleep(1);
write(_pipe[1],masg,strlen(masg)+1);
}
}
else//father
{
close(_pipe[1]);
char ret[100];
while(1)
{
int ss=read(_pipe[0],ret,sizeof(ret));
ret[ss]='\0';
printf("father recived: %s\n",ret);
}
}
return 0;
}
最终实现在子进程中写字符串,而从父进程中读取。
命名管道
Linux下有两种方式创建命名管道,一是在Shell下交互地建立一个命名管道;二是在程序
中使用系统函数建立命名管道。 Shell方式下可使用mknod或mkfifo命令,下面使用mkfifo创建命名管道。命名管道和管道的使用方法基本是相同的。只是使用命名管道时,必须先调用open()将其打开。因为命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。
client.c
#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
int main()
{
int fd=open("./myfifo",O_WRONLY);
if(fd<0)
{
perror("open");
return -1;
}
char buf[1024];
while(1)
{
int ret=read(0,buf,sizeof(buf));
if(ret<0)
{
perror("read");
return -1;
}
buf[ret]=0;
int w=write(fd,buf,strlen(buf));
if(w<0)
{
perror("write");
return -1;
}
}
close(fd);
return 0;
}
server.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int s = mkfifo("./myfifo",0666|S_IFIFO);
if(s<0)
{
perror("mkfifo");
return -1;
}
int fd = open("./myfifo",O_RDONLY);
if(fd<0)
{
perror("open");
return -1;
}
char buf[1024];
while(1)
{
int ret=read(fd,buf,sizeof(buf));
if(ret<0)
{
perror("read");
return -1;
}
if(ret==0)
break;
buf[ret-1]=0;
printf("client say:%s\n",buf);
}
close(fd);
return 0;
}
实现client从键盘读入并写入myfifo文件,而server从文件中读取。
注:既然管道的通信中存在收发两种状态,那么在运行中就会由于关闭其中的某一通道导致不同的情况
1. 如果指向管道写端的⽂件描述符关闭了(管道写端的引⽤计数等于0),⽽仍然有进程 从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到⽂件末尾⼀样。
2. 如果指向管道写端的⽂件描述符没关闭(管道写端的引⽤计数⼤于0),⽽持有管道写端的 进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
3. 如果指向管道读端的⽂件描述符关闭了(管道读端的引⽤计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE(kill -7),通常会导致进程异常终⽌。
4. 如果指向管道读端的⽂件描述符没关闭(管道读端的引⽤计数⼤于0),⽽持有管道读端的 进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再 次write会阻塞,直到管道中有空位置了才写⼊数据并返回。
XSI IPC
如果说管道是通过⽂件描述符来实现,那么XSI IPC 中消息队列、信号量以及共享内存都是依托标识符和键来实现的。内核会为每个IPC对象维护⼀个数据结构,消息队列,共享内存和信号量都有这样⼀个共同的数据结构,其中的参数key可以通过ftok接口给予一个路径名和一个权限(可自行生成)创建。
struct ipc_perm {
key_t __key; /* Key supplied to xxxget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
特点:
1. 生命周期随内核,由于XSI IPC的三种通信方式都是系统专门为了进程间通信提供的方式,其接口都为系统调用接口;
2. 可以实现双向通信;
3. 是面向数据块的通信,存在消息长度的上限 MSGMAX;
4. 自带同步,互斥和控制机制,解决了数据读写的二义性;
5. 各个接口都含有xxget与xxctl两个接口用来创建和控制,各自有拥有一些独有的接口,例如消息队列中int msgget(key_t key, int msgflg);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
其中的msgflg参数有IPC_CREAT 与IPC_EXCL两种选择,如果单独使⽤IPC_CREAT, XXXget()函数要么返回⼀个已经存在的共享内存的操作符,要么返回⼀个新建的共享内存的标识符;如果将IPC_CREAT和IPC_EXCL标志⼀起使⽤, XXXget()将返回⼀个新建的IPC标识符,如果该IPC资源已存在,或者返回-1。
IPC_EXEL标志本⾝并没有太⼤的意义,但是和IPC_CREAT标志⼀起使⽤可以⽤来保证所得的对象是新建的,⽽不是打开已有的对象。
当然既然消息队列、信号量与共享内存声明周期都随内核,那么我们就存在在系统中查找它们的指令:
查找指令:ipcs -q(消息队列) -s(信号量) -m(共享内存)
删除指令:ipcrm -q -s -m
消息队列
消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。
除了上述所介绍的msgget与msgctl,还有收数据和发数据两个接口,实现队列消息的入队和出队。 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
其主要功能可以通过man在系统中自行查阅
代码实现
comm.h
#ifndef _COMM_H_
#define _COMM_H_
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<string.h>
#define PATHNAME "."
#define PROJ_ID 0x4567
#define SERVER_TYPE 1
#define CLIENT_TYPE 2
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1024];/* message data */
};
int createMsQueue();
int getMsQueue();
int destroyMsQueue(int msqid);
int sendMsg(int msgid,long type,char *buff);
int rcvMsg(int msgid,long sndtype,long type,char *buff);
#endif
comm.c
#include"comm.h"
int commMsQueue(int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int msqid = msgget(key,flags);
if(msqid < 0)
{
perror("msgget");
return -1;
}
return msqid;
}
int createMsQueue()
{
return commMsQueue(IPC_CREAT | IPC_EXCL);
}
int getMsQueue()
{
return commMsQueue(IPC_CREAT);
}
int destroyMsQueue(int msqid)
{
int s = msgctl(msqid,IPC_RMID,0);
if(s < 0)
{
perror("msgctl");
return -1;
}
return 0;
}
int sendMsg(int msgid,long type,char *buff)
{
struct msgbuf ms;
ms.mtype = type;
strcpy(ms.mtext,buff);
int mag = msgsnd(msgid,&ms,1024,0);
if(mag < 0)
{
perror("msgsnd");
return -1;
}
return 0;
}
int rcvMsg(int msgid,long sndtype,long type,char *buff)
{
struct msgbuf ms;
ms.mtype = type;
int mag = msgrcv(msgid,&ms,1024,sndtype,0);
if(mag < 0)
{
perror("msgrcv");
return -1;
}
strcpy(buff,ms.mtext);
return 0;
}
server.c
#include"comm.h"
int main()
{
int msgid = createMsQueue();
char buf[1024];
while(1)
{
rcvMsg(msgid,CLIENT_TYPE,SERVER_TYPE,buf);
printf("client say:%s\n",buf);
printf("Please Enter#");
fflush(stdout);
int s = read(0,buf,sizeof(buf));
if(s < 0)
{
perror("read");
return -1;
}
buf[s-1] = 0;
sendMsg(msgid,SERVER_TYPE,buf);
sleep(1);
}
destroyMsQueue(msgid);
return 0;
}
client.c
#include"comm.h"
int main()
{
int msgid = getMsQueue();
char buf[1024];
while(1)
{
printf("Please Enter#");
fflush(stdout);
int s = read(0,buf,sizeof(buf));
if(s < 0)
{
perror("read");
return -1;
}
buf[s-1] = 0;
sendMsg(msgid,CLIENT_TYPE,buf);
rcvMsg(msgid,SERVER_TYPE,CLIENT_TYPE,buf);
printf("server say:%s\n",buf);
}
return 0;
}
通过消息队列实现在两个终端中的两个进程中进行通信
信号量
信号量的本质是⼀种数据操作锁,也就是说其本⾝不具有数据交换的功能,⽽是通过控制其他的通信资源(⽂件,外部设备)来实现进程间通信, 它本⾝只是⼀种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能,对临界资源实现一个保护;在保护过程中需要保证信号量的操作时原子的,因此除了创建和控制两个接口之外,还存在一个进行PV操作的接口,其操作过程为原子的。并且由于信号量作为一个计数器记录资源可用,在申请后必须进行初始化。需要主要的一点是,信号量在申请时为一个集合,并未一个单个计数器。
初始化使用的接口为控制接口,其最后一个可变参数在初始化时为一个union semum结构,定义如下:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
struct seminfo * _buf;
};
其中val为初始化的数值。
PV操作的接口为int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
id与之前相同,其中的sembuf结构体定义如下:
struct sembuf{
short sem_num;//除非使用一组信号量,否则它为0
short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
这里的flg中的可选值SEM_UNDO可以实现回滚,防止死锁:程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。
下面进行一个实例证明信号量对临界区域的保护机制,父子进程同时向屏幕输出,分别输出AA,BB,保证输出时分开输出。
comm.h
#ifndef _COMM_H_
#define _COMM_H_
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<unistd.h>
#define PATHNAME "."
#define PROJ_ID 0x456
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int createSemSet(int nums);
int getSemSet(int nums);
int destroySemSet(int semid);
int initSemSet(int semid,int nums,int val);
int P(int semid,int which);
int V(int semid,int which);
#endif
comm.c
#include"comm.h"
static int commSemSet(int nums,int flags)
{
int 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 createSemSet(int nums)
{
return commSemSet(nums,IPC_CREAT | IPC_EXCL |0666);
}
int getSemSet(int nums)
{
return commSemSet(nums,IPC_CREAT);
}
int destroySemSet(int semid)
{
if(semctl(semid,0,IPC_RMID)<0)
{
perror("semctl");
return -1;
}
return 0;
}
int initSemSet(int semid,int nums,int val)
{
union semun s_un;
s_un.val = val;
if(semctl(semid,nums,SETVAL,s_un)<0)
{
perror("semctl");
return -1;
}
return 0;
}
int sem_op(int semid,int op,int which)
{
struct sembuf sem;
sem.sem_num = which;
sem.sem_op = op;
sem.sem_flg = 0;
return semop(semid, &sem,1);
}
int P(int semid,int which)
{
return sem_op(semid,-1,which);
}
int V(int semid,int which)
{
return sem_op(semid,1,which);
}
testSem.c
#include"comm.h"
int main()
{
int semid = createSemSet(1);
initSemSet(semid,0,1);
pid_t id = fork();
while(1)
{
if(id<0)
{
perror("fork");
return -1;
}
else if(id == 0)//父进程
{
P(semid,0);申请资源
printf("A");打印第一个A
fflush(stdout);//刷新缓冲区
usleep(2422);//为了错开时间,保证没有信号量保护时,打印时乱序
printf("A ");//打印第二个A
fflush(stdout);
usleep(1222);
V(semid,0);
sleep(1);
}
else
{
int semid = getSemSet(0);
P(semid,0);
printf("B");
fflush(stdout);
usleep(2123);
printf("B ");
fflush(stdout);
usleep(2111);
V(semid,0);
sleep(1);
}
}
destroySemSet(semid);
return 0;
}
最终实现对显示器资源的保护,打印过程中有序,且分离。
共享内存
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程,其最大的特点为速度最快,但同时共享内存并未提供同步互斥机制,因此可以结合上文中的信号量一起使用,使用信号量实现同步互斥,运用共享内存实现进程通信。
创建接口shmget(key_t key,size_t size,int shmflg);
其中第二个参数,size以字节为单位指定需要共享的内存容量,建议使用一页或一页的整数倍(即4K),因为申请中系统为了避免内存碎片问题会申请4k整数倍大小空间,但会返回用户申请大小, 例如用户申请4097大小,系统会开辟8K,但只返回4097大小空间。
还有一对接口void *shmat(int shm_id, const void *shm_addr, int shmflg);
和int shmdt(const void *shmaddr);
关联与去关联
第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间,同理,dt函数则为将共享内存从当前进程中分离。
下面对共享内存进行测试,由于没有同步与互斥,因此分开进行写和读
comm.h
#ifndef _SHM_H_
#define _SHM_H_
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string.h>
#define PATHNAME "."
#define PROJ_ID 0x333
#define shmsize 4096
int creatShm();
int getShm();
void destroyShm(int shmid);
#endif
comm.c
#include"comm.h"
static int commShm(int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int shmid = shmget(key,shmsize,flags);
return shmid;
}
int creatShm()
{
return commShm(0666|IPC_CREAT|IPC_EXCL);
}
int getShm()
{
return commShm(IPC_CREAT);
}
void destroyShm(int shmid)
{
shmctl(shmid,IPC_RMID,0);
}
client.c
#include"comm.h"
int main()
{
int shmid = getShm();
int count = 10;
char *buf = "hello server!";
char * shm = shmat(shmid,NULL,0);
while(count--)
{
strcpy(shm,buf);
}
return 0;
}
server.c
#include"comm.h"
int main()
{
int shmid = creatShm();
int count = 10;
char * shm = shmat(shmid,NULL,0);
sleep(10);
while(count--)
{
printf("%s\n",shm);
sleep(1);
}
destroyShm(shmid);
}
先运行server进行创建并sleep10秒,再运行client写10个数据。