linux下C语言实现进程通信与共享内存

      大家好,我是练习时长两年半的练习生,喜欢唱、跳、rap、敲代码,键来!

        在前面我们已经讲过,早期的进程间通信有三种——无名管道、有名管道、信号,(传送门:Linux C 进程间的通信——无名管道、有名管道、信号),今天就来浅谈一下在system V IPC的三种对象,也是进程通信的另外三种姿势——共享内存、消息队列、信号量。

目录

一、共享内存

        (一)概念

        (二)基操

        (三)相关API   

        (四)示例代码

二、消息队列

        (一)消息队列通信原理

        (二)基操

        (三)相关API

        (四)示例代码

三、信号量

(一)概念

(二)基操

(三)相关API

(四)代码实例

四、总结

补充:shell查看和删除IPC对象

一、共享内存
        (一)概念
        共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间,进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高进程通信的效率。

        (二)基操
                1》创建或者获取共享内存
                2》将共享内存映射到用户空间 

                3》解除映射
                4》删除共享内存 

        (三)相关API   
        1. 创建密钥 ftok

             注意!!!
        只要pathname和proj_id不变,就可以获得相同的key,如下,进程A和进程B获得的key相同。
        例子:
               进程A:key_t key1=ftok("xx.txt",0xa);
               进程B:key_t key2=ftok("xx.txt",0xa);

#include <sys/types.h>
#include <sys/ipc.h>
 
/**
  ***********************************
  *@brief  创建密钥key
  *@param  pathname:带路径的文件名
           proj_id:数字
       
  *@retval key_t
            成功返回key
            失败返回-1,并返回错误码EOF
  ***********************************
  */
key_t ftok(const char *pathname, int proj_id);

        2. 创建共享内存 shmget

#include <sys/ipc.h>
#include <sys/shm.h>
 
/**
  ***********************************
  *@brief  创建或者获取共享内存
  *@param  key:密钥 
                IPC_PRIVATE:系统自动分配key
           size:共享内存的大小
           shmflg:权限,一般写为 IPC_CREAT|0666
  *@retval int 
            成功返回共享内存的id
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int shmget(key_t key, size_t size, int shmflg);

        3. 共享内存映射到用户空间 shmat

#include <sys/types.h>
#include <sys/shm.h>
 
/**
  ***********************************
  *@brief  将共享内存映射到用户空间
  *@param  shmid:共享内存的id 
           shmaddr:需要映射到什么地方(地址)
           shmflg:访问权限
                    SH_RDONLY:只读
                    0:默认,可读可写
  *@retval int 
            成功返回映射后的地址
            失败返回-1,并返回错误码EOF
  ***********************************
  */
void *shmat(int shmid, const void *shmaddr, int shmflg);

          4. 解除映射 shmdt

#include <sys/types.h>
#include <sys/shm.h>
 
/**
  ***********************************
  *@brief  解除映射
  *@param  shmaddr:需要解除映射的地址
           
  *@retval int 
            成功返回0
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int shmdt(const void *shmaddr);
        5. 管理共享内存 shmctl

#include <sys/ipc.h>
#include <sys/shm.h>
 
/**
  ***********************************
  *@brief  删除
  *@param  shmid:需要解除映射的地址
           cmd:IPC_STAT  (获取对象属性)
                IPC_SET   (设置对象属性)
                IPC_RMID  (删除对象)
           buf:如果cmd为IPC_RMID,则此处置NULL
           
  *@retval int 
            成功返回0
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
 
struct shmid_ds {
        struct ipc_perm shm_perm;    /* Ownership and permissions */
        size_t          shm_segsz;   /* Size of segment (bytes) */
        time_t          shm_atime;   /* Last attach time */
        time_t          shm_dtime;   /* Last detach time */
        time_t          shm_ctime;   /* Last change time */
        pid_t           shm_cpid;    /* PID of creator */
        pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
        shmatt_t        shm_nattch;  /* No. of current attaches */
        ...
};

       

        (四)示例代码
        shm_write.c 和 shm_read.c代码基本一样,代表两个不同的进程,一个写入一个读取。shm_write.c中每次输入一行数据就会覆盖原来在共享内存空间的数据,输入quit退出输入状态,然后解除映射删除空间。shm_read.c读取到quit后也会退出,然后解除映射。

shm_write.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
 
#define SIZE 200    //共享内存的大小
 
