【Linux】进程通信篇Ⅱ:共享内存、消息队列、信号量


一、共享内存

我们知道,进程间通信的本质就是:让不同的进程,看到同一份资源

这里要介绍的同一份资源就是:内存块,即 共享内存(shared memory,简写为 shm)

共享内存的原理:
1.创建(key 和 共享内存)
2.关联进程 和 取消关联
3.释放共享内存

内存中的每块共享内存,会有一个 struct shm 结构体,里面放着共享内存的全部属性,OS 通过这个结构体建立链表关系来对所有的共享内存进行管理,就等于把管理 shm 的问题转化成了管理链表的问题。

故:

共享内存
=
共享内存的内核数据结构 (伪代码:struct shm)
+
真正开辟的内存空间

1.1 一些接口

1. shmget 函数:申请一个 system v 的共享内存块

头文件:

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

// umask的头文件如下
#include <sys/types.h>
#include <sys/stat.h>

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

参数 key:

  • 使用 ftok 函数设置的唯一标识码,他虽由用户设置,却是在内核中使用的


参数 size

  • 需要申请共享内存块的大小,单位为字节,不足 PAGE 页(4KB)时,会向上对齐到 PAGE 页


参数 shmflg:

  • 选项 IPC_CREAT and IPC_EXCL

  • 单独使用 IPC_CREAT:创建一个共享内存,如果共享内存不存在,就创建,如果已经存在就获取已经存在的共享内存并返回。

  • IPC_CREAT | IPC_EXCL :IPC_EXCL 必须要配合 IPC_CREAT 使用,创建一个共享内存,如果共享内存不存在,就创建,如果已经存在就出错返回
    意味着,一起使用时,如果创建成功,对应的shm,一定是最新的!

  • IPC_CREAT | IPC_EXCL | 0666 :上面的基础上,添加权限(可以配合函数 umask(0) 使用)


返回值:

  • 成功会返回一个共享内存标识符,失败返回 -1

2. ftok 函数:设置唯一标识码

头文件

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

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

参数 pathname

  • 用户设置的路径


参数 proj_id

  • 用户设置的项目 id


返回值:

  • 根据用户传入的参数,结合一定的算法,返回一个冲突概率很低的值。ket_t 就是一个 32 位的整数,是对 int 的封装

3. shmctl 函数:控制 system v 的共享内存块(可以删除、查看…)

头文件

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

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

参数 shmid

  • 需要的共享内存块的 shmid


