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 
#include 

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

        2. 创建共享内存 shmget

#include 
#include 

/**
  ***********************************
  *@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 
#include 

/**
  ***********************************
  *@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 
#include 

/**
  ***********************************
  *@brief  解除映射
  *@param  shmaddr:需要解除映射的地址
           
  *@retval int 
			成功返回0
			失败返回-1,并返回错误码EOF
  ***********************************
  */
int shmdt(const void *shmaddr);

        5. 管理共享内存 shmctl

#include 
#include 

/**
  ***********************************
  *@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 
#include 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 
#include 

#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 ,运行效果如下,共享内存的基本操作,很简单是吧。

Linux C 进程间的通信——共享内存、消息队列、信号量_第1张图片


二、消息队列

        (一)消息队列通信原理

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

        (二)基操

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

        (三)相关API

        1. 创建或获取消息队列的ID msgget

#include 
#include 
#include 

/**
  ***********************************
  *@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 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 

#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;
}

效果如下:

Linux C 进程间的通信——共享内存、消息队列、信号量_第2张图片

         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 
#include 
#include 

/**
  ***********************************
  *@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 
#include 
#include 
#include 
#include 
#include 


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 
#include 
#include 
#include 
#include 
#include 


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;
}

执行结果:

Linux C 进程间的通信——共享内存、消息队列、信号量_第3张图片

四、总结

        到此为止,进程间的通信基本介绍完了,早期:有名管道、无名管道、信号,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

你可能感兴趣的:(进程通信,c语言,linux,vim)