进程通信简介:
IPC(进程间通信),前面我们学习了进程控制,了解进程间隔离的,各自有独立的虚拟内存空间无法直接访问对方的地址空间、进程调度和均衡则让进程互不干扰地使用处理器资源。但有时需要多个进程相互协作完成任务,而我们在没有学习进程间通信时我只能通过fork或exec传送打开的文件,或文件系统,进程之间的通信很不方便。
那么进程间通信的目的就很明确了:
1.数据传输:一个进程需要将自己的数据传递给另一个进程。
2.资源共享:多个进程之间需要共享部分资源。
3.通知事件:一个进程需要自己的所发生的事件传递给另一个进程,通知他们发生了什么事件(如:子进程退出需要通知父进程,父进程获取子进程状态)。一个进程需要发送一个或者一组消息给另一个进程。
4.进程控制:有些进程需要完全控制另一个进程的执行(如debug),此时进程希望能拦截另一个进程所有的陷入和异常,并及时直到它的状态。
进程间通信发展:
管道、System V进程间通信、POSIX进程通信
进程间通信的方式:
-
管道
1.管道
2.匿名管道 -
System V IPC:
1.System V 消息队列
2.System V 共享内存
3.System V 信号量 -
POSIX进程间通信:
1.消息队列
2.共享内存
3.信号量
4.互斥量
5.条件变量
6.读写锁
进程间通信管道(匿名管道):
这片博客的重点匿名管道从这就要开始了,管道是unix系统IPC最古老的方式,并且所有的Unix系统都支持这种通信机制。我们把进程连接到另一个进程的一个数据“流”称作管道,本片博客讲述无名管道。
-
无名管道特点:
1.管道是半双工(数据只能在一个方向上流动)。
2.管道只能在具有亲缘关系的进程之间通信(即具有共同祖先的进程之间通信)。
3.管道通信是面向字节流的。(字节流特点:数据无规则、无明显边界、收发灵活)。
4.管道的生命周期随进程。
5.管道自带同步与互斥(概念:1.临界资源及共享资源;2.临界区即对临界资源操作的代码;3.同步即临界资源访问的时序性控制;4.互斥即对临界资源访问的同一时间的唯一性控制,保护临界资源的安全性)。 -
管道创建:
#include <unistd.h>
int pipe(int pipefd[2]);
返回值:成功返回0,失败返回-1;
- 管道的原理:
在shell命令中当我们输入ls | more命令时会看到
ls | more 即将ls的输出做为more的输入最终得到more的结果。
对于单个进程的管道几乎是没有用的,通过管道我们可以完成父子进程间的通信。
父子进程通过管道通信原理
父进程读子进程写的原理过程:
管道使用实例:
test.c:父进程读文件子进程写文件
//父进程读,子进程写
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main(void)
{
char* msg = "hello pipe futher and sub\n";
char buf[1024];
//创建管道
int fd[2];
if(pipe(fd) < 0)
{
print("pipe error!");
exit(-1);
}
pid_t pid = fork();
if(pid < 0)
{
print("fork error!");
exit(-1);
}
//子进程写,父进程读
if(pid == 0)
{
//子进程关闭读端
close(fd[0]);
//写操作
write(fd[1],msg,strlen(msg));
exit(1);
}
else
{
wait(NULL);
//父进程关闭写端
close(fd[1]);
//读操作
size_t s = read(fd[0],buf,strlen(msg));
if(s>0)
{
print(buf);
}
else
{
print("read error!");
exit(-1);
}
}
return 0;
}
可以发现父进程读子进程写这个程序分为一下步骤:
1.创建管道
2.创建子进程
3.子进程关闭读端,子进程进行写操作
4.子进程退出
5.父进程进行等待
6.父进程关闭写端,父进程读取管道内容
类似于上述的过程可以来做一个小程序,可以将一片英语文件全部转化问大写,子进程来完成转换工作,父进程完成写如工作。
代码如下:
translate.c:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<assert.h>
#define IN "in"
#define OUT "out"
//小写字母翻译为大写字母
void translation(char* buf,char* tran_buf,int len)
{
if(buf == NULL)
return;
int i = 0;
for(i = 0;i<len;i++)
{
if('a'<=buf[i] && buf[i]<='z')
buf[i] -= ('a'-'A');
tran_buf[i] = buf[i];
}
}
int main(void)
{
//打开输入输出文件
FILE* f_in = fopen(IN,"r");
FILE* f_out = fopen(OUT,"w");
int fd[2];
assert(f_in != NULL && f_out != NULL);
pid_t pid = -1;
//创建管道
if(pipe(fd)<0)
{
perror("pipe");
exit(-1);
}
//创建子进程
if((pid = fork())<0)
{
perror("fork");
exit(-1);
}
if(pid == 0)
{
ssize_t s = 0;
char buf[1024];
//关闭管道读端
close(fd[0])
while((s = fread(buf,sizeof(char),1023,f_in)))
{
if(s>0)
{
char out[1024] = {0};
printf("%s\n",buf);
translation(buf,out,s);
write(fd[1],out,strlen(buf));
}
if(feof(f_in) == 0)
break;
}
//关闭管道写端
close(fd[1]);
printf("write over!\n");
exit(1);
}
//进程等待
wait(NULL);
ssize_t s = 0;
char buf[1024];
//关闭管道写端
close(fd[1]);
s = read(fd[0],buf,1024);
//关闭管道读端
close(fd[0]);
printf("%s\n",buf);
fwrite(buf,sizeof(char),strlen(buf),f_out);
return 0;
}
- 管道的读写规则:
1.当管道写满数据时,写入数据:
O_NONBLOCK:disable设置时,对管道的写操作会被阻塞。直到有数据被读取时才能继续写入数据。
O_NONBLOCK:enable设置时,对管道的写操作会立即返回-1,errno值为EAGAIN。
2.当管道为空时,读取数据:
O_NONBLOCK:disable设置时,对管道的读操作会被阻塞。直到有数据被写入时才能继续读取数据。
O_NONBLOCK:enable设置时,对管道的读操作会立即返回-1,errno值为EAGAIN。
3.如果所有写端全部被挂起(所有写描述符被关闭)时,这时读取数据,读取完管道中的数据时,则会返回0.
4.如果所有读端全部被挂起时,这时如果写入数据,则会触发异常,操作系统会给进程发送SIGPIPE信号(后面会解释),进程收到退出。
5.当写入数据不大于PIPE_BUF时,Linux保证写入数据的原子性。
6.当写入数据大于PIPE_BUF时,Linux不再保证写入数据的原子性。