进程间通信

目录

一、简介

二、进程间通信的机制

三、管道和 FIFO

 无名管道

代码编写

验证

有名管道

代码编写

验证

四、消息队列

代码编写

 验证

五、共享内存

代码编写

验证 


一、简介

        进程间通信(interprocess communication,简称 IPC) 指两个进程之间的通信。 系统中的每一个进程都有各自的地址空间,并且相互独立、隔离, 每个进程都处于自己的地址空间中。 所以同一个进程的不同模块(譬如不同的函数)之间进行通信都是相对简单的,譬如使用全局变量等

        但是,两个不同的进程之间要进行通信通常是比较难的,因为这两个进程处于不同的地址空间中;通常情况下,大部分的程序是不要考虑进程间通信的,对于一些复杂、大型的应用程序,则会根据实际需要将其设计成多进程程序

二、进程间通信的机制

IPC发展

 总结如下:
⚫ UNIX IPC:管道、 FIFO、信号;
⚫ System V IPC:信号量、消息队列、共享内存;
⚫ POSIX IPC:信号量、消息队列、共享内存;
⚫ Socket IPC:基于 Socket 进程间通信

现在linux使用的进程间通信方式包括

        ①管道(pipe)和有名管道(FIFO)

        ②信号(signal)

        ③消息队列

        ④共享内存

        ⑤信号量

        ⑥套接字(socket)

三、管道和 FIFO

        管道是 UNIX 系统上最古老的 IPC 方法,把一个进程连接到另一个进程的数据流称为管道,管道被抽象成一个文件,管道包括三种

⚫ 普通管道 pipe:通常有两种限制,一是单工,数据只能单向传输;

                                二是只能在父子或者兄弟进程间使用;
⚫ 流管道 s_pipe:去除了普通管道的第一种限制,为半双工,可以双向传输;

                                 只能在父子或兄弟进程间使用;
⚫ 有名管道 name_pipe(FIFO):去除了普通管道的第二种限制,并且允许在不相关(不是父子或兄弟关系)的进程间进行通讯。

        总结:普通管道可用于具有亲缘关系的进程间通信,并且数据只能单向传输,如果要实现双向传输,则必须要使用两个管道;而流管道去除了普通管道的第一种限制,可以半双工的方式实现双向传输,但也只能在具有亲缘关系的进程间通信;而有名管道(FIFO)则同时突破了普通管道的两种限制,即可实现双向传输、又能在非亲缘关系的进程间通信

 无名管道

int pipe(int pipefd[2]);

         pipe() 函数用于创建一个无名管道。pipe()函数会创建一个大小为2的文件描述符数组pipefd,其中pipefd[0]表示读端,pipefd[1]表示写端。通过管道的写端写入的数据可以从管道的读端读取到。当调用pipe()函数成功时,它会返回0,否则返回-1

代码编写

        使用 pipe 函数创建了一个无名管道,并创建了一个子进程。在父进程中,通过 write 函数将消息写入管道;在子进程中,通过 read 函数从管道中读取数据。父子进程之间通过无名管道实现简单的通信。

      创建子进程之后, 保证为父进程先运行,延迟了0.5秒,为了让父进程先执行完写入操作,然后再让子进程执行读取操作,保证了父进程先运行的顺序。在父进程中,先关闭读端,然后写入字符串,打印写入的字符串;在子进程中延时结束之后关闭写端,把buf中的数据读出来并打印读到的数据

验证

 可以看到,子进程读到了父进程写在buf里面的数据

有名管道

        有名管道是一种基于文件系统的管道,这意味着它与其他文件一样可以通过文件路径进行访问。创建一个有名管道需要使用 mkfifo 函数,在创建之后,就可以像读写普通的文本文件一样来对有名管道进行读写操作。由于有名管道是在文件系统中被创建的,因此即使进程退出,它仍然可以存储在文件系统中,直到被显式地删除。

代码编写

下面创建一个读文件用来接收数据,一个写文件用来发送数据

read文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/types.h>
#define FIFO_FILE "./myfifo"
#define BUF_SIZE 256

int main(int argc, char **argv)
{
    int pipe_fd, len;
    char buf[BUF_SIZE];

    /*创建有名管道*/
    if (access(FIFO_FILE, F_OK) == -1)
    {
        if (mkfifo(FIFO_FILE, 0666) == -1)
        {
            perror("mkfifo error");
            exit(EXIT_FAILURE);
        }
    }
    /*打开管道*/
    if ((pipe_fd = open(FIFO_FILE, O_RDONLY | O_NONBLOCK, 0)) == -1)
    {
        perror("open FIFO_FILE error");
        exit(EXIT_FAILURE);
    }
    while (1)
    {
        memset(buf, 0, sizeof(buf));
        if ((len = read(pipe_fd, buf, BUF_SIZE)) == -1)
        {
            perror("open FIFO_FILE error");
            exit(EXIT_FAILURE);
        }
        printf("read %s from FIFO\n", buf);
        sleep(1);
    }
    pause();
    unlink(FIFO_FILE); // 删除管道
}

        用mkfifo函数创建一个名为./myfifo管道文件,权限为0666,用非阻塞的方式打开,然后死循环一直读有没有数据,每次都会清空buf

