15章 进程间通信之共享内存区(Posix、System V共享内存)

共享内存区介绍

共享内存是可用IPC形式中最快的。一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递就不再涉及内核(不需要执行内核调用传递数据)。然而往该共享内存区存放信息或从中取走信息的进程间通常需要某种形式的同步。

传统IPC:
15章 进程间通信之共享内存区(Posix、System V共享内存)_第1张图片

  • 服务器从输入文件读。该文件的数据由内核读入自己的内存空间,然后从内核复制到服务器进程。
  • 服务器往一个管道、 FIFO或消息队列以一条消息的形式写入这些数据。这些IPC形式通常需要把这些数据从进程空间复制到内核。
  • 客户从该IPC通道读出这些数据,这通常需要把这些数据从内核复制到进程空间。
  • 最后客户将这些数据从由write函数的第二个参数指定的客户缓冲区复制到输出文件。

涉及四次数据复制。而且这四次复制是在内核和某个进程间进行的,往往开销很大(比纯粹在内核中或单个进程内复制数据的开销大)

共享内存:
15章 进程间通信之共享内存区(Posix、System V共享内存)_第2张图片

  • 服务器从输入文件读。该文件的数据由内核读入共享内存。
  • 客户器从该IPC通道读出这些数据,这通常需要把这些数据从内核复制到进程空间

涉及2次文件复制,一次从输入文件到共享内存区,另一次从共享内存区到输出文件。

Posix共享内存

Posix提供了两种在无亲缘关系进程间共享内存区的方法

  • 1、内存映射文件:,由open打开,然后通过mmap将描述符映射到当前进程地址空间,可以在父子进程共享,也可在无亲缘关系进程间共享,因为有实际的文件系统路径,任何一个进程可以得到它。
  • 2、共享内存区对象:由shm_open打开一个名字(获取文件系统路径名),返回描述符然后由mmap映射到当前地址空间。这就是Posix共享内存的方法。首先指定待打开共享内存区的Posix IPC名字来调用shm_open。取得一个描述符后使用mmap把它映射到内存。其结果类似于内存映射文件,不过共享内存区对象不必作为一个文件来实现。

第1种方法可以查看高级IO之存储映射IO

系统调用

/*创建新的共享内存区对象或者打开一个对象
name:指定一个名字
oflag:读写或创建标志位,类似前面其他IPC。
mode:权限位,对于不同用户组的权限设置。
*/
extern int shm_open (const char *__name, int __oflag, mode_t __mode);

/* 删除一个共享内存区对象的名字,删除名字不会影响对于其底层对象的引用,直到该对象关闭为止。删除名字为了防止后续open等取得成功。
*/
extern int shm_unlink (const char *__name);

/*
修改普通文件或者共享内存区对象的大小
将共享内存区对象设置或者修改成__length字节。
*/
extern int ftruncate (int __fd, __off_t __length);

/*
当打开一个已存在的对象,可通过此查看对象信息。
对于普通文件,此结构含有12个成员,但是对于共享内存区,只有4个成员有用。
*/
extern int fstat (int __fd, struct stat *__buf);

简单程序操作共享内存

shmcreate程序:

int main(int argc , char **argv)//shmcreate程序,读取共享内存区数据
{
    int fd;
    char *ptr;
    off_t length;
    if(argc != 3)
        err_sys("./ ");
    length = atoi(argv[2]);
    fd = Shm_open(argv[1] , O_RDWR|O_CREAT|O_EXCL , FILE_MODE);//创建,当存在出错,读写方式打开
    Ftruncate(fd , length);//指定共享内存区对象长度
    ptr = (char *)Mmap(NULL , length , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);//将共享内存区映射到当前进程
    exit(0);//退出后,系统不会删除该共享内存区对象,因为共享内存区通过路径标识,至少具有随内核持续性。
}

shmunlink程序:

int main(int argc , char **argv)//shmunlink程序,删除共享内存区名字
{
    if(argc != 2)
        err_sys("./ ");

    Shm_unlink(argv[1]);//删除共享内存区名字

    exit(0);
}

shmread程序:

