进程间通信(IPC)

管道

管道是一个特殊的共享文件,属于操作系统(也就不是fork()的复制对象),两个进程通过操作系统内核空间进行读写通信
特点:
1.管道的读取属于一次性动作,读取即释放,普通管道只允许半双工通信,若要实现两个进程双向通信,则需要两个管道
2.管道则需要先通过操作系统访问文件再获得内存数据。

匿名管道
1.只允许有血缘关系之间的进程通信
2.生命周期和进程关联

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#define BUF_SIZE 30
int main(int arcg, char *argv[])
{
    int fds1[2], fds2[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message";
    char buf[BUF_SIZE];
    pid_t pid;
    pipe(fds1), pipe(fds2);
    pid = fork();
    if (pid == 0)
    {
        write(fds1[1], str1, sizeof(str1));
        read(fds2[0], buf, BUF_SIZE);
        printf("child proc output:%s\n", buf);
    }
    else
    {
        read(fds1[0], buf, BUF_SIZE);
        printf("Parent proc output:%s\n", buf);
        write(fds2[1], str2, sizeof(str2));
        sleep(3);
    }
    return 0;
}

命名管道
1.可以在两个无亲缘关系的进程间通信
2.生命周期独立于进程存在,直到最后一个使用它的进程关闭它或者显示删除该文件
3.需要创建文件用于管道通信

//write.cpp
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // 打开命名管道,O_WRONLY 表示只写模式
    int fifo_fd = open("./myfifo", O_WRONLY | O_CREAT);
    if (fifo_fd == -1)
    {
        perror("Failed to open FIFO for writing");
        exit(EXIT_FAILURE);
    }

    char message[] = "Hello from the writer!\n";
    ssize_t bytes_written = write(fifo_fd, message, strlen(message));
    if (bytes_written == -1)
    {
        perror("Write to FIFO failed");
    }
    else
    {
        printf("Wrote %ld bytes to FIFO\n", (long)bytes_written);
    }

    // 关闭文件描述符
    close(fifo_fd);
    return 0;
}
//read.cpp
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // 打开命名管道,O_RDONLY 表示只读模式
    int fifo_fd = open("./myfifo", O_RDONLY | O_CREAT);
    if (fifo_fd == -1)
    {
        perror("Failed to open FIFO for reading");
        exit(EXIT_FAILURE);
    }

    char buffer[100];
    ssize_t bytes_read = read(fifo_fd, buffer, sizeof(buffer) - 1);
    if (bytes_read == -1)
    {
        perror("Read from FIFO failed");
    }
    else
    {
        buffer[bytes_read] = '\0'; // 添加字符串结束符
        printf("Read from FIFO: %s\n", buffer);
    }

    // 关闭文件描述符
    close(fifo_fd);
    return 0;
}

套接字对 socketpair()

在这里插入图片描述
特点:通过sendmsg()和recvmsg()通信,可以允许同时发送接收多个缓冲区,已经可以附带文件描述符等辅助信息

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

