为什么要进行进程间通信?
进程之间具有独立性,每个进程都有自己的虚拟地址空间,进程A并不知道进程B的虚拟地址空间中的数据。这好比一个人不知道另一个人脑子中怎么想的一样。
介质?
进程间通信需要“介质”,两个进程都能访问到的公共资源。
借助文件就可以完成进程间通信,这是最简单,最常见手段。
操作系统专门提供的进程间通信方式:匿名管道 命名管道 消息队列 共享内存 信号量
工作中,最最最重要的进程间通信方式——网络
管道
内核中的一块内存(构成一个队列),使用一对文件描述符进行访问,读文件描述符就是队列中读数据,写文件描述符是队列中插入数据。
匿名管道:
#include <unistd.h>
功能:
创建一无名管道
原型:
int pipe(int fd[2])
参数:
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端。
返回值:
成功返回0,失败返回错误代码。
一个进程,pipe的演示:
#include<stdio.h> #include<unistd.h> #include<string.h> //一个进程,pipe的演示 int main() { //使用pipe 函数创建一对文件描述符,通过这一对文件描述符,就可以操作内核中的管道 int fd[2]; int ret = pipe(fd); //peipe创造输出型数组,两个 if (ret < 0) { perror("pipe"); //最常见失败方式,文件描述符用尽 return 1; } //fd[0] 用于读数据 //fd[1] 用于写数据 char buf[1024] = "hehe"; write(fd[1], buf, strlen(buf)); char buf_output[1024] = { 0 }; ssize_t n = read(fd[0], buf_output, sizeof(buf_output)-1); buf_output[n] = '\0'; printf("%s\n", buf_output); //管道使用完成之后需要关闭描述符 close(fd[0]); close(fd[1]); } //Makefile test:test.c gcc $^ -o $@
进程间通信-管道:
代码演示:
#include<stdio.h> #include<unistd.h> #include<string.h> int main() { //使用pipe 函数创建一对文件描述符,通过这一对文件描述符,就可以操作内核中的管道 int fd[2]; int ret = pipe(fd); //peipe创造输出型数组,两个 if (ret < 0) { perror("pipe"); //最常见失败方式,文件描述符用尽 return 1; } //fd[0] 用于读数据 //fd[1] 用于写数据 ret = fork(); //创建子进程 if (ret>0) { //father //写数据 char buf[1024] = "hehe"; write(fd[1], buf, strlen(buf)); //给fd[1]写,写buf里面的,长度是strlen(buf) } else if (ret = 0) { //child //读数据 char buf_output[1024] = { 0 }; ssize_t n = read(fd[0], buf_output, sizeof(buf_output)-1); buf_output[n] = '\0'; printf("child read: %s\n", buf_output); } else { perror("fork"); } //管道使用完成之后需要关闭描述符 close(fd[0]); close(fd[1]); } //write 和 read 代码互换 就实现反功能 //Makefile test:test.c gcc $^ -o $@
管道类似队列:写了就是入队列,读就是出队列,读了,管道中就没有了。因此,如果多个进程都读,但是它只能读到一个数据,谁先执行 read 谁读到数据,因为“同步互斥机制”,不会读到一人一半数据。
注意:
1.多个进程读写管道,数据不会发生错乱
2.如果管道为空,尝试读,就会在 read 阻塞。
3.如果管道满了,尝试写,就会在write 函数出阻塞
注:如果一个正在进行的进程堵塞了,利用 gdb attach pid<进程的pid> 就可以进行调试,在输入 bt 查看调用栈,就可以看阻塞到哪了。
匿名管道特点:
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 流式工作,就是水管一样
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的。(一个进,一个出)
命名管道
命名实现:
mkfifo filename<文件名> 这个文件名是:p 类型,即管道文件
代码实现:分成两个文件writer.c 和 reader.c
//先执行 myfifo myfifo 命令,创建管道文件 myfifo //vim reader.c 文件中 #include<stdio.h> #include<unistd.h> #include<string.h> int main() { //对命名管道操作和文件操作一摸一样 int fd = open("./myfifo", O_RDONLY); //只读打开管道文件 if (fd < 0) { perror("reader open"); return 1; } while (1) //开始读 { char buf[1024] = { 0 }; sszie_t n = read(fd, buf, sizeof(buf)); if (n < 0) { perror("read"); return 1; } if (n == 0) { //写端写完,读端读完 printf("read over\n"); return 0; } buf[n] = '/0'; printf("[read] %s\n", buf); } close(fd); } //vim 编辑小技巧: //打开多个标签页: :tabe filename<文件名> //切换标签页: gt到下一个标签页 gT到上一个标签页 :q 关闭当前标签页 :qa 全部关闭 //f [字符] 行内移动到指定字符 //某个字符打错了,按 s,输入字符就好了 //快速调整连个相邻字符,光标放在前一个字符,然后按:x p
//vim writer.c 文件中 #include<stdio.h> #include<unistd.h> #include<fcntl.h> #include<string.h> int main() { //对命名管道操作和文件操作一摸一样 int fd = open("./myfifo", O_RDONLY); //只读打开管道文件 if (fd < 0) { perror("reader open"); return 1; } while (1) //开始写 { printf("->"); //给用户写提示 fflush(stdout); char buf[1024] = { 0 }; //让用户输入一个字符串让后再写 read(0, buf, sizeof(buf)-1); //stdin 文件标识符 0 sszie_t n = write(fd, buf, strlen(buf)); } close(fd); }
Makefile文件:
//Makefile .PHONY:all all : reader writer //一个make生成所有的 reader : reader.c gcc $^ -o $@ writer : writer.c gcc $^ -o $@ .PHONY:clean clean : rm reader writer
当然,是两个文件,很麻烦,只是演示这个过程的实现,明白原理,熟练一下open,write 操作。
命名管道特点:除了匿名管道的第一个亲缘关系,其他都是。