LinuxC编程——进程间通信(一)(管道)

复杂的编程环境通常使用多个相关的进程来执行有关操作。进程之间必须进行通信,来共享资源和信息。因此,要求内核提供必要的机制,这些机制通常称为进程间通信(InterProcess Communication, IPC)。

一、Linux平台通信方式发展史

  1. 早期通信方式:早期的Unix IPC包括管道、FIFO和信号
  2. AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了“system V IPC”,其通信进程主要局限在单个计算机内。
  3. BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了只能在同一计算机通信的限制,形成了基于套接字(socket)的进程间通信机制。

二、进程间通信方式⭐⭐⭐

  1. 早期通信:无名管道(pipe),有名管道(fifo)、信号(sem)
  2. system V IPC:共享内存(share memory) 、信号灯集(semaphore)、消息队列(message queue)
  3. BSD:套接字(socket)

三、无名管道

3.1 特点⭐⭐⭐

  1. 只能用于具有亲缘关系的进程间进行通信
  2. 半双工通信,具有固定的读端与写端
    (单工:只能单方面传输信息->广播
    半双工:可以双向,但是同一时间只能一个方向传输信息
    全双工:可以双向同时传输信息)
  3. 无名管道可以看作一种特殊的文件,对它的读写采用文件IO:read、write
  4. 管道是基于文件描述符通信方式。当一个无名管道创建会自动创建两个文件描述符,分别的fd[0]、fd[1],其中fd[0]固定的读端,fd[1]固定的写端

3.2 函数pipe

int pipe(int fd[2])

  • 功能:创建无名管道
  • 参数:文件描述符(fd[0]:读端 fd[1]:写端)
  • 返回值:成功 0;失败 -1
    注📢:管道要用文件I/O进行操作(read,write,close)且管道创建后,fd[0]=3,fd[1]=4
    例:
    在这里插入图片描述
    在这里插入图片描述

3.3 注意事项⭐⭐⭐

  1. 当管道中无数据时,读操作会阻塞;管道中无数据,将写端关闭,读操作会立即返回
  2. 管道中装满(管道大小64K)数据写阻塞,一旦有4k空间,写继续,直到写满为止
  3. 只有在管道的读端存在时,向管道中写入数据才有意义。否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号 (通常Broken pipe错误)。(GDB调试可以查看到)

代码示例:

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

int main(int argc, char const *argv[])
{
    int fd[2] = {0};
    if (pipe(fd) < 0)
    {
        perror("pipe error");
        return -1;
    }
    printf("fd[0]:%d  fd[1]:%d\n", fd[0], fd[1]);

    char buf1[32] = {"hello world!"};
    char buf2[32] = {0};
    // write(fd[1],buf1,strlen(buf1)); //往管道写入buf1
    // ssize_t s = read(fd[0],buf2,32);  //从管道读取数据到buf2
    // printf("%s %d\n",buf2,s);
    // close(fd[0]);
    // close(fd[1]);
#if 0
    // 1.管道中无数据,读阻塞
    read(fd[0], buf2, 32);
#endif
#if 0
    // 2.将写端关闭,读操作会立即返回
    close(fd[1]);
    read(fd[0],buf2,32);
#endif
#if 1
    //3.1 当无名管道中写满数据64k,写阻塞
    char buf[65536] = {0};
    write(fd[1], buf, 65536);
    printf("full\n");
    write(fd[1], "a", 1);
    printf("write a ok\n");
    //至少读出4k的空间,才能继续写
    read(fd[0], buf, 4095);
    write(fd[1], 'a', 1);
    printf("write 'a' ok\n");
#endif
#if 1
    // 3.1 将读端关闭,继续写
    close(fd[0]);
    write(fd[1], "a", 1);
    printf("ok...\n");
#endif
    // close(fd[0]);
    // close(fd[1]);
    return 0;
}

第三种情况管道破裂,通过GDB调试查看到的结果如下:
在这里插入图片描述

3.4 练习

父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,输入一次打印一次,当输入quit结束,使用无名管道

