Linux 系统编程 学习:04-进程间通信2:System V IPC(1)

Linux 系统编程 学习:04-进程间通信2:System V IPC(1)

背景

上一讲 进程间通信:Unix IPC-信号中,我们介绍了Unix IPC中有关信号的概念,以及如何使用。

IPC的方式通常有:

  • Unix IPC包括:管道(pipe)、命名管道(FIFO)与信号(Signal)
  • System V IPC:消息队列、信号量、共享内存
  • BSD套接字:Socket(支持不同主机上的两个进程IPC)

我们在这一讲介绍System V IPC:消息队列、共享内存、信号量(下一讲)

知识

System V IPC:

  • V 指的是第五代
  • IPC (线程间通信, Inter-Process Communication)

其中包括:

  • 消息队列:增强型的管道,发送的信息是可以包含特殊标记的(mtype)。消息队列支持不同消息的筛选接收,按先进先出的方式去读取。

由于在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达MySQL,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发 too many connections 错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。

  • 共享内存:在内核空间中开辟一片内存,并且将其映射到虚拟内存中使用。

所有进程都可以共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。就像访问进程独有的内存区域一样快,并不需要通过系统调用或其他需要切入内核的过程来完成。同时还能避免对数据的各种不必要的复制。

  • 信号量:一个定义在内核系统中的特殊的变量

信号量及信号量上的操作是E.W.Dijkstra在1965年提出的一种解决同步、互斥问题的较通用的方法,并在很多操作系统中得以实现, Linux改进并实现了这种机制。

在shell脚本中,支持了对于IPC使用情况 的查看与删除

ipcs -a:查看所有IPC对象
    -q,显示当前系统中  消息队列  的使用
    -m,显示当前系统中  共享内存  的使用
    -s,显示当前系统中  信号量    的使用

删除某个IPC对象:

ipcrm -m id号  #删除指定id号的共享内存
    -q,删除当前系统中 消息队列 的标识符
    -m,删除当前系统中 共享内存 的标识符
    -s,删除当前系统中 信号灯集 的标识符

使用System V IPC通信
使用System V IPC通信具体步骤:
1)使用ftko创建key
2)通过创建出来的key进行不同类型的通信
下面进行详细说明。

使用ftko创建key

描述:创建一个IPC对象所使用的key键值。

基于ftok创建的key值需要相同,而亲缘进程通信可不使用ftok。

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

参数解析:

  • pathname:指定一个存在的路径
  • proj_id :指定一个任意的ID号(只用到了8位)

返回值:
成功:返回key值
失败:返回 -1,设置errno(与stat函数有关)

例程:

    key_t key;
    key = ftok("/tmp", 13);
    if(key == -1)
    {
        perror("ftok");
        return -1;
    }
    printf("%d\n", key);

ftok 注意事项

ftok是否一定会产生唯一的key值?

系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。

在一般的UNIX实现中,是将pathname的索引节点号取出(查询文件索引节点号的方法是: ls -i),前面加上子序号proj_id得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的proj_id值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。

但当删除重建文件后,索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同。

在实际应用中,很容易产生的一个理解是,在proj_id相同的情况下,只要文件(或目录)名称不变,就可以确保ftok返回始终一致的键值。然而,这个理解并非完全正确,有可能给应用开发埋下很隐晦的陷阱。

因为ftok的实现存在这样的风险:即在访问同一共享内存的多个进程先后调用ftok函数的时间段中,如果pathname指定的文件(或目录)被删除且重新创建,则文件系统会赋予这个同名文件(或目录)新的i节点信息,于是这些进程所调用的ftok虽然都能正常返回,但得到的键值却并不能保证相同。

由此可能造成的后果是,原本这些进程意图访问一个相同的共享内存对象,然而由于它们各自得到的键值不同,实际上进程指向的共享内存不再一致;如果这些共享内存都得到创建,则在整个应用运行的过程中表面上不会报出任何错误,然而通过一个共享内存对象进行数据传输的目的将无法实现。

所以如果要确保key_t值不变,要么确保ftok的文件不被删除(可以用mv移动),要么不用ftok,指定一个固定的key_t值。

另外:创建进程在通知其他进程挂接的时候,建议不使用ftok方式来获取Key,而使用文件或者进程间通信的方式告知。

通过创建出来的key进行不同类型的通信

消息队列(Message queue)

权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。
消息队列是随内核持续的。如果不自主去释放资源,消息队列是会悄无声息的存在的。