#define BUF_SIZE 1024
int main()
{
    int sockfd[2];                                              // 存储套接字对
    unsigned char SendBuffer[2][10] = {{"hellow"}, {"world!"}}; // 消息缓冲区

    // 创建已连接套接字对
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd) == -1)
    {
        perror("Error creating socket pair");
        return -1;
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1)
    {
        perror("Error forking");
        return -1;
    }

    // 子进程(发送方)
    if (pid == 0)
    {
        close(sockfd[0]); // 关闭读套接字

        // 打开文件并获取文件描述符
        int file_socket = open("example.txt", O_RDONLY);
        if (file_socket == -1)
        {
            perror("Error opening file");
            return -1;
        }

        // 准备消息
        // 准备I/O向量
        struct iovec iov[2];
        iov[0].iov_base = SendBuffer[0];
        iov[0].iov_len = sizeof(SendBuffer[0]);
        iov[1].iov_base = SendBuffer[1];
        iov[1].iov_len = sizeof(SendBuffer[1]);

        struct msghdr message = {0};
        message.msg_iov = iov;
        message.msg_iovlen = 2;
        // 辅助控制信息
        char control_data[CMSG_SPACE(sizeof(int))]; // 开辟存储辅助数据内存 辅助信息为一个文件描述符 4字节 = sizeof(int)
        message.msg_control = control_data;
        message.msg_controllen = sizeof(control_data);

        // 构建控制信息头部
        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message); // 获取消息头的第一个辅助数据块
        cmsg->cmsg_len = CMSG_LEN(sizeof(int));         // 一个辅助数据块的总长度
        cmsg->cmsg_level = SOL_SOCKET;                  // 源层协议 SOL_SOCKET表示这是与套接字相关的辅助数据
        cmsg->cmsg_type = SCM_RIGHTS;                   // 辅助数据的类型  SCM_RIGHTS表示辅助数据用于传递文件描述符。

        // 将文件描述符复制到辅助数据中
        *((int *)CMSG_DATA(cmsg)) = file_socket;

        //  发送消息
        if (sendmsg(sockfd[1], &message, 0) == -1)
        {
            perror("Error sending message");
            close(file_socket);
            return -1;
        }

        close(file_socket); // 关闭文件描述符
        close(sockfd[1]);   // 关闭写端

        exit(0);
    }

    // 父进程(接收方)
    close(sockfd[1]); // 关闭写端

    // 准备消息
    // 设置I/O向量
    unsigned char RecvBuffer[2][10]; // 消息缓冲区
    struct iovec iov[2];
    iov[0].iov_base = RecvBuffer[0];
    iov[0].iov_len = sizeof(RecvBuffer[0]);
    iov[1].iov_base = RecvBuffer[1];
    iov[1].iov_len = sizeof(RecvBuffer[1]);

    struct msghdr message = {0};
    message.msg_iov = iov;
    message.msg_iovlen = 2;
    char control_data[CMSG_SPACE(sizeof(int))]; // 开辟存储辅助数据内存 辅助信息为一个文件描述符 4字节 = sizeof(int)
    message.msg_control = control_data;
    message.msg_controllen = sizeof(control_data);

    // 接收消息
    // 接收消息
    if (recvmsg(sockfd[0], &message, 0) == -1)
    {
        perror("Error receiving message");
        return -1;
    }
    printf("消息:%s %s\n", RecvBuffer[0], RecvBuffer[1]);

    // 从辅助信息中获取文件描述符
    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message); // 获取消息头的第一个辅助数据块
    int received_fd;
    memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int)); // 从数据段首地址复制

    // 读取文件内容
    printf("接收的文件描述符:%d\n", received_fd);
    char buffer[BUF_SIZE];
    ssize_t bytes_read = read(received_fd, buffer, sizeof(buffer));
    if (bytes_read == -1)
    {
        perror("Error reading file");
        return -1;
    }
    // 打印文件内容
    printf("文件内容: %.*s\n", (int)bytes_read, buffer);

    close(received_fd); // 关闭收到的文件描述符
    close(sockfd[0]);   // 关闭读端

    return 0;
}

共享内存

共享内存允许多个毫不相干的进程读取和写入同一块物理内存,当某个进程往共享内存中写入数据时,其它进程就能够立马读取到共享内存中的数据,从而达到进程间通信的目的。这也是所有进程间通信方式中最快的一种。
缺点:
用于进程间通信时,共享内存本身不支持阻塞等待操作。这是因为当读端读取数据后,数据并不会在内存中清空。因此读端和写端可以同时访问内存空间,即全双工。因为共享内存本质是进程直接访问内存,无法主动停止读取,如果读端不加以限制,那么将持续读取数据。同理,写端也会持续写入数据。换句话说,共享内存本身没有访问控制
原理:
CPU执行语句的过程大致就是先找到当前进程的PCB(进程控制块)里的进程虚拟地址空间
当A进程需要与B进程通信时,只需要通过页表,将共享区虚拟地址映射到物理地址,对该物理地址直接进行写入;而B进程则是通过页表,取虚拟地址映射到物理地址,从该物理地址直接进行读取。
在这里插入图片描述

共享内存的使用