int main(int argc, char *argv[])
{
    key_t key;
    int shmid;
    char *addr;
 
    //手动创建key
    if((key = ftok("./",0xa)) < 0){
        perror("ftok error");
        exit(1);
    }
 
    //创建共享内存,获取id号
    if((shmid = shmget(key,SIZE,IPC_CREAT|0666)) < 0){
        perror("shmget error");
        exit(1);
    }
 
    //映射共享内存到用户空间
    if((addr = shmat(shmid,NULL,0)) < 0){
        perror("shmat error");
        exit(1);
    }
 
    //往共享内存空间中写入数据,输入quit退出
    while(1){
        printf("please input string:");
        if(!strncmp(fgets(addr,SIZE,stdin),"quit",4))
            break;
    }
 
    //解除映射
    if((shmdt(addr) < 0)){
        perror("shmdt error");
        exit(1);
    }
 
    //删除共享内存
    if((shmctl(shmid,IPC_RMID,NULL) < 0)){
        perror("shmctl error");
        exit(1);
    }
 
    return 0;
}

     shm_read.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
 
#define SIZE 200
 
int main(int argc, char *argv[])
{
    key_t key;
    int shmid;
    char *addr;
 
    if((key = ftok("./",0xa)) < 0){
        perror("ftok error");
        exit(1);
    }
 
    if((shmid = shmget(key,SIZE,IPC_CREAT|0666)) < 0){
        perror("shmget error");
        exit(1);
    }
 
    if((addr = shmat(shmid,NULL,0)) < 0){
        perror("shmat error");
        exit(1);
    }
 
    //读取共享内存中的数据,如果内存中的数据为quit则退出
    while(1){
        printf("%s",addr);
        if(!strncmp(addr,"quit",4))
            break;
        sleep(1);
    
    }
 
    if((shmdt(addr) < 0)){
        perror("shmdt error");
        exit(1);
    }
 
    return 0;
}
 

       需要在ubuntu中开两个终端,分别运行 shm_read.c 和 shm_write.c ,运行效果如下,共享内存的基本操作,很简单是吧。

二、消息队列
        (一)消息队列通信原理
                 1》消息队列中可以有不同类型的消息
                 2》发送消息和接收消息时,必须要指定消息的类型
                 3》进程在通信时,发送的消息类型必须和接收的消息类型相同

        (二)基操
                1》创建或者获取消息队列
                2》写入或读取数据
                3》管理消息队列

        (三)相关API
        1. 创建或获取消息队列的ID msgget

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
 
/**
  ***********************************
  *@brief  创建或者获取消息队列
  *@param  key: 密钥
           msgflg:权限 IPC_CREAT|0666          
  *@retval int 
            成功返回id
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int msgget(key_t key, int msgflg);
        2. 发送消息到消息队列 msgsnd

/**
  ***********************************
  *@brief  往消息队列里写入数据
  *@param  msqid:消息队列id
           msgp:消息的数据包,需要自己定义结构体
                 struct msgbuf {
                    long mtype;        // 消息类型,且要大于0  第一个成员必须是消息类型
                    char mtext[10];    //  消息正文
                 };
           msgsz:消息正文中消息的实际长度
           msgflg: 0 ---如果不能立即发送,则阻塞
                    IPC_NOWAIT ----    如果不能立即发送,则返回      
  *@retval int 
            成功返回0
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
        3. 从消息队列中接收消息 msgrcv

/**
  ***********************************
  *@brief  从消息队列中接收消息
  *@param  msqid:消息队列id
           msgp:消息的数据包,需要自己定义结构体
                 struct msgbuf {
                    long mtype;        // 消息类型,且要大于0  第一个成员必须是消息类型
                    char mtext[10];    //  消息正文
                 };
           msgsz:消息正文中消息的实际长度
           msgtyp:消息的类型
                    0:按顺序接收消息队列中第一个消息。
                    > 0:接收消息队列中第一个类型为msgtyp的消息.
                    < 0:接收消息队列中类型值不大于msgtyp的绝对值且类型值又最小的消息。
           msgflg: 0 ---若无消息函数会一直阻塞
                    IPC_NOWAIT ----    若没有消息,进程会立即返回ENOMSG  
   
  *@retval ssize_t
            成功返回接收到的消息的长度
            失败返回-1,并返回错误码EOF
  ***********************************
  */
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
        4. 管理消息队列对象 msgctl