创建消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg); // 创建一个消息队列。(消息队列的数量会受到系统消息队列数量的限制)

参数解析:
key :

  • 可填入由ftok函数创建而来的#key 。
  • 如果是父子关系的进程间通信的话,这个标识符用IPC_PRIVATE来代替。(即不使用ftok) 则自动产生一个随机的新key键值

msgflg :

  • IPC_CREAT :如果该KEY对应IPC 不存在,则创建。创建时可以用“|0XXX”给定具体的访问权限。(执行权限默认无效)
  • IPC_EXCL :如果该KEY对应IPC 存在,则报错。

返回值:

  • 成功:该消息队列的ID
  • 失败:-1

消息队列的控制

在使用消息队列收发之前应该用msgctl设置好属性。

msgctl命令只有下列两种进程可以执行: 有效用户ID等于msg_perm.cuid或msg_per.uid、具有超级用户特权的进程.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

struct msqid_ds {
▲  struct ipc_perm msg_perm;     /*   权限相关信息            */
   time_t          msg_stime;    /*   最后一次发送消息的时间   */
   time_t          msg_rtime;    /*   最后一次接收消息的时间   */
   time_t          msg_ctime;    /*   最后一次状态变更的时间   */
   unsigned long   __msg_cbytes; /*   当前消息队列中数据尺寸   */
   msgqnum_t       msg_qnum;     /*   消息队列中的消息个数     */
▲  msglen_t        msg_qbytes;   /*   消息队列中的最大数据尺寸  */
   pid_t           msg_lspid;    /*   最后一个发送消息的PID    */
   pid_t           msg_lrpid;    /*   最后一个接收消息的PID    */
};

struct ipc_perm {
   key_t          __key;       /* Key supplied to msgget(2) */
▲  uid_t          uid;         /* Effective UID of owner */
▲  gid_t          gid;         /* Effective GID of owner */
   uid_t          cuid;        /* Effective UID of creator */
   gid_t          cgid;        /* Effective GID of creator */
▲  unsigned short mode;        /* Permissions */
   unsigned short __seq;       /* Sequence number */
};

参数解析:
msqid :对应的消息队列ID(可由 msgget 函数得到消息队列ID)
cmd :传入的消息队列结构体地址(有固定格式)

  • IPC_STAT:获得msgid的消息队列头数据到buf中(必须具有读取权限)
  • IPC_SET:设置消息队列的属性,属性需先存储在buf中,可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes
  • IPC_RMID:立即删除消息队列,并且唤醒阻塞在本消息队列中的所有进程;忽略第三个参数
  • IPC_INFO (Linux-specific)、MSG_INFO (Linux-specific)、MSG_STAT (Linux-specific) 这些命令我不再展开。

buf:消息队列管理结构体

返回值:IPC_STAT、IPC_SET、IPC_RMID成功返回0,失败返回-1,错误代码如下:

  • EACCESS:参数cmd为IPC_STAT,确无权限读取该消息队列
  • EFAULT:参数buf指向无效的内存地址
  • EIDRM:标识符为msqid的消息队列已被删除
  • EINVAL:无效的参数cmd或msqid
  • EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行

发送与接收消息

在消息队列中,消息是先进先出的。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

#define _GNU_SOURCE // msgflg 有一些 flag 需要使用这个宏

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 多了一个参数 msgtyp

/* 参考的结构体 */
struct msgbuf {
    long mtype;      /* 消息类型     */
    char mtext[1];   /* 所发送的消息 */
};