(一)创建共享内存(物理内存)
在这里插入图片描述①key
shmget会根据key值创建/访问一个共享内存,因此当创建多个共享内存时,每一个key值要独一无二。
获得key值可以使用库函数ftok专门获取一个独一无二的key_t类型值。
在这里插入图片描述
参数pathname:为路径,必须是真实存在且可以访问的路径。
参数proj_id:是int类型数字,且必须传入非零值。
返回值 :成功返回key_t值,失败返回-1。
ftok函数内部会根据路径和proj_id通过算法生成一个独一无二的key_t返回值。
多进程通信时,需要通信双方使用同一个key值,因此双方使用的ftok参数应该一致。
②size
该参数用于确定共享内存大小。
一般而言是4096的整数倍,因为内存的块的大小就是4KB即4096B。因此即便我们需要的空间大小不是块大小的整数倍,操作系统实际上也还是分配块的倍数个。但在使用时,那些超过size大小的多余分配空间不能访问。
③shmflg
该参数用于确定共享内存属性。
使用上为:标志位 | 内存权限
标志位参数有两种:IPC_CREAT、IPC_EXCL
常用的使用方法:
在这里插入图片描述
值得注意PC_EXCL无法单独使用。
通常情况下在多进程通信时,创建方使用IPC_CREAT | IPC_EXCL,接收方使用0即可。
④返回值
返回值为int类型,称为shmid。每一个共享内存都会有一个shmid,用于连接与分离时传递参数。

(二)链接
用于将物理共享内存段附加(attach)到当前进程的虚拟地址空间
在这里插入图片描述
shmid即shmget返回值。
shmaddr用于确定将共享内存挂在进程虚拟地址哪个位置,一般填nullptr即可代表让内核自己确定位置。(一般在堆区,也可以是其他区域)
shmflg用于确定挂接方式,一般填0。
连接成功返回共享内存在进程中虚拟内存的起始地址,失败返回-1。

(三)分离
shmdt() 函数用于将共享内存段从当前进程的地址空间中分离(detach)。这意味着虽然共享内存段本身仍然存在,并且可能被其他进程使用,但它对调用了 shmdt() 的进程不再直接可见或可访问。分离并不意味着删除共享内存,它仅仅是断开当前进程对该内存区域的访问路径。
在这里插入图片描述
shmaddr:之前通过 shmat() 调用返回的指向共享内存段起始地址的指针。传递这个地址给 shmdt() 以告知系统要从当前进程的地址空间中分离哪个共享内存段。
(四).销毁
在这里插入图片描述
该接口本身用于控制共享内存,可用于销毁。
shmid:shmget()的返回值,cmd传入IPC_RMID,buf传nullptr。
(五)指令查看删除共享内存
查看指令:ipcs -m
删除:ipcrm -m [shmid]
在这里插入图片描述
演示代码:

//writer.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>

#define SHM_KEY 123456
#define SHM_SIZE 100

void write_to_shared_mem(int shmid) {
    const char *data = "Hello from Writer!";
    char *shmaddr = (char*)shmat(shmid, NULL, 0);//连接共享内存和进程空间
    if (shmaddr == (void*) -1) {
        perror("shmat");
        exit(1);
    }
    strncpy(shmaddr, data, strlen(data)+1);//直接向虚拟地址写数据
    printf("Writer: Data written to shared memory.\n");
    shmdt(shmaddr);//分离共享内存连接
}

int main() {
    key_t key = ftok(".", 131);//创建唯一的key
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    // 创建共享内存(如果不存在)
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    //向共享内存写数据
    write_to_shared_mem(shmid);

    return 0;
}
//reader.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>

#define SHM_KEY 123456
#define SHM_SIZE 100

void read_from_shared_mem(int shmid) {
    char *shmaddr = (char*)shmat(shmid, NULL, 0);连接共享内存和进程空间
    if (shmaddr == (void*) -1) {
        perror("shmat");
        exit(1);
    }
    printf("Reader: Read from shared memory: %s\n", shmaddr);
    shmdt(shmaddr);
}