/**
  ***********************************
  *@brief  管理消息队列对象
  *@param  msqid:消息队列id
           cmd:IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
                IPC_SET: 设置消息队列的属性。这个值取自buf参数。
                IPC_RMID:从系统中删除消息队列。
           buf:cmd为IPC_RMID时,此处为NULL
           
  *@retval int 
            成功返回0
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
 
struct msqid_ds {
    struct ipc_perm msg_perm;     /* Ownership and permissions */
    time_t          msg_stime;    /* Time of last msgsnd(2) */
    time_t          msg_rtime;    /* Time of last msgrcv(2) */
    time_t          msg_ctime;    /* Time of last change */
    unsigned long   __msg_cbytes; /* Current number of bytes in queue (nonstandard) */
    msgqnum_t       msg_qnum;     /* Current number of messages in queue */
    msglen_t        msg_qbytes;   /* Maximum number of bytes allowed in queue */
    pid_t           msg_lspid;    /* PID of last msgsnd(2) */
    pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};
        (四)示例代码
                msg_write.c 和 msg_read.c 基本一样,用两个终端分别运行,代表两个不同的进程,实现两个进程的通信。代码按照上面的基操一步一步来,没什么难度,注释尽量写的详细了。

msg_write.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <string.h>
 
#define N 50     //消息队列的大小
 
//数据包需要自己定义
struct msgbuf{
    long mytype;    //第一个成员必须代表消息的类型
    char mydate[N];    //变量类型可以自己随便定义
};
 
int main(int argc, char *argv[])
{
    key_t key;    //密钥
    int msgid;    //消息队列的id
    struct msgbuf sm;
 
    if((key = ftok("./",0xa)) < 0){    //手动创建密钥,可以参考共享内存里的key创建
        perror("ftok error");
        exit(1);
    }
 
    //创建消息队列,成功则返回队列的id
    if((msgid = msgget(key,IPC_CREAT|0666)) < 0){
        perror("msgget error");
        exit(1);
    }
 
    while(1){
        bzero(sm.mydate,sizeof(sm.mydate));    //将结构体里的数组清零
        fgets(sm.mydate,sizeof(sm.mydate),stdin);    //从键盘上输入字符串,字符串中包含\n
        msgsnd(msgid,&sm,sizeof(struct msgbuf)-sizeof(long),0); //发送数据到消息队列里
    }
 
    //删除消息队列
    if(msgctl(msgid,IPC_RMID,NULL) < 0){
        perror("msgctl error");
        exit(1);
    }
 
    return 0;
}
msg_read.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <string.h>
 
#define N 50
 
struct msgbuf{
    long mytype;
    char mydate[N];
};
 
int main(int argc, char *argv[])
{
    key_t key;
    int msgid;
    struct msgbuf sm;
 
    if((key = ftok("./",0xa)) < 0){
        perror("ftok error");
        exit(1);
    }
 
    if((msgid = msgget(key,IPC_CREAT|0666)) < 0){
        perror("msgget error");
        exit(1);
    }
 
    while(1){
        msgrcv(msgid,&sm,sizeof(struct msgbuf)-sizeof(long),0,0); //从消息队列中读取数据
        printf("%s",sm.mydate);
    }
 
    if(msgctl(msgid,IPC_RMID,NULL) < 0){
        perror("msgctl error");
        exit(1);
    }
 
    return 0;
}
 
效果如下:

         mag_write 不断往队列里写数据,msg_read 不断地从队列里取数据,所以消息队列的占用的内存大小为0,当然也可以先执行 msg_write ,往里面写多条数据,那队列占用的字节的大小就为 sizeof(struct msgbuf) * n,n为输入的次数。msg_read 去掉while循环也可以每次执行就取一条数据,要怎么玩得看需求,需求决定一切。输入quit退出while循环我没写,可以参考共享内存的quit退出。

三、信号量
(一)概念
        由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。信号量,也叫信号灯(semaphore),它是不同进程间或一个给定进程内部不同线程间同步的机制,也属于进程间的通信。

信号灯的种类 
    
    1》二值信号灯:表示资源是否可用

        值为0或1。与互斥锁类似,资源可用时值为1,不可用时值为0。

    2》计数信号灯:表示资源的多少
        值在0到n之间。用来统计资源,其值代表可用资源数

    3》P/V操作  
        P为减,V为加,加减某个自定义的整数,灯的值为负数(value < 0)则阻塞,值大于等于零(value >= 0)不阻塞。
 