int main(int argc , char **argv)//shmread程序,读共享内存区
{
    int fd;
    unsigned char *ptr , c;
    struct stat stat;

    if(argc != 2)
        err_sys("./ ");
    fd = Shm_open(argv[1] , O_RDONLY , FILE_MODE);//打开共享内存区
    Fstat(fd , &stat);//必须通过取地址传进去,因为函数式填充结构体内容,需要分配空间,声明结构体变量分配了存储空间。
    ptr = (unsigned char *)Mmap(NULL , stat.st_size , PROT_READ , MAP_SHARED , fd , 0);//将共享内存区映射到当前进程
    //注意Shm_open和mmap指定的权限应该一致,这里是O_RDONLY
    Close(fd);//关闭描述符
    for(int i = 0 ; i//if( (c = *ptr++) != i % 256)
        //    err_ret("ptr[%d] = %d" , i ,c);//读取共享内存区数据,并验证,假如错误,输出错误
        printf("%d\n" , *ptr++);
    }
    exit(0);//退出后,系统不会删除该共享内存区对象,因为共享内存区通过路径标识,至少具有随内核持续性。
    //这些东西都有什么难度,全部都是验证性的事情而已,仅此而已好好学会调用不必要求这么多。
}

15章 进程间通信之共享内存区(Posix、System V共享内存)_第3张图片

共享内存区在不同进程映射地址可以不同

进程映射的地址不同,但是都是指向同一个共享内存区。

int main(int argc , char **argv)//测试共享内存区映射到不同进程的不同地址
{
    int fd , childfd;
    int *ptr;
    fd = Shm_open("myshmtest" , O_RDWR|O_CREAT|O_EXCL , FILE_MODE);//创建,当存在出错,读写方式打开
    Ftruncate(fd , sizeof(int) );//指定共享内存区对象长度
    if((childfd = Fork()) == 0){//child
        ptr = (int *)Mmap(NULL , sizeof(int) , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);
        printf("child :shmptr = %p\n" , ptr);
        sleep(1);
        printf("child print value = %d\n" , *ptr);
        exit(0);
    }
    ptr = (int *)Mmap(NULL , sizeof(int) , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);//将共享内存区映射到当前进程
    printf("parent :shmptr = %p\n" , ptr);
    *ptr = 555;
    Waitpid(childfd , NULL , 0);//等子进程结束
    Shm_unlink("myshmtest");//删除共享去内存名
    exit(0);
}

这里写图片描述
这里虽然映射地址是一样的,但是这是一个特例,多加几个共享内存区操作,那么映射地址就不会一样。

多个进程同时给共享计数器持续加1

Posix共享内存区和Posix信号量都以名字来访问,所以只要进程有足够的权限都可以访问这个两个对象,那么就可以实现非亲缘关系进程同步。
很简单,慢慢来,不着急不着急。
server:

struct shmstruct{
    int count;//计数放进共享内存区域
};
sem_t *mutex;
int main(int argc , char **argv)//服务器设定共享内存区以及初始化信号量
{
    int fd;
    struct shmstruct *ptr;
    Shm_unlink("myshmtest");
    fd = Shm_open("myshmtest" , O_RDWR|O_CREAT|O_EXCL , FILE_MODE);//创建共享内存区
    Ftruncate(fd , sizeof(struct shmstruct) );//指定共享内存区对象长度
    ptr = (struct shmstruct  *)Mmap(NULL , sizeof(struct shmstruct) , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);//将共享内存区映射到当前进程
    Close(fd);//关闭操作符

    Sem_unlink("mymutex");
    mutex = Sem_open("mymutex" , O_CREAT|O_EXCL , FILE_MODE , 1);//初始化信号量
    Sem_close(mutex);
    exit(0);
}

client:

struct shmstruct{
    int count;//计数放进共享内存区域
};
sem_t *mutex;
int main(int argc , char **argv)//多个客户机通过同步读取共享内存区
{
    int fd , nloop;
    struct shmstruct *ptr;
    if(argc != 2)
        err_sys("...");
    nloop = atoi(argv[1]);//循环次数
    fd = Shm_open("myshmtest" , O_RDWR , FILE_MODE);//读取共享内存区
    ptr = (struct shmstruct  *)Mmap(NULL , sizeof(struct shmstruct) , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);//将共享内存区映射到当前进程
    Close(fd);//关闭操作符

    mutex = Sem_open("mymutex" , 0);//从路径读取信号量
    for(int i = 0; i < nloop ; ++i){
        Sem_wait(mutex);
        printf("pid=%ld : %d\n"  , getpid() , ptr->count++);
        Sem_post(mutex);
    }
    Sem_close(mutex);
    exit(0);
}

这里写图片描述
15章 进程间通信之共享内存区(Posix、System V共享内存)_第4张图片
15章 进程间通信之共享内存区(Posix、System V共享内存)_第5张图片
任务切换,最后符合效果。