int main() {
    key_t key = ftok(".", 131);//创建key
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    // 连接到现有的共享内存
    int shmid = shmget(key, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 等待一段时间以确保写入完成(实际应用中应使用信号量或互斥锁等同步机制)
    sleep(2); 

    read_from_shared_mem(shmid);//读取共享内存数据

    return 0;
}

消息队列

消息队列的原理

消息队列是链表队列,内核提供一个struct msqid_ds *msgque[MSGMNI]数组维护内核的一个消息队列列表,因此linux系统支持的最大消息队列数由msgque数组大小来决定,每一个msqid_ds表示一个消息队列,并通过msqid_ds.msg_first、msg_last维护一个先进先出的msg链表队列,当发送一个消息到该消息队列时,把发送的消息构造成一个msg结构对象,并添加到msqid_ds.msg_first、msg_last维护的链表队列,同样,接收消息的时候也是从msg链表队列尾部查找到一个msg_type匹配的msg节点,从链表队列中删除该msg节点,并修改msqid_ds结构对象的数据。
下面是消息队列的结构
在这里插入图片描述
数据结构解释:
msgque[MSGMNI]是一个msqid_ds结构的指针数组,每个msqid_ds结构指针代表一个系统消息队列,msgque[MSGMNI]的大小为MSGMNI=128,也就是说系统最多有MSGMNI=128个消息队列

 一个消息队列结构
struct msqid_ds
{
        struct ipc_perm msg_perm;
        struct msg *msg_first;  /*消息队列头指针*/
        struct msg *msg_last;/*消息队列尾指针*/

        __kernel_time_t msg_stime;/*最后一次插入消息队列消息的时间*/
        __kernel_time_t msg_rtime;/*最后一次接收消息即删除队列中一个消息的时间*/
        __kernel_time_t msg_ctime; 
        struct wait_queue *wwait;  /*发送消息等待进程队列*/
        struct wait_queue *rwait;  
        unsigned short msg_cbytes;
        unsigned short msg_qnum;/*消息队列中的消息个数*/
        unsigned short msg_qbytes;
        __kernel_ipc_pid_t msg_lspid; /*最后一次消息发送进程的pid*/
        __kernel_ipc_pid_t msg_lrpid; /*最后一次消息发送进程的pid*/

};
struct msg 消息节点结构:
msqid_ds.msg_first,msg_last维护的链表队列中的一个链表节点

struct msg
{
       msg *msg_next;              /*下一个msg*/
       long msg_type;                /*消息类型*/
       *msg_spot;                       /*消息体开始位置指针*/
        msg_ts;                           /*消息体长度*/
        message;                     /*消息体*/
}
消息体message可自定义结构

struct msgbuf
{
   long mtype;        --消息类型
   char mtext[n];     --消息内容
}

消息队列的使用

(1)消息队列Key的获取
在程序中若要使用消息队列,必须要能知道消息队列key,因为应用进程无法直接访问内核消息队列中的数据结构,因此需要一个消息队列的标识,让应用进程知道当前操作的是哪个消息队列,同时也要保证每个消息队列key值的唯一性
1.通过ftok函数获取

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

fname:就是你指定的文件名(已经存在的文件名),一般使用当前目录
id:子序号。虽然是int类型,但是只使用8bits

如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值就为0x26010002。

查看文件的索引节点号
在这里插入图片描述
当删除重建文件后,索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同,如果要确保key_t值不变,要么确保ftok的文件不被删除,要么不用ftok,指定一个固定的key_t值。

2.直接定义

 #define MSG_KEY      123456
自定义key的方式要注意避免消息队列的重复。

(2)创建或打开一个消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

参数key:消息队列Key
参数:msgflg:
IPC_PRIVATE:创建一个该进程独占的消息队列,其它进程不能访问该消息队列
IPC_CREAT:若消息队列不存在,创建一个新的消息队列,若消息队列存在,返回存在的消息队列
IPC_CREAT | IPC_EXCL: IPC_EXCL标志本身没有多大意义,与IPC_CREAT一起使用,保证只创建新的消息队列,若对应key的消息队列已经存在,则返回错误
IPC_NOWAIT:小队列以非阻塞的方式获取(若不能获取,立即返回错误)
返回值:成功返回队列ID,失败返回 -1

(3)发送一个消息到消息队列

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msqid:消息队列的ID
msgp:向队列中写入的数据,指向消息缓冲区的指针(上面msg 结构体的message段),此位置用来暂时存储发送和接收的消息,是一个用户可定义的结构体,如下

struct msgbuf{
	long mtype;//消息类型(>0)
	char mtext[128];//消息文本
};

msgsz:数据的长度
msgflg
      0:忽略该标志位,以阻塞的方式发送消息到消息队列
      IPC_NOWAIT:以非阻塞的方式发送消息,若消息队列满,函数立即返回。
返回值:
     0: 成功
     -1:非阻塞方式访问满消息队列返回
     EACCES:没有该消息队列写权限
     EFAULT:消息队列地址无法获取
     EIDRM:消息队列已经被删除
     EINTR:消息队列等待写入的时候被中断
     ENOMEM:内存不够

(4)从消息队列接收一个消息到msgbuf

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

msqid:消息队列的ID
msgp:写入的数据,指向消息缓冲区的指针,此位置 用来暂时存储发送和接收的消息,是一个用户可自定义的结构体如:

struct msgbuf{
	long mtype;//消息类型(>0)
	char mtext[128];//消息文本
};

msgsz:数据的长度
msgtyp
      msgtyp等于0,则返回队列的最早的一个消息。
      msgtyp大于0,则返回其类型为msgtyp的第一个消息
      msgtyp小于0,则返回其类型小于或等于mtype参数的绝对值的最小的一个消息。
可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。
msgflg:
      msgtyp0:表示忽略,没有得到消息阻塞等待
      msgtypIPC_NOWAIT:如果消息队列为空,不阻塞等待,返回一个ENOMSG

返回值:
     0:成功
     -1:消息长度大于msgsz
     EACCES:没有该消息队列读权限
     EFAULT:消息队列地址无法获取
     EIDRM:消息队列已经被删除
     EINTR:消息队列等待写入的时候被中断
     ENOMEM:内存不够
(5)消息队列的控制

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msqid:为消息队列的qid
cmd:为该函数要对消息队列执行的操作
     IPC_STAT:取出消息队列的msqid_ds结构体并将参数存入buf所指向的msqid_ds结构对象中
     IPC_SET:设定消息队列的msqid_ds 数据中的msg_perm 成员。设定的值由buf 指向的msqid_ds
结构给出。
     IPC_EMID:将队列从系统内核中删除。
buf:消息队列msqid_ds结构体指针

示例代码:

//get.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#define MSGQUEUE_KEY 123456
struct my_msg_st
{
    long mtype;
    char mtext[128];
};
int main()
{
    struct my_msg_st readBuf;
    struct my_msg_st writeBuf = {988, "think you"};
    int msgId = 0;
    key_t key = ftok(".", MSGQUEUE_KEY); // 获取键值,可以是字母或者数字
    printf("%x\n", key);                 // 16进制
    msgId = msgget(key, IPC_CREAT | 0777);
    if (msgId == -1)
    {
        perror("msgget");
        return -1;
    }
    msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);
    printf("%s\n", readBuf.mtext);
    msgsnd(msgId, &writeBuf, strlen(writeBuf.mtext), 0);
    msgctl(msgId, IPC_RMID, NULL); // 将队列从系统内核中删除
    return 0;
}
//send.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#define MSGQUEUE_KEY 123456
struct my_msg_st
{
    long mtype;
    char mtext[128];
};
int main()
{
    struct my_msg_st writeBuf = {888, "hello world"};
    struct my_msg_st readBuf;
    int msgId = 0;
    key_t key = ftok(".", MSGQUEUE_KEY); // 获取键值可以是字母或者数字
    msgId = msgget(key, IPC_CREAT | 0777);
    if (msgId == -1)
    {
        perror("msgget");
        return -1;
    }
    msgsnd(msgId, &writeBuf, strlen(writeBuf.mtext), 0);
    msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 988, 0);
    printf("%s\n", readBuf.mtext);
    msgctl(msgId, IPC_RMID, NULL); // 将队列从系统内核中删除
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值