参数解析:
msqid :对应的消息队列ID(可由 msgget 函数得到消息队列ID)
msgp :传入的消息队列结构体地址(有固定格式)
msgsz :发送消息的长度(应该与#msgp.mtext定义的长度保持一致)
msgtyp :(msgrcv函数用的)指定消息队列中消息的类型

  • 如果 msgtyp = 0,读出消息队列中第一条消息
  • 如果 msgtyp >0,接收类型等于msgtyp的第一个消息
  • 如果 msgtyp <0,接收类型等于或者小于msgtyp绝对值的第一个消息

msgflg :

  • IPC_NOWAIT 非阻塞地读出、写入(当消息队列已满的时候,msgsnd函数不等待立即返回)
  • MSG_COPY (since Linux 3.8)

在msgtyp指定的队列中的序号位置,以非破坏性方式获取消息的副本(消息被视为从0开始编号)。
MSG_COPY必须与IPC_NOWAIT一起指定;如果给定位置没有可用的消息,则调用将立即失败,并出现错误ENOMSG。
MSG_COPY不能和MSG_EXCEPT同时指定,因为它们的意义是互斥的。
当内核配置了 ONFIG_CHECKPOINT_RESTORE 选项时MSG_COPY才可用。

  • MSG_EXCEPT 读取标识不等于 msgtype 的第一个消息
  • MSG_NOERROR 若长度大于 #msgsz 字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。

返回值:失败时,收发函数都返回-1,errno表示错误;成功时,msgsnd()返回0、msgrcv()返回实际复制到的字节数。
成功返回0;失败返回-1。并设置错误码:

  • E2BIG:消息数据长度大于msgsz而msgflag没有设置IPC_NOERROR
  • EIDRM:标识符为msqid的消息队列已被删除
  • EACCESS:无权限读取该消息队列
  • EFAULT:参数msgp指向无效的内存地址
  • ENOMSG:参数msgflg设为IPC_NOWAIT,而消息队列中无消息可读
  • EINTR:等待读取队列内的消息情况下被信号中断

在不使用 IPC_NOWAIT 的情况下,msgrcv()解除阻塞的条件有以下三个:

消息队列中有了满足条件的消息。
msqid代表的消息队列被删除。
调用msgrcv()的进程被信号中断。

我们注意到,在函数原型中有的默认结构体mtext虽然定义为char类型,并不代表消息只能是一个字符,消息内容可以为任意类型,由用户根据需要定义。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

typedef struct _mydata {
    /* data */
    char name[16];
    char text[48];
}mydata;

typedef struct _my_msgbuf {
        long mtype;                     /* 消息类型,必须大于0 */
        mydata data;                    /* 所发送的消息 */
} my_msgbuf;

int main(int argc, char *argv[])
{
    key_t key;
    int msqid;
    struct msgbuf msgbuffer_w;
    struct msgbuf msgbuffer_r;
    struct msqid_ds buf;
    my_msgbuf buffer_w;
    my_msgbuf buffer_r;
    int ret, i;

#ifdef _GNU_SOURCE
    printf("系统预定义的 msgbuf有关大小:\n");
    printf("msgbuf = %ld\n", sizeof(msgbuffer_r));
    printf("msgbuf.mtype = %ld\n", sizeof(msgbuffer_r.mtype));
    printf("msgbuf.mtext = %ld\n", sizeof(msgbuffer_r.mtext));
#endif

    // 创建key
    key = ftok("/tmp", 2);
    // 创建 消息队列
    msqid = msgget(key, IPC_CREAT | 0666);
    printf("msqid = %d\n", msqid);

    ret = msgctl(msqid, IPC_STAT, &buf);
    if( ret ) perror("megctl, IPC_STAT");
    printf("获取到的 msqid_ds info:\n");
    printf("消息队列中的最大数据尺寸  %ld\n", buf.msg_qbytes);
    printf("消息队列中的消息个数      %ld\n", buf.msg_qnum);
    system("ipcs -q");

    buf.msg_qbytes = 10 * sizeof(mydata);
    ret = msgctl(msqid, IPC_SET, &buf);
    if( ret ) perror("megctl, IPC_SET");
    printf("[%d] Get msgid info \n", ret);

    ret = msgctl(msqid, IPC_STAT, &buf);
    if( ret ) perror("megctl, IPC_STAT");
    printf("获取到的 msqid_ds info:\n");
    printf("消息队列中的最大数据尺寸  %ld\n", buf.msg_qbytes);
    printf("消息队列 当前 消息个数      %ld\n", buf.msg_qnum);

    printf("\n\n\n\n");
    for (i = 0; i < 5; ++i) {
        printf("向消息队列中发送了消息\n");
        // 发送消息
        buffer_w.mtype = 0xa;
        sprintf(buffer_w.data.name, "%c", 'a' + i);
        sprintf(buffer_w.data.text, "hello custom data 1 %d", i);
        //ret = msgsnd(msqid, &msgbuffer_w, sizeof(msgbuffer_r.mtext), IPC_NOWAIT);
        ret = msgsnd(msqid, &buffer_w, sizeof(buffer_w.data), IPC_NOWAIT);
        if(ret ==-1) perror("msgsnd");

        buffer_w.mtype = 0x2;
        sprintf(buffer_w.data.name, "%c", 'a' + i);
        sprintf(buffer_w.data.text, "hello custom data 2 %d", i);
        //ret = msgsnd(msqid, &msgbuffer_w, sizeof(msgbuffer_r.mtext), IPC_NOWAIT);
        ret = msgsnd(msqid, &buffer_w, sizeof(buffer_w.data), IPC_NOWAIT);
        if(ret ==-1) perror("msgsnd");
    }

    // 获取剩下所有的 消息
    ret = msgctl(msqid, IPC_STAT, &buf);
    if( ret ) perror("megctl, IPC_STAT");
    printf("获取到的 msqid_ds info:\n");
    printf("消息队列 当前 消息个数      %ld\n", buf.msg_qnum);

    printf("\n\n\n\n");
    // return 0;
    printf("接收消息[由于 mtype 不一致,导致读取失败]\n");
    memset(&buffer_r, 0, sizeof(msgbuffer_r));
    ret = msgrcv(msqid, &buffer_r, sizeof(buffer_r.data), 0xc, IPC_NOWAIT );//| MSG_EXCEPT);
    if (ret == -1)  perror("msgrcv");
    printf("msgrcv ret = %d\n", ret);

    // 接收所有的消息
    for (i = 0; i < buf.msg_qnum; ++i) {
        memset(&buffer_r, 0, sizeof(msgbuffer_r));
        ret = msgrcv(msqid, &buffer_r, sizeof(buffer_r.data), 0, IPC_NOWAIT);
        if (ret != 0 && ret != sizeof(buffer_r.data))
        {
            perror("msgrcv");
        }
        printf("[%s]:[%s]\n", buffer_r.data.name, buffer_r.data.text);
    }

    // 删除消息队列
    ret = msgctl(msqid, IPC_RMID, NULL);
    if( ret ) perror("megctl, IPC_RMID");
    system("ipcs -q");

    return 0;
}

共享内存(Share memory)

在Linux中,每个进程都有自己的PCB(进程控制块)和地址空间,并且都有一个对应的页表,负责将进程的虚拟内存和物理内存进行映射,通过MMU来管理。

共享内存允许两个或更多进程访问同一块内存。当一个进程改变了这块内存中的内容的的时候,其他进程都会察觉到这个更改。
共享内存块,被映射到不同进程的地址空间内,从而实现了高效率的资源共享。但是系统内核没有对访问共享内存的进程进行同步机制,如果有这方面的需求,比如需要不同进程进程对共享内存区进行读写操作,则必须要提供自己的同步措施,保护临界资源。通常我们使用信号量机制实现进程间同步与互斥。

共享内存一般不单独使用,而是要配合信号量,互斥锁等协调机制。
http://blog.csdn.net/a1414345/article/details/69389647

http://blog.chinaunix.net/uid-25365622-id-3054027.html

如何防范共享内存陷阱:http://blog.csdn.net/ydyang1126/article/details/52804268

mmap和shm的区别

1、mmap是在磁盘上建立一个文件,每个进程地址空间中开辟出一块空间进行映射。而对于shm而言,shm每个进程最终会映射到同一块物理内存。shm保存在物理内存,这样读写的速度要比磁盘要快,但是存储量不是特别大。
2、相对于shm来说,mmap更加简单,调用更加方便,所以这也是大家都喜欢用的原因。
3、另外mmap有一个好处是当机器重启,因为mmap把文件保存在磁盘上,这个文件还保存了操作系统同步的映像,所以mmap不会丢失,但是shmget就会丢失。

创建共享内存

#include <sys/ipc.h>
#include <sys/shm.h>

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

参数解析:
key :

  • 可填入由ftok函数创建而来的#key 。
  • 如果是父子关系的进程间通信的话,这个标识符用IPC_PRIVATE来代替。(即不使用ftok) 则自动产生一个随机的新key键值

size:

  • 新建共享内存的大小,一般填写 1024 的整数倍(以字节为单位);获取已存在的共享内存块标识符时,该参数为0。

shmflg :(可以相或)

  • mode :共享内存的访问权限
  • IPC_CREAT :如果该KEY对应IPC 不存在,则创建。创建时可以用“|0XXX”给定具体的访问权限。(执行权限默认无效)
  • SHM_HUGELTB (since Linux 2.6): 使用"大页面"来分配共享内存
  • SHM_NORESERVE (since Linux 2.6.15): 不在交换分区中为这块内存保留空间,因此当物理内存不足的时候,对该共享内存执行写操作将触发SIGSEGV信号
  • SHM_HUGE_2MB, SHM_HUGE_1GB (since Linux 3.8):参考有关的手册
  • IPC_EXCL :与IPC_CREAT 连用;确保此调用创建段;如果该KEY对应IPC 存在,则报错。

当多个进程都能创建共享内存的时候,如果key出现相同的情况,并且一个进程需要创建的共享内存的大小要比另外一个进程要创建的共享内存小,共享内存大的进程先创建共享内存,共享内存小的进程后创建共享内存,小共享内存的进程就会获取到大的共享内存进程的共享内存, 并修改其共享内存的大小和内容,从而可能导致大的共享内存进程崩溃。

返回值:

  • 成功:该共享内存的ID
  • 失败:-1

关联共享内存

共享内存被创建、共享之后,我们不能立即访问它,而是需要先使用shmat将它关联到进程的地址空间中。使用完共享内存后,我们也需要调用shmdt将它从进程的地址空间中分离。

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

shmat

描述:共享内存标识符为shmid的内存,成功后把共享内存区对象连接(attach)到到调用进程的地址空间shmaddr上。

参数解析:
shmid:由 shmget 调用返回的共享内存标识符
shmaddr:

  • NULL:由系统自动分配(建议选择此项,以增强可移植性)
  • 非空且 shmflg指定了SHM_RND(圆整):连接的地址会自动向下调整为SHMLBA(低边界地址的倍数)的整数倍。公式:shmaddr - (shmaddr % SHMLBA);
  • 非空且 shmflg未指定SHM_RND(圆整):以shmaddr为连接地址。此时,shmaddr需要是一个页对齐地址。

shmflg:

  • SHM_EXEC (Linux-specific; since Linux 2.6.9):允许这片内存被执行。(调用者需要由执行权限)
  • SHM_RDONLY :只读权限(没有只写权限;默认是可读可写)
  • SHM_REMAP (Linux-specific):如果之前这片内存已经被分配了,那么就重新映射,shmaddr不能为空。(通常,如果此地址范围中已存在映射而没有指定SHM_REMAP ,则会导致EINVAL错误。)

返回值:成功返回共享内存的地址;失败返回-1,设置errno。

(参考 shmctl 函数原型有关结构体)shmat成功时,将修改内核数据结构shmid_ds的部分字段:将shm_nattach加1、将shm_Ipid设置为调用进程的PID、将shm_atime设置为当前的时间。

shmdt

描述:shmdt将关联到shm_addr处的共享内存从进程空间中分离(detach)。

shmaddr:由 shmat 调用成功返回的共享内存地址

参数解析:
shmat: 由shmat调用成功时的共享内存地址。

返回值:

  • 成功时,返回0,当前进程与之前被关联到的共享内存地址分离;
  • 失败返回-1,且设置errno。

分离时,将修改内核数据结构shmid_ds的部分字段:(参考 shmctl 函数原型有关结构体)将shm_nattach减1、将shm_lpid设置为调用进程的PID。当shm_nattach为0时,内核才从物理上删除这个共享段

控制一片共享内存

对共享内存区的控制是通过函数shmctl来完成的。读取或者设置共享内存有关属性(完成对共享内存的控制)

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);