(二)基操
            1》创建或获取信号量
            2》对信号灯进行PV操作(P为减法,V为加法)
            3》删除信号量---收尾

(三)相关API
        1. 创建或获取信号量 semget

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
 
/**
  ***********************************
  *@brief  创建或获取信号量
  *@param  key:密钥
           nsems:信号灯的个数
           semflg:IPC_CREAT|0666
           
  *@retval int 
            成功返回id
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int semget(key_t key, int nsems, int semflg);

        2. 对信号灯进行PV操作 semop

/**
  ***********************************
  *@brief  对信号灯进行操作
  *@param  semid:信号量id
           sops:结构体指针  
           nsops:要操作的信号灯的个数
           
  *@retval int 
            成功返回0
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int semop(int semid, struct sembuf *sops, size_t nsops);
 
struct sembuf {
    unsigned short  sem_num;        /* semaphore index in array 信号灯的编号,从0开始 */
    short           sem_op;         /* semaphore operation -1:p操作,1:表示v操作*/
    short           sem_flg;        /* operation flags   默认:0,  IPC_NOWAIT,  SEM_UNDO */
};
        3. 管理信号量 semctl

/**
  ***********************************
  *@brief  管理信号量
  *@param  semid:信号量id
           semnum:信号灯的下标  
           cmd:    IPC_RMID:删除信号量
                    GETVAL:获取到信号灯的value值
                    SETVAL:设置信号灯的value值
                    GETALL:获取多个信号灯的value值
                    SETALL:设置多个信号灯的value值
           ...:变参,如果cmd不是IPC_RMID,则有第四个参数,这个参数是联合体
               union semun {
                  int              val;    /* Value for SETVAL 设置某个信号灯的值*/ 
                  struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
                  unsigned short  *array;  /* Array for GETALL, SETALL */ 
                  struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
               };
           
  *@retval int 
            GETVAL:成功返回value值
            其他成功返回0
            失败返回-1,并返回错误码EOF
  ***********************************
  */
int semctl(int semid, int semnum, int cmd, ...);
(四)代码实例
        先执行sem_P.c(./sem_P 1 -10),输入一个-10传给灯ss1的value,让该进程处于阻塞状态,然后执行sem_V.c(./sem_V.c 1 2),每次执行灯ss1就会加2,执行5次后ss1的value会加到10,此时原来ss1的value = -10会被抵消为0,sem_P.c解除阻塞,打印“NO WAIT”,这就是进程的同步。注意,每个灯都是独立的灯,要对同一个灯进程操作(P或V)。

sem_P.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
 
 
int main(int argc, char *argv[])
{
    if(argc != 3){    //main传参,第一个为灯的下标,第二个为负数,表示P减操作
        printf("argment int[0,1,2] and int[-]\n");
        exit(1);
    }
 
    key_t key;
    int semid;
    char str[50];
    int nsem = atoi(argv[1]);    //字符串转整数
    int value = atoi(argv[2]);
 
    
    if((key = ftok("./",0xb)) < 0){    //获取密钥
        perror("ftok");
        exit(1);
    }
 
    if((semid = semget(key,3,IPC_CREAT|0666)) < 0){    //创建有3个信号灯的信号量,返回id
    perror("semget");
    exit(1);
    }
 
    struct sembuf ss0 = {0,value,0};    //初始化灯0
    struct sembuf ss1 = {1,value,0};    //初始化灯1
    struct sembuf ss2 = {2,value,0};    //初始化灯2
    struct sembuf buf[3] = {ss0,ss1,ss2};
 
    /*
    //对单个ss0灯进行初始化,和上面的效果是一样的
    buf[0].sem_num = 0;    //灯的下标
    buf[0].sem_op = -4;    //要加或减的值
    buf[0].sem_flg = 0;    //0为阻塞,IPC_NOWAIT不阻塞
    */
 
 
    //value为负数则阻塞等待,直到另一个进程将value加到>=0即可解除阻塞
    if(semop(semid,&buf[nsem],1) < 0){    
        perror("semop");
        exit(1);
    }
 
    printf("NO WAIT\n");   
    //printf("value:%d\n",semctl(semid,nsem,GETVAL,NULL));
 
    if(semctl(semid,0,IPC_RMID,NULL) < 0){    //删除信号量
        perror("semctl");
        exit(1);
    }
 
    return 0;
}
sem_V.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
 
 
int main(int argc, char *argv[])
{
    if(argc != 3){    //main传参,第一个参数为灯的下标,第二个为正整数,表示V加操作
        printf("argment int[0,1,2] and int[+]\n");
        exit(1);
    }
 
    key_t key;
    int semid;
    char str[50];
    int nsem = atoi(argv[1]);    //字符串转整数
    int value = atoi(argv[2]);
 
    
    if((key = ftok("./",0xb)) < 0){    //密钥
        perror("ftok");
        exit(1);
    }
 
    if((semid = semget(key,3,IPC_CREAT|0666)) < 0){    //创建有3个灯的信号量
        perror("semget");
        exit(1);
    }
 
    struct sembuf ss0 = {0,value,0};    //初始化灯
    struct sembuf ss1 = {1,value,0};
    struct sembuf ss2 = {2,value,0};
    struct sembuf buf[3] = {ss0,ss1,ss2};
 
    if(semop(semid,&buf[nsem],1) < 0){    //对下标为nsem(传进来的第一个参数)进行操作
        perror("semop");
        exit(1);
    }
 
    printf("value:%d\n",semctl(semid,nsem,GETVAL,NULL));    //打印当前操作的灯的值
    return 0;
}
 
