进程间通信

概念

进程间通信(Inter-Process Communication,IPC)是指在操作系统中,两个或多个独立的进程之间进行数据交换和信息共享的机制。在多任务和多进程的操作系统中,进程通常是相互独立的,IPC 提供了一种机制,使它们能够协同工作、交换数据和实现同步。

方式

  1. 管道(Pipe):匿名管道(Anonymous Pipe)与命名管道(Named Pipe,也称为 FIFO)
  2. 消息队列(Message Queues)
  3. 共享内存(Shared Memory)
  4. 信号(Signals)
  5. 套接字(Sockets)
  6. 信号量(Semaphores)

两个管道

匿名管道(Anonymous Pipe)

  • 是一种用于实现进程间通信的 IPC 机制,通常是通过 pipe
    系统调用创建的,它允许一个进程将数据写入管道,而另一个进程则可以从管道中读取这些数据。匿名管道通常用于父子进程之间的通信,因为它们共享同一个地址空间。

本质

  • 管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。

代码流程总结

  1. 父进程创建了一个匿名管道,然后创建了一个子进程。
  2. 父进程通过管道将数据写入,而子进程从管道中读取这些数据。
  3. 父进程需要关闭管道的读取端,而子进程需要关闭管道的写入端,以确保在适当的时候关闭文件描述符。

匿名管道具体实例

#include 
#include 
#include 

