进程间通信
Posted DR5200
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程间通信相关的知识,希望对你有一定的参考价值。
一.进程间通信介绍
进程间通信目的
(1). 数据传输:一个进程需要将它的数据发送给另一个进程(如A进程要把数据传输给B进程,让B进程进行一些业务处理)
举例 :
log.txt 中有一些内容
cat log.txt | grep hello
cat 进程将log.txt中的数据通过管道交给grep进程
(2). 资源共享:多个进程之间共享同样的资源。
(3). 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
(4). 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
补充知识 :
(1). 前面的文章说过进程之间是具有独立性的,那就说明两个进程要想完成数据共享成本是比较高的,因为进程的独立性就体现在数据各自私有,所以进程间通信,一般一定要借助第三方(OS)资源
(2). 通信的本质就是"数据的拷贝"
进程A->数据拷贝给OS->OS把数据拷贝给进程B
OS一定要提供一段内存区域,能够被双方进程看到
(3). 进程间通信本质 : 让不同的进程,看到同一份资源(内存,文件内核缓冲等),资源由操作系统哪些模块提供,就有了不同的进程间通信方式
(4). 进程间通信是有标准的(system V标准/POSIX标准)
进程间通信发展
(1). 管道
(2). System V进程间通信
实现一台机器上的若干进程通信
(3). POSIX进程间通信
实现进程间可以跨网络通信(QQ聊天时就实现了跨主机通信)
进程间通信分类
管道 :
(1). 匿名管道pipe
(2). 命名管道
System V IPC :
(1). System V 共享内存
(2). System V 消息队列
(3). System V 信号量
POSIX IPC :
(1). 消息队列
(2). 共享内存
(3). 信号量
(4).互斥量
(5).条件变量
(6). 读写锁
二.管道
匿名管道
(1). 管道是Unix中最古老的进程间通信的形式。
(2). 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
(3). 管道只能进行单向通信
管道通信 : 管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。实际上管道的设计也是遵循UNIX的“一切皆文件”设计原则的,它本质上就是一个文件。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。
pipe函数介绍
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
// 以读方式和写方式打开同一个文件,返回的两个文件描述符填入fd数组中
返回值:成功返回0,失败返回错误代码
匿名管道实现步骤 :
(1). 调用pipe函数创建匿名管道,传入的输出型参数fd[2]调用后fd[0]为读端,fd[1]为写端
(2). fork创建子进程,子进程fd[0]同样为读端,fd[1]为写端
(3). 关闭父进程读端,子进程写端(或关闭父进程写端,子进程读端)
(4). 父进程向管道写,子进程从管道读,从而实现进程间通信
// 父子进程通过匿名管道实现通信的测试代码
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write father->read
int main()
{
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("pipe!\\n");
return -1;
}
//printf("fd[0] : %d\\n",fd[0]); 3
//printf("fd[1] : %d\\n",fd[1]); 4
pid_t id = fork();
if(id == 0)
{
close(fd[0]);
const char* msg = "I am a child\\n";
int count = 10;
while(count)
{
write(fd[1],msg,strlen(msg));
count--;
sleep(1);
}
exit(0);
}
close(fd[1]);
char buf[64];
while(1)
{
ssize_t s = read(fd[0],buf,sizeof(buf));
if(s > 0)
{
buf[s] = '\\0';
printf("child send to father : %s",buf);
}
else if(s == 0)
{
printf("read file end !\\n");
break;
}
else
{
perror("read\\n");
break;
}
}
waitpid(id,NULL,0);
return 0;
}
关于这段代码的几点分析和扩展
(1). 为什么子进程休眠1秒,运行结果父进程也休眠1秒呢 ?
临界资源 : 被多个进程共享的资源
临界区 : 访问临界资源的代码
进程互斥 : 在使用系统资源时,一个进程正在使用,另一个进程必须阻塞等待,不能同时使用
管道自带同步与互斥机制,进程互斥可以解决数据混乱的问题,如果没有互斥机制,子进程在写入的时候,父进程就有可能来读取,就会发生意料之外的结果
管道内部已经自动提供了互斥与同步机制,当子进程向管道写入数据,然后sleep,父进程去管道读取数据打印,然后read 识别到管道为空,父进程不再进行读取,阻塞式的等待子进程写入,所以并非是父进程sleep了,而是因为子进程写的慢,导致父进程阻塞式等待
(2). 子进程一直写,父进程sleep不去读,写满缓冲区之后,子进程会等待父进程读取之后再写入,子进程被挂起
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write father->read
int main()
{
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("pipe!\\n");
return -1;
}
//printf("fd[0] : %d\\n",fd[0]);
//printf("fd[1] : %d\\n",fd[1]);
pid_t id = fork();
if(id == 0)
{
close(fd[0]);
const char* msg = "I am a child\\n";
while(1)
{
write(fd[1],msg,strlen(msg));
}
exit(0);
}
close(fd[1]);
char buf[64];
while(1)
{
sleep(100);
ssize_t s = read(fd[0],buf,sizeof(buf));
if(s > 0)
{
buf[s] = '\\0';
printf("child send to father : %s",buf);
}
else if(s == 0)
{
printf("read file end !\\n");
break;
}
else
{
perror("read\\n");
break;
}
}
waitpid(id,NULL,0);
return 0;
}
(3). 父进程一直在读取,子进程写入5行后不再写入,父进程会等待子进程写入之后再读取,父进程被挂起
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write father->read
int main()
{
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("pipe!\\n");
return -1;
}
//printf("fd[0] : %d\\n",fd[0]);
//printf("fd[1] : %d\\n",fd[1]);
pid_t id = fork();
if(id == 0)
{
close(fd[0]);
const char* msg = "I am a child\\n";
int count = 10;
while(count)
{
if(count == 5)
{
sleep(1000);
}
else
{
sleep(1);
write(fd[1],msg,strlen(msg));
count--;
}
}
exit(0);
}
close(fd[1]);
char buf[64];
while(1)
{
ssize_t s = read(fd[0],buf,sizeof(buf));
if(s > 0)
{
buf[s] = '\\0';
printf("child send to father : %s",buf);
}
else if(s == 0)
{
printf("read file end !\\n");
break;
}
else
{
perror("read\\n");
break;
}
}
waitpid(id,NULL,0);
return 0;
}
(4). 父进程读端关闭,子进程写端再写入就无意义,操作系统会杀掉子进程,子进程会收到13号信号(SIGPIPE)
运行结果 :
child send to father : I am a child
child exit sigal : 13
// kill -l 查看信号
13) SIGPIPE
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write father->read
int main()
{
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("pipe!\\n");
return -1;
}
//printf("fd[0] : %d\\n",fd[0]);
//printf("fd[1] : %d\\n",fd[1]);
pid_t id = fork();
if(id == 0)
{
close(fd[0]);
const char* msg = "I am a child\\n";
int count = 5;
while(count)
{
if(count == 2)
{
sleep(1000);
}
else
{
sleep(1);
write(fd[1],msg,strlen(msg));
count--;
}
}
exit(0);
}
close(fd[1]);
char buf[64];
while(1)
{
ssize_t s = read(fd[0],buf,sizeof(buf));
if(s > 0)
{
buf[s] = '\\0';
printf("child send to father : %s",buf);
}
else if(s == 0)
{
printf("read file end !\\n");
break;
}
else
{
perror("read\\n");
break;
}
close(fd[0]);
break;
}
int status;
waitpid(id,&status,0);
printf("child exit,sign : %d\\n",status & 0x7F);
return 0;
}
(5). 对挂起的理解 :
所谓挂起是将进程的PCB由R状态设置成非R,然后将进程的PCB链入等待队列.
进程被唤醒(将进程的PCB由非R状态设置为R状态,将PCB链入运行队列中)
(6). 为什么子进程退出,父进程也退出了呢 ?
如果子进程写端关闭,父进程读端read返回值为0,代表文件结束
(7). 如果打开文件的进程退出了,文件也会被释放掉,所以管道的生命周期是随进程的
(8). 管道是半双工,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
全双工 : 指可以同时(瞬时)进行信号的双向传输(A→B且B→A)
半双工 : 只允许甲方向乙方传送信息,而乙方不能向甲方传送
(9). 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道
(10). 管道的大小是多大呢?
ulimit 是一条查看系统资源的命令,可以看到管道的大小
ulimit -a
// 代码测试管道的大小
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write father->read
int main()
{
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("pipe!\\n");
return -1;
}
pid_t id = fork();
if(id == 0)
{
close(fd[0]);
char c = 'a';
int count = 0;
while(1)
{
write(fd[1],&c,1);
count++;
printf("%d\\n",count);
}
exit(0);
}
close(fd[1]);
char buf[64];
while(1)
{
sleep(1000);
ssize_t s = read(fd[0],buf,sizeof(buf));
if(s > 0)
{
buf[s] = '\\0';
printf("child send to father : %s",buf);
}
else if(s == 0)
{
printf("read file end !\\n");
break;
}
else
{
perror("read\\n");
break;
}
close(fd[0]);
break;
}
waitpid(id,NULL,0);
return 0;
}
让父进程一直不读取,子进程一直在写入,就可以得出管道的大小为 65536 字节
命名管道
匿名管道可以让具有亲缘关系的进程完成通信,那毫无关系的进程怎么完成通信呢? 我们可以借助命名管道来完成,不同进程可以通过文件名打开同一个文件,就可以让不同进程看到同一份资源,但普通文件是很难做到通信的,实际上这个文件在磁盘上只是一个标识符,没有任何内容
命令创建命名管道
mkfifo fifo
举例 :
一个进程下执行如下命令
while :; do echo "hello world" ;sleep 1;done > fifo
一个进程下执行如下命令
cat < fifo
可以看到两个进程通过 fifo 命名管道完成了通信,当把读端关闭的时候,写端就无意义了,操作系统将写端杀掉了
函数创建命名管道
int mkfifo(const char *filename,mode_t mode);
创建成功返回0,失败返回-1
下面来写代码完成两个不相关的进程间的通信
// commit.h
#pragma once
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#define FILE_NAME "myfifo"
// colient.c
#include"comm.h"
int main()
{
int fd = open(FILE_NAME,O_WRONLY);
if(fd < 0)
{
perror("open\\n");
return 1;
}
char以上是关于进程间通信的主要内容,如果未能解决你的问题,请参考以下文章