向一个服务器发送消息

server:

#define MESGSIZE 256//每条消息最大长度
#define NMESG 16//支持的消息条数
struct shmstruct{
    sem_t mutex;//
    sem_t nempty;//
    sem_t nstored;//生产消费者模型需要的信号量
    int nput;//存放一个消息的下一个位置的下标
    long noverflow;//溢出计数器,客户发送消息,但是没有存放消息的位置,那么溢出计数器加1
    sem_t noverflowmutex;//多个客户可能同时溢出,所以需要信号量
    long msgoff[NMESG];//每条消息的偏移地址,指出消息的起始位置
    char msgdata[NMESG * MESGSIZE];//实际消息缓存区,长度×条数
};
int main(int argc , char **argv)//server消费者部分
{
    struct shmstruct *ptr;
    int fd;
    int index , lastoverflow;
    long offset , temp;
    Shm_unlink("myshm");//首先清除共享内存区

    fd = Shm_open("myshm" , O_CREAT|O_RDWR|O_EXCL , FILE_MODE);//创建共享内存区
    ptr = (struct shmstruct *)Mmap(NULL , sizeof(struct shmstruct) , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);//映射都当前进程,并且返回首地址
    Ftruncate(fd , sizeof(struct shmstruct));//设定共享内存长度
    Close(fd);//关闭标识符
    for(int i = 0 ; i < NMESG ; ++i){
        ptr->msgoff[i] = i * MESGSIZE;//初始化消息偏移量
    }
    Sem_init(&ptr->mutex , 1 , 1);//初始化基于内存区信号量,变量存放在共享内存区
    Sem_init(&ptr->nempty , 1 , NMESG);//初始化基于内存区信号量,缓冲区空余NMESG
    Sem_init(&ptr->nstored , 1 , 0);//初始化基于内存区信号量,缓冲区没有存储
    Sem_init(&ptr->noverflowmutex , 1 , 1);//初始化

    //consumer
    index = 0;//从0开始消费(打印消息)消息
    lastoverflow = 0;//溢出计数修改比较变量
    for( ; ; ){//一直消费
        Sem_wait(&ptr->nstored);//开始肯定阻塞在此,因为没有存放数据
        Sem_wait(&ptr->mutex);//
        offset = ptr->msgoff[index];
        printf("index = %d : %s\n" , index , &ptr->msgdata[offset]);
        if(++index >= NMESG)//构建循环消费
            index = 0;
        Sem_post(&ptr->mutex);
        Sem_post(&ptr->nempty);//释放空闲一个存储空间

        Sem_wait(&ptr->noverflowmutex);//获取,当溢出计数改变,那么打印
        temp = ptr->noverflow;
        Sem_post(&ptr->noverflowmutex);
        if(temp != lastoverflow){//溢出计数改变,那么打印溢出数。
            printf("noverflow = %d\n" , temp);
            lastoverflow = temp;//保存上一次溢出计数。
        }
    }
    exit(0);
}

clien:

#define MESGSIZE 256//每条消息最大长度
#define NMESG 16//支持的消息条数
struct shmstruct{
    sem_t mutex;//
    sem_t nempty;//
    sem_t nstored;//生产消费者模型需要的信号量
    int nput;//存放一个消息的下一个位置的下标
    long noverflow;//溢出计数器,客户发送消息,但是没有存放消息的位置,那么溢出计数器加1
    sem_t noverflowmutex;//多个客户可能同时溢出,所以需要信号量
    long msgoff[NMESG];//每条消息的偏移地址,指出消息的起始位置
    char msgdata[NMESG * MESGSIZE];//实际消息缓存区,长度×条数
};
int main(int argc , char **argv)//client 消息数和停顿微秒数
{
    int fd , i  , nloop , nusec;
    char mesg[MESGSIZE];
    struct shmstruct *ptr;
    long offset;
    if(argc != 3)
        err_sys("usage error");
    nloop = atoi(argv[1]);//atoi可以学习哈
    nusec = atoi(argv[2]);

    fd = Shm_open( "myshm" , O_RDWR , FILE_MODE);
    ptr = (struct shmstruct *)Mmap(NULL , sizeof(struct shmstruct) , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);
    Close(fd);
    for(i = 0 ; i < nloop; ++i){
        sleep(nusec);//学习延迟函数
        snprintf(mesg , MESGSIZE , "pid %ld : message %d" , (long)getpid() , i);//学习
        if(sem_trywait(&ptr->nempty) == -1){//获取不到信号量,不阻塞,并返回-1
            if(errno == EAGAIN){//获取不到信号量因为其他没有释放
                Sem_wait(&ptr->noverflowmutex);//为了修改溢出计数器,获得信号量
                ptr->noverflow++;
                Sem_post(&ptr->noverflowmutex);//释放
                continue;//没有位置了,下面没有必要接着存储了,赶紧进行下一次循环吧
            }
            else{
                err_sys("sem_trywait error");//try_wait函数出错了,得告诉用户进程
            }
        }
        Sem_wait(&ptr->mutex);//写开始信号量
        offset = ptr->msgoff[ptr->nput];
        if(++(ptr->nput) >= NMESG)//指向下一个位置
            ptr->nput = 0;//环形处理
        Sem_post(&ptr->mutex);
        strcpy(&ptr->msgdata[offset] , mesg);//存放消息,实际上就是将一个内存数据搬运到另外一个内存
        Sem_post(&ptr->nstored);//已经存放了消息,消费者,你可以开始消费了。写入的消息就是进程pid和i数值
    }
    exit(0);
}

