进程间通信目的
- 数据传输: 一个进程需要把它的数据发送给另一个进程
- 资源共享:多个进程间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程具有独立性,让其通信时具有难度的,因此,想让两个进程进行通信的前提条件就是,需要让不同的进程看到同一份资源,这个资源通常指的是内存。
进程间通信的分类
-
管道
- 匿名管道pipe
- 命名管道
-
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
-
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道只能单向通信。
它是一种半双工机制,只能一端来读,一端来写,遵循先进先出的原理,并且数据只能被读取一次,当此段数据被读取后,马上会从数据中消失。(写入的数据每次都添加到管道缓冲区的末尾,读数据的时候都是从缓冲区的头部读出数据的。)
匿名管道
匿名管道只能用于有亲缘关系的进程,例如父子进程
匿名管道只能用在同一台计算机中,它只能是单向的。
#include<unistd.h>
功能:创建一个无名管道
int pipe(int fd[2]);
参数:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:
成功返回0,失败返回错误代码
实例:键盘读取数据,写入管道,读取管道,写到屏幕
站在文件描述符角度理解管道
- 父进程调用pipe()开辟管道,得到两个文件描述符,指向管道的两端,
- 父进程调用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;
}
站在内核的角度理解管道
描述文件是一个面向对象的过程,c语言用结构体来描述对象,但结构体中不能放调用方法,但是可以存放函数指针,进程的file结构体中存放的就是一个个函数指针,指向各种文件的操作方法。
操作系统通过管理pcb来管理进程,而每一个进程都有一个struct_File结构体,其中含有一个指针数组,用来存放要打开的文件,这个指针数组其中一个元素又指向file结构体,file结构体中的f_inode指向文件的inode,f_op指向操作文件的方法,所以我们看待管道,就如同看待文件一样,它们的使用是类似的,印证了“Linux中一切皆文件的思想”。
管道的读写规则
当没有数据可读时:
O_NONBLOCK disable(非阻塞模式禁止):read调用阻塞,即读进程暂停执行,一直等到有效数据来到为止。
O_NONBLOCK enable(非阻塞模式启动):read调用返回-1,error值为EAGAIN。
当管道满时
O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable:write调用返回-1,error值为EAGAIN。
管道写端对应的文件描述符被关闭,read返回0;
管道读端对应的文件描述符被关闭,write会产生信号SIGPIPE,进而导致write退出
当写入的数据量不大于PIPE_BUF,也就是管道的上限,linux将保证管道的原子性,相反,要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性管道的上限一般为4k或8k
原子性:通常对临界资源的访问只有两种状态,要么访问,要么不访问
临界资源:多进程共享的内存资源
临界区:访问临界资源的代码
互斥:任何时刻只允许一方进行对临界资源的访问
同步:在保证临界资源安全的条件下,(通常是互斥),让多进程访问临界资源具有一定顺序性,称为同步(同步目标:协同程序步调,避免解问题)
管道读写常遇见的四种情况
1. 父进程一直写,子进程一直等待就是不读,这样会导致管道被写满,此时write调用阻塞,直到有数据被读走
2. 父进程向管道里写入一条数据直接退出了,子进程读数据,读完此条数据后 read返回0
3. 父进程一直写,子进程几秒之后退出,管道读端对应的文件描述符关闭,write函数会产生SIGPIPE信号,导致write退出
4. 父进程不写数据,也不关闭文件描述符,子进程一直读,此时子进程调用read阻塞,程序会被卡住,即读进程暂停执行,一直等到有效数据来为止。
管道的特点
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信。通常一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间可应用该管道。
- 管道提供流式服务(即单向通信)。
- 一般而言,进程退出,进程退出,管道释放,所以管道的生命周期随进程。(文件的生命周期也随进程)
- 一般内核会对管道操作进行同步与互斥。
- 管道是半双工的,数据只能向一个方向流动,需要双方通信时建立两个管道。
命名管道
管道应用的一个限制就是只能在具有公共祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这件事情,它经常被称为命名管道。命名管道是一种特殊类型的文件。
创建命名管道
- 从命令行创建
mkfifo filename
- 命名管道也可以从程序里创建,相关函数是:
int mkfifo(const char *filename,mode_t mode);
命名管道和匿名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open。
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于他们创建与打开的方式不同,但是一旦这些工作完成之后,它们具有相同的语义。
命名管道的打开规则
-
如果当前打开操作是为了读而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功。 -
如果当前打开操作是为写而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。