《linux系统编程 —— 3.IPC之管道(上)》

1.概念扫盲

1.1 IPC的概念

        IPC (Inter-Process Communication) 是指进程间通信的技术和机制。在操作系统中,不同的进程可能需要相互交换信息、共享资源或协调工作,而 IPC 提供了一种机制,使得这些进程可以进行有效的通信和数据交换。

        IPC 提供了各种通信方式和机制,包括管道、消息队列、共享内存、信号量、套接字等。这些机制允许进程之间进行数据传输、同步操作、共享资源等,以实现进程之间的协作和通信。

        通过使用 IPC,进程可以在共享资源的基础上进行协同工作,实现数据共享、任务协作和系统集成。IPC 在操作系统和网络编程中起着重要的作用,它使得不同进程之间能够相互通信和协作,提高了系统的灵活性、可扩展性和效率。

2. 管道

        在Linux C语言中,管道(Pipeline)是一种进程间通信的机制,可以用于在C程序中实现进程间的数据传输,管道是通过操作系统提供的系统调用函数 pipe 来创建的。pipe 函数创建一个管道,返回两个文件描述符,一个用于读取管道数据,另一个用于写入管道数据。这两个文件描述符分别被称为管道的读端和写端。

        管道有以下两种

        1、匿名管道 pipe:适用于亲缘关系进程间的、一对一的通信

        2、具名管道 fifo :适用于任何进程间的一对一、多对一的通信

2.1匿名管道

管道的创建方法如下

#include <unistd.h>

int pipe( int fd[2] );

        将一个有两个元素的整形数组地址传入到pipe函数中,该数组即可被初始化,初始化后的数组中即包含了两个文件描述符,分别代表着管道的读写端。其中fd[0]代表着文件的读端。fd[1]代表着文件的写端。

        下面是一个父进程向子进程发送信息,子进程接收信息并显示出来,如果父进程发送quit则子进程退出的代码例程:

#define BUFFERMAX 1024
int main()
{
    int pipefd[2];  
    int ret = pipe( pipefd );

    if(ret == -1)
    {
        perror("pipe");
    }

    pid_t pid_fd = fork();
    int status = 0;

    if(pid_fd == 0) //子  == 进程2
    {
        char getmsg[BUFFERMAX];
        while (1)
        {
            close(pipefd[1]);
            bzero(getmsg,BUFFERMAX);
            read(pipefd[0],getmsg,BUFFERMAX);
            printf("读到的字符串是%s\n",getmsg);
            if( strstr(getmsg,"quit") != NULL)
            {
                printf("找到了子串,子进程退出\n");
                exit(0);
            }
        }
    }
    else if(pid_fd > 0) //父 == 进程1
    {
        char inputmsg[BUFFERMAX];
        while (1)
        {
            close(pipefd[0]);
            bzero(inputmsg,BUFFERMAX);
            printf("请输入要发送的字符串\n");
            scanf("%s",inputmsg);
            write(pipefd[1],inputmsg,BUFFERMAX);
            SLEEP_MS(10);
            if( strstr(inputmsg,"quit") != NULL)
            {
                wait(&status);
                break;
            }              
        }       
    }
}

2.2 管道的读写特性

2.2.1 读者: 对管道拥有读权限的进程

       

        在理解读取管道时的阻塞和非阻塞行为时,需要考虑管道的读写端的状态和行为。

        当有写者存在时,也就是有进程向管道中写入数据,读者在读取管道时的行为如下:

        1、如果管道中有数据可读,读者会正常读取数据,并将其从管道中移除。

        2、如果管道中没有数据可读,读者会进入阻塞状态,等待写者向管道中写入数据。

        当没有写者存在时,也就是没有进程向管道中写入数据,读者在读取管道时的行为如下:

        3、如果管道中有数据可读,读者会正常读取数据,并将其从管道中移除。

        4、如果管道中没有数据可读,读者不会进入阻塞状态,而是立即返回,返回值为0,表示没有读取到任何数据。

2.2.2 写者: 对管道拥有写权限的进程

      

        当有读者存在时,也就是有进程从管道中读取数据,写者在写入管道时的行为如下:

1、如果管道的缓冲区未满,写者会正常写入数据,并将其存储在管道中。
2、如果管道的缓冲区已满,写者会进入阻塞状态,等待读者从管道中读取数据,以腾出空间供写入。