int main() {
    int pipefd[2]; // 用于存放管道的文件描述符

    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

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

    if (child_pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (child_pid == 0) { // 子进程
        close(pipefd[1]); // 关闭写入端

        char buffer[100];
        read(pipefd[0], buffer, sizeof(buffer));
        printf("Child process received: %s\n", buffer);

        close(pipefd[0]); // 关闭读取端
        _exit(EXIT_SUCCESS);
    } else { // 父进程
        close(pipefd[0]); // 关闭读取端

        const char *data = "Hello,Pipe!";
        //write(pipefd[1], data, strlen(data));

        close(pipefd[1]); // 关闭写入端
        wait(NULL); // 等待子进程结束

        exit(EXIT_SUCCESS);
    }
}

匿名管道是单向的,所以如果需要双向通信,通常需要创建两个管道。匿名管道是在相关进程间创建的,因此通常用于具有亲缘关系的进程之间的通信。

说明

  • 匿名管道的读取端(管道的接收端)会在写入端(管道的发送端)没有数据可读之前阻塞。这是管道的基本行为。

为什么上面的例子直接退出了呢

  • 如果读取端尝试从空管道中读取数据,读取端会被阻塞,直到写入端写入数据为止。如果写入端被关闭,而没有写入更多数据,读取端会在读取到管道末尾时返回0,表示没有更多数据可读。也就是代码中的close(pipefd[1]); // 关闭写入端

命名管道(Named Pipe,也称为 FIFO)(先进先出)

  • 是一种在文件系统中创建的特殊文件,用于实现不同进程之间的通信。与匿名管道不同,命名管道允许在无关联的进程之间进行通信。它通过文件系统中的路径来标识,因此进程可以通过路径名找到相同的命名管道。
  • Linux中通过系统调用mknod()或mkfifo()来创建一个命名管道
  • 使用open()函数通过文件名可以打开已经创建的命名管道,而无名管道不能由open打开。当命名管道不再被任何进程打开时,它没有消失,还可以再次被打开,就像打开一个磁盘文件一样。可以用删除普通文件的方法将其删除,实际删除的事磁盘上对应的节点信息。

命名管道具体实例(用mknod宏控,分别完成了mkfifo和mknod的方式通信)

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

#define mknod

int main() {
     char *fifo_path = "/tmp/my_fifo"; // 指定命名管道的路径

#ifdef mknod
    if (mkfifo(fifo_path, 0666) == -1) {
        perror("mkfifo");
        exit(EXIT_FAILURE);
    }
#else
    mode_t mode = S_IFIFO | 0666;  // 设置文件类型为FIFO(命名管道)以及权限

    if (mknod(fifo_path, mode, 0) == -1) {
        perror("mknod");
        exit(EXIT_FAILURE);
    }
#endif 
    pid_t child_pid = fork();

    if (child_pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
#ifdef mknod
    if (child_pid == 0) { // 子进程
        int fd = open(fifo_path, O_RDONLY); // 以只读方式打开命名管道

        char buffer[100];
        read(fd, buffer, sizeof(buffer));
        printf("Child process received: %s\n", buffer);

        close(fd);
        exit(EXIT_SUCCESS);
    } else { // 父进程
        int fd = open(fifo_path, O_WRONLY); // 以只写方式打开命名管道

         char *data = "Hello, FIFO!";
        write(fd, data, strlen(data));

        close(fd);
        wait(NULL);

        unlink(fifo_path); // 删除命名管道

        exit(EXIT_SUCCESS);
    }
#else
    if (child_pid == 0) { // 子进程
        int fd = open(fifo_path, O_RDONLY);

        char buffer[100];
        read(fd, buffer, sizeof(buffer));
        printf("Child process received: %s\n", buffer);

        close(fd);
        exit(EXIT_SUCCESS);
    } else
    {  // 父进程
        int fd = open(fifo_path, O_WRONLY);

        const char *data = "Hello, FIFO!";
        size_t len = strlen(data);
        
        // 复制字符串内容到缓冲区
        char buffer[len + 1];
        strcpy(buffer, data);

        // 使用缓冲区的地址进行写入
        write(fd, buffer, len);

        close(fd);
        wait(NULL);

        exit(EXIT_SUCCESS);
    }
#endif 

    return 0;
}

管道这种进程通信方式虽然使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。

消息队列

  • 消息队列,它允许一个进程向另一个进程发送消息。消息队列在系统中以标识符存在,进程通过标识符进行消息的发送和接收。消息队列提供了一种异步通信的方式,即发送方和接收方之间不需要同步进行通信,它们可以独立地发送和接收消息。
  • 消息队列,就是一个消息链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
  • 消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
  • 可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程从消息队列中读取消息。

本质

  • 本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构

多重理解

  • 消息队列允许一个或多个进程向它写入或读取消息
  • 消息队列可以实现消息的随机查询,不一定非要以先进先出的次序读取消息,也可以按消息的类型读取
  • 某个进程往一个队列写入消息之前,并不需要另一个进程在该消息队列上等待消息的到达。
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_MESSAGE_SIZE 1024

// 定义消息结构
struct message {
    long mtype; // 消息类型
    char mtext[MAX_MESSAGE_SIZE]; // 消息内容
};

int main() 
{
    key_t key = ftok("/tmp", 'A'); // 通过路径名和项目标识符生成唯一的 key
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 创建或获取消息队列
    int msgid = msgget(key, IPC_CREAT | 0666);//IPC_CREAT 表示如果消息队列不存在则创建它,而 0666 是消息队列的权限位。
    if (msgid == -1) {
        perror("msgget");
        exit(EXIT_FAILURE);
    }

    // 发送消息
    struct message msg_to_send;
    msg_to_send.mtype = 1; // 设置消息类型
    strcpy(msg_to_send.mtext, "Hello, Message Queue!");

    if (msgsnd(msgid, &msg_to_send, sizeof(msg_to_send.mtext), 0) == -1) {
        perror("msgsnd");
        exit(EXIT_FAILURE);
    }

    // 接收消息
    struct message msg_to_receive;
    if (msgrcv(msgid, &msg_to_receive, sizeof(msg_to_receive.mtext), 1, 0) == -1) {
        perror("msgrcv");
        exit(EXIT_FAILURE);
    }
    // 打印接收到的消息
    printf("Received message: %s\n", msg_to_receive.mtext);

    // 删除消息队列
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        exit(EXIT_FAILURE);
    }

    return 0;
}

注意

  • 由于用户进程写入数据到内存中的消息队列时,会发生从用户态拷贝数据到内核态的过程;同样的,户进程读取内存中的消息数据时,会发生从内核态拷贝数据到用户态的过程。因此,如果数据量较大,消息队列就会造成频繁系统调用,需要消耗更多的时间以便内核参与。

疑问解答

问题1

如果消息队列已经存在,并且已经读了一遍,再去读还会有已经读到过的信息吗?

不会,消息队列的内容在读过一次后会被取出,不会继续保存在消息队列中

说明

  • 如果一个进程已经读取了队列中的消息,并且没有通过 msgctl 函数将消息队列删除,那么即使该进程再次调用 msgrcv
    读取消息,如果队列中没有新的消息,进程还会被被阻塞一直等待新消息的到来。

问题2

我们只能自己创建链表,将数据从kernel中读出来保存在自己创建的链表里吗?链表不是在kernel中吗?

  • 在使用消息队列时,消息队列本身并没有提供直接的链表结构。消息队列的目的是允许进程之间通过一个系统内的中介来传递消息,而不是存储和管理消息的历史记录。
  • 当使用msgrcv 函数从消息队列中接收消息时,消息被传递给应用程序,我创建了一个简单的链表数据结构,将接收到的消息依次链接起来,链表是在用户空间中的应用程序内创建和维护的。

问题3

一会儿消息队列,一会儿链表,他们有什么区别?

  • 消息队列和链表是两个不同的概念。消息队列是一种进程间通信的机制,而链表是一种数据结构,用于在应用程序内部组织和存储数据。
  • 如果你的目标是保留消息历史记录,并且你需要一种结构来存储这些历史消息,你可以选择使用链表或其他数据结构。在这种情况下,你需要在应用程序中自行创建链表来存储消息历史记录。消息队列只负责消息的传递,而不会保留历史记录。

共享内存(Shared Memory)

内存共享是另一种进程间通信的方式,它允许多个进程共享同一块内存区域,从而实现数据的共享。与消息队列不同,内存共享更加灵活,允许进程直接读写共享的内存,而不需要通过中介来传递消息。

创建共享内存并写入信息

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

#define SHM_SIZE 1024

int main() {
    int times = 0;
    key_t key = ftok("/tmmmm", 'A');//ftok 函数是用于生成一个键值的函数,通常用于为 IPC(Inter-Process Communication)机制创建唯一的标识符
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 创建或获取共享内存
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 将共享内存连接到当前进程的地址空间
    char *shared_memory = shmat(shmid, NULL, 0);
    if (shared_memory == (char*)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 在共享内存中写入数据
    strcpy(shared_memory, "Hello, Shared Memory!");

     while(1)
     {
        printf("Message in shared memory: %s\n",shared_memory);
         sleep(5);
     }
    //分离共享内存
    if (shmdt(shared_memory) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    return 0;
}

连接到已存在的共享内存并进行读写操作

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

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmmmm", 'A');
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 获取已存在的共享内存标识符
    int shmid = shmget(key, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 连接到共享内存
    char *shared_memory = shmat(shmid, NULL, 0);
    if (shared_memory == (char*)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 在共享内存中进行读写操作
    strcpy(shared_memory, "Hello, Shared Memory8888!");

    // 打印共享内存中的内容
    while(1)
    {
        printf("Message in shared memory: %s\n",shared_memory);
        sleep(5);
    }

    // 分离共享内存
    if (shmdt(shared_memory) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    return 0;
}

验证结果

process_communication$ ./shared_memory_touch
Message in shared memory: Hello, Shared Memory!
Message in shared memory: Hello, Shared Memory!
Message in shared memory: Hello, Shared Memory8888!

成功修改内容为Hello, Shared Memory8888!

信号(Signals)

使用信号是一种在进程之间进行简单通信的方法。信号是一种异步通知机制,允许一个进程向另一个进程发送信号,而另一个进程可以安装信号处理函数来响应这些信号。

非父子进程间发送信号

进程1,等待进程2的信号

#include 
#include 
#include 
#include 

// 信号处理函数
void signal_handler(int signum) {
    if (signum == SIGUSR1) {
        printf("Received SIGUSR1 signal.\n");
    }
}

int main() {
    // 注册信号处理函数
    if (signal(SIGUSR1, signal_handler) == SIG_ERR) {
        perror("signal");
        exit(EXIT_FAILURE);
    }

    	printf("PID of Process 1: %d\n", getpid());
        printf("Waiting for signal...\n");
        // 等待信号的到来
        pause();
        printf("Signal received. Exiting.\n");
    return 0;
}

进程2发送中断信号给进程1

#include 
#include 
#include 
#include 
int main() {
    // 获取进程1的PID,这里假设进程1已经在运行
    pid_t pid =3025; /* 进程1的PID */;

    // 向进程1发送SIGUSR1信号
    if (kill(pid, SIGUSR1) == -1) {
        perror("kill");
        exit(EXIT_FAILURE);
    }
    return 0;
}

验证结果

process_communication$ ./signals
PID of Process 1: 3025
Waiting for signal...
Received SIGUSR1 signal.
Signal received. Exiting.

套接字(Sockets)

通过套接字,进程可以在网络上进行通信,也可以用于本地进程间通信。套接字提供了一种通用的通信机制,适用于不同主机间的进程通信。

前面讲的基于套接字的通信实在太多了,在这里就不举例了

信号量(Semaphores)

信号量是一种同步工具,用于协调多个进程对共享资源的访问。

两个进程,互斥:

进程1

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

#define SEM_NAME "/my_semaphore"

int main() {
    sem_t *sem;

    // 创建或打开信号量
    sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        exit(EXIT_FAILURE);
    }

    // 生产者等待信号量
    printf("Producer waiting...\n");
    sem_wait(sem);

    // 执行生产者的工作
    printf("Producing...\n");
    sleep(10);

    // 释放信号量
    sem_post(sem);
    printf("释放信号量\n");

    // 关闭并删除信号量
    sem_close(sem);
    sem_unlink(SEM_NAME);

    return 0;
}

进程2

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

#define SEM_NAME "/my_semaphore"

int main() {
    sem_t *sem;

    // 打开已存在的信号量
    sem = sem_open(SEM_NAME, 0);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        exit(EXIT_FAILURE);
    }

    // 消费者等待信号量
    printf("Consumer waiting...\n");
    sem_wait(sem);

    // 执行消费者的工作
    printf("Consuming...\n");
    sleep(2);

    // 释放信号量
    sem_post(sem);
    printf("释放信号量\n");

    // 关闭信号量
    sem_close(sem);

    return 0;
}

编译与验证结果

分别编译:

gcc semaphores_consumer.c -o semaphores_consumer -lpthread
gcc semaphores_producer.c -o semaphores_producer -lpthread

注意:一定要在参数后加-lpthread,否则会报错

结果

进程1:

process_communication$ ./semaphores_producer
Producer waiting...
Producing...
释放信号量

进程2

process_communication$ ./semaphores_consumer
Consumer waiting...
Consuming...
释放信号量

验证成功

疑问

必须进程1释放了信号量,进程2才能使用吗?

  • 是的,信号量是一种用于控制对临界区(共享资源)的访问的同步机制。信号量的值可以被多个进程共享,它的值代表了可以同时进入临界区的进程数。当一个进程进入临界区时,信号量的值减少;当一个进程离开临界区时,信号量的值增加。
  • 在上述的例子中,进程1和进程2都通过 sem_wait 函数等待信号量。当信号量的值大于零时,进程可以进入临界区。当信号量的值为零时,进程将被阻塞等待,直到信号量的值变为大于零。
  • 进程1在进入临界区之前调用了 sem_wait,并在离开临界区之后调用了 sem_post,这样确保了在有其他进程在临界区时,不会有多个进程1同时进入。同样,进程2也遵循相同的原则。

总结

我只是简单完成了demo,希望能够简单明了的说明原理,希望在技术的深入上能够有所帮助

你可能感兴趣的:(linux,进程间通信)