Linux进程间通信-管道

进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某个事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程是具有独立性,所以要让其通信是有难度的。因此,想要让两个进程通信的前提条件就是,需要让不同的进程看到同一份资源,这个“资源”通常指的是某一块物理内存。

进程间通信的分类

1.管道

  • 匿名管道pipe
  • 命名管道

2.System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

3.POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

本片博客主要介绍管道的相关内容。

管道


什么是管道

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道只能单向通信。

匿名管道

匿名管道只能用于有亲缘关系的进程,常用于父子进程。

#include<unistd.h>
功能:创建一个无名管道

原型:
    int pipe(int fd[2]);

参数:
    fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端

返回值:
    成功返回0,失败返回错误代码

实例:键盘读取数据,写入管道,读取管道,写到屏幕

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
    int fd[2];
    char buf[100];
    int len= 0;

    if(pipe(fd)== -1)
    {
        perror("make pipe");
        exit(1);
    }
    while(fgets(buf,100,stdin))
    {
        len=strlen(buf);
        if(write(fd[1],buf,len)!=len)
        {
            perror("write to pipe");
            break;
        }
        memset(buf,0x00,sizeof(buf));
        if((len=read(fd[0],buf,100))== -1)
        {
            perror("read from pipe");
            break;
        }
        if(write(1,buf,len)!=len)
        {
            perror("write to stdout");
            break;
        }
    }
    return 0;
}

运行结果:

使用fork共享管道的原理

深度理解管道

站在文件描述符的角度来理解管道

代码实现:

#include<stdio.h>
#include<unistd.h>
#include<string.h>

int main()
{
    int fd[2]={0};
    pipe(fd);//创建管道

    pid_t id=fork();//创建子进程

    if(id== 0)
    {
        close(fd[1]);//子进程关闭写文件描述符,让子进程读
        char buf[1024];
        while(1)
        { 
            ssize_t s=read(fd[0],buf,sizeof(buf)-1);
            if(s>0)
            {
                 buf[s]=0;
                 printf("I am child,I got parent's message: '%s'\n",buf);
            }
        }
    }
    else
    {
        close(fd[0]);//父进程关闭读文件描述符,让父进程写
        char msg[]="hello world!";
        while(1)
        {
            write(fd[1],msg,strlen(msg));
            sleep(1);
        }
    }
    return 0;
}

通过这种方式,让父进程一直往管道里写,而子进程一直从管道里读取数据,从而实现了父子之间通信

站在内核的角度理解管道

所以我们看待管道,就如同看待文件一样,它们的使用是类似的,印证了“Linux中一切皆文件的思想”。

管道的读写规则

1.当没有数据可读时:

  • O_NONBLOCK disable(非阻塞模式禁止):read调用阻塞,即读进程暂停执行,一直等到有效数据来到为止。
  • O_NONBLOCK enable(非阻塞模式启动):read调用返回-1,error值为EAGAIN。

2.当管道满时:

  • O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
  • O_NONBLOCK enable:write调用返回-1,error值为EAGAIN。

3.如果所有管道写端对应的文件描述符被关闭,则read返回0.

4.如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。

5.当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。相反,要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性管道的上限一般为4k或8k。

这里我们需要解释几个概念:

多进程共享的资源我们叫做“临界资源”。访问临界资源的代码叫做“临界区”。任何时刻只允许一个进程访问临界区资源叫“互斥”,互斥能够保证安全性,但互斥并不一定是合理的。在保证临界资源安全的前提条件下(通常是互斥),让多进程访问临界资源具有一定的顺序性,我们称之为同步。同步的作用:协同进程步调,避免饥饿问题。

通常我们对临界资源要么不访问要么就访问完,没有中间态,我们称之为“原子性”。

管道读写常见的4种情况

情况1:

父进程一直写,但子进程一直等待就是不读,这样就会将管道写满。此时write调用阻塞,直到有数据被读走。

情况2:

父进程向管道里写入一条数据后直接退出了,子进程读数据读完此条数据后read函数返回0。

情况3:

父进程一直写入数据,子进程几秒钟之后退出,管道读端对应的文件描述符被关闭,write操作会产生信号SIGPIPE,进而导致write进程退出。

情况4:

父进程不写数据,也不关闭写文件描述符,子进程一直读,此时子进程调用read阻塞,程序会被卡住,即读进程暂停执行,一直等到有效数据来到为止。

管道的特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信。通常一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间可应用该管道。
  • 管道提供流式服务(即单向通信)。
  • 一般而言,进程退出,进程退出,管道释放,所以管道的生命周期随进程。(文件的生命周期也随进程)
  • 一般内核会对管道操作进行同步与互斥。
  • 管道是半双工的,数据只能向一个方向流动,需要双方通信时建立两个管道。

命名管道

  • 管道应用的一个限制就是只能在具有公共祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这件事情,它经常被称为命名管道。命名管道是一种特殊类型的文件。

创建命名管道

1.可以从命令行上创建

mkfifo filename

2.命名管道也可以从程序里创建,相关函数是:

int mkfifo(const char *filename,mode_t mode);

创建命名管道:

运行后,可以发现多了一个文件

匿名管道和命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open。
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于他们创建与打开的方式不同,但是一旦这些工作完成之后,它们具有相同的语义。

命名管道的打开规则

1.如果当前打开操作是为了读而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
  • O_NONBLOCK enable:立刻返回成功。

2.如果当前打开操作是为写而打开FIFO时:

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

用命名管道实现server&client通信

Makefile

.PHONY:all
all:ClientPipe ServerPipe
ClientPipe:ClientPipe.c
	gcc -o $@ $^
ServerPipe:ServerPipe.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm -f ClientPipe ServerPipe

ServerPipe.c

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>

#define ERR_EXIT(m)\
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

int main()
{
    if(mkfifo("mypipe",0644)< 0)
    {
        ERR_EXIT("mkfifo");
    }
    int rfd=open("mypipe",O_RDONLY);
    if(rfd< 0)
    {
        ERR_EXIT("open");
    }

    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]=0;
            printf("client say# %s\n",buf);
        }
        else if(s==0)
        {
            printf("client quit,exit now!\n");
            exit(EXIT_SUCCESS);
        }
        else
        {
            ERR_EXIT("read");
        }
    }
    close(rfd);
    return 0;
}

ClientPipe.c

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>

#define ERR_EXIT(m)\
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

int main()
{
    int wfd=open("mypipe",O_WRONLY);
    if(wfd< 0){
        ERR_EXIT("open");
    }

    char buf[1024];
    while(1)
    {
        buf[0]=0;
        printf("Please Enter# ");
        fflush(stdout);
        ssize_t s=read(0,buf,sizeof(buf)-1);
        if(s > 0)
        {
            buf[s]=0;
            write(wfd,buf,strlen(buf));
        }
        else
        {
            ERR_EXIT("read");
        }
    }
    close(wfd);
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值