进程间通信
我们知道,在进程运行期间,每个进程都是相互独立的,而有的时候,我们需要将多个一个进程的数据传输给另外一个进程,或者实现多个进程共享同一份资源,又或者是一个进程发消息给另外一个进程,此时。就需要进行多个进程之间的通信,本文将在linux操作系统下简介进程间通信的基本概念以及方法。
1.进程间通信的分类
*管道通信(分匿名管道和命名管道)
*System V IPC(主要有三种方式:消息队列,共享内存,信号量)
管道通信
1.管道的概念
管道是unix操作系统下一种古老的通信方式,我们把一个一个进程连接到另外一个进程的数据流称之为管道,就像生活中的水管道一样,两个进程可以通过管道来进行数据的传输。
下面我们将站在文件描述的角度,来深入理解管道。我们知道,当一个程序变成进程的时候,操作系统会给这个进程分配PCB资源,每个进程拥有4G的虚拟地址空间,并且建设起来虚拟地址到物理地址之间的映射,而每个PCB中都有一个*file的字段,它指向一个files_struct的结构体,这个结构体保存着进程打开文件的情况,每个进程默认打开0,1,2三个分表代表标准输入,标准输出,标准出错的文件。我们可以创建一个管道,即打开一个文件,然后file_struct里面就多了两个文件描述符,一个指向文件的写端,一个指向文件的读端,当我们创建子进程的时候,子进程和父进程一样,打开这个文件的读写端,当我们关闭子进程读端和父进程写端的时候,就可以利用文件描述符来进行不同进程之间的通信了。这就是我们下面将介绍的匿名管道。
2.匿名管道
linux下,我们需要调用pipe()来创建匿名管道,
函数原型: int pipe(int fd[2]);
这个函数接受一个只有两个元素的整形数组,是一个输出型参数,调用完成后fd[0]表示读端,fd[1]表示写端,成功调用返回0,失败调用返回错误代码。
下面我们来写一个小程序。
#include<stdio.h> 2 #include<stdlib.h> 3 #include<string.h> 4 #include<unistd.h> 5 int main() 6 { 7 8 int fd[2]; 9 int len; 10 int ret=pipe(fd); 11 if(ret!=0) 12 { 13 perror("pipe"); 14 exit(1); 15 } 16 pid_t pid=fork(); 17 if(pid==-1) 18 { 19 printf("fork"); 20 exit(1); 21 } 22 if(pid==0) 23 {//child 24 close(fd[0]); 25 write(fd[1],"hello",5); 26 close(fd[1]); 27 exit(1); 28 } 29 close(fd[1]); 30 char buf[10]={0}; 31 read(fd[0],buf,10); 32 printf("buf is:%s",buf); 33 return 0; 34 }
这个程序就是利用我们的匿名管道来进程父子进程之间的通信。
我们来总结一下管道的特点。
1.管道只能进行单向的数据通信。如果要进行双向通信,就要建立两个管道。
2.管道的生命周期随进程,进程结束,管道就是释放。
3.管道只适用于父子或者有亲缘关系的进程之间的通信。
4.带同步机制。
5.管道在通信时是面向字节流的。
同时,我们还介绍两个概念,我把两个进程看到的同一份公共资源称之为临界资源,相同的代码区成为临界区,我们可以看出,在访问临界资源的时候,是必须要互斥的。同时还要保持访问资源时候的原子性,即要么做,要么不做,不可以做到一半不做了。
管道的读写规则。
既然我们利用管道来进行单向的数据传输,那么就会有下面几种情况出现。
1.当读端没有数据可以从管道中读取的时候,read调用阻塞,进程暂停,等到有数据写入的时候再解除阻塞。
2.如果读端被关闭,而写端还在往里面写数据,此时操作系统就会给进程发送一个13号信号关闭。
3.命名管道
我们同样可以利用命令mkfifo来创建一个命名管道或者调用mkfifo(const char* filename,mode_t mode)接口来创建,这里权限我们通常设置成0644。
那么匿名管道和命名管道有什么区别呢?
1.匿名管道是通过pipe函数来创建的,而命名管道可以通过命令来创建。
2.命名管道即创建一个有名字的管道文件,打开用open。
3.一旦这些工作做完,那么两者将具有相同的意义。
我们下面来利用命名管道实现一个小的聊天系统,实现两个进程之间的会话。
client.c
#include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/stat.h> 4 #include<stdlib.h> 5 #include<unistd.h> 6 #include<fcntl.h> 7 #include<string.h> 8 int main() 9 { 10 int wfd=open("mypipe",O_WRONLY); 11 if(wfd<0) 12 { 13 perror("open"); 14 exit(1); 15 } 16 char buf[1024]; 17 while(1) 18 { 19 buf[0]=0; 20 printf("please Enter:"); 21 fflush(stdout); 22 ssize_t s=read(0,buf,sizeof(buf)-1); 23 if(s>0) 24 { 25 buf[s]=0; 26 write(wfd,buf,strlen(buf)); 27 28 } 29 else if(s==0) 30 exit(0); 31 32 } 33 close(wfd); 34 return 0; 35 }
server.c
1 #include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/stat.h> 4 #include<fcntl.h> 5 #include<unistd.h> 6 #include<stdlib.h> 7 int main() 1 #include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/stat.h> 4 #include<fcntl.h> 5 #include<unistd.h> 6 #include<stdlib.h> 7 int main() 8 { 9 if(mkfifo("mypipe",0644)<0) 10 { 11 perror("mkfifo"); 12 exit(0); 13 } 14 int rfd=open("mypipe",O_RDONLY); 15 if(rfd<0) 16 { 17 perror("open"); 18 exit(1); 19 } 20 char buf[1024]; 21 while(1) 22 { 23 buf[0]=0; 24 printf("please wait ...\n"); 25 ssize_t s=read(rfd,buf,sizeof(buf)-1); 26 if(s>0) 27 { 28 29 buf[s-1]=0; 30 printf("client say:%s\n",buf); 31 32 } 33 else if(s==0) 34 { 35 printf("client quit,please quie now\n"); 36 exit(1); 37 } 38 else { 39 exit(1); 40 } 41 42 } 43 close(rfd); 44 return 0;
消息队列
前面我们讲了管道的两种形式,下面我们来看看利用系统IPC资源来实现进程间通信的几种方式,首先是消息队列。
首先,消息队列提供了一个进程向另外一个进程发送有类型数据块的方法,这样做的目的就是为了方式通信时候拿到自己发送出去的数据块,在进行通信的时候,两个进程看到同一份IPC资源,然后一个进程将自己发送的数据块放入消息队列当中,另外一个进程从队头拿出对方发送的数据块。
特点:消息队列是系统的IPC资源,所以它的生命周期随内核,我们需要手动的删除。不然会造成资源的泄露。
下面我们来看一看几个消息队列的系统调用接口。
1.msgget()
原型:int msgget(key_t key,int msgflg)
说明:这个函数接受两个参数,key是两个进程看到同一份IPC资源的名字,由ftok()函数产生,需要用户自定义,msgflg由9个权限标志位构成,它们的用法和创建文件时候mode是一样的,我们一般使用IPC_CREAT|IPC_EXCL表示如果调用成功,创建的就是一个全新的消息队列,函数成功返回消息队列的标识码,失败返回-1
2.msgctl()
原型:int msgctl(int msqid,int cmd,struct msqid_ds*buf);
说明:msqid就是消息队列的id,由msgget产生,cmd表示要采取的动作,一般有三种,删除为IPC_RMID。最后一个参数一般设置为0.
3.msgsnd()
原型:int msgsnd(int msqid,const void*msgp,size_t msgsz,int msgflg);
说明:msqid是消息队列标识码,msgp是指向要发送消息,msgsz表示msgp指向消息长度,msgflg控制着当前消息队列满或者达到系统上限时候发生的动作。默认0;IPC_WAIT表示队列满不等待,返回错误。
4.msgrcv()
原型:int msgrcv(int msqid,const void*msgp,size_t msgsz,long msgtyp
,int msgflg)
说明:msqid是消息队列的标识符,msgp是指向准备接收的消息,msgsz指向消息长度,这个长度不包括存放消息类型的那个long int,msgfalg控制着队列中没有相应类型的消息可供接受时候发生的事。成功返回实现放到接收缓冲区里面的字符个数,失败返回-1.
而我们需要用户自定义消息都列中数据块 msgbuf 的结构体
struct msgbuf
{
long mtype;
long mtype;
char mtxt[1];
}
}
下面我们就来写代码:
comm.h
1 #ifndef _COMM_H_ 2 #define _COMM_H_ 3 #include<stdio.h> 4 #include<stdlib.h> 5 #include<string.h> 6 #include<sys/types.h> 7 #include<sys/ipc.h> 8 #include<sys/msg.h> 9 #define PATHNAME "./" 10 #define PROJ_ID 0x666 11 #define SERVER_TYPE 1 12 #define CLIENT_TYPE 2 13 struct msgbuf{ 14 15 long mtype; 16 char mtext[1024]; 17 }; 18 int creatMsgQueuq(); 19 int getMsgQueuq(); 20 int destroyMsgQueu(int msgid); 21 int sendMsg(int msgid,int who,char*msg); 22 int recvMsg(int msgid,int recvType,char out[]); 23 #endif
comm.c
1 #include"comm.h" 2 static int commMsgQueue(int flag) 3 { 4 5 key_t _key=ftok(PATHNAME,PROJ_ID); 6 if(_key<0) 7 { 8 perror("ftok"); 9 return -1; 10 } 11 int msgid=msgget(_key,flag); 12 if(msgid<0) 13 { 14 perror("msgget"); 15 return -1; 16 } 17 return msgid; 18 } 19 int createMsgQueue() 20 { 21 22 return commMsgQueue(IPC_CREAT|IPC_EXCL|0666); 23 } 24 int getMsgQueue() 25 { 26 27 return commMsgQueue(IPC_CREAT); 28 } 29 int destoryMsgQueue(int msgid) 30 { 31 if(msgctl(msgid,IPC_RMID,NULL)<0) 32 { 33 perror("msgctl"); 34 return -1; 35 } 36 return 0; 37 38 } 39 int sendMsg(int msgid,int who,char *msg) 40 { 41 struct msgbuf buf; 42 buf.mtype=who; 43 strcpy(buf.mtext,msg); 44 if(msgsnd(msgid,(void *)&buf,sizeof(buf.mtext),0)<0) 45 { 46 perror("msgsnd"); 47 return -1; 48 } 49 50 return 0; 51 } 52 int recvMsg(int msgid,int recvType,char out[]) 53 { 54 struct msgbuf buf; 55 if(msgrcv(msgid,(void*)&buf,sizeof(buf.mtext),recvType,0)<0) 56 { 57 perror("msgrcv"); 58 return -1; 59 } 60 strcpy(out,buf.mtext); 61 return 0; 62 }
client.c
1 #include"comm.h" 2 int main() 3 { 4 int msgid=createMsgQueue(); 5 char buf[1024]; 6 while(1) 7 { 8 buf[0]=0; 9 printf("please Enter:"); 10 fflush(stdout); 11 ssize_t s=read(0,buf,sizeof(buf)); 12 if(s>0) 13 { 14 buf[s-1]=0; 15 sendMsg(msgid,CLIENT_TYPE,buf); 16 printf("send done,wait recv..\n"); 17 } 18 recvMsg(msgid,SERVER_TYPE,buf); 19 printf("server# %s\n",buf); 20 } 21 return 0; 22 } ~
server.c
1 #include"comm.h" 2 int main() 3 { 4 5 int msgid=createMsgQueue(); 6 char buf[1024]; 7 while(1) 8 { 9 buf[0]=0; 10 recvMsg(msgid,CLIENT_TYPE,buf); 11 printf("client#%s",buf); 12 printf("please enter #"); 13 fflush(stdout); 14 ssize_t s=read(0,buf,sizeof(buf)); 15 if(s>0) 16 { 17 18 buf[s-1]=0; 19 sendMsg(msgid,SERVER_TYPE,buf); 20 printf("send done,wait recv ...\n"); 21 } 22 } 23 destroyMsgQueue(msgid); 24 return 0; 25 26 }
既然我们利用的系统IPC资源来进行进程间通信,那么一定要了解两个命令来查看 和删除IPC资源,不然就会造成系统资源的泄露。
1.IPCS
IPCRM(删除系统IPC资源)
共享内存
我们都知道,每个进程都一份PCB,有自己独立的虚拟空间,同时也有对应的页表来进行虚拟地址到物理地址的映射,共享内存就是让两个进程通过页表的映射看到同一份物理地址上的文件,这样实现了进程间的通信,并且省去了前面所讲的通信方式之中数据的拷贝过程,是进程之间通信最高效率的手段。
系统调用接口:
shmget()
原型:int shmget(key_t key,size_t size,int shmflg)
说明:key就是共享内存段的名字,size是内存大小,shmflg由九个标志位,和创建文件时mode用法一样。
shmat()
原型:void * shmat(int shmid,const void *shmaddr,int shmflg)
说明:shmid共享内存段名字,shmaddr指定连接地址一般设置成NULL系统自动分配,shmflg两个标志位,一般设为0.
shmdt()
原型:int shmdt(const void *shmaddr)
说明:指的是由shmat返回的地址
shmctl()
原型:int shmctl(int shmid,int cmd,struct_ds*buf)
说明:shmid共享内存标识码,cmd要采取的动作,buf指向一个保存着共享内存的模式状态和访问权限的数据结构。
信号量
我们把两个进程看到的同一份资源叫做临界资源,把进程访问临界资源的代码区叫做临界区。下面将的信号量就是一个主要用来同步和互斥的,他本身相当于一个计数器,衡量着临界资源的多少,我们把二元信号量就叫做互斥锁。P操作表示申请资源,对应信号量减一,v操作表示释放临界资源 ,信号量加一。
并且pv操作要在访问临界区前后进行,并且我们必须保证P,V操作的原子性,其中信号量S>0表示的是资源池中可用临界资源的个数,而S=0表示无可用资源,S<0表示等待队列中进程个数。
系统调用函数:
semget()
原型:int semget(key_t key,int nsems,int semflg)
说明:key,信号集的标识符,nsems表示信号集中信号的个数,semflg由九个权限位构成,用法和创建文件时使用的mode是一样的。
semctl()
原型:int semctl(int semid,int semmum,int cmd,...)
说明:semid 信号量集的标识符,senmmum信号集中信号量的序号,cmd将要采取的三个动作SETVAL,这是一个可变参数列表,最后一个参数根据命令的不同而不同。
semop()
原型: int semop(int semid,struct sembuf*sops,unsigned nsops);
说明:semid 信号量集的标识码,也就是semget函数的返回值。
sops是一个指向一个结构数值的指针,nsops信号量的个数
struct sembuf
{
short sem_num;
short sem_op;
short sem_flg;
}
}
sem_num是信号量的编号,sem_op是信号量一次PV操作加减的数值,一般只会用到两个值,V操作sem_op=1,而p操作sem_op=-1,sem_flag的两个取值是IPC_NOWAIT和SEM_UNDO(回滚操作)
下面来写个代码:
comm.h
1 #ifndef _COMM_H_ 2 #define _COMM_H_ 3 #include<stdio.h> 4 #include<sys/types.h> 5 #include<sys/ipc.h> 6 #include<sys/sem.h> 7 #define PATHNAME "./" 8 #define PROJ_ID 0x6666 9 union semun 10 { 11 int val; 12 struct semid_ds *buf; 13 unsigned short *array; 14 struct seminfo *__buf; 15 16 }; 17 int createSemSet(int nums); 18 int initSem(int semid,int nums,int initVal); 19 int getSemSet(int nums); 20 int P(int semid, int who); 21 int V(int semid, int who); 22 int destorySemSet(int semid); 23 #endif
comm.c
1 #include"comm.h" 2 static int commSemSet(int nums,int flags) 3 { 4 key_t _key=ftok(PATHNAME,PROJ_ID); 5 if(_key<0) 6 { 7 perror("ftok"); 8 return -1; 9 } 10 int semid=semget(_key,nums,flags); 11 if(semid<0) 12 { 13 printf("semget"); 14 return -1; 15 } 16 return semid; 17 } 18 int createSemSet(int nums) 19 { 20 return commSemSet(nums,IPC_CREAT|IPC_EXCL|0666); 21 22 } 23 int getSemSet(int nums) 24 { 25 return commSemSet( nums,IPC_CREAT); 26 } 27 int initSem(int semid,int nums,int initval) 28 { 29 union semun _un; 30 _un.val=initval; 31 if(semctl(semid,nums,SETVAL,_un)<0) 32 { 33 perror("semctl"); 34 return -1; 35 } 36 return 0; 37 } 38 static int commPV(int semid,int who,int op) 39 { 40 struct sembuf _sf; 41 _sf.sem_num=who; 42 _sf.sem_op=op; 43 _sf.sem_flg=0; 44 if(semop(semid,&_sf,1)<0) 45 { 46 perror("semop"); 47 return -1; 48 } 49 return 0; 50 } 51 int P( int semid,int who) 52 { 53 54 return commPV(semid,who,-1); 55 } 56 int V(int semid,int who) 57 { 58 return commPV(semid,who,1); 59 60 } 61 int destroySemSet(int semid) 62 { 63 64 if(semctl(semid,0,IPC_RMID)<0) 65 { 66 perror("semctl"); 67 return -1; 68 } 69 }
test_sem.c
2 int main() 3 { 4 int semid=createSemSet(1); 5 initSem(semid,0,1); 6 pid_t id=fork(); 7 if(id==0) 8 { 9 //child 10 int _semid=getSemSet(0); 11 while(1) 12 { 13 P(_semid,0); 14 printf("A"); 15 fflush(stdout); 16 usleep(123456); 17 printf("A"); 18 fflush(stdout); 19 usleep(321456); 20 V(_semid,0); 21 } 22 } 23 else 24 { 25 while(1) 26 { 27 P(semid,0); 28 printf("B"); 29 fflush(stdout); 30 usleep(223456); 31 printf("B"); 32 fflush(stdout); 33 usleep(123456); 34 V(semid,0); 35 } 36 wait(NULL); 37 } 38 destroySemSet(semid); 39 return 0; 40 }