没有读者存在时,也就是没有进程从管道中读取数据,写者在写入管道时的行为如下:

3、无论缓冲区是否已满,写者会立即收到 `SIGPIPE` 信号,该信号表示管道的写端已经关闭或无法写入数据。通常情况下,写者需要正确处理该信号,以避免程序异常终止。

 下面演示一下当写端已经关闭时,read会立即返回的现象:我们基于上面的代码做了一些增添,让父进程写入一次数据后便直接退出(即此时已经没有写者存在)

int main()
{
    int pipefd[2];  
    int ret = pipe( pipefd );

    if(ret == -1)
    {
        perror("pipe");
    }

    pid_t pid_fd = fork();
    int status = 0;

    if(pid_fd == 0) //子  == 进程2
    {
        char getmsg[BUFFERMAX];
        while (1)
        {
            close(pipefd[1]);
            bzero(getmsg,BUFFERMAX);
            read(pipefd[0],getmsg,BUFFERMAX);
            printf("读到的字符串是%s\n",getmsg);
            if( strstr(getmsg,"quit") != NULL)
            {
                printf("找到了子串,子进程退出\n");
                exit(0);
            }
        }
    }
    else if(pid_fd > 0) //父 == 进程1
    {
        char inputmsg[BUFFERMAX];
        while (1)
        {
            close(pipefd[0]);
            bzero(inputmsg,BUFFERMAX);
            printf("请输入要发送的字符串\n");
            scanf("%s",inputmsg);
            write(pipefd[1],inputmsg,BUFFERMAX);

            if( strstr(inputmsg,"quit") != NULL)
            {
                wait(&status);
                break;
            }    

            printf("父进程退出,此时无写者存在\n");
            exit(0);          
        }       
    }
}

此时显示结果如下,可以看到子进程不断执行printf语句,未被阻塞。

请输入要发送的字符串
123
父进程退出,此时无写者存在
读到的字符串是123
root@ubuntu:/mnt/hgfs/share_file/系统编程/2.system、exec、管道# 读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是
读到的字符串是

思考1:那么read函数又是怎么知道写端被关闭了呢?

读端或写端关闭的情况通常有以下几种:

1. 写端关闭:当写端所属的进程或文件描述符调用了 `close` 函数关闭了管道的写端,我们可以认为写端已关闭。

2. 读端关闭:当读端所属的进程或文件描述符调用了 `close` 函数关闭了管道的读端,我们可以认为读端已关闭。

3. 进程终止:当与管道相关联的进程终止时,操作系统会自动关闭进程的所有打开文件描述符,包括管道的读端和写端。因此,当与管道相关的进程终止时,我们可以认为管道的读端和写端都已关闭。

4. 程序执行完毕:在某些情况下,写端或读端可能是由同一个程序在不同的时刻打开和关闭的。当程序执行完毕,也就是主程序 `main` 函数结束时,操作系统会自动关闭程序打开的所有文件描述符,包括管道的读端和写端。

        需要注意的是,关闭管道的读端和写端并不是互斥的。当读端和写端都关闭后,管道才被认为完全关闭,否则管道仍然是部分打开状态。在读端和写端关闭后,进程仍然可以继续操作打开的另一端。

        而read函数并不直接判断是否有写者存在。它的行为是基于管道的状态来确定是否阻塞,以及何时返回。如果没有写者存在,读者会在读取到管道中的所有数据后,最终读取到 0 字节,并返回表示管道结束的标志。这是因为在管道中,读者读取到所有数据后,若写端关闭,将会收到一个 EOF(文件结束)信号,从而读取返回 0 字节。

思考2:在第一个例程中,子进程里使用了close关闭了写端,为什么read依然会被阻塞?

        在子进程中关闭了管道的写端,这意味着子进程无法再向管道写入数据。然而,关闭写端并不会立即导致 read 函数阻塞返回,因为管道的读端仍然打开着。

        在管道中,读端的阻塞行为受到写端是否关闭和管道中是否有数据的影响。当管道中有数据可读时,read 函数会正常读取数据并返回。但当管道中没有数据时,read 函数会阻塞等待,直到有数据可读或者写端关闭。

        在代码中,子进程在每次循环开始时关闭了管道的写端。但由于父进程在循环中不断向管道写入数据,所以管道中始终有数据可读。因此,子进程的 read 函数不会被阻塞,它会一直读取到父进程写入的数据。

        如果想在子进程中的 read 函数阻塞等待,可以考虑在父进程中关闭管道的写端,这样子进程在读取完父进程写入的数据后,当再次尝试读取时会发现写端已经关闭,此时 read 函数会返回0,表示管道结束。