/*
  练习:父子进程实现通信,父进程循环从终端输入数据,
  子进程循环打印数据,当输入quit结束,使用无名管道
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char const *argv[])
{
    char buf[32] = {0};
    int fd[2] = {0};
    if(pipe(fd)<0) //创建无名管道
    {
        perror("pipe err");
        return -1;
    }
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if(pid == 0)
    {
        while (1) //子进程循环从管道读取数据,管道为空阻塞
        {
            read(fd[0],buf,32);
            if(strcmp(buf,"quit")==0)
                exit(0);
            printf("%s\n",buf);
        }  
    }
    else
    {
        while(1)//循环从终端输入数据,循环往管道写入数据
        {
            //scanf("%s",buf);
            fgets(buf,32,stdin);
            if(buf[strlen(buf)-1] == '\n')
                buf[strlen(buf)-1] = '\0';
            write(fd[1],buf,strlen(buf)+1);
            if(strcmp(buf,"quit")==0)
                exit(0);
        }
        wait(NULL);
    }
    close(fd[0]);
    close(fd[1]);
    return 0;
}

四、有名管道

4.1 特点⭐⭐⭐

  1. 可以用于两个不相关的进程之间通信
  2. 有名管道可以通过路径名指出,在文件系统中可见,但内容存放在内存里
  3. 通过文件IO操作
  4. 遵循先进先出,故不支持lseek操作
  5. 半双工通信

4.2 函数 mkfifo

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

  • 功能:创健有名管道
  • 参数:
    • filename:有名管道文件名
    • mode:权限
  • 返回值:成功:0;失败:-1,并设置errno号
    注意对错误的处理方式:📢📢
    如果错误是file exist时,注意加判断,如:if(errno == EEXIST)
    执行如下代码:
    在这里插入图片描述
    第一次运行:
    在这里插入图片描述
    再次运行:
    在这里插入图片描述
    处理方式:捕捉错误码,进行过滤即可👇
    在这里插入图片描述
  1. 由上面的有名管道特点的第二条可以知道,写入有名管道的内容并非存放在文件中,而是存在内存,也就是说有名管道文件的大小为0,下面进行验证:
    在这里插入图片描述
    在写后面加一个while死循环,运行后会等写完阻塞,不读出数据。然后在终端可以查看当前管道文件的大小,结果是大小为0👇
    在这里插入图片描述

4.3 注意事项⭐⭐

  1. 只写方式,写阻塞,直到另一个进程将读打开
  2. 只读方式,读阻塞,直到另一个进程将写打开
  3. 可读可写,管道中无数据,读阻塞。

验证1,2:
创建两个c文件,一个以只读方式打开有名管道,从中读数据;另一个以只写方式打开同一个有名管道,从中写数据。
只读
在这里插入图片描述
只写
在这里插入图片描述
通过运行可以看到,运行了其中任意一个,会发生阻塞,只有当再运行另一个才可以解除阻塞,两个程序得以顺利执行下去。验证了只写方式,写阻塞,直到另一个进程将读打开只读方式,读阻塞,直到另一个进程将写打开。
还可以得知,上面程序并不是在read或iwrite发生阻塞,而是在open函数处发生了阻塞

补充:如果所有写进程都关闭命名管道,则只读进程的读操作会认为到达文件末尾,读操作解除阻塞并返回
验证:
以只读方式打开有名管道的程序代码1.c👇
在这里插入图片描述
以只写方式打开有名管道程序代码2.c👇
在这里插入图片描述
先执行1.c,会发生阻塞;再执行2.c,2.c不会往管道写入数据(保证1.c不会因为管道中有数据而解除阻塞),2.c间隔1秒关闭管道,可以看到原先阻塞的1.c也会在2.c执行1秒后解除阻塞,且read返回值为0。

4.4 练习

通过有名管道实现cp文件复制
方法一:两个c文件,一个只读管道,一个只写管道
3cp_MkfifoToDest.c

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

int main(int argc, char const *argv[])
{
    if(argc != 2)
    {
        printf("Please input %s <des>\n",argv[0]);
        return -1;
    }
    if(mkfifo("./fifo",0666) < 0)//创建有名管道
    {
        //处理文件已存在的情况
        if(errno == EEXIST)//EEXTST=17
        {
            printf("file eexist\n");
        }
        else
        {
            perror("mkfifo err");
            return -1;
        }
    }
    //打开管道和目标文件
    int fd = open("./fifo",O_RDONLY);
    //此处一定不要用可读可写的方式打开
    //若以可读可写的方式打开,管道中无数据则读阻塞
    if(fd<0)
    {
        perror("open fifo err");
        return -1;
    }
    int dest = open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("open dest err");
        return -1;
    }
    //循环读管道,写目标文件
    ssize_t s;
    char buf[32] = {0};
    while ((s=read(fd,buf,32)) != 0)
        write(dest,buf,s);
    close(fd);
    close(dest);
    return 0;
}

3cp_SrcToMkfifo.c

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

int main(int argc, char const *argv[])
{
    if(argc != 2)
    {
        printf("Please input %s <src>\n",argv[0]);
        return -1;
    }
    if(mkfifo("./fifo",0666) < 0)//创建有名管道
    {
        //处理文件已存在的情况
        if(errno == EEXIST)//EEXTST=17
        {
            printf("file eexist\n");
        }
        else
        {
            perror("mkfifo err");
            return -1;
        }
    }
    //打开管道和源文件
    int fd = open("./fifo",O_WRONLY);
    if(fd<0)
    {
        perror("open fifo err");
        return -1;
    }
    int src = open(argv[1],O_RDONLY);
    if(fd<0)
    {
        perror("open src err");
        return -1;
    }

    ssize_t s;
    char buf[32] = {0};
    while ((s=read(src,buf,32)) != 0)
    {
        write(fd,buf,s);
    }
    close(fd);
    close(src);
    return 0;
}

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

方法二:单个c文件,用父子进程实现,父进程只写有名管道,子进程只读有名管道

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

int main(int argc, char const *argv[])
{
    if (argc != 3)
    {
        printf("Please iniput %s <src> <dest>\n", argv[0]);
        return -1;
    }
    if (mkfifo("./fifo", 0666) < 0) //创建有名管道
    {
        //处理文件已存在的情况
        if (errno == EEXIST) //EEXTST=17
        {
            printf("file eexist\n");
        }
        else
        {
            perror("mkfifo err");
            return -1;
        }
    }
    //打开管道、源文件、目标文件
    
    int src = open(argv[1], O_RDONLY);
    if (src < 0)
    {
        perror("open src err");
        return -1;
    }
    int dest = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (dest < 0)
    {
        perror("open dest err");
        return -1;
    }

    ssize_t s;
    char buf[32] = {0};

    pid_t pid = fork(); // 创建子进程
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0) //子进程从有名管道中读出数据,写到目标文件中
    {
        int fd = open("./fifo", O_RDONLY); //管道设置为只读
        if (fd < 0)
        {
            perror("open fifo err");
            return -1;
        }
        while ((s = read(fd, buf, 32)) != 0)
            write(dest, buf, s);
        printf("child end...\n");
        close(fd);
        exit(0);
    }
    else //父进程从源文件读出数据,写到有名管道中
    {
        int fd = open("./fifo", O_WRONLY); //管道设置为只写
        if (fd < 0)
        {
            perror("open fifo err");
            return -1;
        }
        while ((s = read(src, buf, 32)) != 0)
            write(fd, buf, s);
        printf("parent end...\n");
        close(fd);
        wait(NULL);
    }
    close(src);
    close(dest);
    return 0;
}

注:该程序容易被怀疑最后在子进程的read(fd, buf, 32)会发生阻塞,其实不然,这里用到了上面的一个要点:如果所有写进程都关闭命名管道,则只读进程的读操作会认为到达文件末尾,读操作解除阻塞并返回

五、无名管道与有名管道对比⭐⭐

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sunqk5665

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值