进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件
进程控制:有些进程希望完全控制另一个进程的执行(Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信分类
- 管道
- System V进程间通信
POSIX进程间通信
管道
匿名管道pipe
命名管道FIFO
System V进程间通信
System V 消息队列
- System V 共享内存
System V 信号量
POSIX进程间通信
消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
在本篇中我们先讨论管道,在之后的文章中会对SystemV版本的进程间通信作介绍。
管道
管道是Unix中最古老的一种通信方式,它是一个进程通往另一个进程的数据流。
对于管道而言,它只能单向通信,即进程1能够通过已创建的管道将数据传达到进程2,但进程2不能通过这个管道传数据给进程1,必须重新再创建一个管道。
匿名管道
匿名管道是一个仅能在有亲缘关系的进程之间进行通信的管道(如父子进程)
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd[2]表示一个文件描述符数组,其中fd[0]表示读端,fd[1]表示写段
函数调用成功返回0,失败返回-1。
测试代码:从键盘读取数据写入管道,从管道中读取数据写到屏幕
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int fd[2];
int ret=pipe(fd);
char buf[100]={0};
if(ret<0)
{
perror("pipe");
return 1;
}
printf("pipe OK\n");
int len;
while(fgets(buf,100,stdin))
{
len=strlen(buf);
//write into pipe
ssize_t write_size=write(fd[1],buf,len);
if(write_size<0)
{
perror("write");
return 1;
}
memset(buf,0x00,sizeof(buf));
//read from pipe
ssize_t read_size=read(fd[0],buf,100);
if(read_size < 0)
{
perror("read");
return 1;
}
//write to pipe
if(write(1,buf,len)!=len)
{
perror("write");
return 1;
}
}
return 0;
}
如上我们从键盘输入一段数据到标准输入,就有相同的数据从标准输出输出。
用fork来共享管道
前面我们说到,匿名管道是用于有亲缘关系的进程间通信的,那么我们fork创建出的子进程是如何来共享管道的呢?
父进程在fork出子进程后,子进程拥有了和父进程一样的代码,当然也拥有和父进程一样的文件描述符,在fork出子进程后,父进程关闭管道的读端fd[0],子进程关闭管道的写端fd[1],从而来实现父子进程之间的管道通信,即从子进程向管道内写数据,父进程从管道内读数据。
管道的读写规则
当没有数据数据可读时
- O_NONBLOCK(阻塞打开): read 调用阻塞,进程暂停执行,一直等到有数据来为止
- O_NONBLOCK (非阻塞打开):调用返回-1,errno值为EAGAIN
当管道满的时候
- O_NONBLOCK(阻塞打开): write 调用阻塞,进程暂停执行,一直等到有数据读走数据
- O_NONBLOCK (非阻塞打开):调用返回-1,errno值为EAGAIN
如果所有管道写段对应的文件描述符关闭,则read返回0。
如果所有管道读端对应的文件描述符关闭,则write操作产生信号SIGPIPE,进而导致write进程退出。
当要写入的数据不大于管道的大小时,Linux保证数据写入的原子性。
当要写入的数据大于管道的大小时,Linux将不再保证数据写入的原子性。
匿名管道的特点
- 只能用于具有亲缘关系的进程;通常管道由一个进程创建,然后该进程调用fork,父子进程之间就可用该管道。
- 管道面向字节流(如果一次写100个,可以每次读10个这样来读)
- 管道的生命周期是随进程。进程退出,管道在内核中所对应的资源就释放
- 内核会对管道操作进行同步与互斥
- 管道只能往单向方向通信,若要实现双向通信必须创建两个管道
命名管道(FIFO)
命名管道相较于匿名管道的不同,就是能够适用于任意进程间的通信。
我们可以直接在命令行输入mkfifo+文件名
的方式来创建命名管道,也可以在程序中调用mkfifo函数来创建命名管道
不同的是,匿名管道在用pipe函数创建的同时也打开了,而命名管道在mkfifo创建了之后,还需用open函数打开该命名管道。
命名管道的打开规则
当以只读方式打开时(O_RDONLY)
- O_NONBLOCK(阻塞打开 O_RDONLY): read 调用阻塞,进程暂停执行,一直等到有进程以写方式打开
- O_NONBLOCK (非阻塞打开 O_RDONLY|O_NONBLOCK ):open函数立刻返回,read会直接返回0,当有其他进程按写方式打开时,read返回-1(虽然按写方式打开,但没人写数据)
当以只写方式打开时
- O_NONBLOCK(阻塞打开 O_WRONLY): write 调用阻塞,进程暂停执行,一直等到有进程以只读方式打开
- O_NONBLOCK (非阻塞打开 O_WRONLY|O_NONBLOCK):立即返回失败。
用命名管道实现server&client通信
server
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
umask(0);
int ret=mkfifo("./myfifo",0666);
if(ret < 0)
{
perror("mkfifo");
}
printf("mkfifo ok!\n");
int rfd = open("myfifo",O_RDONLY);
if(rfd < 0)
{
perror("open");
}
printf("open ok!\n");
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 -1] =0;
printf("client say %s\n",buf);
}
else if(s == 0)
{
printf("read done!\n");
return 0;
}
else
{
perror("read");
}
}
close(rfd);
return 0;
}
client
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int wfd = open("myfifo",O_WRONLY);
if(wfd < 0)
{
perror("open");
}
printf("open ok!\n");
char buf[1024];
while(1)
{
buf[0]=0;
printf("Please Enter...\n");
ssize_t s = read(0 , buf, sizeof(buf)-1);
if(s > 0)
{
buf[s] =0;
write(wfd,buf,sizeof(buf)-1);
}
else
{
perror("read");
}
}
close(wfd);
return 0;
}
结果如下(server端先运行)