/* 需要使用到以下结构体 */
struct shmid_ds
{
   struct ipc_perm  shm_perm;        //操作许可,里面包含共享内存的用户ID、组ID等信息
   int  shm_segsz;                   //共享内存段的大小,单位为字节
   __kernel_time_t  shm_atime;       //最后一个进程访问共享内存的时间
   __kernel_time_t  shm_dtime;       //最后一个进程离开共享内存的时间
   __kernel_time_t  shm_ctime;       //最后一次修改共享内存的时间
   __kernel_ipc_pid_t  shm_cpid;     //创建共享内存的进程ID
   __kernel_ipc_pid_t  shm_lpid;     //最后操作共享内存的进程ID
   ushort  shm_nattch;               //当前使用该共享内存段的进程数量
   ushort  shm_unused;
   void  *shm_unused2;
   void  *shm_unused3;
};

参数解析:
shmid:由 shmget 调用返回的共享内存标识符
cmd:控制命令

cmd :

  • IPC_STAT 获取该共享内存信息,储存在#buf中
  • IPC_SET 设置共享内存的状态,把#buf中的uid、gid、mode复制到共享内存的shmid_ds结构内(只有对于该共享内存有读取权限的进程才能够执行)
  • IPC_RMID标记该共享内存即将被删除(当所有进程都与这个共享内存分离之后,这个共享内存才会真正被删除。)
  • IPC_IFNO(Linux-specific) 获取关于共享内存的系统限制值信息
  • SHM_INFO(Linux-specific)
  • SHM_STAT(Linux-specific) :形如IPC_STAT,但是shmid为该SHM在内核中记录所有SHM信息的数组下标,因此通过迭代所有下标可以获得系统中所有SHM的相关信息( 只要使用计次循环自减就可以得到所有结果了 )
  • SHM_LOCK(Linux-specific) 禁止这片共享内存交换到swap分区(如果在设置前就已经被交换到swap分区,则任何进程对其的访问都将遇到页错误)
  • SHM_UNLOCK(Linux-specific) 允许这片共享内存交换到swap分区

buf:属性信息结构体

返回值:
成功时:

  • cmd 为 IPC_INFO 或 SHM_INFO 或 ,内核中记录所有SHM信息数组的下标的最大值( 只要使用计次循环自减就可以得到所有结果了 )
  • cmd 为 SHM_STAT 时,返回值为shmid的共享内存ID
  • 其他操作返回0

失败返回-1,且设置errno:

  • EACCESS:参数cmd为IPC_STAT,确无权限读取该共享内存
  • EFAULT:参数buf指向无效的内存地址
  • EIDRM:标识符为msqid的共享内存已被删除
  • EINVAL:无效的参数cmd或shmid
  • EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行

#### 共享内存的使用
> 对于共享内存的操作方式:(其实就是一片内存地址,对于内存怎么操作,就怎么使用它就好)

#### 共享内存注意事项
**shmctl(IPC_RMID) 与 shmdt 先后顺序问题**

如果共享内存已经与所有访问它的进程断开了连接,则调用IPC_RMID子命令后,系统将立即删除共享内存的标识符,并删除该共享内存区,以及所有相关的数据结构;
如果仍有别的进程与该共享内存保持连接,则调用IPC_RMID子命令后,该共享内存并不会被立即从系统中删除,而是被设置为IPC_PRIVATE状态,并被标记为"已被删除"(使用ipcs命令可以看到dest字段);直到已有连接全部断开,该共享内存才会最终从系统中消失。

