进程间通信
概念
进程间通信是干什么的?
我们先来思考一个问题, 当两个人面对面交谈的时候, 为什么一个人能够听得见另一个人说话呢, 是因为声音有空气作为介
质, 通过空气这个介质, 双方才得以听到对方的讲话, 进程间通信就和人与人说话一样, 通过一个公共的媒介, 进行进程间的数
据传输, 资源共享, 进程之间的协同控制, 事件通知等等.
目的
操作系统为什么要给用户提供进程间通信方式?
因为每个进程之间都是独立的, 进程间通信需要一个公共的媒介, 进程间通信方式就是这个公共的媒介, 但是有不同的需求
不同的应用场景, 因此操作系统提供了多种不同的进程间通信方式, 继续往下看.
方式
管道
1. 命名管道
2. 匿名管道
System V 标准
消息队列 共享内存 信号量
POSIX 标准
消息队列 共享内存 信号量 互斥锁 条件变量 读写锁
注:
在这篇博客中我们着重说的是管道, POSIX中的信号量互斥锁以及后面的两个我们在后面的多线程中会详细讲解, 消息队列
已经很少用了, 后面博客中我们能会说一说原理
管道
管道是用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件, 也叫做 pipe 文件, 管道提供字节流服务.
我们把从一个进程连接到另一个进程的一个数据流称为一个管道, 管道在内核中创建, 因为在用户态各个进程之间是独立的
管道的本质:
操作系统在内核中提供的一块缓冲区 (只要进程能够访问到这块缓冲区就可以实现通信)
管道的特点:
1. 管道是一个半双工通信, 提供双向选择, 但是只能单向传输
2. 匿名管道只能用于具有亲缘关系的进程间通信,命名管道可以用于任意的进程间通信
3. 管道的读写特性:
若没有数据, 则read会阻塞, 直到读取到数据
若管道中数据满了, 则write会阻塞, 直到有空闲空间(有数据被读走)
若所有读端被关闭, 则write触发异常
若写端关闭, 则read读完数据返回0
5. 管道创建成功后, 提供IO操作, 返回文件描述符作为句柄
文件描述符有两个: 一个用于读取数据, 一个用于写入数据
6. 管道提供字节流服务(传输灵活 / 数据粘连)
7. 管道自带同步于互斥功能
读写操作数据大小不超过PIPE_BUF大小, 读写操作受保护
同步: 保证临界资源访问的时序可控性(一个进程操作完了另一个才能操作)
互斥: 保证临界资源同一时间唯一访问性(一个进程操作的时候别的进程都不能操作)
管道的同步于互斥的体现就是:
管道没有放数据则读阻塞, 放了数据之后唤醒对方, 对方读数据--------------同步
数据往管道中没放完别人不能读也不能写------------互斥
在这里要给大家说的事, 进程在操作管道的时候, 如果没有用到哪一端, 则把这一端关闭掉.
匿名管道:
int pipe(int fd[2]); 参数: fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端 返回值: 成功返回0,失败返回错误代码
我们在代码中体会匿名管道的使用:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { //创建管道 int pipefd[2]; int ret = pipe(pipefd); if(ret < 0) { perror("pipe error"); return -1; } //使用匿名管道实现子进程与父进程之间的通信 int pid = fork(); if(pid < 0) { return -1; } else if (pid == 0) { //child const char* ptr = "Acesses"; write(pipefd[1], ptr, strlen(ptr)); } else { //parent char buf[1024] = {0}; ret = read(pipefd[0], buf, 1023); if(ret < 0) { perror("read error"); } else { printf("buf:[%s]\n", buf); } } return 0; }
编译运行:
可以看到, 父进程成功读取到了子进程写入的数据.
然后我们来体会管道的特性:
(1)若没有数据, 则read会阻塞, 直到读取到数据
我们让子进程先睡3秒看看.
else if (pid == 0) { //child sleep(3); const char* ptr = "Acesses"; write(pipefd[1], ptr, strlen(ptr)); }
运行:
可以看到, 父进程等待了三秒之后才读到了子进程写入的数据.
(2)若管道中数据满了, 则write会阻塞, 直到有空闲空间(有数据被读走)
我们对代码再稍作修改, 在子进程中一直写入数据, 直到缓冲区写满.
else if (pid == 0) { //child int i = 0; while(1) { const char* ptr = "Acesses"; i += write(pipefd[1], ptr, strlen(ptr)); printf("ret:%d\n", i); } }
运行:
可以看到, 当我们写入了接进64k的数据后, 缓冲区满了, 这个时候不能写入了, write阻塞.
(3)若所有读端被关闭, 则write触发异常
这次我们在父进程中写入, 因为如果子进程先退出的话, 子进程并没有退出, 体现的不是很明显.
else if (pid == 0) { //child close(pipefd[0]); sleep(1000); //不读取数据 char buf[1024] = {0}; ret = read(pipefd[0], buf, 1023); if(ret < 0) { perror("read error"); } else { printf("buf:[%s]\n", buf); } } else { //parent close(pipefd[0]); sleep(1); //让子进程先把读端关闭父进程再开始写 int i = 0; while(1) { const char* ptr = "Acesses"; i += write(pipefd[1], ptr, strlen(ptr)); printf("ret:%d\n", i); } }
运行:
我们可以看到, 关闭所有读端之后, 父进程的写入触发异常, 程序直接退出
(4)若写端关闭, 则read读完数据返回0
我们还是稍作修改, 关掉两个写, 然后让子进程读数据
else if (pid == 0) { //child close(pipefd[1]); char buf[1024] = {0}; ret = read(pipefd[0], buf, 1023); if(ret < 0) { perror("read error"); } else { printf("buf:[%s]\n", buf); } } else { //parent close(pipefd[1]); sleep(1000); int i = 0; while(1) { const char* ptr = "Acesses"; i += write(pipefd[1], ptr, strlen(ptr)); printf("ret:%d\n", i); } }
运行:
可以看到, 当所有写端关闭, 什么也没有读到, 读返回0
管道符的实现
我们在执行一个命令比如 ps -ef | grep pipe 的时候, 这个 | 就是一个管道符, 它的作用是连接两个命令, 将前面的输出结果
作为后面的输入, 这个管道就是使用匿名管道来实现的, 父进程是shell, ps 和 grep是两个子进程, 具有亲缘关系, shell创建
两个子进程之前先创建一个管道, 两个子进程就可以通过这个管道进行数据的传输, ps将打印结果全部重定向到grep中, 然后
grep循环从标准输入读取数据, 所以对标准输入也进行重定向, grep从管道读取端读取数据.
创建两个子进程进行程序替换, 然后关闭用不到的一端.
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <sys/wait.h> int main() { int fd[2]; int ret = pipe(fd); if(ret < 0) { perror("pipe error"); return -1; } int pid1 = fork(); if(pid1 == 0) { //child1 //不用读端, 关闭 close(fd[0]); dup2(fd[1], 1); execlp("ps","ps","-ef",NULL); exit(0); } int pid2 = fork(); if(pid2 == 0) { //child2 //不用写端, 关闭 close(fd[1]); dup2(fd[0], 0); execlp("grep","grep","pipe",NULL); exit(0); } //父进程也打开管道了, 要关闭, 不然的话子进程二会一直循环读取, 不会退出 close(fd[0]); close(fd[1]); waitpid(pid1, NULL, 0); printf("child1 exit---\n"); waitpid(pid2, NULL, 0); printf("child2 exit---\n"); return 0; }
运行:
命名管道:
为管道创建了一个管道文件, 这个管道文件就是管道的名字
创建:
1. 直接在命令行创建
mkfifo filename
2. 在程序里面创建
int mkfifo(const char *pathname, mode_t mode); pathname : 管道文件的路径 mode : 文件的打开权限
命名管道的打开特性:
若管道文件没有被写的方式打开, 则以只读打开会阻塞
若管道文件没有被读的方式打开, 则以只写打开会阻塞
读写特性和匿名管道基本一样
命名管道的基本使用 (实现进程间的通信):
fifo_read.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/stat.h> #include <sys/fcntl.h> int main() { const char* file = "./tmp.fifo"; umask(0); int ret = mkfifo(file, 0664); if(ret < 0) { //如果文件不是因为已经存在而报错, 则退出 if(errno != EEXIST) { perror("mkfifo error"); return -1; } } printf("create fifo success!!\n"); int fd = open(file, O_RDONLY); if(fd < 0) { perror("open error"); return -1; } printf("open fifo success!!\n"); while(1) { char buf[1024] = {0}; int ret = read(fd, buf, 1023); if(ret < 0) { perror("read error"); return -1; } else if(ret == 0) { printf("write close\n"); return -1; } printf("buf:[%s]\n", buf); } close(fd); return 0; }
fifo_write.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/stat.h> #include <sys/fcntl.h> int main() { const char* file = "./tmp.fifo"; umask(0); int ret = mkfifo(file, 0664); if(ret < 0) { //如果文件不是因为已经存在而报错, 则退出 if(errno != EEXIST) { perror("mkfifo error"); return -1; } } printf("create fifo success!!\n"); int fd = open(file, O_WRONLY); if(fd < 0) { perror("open error"); return -1; } printf("open fifo success!!\n"); while(1) { char buf[1024] = {0}; scanf("%s", buf); write(fd, buf, strlen(buf)); } close(fd); return 0; }
运行: