Linux 进程间通信基础(五)--共享内存

近正好有一些空余时间,在这里总结一下曾经使用过的Linux进程间通信的几种方法,贴出来帮助有需要的人,也有助于自己总结经验加深理解。上一次我们梳理了fifo管道的相关知识,这一次梳理共享内存。

(一)概念

在介绍共享内存之前,我们先来简单说一说System V IPC通信机制。

System V IPC机制最初是由AT&T System V.2版本的UNIX引入的。这些机制是专门用于IPC(Inter-Process Communication 进程间通信)的,它们在同一个版本中被应用,又有着相似的编程接口,所以它们通常被称为System V IPC通信机制。

共享内存是三个System V IPC机制中的第二个。共享内存允许不同进程之间共享同一段逻辑内存,对于这段内存,它们都能访问,或者修改它,没有任何限制。所以它是进程间传递大量数据的一种非常有效的方式。注意到我上面说“共享内存允许不同进程之间共享同一段逻辑内存”,这里是逻辑内存。也就是说共享内存的进程访问的可以不是同一段物理内存,这在X/Open标准中没有明确的规定,但是大多数的系统实现都将进程之间的共享内存安排为同一段物理内存。

共享内存之际上是由IPC机制分配的一段特殊的物理内存,它可以被映射到该进程的地址空间中,同时也可以被映射到其他拥有权限的进程的地址空间中。就像是使用了malloc分配内存一样,只不过这段内存是可以共享的。

(二)共享内存的创建,映射,访问和删除

IPC提供了一套API来控制共享内存,使用共享内存的步骤通常是:

1)创建或获取一段共享内存;

2)将上一步创建的共享内存映射到该进程的地址空间;

3)访问共享内存;

4)将共享内存从当前的进程地址空间分离;

5)删除这段共享内存;

现在我们来详细说明一下每一个步骤:

1)我们可以使用shmget()函数来创建一段共享内存:

int shmget( key_t key, size_t size, int shmflg );

其中,

--key:你为这段共享内存取得名字,系统利用它来区分共享内存,访问同一段共享内存的不同进程需要传入相同的名字。

--size:共享内存的大小

--shmflg:是共享内存的标志,包含9个比特标志位,其内容与创建文件时的mode相同。有一个特殊的标志IPC_CREAT可以和权限标志以或的形式传入。

2)我们可以使用函数shmat()来映射共享内存:

void* shmat( int shm_id, const void* shm_addr, int shmflg );

其中,

--shm_id:是共享内存的ID,shmget()函数的返回值。

--shm_addr:指定共享内存连接到当前进程地址空间的位置,通常我们传入NULL,表示让系统来进行选择。

--shmflg:一组控制的标志,我们通常输入0,也有可能输入SHM_RDONLY,表示共享内存段只读。

--函数返回值是共享内存的首地址指针。

3)我们可以使用函数shmdt()来分离共享内存:

int shmdt( void* shm_p );

其中,

--shm_p:就是共享内存的首地址指针,也即是shmat()的返回值。

--成功返回0,失败时返回-1。

4)我们可以使用shmctl()函数来控制共享内存:

int shmctl( int shm_id, int command, struct shmid_ds* buf );

其中,

--shm_id:是共享内存的标示符,也即是shmget()的返回值。

--command:是要采取的动作,它有三个有效值,如下所示:

 说明
IPC_STAT 把buf结构中的值设置为共享内存的关联值
IPC_SET                                     如果拥有足够的权限,将共享内存的值设置为buf中的值
IPC_RMID 删除共享内存段

--buf:buf是一个shmid_ds结构的指针它可以设置共享内存的关联值。shmid_ds的结构如下所示:

struct shmid_ds {
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
}

好,那么我们现在就来写一个小例子,使用以下共享内存。我们打算设置一个书写程序,它负责读取用户的输入,把用户输入的字符串写入到共享内存中去。再设置一个读取程序,从共享内存中读取数据打印出来。我们需要先写一个共同的头文件,它定义了一些相关信息。我们需要决定这段共享内存何时被读取,何时被写入的问题。所以我们需要手动设置一个标志,用这个标志告诉程序,此时共享内存是否可以被写入,是否可以被读取。我们新建一个"shmem_def.h"文件,然后输入:

#ifndef _SHMEM_DEF_H_
#define _SHMEM_DEF_H_

#define SHAMEM_DATA_SIZE    (32)        /* 共享内存的大小 */
#define SHAMEM_KEY          (1111)      /* 共享内存的标志 */

/* 共享内存的储存状态,EMPT时可写,FULL时可读 */
typedef enum share_memory_flag
{
    SHARE_MEMORY_FLAG_EMPT,
    SHARE_MEMORY_FLAG_FULL
}SHMFLAG;

/* 共享内存结构 */
typedef struct share_memory
{
    SHMFLAG flag;
    char data[SHAMEM_DATA_SIZE];
}SHAMEM;

#endif /* _SHMEM_DEF_H_ */

好了,那么我们现在先来写读取程序,它会每隔5秒钟定时检查共享内存的储存状态,如果储存状态时FULL,就表示有数据,它就会读取这些数据打印出来,并清空共享内存。好了我们按照上面的步骤尝试使用共享内存,新建一个文件shmem-reader.c:

#include 
#include 
#include 
#include 
#include 

#include "shmem_def.h"

int main()
{
    int shmfd;
    char buffer[SHAMEM_DATA_SIZE];
    SHAMEM* sharemem; 

    /* 创建共享内存 */
    shmfd = shmget( SHAMEM_KEY, sizeof(SHAMEM), 0666 | IPC_CREAT );
    if( shmfd < 0 )
    {
        printf( "error:cannot apply a share memory area.\n" );
        return 1;
    }

    /* 将共享内存映射到进程地址空间 */
    sharemem = shmat( shmfd, 0, 0 );
    if( sharemem == (SHAMEM*)-1 )
    {
        printf( "error cannot add the share memory to this thread.\n" );
        shmctl( shmfd, IPC_RMID, 0 );
        return 1;
    }

    /* 初始化共享内存 */
    sharemem->flag = SHARE_MEMORY_FLAG_EMPT;
    memset( sharemem->data, 0, SHAMEM_DATA_SIZE );
    
    do
    {
        /* 如果共享内存为空,程序将会挂起5秒 */
        if( sharemem->flag == SHARE_MEMORY_FLAG_EMPT )
        {
            sleep( 5 );
            continue;
        }

        /* 读入共享内存中的数据,并清空共享内存 */
        memcpy( buffer, sharemem->data, SHAMEM_DATA_SIZE );
        memset( sharemem->data, 0, SHAMEM_DATA_SIZE );
        sharemem->flag = SHARE_MEMORY_FLAG_EMPT;

        printf( "read:%s\n", buffer );
    }
    while( memcmp( buffer, "close", 5 ) != 0 );

    /* 关闭工作 */
    shmdt( sharemem );
    shmctl( shmfd, IPC_RMID, 0 );

    return 0;
}

接下来完成书写程序,书写程序会要求用户输入数据,然后当共享内存状态为空时写入它。我们来新建一个shmem-writer.c文件:

#include 
#include 
#include 
#include 
#include 

#include "shmem_def.h"

int main()
{
    int shmfd;
    char buffer[SHAMEM_DATA_SIZE];
    SHAMEM* sharemem;

    /* 创建共享内存 */
    shmfd = shmget( SHAMEM_KEY, sizeof(SHAMEM), 0666 | IPC_CREAT );
    if( shmfd < 0 )
    {
        printf( "error:cannot apply a share memory area.\n" );
        return 1;
    }

    /* 将共享内存映射到进程地址空间 */
    sharemem = shmat( shmfd, 0, 0 );
    if( sharemem == (SHAMEM*)-1 )
    {
        printf( "error cannot add the share memory to this thread.\n" );
        shmctl( shmfd, IPC_RMID, 0 );
        return 1;
    }

    do
    {
        /* 如果共享内存为满,程序将会挂起5秒 */
        if( sharemem->flag == SHARE_MEMORY_FLAG_FULL )
        {
            sleep( 5 );
            continue;
        }

        /* 读取用户输入并写入共享内存 */
        printf( "Please input something to share memory:" );
        scanf( "%s", buffer );
        memcpy( sharemem->data, buffer, SHAMEM_DATA_SIZE );
        sharemem->flag = SHARE_MEMORY_FLAG_FULL;
    }
    while( memcmp( buffer, "close", 5 ) != 0 );
    
    /* 关闭工作 */
    shmdt( sharemem );
    shmctl( shmfd, IPC_RMID, 0 );

    return 0;
}

我们来编译一下这两个程序:

root@workspace-server:/home/root/workspace/shmem# gcc shmem-reader.c -o shmr
root@workspace-server:/home/root/workspace/shmem# gcc shmem-writer.c -o shmw

我们先来执行读取程序:

root@workspace-server:/home/root/workspace/shmem# ./shmr

可以看到程序卡住了,因为没有可读取的数据,现在我们另外启动一个窗口执行写入程序,然后尝试输入一些东西:

root@workspace-server:/home/root/workspace/shmem# ./shmw
Please input something to share memory:hello
Please input something to share memory:hi
Please input something to share memory:areyouok?
Please input something to share memory:close
root@workspace-server:/home/root/workspace/shmem# 

我们再回过头来看一下读取程序的输出:

root@workspace-server:/home/root/workspace/shmem# ./shmr
read:hello
read:hi
read:areyouok?
read:close
root@workspace-server:/home/root/workspace/shmem# 

可以看到完整的读取了我们输入的数据。这就可以在两个进程之间使用共享内存了。

(三)一个更健壮的共享内存例子

在上一个程序中我们完整的使用的共享内存,不过可以看出我们的使用方式其实非常不安全,比如我们需要依靠手动设置储存标志为空或者为满来决定共享内存的读写状态,这其实是非常危险的,因为设置标志的操作都不是原子操作。可能出现同时读取和写入共享内存的情况,导致数据丢失或系统崩溃。并且程序每过5秒检查的方法也会占用很大的CPU开销,所以我们现在来尝试着稍微改进一下我们的程序,使它看起来不能么笨拙了。

首先我们采用linux的互斥锁来进行访问限制,防止两个线程同时访问共享内存。在一个进程访问共享内存并上锁时,另一个想要访问的线程只能等待。

其次还记得我们曾经介绍过的信号嘛,我们采用信号来进行读写的同步工作,当写入程序向共享内存写入了一段数据后,他会向读取程序发送信号,告诉读取程序可以读取数据了。

那么首先我们要来更改一下我们的头文件,去除掉了共享内存结构中的标志位,和储存状态的枚举类型,加入了全局锁和信号ID,我们将它保存为"shmem_def2.h":

#ifndef _SHMEM_DEF2_H_
#define _SHMEM_DEF2_H_

#include 

#define SHAMEM_DATA_SIZE    (32)        /* 共享内存的大小 */
#define SHAMEM_KEY          (1112)      /* 共享内存的标志 */
#define SHAMEM_SIG          (63)        /* 读取信号 */

/* 全局互斥锁 */
pthread_mutex_t shm_lock = PTHREAD_MUTEX_INITIALIZER;

/* 共享内存结构 */
typedef struct share_memory
{
    char data[SHAMEM_DATA_SIZE];
}SHAMEM;

#endif /* _SHMEM_DEF2_H_ */

接下来是读取程序,我们需要重新声明一个信号接收函数print_mem(),即程序将在收到读取信号时调用它,而它的工作就是从共享内存读入数据打印在屏幕上,并清空共享内存。注意这是在接收到信号的之后的工作,而平时将不影响程序的主循环正常进行,我们将它保存为"shmem-reader2.c":

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "shmem_def2.h"

/* 声明信号接收函数 */
void print_mem( int sig );

SHAMEM* ShareMem;
int running;

int main()
{
    int shmfd;
    char buffer[SHAMEM_DATA_SIZE];
    struct sigaction act;
    struct sigaction oact;
    
    running = 1;

    /* 创建共享内存 */
    shmfd = shmget( SHAMEM_KEY, sizeof(SHAMEM), 0666 | IPC_CREAT );
    if( shmfd < 0 )
    {
        printf( "error:cannot apply a share memory area.\n" );
        return 1;
    }

    /* 将共享内存映射到进程地址空间 */
    ShareMem = shmat( shmfd, 0, 0 );
    if( ShareMem == (SHAMEM*)-1 )
    {
        printf( "error cannot add the share memory to this thread.\n" );
        shmctl( shmfd, IPC_RMID, 0 );
        return 1;
    }

    /* 初始化共享内存 */
    memset( ShareMem->data, 0, SHAMEM_DATA_SIZE );
    
    /* 注册信号接收函数 */
    act.sa_handler = print_mem;
    act.sa_flags = 0;
    sigemptyset( &act.sa_mask );
    sigaction( SHAMEM_SIG, &act, &oact );

    /* 程序主循环 */
    do
    {
        /* Anything */
    }
    while( running == 1 );

    /* 关闭工作 */
    shmdt( ShareMem );
    shmctl( shmfd, IPC_RMID, 0 );

    return 0;
}

/* 信号接收函数 */
void print_mem( int sig )
{
    char buffer[SHAMEM_DATA_SIZE];

    /* 读入共享内存中的数据,并清空共享内存 */
    pthread_mutex_lock( &shm_lock );
    memcpy( buffer, ShareMem->data, SHAMEM_DATA_SIZE );
    memset( ShareMem->data, 0, SHAMEM_DATA_SIZE );
    pthread_mutex_unlock( &shm_lock );

    printf( "read:%s\n", buffer );

    if( memcmp( buffer, "close", 5 ) == 0 )
    {
        running = 0;
    }

    return;
}

最后是写入程序,写入程序的改动不大,除了需要加上互斥锁以外,就是需要用户开头输入读取程序的进程pid,并在写入后向该进程发送信号,现在我们来完成它,保存为"shmem-writer2.c":

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "shmem_def2.h"

int main()
{
    int shmfd;
    int readthread_pid;
    char buffer[SHAMEM_DATA_SIZE];
    SHAMEM* sharemem;

    /* 创建共享内存 */
    shmfd = shmget( SHAMEM_KEY, sizeof(SHAMEM), 0666 | IPC_CREAT );
    if( shmfd < 0 )
    {
        printf( "error:cannot apply a share memory area.\n" );
        return 1;
    }

    /* 将共享内存映射到进程地址空间 */
    sharemem = shmat( shmfd, 0, 0 );
    if( sharemem == (SHAMEM*)-1 )
    {
        printf( "error cannot add the share memory to this thread.\n" );
        shmctl( shmfd, IPC_RMID, 0 );
        return 1;
    }

    /* 要求用户输入读取程序的进程pid */
    printf( "Please input reader thread's pid:" );
    scanf( "%d", &readthread_pid );
    fflush( stdin );
    
    do
    {
        /* 读取用户输入并写入共享内存 */
        printf( "Please input something to share memory:" );
        scanf( "%s", buffer );
        pthread_mutex_lock( &shm_lock );
        memcpy( sharemem->data, buffer, SHAMEM_DATA_SIZE );
        pthread_mutex_unlock( &shm_lock );
        
        /* 发送信号给读取程序 */
        kill( readthread_pid, SHAMEM_SIG );
    }
    while( memcmp( buffer, "close", 5 ) != 0 );

    /* 关闭工作 */
    shmdt( sharemem );
    shmctl( shmfd, IPC_RMID, 0 );

    return 0;
}

好了我们先来编译这两个文件:

root@workspace-server:/home/root/workspace/shmem# gcc shmem-reader2.c -o shmr2
root@workspace-server:/home/root/workspace/shmem# gcc shmem-writer2.c -o shmw2

然后还是先执行读取程序:

root@workspace-server:/home/root/workspace/shmem# ./shmr2

我们再另外启动一个终端,首先来查看一下读取程序的进程pid:

root@workspace-server:/home/root/workspace/shmem# ps aux|grep shmr2
root      1266 99.5  0.0   4380   720 pts/0    R+   23:15   0:40 ./shmr2
root      1268  0.0  0.0  21536  1088 pts/1    S+   23:15   0:00 grep --color=auto shmr2

可以看到读取程序的进程pid号是1266,现在我们启动写入程序,并输入读取程序的进程pid号:

root@workspace-server:/home/root/workspace/shmem# ./shmw2
Please input reader thread's pid:1266
Please input something to share memory:

没有问题,现在我们来输入一些数据:

root@workspace-server:/home/root/workspace/shmem# ./shmw2
Please input reader thread's pid:1266
Please input something to share memory:hello
Please input something to share memory:fuck
Please input something to share memory:no
Please input something to share memory:goodbye
Please input something to share memory:close
root@workspace-server:/home/root/workspace/shmem# 

然后再回过头来看一下读取程序的终端输入:

root@workspace-server:/home/root/workspace/shmem# ./shmr2
read:hello
read:fuck
read:no
read:goodbye
read:close
root@workspace-server:/home/root/workspace/shmem# 

可以看到也可以准确无误的读取我们输入的内容,并且不再像上一个程序那要有时需要等待5秒这样僵硬了。

你可能感兴趣的:(Linux程序设计)