进程间通信
1.管道
对于具有公共祖先的进程,其管道是建立在3-4G的内核空间中的。每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
#include <sys/wait.h> #include<stdio.h> #include<sys/types.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main(void) { pid_t pid; int fd[2]; if(pipe(fd)<0) { perror("pipe"); exit(1); } printf("fd[0]=%d, fd[1]= %d\n",fd[0],fd[1]); if((pid=fork())<0) { perror("fork"); exit(1); } else if(pid==0)//child { char c_str[1024]; int n; close(fd[1]);//关闭写端口 n=read(fd[0],c_str,sizeof(c_str)/sizeof(c_str[0]));//由于不知道读多少,所以读取最大长度 close(fd[0]); write(STDOUT_FILENO,c_str,n); } else//parents { char str[]="hello pipe!\n"; sleep(2); close(fd[0]);//关闭读端口 write(fd[1],str,strlen(str)); close(fd[1]); wait(NULL);//等待回收子进程资源 } return 0; }
在父进程没有传输数据在管道中时,子进程中的read函数会阻塞等待。我们可以使用fcntl函数改变一个已经打开文件的属性,如重新设置读、写、追加、非阻塞等标志。
#include<stdio.h> #include<sys/types.h> #include<sys/wait.h> #include<unistd.h> #include<stdlib.h> #include<string.h> #include<fcntl.h> #include<errno.h> int main(void) { pid_t pid; int fd[2]; if(pipe(fd)<0) { perror("pipe"); exit(1); } printf("fd[0]=%d, fd[1]= %d\n",fd[0],fd[1]); if((pid=fork())<0) { perror("fork"); exit(1); } else if(pid==0)//child { char c_str[1024]; int n,flags; flags=fcntl(fd[0],F_GETFL); flags |=O_NONBLOCK; if(fcntl(fd[0],F_SETFL,flags)==-1) { perror("fcntl"); exit(1); } close(fd[1]);//关闭写端口 tryagain: n=read(fd[0],c_str,sizeof(c_str)/sizeof(c_str[0]));//由于不知道读多少,所以读取最大长度 if(n<0) { if(errno==EAGAIN) { write(STDOUT_FILENO,"try again...\n",13); sleep(1); goto tryagain; } perror("read"); exit(1); } close(fd[0]); write(STDOUT_FILENO,c_str,n); } else//parents { char str[]="hello pipe!\n"; sleep(2); close(fd[0]);//关闭读端口 write(fd[1],str,strlen(str)); close(fd[1]); wait(NULL);//等待回收子进程资源 } return 0; }
此时,read已经不再是阻塞了。需要注意的是,使用管道技术,应该在fork之前创建管道。
2.FIFO
FIOF也被称为命名管道。未命名的管道pipe只能在两个有共同祖先的进程之间使用。但是通过FIFO,完全不相关的进程也能交换数据。
分别创建只读和只写文件fifo_r.c和fifo_r.c:
/*只读:fifo_r.c*/ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <fcntl.h> #include <sys/stat.h> #include <string.h> void sys_err(const char *str, int exitno) { perror(str); exit(exitno); } int main(int argc, char *argv[]) { int fd, len; char buf[1024]; if (argc < 2) { printf("usage:%s fifoname\n",argv[0]); exit(1); } if(access(argv[1],F_OK)==-1) { if(mkfifo(argv[1],0775)==-1) { sys_err("mkfifo",1); } } printf("1\n"); fd = open(argv[1], O_RDONLY); if (fd < 0) sys_err("open", 1); printf("2\n"); len = read(fd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); close(fd); return 0; }
/*只写:fifo_w.c*/ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <fcntl.h> #include <sys/stat.h> #include <string.h> void sys_err(const char *str, int exitno) { perror(str); exit(exitno); } int main(int argc, char *argv[]) { int fd; char buf[1024] = "hello nanmed pipe!\n"; if (argc < 2) { printf("usage:%s fifoname\n",argv[0]); exit(1); } if(access(argv[1],F_OK)==-1) { if(mkfifo(argv[1],0775)==-1) { sys_err("mkfifo",1); } } printf("1\n"); fd = open(argv[1], O_WRONLY); if (fd < 0) sys_err("open", 1); printf("2\n"); write(fd, buf, strlen(buf)); close(fd); return 0; }
先运行写进程,此时程序阻塞在只写打开的open函数,再ctrl+shift+n,打开新的终端,运行只读进程:
此时读进程正确读取了写进程的数据。反之,先执行读取进程,读进程也会阻塞在只读open函数处,直到写入数据。前提是没有指定O_NONBLOCK标志。
FIFO和PIPE的最大数据量可以通过fpathconf函数得到:
在创建了FIFO或者PIPE之后使用:printf("FIFO_PIPE_BUF_SIZE = %ld\n",fpathconf(fd, _PC_PIPE_BUF));
可以发现,下ubuntu 16.04中,FIFO和PIPE的缓冲区大小为4096个字节。不同的系统版本,可能存在差异。
3.内存共享映射
sysconf(_SC_PAGESIZE)的返回值,在本文的ubuntu16.04中为4096字节。故off的值应该是4096的整数倍,通常该值设置为0。
现在,使用mmap实现一个复制指令:
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <signal.h> 4 #include <stdlib.h> 5 #include <unistd.h> 6 #include <sys/time.h> 7 #include <sys/resource.h> 8 #include <sys/types.h> 9 #include <sys/stat.h> 10 #include <fcntl.h> 11 #include <syslog.h> 12 #include <string.h> 13 #include <sys/mman.h> 14 15 16 #define COPYINCR (1024*1024*1024) /* 1 GB */ 17 int main(int argc, char *argv[]) 18 { 19 int fdin, fdout; 20 void *src, *dst; 21 size_t copysz; 22 struct stat sbuf; 23 off_t fsz = 0; 24 if (argc != 3) 25 printf("usage: %s <fromfile> <tofile>", argv[0]); 26 if ((fdin = open(argv[1], O_RDONLY)) < 0) 27 printf("can’t open %s for reading", argv[1]); 28 if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0666)) < 0) 29 printf("can’t creat %s for writing", argv[2]); 30 if (fstat(fdin, &sbuf) < 0) /* need size of input file */ 31 printf("fstat error"); 32 if (ftruncate(fdout, sbuf.st_size) < 0) /* 文件字节数:sbuf.st_size ,set output file size */ 33 printf("ftruncate error"); 34 35 if ((sbuf.st_size - fsz) > COPYINCR) 36 copysz = COPYINCR; 37 else 38 copysz = sbuf.st_size - fsz; 39 if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz)) == MAP_FAILED) 40 printf("mmap error for input"); 41 if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE,MAP_SHARED, fdout, fsz)) == MAP_FAILED) 42 printf("mmap error for output"); 43 44 memcpy(dst, src, copysz); /* does the file copy */ 45 46 munmap(src, copysz);//释放内存 47 munmap(dst, copysz);//释放内存 48 49 50 exit(0); 51 }
使用Vim打开对比,内容自然也是完全一致的:
这个例子相当于在磁盘的main.c映射一个地址空间到src(只读),然后创建另一个文件,可读可写,通过前面映射的只读地址空间,将其内容拷贝到此时创建的main.c.copy中。
这个思想可以应用在多进程的通信中。本文目前描述的情况,都是最简单的场景,不存在进程间的竞争关系,如多个进程同时写一个文件,此时则需要执行相应的处理方法,如信号量,互斥锁等,这个在后面的随笔中再介绍。
消息邮箱和socket的进程间通信方法,也将在后续随笔中介绍。
欢迎加入作者的小圈子
扫描下方左边二维码加入QQ交流群,扫描下方右边二维码关注个人微信公众号并获取更多隐藏干货,QQ交流群:816747642 微信公众号:Crystal软件学堂
作者:Crystal软件学堂 bilibili视频教程地址:https://space.bilibili.com/5782182 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在转载文章页面给出原文连接。 如果你觉得文章对你有所帮助,烦请点个推荐,你的支持是我更文的动力。 文中若有错误,请您务必指出,感谢给予我建议并让我提高的你。 |