write文件

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

#define FIFO_FILE "./myfifo"
#define BUF_SIZE 256
int main(int argc, char *argv[])
{
    int pipe_fd, len;
    char buf[BUF_SIZE];

    if (argc != 2)
    {
        fprintf(stderr, "uasge:%s <data>", argv[0]);
        exit(EXIT_FAILURE);
    }
    len = strlen(argv[1]);
    /*打开管道*/
    if ((pipe_fd = open(FIFO_FILE, O_WRONLY | O_NONBLOCK, 0)) == -1)
    {
        perror("open FIFO_FILE error");
        exit(EXIT_FAILURE);
    }

    /*向管道写数据*/
    if ((len = write(pipe_fd, argv[1], BUF_SIZE)) == -1)
    {
        perror("write FIFO_FILE error");
        exit(EXIT_FAILURE);
    }
    else
        printf("write %s to the FIFI\n", buf);

    close(pipe_fd);
    return 0;
}

在这需要打开read文件中的管道,从命令行中获取的数据写道buf里面,read文件就会读取到

验证

可以看到read文件一直在循环进行,当write发来数据123就会马上打印,之后继续读buf 

四、消息队列

        消息队列是消息的链表, 存放在内核中并由消息队列标识符标识, 消息队列克服了信号传递信息少、 管道只能承载无格式字节流以及缓冲区大小受限等缺陷。

         特点:消息队列中的消息是有类型的、有格式的。消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序读取,编程时可以按消息的类型读取。消息队列允许一个或多个进程向它写入或者读取消息。与无名管道、命名管道一样,从消息队列中读出消息,消息队列中对应的数据都会被删除。每个消息队列都有消息队列标识符,消息队列的标识符在整个系统中是唯一的。
只有内核重启或人工删除消息队列时,该消息队列才会被删除。若不人工删除消息队列,消息队列会一直存在于系统中

代码编写

发送代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>

#define TEXT_SIZE 256
#define NAME_SIZE 256

#define MSG_TYPE 30
#define MSG_KEY 2023

/*定义一个消息类型结构*/
typedef struct msgget
{
    long type;            /*消息类型*/
    char text[TEXT_SIZE]; /*消息正文*/
    char name[NAME_SIZE]; /*发送者的姓名*/
} MSG;

int main(int argc, char *argv[])
{

    int msg_id;
    MSG msg = {0};
    /*获取KEY值*/
    key_t key = ftok("./", 2023);
    if (key == -1)
    {
        perror("ftok");
        return -1;
    }
    printf("key=%#x\n", key);
    /*创建一个消息队列*/
    if ((msg_id = msgget(key, IPC_CREAT |IPC_EXCL| 0666)) == -1)
    {
        perror("msgget");
        return -1;
    }
    printf("msg_id=%d\n", msg_id);

    memset(&msg, 0, sizeof(msg));
    msg.type = MSG_TYPE;
    strcpy(msg.name, "msgsnd");
    strcpy(msg.text, "hello msgget");
    if (msgsnd(msg_id, &msg, sizeof(MSG) - sizeof(long), 0) == -1)
    {
        perror("msgsnd");
        return -1;
    }
  
    return 0;
}

         使用 typedef 来命名结构体为MSG,实例一个结构体为msg,ftok() 函数是一个用于生成系统IPC键值的函数,用ftok获取系统唯一的key值,接着使用msgget创建一个新的消息队列,IPC_EXCL:检测消息队列是否存在,并给予0666权限,然后把msg结构体清0,使用字符串复制函数来复制字符串内容到该成员的字符数组中,最后用msgsnd函数发送,sizeof(MSG) - sizeof(long)用来计算发送的消息长度,0调用阻塞直到条件满足为止,也就是如果消息队列已满,那么 msgsnd() 将会暂停程序执行,直到有足够的空间存放当前要发送的消息。只有当消息成功发送到消息队列之后,才会继续往下执行程序。

接收代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>

#define TEXT_SIZE 256
#define NAME_SIZE 256

#define MSG_TYPE 30
#define MSG_KEY 2023

typedef struct msgget
{
    long type;            /*消息类型*/
    char text[TEXT_SIZE]; /*消息正文*/
    char name[NAME_SIZE]; /*发送者的姓名*/
} MSG;

