进程间通信(IPC)
Linux环境下,进程地址空间相互独立,任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能互相访问,要交换数据必须通过内核,在内核中开辟一块缓冲区(4096,buf),进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据拷走,内核提供的这种机制称为进程间通信(IPC)。
常用的进程间通信的方式有:
1、管道(使用最简单,要求有血缘关系)
2、信号(开销最小)
3、共享映射区(无血缘关系)
4、本地套接字(最稳定)
管道
其本质是一个伪文件(实际上为内核缓冲区)
管道的原理:内核使用环形队列机制,借助内核缓冲区实现。
特质:1、 伪文件 2、管道中的数据只能一次读取 3、数据在管道中只能单向流动
局限性:1、自己写不能自己读 2、数据不可以反复读取 3、半双工通信 4、血缘关系进程间可用
pipe函数
创建并打开管道。父子进程共享文件描述符
int pipe(int fd[2]);
参数:fd[0]:读端
fd[1]:写端
成功返回0
失败返回-1 errno
管道的读写行为 fd[0]读端 、 fd[1]写端
读管道:管道有数据,read返回实际读到的字节数
管道无数据:1、无写端,read返回0(读到文件结尾)2、有写端,read阻塞等待
写管道:无读端,异常终止(由于sigpipe)
有读端:1、管道已满,阻塞等待 2、阻塞未满,返回写出的字节个数
使用管道实现父子间进程通信,完成ls |wc-l 父进程实现ls , 子进程实现wc-l (最后需要让父进程区实现wc , 子进程实现ls)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
int main(int argc , char *argv[])
{
pid_t pid;
int fd[2];
//char buf[1024];
int ret;
// int a;
ret = pipe(fd);
if(ret == -1){
perror("pipe error");
exit(1);
}else if(ret == 0){
pid = fork();
if(pid > 0){
close(fd[0]);
//execlp("ls" , "ls" , NULL);
dup2(fd[1] , STDOUT_FILENO);
execlp("ls" , "ls" , NULL);
//write(fd[1] , buf , sizeof(buf));
close(fd[1]);
}else if(pid ==0){
close(fd[1]);
dup2(fd[0] , STDIN_FILENO);
//ret = read(fd[0] , buf ,sizeof(buf));
execlp("wc" , "wc" , "-l" , NULL );
close(fd[0]);
}
}
return 0 ;
}
兄弟进程间通信 fd[0]:读端 fd[1]:写端
int main(int argc , char* argv[])
{
pid_t pid;
int i ;
int fd[2];
for(i = 0 ; i < 2 ; i++)
{
pid = fork();
if(pid == 0){
break;
}
}
if( i == 2 ){ //父进程
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
}else if(i == 0){ //兄进程 实现ls;
close(fd[0]);
dup2(fd[1] , STDIN_FILENO);
execlp("ls" , "ls" , NULL);
}else if(i == 1){
close(fd[1]);
dup2(fd[0] , STDOUT_FILENO);
execlp("wc" , "wc" , "-l" , NULL);
}
}
管道的优点:简单,相比信号,套接字时间进程间通信简单很多。
缺点:只能单向通信,双向通信需要建立两个管道 。 只能用于父子、兄弟进程间通信。
FIFO
命名管道,makefifo 管道名
int mkfifo("管道名" , 权限/0644)
共享存储映射、存储映射I/O
mmap函数
创建共享内存映射 , 需要包含头文件#include<sys/mman.h>
void *mmap(void*addr , size_t length , int prot , int flags, int fd , off_t offset);
参数:addr :指定映射区的首地址 , 通常传NULL,表示让系统自动分配。
length:共享内存映射区的大小 (< = 文件实际大小)
prot:共享内存映射区的读写属性 PROT_READ、PROT_WRITE 、 |。
flags:标注共享内存的共享属性。MAP_SHARED、 MAP_PRIVATE
fd:用于创建共享映射区的那个文件的文件描述符
offset :默认0,表示映射文件全部。偏移位置。4K的整数倍。
返回值:
成功:返回映射区的首地址。
失败:MAP_FAILED(void*(-1)) 设置errno。map_failed
munmap函数
释放映射区
int munmap(void *addr , size_t length)
参数:addr : mmap的返回值 。 length 大小
返回值:成功返回0,失败返回-1
mmap 和 munmap函数使用实例:
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/mman.h>
int main(int argc , char*argv[])
{
char*p = NULL;
int fd ;
fd = open("textmmap" , O_WRDR|O_CREAT|O_TRUNC , 0644);
lseek(fd , 20 , SEEL_END);
//引起文件IO操作
write(fd , "\0" , 1);
int len = lseek(fd , 0 , SEEK_END);
p = mmap(NULL, len , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);
if(p == MAP_FAILED){
perror("mmap error");
exit(1);
}
//接着可以使用对指针的操作对文件进行操作
strcpy(p , "hello mmap\n") ;
printf("-----%s" , p);
//清空文件映射区
int ret = munmap( p , len);
if(ret == -1){
perror("munmap error");
exit(1);
}
close(fd);
return 0;
}
mmap的注意事项
1、用于创建映射区的文件大小为0,实际指定非0 大小创建映射区,会出现总线错误。
2、用于创建映射区的文件大小为0,实际指定 0 大小创建映射区,会出现错误(无效参数)。
3、用于创建映射区的文件读写属性为只读,映射区属性为读写,会出现错误(无效参数)。
4、创建映射区需要read权限,当访问权限指定为MAP_SHARED时, mmap的读写权限应该<=文件的open权限。说明mmap必须要有读权限,只有写是不行的。
5、文件描述符fd在mmap创建映射区完成即可关闭,后续访问文件,用地址访问。
6、offset必须是4096的整数倍。(mmu映射的最小单位是4k)。
7、对申请的映射区内存不能越界访问。
8、munmap用于释放的地址,必须要是mmap返回的地址。
9、映射区的访问权限为私有MAP_PRIVATE , 对内存所做修改,只在内存有效,不会返回到物理磁盘上。
mmap函数的保险调用方式:
1、open的时候指定O_RDWR;
2、mmap(NULL , 有效文件大小 , PROT_WRITE|PROT_READ , MAP_SHARED , fd , 0);
总结:mmap创建映射区出错概率非常高,一定要检查返回值 MAP_FAILED