linux应用 进程间通信之管道

1、定义

       管道是一种在Unix和类Unix系统中用于进程间通信的机制。管道可以分为匿名管道和命名管道两种类型。

1.1 匿名管道(Anonymous Pipe)

        匿名管道是一种单向通信机制,只能在具有共同祖先的进程之间使用。它通过pipe系统调用创建,其中一个进程作为读端,另一个进程作为写端。

优点:

  • 简单易用,不需要额外的系统调用来创建和使用。
  • 适用于需要在具有共同祖先的两个进程之间进行单向通信的场景,比如父子进程之间的通信。

缺点:

  • 单向通信,无法实现双向通信。
  • 有限的缓冲区大小,可能会导致阻塞。
  • 仅适用于具有共同祖先的进程之间的通信。
1.2 命名管道(FIFO)

        命名管道是一种特殊类型的文件,允许无关的进程之间进行通信。它通过mkfifo系统调用创建,可以在文件系统中看到。

优点:

  • 允许无关的进程进行通信,不需要具有共同祖先。
  • 可以实现双向通信。
  • 适用于需要在无关的进程之间进行通信的场景。

缺点:

  • 需要额外的系统调用来创建和使用。
  • 通信双方需要事先知道管道的路径。
  • 无法传递复杂的数据结构,只能传递字节流。
1.3 小结

        匿名管道适用于具有共同祖先的两个进程之间的单向通信,比如父子进程之间的通信。命名管道适用于无关的进程之间的通信,可以实现双向通信,适用于需要在不同进程之间进行数据交换的场景,比如进程间的协作或数据传输。
        管道通信是基于字节流的,不支持传递复杂的数据结构,因此在需要传递结构化数据或大量数据的情况下,可能需要考虑其他进程间通信机制,比如消息队列、共享内存或套接字等

2. 编程测试

2.1 匿名管道编程

匿名管道只能进行单向通信,因此创建两个管道实现父子间进程的双向通信:

linux应用 进程间通信之管道_第1张图片

创建完描述符后关闭自己不需要的功能,让管道1负责父进程写子进程读,管道2负责子进程写父进程读,实现效果如下:

linux应用 进程间通信之管道_第2张图片

编写如下测试代码:

#include 
#include 
#include 
#include 
#include  
#include  

// 打印时分秒的宏        
#define PRINT_MIN_SEC() do { \
            time_t t = time(NULL); \
            struct tm *tm_ptr = localtime(&t); \
            printf("%02d:%02d:%02d:", tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec); \
        } while (0)

int main() 
{
    int pipefd1[2];          // 管道1 父进程写,子进程读 [0]为读端 [1]为写端
    int pipefd2[2];          // 管道2 子进程写,父进程读
    char buffer1[100] = {0}; // 父进程接收缓存
    char buffer2[100] = {0}; // 子进程接收缓存
    pid_t pid;

    if (pipe(pipefd1) == -1 || pipe(pipefd2) == -1) 
    {
        perror("pipe");
        return 0;
    }

    pid = fork();

    if (pid == -1) 
    {
        perror("fork");
        return 0;
    }

    if (pid > 0)    // 父进程
    { 
        close(pipefd1[0]); // 关闭管道1父进程读取端
        close(pipefd2[1]); // 关闭管道2父进程写入端
        // 向管道1写入数据
        write(pipefd1[1], "I am parent send msg", sizeof("I am parent send msg"));

        while(1)
        {
            // 从管道2读取数据
            bzero(buffer1, sizeof(buffer1));
            read(pipefd2[0], buffer1, sizeof(buffer1));
            PRINT_MIN_SEC();
            printf("Read from child: %s\n", buffer1);

            sleep(3);
            write(pipefd1[1], "I am parent send msg", sizeof("I am parent send msg"));
        }


    } 
    else            // 子进程
    { 
        close(pipefd1[1]); // 关闭管道1子进程写入端
        close(pipefd2[0]); // 关闭管道1子进程读取端

        // 向管道2写入数据
        write(pipefd2[1], "I am child send msg", sizeof("I am child send msg"));

        while(1)
        {
            bzero(buffer2, sizeof(buffer2));
            read(pipefd1[0], buffer2, sizeof(buffer2));
            PRINT_MIN_SEC();
            printf("Read from parent: %s\n", buffer2);

            sleep(3);
            write(pipefd2[1], "I am child send msg", sizeof("I am child send msg"));
        }
    }

    return 0;
}

每隔3秒,父进程和子进程都会读取对方发送的消息,并重新发送给对方一次消息,测试结果如下:

linux应用 进程间通信之管道_第3张图片

