UNIX网络编程:共享内存区

Posted Dandelion_gong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UNIX网络编程:共享内存区相关的知识,希望对你有一定的参考价值。

IPC形式除了管道、FIFO、信号量以外,还有共享内存区和消息队列。这里主要堆共享内存进行介绍。
共享内存区是可用IPC形式中最快的。一旦这样的内存区映射到共享它的进程地址空间,这些进程间数据的传递就不再涉及内核。共享内存与其他进程通信方式相比较,不需要复制数据,直接读写内存,是一种效率非常高的进程通信方案。但它本身不提供同步访问机制,需要我们自己控制。在LINUX中,只要把共享内存段连接到进程的地址空间中,这个进程就可以访问共享内存中的地址了。为了实现往该共享内存区存放和取出数据的进程间的同步性,防止自己写入的数据被自己读出。这就到了我们之前提到的信号量大显身手的时候了。
共享内存是系统创建的特殊地址空间,允许不相关的多个进程使用这个内存空间,即多个进程能够使用同一块内存中的数据。

LINUX系统提供的共享内存操作函数与信号量、消息队列等类似,主要有以下几个:
(1)int shmget(key_t key,int shmsz,int shmflg);
Shmget()函数分配一块新的共享内存。key是指ftok所得到的键值。Shmsz指明共享内存的大小,以字节为单位,shmflg的设置是标志信息
如果shmget()函数调用成功则返回共享内存的ID;否则返回-1.
(2)void *shmat(int shmid,const void *shmaddr, int shmflg);
Shmat()函数的作用是连接共享内存与某个进程的地址空间。
Shmid是shmget()函数返回的共享内存ID。Shmaddr是共享内存连接到进程中的存放地址,一般设置为空指针,表示交由系统完成这个工作。
如果shmaddr为0 则此段连接到由内核选择的第一个可用地址上,这是推荐的使用方式
如果shmaddr非零,并且没有指定SHM_RND,则此段链接到addr所指的地址上
如果shmaddr非零且指定SHM_RND,则此段链接到shmaddr - (addr mod ulus SHMLBA)所表示的地址上。SHM_RND的意思是低边界地址倍数,它总是2的乘方。该算式是将地址向下取最近的一个SHMLBA的倍数
Shmflg设置共享内存的控制选项,有两个可能取值:SHM_RND(与shmaddr参数相关)与SHM_RDONLY(只允许读)。如果shmat()函数调用成功则返回指向共享内存的指针;否则返回-1.
(3)int shmdt (const void *shmaddr);
Shmdt()函数用来解除进程与共享内存区域的关联,使当前进程不能继续访问共享内存。参数shmaddr是shmat()函数返回的指针。如果操作成功则返回0;失败则返回-1.

(4)int shmctl(int shmid, int cmd,struct shmid_ds *buf);
Shmctl()函数实现对共享内存区域的控制操作。
cmd:指明所要执行的操作,常用的有以下命令:
IPC_STAT:调用shmctl函数,执IPC_STAT命令,获取一个已存在内存区的大小
IPC_SET :调用shmctl函数,执行IPC_SET命令,设置一个已存在内存区中的值
IPC_RMID:调用shmctl函数,执行IPC_RMID命令,删出共享内存区对象。

下面是一个用信号量和共享内存联合一起实现的一个服务器与客户端之间通信的例子,思路是这样的服务器和客户端根据两个信号量进行沟通,信号量1负责服务器的写和客户端的读,而信号量2负责服务器的读和客户端的写(因为半双工管道的原因,不能同时控制读写)。而读写的信息存放在共享内存中,服务器写进数据到共享内存,并发出消息给客户端,客户端在内存中取出数据读。而信号量的作用就在于同步控制,根据0阻塞进程的特性,防止服务器自己写的数据被自己读的错误发生。

服务器端代码:
ser.cpp

#include "utili.h"

