五、进程间通信(IPC: Inter Processes Communication)
1、概念
-
进程是一个独立的资源分配单元,不同进程(主要是用户进程)之间的资源是独立的,不能再一个进程中直接访问另一个进程的资源。
-
但是进程不是孤立的,不同进程间需要进行信息交互和状态的传递等,因此需要进程间通信。
-
进程间通信的目的:
-
数据传输:一个进程需要将他的数据发送给另一个进程。
-
通知事件:一个进程需要向另一个或一组进程发送消息,通知它发生了某种事件(如进程终止时要通知子进程)。
-
资源共享:多个进程之间共享同样的资源。(需要内核提供互斥和同步机制)
-
进程控制:有些进程希望完全控制力另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够即使知道它的状态改变。
-
2、Linux进程间通信的方式
-
同一主机进程间通信
-
Unix进程间通信方式
-
匿名管道
-
有名管道
-
信号
-
-
System V和POSIX进程间通信方式
-
消息队列
-
共享内存
-
内存映射
-
信号量
-
-
-
不同主机(网络)进程间通信:Socket
匿名管道(PIPE)
管道也叫无名(匿名)管道,它是Unix系统IPC的最古老方式,所有Unix系统都支持这种通信方式。
-
管道的特点:
(1)管道其实是一个在内核内存中维护的缓冲器,这个缓冲区的存储能力是有限的,不同的操作系统大小不一定相同。
(2)管道拥有文件的特质:可以对管道进行读、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
(3)一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
(4)通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和他们被写入管道的顺序是完全一样的
(5)在管道中的数据是单向传递的,一端用于写,另一端用于读,管道时半双工的。
(6)从管道读取数据是一次性操作,数据一旦被读走,他就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用lseek()来随机访问数据。
(7)匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系的进程)之间使用
-
管道的数据结构:循环队列
-
管道的使用
-
创建匿名管道
#include<unistd.h> int pipe(int pipefd[2]);
-
作用:创建一个匿名管道,用来进程间通信
-
参数:int pipefd[2]是一个传出参数
pipefd[0]对应的是管道的读端;
pipefd[1]对应的是管道的写端。
-
返回值:
成功:返回0
失败:返回-1
注意:(1)匿名管道只能用于具有关系的进程之间的通信;
(2)管道默认是阻塞的:
如果管道中没有数据,read阻塞;
如果管道满了,write阻塞。
利用匿名管道实现父子进程之间的通信:
#include<unistd.h> #include<stdio.h> #include<sys/types.h> #include<stdlib.h> #include<string.h> //子进程发送数据给父进程,父进程读取到数据输出 int main(){ //fork之前创建管道 int pipefd[2]; int ret=pipe(pipefd); if(ret==-1){ perror("pipe"); exit(0); } //创建子进程 pid_t pid=fork(); if(pid>0){ //父进程 printf("I am child process,pid:%d",getpid()); char buf[1024]={0}; while(1){ //从管道的读取端读数据 int len=read(pipefd[0],buf,sizeof(buf)); printf("parent receive :%s,pid:%d\n",buf,getpid()); //写数据 char *str="hello,I am parent"; write(pipefd[1],str,strlen(str)); sleep(1); } }else if(pid==0){ //子进程 printf("I am child process,pid:%d",getpid()); char buf[1024]={0}; while(1){ //写数据 char *str="hello,I am child"; write(pipefd[1],str,strlen(str)); sleep(1); int len=read(pipefd[0],buf,sizeof(buf)); printf("child receive :%s,pid:%d\n",buf,getpid()); } }else{ //出错 perror("fork"); exit(0); } return 0; }
-
-
查看管道缓冲大小的命令
ulimit -a
-
查看管道缓冲大小的函数
#include<unistd.h> long fpathconf(int fd,int name);
利用fpathconf查看管道缓冲大小
#include<unistd.h> #include<sys/types.h> #include<stdlib.h> #include<stdio.h> int main(){ int pipefd[2]; int ret=pipe(pipefd); if(ret==-1){ perror("pipe"); exit(0); } //获取管道的大小 long size=fpathconf(pipefd[0],_PC_PIPE_BUF); printf("pipe size:%ld\n",size); return 0; }
-
匿名管道通信案例:实现ps aux | grep xxx 父子进程间通信
/* 父子进程间通信 子进程:ps aux,子进程结束后,将数据发送给父进程, 将标准输出重定向到管道的写端 父进程:获取到数据,过滤 */
#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<wait.h>
#include<string.h>
int main(){
//创建一个管道
int fd[2];
int ret=pipe(fd);
if(ret==-1){
perror("pipe");
exit(0);
}
//创建子进程
pid_t pid=fork();
if(pid>0){
//父进程
//关闭写端
close(fd[1]);
//从管道中读数据
char buf[1024]={0};
int len=-1;
while((len=read(fd[0],buf,sizeof(buf)-1))>0){
//过滤数据输出
printf("%s",buf);
//清空数据
memset(buf,0,1024);
}
wait(NULL);
}else if(pid==0){
//子进程
//关闭读端
close(fd[0]);
//文件重定向 std_fileno->fd[1]
dup2(fd[1],STDOUT_FILENO);
//执行ps aux
execlp("ps","ps","aux",NULL);
perror("execlp");
exit(0);
}else{
perror("fork");
exit(0);
}
return 0;
}
-
管道读写的特点
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作):
(1)所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取后,再次read会返回0,类似于读到文件末尾。
(2)如果有指向管道写端的文件描述符没有关闭(管道写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才会读取数据并返回。
(3)如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常会导致程序异常终止。
(4)如果有指向管道独断的文件描述符没有关闭(管道读端引用计数大于0),而持有管道独断的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满时再次write会被阻塞,直到管道中有空位置才能再次写入数据并返回。
总结:
读管道:
-管道中有数据,read返回实际读到的字节数。
-管道中无数据:
写端被完全关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
写管道:
-管道读端被完全关闭,进程异常终止(进程收到SIGPIPE信号)
-管道读端未完全关闭:
管道已满:write阻塞
管道未满:write将数据写入,并返回实际写入的字节数
-
设置管道非阻塞
fcntl函数
int fd[2]; int ret=pipe(fd); //获取原来的flag int flags=fcntl(fd[0],F_GETFL); //修改flag的值 flags |= O_NONBLOCK; //设置新的flag fcntl(fd[0],F_SETFL,flags);