对于创建的读写描述符有以下属性:

  • O_CLOEXEC:在文件描述符上设置 close-on-exec(FD_CLOEXEC)标志。当一个进程调用 exec 函数时,如果该标志被设置,那么该进程将关闭所有设置了 FD_CLOEXEC 标志的文件描述符。这对于在执行新程序时自动关闭文件描述符非常有用,可以避免在新程序中不必要地继承文件描述符。
  • O_DIRECT:创建一个以“数据包”模式进行 I/O 的管道。对于管道的每次 write 操作都被视为一个单独的数据包,而对管道的 read 操作将一次读取一个数据包。这对于特定类型的数据传输非常有用,例如需要进行原子操作的数据传输。
  • O_NONBLOCK:在文件描述符上设置 O_NONBLOCK 文件状态标志。这将使得对该文件描述符的读写操作变成非阻塞的。当对非阻塞文件描述符进行读写操作时,如果没有数据可用或者无法立即完成写操作,操作将立即返回而不是阻塞等待。

可以通过fcntl来设置相关属性值,例如设置管道1读取非阻塞:

fcntl(pipefd1[0], F_SETFL, O_NONBLOCK)
2.2 有名管道编程

使用两个FIFO可以实现无血缘关系的两个进程之间的通信,编写测试用例,进程1代码如下,实现向fifo1写入数据,并且堵塞读取fifo2中的数据:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PROCESS_1_SEND "/home/fifo1"
#define PROCESS_2_SEND "/home/fifo2"

// 打印时分秒的宏        
#define PRINT_MIN_SEC() do { \
            time_t t = time(NULL); \
            struct tm *tm_ptr = localtime(&t); \
            printf("%02d:%02d:%02d:", tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec); \
        } while (0)

// 进程1
int main() 
{
    int  fd1, fd2;
    int  ret = 0; 
    char buffer[100];

    // 创建FIFO
    if (-1 == access(PROCESS_1_SEND, F_OK))
    {
        if (0 != mkfifo(PROCESS_1_SEND, 0666))
        {
            perror("mkfifo PROCESS_1_SEND err");
            return 0;
        }
    }

    if (-1 == access(PROCESS_2_SEND, F_OK))
    {
        if (0 != mkfifo(PROCESS_2_SEND, 0666))
        {
            perror("mkfifo PROCESS_2_SEND err");
            return 0;
        }
    }   
    fd1 = open(PROCESS_1_SEND, O_RDWR);
    fd2 = open(PROCESS_2_SEND, O_RDWR);
    
    write(fd1, "Process 1 Start", sizeof("Process 1 Start"));
    
    while(1)
    {
        read(fd2, buffer, sizeof(buffer));
        PRINT_MIN_SEC();
        printf("Read from process 2: %s\n", buffer);
        sleep(5);
        write(fd1, "Process 1 Msg", sizeof("Process 1 Msg"));
    }

    close(fd1);
    close(fd2);

    return 0;
}

进程2代码如下,实现向fifo2写入数据,并且堵塞读取fifo1中的数据:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define PROCESS_1_SEND "/home/fifo1"
#define PROCESS_2_SEND "/home/fifo2"

// 打印时分秒的宏        
#define PRINT_MIN_SEC() do { \
            time_t t = time(NULL); \
            struct tm *tm_ptr = localtime(&t); \
            printf("%02d:%02d:%02d:", tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec); \
        } while (0)

// 进程2
int main() 
{
    int fd1, fd2;
    int ret = 0;
    char buffer[100];
    
    fd1 = open(PROCESS_1_SEND, O_RDWR);
    fd2 = open(PROCESS_2_SEND, O_RDWR);

    write(fd2, "Process 2 Start", sizeof("Process 2 Start"));

    while(1)
    {
        read(fd1, buffer, sizeof(buffer));
        PRINT_MIN_SEC();
        printf("Read from process 1: %s\n", buffer);
        sleep(5);
        write(fd2, "Process 2 Msg", sizeof("Process 2 Msg"));
    }

    close(fd1);
    close(fd2);

    return 0;
}

每隔5秒,进程1和进程2都会读取对方发送的消息,并重新发送给对方一次消息,测试结果如下:

linux应用 进程间通信之管道_第4张图片

2.3 注意问题

管道默认是有最大限值的,如果一直写入没有读取的话会造成堵塞,Linux下通过以下命令可以查看:

sysctl fs.pipe-max-size
或者
cat /proc/sys/fs/pipe-max-size

3、总结

本文阐述了管道通信的特点和优缺点,通过测试用例示例了管道在编程中的具体使用。

你可能感兴趣的:(linux应用,linux,c语言)