进程间通信(IPC)1_匿名管道理解

24 篇文章 0 订阅
7 篇文章 0 订阅

进程间通信?

因为每一个进程都有一个虚拟地址空间, 保证了进程的独立性, 但也正是因此导致进程间无法通信. 所以需要操作系统提供进程间通信方式, 又因为通信的场景不同, 所以提供了多种不同的进程间通信方式

进程间通信的目的

  • 数据传输
  • 资源共享
  • 进程控制
  • 通知事件

进程间通信方式:

System V 标准的进程间通信方式:

  • 管道: 用于进程间的数据传输
  • 共享内存: 用于进程间的数据共享
  • 消息队列: 用于进程间的数据传输

管道

内核中的一块缓冲区,通过让多个进程都能访问到同一块缓冲区, 来实现进程间通信(半双工通信)
管道分为 匿名管道命名管道
匿名管道和命名管道区别在于这块内核中的缓冲区是否具有标识符(可见于文件系统的管道文件),其它的进程可以通过这个标识符,找到这块缓冲区(打开同一个管道文件, 进而访问到同一块缓冲区),实现通信.

管道特性

  • 具有同步与互斥的特性
    • 同步:对临界资源(公共资源)访问的合理性----通过条件判断当前进程能否访问,不能访问则等待,能访问在唤醒. 例如: 如果管道没有数据, 使用read()读取数据会阻塞, 如果管道写满了, 使用write()继续写入数据会阻塞
    • 互斥:保证在同一时间, 只有一个进程能对临界资源进行访问, 保证临界资源的安全性; 对管道进行数据操作的大小不超过PIPE_BUF, 保证操作的原子性
    • 如果管道所有的写端被关闭(表示当前没有进程继续写入数据了),read读完管道中的数据后, 直接返回0
    • 如果管道所有的读端被关闭(表示没有进程读取数据了)使用write继续写入数据会触发异常, 程序退出
    • 管道的生命周期随进程, 打开管道的所有进程退出, 管道也会被释放
    • 管道提供字节流传输服务
    • 命名管道
      若命名管道以只读的方式打开, 则会阻塞, 直到这个命名管道被以写的方式打开
      若命名管道以只写的方式打开, 则会阻塞, 直到这个命名管道被以读的方式打开
      若命名管道以读写的方式打开, 则不会阻塞

匿名管道

这块内核中中的缓冲区没有标识符,(没有标识符,其它的进程就不能找到这块缓冲区).所以只能用于具有亲缘关系的进程间通信(比如:子进程通过赋值父进程的方式, 获取到管道的操作句柄进而实现访问同一个管道)

通过代码加深对管道的理解

代码示例1:
父进程向管道写入数据
子进程从管道中读取数据

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
// 匿名管道只能用于具有亲缘关系的进程间通信, 子进程通过复制父进程的方式获取操作句柄
// 创建管道的操作要在创建子进程之前, 否则 子进程复制父进程就复制不到这个管道了
    int pipefd[2] = {-1};
    // 创建一个匿名管道, 通过参数pipefd返回管道的操作句柄
    // pipefd[0] : 用于从管道读取数据 pipefd[1] : 用于向管道写入数据
    // 成功 返回0  失败返回-1
    int ret = pipe(pipefd);
    if(ret < 0)
    {
        perror("pipe error!");
        return -1;
    }
    pid_t pid = fork();
    if(pid < 0)
    {
    	perror("fork error!");
    }
    else if(pid == 0)
    {
    	// 子进程
    	char buf[1024];
    	read(pipefd[0], buf, 1023);
    	printf("child:%s",buf);
    }
    else{
    	// 父进程
    	//sleep(3);
    	char *ptr = "我真的好菜啊!\n";
    	write(pipefd[1], ptr, strlen(ptr));
    }
    return 0;                                                                                                              
}

结果:父进程先于子进程退出, 子进程成为孤儿进程运行在后台
子进程运行在后台时,shell 切换回前台打印提示信息, 然后子进程打印信息就会打印到shell提示信息的后边
在这里插入图片描述
代码示例2:
在代码示例1的基础上,休眠父进程3秒,

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
    int pipefd[2] = {-1};
    int ret = pipe(pipefd);
    if(ret < 0)
    {
        perror("pipe error!");
        return -1;
    }
    pid_t pid = fork();
    if(pid < 0)
    {
    	perror("fork error!");
    }
    else if(pid == 0)
    {
    	// 子进程
    	char buf[1024];
    	read(pipefd[0], buf, 1023);
    	printf("child:%s",buf);
    }
    else{
    	// 父进程
    	sleep(3);
    	char *ptr = "我真的好菜啊!\n";
    	write(pipefd[1], ptr, strlen(ptr));
    }
    return 0;                                                                                                              
}

观察结果验证管道的特性:若管道中没有数据,调用 read 读取数据 则会阻塞一段时间(父进程休眠的时间)

代码示例3:
不停向管道写入数据,写满后, 使用write继续写入数据会阻塞;
管道是一块缓冲区, 大小有限

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
    int pipefd[2] = {-1};
    // 创建一个匿名管道, 通过参数pipefd返回管道的操作句柄
    // pipefd[0] : 用于从管道读取数据 pipefd[1] : 用于向管道写入数据
    // 成功 返回0  失败返回-1
    int ret = pipe(pipefd);
    if(ret < 0)
    {
        perror("pipe error!");
        return -1;
    }
    pid_t pid = fork();
    if(pid < 0)
    {
    	perror("fork error!");
    }
    else if(pid == 0)
    {
    	// 子进程
    	char buf[1024];
    	read(pipefd[0], buf, 1023);
    	printf("child:%s",buf);
    }
    else{
    	// 父进程
    	//sleep(3);
    	// 父进程
    	sleep(2);
    	char *ptr = "我真的好菜啊!\n";
    	int total_len = 0;
    	while(1)
     	{
       		int wlen = write(pipefd[1], ptr, strlen(ptr));
      		total_len += wlen;
   			printf("total_len = %d\n", total_len);
 		}
    }
    return 0;                                                                                                              
}

