进程通信

笔记 · 7 天前 · 16 人浏览
进程通信

  进程通信是指在不同进程之间传输数据或交换信息的过程。由于进程各自拥有独立的内存地址空间,它们之间的数据不能直接通过内存地址来访问,因此需要通过特定的通信机制来实现信息交换。

进程通信的必要性

资源共享:多个进程可能需要访问共享资源,如共享内存、文件等,进程通信是实现资源共享的关键。
协同工作:多个进程可能需要协同完成某项任务,进程间的通信是协调它们工作的重要手段。
状态同步:一个进程的状态变化可能需要通知其他进程,以便它们作出相应的调整。

Linux 中进程间通信方式

只举例管道、消息队列等代码。同时不推荐进行双向操作(双工),因为比较古老,容易出现问题,比如出现混乱,一方发送的消息自己也接收。如果要实现全双工,可以使用两条消息队列(管道)分别负责两个方向的通信。

1. 管道(Pipes)

  匿名管道(Anonymous Pipes):仅存在于内存中的管道,用于具有亲缘关系的进程间通信(如父子进程)。它是一种半双工通信方式,数据只能单向流动。通过pipe()系统调用创建。

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

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

    // 将程序传递进来的第一个命令行参数 通过管道传输给子进程
    if (argc != 2)
    {
        fprintf(stderr, "%s请填写需要传递的信息\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 创建管道
    if(pipe(pipefd) == -1)
    {
        perror("创建管道失败!\n");
        exit(EXIT_FAILURE);
    }

    // 复制父子进程
    pid_t cpid = fork();
    if (cpid == -1)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (cpid == 0)
    {
        // 子进程:读取管道的数据,打印到控制台
        close(pipefd[1]);
        char str[100] = {0}; 
        sprintf(str, "子进程(%d)接收信息\n", getpid());
        write(STDOUT_FILENO, str, sizeof(str));
        char buf;
        while(read(pipefd[0], &buf, 1) > 0)
        {
            write(STDOUT_FILENO, &buf, 1);
        }
        write(STDOUT_FILENO, "\n", 1);
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
    } else
    {
        // 父进程:写入管道数据,提供给子进程读
        // 单向通信,一个写,一个读  =>  先关闭读,再写
        close(pipefd[0]);
        // 将数据写入到管道中
        printf("父进程(%d)向子进程传递信息\n", getpid());
        write(pipefd[1], argv[1], strlen(argv[1]));
        close(pipefd[1]);
        waitpid(cpid, NULL, 0);
        exit(EXIT_SUCCESS);
    }

    return 0;
}

  命名管道(Named Pipes或FIFOs):也称为FIFO(First In First Out),它在文件系统中以文件的形式存在,因此允许任意进程间进行通信。通过mkfifo()或mknod()系统调用创建。命名管道具有持久性,直到被显式删除。代码如下,这里为收发两端的代码,需要打开两个控制台进行测试。

fifo_read.c
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define PIPO_PATH ("/tmp/myfifo")

int main(int argc, char const *argv[])
{
    if (mkfifo(PIPO_PATH, 0664))
    {
        perror("mkfifo");
        if (errno != EEXIST) // 文件已存在
        {
            exit(EXIT_FAILURE);
        }
    }

    // 对有名管道的特殊文件 创建fd
    int fd = open(PIPO_PATH, O_RDONLY);

    if (fd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    char buf[100];
    ssize_t read_num;

    // 读取管道信息写入到控制台
    while((read_num = read(fd, buf, 100)) > 0) 
    {
        write(STDOUT_FILENO, buf, read_num);
    }

    if(read_num < 0)
    {
        perror("read");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("接受管道信息完成 -->> 进程终止\n");
    close(fd);

    // 释放管道:清除对应的特殊文件
    if(unlink(PIPO_PATH))
    {
        perror("unlink");
        exit(EXIT_FAILURE);
    }

    return 0;
}
fifo_write.c
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define PIPO_PATH ("/tmp/myfifo")

int main(int argc, char const *argv[])
{
    if (mkfifo(PIPO_PATH, 0664))
    {
        perror("mkfifo");
        if (errno != EEXIST) // 文件已存在
        {
            exit(EXIT_FAILURE);
        }
    }

    // 对有名管道的特殊文件 创建fd
    // 创建完成之后,后续是可以重复使用的,不推荐使用,推荐每次用完释放
    int fd = open(PIPO_PATH, O_WRONLY);

    if (fd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    char buf[100];
    ssize_t read_num;

    // 读取控制台数据写入到管道中
    while((read_num = read(STDIN_FILENO, buf, 100)) > 0) 
    {
        write(fd, buf, read_num);
    }

    if(read_num < 0)
    {
        perror("read");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("发送数据到管道完成 -->> 进程终止\n");
    close(fd);

    // 释放管道:清除对应的特殊文件
    if(unlink(PIPO_PATH))
    {
        perror("unlink");
        exit(EXIT_FAILURE);
    }

    return 0;
}

2. 信号(Signals)

  信号是软件层次上对中断机制的一种模拟,用于通知进程某个事件已经发生。进程可以选择忽略信号、捕捉信号并执行特定的信号处理函数,或者按默认方式处理信号。信号主要用于异步通知,不适用于数据传输。

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

void sigint_handler(int signum)
{
    printf("\nCOPY THAT __ %d __ SIGNAL : END\n", signum);
    exit(signum);
}

int main(int argc, char const *argv[])
{
    if (signal(SIGINT, sigint_handler) == SIG_ERR)
    {
        perror("signal");
        return 1;
    }

    while (1)
    {
        sleep(1);
        printf("Hello, World!\n");
    }

    return 0;
}

3. 消息队列(Message Queues)

  消息队列是消息的链表,存放在内核中并由消息队列标识符标识。它允许一个或多个进程向它写入与读取消息。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>


int main(int argc, char const *argv[])
{
    // 创建消息队列
    struct mq_attr attr;
    // 有用的参数  表示消息队列的容量
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 100;
    //  被忽略的消息  在创建消息的时候用不到
    attr.mq_flags = 0;
    attr.mq_curmsgs = 0;

    char * mq_name = "/parent_child_mq";
    mqd_t mqdes = mq_open(mq_name, O_RDWR | O_CREAT, 0664, &attr);

    if (mqdes == (mqd_t) -1)
    {
        perror("mq_open");
        exit(EXIT_FAILURE);
    }

    // 创建父子进程
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0)
    {
        // 子进程  等待接收消息队列中的信息
        char read_buf[100];
        struct timespec time_info;
        for (size_t i = 0; i < 10; i++)
        {
            // 清空接收数据的缓冲区
            memset(read_buf, 0, 100);

            // 设置接收数据的等待时间
            clock_gettime(0, &time_info); 
            time_info.tv_sec += 15;  // 等待 15 s

            // 接受消息队列的数据  打印到控制台
            if (mq_timedreceive(mqdes, read_buf, 100, NULL, &time_info) == -1)
            {
               perror("mq_timedreceive");
            }
            printf("子进程接收到数据:%s\n", read_buf);
        }

    } else
    {
        // 父进程  发送消息到消息队列中
        char send_buf[100];
        struct timespec time_info;

        for (size_t i = 0; i < 10; i++)
        {
            // 清空处理buf
            memset(send_buf, 0, 100);
            sprintf(send_buf, "父进程的第%ld次发送消息\n", i + 1);

            // 获取当前的具体时间 clock_gettime(CLOCK_REALTIME, &time_info);
            clock_gettime(0, &time_info); 
            time_info.tv_sec += 5;  // 等待 5 s
            // 发送消息
            if (mq_timedsend(mqdes, send_buf, strlen(send_buf), 0, &time_info) == -1)
            {
                perror("mq_timedsend");
            }
            printf("父进程发送一条消息,休眠 1 s\n");
            sleep(1);
        }
    }

    // 最终不管是父进程还是子进程都需要释放消息队列的引用
    close(mqdes);
    // 清除消息队列只需要执行一次
    if (pid > 0)
    {
        mq_unlink(mq_name);
    }

    return 0;
}

4. 共享内存(Shared Memory)

  共享内存允许多个进程访问同一块内存区域。这种方式通常比较高效,因为数据不需要在进程间复制。但是,使用共享内存需要处理进程间的同步和互斥问题,以防止数据不一致。

#include <sys/mman.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

#define SHM_SIZE 1024

int main(int argc, char const *argv[])
{
    char * share;
    int statu_loc;
    // 1. 创建一个共享内存对象
    char shm_name[BUFSIZ] = {0};
    sprintf(shm_name, "/letter%d", getpid());
    int fd = shm_open(shm_name, O_RDWR | O_CREAT, 0644);
    if (fd < 0)
    {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    // 2. 设置共享内存对象大小
    ftruncate(fd, SHM_SIZE);

    // 3. 内存映射
    share = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    if (share == MAP_FAILED)
    {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    // 映射完成,关闭fd连接 不是释放
    close(fd);

    // 4. 使用内存映射实现进程间的通讯
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0)
    {
        // 子进程
        strcpy(share, "This is a test!\n");
        printf("子进程__%d__完成发送\n", getpid());
    } else
    {
        // 父进程
        waitpid(pid, &statu_loc, 0); // 状态可以为 NULL
        printf("父进程__%d__接收子进程__%d__:%s\n", getpid(), pid, share);

        // 5. 释放映射区
        int res_munmap = munmap(share, SHM_SIZE);
        if (res_munmap == -1)
        {
            perror("munmap");
            exit(EXIT_FAILURE);
        }
    }

    // 6. 释放共享内存对象
    shm_unlink(shm_name);
    return 0;
}

5. 套接字(Sockets)

  套接字是一种更为通用的进程间通信机制,它不仅可用于同一主机上的进程间通信,还可用于不同主机间的网络通信。套接字可以基于不同的协议族(如UNIX域套接字、TCP/IP套接字等)和类型(如流式套接字、数据报套接字等)来实现。

代码 MakeFile 编写

CC := gcc
FAILNAME:FALINAME.c   # FAILNAME : 文件名 
    -$(CC) -o $@ $^
    -./$@ 
    -rm ./$@
C
Theme Jasmine by Kent Liao

本网站由 又拍云 提供CDN加速/云存储服务

鄂ICP备2023005457号    鄂公网安备 42011302000815号