执行结果:

四、总结
        到此为止,进程间的通信基本介绍完了,早期:有名管道、无名管道、信号,system V IPC:共享内存、消息队列、信号量。除了这六种以外呢其实还有另一种进程间的通信——域通信,不过域通信就要结合TCP或者UDP来操作,基本和管道差不多,这部分有缘再更。

        不难发现,这次讲的三种进程通信的操作有很多相似之处,它们都属于IPC对象,操作和函数基本差不多,比如:创建或获取(shmget、msgget、semget),管理(shmctl、msgctl、semctl),功能参数大同小异,我们可以记住其中一种操作,就可以举一反三。

        进程通信的操作并不难,难就难在函数多,参数多,参数还结合了结构体联合体,就很恶心,这也是我大部分代码都是介绍函数的原因,重点在对各种API函数的介绍上。

        此外这些例子只能单向通信,要实现双向通信就得加个进程(fork)或线程(pthread_ctreate),也不难,有兴趣的可以自己试一下。

补充:shell查看和删除IPC对象
ipcs -m    //查看共享内存
ipcrm -m id    //要删除的共享内存id
ipcs -q    //查看消息队列
ipcrm -q id    //要删除的消息队列id
ipcs -s    //查看信号量
ipcrm -s id    //要删除的信号量id
————————————————
版权声明:本文为CSDN博主「诡谲神知」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_53612102/article/details/126816931

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux系统中,不同进程之间可以通过多种方式进行通信,如管道、共享内存、信号和套接字等。下面将分别介绍这几种通信方式: 1. 管道:管道是一种单向的、字节流的通信方式,只能在具有亲缘关系的进程之间使用,其中一个进程写入管道,另一个进程从管道中读取数据。使用管道通信需要调用pipe()函数创建管道,然后通过fork()函数创建子进程,父子进程分别使用管道读写函数进行通信。 2. 共享内存共享内存是一种快速的内存共享方式,多个进程可以访问同一块共享内存,并可以通过访问内存来进行进程间通信。使用共享内存需要调用shmget()函数创建共享内存区域,然后通过shmat()函数将其附加到进程的地址空间中。 3. 信号:信号是Linux系统中常用的一种进程间通信方式,一个进程可以向另一个进程发送信号来通知其发生的事件。信号有预定义的编号,例如SIGINT表示用户中断进程的信号。使用信号通信需要调用signal()函数设置信号处理函数,当发生对应的信号时,内核会调用相应的信号处理函数进行处理。 4. 套接字:套接字是一种全双工的通信方式,可以在不同主机或同一主机的进程间进行通信。套接字通信需要使用socket()函数创建套接字并指定地址和协议,然后通过bind()函数进行绑定,accept()函数接受连接请求,read()和write()函数进行数据的读写操作。 以上是Linux系统中常用的几种进程间通信方式,不同的通信方式各有优劣,根据不同的应用场景选择不同的通信方式可以提高效率和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值