一.实现原理
首先利用了进程的一个共性,即:用户空间不共用,内核空间共用
每个进程各自有不同的用户地址空间, 任何一个进程的全局变量在另一个进程中都看不到,所有进程之间要交换数据必须通过内核,因此可以在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。管道就是内核当中的一块缓冲区。
Linux实现进程间通信主要有四种途径:pipe管道,fifo有名管道,内存共享映射,以及Socket。
二.pipe管道
由pipe()函数创建:
#include <unistd.h> int pipe(int filedes[2]);
其主要处理有血缘关系的进程间通信(即:通过fork来创建的进程)。
实现原理:当调用pipe函数后,内核当中会创建一条管道(环形队列),并存在两个文件描述符,而通过fork()函数创建的子进程可以继承PCB和文件描述符表,因此可以拿到pipe管道对应的两个文件描述符,可通过这两个文件描述符实现进程间通信。
使用pipe管道需要注意:
两个进程通过一个管道只能实现单向通信。因此创建好管道后,需要确立通信方向。即:究竟是父写子读,还是子写父读。确立单工的通信模式可以规避诸如此类的问题:假设父进程通过管道写了一段内容,子进程通过管道也写了一段内容,回头分不清楚,这段内容究竟是谁写的?若要实现双向通信,则必须使用两个管道。
例1:
#include <unistd.h> #include <stdio.h> #include <sys/wait.h> #include <sys/types.h> #include <string.h> #include <stdlib.h> int main() { int fd[2]; pid_t pid; char str[] = "This is a new question "; char buf[1024]; // 父写子读 if (pipe(fd) < 0) { perror("pipe"); } pid = fork(); if (pid > 0) { // parent // 关闭父读 close(fd[0]); write(fd[1], str, strlen(str)); sleep(3); // 回收子进程 wait(NULL); } else if (pid == 0) { // child // 关闭子写 close(fd[1]); int len = read(fd[0], buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); } else { perror("fork"); } return 0; }
运行结果:
This is a new question
使用pipe管道须注意:
1.写端关闭,读端读完管道里的内容时,再次读,返回0,相当于读到EOF 2.写端未关闭,写端暂时无数据,读端读完管道里的数据后,再次读,阻塞 3.读端关闭,写端写管道,产生SIGPIPE信号(17),这个信号会导致写进程终止 4.读端未读管道数据,当写端写满管道数据后,再次写,阻塞
例2:改变管道的阻塞属性,使子进程在读管道时不被阻塞
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <fcntl.h> #include <string.h> #include <errno.h> int main() { int fd[2], flags, len; pid_t pid; char str[] = "This is not impossibile "; char buf[1024]; if (pipe(fd) < 0) { perror("pipe"); } // 设置读管道为非阻塞 flags = fcntl(fd[0], F_GETFL); flags |= O_NONBLOCK; fcntl(fd[0], F_SETFL, flags); pid = fork(); /*父写子读*/ if (pid > 0) { // parent close(fd[0]); sleep(6); write(fd[1], str, strlen(str)); wait(NULL); } else if (pid == 0) { // child close(fd[1]); tryagain: len = read(fd[0], buf, sizeof(buf)); if (len == -1) { if (errno == EAGAIN) { // 读一个非阻塞文件,如果数据没到达,就会出现EAGAIN write(STDOUT_FILENO, "try again ", 10); sleep(1); goto tryagain; } else { perror("read"); exit(1); } } write(STDOUT_FILENO, buf, len); } else { perror("fork"); } return 0; }
运行结果:
try again
try again
try again
try again
try again
try again
This is not impossibile
例3:可以使用fpathconf()函数来获取管道缓冲区的大小
#include <unistd.h> #include <stdio.h> int main() { int fd[2]; if (pipe(fd) < 0) { perror("pipe"); } long value = fpathconf(fd[0], _PC_PIPE_BUF); printf("The pipe buf is: %ld ", value); close(fd[0]); close(fd[1]); return 0; }
运行结果:
The pipe buf is: 4096
三.fifo有名管道
由mkfifo()函数创建管道文件
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
其可以来处理无血缘关系的进程间通信。
实现原理:在磁盘当中创建一个文件结点(仅仅是一个文件结点,没有大小),标记内核当中的一个pipe,继而通过操作该文件(open该文件,然后进行读写)实现进程间通信。 实际上也是在内核中创建了一个缓冲区。
使用fifo管道需要注意:
1.当以只写的方式打开FIFO管道时,如果没有FIFO的读端打开,则open写打开会阻塞。
2.FIFO内核实现时可以支持双向通信。(pipe单向通信,因为父子进程共享一个FILE 结构体)
3.FIFO管道可以有一个读端,多个写端,也可以有多个读端,一个写端。
4.mkfifo同时也是一个命令,可直接在终端下使用mkfifo命令创建一个管道文件:
例:使用fifo管道实现进程间通信
写端:
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <time.h> #include <string.h> int main() { int fd; char str[] = "This is a new question "; int rand_value; char buf[100]; if (access("myfifo", F_OK) < 0) { // File not exists int ret = mkfifo("myfifo", 0777); if (ret < 0) { perror("mkfifo"); exit(1); } } else { // file exists fd = open("myfifo", O_WRONLY); if (fd < 0) { perror("open"); exit(2); } srand((int)time(0)); /*利用time(0)作为随机数种子*/ while (1) { rand_value = 1 + (int)(10.0*rand() / (RAND_MAX+1.0)); memset(buf, 0, sizeof(buf)); sprintf(buf, "current random value is: %d ", rand_value); write(fd, buf, strlen(buf)); sleep(2); } close(fd); } return 0; }
读端:
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> int main() { int fd; char buf[1024]; if (access("myfifo", F_OK) < 0) { perror("access"); exit(1); } else { fd = open("myfifo", O_RDONLY); if (fd < 0) { perror("open"); exit(2); } while (1) { int len = read(fd, buf, sizeof(buf)); if (len < 0) { perror("read"); exit(3); } else if (len > 0) { write(STDOUT_FILENO, buf, len); } } close(fd); } return 0; }
先运行写端,再运行读端:
current random value is: 10
current random value is: 8
current random value is: 3
current random value is: 5
current random value is: 10
current random value is: 10
current random value is: 8
current random value is: 4
current random value is: 8
current random value is: 1
current random value is: 8
current random value is: 5
current random value is: 5
current random value is: 7
四.内存共享映射(mmap()&munmap())
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
内存共享映射同样可以用来处理无血缘关系的进程间通信。
实现原理:mmap()函数可以将磁盘文件中的某一部分内容映射到内存,通过设置相关参数还可以实现磁盘文件内容与内存同步,若进程1通过修改内存来更新文件,进程2如果再将该文件映射到内存,当文件内容发生改变后,进程2所映射的内存同样也会发生改变,因此可实现进程间通信,此方法与FIFO类似,仍然需要一个文件作为交互媒介,与FIFO不同的是,将文件内容映射到内存后,对文件的读写可直接使用指针来操作,不再需要使用read()/write()函数。且用来交互的文件不必再是一个管道文件,普通文件亦可,通信结束后,还可以将该文件删除。
参数说明:
addr: 如果addr参数为NULL,内核会自己在进程地址空间中选择合适的地址建立映射。如果addr不是NULL,则给内核一个提示,应该从什么地址开始映射,内核会选择addr之上的某个合适的地址开始映射。建立映射后,真正的映射首地址通过返回值可以得到。 length: 申请内存的长度。 prot: 设置映射内存的访问权限。 flags: 状态标志。指定映射类型,SHARED(共享)/PRIVATED(私有)等 fd: 文件描述符,映射磁盘文件的文件描述符 offset: 你要映射的磁盘文件从什么地址开始偏移。偏移量必须是分页大小的整数倍,偏移的时候是按页面为单位偏移的。 返回值: mmap()函数所申请的内存的首地址。
使用内存共享映射还需要注意:
1.用于进程间通信时,一般设计成结构体,来传输通信的数据
2.进程间通信的文件,应该设计成临时文件
3.当报总线错误时,优先查看共享文件是否有存储空间
例:
写端:
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #define MAPLEN 0x1000 struct STU { int id; char name[20]; char sex; }; int main(int argc, char *argv[]) { struct STU *mm; int fd, i = 0; if (argc < 2) { printf("./a.out filename "); exit(1); } fd = open(argv[1], O_CREAT|O_RDWR, 0777); if (fd < 0) { perror("open"); exit(2); } if (lseek(fd, MAPLEN-1, SEEK_SET) < 0) { perror("lseek"); exit(3); } if (write(fd, "