进程间通信常见方式 Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,Inter Process Communication
).
在进程间完成数据传递需要借助操作系统提供特殊的方法 ,如:文件、管道、信号、内存共享、消息队列、套接字、命名管道等。常用的进程间通信方式有:
管道(使用最简单)
信号(开销最小),只能携带固定的少量信息
共享映射区
mmap
函数的参数使用注意事项
用于非血缘关系的进程通信
本地套接字(最稳定)
管道 基本概念 管道是一种最基本的IPC
机制,作用于有血缘关系的进程之间,完成数据传递 。调用pipe
系统函数即可创建一个管道。有如下特质:
其本质是一个伪文件 (实为内核缓冲区 )
有两个文件描述符引用 ,一个表示读端 ,一个表示写端
规定数据从管道的写端流入管道,从读端流出
管道的原理:管道实为内核使用环形队列机 制,借助内核缓冲区 (4K)实现。
管道的局限性:
数据不能自己读自己写
数据一旦被读走,便不在管道中存在,不可反复读取
由于管道采用半双工通信 方式。因此数据只能在一个方向上流动
只能在有公共祖先的进程间使用管道
管道使用方法 pipe
函数: 创建并打开 管道。
函数调用成功返回r/w
两个文件描述符,无需open
,但需手动close
。规定:fd[0] :r, fd[1]:w
, 类似于0对应标准输入,1对应标准输出 。向管道文件读写数据其实是在读写内核缓冲区。
1 2 3 4 5 int pipe (int pipefd[2 ]) ;
刚fork
完成时父进程关闭读端,子进程关闭写端,此时数据能在pipe
中单向流动,父子进程能够完成通信 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> int main (int argc,char * argv[]) { int ret=0 ; int pipefd[2 ]; pid_t pid=0 ; char * str="loveyou\n" ; char buf[1024 ]; ret=pipe(pipefd); if (ret==-1 ) perr_exit("pipe error" ); pid=fork(); if (pid>0 ){ close (pipefd[0 ]); write (pipefd[1 ],str,sizeof (str)); close (pipefd[1 ]); }else if (pid==0 ){ close (pipefd[1 ]); ret=read (pipefd[0 ],buf,sizeof (buf)); write (STDOUT_FILENO,buf,ret); close (pipefd[0 ]); } return 0 ; }
管道读写行为
读管道
管道中有数据:read
返回实际读到的字节数
管道中无数据:
写端全关闭:read返回0
写端没有被全部关闭 (仍有写端打开), read
阻塞等待(不久的将来可能会有数据抵达, 此时会让出CPU
写管道
管道读端全部被关闭, 进程异常终止 (也可以捕捉SIGPIPE
信号, 使进程不终止)
有读端打开
管道未满:写数据,write
将数据写入,返回写入字节数
管道已满,write
阻塞(少见)
获取管道缓冲区大小: ulimit -a
父子进程 通信练习:实现 ls | wc -l
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> int main (int argc,char * argv[]) { int fd[2 ]; pid_t pid; int ret=0 ; ret=pipe(fd); if (ret==-1 ) perr_exit("pipe error" ); pid=fork(); if (pid==-1 ) perr_exit("fork error" ); if (pid>0 ){ close (fd[1 ]); dup2(fd[0 ],STDIN_FILENO); execlp("wc" ,"wc" ,"-l" ,NULL ); perr_exit("execlp wc error" ); }else if (pid==0 ){ close (fd[0 ]); dup2(fd[1 ],STDOUT_FILENO); execlp("ls" ,"ls" ,NULL ); perr_exit("execlp ls error" ); } return 0 ; }
兄弟进程间通信:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> int main (int argc,char * argv[]) { int fd[2 ]; pid_t pid; int ret=0 ; int i=0 ; ret=pipe(fd); if (ret==-1 ) perr_exit("pipe error" ); for (i=0 ;i<2 ;++i){ pid=fork(); if (pid==-1 ) perr_exit("fork error" ); if (pid==0 ) break ; } if (i==2 ){ close (fd[0 ]); close (fd[1 ]); wait(NULL ); wait(NULL ); }else if (i==0 ){ close (fd[0 ]); dup2(fd[1 ],STDOUT_FILENO); execlp("ls" ,"ls" ,NULL ); perr_exit("execlp ls error" ); }else if (i==1 ){ close (fd[1 ]); dup2(fd[0 ],STDIN_FILENO); execlp("wc" ,"wc" ,"-l" ,NULL ); perr_exit("execlp wc error" ); } return 0 ; }
注意:管道可以一个读端, 多个写端, 但是不建议这样做。默认管道的大小是4k。
命名管道FIFO 基本概念 为区分pipe
,将FIFO
称为命名管道。FIFO
可用于不相关进程间的数据交换 。
FIFO
是Linux基础文件类型中的一种 , 但是FIFO
文件在磁盘上没有数据块, 仅仅用来标识内核中的一条通道, 各进程可以打开这个文件进行read/write, 实际上是在读写内核通道, 这样就实现了进程间通信
创建方式:
1 2 int mkfifo (const char * pathname,mode_t mode) ;
用FIFO进行通信几乎只有文件读写操作, 比较简单。
文件通信 读普通文件不会造成read
阻塞, 如果子进程睡1秒再写, 父进程由于刚开始读不到数据read
直接返回0。
没有血缘关系的进程也可以用文件进行进程间通信。
使用 写进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> int main (int argc,char * argv[]) { int fd=0 ; int i=0 ; char buf[4096 ]; if (argc<2 ){ printf ("Enter like this:./a.out fifoname\n" ); return -1 ; } fd=open (argv[1 ],O_WRONLY); if (fd==-1 ) perr_exit("open error" ); while (1 ){ sprintf (buf,"love you:%d\n" ,i++); write (fd,buf,strlen (buf)); sleep(1 ); } close (fd); return 0 ; }
读进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> int main (int argc,char * argv[]) { int fd=0 ; int i=0 ; int len=0 ; char buf[4096 ]; if (argc<2 ){ printf ("Enter like this:./a.out fifoname\n" ); return -1 ; } fd=open (argv[1 ],O_RDONLY); if (fd==-1 ) perr_exit("open error" ); while (1 ){ len=read (fd,buf,sizeof (buf)); write (STDOUT_FILENO,buf,len); sleep(1 ); } close (fd); return 0 ; }
MMAP 基本概念
存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射, 于是当从缓冲区中取数据, 就相当于读文件中的相应字节。 与此类似, 将数据存入缓冲区, 则相应的字节就自动写入文件, 这样就可以在不使用read
和write
函数的情况下, 使用指针完成I/O
操作 。可以借助共享内存和指针来访问磁盘文件。
使用这种方法, 首先应通知内核, 将一个文件映射到存储区域中 , 这个映射工作可以通过mmap函数
来实现。
基本使用方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void *mmap (void *addr, size_t length, int prot, int flags, int fd, off_t offset) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { int len, ret; char * p = NULL ; int fd = open ("mytest.txt" ,O_CREAT|O_RDWR,0644 ); if (fd < 0 ) { perror("open" ); exit (0 ); } len = ftruncate(fd,4 ); if (len == -1 ) { perror("ftruncate" ); exit (0 ); } p = mmap(NULL ,4 ,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 ); if (p == MAP_FAILED) { perror("mmap" ); exit (0 ); } strcpy (p,"abc" ); ret = munmap(p,4 ); if (ret == -1 ) { perror("munmap" ); exit (0 ); } close (fd); return 0 ; }
od -tcx filename
:以16进制查看文件
MMAP使用注意事项 创建映射区的权限应该小于等于文件打开的权限,创建映射区的过程隐含一次对文件的读操作。
段错误:gdb,直接run就可以抓取到段错误的位置。
可以open
的时候O_CREAT
一个新文件来创建映射区,但是要拓展文件大小, 否则会出现总线错误 . 当然 mmap
时指定size=0
,mmap
会报错
如果open
时指定O_RDONLY
,mmap
时PROT
参数指定PROT_READ|PROT_WRITE
会报错,无效参数(注意ftruncte()
函数需要写权限, 否则无法拓展文件大小 ). 如果都用只读权限, 不会出错. 要创建映射区, 文件必须有读权限
文件描述符先关闭,对mmap
映射没有影响,建立完映射区后fd
即可关闭
如果文件偏移量为1000,mmap
会报错,因为偏移量必须是4K
的整数倍(MMU
映射的最小单位为4K
)
对mem
进行越界操作:小范围的越界问题不大, 但是最好不要这样(操纵不安全的内存, 操作系统不给保障)
如果mem++
,munmap
不会成功(与malloc
一样, 释放的内存的指针必须是申请得来的初始的指针, 如果要改变指针的值, 拷贝一份用 )
除了第一个参数, 后面的参数都可能导致失败
无论mmap
多复杂,一定要检查mmap
的返回值
总结:
创建映射区过程中,隐含一次对映射文件的读操作
当MAP_SHARED
时,要求映射区的权限应该<=文件打开的权限(出于对映射区的保护) ,而MAP_PRIVATE
则无所谓因为mmap
中的权限是对内存的限制
特别注意,当映射区文件大小为0时,不能创建映射区,所以:用于映射的文件必须要有实际大小!!mmap
使用时候经常会出现总线错误,通常是由于共享文件存储空间大小引起的
munmap
传入的地址一定是mmap
的返回地址,坚决杜绝指针++
操作
文件偏移量必须为4K
的整数倍
mma
p创建映射区出错概率很高,一定要检查返回值,确保映射区建立成功后再进行后续操作
mmap优点
对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O
读写,提高了文件读取效率。
实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
提供进程间共享内存及相互通信的方式 。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。 同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可以直接使用已经保存在内存中的文件数据。
可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap
映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap
都可以发挥其功效。
mmap父子进程通信 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <unistd.h> #include <sys/wait.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/types.h> int var = 100 ;int main () { int * p; pid_t pid; int fd; fd = open ("temp" ,O_CREAT |O_RDWR|O_TRUNC,0644 ); if (fd < 0 ) { perror("open error" ); exit (1 ); } unlink("temp" ); ftruncate(fd,4 ); p = (int *)mmap(NULL ,4 ,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 ); if (p == MAP_FAILED) { perror("mmap error" ); exit (1 ); } close (fd); pid = fork(); if (pid == 0 ) { *p = 2000 ; var = 1000 ; printf ("child, *p = %d, var = %d\n" ,*p, var); } else { sleep(1 ); printf ("parent, *p = %d, var = %d\n" ,*p, var); wait(NULL ); int ret = munmap(p,4 ); if (ret == -1 ) { perror("munmap error" ); exit (1 ); } } }
父子等有血缘关系的进程直接也可以通过mmap
建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags
:
MAP_PRIVATE
(私有映射): 父子进程各自独占映射区
MAP_SHARED
(共享映射):父子进程共享映射区
结论:父子进程共享:
打开的文件
mmap
建立的映射区(但必须使用MAP_SHARED)
匿名映射 使用映射区来完成文件读写操作十分方便,父子进程间通信也比较容易,但缺陷是:每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open
一个temp
文件,创建好了再unlink
、close
,比较麻烦。可以直接使用匿名映射来代替。linux提供了相应的方法,无需依赖一个文件即可创建映射区,同样需要借助标志位参数flags
来指定:
使用MAP_ANONYMOUS(或MAP_ANON)
,如:
1 int *p = mmap(NULL ,4 ,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1 ,0 );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <unistd.h> #include <sys/wait.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/types.h> int var = 100 ;int main () { int * p; pid_t pid; p = (int *)mmap(NULL ,4 ,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1 ,0 ); if (p == MAP_FAILED) { perror("mmap error" ); exit (1 ); } pid = fork(); if (pid == 0 ) { *p = 2000 ; var = 1000 ; printf ("child, *p = %d, var = %d\n" ,*p, var); } else { sleep(1 ); printf ("parent, *p = %d, var = %d\n" ,*p, var); wait(NULL ); int ret = munmap(p,4 ); if (ret == -1 ) { perror("munmap error" ); exit (1 ); } } }
注意:MAP_ANONYMOUS
和MAP_ANON
两个宏是linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立:
1 2 fd = open ("dev/zero" ,O_RDWR); p = mmap(NULL , size ,PROT_READ|PROT_WRITE,MMAP_SHARED,fd,0 );
/dev/zero
-文件白洞 , 里面有无限量的’\0’, 要多少有多少
/dev/null
-文件黑洞 , 可以写入任意量的数据
所以在创建映射区时可以用zero文件, 就不用自己创建文件然后拓展大小了
mmap无血缘关系进程间通信 实质上mmap
是内核借助文件帮助创建的一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap
来完成通信,只要设置相应的标志位参数flags
即可。若想实现共享,应该使用MAP_SHARED
要点:必须是同一个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> #include <sys/types.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> struct STU { int id; char name[20 ]; char sex; }; void sys_err (char * str) { perror(str); exit (-1 ); } int main (int argc, char * argv[]) { int fd; struct STU student = {10 ,"xiaoming" ,'m' }; char * mm; if (argc < 2 ) { printf ("./a.out file_shared\n" ); exit (-1 ); } fd = open (argv[1 ],O_RDWR|O_CREAT,0644 ); if (fd == -1 ) { sys_err("open error" ); } ftruncate(fd,sizeof (student)); mm = mmap(NULL , sizeof (student),PROT_READ|PROT_WRITE, MAP_SHARED,fd,0 ); if (mm == MAP_FAILED) { sys_err("mmap error" ); } close (fd); while (1 ) { memcpy (mm,&student,sizeof (student)); student.id ++; sleep(2 ); } munmap(mm,sizeof (student)); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> #include <sys/types.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> struct STU { int id; char name[20 ]; char sex; }; void sys_err (char * str) { perror(str); exit (-1 ); } int main (int argc, char * argv[]) { int fd; struct STU student ; struct STU * mm ; if (argc < 2 ) { printf ("./a.out file_shared\n" ); exit (-1 ); } fd = open (argv[1 ],O_RDONLY); if (fd == -1 ) { sys_err("open error" ); } mm = mmap(NULL , sizeof (student),PROT_READ, MAP_SHARED,fd,0 ); if (mm == MAP_FAILED) { sys_err("mmap error" ); } close (fd); while (1 ) { printf ("id = %d\tname=%s\t%c\n" ,mm->id,mm->name,mm->sex); sleep(2 ); } }
无血缘关系进程间通信, 不能用匿名映射
strace
: 追踪一个可执行文件在执行过程中所有的系统调用。