进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某个事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程是具有独立性,所以要让其通信是有难度的。因此,想要让两个进程通信的前提条件就是,需要让不同的进程看到同一份资源,这个“资源”通常指的是某一块物理内存。
进程间通信的分类
1.管道
- 匿名管道pipe
- 命名管道
2.System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
3.POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
本片博客主要介绍管道的相关内容。
管道
什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道只能单向通信。
匿名管道
匿名管道只能用于有亲缘关系的进程,常用于父子进程。
#include<unistd.h>
功能:创建一个无名管道
原型:
int pipe(int fd[2]);
参数:
fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:
成功返回0,失败返回错误代码
实例:键盘读取数据,写入管道,读取管道,写到屏幕
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int fd[2];
char buf[100];
int len= 0;
if(pipe(fd)== -1)
{
perror("make pipe");
exit(1);
}
while(fgets(buf,100,stdin))
{
len=strlen(buf);
if(write(fd[1],buf,len)!=len)
{
perror("write to pipe");
break;
}
memset(buf,0x00,sizeof(buf));
if((len=read(fd[0],buf,100))== -1)
{
perror("read from pipe");
break;
}
if(write(1,buf,len)!=len)
{
perror("write to stdout");
break;
}
}
return 0;
}
运行结果:
使用fork共享管道的原理
深度理解管道
站在文件描述符的角度来理解管道
代码实现:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd[2]={0};
pipe(fd);//创建管道
pid_t id=fork();//创建子进程
if(id== 0)
{
close(fd[1]);//子进程关闭写文件描述符,让子进程读
char buf[1024];
while(1)
{
ssize_t s=read(fd[0],buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
printf("I am child,I got parent's message: '%s'\n",buf);
}
}
}
else
{
close(fd[0]);//父进程关闭读文件描述符,让父进程写
char msg[]="hello world!";
while(1)
{
write(fd[1],msg,strlen(msg));
sleep(1);
}
}
return 0;
}
通过这种方式,让父进程一直往管道里写,而子进程一直从管道里读取数据,从而实现了父子之间通信
站在内核的角度理解管道
所以我们看待管道,就如同看待文件一样,它们的使用是类似的,印证了“Linux中一切皆文件的思想”。
管道的读写规则
1.当没有数据可读时:
- O_NONBLOCK disable(非阻塞模式禁止):read调用阻塞,即读进程暂停执行,一直等到有效数据来到为止。
- O_NONBLOCK enable(非阻塞模式启动):read调用返回-1,error值为EAGAIN。
2.当管道满时:
- O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
- O_NONBLOCK enable:write调用返回-1,error值为EAGAIN。
3.如果所有管道写端对应的文件描述符被关闭,则read返回0.
4.如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
5.当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。相反,要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性管道的上限一般为4k或8k。
这里我们需要解释几个概念:
多进程共享的资源我们叫做“临界资源”。访问临界资源的代码叫做“临界区”。任何时刻只允许一个进程访问临界区资源叫“互斥”,互斥能够保证安全性,但互斥并不一定是合理的。在保证临界资源安全的前提条件下(通常是互斥),让多进程访问临界资源具有一定的顺序性,我们称之为同步。同步的作用:协同进程步调,避免饥饿问题。
通常我们对临界资源要么不访问要么就访问完,没有中间态,我们称之为“原子性”。
管道读写常见的4种情况
情况1:
父进程一直写,但子进程一直等待就是不读,这样就会将管道写满。此时write调用阻塞,直到有数据被读走。
情况2:
父进程向管道里写入一条数据后直接退出了,子进程读数据读完此条数据后read函数返回0。
情况3:
父进程一直写入数据,子进程几秒钟之后退出,管道读端对应的文件描述符被关闭,write操作会产生信号SIGPIPE,进而导致write进程退出。
情况4:
父进程不写数据,也不关闭写文件描述符,子进程一直读,此时子进程调用read阻塞,程序会被卡住,即读进程暂停执行,一直等到有效数据来到为止。
管道的特点
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信。通常一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间可应用该管道。
- 管道提供流式服务(即单向通信)。
- 一般而言,进程退出,进程退出,管道释放,所以管道的生命周期随进程。(文件的生命周期也随进程)
- 一般内核会对管道操作进行同步与互斥。
- 管道是半双工的,数据只能向一个方向流动,需要双方通信时建立两个管道。
命名管道
- 管道应用的一个限制就是只能在具有公共祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这件事情,它经常被称为命名管道。命名管道是一种特殊类型的文件。
创建命名管道
1.可以从命令行上创建
mkfifo filename
2.命名管道也可以从程序里创建,相关函数是:
int mkfifo(const char *filename,mode_t mode);
创建命名管道:
运行后,可以发现多了一个文件
匿名管道和命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open。
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于他们创建与打开的方式不同,但是一旦这些工作完成之后,它们具有相同的语义。
命名管道的打开规则
1.如果当前打开操作是为了读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
- O_NONBLOCK enable:立刻返回成功。
2.如果当前打开操作是为写而打开FIFO时:
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。
用命名管道实现server&client通信
Makefile
.PHONY:all
all:ClientPipe ServerPipe
ClientPipe:ClientPipe.c
gcc -o $@ $^
ServerPipe:ServerPipe.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f ClientPipe ServerPipe
ServerPipe.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#define ERR_EXIT(m)\
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
if(mkfifo("mypipe",0644)< 0)
{
ERR_EXIT("mkfifo");
}
int rfd=open("mypipe",O_RDONLY);
if(rfd< 0)
{
ERR_EXIT("open");
}
char buf[1024];
while(1)
{
buf[0]=0;
printf("Please wait...\n");
ssize_t s=read(rfd,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s]=0;
printf("client say# %s\n",buf);
}
else if(s==0)
{
printf("client quit,exit now!\n");
exit(EXIT_SUCCESS);
}
else
{
ERR_EXIT("read");
}
}
close(rfd);
return 0;
}
ClientPipe.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#define ERR_EXIT(m)\
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
int wfd=open("mypipe",O_WRONLY);
if(wfd< 0){
ERR_EXIT("open");
}
char buf[1024];
while(1)
{
buf[0]=0;
printf("Please Enter# ");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s]=0;
write(wfd,buf,strlen(buf));
}
else
{
ERR_EXIT("read");
}
}
close(wfd);
return 0;
}