int main(int argc, char *argv[])
{
    /*创建一个唯一的键值并判断是否创建成功*/
    key_t key_id;
    key_id = ftok(argv[1], ID);
    if(key_id == -1){
        printf("ftok error.\\n");
        exit(1);
    }
    //用上面创建的键值创建一块大小为1024的共享内存。要么创建要么返回EEXIST错误,权限为所数用户、所数组、其他用户可读可写。并判断它是否创建成功
    key_t shm_id = shmget(key_id, 1024, IPC_CREAT|IPC_EXCL|0666);
    if(shm_id == -1){
        printf("shmget error.\\n");
        exit(1);
    }
    //在创建的共享内存中建立连接,地址由系统分配,并检测连接是否成功
    char* addr = (char *)shmat(shm_id, NULL, 0);
    if(addr == (char *)-1){
        printf("shmat error.\\n");
        shmctl(shm_id, IPC_RMID, NULL);
        exit(1);
    }
    //重新创建一个新的键值并判断是否创建成功
    key_t sem_key, sem_id;
    sem_key = ftok(argv[1], ID1);
    if(sem_key == -1){
        printf("sem ftok error.\\n");
        exit(1);
    }
    //用上面创建的键值创建两个信号量集合,并判断信号量集合是否创建成功
    sem_id = semget(sem_key, 2, IPC_CREAT|IPC_EXCL|0666);
    if(sem_id == -1){
        printf("semget error.\\n");
        exit(1);
    }
    //给两个信号量都赋初值0
    union semun init;
    init.val = 0;
    semctl(sem_id, 0, SETVAL, init);
    semctl(sem_id, 1, SETVAL, init);

    //定义两个sembuf的结构体,用来控制信号量资源数
    struct sembuf p = {0, -1, 0};
    struct sembuf v = {1, 1, 0};

    while(1){
        //服务器写数据【此时0号信号量的值为0,处于阻塞状态,所以它不会将自己写入的数据读出】
        printf("Ser:>");
        gets(addr);
        //判断它写入的数据是否为quit,
        if(strncmp(addr, "quit", 4) == 0){
            semop(sem_id, &v, 1);   //用结果体v对相应的信号量进行设置。保证结束信息可以令客户端读到,防止客户端一直在等待服务器端的数据
            shmdt(addr);            //断开共享内存连接
            shmctl(shm_id, IPC_RMID, NULL);   //删除共享内存区
            //删除之前创建的两个信号量集
            semctl(sem_id, 0, IPC_RMID);
            semctl(sem_id, 1, IPC_RMID);
            break;
        }
        //如果输入数据部为quit,用结果体v对相应的信号量进行设置。保证信息可以令客户端读到
        semop(sem_id, &v, 1);

        //服务器读数据【此时1号信号量的值为0,客户端读操作处于阻塞状态】
        semop(sem_id, &p, 1);//改变0号信号量的值,让服务器的读操作处于运行状态
        printf("Cli:>%s\\n", addr);
        //比较客户端传来的数据是不是quit,如果是quit则断开连接,并将值前创建的共享内存区和信号量删除掉然后退出。
        if(strncmp(addr, "quit", 4) == 0){
            shmdt(addr);
            semctl(shm_id, IPC_RMID, NULL);
            semctl(sem_id, 0, IPC_RMID);
            semctl(sem_id, 1, IPC_RMID);
            break;
        }
    }

        return 0;
}

客户端程序:
cli.cpp:

#include "utili.h"

int main(int argc, char * argv[])
{
    //用与服务器端同样的路径与数值创建一个相同的键值,并判断是否创建成功
    key_t key_id = ftok(argv[1], ID);
    if(key_id == -1){
        printf("ftok error.\\n");
        exit(1);
    }
    //利用该键值查找服务器端所创建的共享内存的id号,并判断是否查找成功
    key_t shm_id = shmget(key_id, 0, 0);
    if(shm_id == -1){
        printf("shmget error.\\n");
        exit(1);
    }
    //与共享内存建立连接,并保证创建连接成功
    char *addr = (char *)shmat(shm_id,NULL,0);
    if(addr == (void *)-1)
    {
        printf("shmat error.\\n");
        exit(1);
    }
     //重新创建一个与服务器中相同的sem_key,用来创建信号量集
     key_t sem_key = ftok(argv[1],ID1);
     if(sem_key == -1)
     {
         printf("sem ftok error.\\n");
         exit(1);
     }   
     //查找服务器创建的两个信号量集的id号
     key_t sem_id = semget(sem_key, 0, 0);
     if(sem_id == -1)
     {   
         printf("semget error.\\n"); 
         exit(1);
     }
     //创建两个sembuf型的结构体,p、v对应操作的信号量下标量semnum(第一个参数)应该与服务器端相反,这样才能实现同步
     struct sembuf p = {1,-1,0};
     struct sembuf v = {0,1,0};

     while(1){
         //客户端读数据,并判断服务器端传来的数据是否为quit,如果为quit直接程序结束
         semop(sem_id, &p, 1);
         printf("Ser:>%s\\n", addr);
         if(strncmp(addr, "quit", 4) == 0){
             break;
         }

         //所读到的数据不为quit,客户端进行写数据【此时1号信号量的值为0,客户端的读操作阻塞】
         printf("Cli:>");
         gets(addr);
         //如果客户端传送的数据为quit,则将0号信号量的值加1,然后在退出程序,保证服务器端可以读到数据,从而断开连接,防止服务器端一直等待客户端的数据
         if(strncmp(addr, "quit", 4) == 0){
             semop(sem_id, &v, 1);
             break;
         }
         //如果不为quit,则将0号信号量的值加1,让服务器的读操作处于运行状态
         semop(sem_id, &v, 1);
    }   

         return 0;
}

头文件:
utili.h:

#pragma once

#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>

using namespace std;

#define ID  0xFF
#define ID1 0xFE

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 */
    void *__pad;
}; 

程序运行结果:
这里写图片描述

这里才真正的体现了信号量的作用,它可以实现同步机制,其实相比于共享内存,IPC形式中的消息队列本身就带有同步形式,后序会对其继续进行整理,欢迎来访~~。

以上是关于UNIX网络编程:共享内存区的主要内容,如果未能解决你的问题,请参考以下文章

Linux环境编程之共享内存区:共享内存区简单介绍

linux c编程:Posix共享内存区

C 中的共享内存代码片段

System V IPC 之共享内存

Linux系统编程 --进程间通信 -共享内存

Unix系统编程()进程内存布局