2.2.3 通过fcntl改变管道的阻塞状态

 要改变文件的阻塞状态,可以使用 F_SETFL 命令并结合使用 O_NONBLOCK 标志。示例如下:

int flags = fcntl(fd, F_GETFL); // 获取文件的状态标志
flags |= O_NONBLOCK; // 设置非阻塞标志
fcntl(fd, F_SETFL, flags); // 设置文件的状态标志

完整代码如下:

#define BUFFERMAX 1024
int main()
{
    int pipefd[2];  
    int ret = pipe( pipefd );

    if(ret == -1)
    {
        perror("pipe");
    }

    pid_t pid_fd = fork();
    int status = 0;

    if(pid_fd == 0) //子  == 进程2
    {
        char getmsg[BUFFERMAX];
        int flags = fcntl(pipefd[0], F_GETFL); // 获取文件的状态标志
        flags |= O_NONBLOCK; // 设置非阻塞标志
        fcntl(pipefd[0], F_SETFL, flags); // 设置文件的状态标志

        while (1)
        {
            close(pipefd[1]);
            bzero(getmsg,BUFFERMAX);
            read(pipefd[0],getmsg,BUFFERMAX);
            printf("读到的字符串是%s\n",getmsg);

            if( strstr(getmsg,"quit") != NULL)
            {
                printf("找到了子串,子进程退出\n");
                exit(0);
            }
        }
    }
    else if(pid_fd > 0) //父 == 进程1
    {
        char inputmsg[BUFFERMAX];
        while (1)
        {
            close(pipefd[0]);
            bzero(inputmsg,BUFFERMAX);
            printf("请输入要发送的字符串\n");
            scanf("%s",inputmsg);
            write(pipefd[1],inputmsg,BUFFERMAX);
            // SLEEP_MS(20000);

            if( strstr(inputmsg,"quit") != NULL)
            {
                wait(&status);
                break;
            }            
        }       
    }
}

现象是子进程不断 printf “读到的字符串是”

2.2.4 练习

编程实现下述命令的执行效果,查看系统进程列表中的指定进程信息:

gec@ubuntu:~$ ps ajx | grep 'xxx' --color
253
gec@ubuntu:~$ 

使用dup2重定向,将写入端重定向到STDOUT缓冲区,将读端重定向到STDIN缓冲区,再使用相关shell命令即可。完整代码如下:

#define BUFFERMAX 4096

int main(int argc, char *argv[])
{
    int pipefd[2];  //读0写1
    int ret = pipe(pipefd);

    if (ret == -1)
    {
        perror("pipe");
    }

    pid_t pid_fd = fork();

    int status = 0;
    if(pid_fd == 0) //子  == 进程2
    {
        dup2(pipefd[0],STDIN_FILENO);
        printf("子进程执行ing\n");
        execlp("grep", "grep", argv[1],"--color", NULL);
        perror("execlp");

    }
    else if(pid_fd > 0) //父 == 进程1
    {
        dup2(pipefd[1],STDOUT_FILENO);

        execlp("ps", "ps", "ajx",NULL);
        perror("execlp");
        SLEEP_MS(10);

        wait(&status);     //回收子进程
    }
}

效果如下:

root@ubuntu:/mnt/hgfs/share_file/系统编程/2.system、exec、管道# gcc test.c 
root@ubuntu:/mnt/hgfs/share_file/系统编程/2.system、exec、管道# ./a.out bash
子进程执行ing
  2369   2531   2531   2531 ?            -1 Ss       0   0:00 bash
  2695   3530   3530   3530 pts/7      5486 Ss       0   0:00 /bin/bash --init-file /root/.vscode-server/bin/441438abd1ac652551dbe4d408dfcec8a499b8bf/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh
  5486   5487   5486   3530 pts/7      5486 S+       0   0:00 grep bash --color
root@ubuntu:/mnt/hgfs/share_file/系统编程/2.system、exec、管道# 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值