需要说明的是:一旦通过shmctl对共享内存进行了删除操作,则该共享内存将不能再接受任何新的连接,即使它依然存在于系统中!所以,可以确知,在对共享内存删除之后不可能再有新的连接,则执行删除操作是安全的;否则,在删除操作之后如仍有新的连接发生,则这些连接都将可能失败!

**多次shmat会有什么样的问题:**
一个进程是可以对同一个共享内存多次 shmat进行挂载的,物理内存是指向同一块,如果shmaddr为NULL,则每次返回的线性地址空间都不同。而且指向这块共享内存的引用计数会增加。也就是进程多块线性空间会指向同一块物理地址。这样,如果之前挂载过这块共享内存的进程的线性地址没有被shmdt掉,即申请的线性地址都没有释放,就会一直消耗进程的虚拟内存空间,很有可能会最后导致进程线性空间被使用完而导致下次shmat或者其他操作失败。

##### 父子进程通信范例:
```c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <error.h>
#define SIZE 1024

int main()
{
    int shmid;
    char *shmaddr;
    struct shmid_ds buf;
    int flag = 0;
    int pid;

    shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600 );

    if ( shmid < 0 )
    {
        perror("get shm  ipc_id error");
        return -1;
    }

    pid = fork();
    if ( pid == 0 )
    {
        shmaddr = (char *)shmat( shmid, NULL, 0 );
        if ( (int)shmaddr == -1 )
        {
            perror("shmat addr error");
            return -1;
        }

        strcpy( shmaddr, "Hi, I am child process!\n");
        shmdt( shmaddr );

        return  0;
    } else if ( pid > 0) {
        sleep(3 );
        flag = shmctl( shmid, IPC_STAT, &buf);

        if ( flag == -1 )
        {
            perror("shmctl shm error");
            return -1;
        }


        printf("shm_segsz =%d bytes\n", buf.shm_segsz );
        printf("parent pid=%d, shm_cpid = %d \n", getpid(), buf.shm_cpid );
        printf("chlid pid=%d, shm_lpid = %d \n",pid , buf.shm_lpid );

        shmaddr = (char *) shmat(shmid, NULL, 0 );
        if ( (int)shmaddr == -1 )
        {
            perror("shmat addr error");
            return -1;
        }

        printf("%s", shmaddr);
        shmdt( shmaddr );
        shmctl(shmid, IPC_RMID, NULL);
    }else{
        perror("fork error");
        shmctl(shmid, IPC_RMID, NULL);
    }

    return 0;
}
posted @ 2019-03-18 17:14  schips  阅读(293)  评论(0编辑  收藏  举报