Linux进程间通信(一)
进程间通信
概念:进程是一个独立的资源分配单位,不同进程之间有关联,不能在一个进程中直接访问另一个进程的资源。
- 进程和进程之间的资源是相互独立的,一个进程不能直接访问另外一个进程的资源,但是进程和进程之间不是相互独立的。
通信目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知某些或某个进程发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
如何实现进程通信?
要让两个不同的进程实现通信,前提条件是让它们看到同一份资源。所以要想办法让他们看到同一份资源,就需要采取一些手段,可以分为下面几种。
通信方式分类
1.管道
- 匿名管道pipe
- 命名管道
2.System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
3.POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
概念:我们把一个进程连接到另一个进程的一个数据流称为一个“管道”。
管道的特点:
- 数据只能从管道的一端写入,从另一端读出
- 写入管道的数据遵循先入先出的原则
- 管道所传达的数据是无格式的,这要求管道的读出方和写入方必须事先约定好数据的格式
- 管道不是普通的文件,不属于某个文件系统,只存在于内存中
- 管道读数据是一次性的,数据一旦被读走,它就从管道中抛弃,释放空间
- 管道是一种特殊的文件类型,会在应用层打开两个文件描述符fd[0]对应的是写端,fd[1]对应的是读端
- 管道只能服务于有血缘关系的两个进程
匿名管道
创建匿名管道-----pipe系统调用
int pipe(int pidefd[2]);
功能:创建无名管道
参数:pipefd:为int类型数组的首地址,其存放了管道的文件描述符pipefd[0]、pipefd[1]
当一个管道建立的时候,他会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。
返回值:成功:0 失败:-1
- 文件描述符就是操作系统为了高效管理已经打开文件所创建的一个索引(文件描述符在前面的文章介绍过)
匿名管道创建原理:
调用pipe函数后,OS会在fd_array数组中分配两个文件描述符给管道,一个是读,一个是写,并把这两个文件描述符放到用户传进来的数组中,fd[0]代表管道读端,fd[1]代表管道写端。这样一个管道就创建好了。
实例演示:
实例1:观察两个文件描述符的值
#include <stdio.h>
#include <unistd.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1){
// 管道创建失败
perror("make piep");
//用于退出进程
exit(-1);
}
// 成功返回0
// pipefd[0] 代表读端
// pipefd[1] 代表写端
printf("fd[0]:%d, fd[1]:%d\n", pipefd[0], pipefd[1]);
return0;
}
运行结果如下:
显然,pipefd这个数组里面放的是两个文件描述符,分别是3和4,因为0,1,2文件描述符在进程创建的时候会由系统自动创建。
实例2:尝试使用管道读写数据
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1){
// 管道创建失败
perror("make piep");
exit(-1);
}
char buf[64] = "hello world";
// 写数据
write(pipefd[1], buf, sizeof(buf)/sizeof(buf[0]));
// 读数据
memset(buf,0,sizeof(buf));// 清空buf
ssize_t s = read(pipefd[0], buf, 11);
buf[s] = '\0';
printf("%s\n", buf);
return 0;
}//成功输出hello world
可以看见对管道的操作,实际上就是对两个读写文件的操作,本质就是对文件的操作和使用。
管道的本质
Linux下一切皆文件,看待管道,其实时可以像看待文件一样。且管道和文件使用方法是一致的。管道的生命周期随进程。
父子进程通过匿名管道通信
原理:匿名管道是提供给有亲缘关系两个进程进行通信的。所以我们可以在创建管道之后通过fork函数创建子进程,这样父子进程就看到同一份资源,且父子进程都有这个管道的读写文件描述符。我们可以关闭父进程的读端,关闭子进程的写端,这样子进程往管道里面写数据,父进程往管道里面读数据,这样两个进程就可以实现通信了。
原理解读:
fork函数调用成功后,将为子进程申请PCB和用户内存空间,子进程是父进程的副本,在用户空间将复制父进程用户空间所有的数据(代码段、数据段、BBS、栈、堆,实际上是复制的父进程的虚拟空间的地址),子进程从父进程继承下列属性:有效用户、组号、进程组号、环境变量、信号处理方式设置、信号屏蔽集合、当前工作目录、根目录、文件模式掩码、文件大小限制和打开的文件描述符(特别注意:共享同一文件表项)。
共享同一文件表项就造成了一种现象,父子进程无论谁对文件进行操作,那么另外一个进程的文件表也会受到相同的影响。
从图中可以看出,虽然在子进程的表项中式复制了关于打开文件的信息,但是他们是共享文件表的,所以如果一个进程对文件指针进行移动,那么肯定会影响到另外的进程。
思考:这是不是和写时拷贝相违背了,为什么文件表就能共享了呢?
要知道在linux源码中,每个进程都存在一个PCB结构体,每个PCB中,存放了一个结构体指针指向一个我们理解为文件描述符的结构体struct file,而这个结构体里,才存了文件的id,值得注意的是,这个结构体里有一个指针才是指向真正文件的。文件系统存在于磁盘当中,对磁盘的操作操作系统不会拷贝一份文件给子进程,相反,像那些临时创建存放于堆区和栈区的数据,操作系统会采用写时拷贝,进行复制。
总结:父子进程共享文件表,对文件表进行的任何操作都会对父子进程造成相同的影响,与写时拷贝进行区分。
父子进程通过创建匿名管道通信具体过程如下:
1.父进程创建管道(管道创建要在进程创建之前)
2.fork创建子进程(子进程继承父进程的管道文件描述符)
3.关闭父进程的写段,子进程的读端
实例演示: 子进程每隔1秒往管道里面写数据,父进程每隔1秒往管道里读数据
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1){
// 管道创建失败
perror("make piep");
exit(-1);
}
pid_t id = fork();
if (id < 0){
perror("fork failed");
exit(-1);
}
else if (id == 0){
// child
// 关闭读端
close(pipefd[0]);
const char* msg = "I am child...!\n";
//int count = 0;
// 写数据
while (1){
ssize_t s = write(pipefd[1], msg, strlen(msg));
printf("child is sending message...\n");
sleep(1);
}
}
else{
// parent
close(pipefd[1]);
char buf[64];
while (1){
ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
if (s > 0){
buf[s] = '\0';// 字符串后放一个'\0'
printf("father get message:%s", buf);
}
else if (s == 0){
// 读到文件结尾 写端关闭文件描述符 读端会读到文件结尾
printf("father read end of file...\n ");
}
sleep(1);
}
}
return 0;
}
运行结果如下:
匿名管道读写规则
读写规则总结:
- 当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。 - 当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN - 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性
注意:O_NONBLOCK是非阻塞的标志位,指定管道对我们的操作要么成功,要么立刻返回错误,不被阻塞。
管道特点(了解)
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创
建,然后该进程调用fork,此后父、子进程之间就可应用该管道。管道提供流式服务。也就是你想往管道里读写多少数据是根据自身来定的
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
半双工是指传输过程中同时只能向一个方向传输,一方的数据传输结束之后,另外一方再回应。双方传输数据是不可以同时进行的
全双工是指两方能同时发送和接受数据。在这种情况下就没有拥堵的危险,数据的传输也就更快
命名管道
概念:无名管道,由于没有名字,所以只能用于亲缘关系的进程通信。为了克服这个缺点,提出了命名管道(FIFO)。
命名管道不同于无名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信,因此,通过FIFO不相关的进程也能交换数据。
- FIFO在文件系统(磁盘上)中作为一个特殊文件而存在,但是FIFO中的内容却存放在内存中。
- 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。
- FIFO有名字,不相关的进程可以通过打开命名通道进行通信。
创建命名管道
1.通过命令创建命名管道
mkfifo filename
2.通过函数创建命名管道
int mkfifo(const char *pathname, mode_t mode);
功能:创建命名管道
参数:pathname:普通的路径名,也就是创建后FIFO的名字。
mode:文件的权限,与打开普通文件的open函数中的mode参数类似。
返回值:成功:0 (状态码) 失败:如果文件已经存在,则会出错返回-1
代码示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO "./fifo"
int main()
{
umask(0);
// 创建管道
int ret = mkfifo(FIFO, 0666);
if (ret == -1){
perror("make fifo");
exit(-1);
}
}
运行结果如下:
上面说过,管道其实就是一种特殊的文件,管道文件大小是0,因为上面介绍过,管道文件的内容都存放在内存当中。
命名管道读写操作以及注意事项
一旦创建了一个FIFO,就可以用open打开它,常见的文件I/O都可以作用于FIFO文件。
FIFO严格的遵循先进先出的原则,对管道以及FIFO的读总是从开始处返回数据,对它们的写则是把数据添加到末尾。
- 一个为只读而打开一个管道的进程会阻塞直到另外一个进程为只写打开该管道
- 一个为只写而打开一个管道的进程会阻塞直到另外一个进程为只读打开该管道
读写规则:
读管道
- 管道中有数据,read返回返回实际读到的字节数
- 管道中无数据:(1)若管道写端被全部关闭,read返回0
(2)若写端没有全部关闭,read阻塞等待
写管道
- 管道读端全部被关闭,进程异常终止
- 管道读端没有全部关闭:(1)若管道已经满了。write阻塞
(2)若管道没满,write将数据写入,并返回实际写入的字节数
使用命名管道进行通信
接下来我会使用命名管道实现简单的版本聊天。
talkA.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
//先读后写
//以只读的方式打开管道1
//以只写的方式打开管道2
define SIZE 1024
int main()
{
int fdr = -1;
int fdw = -1;
int ret = -1;
char buf[SIZE];
//以只读的方式打开管道1
fdr = open("fifo1",O_RDONLY);
if(-1==fdr)
{
perror("open");
return 1;
}
printf("以只读的方式打开管道1....\n");
//以只写的方式打开管道2
fdw = open("fifo2",O_WRONLY);
if(-1==fdw)
{
perror("open");
return 1;
}
printf("以只写的方式打开管道2....\n");
//循环读写
while(1)
{
//读管道1
memset(buf,0,SIZE);
ret = read(fdr,buf,SIZE);
if(ret<=0)
{
perror("read");
break;
}
printf("read:%s\n",buf);
//写管道2
memset(buf,0,SIZE);
fgets(buf,SIZE,stdin);
//去掉最后一个换行符
if('\n'==buf[strlen(buf)-1])
buf[strlen(buf)-1]=0;
//写管道
ret = write(fdw,buf,strlen(buf));
if(ret<=0)
{
perror("write");
break;
}
printf("write ret:%d\n",ret);
}
//关闭文件描述符
close(fdr);
close(fdw);
}
talkB.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
//以只读的方式打开管道2
//以只写的方式打开管道1
#define SIZE 1024
int main()
{
int fdr = -1;
int fdw = -1;
int ret = -1;
char buf[SIZE];
//以只写的方式打开管道1
fdw = open("fifo1",O_WRONLY);
if(-1==fdw)
{
perror("open");
return 1;
}
printf("以只写的方式打开管道1....\n");
//以只读的方式打开管道2
fdr = open("fifo2",O_RDONLY);
if(-1==fdr)
{
perror("open");
return 1;
}
printf("以只读的方式打开管道2....\n");
//循环读写
while(1)
{
//写管道1
memset(buf,0,SIZE);
fgets(buf,SIZE,stdin);
//去掉最后一个换行符
if('\n'==buf[strlen(buf)-1])
buf[strlen(buf)-1]=0;
//写管道
ret = write(fdw,buf,strlen(buf));
if(ret<=0)
{
perror("write");
break;
}
printf("write ret:%d\n",ret);
//读管道2
memset(buf,0,SIZE);
ret = read(fdr,buf,SIZE);
if(ret<=0)
{
perror("read");
break;
}
printf("read:%s\n",buf);
}
//关闭文件描述符
close(fdr);
close(fdw);
}
运行结果如下:可以实现阻塞式的数据读取
当两个进程通信的时候,我们查看fifo的大小:
可以发现,管道的大小没有发生变化。其实两个进程通信是在内存中进行的,并没有把数据写到管道中,因为管道只是一个符号性的文件。如果是在管道写数据,那么IO次数会很多,效率太低了。
Linux进程间通信(一)的更多相关文章
- Linux进程间通信(一): 信号 signal()、sigaction()
一.什么是信号 用过Windows的我们都知道,当我们无法正常结束一个程序时,可以用任务管理器强制结束这个进程,但这其实是怎么实现的呢?同样的功能在Linux上是通过生成信号和捕获信号来实现的,运行中 ...
- Linux进程间通信(二):信号集函数 sigemptyset()、sigprocmask()、sigpending()、sigsuspend()
我们已经知道,我们可以通过信号来终止进程,也可以通过信号来在进程间进行通信,程序也可以通过指定信号的关联处理函数来改变信号的默认处理方式,也可以屏蔽某些信号,使其不能传递给进程.那么我们应该如何设定我 ...
- Linux进程间通信(三):匿名管道 popen()、pclose()、pipe()、close()、dup()、dup2()
在前面,介绍了一种进程间的通信方式:使用信号,我们创建通知事件,并通过它引起响应,但传递的信息只是一个信号值.这里将介绍另一种进程间通信的方式——匿名管道,通过它进程间可以交换更多有用的数据. 一.什 ...
- Linux进程间通信(四):命名管道 mkfifo()、open()、read()、close()
在前一篇文章—— Linux进程间通信 -- 使用匿名管道 中,我们看到了如何使用匿名管道来在进程之间传递数据,同时也看到了这个方式的一个缺陷,就是这些进程都由一个共同的祖先进程启动,这给我们在不相关 ...
- Linux进程间通信(五):信号量 semget()、semop()、semctl()
这篇文章将讲述别一种进程间通信的机制——信号量.注意请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物.有关信号的更多内容,可以阅读我的另一篇文章:Linux进程间通信 -- 信号.下面 ...
- Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()
下面将讲解进程间通信的另一种方式,使用共享内存. 一.什么是共享内存 顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存.共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式 ...
- Linux进程间通信(七):消息队列 msgget()、msgsend()、msgrcv()、msgctl()
下面来说说如何用不用消息队列来进行进程间的通信,消息队列与命名管道有很多相似之处.有关命名管道的更多内容可以参阅我的另一篇文章:Linux进程间通信 -- 使用命名管道 一.什么是消息队列 消息队列提 ...
- Linux进程间通信(九):数据报套接字 socket()、bind()、sendto()、recvfrom()、close()
前一篇文章,Linux进程间通信——使用流套接字介绍了一些有关socket(套接字)的一些基本内容,并讲解了流套接字的使用,这篇文章将会给大家讲讲,数据报套接字的使用. 一.简单回顾——什么是数据报套 ...
- [转]Linux进程间通信——使用消息队列
点击此处阅读原文 另收藏作者ljianhui的专栏初学Linux 下面来说说如何使用消息队列来进行进程间的通信,消息队列与命名管道有很多相似之处.有关命名管道的更多内容可以参阅我的另一篇文章:Linu ...
- Linux 进程间通信(二) 管道
Linux 进程间通信-管道 进程是一个独立的资源分配单位,不同进程之间的资源是相互独立的,没有关联,不能在一个进程中直接访问另一个进程中的资源.但是,进程不是孤立的,不同的进程之间需要信息的交换以及 ...
随机推荐
- Windows 电脑杀毒简单有效的方式
Windows 电脑杀毒通常会选择杀毒软件,这样太笨重,且容易占内存和存在流氓软件侵入. 推荐使用 Windows 自带的恶意软件删除工具 按住 Win + R 键,弹出运行窗口,输入 mrt. 系统 ...
- Git 03 理论
参考源 https://www.bilibili.com/video/BV1FE411P7B3?spm_id_from=333.999.0.0 版本 本文章基于 Git 2.35.1.2 四个区域 G ...
- 图解OSI七层模型
七层模型,亦称OSI(Open System Interconnection)参考模型,是参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系.它是一个七层的.抽象的模型体 ...
- Go语言Tips
时间日期格式化 time.Now().Format("2006-01-02") 原生DefaultServeMux支持restful路由 ref: https://towardsd ...
- java过滤器的写法
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 原文地址:http://t.csdn.cn/ZD88A ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 过滤器实际上就是 ...
- 2022年NISP考试时间|NISP一级考试时间|NISP|网安伴|NISP管理中心
NISP一级~~国家信息安全水平考试一级证书 NISP一级证书是由中国信息安全测评中心颁发的国家级认证证书.面向全社会各行各业通用的信息安全意识普及和信息安全保护知识培训,是在任何单位和工作中都应具备 ...
- 有一个线性表,采用带头结点的单链表L来存储,设计一个算法将其逆置,且不能建立新节点,只能通过表中已有的节点的重新组合来完成。
有一个线性表,采用带头结点的单链表L来存储,设计一个算法将其逆置,且不能建立新节点,只能通过表中已有的节点的重新组合来完成. 分析:线性表中关于逆序的问题,就是用建立链表的头插法.而本题要求不能建立新 ...
- 系统CPU飙高,怎么排查?
cpu是整个电脑的核心计算资源,对于一个应用进程来说,cpu的最小执行单元是线程. 导致cpu飙高的原因有几个方面: cpu上下文切换过多,对于cpu来说,同一时刻下每个cpu核心只能运行一个线程,如 ...
- springboot配置(yami配置文件,JSR303数据校验,多环境配置)
yami配置文件 YAML是 "YAML Ain't a Markup Language" (YAML不是一种标记语言)的递归缩写.在开发的这种语言时,YAML 的意思其实是:&q ...
- day01-GUI坦克大战01
JavaGUI-坦克大战 1.Java绘图坐标体系 坐标体系介绍:下图说明了一个Java坐标体系.坐标原点位于左上角,以像素为单位.在Java坐标体系中,第一个是x坐标,表示当前位置为水平方向,距离坐 ...