在这里插入图片描述
代码示例4:
关闭管道所有的读端pipefd[0], 使用write继续写入数据 观察结果
注意: 父子进程都有读写端

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
    int pipefd[2] = {-1};
    // 创建一个匿名管道, 通过参数pipefd返回管道的操作句柄
    // pipefd[0] : 用于从管道读取数据 pipefd[1] : 用于向管道写入数据
    // 成功 返回0  失败返回-1
    int ret = pipe(pipefd);
    if(ret < 0)
    {
        perror("pipe error!");
        return -1;
    }
    pid_t pid = fork();
    if(pid < 0)
    {
    	perror("fork error!");
    }
    else if(pid == 0)
    {
    	// 子进程
    	close(pipefd[0]);	// 关闭读端
    	sleep(100);  // 关闭了读端,休眠一段时间, 让代码不在向下运行
    	char buf[1024];
    	read(pipefd[0], buf, 1023);
    	printf("child:%s",buf);
    }
    else{
    	// 父进程
    	//sleep(3);
    	// 父进程
    	close(pipefd[0]);	// 注意:父进程也有读端
    	sleep(2);
    	char *ptr = "我真的好菜啊!\n";
    	int total_len = 0;
    	while(1)
     	{
       		int wlen = write(pipefd[1], ptr, strlen(ptr));
      		total_len += wlen;
   			printf("total_len = %d\n", total_len);
 		}
    }
    return 0;                                                                                                              
}

结果: 程序直接退出了(write触发了异常)
验证了特性--------若管道所有的读端被关闭, 继续写入数据会触发异常, 程序直接退出.
在这里插入图片描述
代码示例5:
关闭所有的写端pipefd[1], 继续使用 read 读数据.

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
    int pipefd[2] = {-1};
    // 创建一个匿名管道, 通过参数pipefd返回管道的操作句柄
    // pipefd[0] : 用于从管道读取数据 pipefd[1] : 用于向管道写入数据
    // 成功 返回0  失败返回-1
    int ret = pipe(pipefd);
    if(ret < 0)
    {
        perror("pipe error!");
        return -1;
    }
    pid_t pid = fork();
    if(pid < 0)
    {
    	perror("fork error!");
    }
    else if(pid == 0)
    {
    	// 子进程
    	close(pipefd[1]);	// 关闭写端
    	sleep(1);  
    	char buf[1024];
    	int rlen = read(pipefd[0], buf, 1023);
    	printf("child:%d--%s", rlen, buf);
    }
    else{
    	// 父进程
    	//sleep(3);
    	// 父进程
    	close(pipefd[1]);	// 注意:父进程也有写端
    	sleep(100);
    	char *ptr = "我真的好菜啊!\n";
    	int total_len = 0;
    	while(1)
     	{
       		int wlen = write(pipefd[1], ptr, strlen(ptr));
      		total_len += wlen;
   			printf("total_len = %d\n", total_len);
 		}
    }
    return 0;                                                                                                              
}

结果: read没有读到数据, 返回0
若管道所有的写端被关闭, 使用read读完数据后会返回0
如果管道中没有写入数据
1. 所有写端被关闭—继续读数据 read, 会返回0,不再阻塞
2. 写端没有全部关闭—继续读数据 会阻塞
在这里插入图片描述
代码示例6:
通过匿名管道实现管道符的作用
举例命令: ls - l | grep a
grep 从标准输入读取数据进行过滤

  • 通过重定向(dup2) 将标准输出写入到管道中
  • 通过重定向使 grep 从管道读取数据
/**                                                                                                             
 * 通过匿名管道实现管道符的作用
 * 实现: ls -l | grep a
 * ls 运行结束后将进程信息输出到标准输出
 * grep a从标准输入读取数据进行过滤
 */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<wait.h>
int main()
{
  int pipefd[2] = {-1};
  if(pipe(pipefd) < 0){
    perror("pipe error");
    return -1;
  }
  pid_t ls_pid = fork();
  if(ls_pid < 0){
    perror("ls_fork error");
    return -1;
  }
  else if(ls_pid == 0)
  {
    // ls子进程
    // 将标准输出重定向到管道输入端, 向1写入数据就相当于向管道写入数据
    // 关闭写端---ls进程一旦推出, 所有的写端被关闭, grep读完数据后返回0
    close(pipefd[1]);
    dup2(pipefd[1], 1);
    execlp("ls", "ls","-l", NULL);
    exit(0);
  }
  pid_t grep_pid = fork();
  if(grep_pid == 0){
    // grep 子进程
    // 关闭写端---ls进程一旦推出, 所有的写端被关闭, grep读完数据后返回0
    close(pipefd[1]);
    // 将标准输入重定向到管道读取端,
    dup2(pipefd[0], 0);
    execlp("grep", "grep", "a", NULL);
    exit(0);
  }
  // 父进程关闭读写端
  close(pipefd[1]);
  close(pipefd[0]);
  waitpid(ls_pid, NULL, 0);
  waitpid(grep_pid, NULL, 0);
 
 
  return 0;
}

结果:
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值