进程间通信
进程间具有独立性,那么进程间如何进行通信呢?操作系统会提供一个公共资源,进程间通过访问这个公共资源而实现进程间的通信。
实现进程间通信的方式:
- 管道
- 消息队列
- 共享内存
- 信号量
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程;
- 资源共享:多个进程之间共享同样的资源;
- 通知事件:一个进程需要向另一个进程或一组进程发送消息,通知它们发生了某种事情(如进程终止时要通知父进程);
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变;
管道
- 管道是Unix中最古老的进程间通信的形式;
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”;
- 管道的实质就是操作系统所提供的一块内存;
管道特点:
每个管道只有一个页面作为缓冲区,该页面是按照环形缓冲区的方式来使用的。这种访问方式是典型的“生产者——消费者”模型。当“生产者”进程有大量的数据需要写时,而且每当写满一个页面就需要进行睡眠等待,等待“消费者”从管道中读走一些数据,为其腾出一些空间。相应的,如果管道中没有可读数据,“消费者” 进程就要睡眠等待,具体过程如下图所示:
匿名管道
管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道,一般文件I/O的函数都可以用来操作管道(lseek除外)。
pipe函数:
作用:创建一个无名管道
#include<unistd.h>
int pipe(int fd[2]);
【参数】:
- fd :文件描述符数组,其中fd[0]表示读端,fd[1]表示写端;
- 返回值:成功返回0,失败返回错误代码;
【例】从键盘读取数据,写入管道,读取管道,写到屏幕:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main(){
int fds[2];
char buf[100];
int len;
if(-1 == pipe(fds)){
perror("pipe!!");
exit(1);
}
//read from stdin
while(fgets(buf,100,stdin)){
len = strlen(buf);
//write into pipe
if(write(fds[1],buf,len) != len){
perror("write to pipe!");
break;
}
memset(buf,0x00,sizeof(buf));
//read from pipe
if((len = read(fds[0],buf,100)) == -1){
perror("read from pipe!");
break;
}
//write to stdout
if(write(1,buf,len) != len){
perror("write to stdout");
break;
}
}
}
【运行结果】:
父子进程共享管道:
单独创建一个无名管道,并没有实际的意义。我们一般是在一个进程在由pipe()创建管道后,一般再由fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。
【例】:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
int main(){
int fds[2];
if(-1 == pipe(fds)){
perror("pipe!!");
exit(1);
}
pid_t pid;
pid = fork();
if(-1 == pid){
perror("fork!!");
exit(1);
}
if(0 == pid){
//子进程在管道中写下"hello"
close(fds[0]);
write(fds[1],"hello",5);
close(fds[1]);
exit(EXIT_SUCCESS);
}
//父进程在管道中读取字符串并放进buf中
close(fds[1]);
char buf[10] = {0};
read(fds[0],buf,10);
printf("buf is : %s\n",buf);
return 0;
}
【程序运行结果】:
匿名管道读写规则:
- 当没有数据可读时:
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止;
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN; - 当管道满的时候:
O_NONBLOCK disable: write 调用阻塞,直到有进程读走数据;
O_NONBLOCK enable : 调用返回 -1,error 值为EAGAIN; - 如果所有的管道写端对应的文件描述符被关闭,则read返回0;
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出;
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性;
- 当要写入的数据量大于PIPE_BUF时,linux将不在保证写入的原子性;
匿名管道特点:
- 只能用于具有公共祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
- 管道提供流式服务(管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等)。
- 写入管道中的数据遵循先入先出的规则。
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
- 一般而言,内核会对管道操作进行同步与互斥(写满就不写,读完就不读)。
- 管道是半双工的(一边进一遍出),数据只能向一个方向流动;需要双方通信时,需要建立两个管道。
命名管道(FIFO)
命名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。
创建一个命名管道:
- 命名管道可以从命令行上创建。
$ mkfifo filename
- 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
【参数】:
- filename为创建的有名管道的全路径名
- mod为创建的命名管道的模式,指明其存取权限
- 命名管道由mkfifo函数创建,打开用open
【open函数】:
#include <fcntl.h>
int open(const char *pathname, int oflag, ... );
返回值:成功则返回文件描述符,否则返回 -1
【参数】:
- 第三个参数(…)仅当创建新文件时才使用,用于指定文件的访问权限位(access permission bits)
- pathname 是待打开/创建文件的路径名
- oflag 用于指定文件的打开/创建模式
oflag 参数可由以下常量(定义于 fcntl.h)通过逻辑或构成:
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
匿名管道与命名管道的区别:
- 匿名函数由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open。
- FIFO(命名管道)与 pipe(匿名管道)之间的区别是它们创建与打开的方式不同,其他大致相同。
命名管道的打开规则:
如果当前打开操作是为读操作而打开FIFO时:
- O_NONBLOCK disable : 阻塞直到有相应进程为写而打开该FIFO;
- O_NONBLOCK enable : 立刻返回成功;
如果当前操作是为写操作而打开FIFO时:
- O_NONBLOCK disable : 阻塞直到有相应进程为读而打开该FIFO;
- O_NONBLOCK enable : 立刻返回失败,错误码为ENXIO;
管道小结:
- 单向通信。
- 匿名管道只能用于有亲缘关系的进程,命名管道可用于随意进程。
- 面向字节流。
- 自带同步互斥。
- 生命周期随进程