进程间通信---管道
进程间通信
概念
进程间通信就是在不同进程之间传播或交换信息数据, 简称IPC(Interprocess communication).
意义
- 数据传输, 资源共享
- 事件通知, 进程控制
本质
让不同的进程看到同一份资源
管道
匿名管道
原理:
匿名管道仅限于本地父子进程之间的通信, 本质就是让两个父子进程先看到同一份被打开的文件资源, 然后父子进程就可以对该文件进行写入或是读取操作, 进而实现进程间通信.
pipe函数:
- int pipe(int pipefd[2]); // #include <unistd.h>
- pipefd: 输出型参数, 数组pipefd用于返回两个指向管道读端和写端的文件描述符. pipefd[0]是管道读端的文件描述符, pipefd[1]是管道写端的文件描述符.
- return int: pipe函数调用成功时返回0, 调用失败时返回-1.
- 注意: 管道的信道是半双工的.
使用(父进程读, 子进程写为例):
- 父进程通过调用pipe函数创建管道
- 父进程创建子进程
- 父进程关闭写端,子进程关闭读端
// 父进程读子进程写---父进程关闭写端,子进程关闭读端
static void test_pipe1()
{// 1.父进程先调用pipe函数,创建匿名管道获取读写端的文件描述符int pipefd[2];memset(pipefd,0,sizeof(pipefd));int ret=pipe(pipefd);if(ret!=0){perror("pipe");exit(1);}// 2.父进程调用fork创建子进程, 在进程创建中子进程会继承父进程的struct file, // 所以父子进程的pipefd中文件描述符会标定同一个匿名管道pid_t id=fork();if(id<0){perror("fork");exit(2);}// 3.因为管道通信是半双工的, 所以需要根据需求关闭父子进程各一个fdif(0==id) {// child codeclose(pipefd[0]);int n=10;while(n--){char buffer[1024];sprintf(buffer,"hello father, I am child: %d",n);write(pipefd[1],buffer,strlen(buffer));sleep(1);}close(pipefd[1]);exit(0);}// father codeclose(pipefd[1]);while(true){char buffer[1024];memset(buffer,0,sizeof(buffer));ssize_t n=read(pipefd[0],buffer,sizeof(buffer)-1);if(n>0){buffer[n]='\\0';printf("chlid# %s\\n",buffer);}else if(0==n){printf("child exit");break;}else {perror("read");exit(3);}}close(pipefd[0]);exit(0);
}int main(int argc,char *argv[],char *env[])
{test_pipe1();return 0;
}
运行结果:
四种特殊情况:
-
写端进程不写, 读端进程一直读, 那么此时会因为管道里面没有数据可读, 对应的读端进程会被挂起, 直到管道里面有数据后, 读端进程才会被唤醒.
-
读端进程不读, 写端进程一直写, 那么当管道被写满后, 对应的写端进程会被挂起, 直到管道当中的数据被读端进程读取后, 写端进程才会被唤醒.
-
写端进程将数据写完后将写端关闭, 那么读端进程将管道当中的数据读完后, 就会继续执行该进程之后的代码逻辑, 而不会被挂起.
-
读端进程将读端关闭, 而写端进程还在一直向管道写入数据, 那么操作系统会通过发送SIGPIPE信号给写端进程从而将写端进程杀掉.
验证第4种情况, 当写端进程一直往匿名管道写时, 读端进程把匿名管道对应的读取文件描述符关闭, 写端进程会收到什么信号(父进程写, 子进程读为例):
void sigpipe_handler(int signum)
{printf("收到信号: %d\\n",signum);exit(4);
}// 父进程写子进程读---父进程关闭读端,子进程关闭写端
static void test_pipe2()
{// 1.pipeint pipefd[2];int ret=pipe(pipefd);if(ret<0){perror("pipe");exit(1);}// 2.forkpid_t id=fork();if(id<0){perror("fork");exit(2);}if(0==id){// child codeclose(pipefd[1]); // 子进程关闭写端int n=3;while(n--){char buffer[1024];ssize_t n=read(pipefd[0],buffer,sizeof(buffer)-1);if(n>0){buffer[n]='\\0';printf("father# %s\\n",buffer);// sleep(1);}else if(0==n) {printf("father close\\n");exit(0);}else{perror("read");exit(3);}}close(pipefd[0]);exit(0);}// father codeclose(pipefd[0]); // 父进程关闭读端// 父进程对SIGPIPE信号进行捕获signal(SIGPIPE,sigpipe_handler);// 父进程一直往匿名管道中写入数据int count=0;while(true){char buffer[1024];sprintf(buffer,"I am father, pid: %d, count %d\\n",getpid(),count++);ssize_t n=write(pipefd[1],buffer,strlen(buffer));if(n>0){// printf("write n>0: n: %d\\n",n);usleep(5000); // 父进程写慢点} else if(0==n){printf("write 0==n: n: %d\\n",n);break;}else{perror("write");exit(5);}}close(1);exit(0);
}
运行结果:
测试管道的大小:
// 测试管道的大小,父进程一直不读,子进程一直写
static void test_pipe_size()
{// 1.pipeint pipefd[2];int ret = pipe(pipefd);if (ret < 0){perror("pipe");exit(1);}// 2.forkpid_t id = fork();if (id < 0){perror("fork");exit(2);}if (0 == id){// child codeclose(pipefd[0]); // 子进程关闭写端// 子进程一直往匿名管道中写int count = 0; // 记录写入了多少个字节数while (true){char one_byte = '$';ssize_t n = write(pipefd[1], &one_byte, 1);if (n > 0){printf("count: %d bytes\\n", ++count);}}close(pipefd[1]);exit(0);}// father codeclose(pipefd[1]); // 父进程关闭读端// 父进程一直不从匿名管道中读for(;;){}close(pipefd[0]);exit(0);
}
结论:我当前Linux版本(Linux VM-12-12-centos 3.10.0-1160.62.1.el7.x86_64 #1 SMP Tue Apr 5 16:57:59 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux)中管道的最大容量是65536字节. 当管道写满时, 写端进程继续写则会进入阻塞状态.
命名管道
原理:
想要实现本地任意两个进程之间的通信, 可以通过命名管道来实现. 命名管道是一种特殊类型的文件(管道文件), 两个进程通过命名管道的文件路径打开同一个管道文件, 此时这两个进程也就看到了同一份资源,进而就可以进行进程间通信了。
mkfifo函数:
int mkfifo(const char *pathname, mode_t mode);
pathname: 根据传入pathname所指定的目录下创建命名管道文件, 默认在当前进程的工作目录下创建.
- /home/yx/code/ipc_fifo/fifofile: 表示在/home/yx/code/ipc_fifo目录下创建fifofile文件
- fifofile: 表示在默认目录下创建fifofile文件
mode: 表示创建命名管道文件的默认权限, 受usmask权限掩码影响, 实际权限=mode&(~umask);
return int: 创建成功返回0, 创建失败返回-1.
使用:
comm.h
#ifndef __COMM_H__
#define __COMM_H__#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>const char * FIFOFILEPATH="fifofile";
const int DEFAULTMODE=0666;
const int BUFFERSIZE=1024;#endif
fifoServer.c
#include "comm.h"static void server()
{// 1.调用mkfifo创建管道文件int ret=mkfifo(FIFOFILEPATH,DEFAULTMODE);if(ret<0) // 成功返回0,失败返回-1{perror("mkfifo");exit(1);}// 2.调用open以读的方式打开管道文件int fifo_fd=open(FIFOFILEPATH,O_RDONLY);if(fifo_fd<0){perror("open");exit(2);}// 3.调用read从管道文件中读取数据char buffer[BUFFERSIZE];while(true){ssize_t n=read(fifo_fd,buffer,sizeof(buffer)-1);if(n>0){buffer[n]='\\0';printf("%s\\n",buffer);} else if(n==0){printf("client exit\\n");break;}else{perror("read");close(fifo_fd); // 关闭fifo_fd文件描述符unlink(FIFOFILEPATH); // 删除管道文件exit(4);}}close(fifo_fd);unlink(FIFOFILEPATH);exit(0);
}int main()
{server();return 0;
}
fifoClient.c
#include "comm.h"static void client()
{// 1.调用open以写的方式打开管道文件int fifo_fd=open(FIFOFILEPATH,O_WRONLY);if(fifo_fd<0){perror("open");exit(1);}// 2.调用write往管道文件中写入数据char buffer[BUFFERSIZE];while(true){// 从0号文件描述符读取数据zprintf("client# ");fflush(stdout);ssize_t n=read(0,buffer,sizeof(buffer)-1);buffer[n]='\\0';// 将读取到的数据往管道文件中写入n=write(fifo_fd,buffer,strlen(buffer));if(n>0){printf("write success n: %d\\n",n);}else if(n==0){// 读端关闭exit(2);}else{// 写入错误exit(3);}}}int main()
{client();return 0;
}
测试:
服务端先启动, 紧接着启动客户端. 客户端发送hello server, 可以看到服务端可以收到, 也即通过命名管道完成进程间通信. 客户端(写端)退出, 服务端(读端)也退出.