参数 cmd:

  • 选项 IPC_STAT:把用户传入 shmid 的相应内核数据结构信息复制到 buf 中(在调用者有读权限的情况下,才能成功
  • 选项 IPC_RMID:删除 shmid 为传入值的共享内存块


输出型参数 buf:

  • 需要得到 ipc 信息时传一个相应类型的值用来接收结果


返回值:

  • 失败返回 -1,成功则根据 cmd 传入的选项返回对应的值
//The buf argument is a pointer to a shmid_ds structure, defined in <sys/shm.h> as follows:

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

//The ipc_perm structure is defined as follows (the highlighted fields  are  settable  using IPC_SET):

    struct ipc_perm {
        key_t          __key;    /* Key supplied to shmget(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 + SHM_DEST and
                                    SHM_LOCKED flags */
        unsigned short __seq;    /* Sequence number */
    };

4. shmat 函数:将进程与共享内存块 关联\ 挂接(attach)

头文件

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

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

参数 shmid

  • 需要的共享内存块的 shmid


参数 shmaddr:

  • 用户可以选择虚拟地址作为共享内存块的起始地址
  • 用户一般不定义,设为 nullptr 让 OS 自主定义即可


参数 shmflg:

  • 选项 SHM_RDONLY:只读
  • 0:可以读写


返回值:

  • 挂接成功,返回共享内存块的虚拟地址的起始地址

5. shmdt 函数:将进程与共享内存块 去关联(detach)

头文件

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

int shmdt(const void *shmaddr);

参数 shmaddr:

  • 共享内存块的起始地址


返回值:

  • 去关联成功返回 0,失败返回 -1

1.2 一些命令

1. ipcs - - 查看三种 ipc 资源

ipcs 就是进程间通信(ipc)资源

ipcs:可以查看 消息队列、共享内存亏块、信号量
-m:查看 共享内存块(memory)
-s:查看 信号量(semaphore)

perms:权限
nattach:当前 ipc 挂接的进程数

2. ipcrm – 删除某种 ipc 资源

ipcrm:删除一个 消息队列、共享内存亏块、信号量
-m:删除一个共享内存块,后接 shmid

1.3 结论

  1. 两个进程管道通信一次,需要进行两次复制。而共享内存间的通信,可以让进程们直接在自己映射的地址空间中访问,减少了拷贝次数()

  2. 管道单方面关闭读写端会有相应的保护,而共享内存没有保护机制(同步互斥)。管道通过系统接口通信,共享内存直接通信

  3. 互斥:任何一个时刻,都只允许一个执行流在进行共享资源的访问,叫做加锁

  4. 我们把任何一个时刻,都只允许一个执行流在进行访问的共享资源,叫做 临界资源。凡是访问临界资源的代码,叫做临界区,控制进出临界区的手段造就了临界资源。



二、消息队列

消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。

每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度。

1. msgget 函数:创建消息队列

头文件

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

int msgget(key_t key, int msgflg)

参数 key:

  • 使用 ftok 函数设置的唯一标识码


参数 msgflg:

  • 选项 IPC_CREAT and IPC_EXCL

  • 单独使用 IPC_CREAT:创建一个共享内存,如果共享内存不存在,就创建,如果已经存在就获取已经存在的共享内存并返回。

  • IPC_CREAT | IPC_EXCL :IPC_EXCL 必须要配合 IPC_CREAT 使用,创建一个共享内存,如果共享内存不存在,就创建,如果已经存在就出错返回
    意味着,一起使用时,如果创建成功,对应的shm,一定是最新的!

  • IPC_CREAT | IPC_EXCL | 0666 :上面的基础上,添加权限(可以配合函数 umask(0) 使用)


返回值:

  • 成功则返回消息队列的标识符

2. msgctl 函数

头文件

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

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

参数 msqid

  • 需要的消息队列的 msqid


参数 cmd:

  • 选项 IPC_STAT:把用户传入 msqid 的相应内核数据结构信息复制到 buf 中(在调用者有读权限的情况下,才能成功
  • 选项 IPC_RMID:删除 msqid 为传入值的共享内存块


输出型参数 buf:

  • 需要得到 ipc 信息时传一个相应类型的值用来接收结果


返回值:

  • 成功返回 >= 0 的值,失败返回 -1
The msqid_ds data structure is defined in <sys/msg.h> as follows:

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

The ipc_perm structure is defined as follows (the highlighted fields  are  settable  using
IPC_SET):

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

3. msgsnd 和 msgrcv 函数,发送和接收消息

头文件

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

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

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数 msqid:

  • 发送和接收访问的同一个消息队列

参数 msgp:

  • 发送或接收的数据块

参数 msgsz:

  • 发送或接收数据块的大小

参数 msgflg:

  • 选项,一般填 0 即可

参数 msgtyp:

  • msgbuf 里面的 mtype
// The msgp argument is a pointer to caller-defined structure of the following general form:

struct msgbuf {
    long mtype;       /* message type, must be > 0 */
    char mtext[1];    /* message data */
};


三、信号量

信号量 / 信号灯(semaphore),本质 就是一个计数器,是一个描述资源数量的计数器

举个例子:

  • 比如我们任何一个执行流,像访问临界资源中的一个子资源的时候,不能直接访问,需要 先申请信号量资源(P操作),此时 count-- 。只要申请信号量成功,未来就一定能拿到一个子资源。(类似摇号)

  • 然后进入进程自己的临界区,访问对应的临界资源。

  • 使用完成后,进程释放信号量资源(V操作),只要将计数器增加 count++,就表示将我们对应的资源进行了归还。

至此,进程通过执行代码来申请,意味着,所有进程都得先看到信号量,信号量就是一个共享资源。(信号量保护共享资源,自己却又是一个共享资源)

故,信号量必须保证自己的 ++ - - 是原子的
也,信号量被归类到了进程间通信

信号量部分未完待续~

1. semget

头文件

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

int semget(key_t key, int nsems, int semflg);

参数 key:

  • 使用 ftok 函数设置的唯一标识码,他虽由用户设置,却是在内核中使用的


参数 nsems:

  • 申请信号量的个数(叫做信号量集)


参数 semflg:

  • 选项 IPC_CREAT and IPC_EXCL

  • 单独使用 IPC_CREAT:创建一个共享内存,如果共享内存不存在,就创建,如果已经存在就获取已经存在的共享内存并返回。

  • IPC_CREAT | IPC_EXCL :IPC_EXCL 必须要配合 IPC_CREAT 使用,创建一个共享内存,如果共享内存不存在,就创建,如果已经存在就出错返回
    意味着,一起使用时,如果创建成功,对应的shm,一定是最新的!

  • IPC_CREAT | IPC_EXCL | 0666 :上面的基础上,添加权限(可以配合函数 umask(0) 使用)


返回值:

  • 成功会返回一个信号量计数器标识符,失败返回 -1

2. semctl 函数:可以直接控制信号量信息

头文件

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

int semctl(int semid, int semnum, int cmd, ...);

参数 semid:

  • 需要的信号量的semid


参数 semnum:

  • 信号量编号,除非为一组信号量,否则为 0


参数 cmd:

  • SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过 union semun 中的 val 成员设置,其作用是在信号量第一次使用前对它进行设置。
  • IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。


返回值:

  • 成功返回 >= 0 的值,失败返回 -1

他有三个或四个参数,如果有第四个参数,它通常是一个union semum结构,定义如下:

//This  function  has  three  or four arguments, depending on cmd.  When there are four, the
//fourth has the type union semun.  The calling program must define this union as follows:

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

//The semid_ds data structure is defined in <sys/sem.h> as follows:

    struct semid_ds {
        struct ipc_perm sem_perm;  /* Ownership and permissions */
        time_t          sem_otime; /* Last semop time */
        time_t          sem_ctime; /* Last change time */
        unsigned long   sem_nsems; /* No. of semaphores in set */
    };

//The ipc_perm structure is defined as follows (the highlighted fields  are  settable  usingIPC_SET):

    struct ipc_perm {
        key_t          __key; /* Key supplied to semget(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 */
    };

3. semop 函数:对信号量进行修改操作

头文件

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

int semop(int semid, struct sembuf *sops, unsigned nsops);

参数 sops:

  • 需要用户自己定义后传入,设置结构体内容,以此达到对信号量的 PV 操作等等
//Each semaphore in a System V semaphore set has the following associated values:

    unsigned short  semval;   /* semaphore value */
    unsigned short  semzcnt;  /* # waiting for zero */
    unsigned short  semncnt;  /* # waiting for increase */
    pid_t           sempid;   /* ID of process that did last op */

//semop() performs operations on selected semaphores in the set indicated by semid.  
//Each of the nsops elements in the array pointed to by sops specifies an operation to be  performed on a single semaphore.  
//The elements of this structure are of type struct sembuf, containing the following members:

struct sembuf{
    short sem_num; // 除非使用一组信号量,否则它为0
    short sem_op;  // 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                   // 一个是+1,即V(发送信号)操作。
    short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,
                   // 并在进程没有释放该信号量而终止时,操作系统释放信号量
};


总结:

共享内存、消息队列、信号量 这三种 ipc 都有各自的内核数据结构体,而结构体的第一个成员都是 struct ipc_perm xx_perm

可以理解为 OS 将这三种 ipc 都放进了一个 struct ipc_perm* ipc_id_arr[] 指针数组中进行管理(这里只是做理解解释,实际上更复杂)。

OS 通过指针,可以找到每个结构体(同时也是每个结构的第一个成员,即 struct ipc_perm xx_perm),在其中找到 key 值就可以确定。

要访问里面的内容时,以共享内存举例

((struct shmid_ds*)ipc_id_arr[n])->other...

对指针进行强转,就可以访问到其中内容了,这也是一种多态。


🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值