15章 进程间通信之共享内存区(Posix、System V共享内存)_第6张图片
服务器消费(打印出来了就相当于消费了,空间可以另作它用了),客户机生产,两者之间进行同步。需要进行同步原语的处理,很牛逼。
这里还有待考察,生产者和消费者模型很牛逼哦。

父子不用共享内存:
fork之后,父子进程资源相同,都有各自的副本count++,所以信号量此时并没有作用。父子进程有各自的内存空间,中间不涉及共享问题。

#include "apue.h"
#define SEMNAME "mysem"
int count = 0;
int main(int argc, char *argv[])
{
    sem_t *mutex;
    int i , nloop;
    if(argc != 2){
        err_sys("pra error");
    }
    nloop = atoi(argv[1]);
    mutex = Sem_open(SEMNAME , O_CREAT|O_EXCL , FILE_MODE , 1 );//初始一个信号量
    Sem_unlink(SEMNAME);//删除名字,但是不会影响打开的mutex。
    setbuf(stdout , NULL);//不加缓冲输出
    if( Fork() == 0 ){//child
        for(i = 0 ; i< nloop ; ++i ){
            Sem_wait(mutex);//获取信号量
            printf("child: %d\n" , count++);
            Sem_post(mutex);//释放信号量
        }
        exit(0);
    }//父进程
    for(i = 0 ; i< nloop ; ++i ){
        Sem_wait(mutex);//获取信号量
        printf("parent: %d\n" , count++);
        Sem_post(mutex);//释放信号量
    }
    sleep(5);
    exit(0);
}

System V共享内存

提供类似Posix共享内存区,但是系统函数调用换了而已。

系统调用函数

/*
__key:共享内存区标识符。
__size:指定内存区大小
    0:访问一个已经存在的共享内存区。
    >0:创建时候,指定大小。
__shmflg:读写权限组合
*/
int shmget (key_t __key, size_t __size, int __shmflg);

/*
调用此函数将shmget创建的共享内存区映射到调用进程的地址空间
__shmid:标识符
__shmaddr:
    NULL:系统自动分配
    addr:一般不同这种
__shmflg:访问权限设置
成功则返回映射区首地址。
*/
void *shmat (int __shmid, const void *__shmaddr, int __shmflg)

/*
调用进程完成某个共享内存区的使用,调用此函数断开内存区。本函数不删除共享内存区,并且当进程终止,附解的共享内存区自动断开。
__shmaddr:shmat返回的首地址
*/
int shmdt (const void *__shmaddr);

/*
操作控制共享内存区。
__shmid:共享内存区标识符
__cmd:操作命令。
    IPC_RMID:删除标识符的共享内存。
    IPC_SET:设置共享内存区,数值来源与buff。
    IPC_STAT:返回共享内存区的shmid_ds结构。
__buf:内存分配交给调用者,这里仅仅传递指针进去而已,供被调用者访问调用者分配的内存。
*/
int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf);

二者共享内存差别

二者主要差别在于Posix共享内存区大小可以通过ftruncate修改大小,而Sys对象大小在调用shmget创建时候固定下来了。

共享内存区暂时搞定,没有什么难度,就是一些简单的系统调用而已。仅此而已,必须明白这个道理了。

你可能感兴趣的:(APUEAndUNPV2,Linux环境编程)