进程间通信
概念
进程间通信就是在不同进程之间传播或交换信息数据, 简称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.因为管道通信是半双工的, 所以需要根据需求关闭父子进程各一个fd
if(0==id)
{
// child code
close(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 code
close(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.pipe
int pipefd[2];
int ret=pipe(pipefd);
if(ret<0)
{
perror("pipe");
exit(1);
}
// 2.fork
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(2);
}
if(0==id)
{
// child code
close(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 code
close(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.pipe
int pipefd[2];
int ret = pipe(pipefd);
if (ret < 0)
{
perror("pipe");
exit(1);
}
// 2.fork
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(2);
}
if (0 == id)
{
// child code
close(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 code
close(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号文件描述符读取数据z
printf("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, 可以看到服务端可以收到, 也即通过命名管道完成进程间通信. 客户端(写端)退出, 服务端(读端)也退出.