int main(int argc, char *argv[])
{
    int msg_id;
    MSG msg = {0};
    key_t key = ftok("./", MSG_KEY);
    if (key == -1)
    {
        perror("ftok");
        return -1;
    }
    printf("key=%#x\n", key);
    if ((msg_id = msgget(key, IPC_CREAT | 0666)) == -1)
        {
            perror("msgget");
            return -1;
        }

    memset(&msg, 0, sizeof(msg));
    if (msgrcv(msg_id, &msg, sizeof(MSG) - sizeof(long), MSG_TYPE, 0) == -1)
    {
        perror("msgrcv");
        return -1;
    }
    printf("发送者:%s\n", msg.name);
    printf("消息:%s\n", msg.text);
     /* 程序退出前删除消息队列 */
    if (msgctl(msg_id, IPC_RMID, NULL) == -1)
    {
        perror("msgctl");
        return -1;
    }
    return 0;
}

        和发送代码差不多, 发送函数msgrcv多了一个参数,就是需要msg_type的消息,和发送代码定义的一样,不然不能接收到发送代码的信息,最后接收到信息就打印出来,并利用msgctl
函数删除消息队列,IPC_RMID:删除由id指示的消息队列,将它从系统中删除并破坏相关数据结构,这里没有修改消息队列的属性,使用NULL表示不使用任何特定选项。

 验证

在发送文件运行之后可以看到key和id,在接收文件运行就会打印出接收到的数据

五、共享内存

        共享内存就是映射一段能被其它进程所访问的内存, 这段共享内存由一个进程创建, 但其它的多个进程都可以访问, 使得多个进程可以访问同一块内存空间。共享内存是最快的 IPC 方式, 它是针对其它进程间通信方式运行效率低而专门设计的, 它往往与其它通信机制, 譬如结合信号量来使用, 以实现进程间的同步和通信。和消息队列一样需要唯一共享存储标识符

         使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。 若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据

代码编写

write文件,往共享内存里面写,相当于发数据

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define SHM_SIZE 32

int main(int argc, char *argv[])
{
    key_t key;
    int shm_id;
    char *buf;
    // 获取唯一的key
    if ((key = ftok("./", 2023)) == -1)
    {
        perror("ftok() failed");
        return -1;
    }
    // 创建共享内存(分配物理内存)
    if ((shm_id = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0600)) == -1)
    {
        perror("shmget() failed");
        return -1;
    }
    // 将虚拟内存 和 物理内存 建立映射
    if ((buf = (char *)shmat(shm_id, NULL, 0)) == NULL)
    {
        perror("shmat() failed");
        return -1;
    }
    // 操作虚拟内存
    strcpy(buf, "hello shm");
    printf("write ' %s ' to buf succeed\n", buf);
    // 释放映射
    if (shmdt(buf) == -1)
    {
        perror("shmdt() failed");
        return 1;
    }
    return 0;
}

         基本思路和消息队列的一样,先获取唯一的key,然后用shmget函数创建共享内存,共享存储段的长度(字节)为shm_size,权限为0600,这样只有创建共享内存区域的进程才能访问和修改该共享内存区域,其他进程和用户则无法访问。这样能够更好地保护数据的安全性和隐私性。接着用shmat函数映射虚拟内存,将一个共享内存段映射到调用进程的数据段中,NULL则表示共享内存映射地址由系统自动指定,0:共享内存具有可读可写权限,然后就把数据拷贝进buf中,并打印看一下是否写入,最后用shmdt函数解除共享映射区,将共享内存和当前进程分离(仅仅是断开联系并不删除共享内存)

read文件,从共享内存区读取数据,相当于接收数据

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define SHM_SIZE 32

int main(int argc, char **argv)
{
    key_t key;
    int shm_id;
    char *buf;
    if ((key = ftok("./", 2023)) == -1)
    {
        perror("ftok() failed");
        return -1;
    }
    if ((shm_id = shmget(key, SHM_SIZE, IPC_CREAT | 0600)) == -1)
    {
        perror("shmget() failed");
        return -1;
    }
    if ((buf = (char *)shmat(shm_id, NULL, 0)) == NULL)
    {
        perror("shmat() failed");
        return -1;
    }
    printf("%s\n", buf);
    if (shmdt(buf) == -1)
    {
        perror("shmdt() failed");
        return 1;
    }
    if((shmctl(shm_id,IPC_RMID,NULL)) == -1)
    {
         perror("shmctl() failed");
        return -1;
    }

    return 0;
}

         代码和上面的差不多,就把共享内存中的数据打印出来,最后用shmctl函数把共享内存删除

验证 

 可以看到执行write代码之后,再运行read代码就把共享内存的数据打印出来了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值