再识 Linux 进程间通信底层原理:SystemV共享内存、消息队列与信号量

发布于:2025-08-08 ⋅ 阅读:(47) ⋅ 点赞:(0)

目录

1.共享内存

1.1.共享内存的概念

1.2共享内存使用指令

1.3共享内存相关函数

        shmget:

        ftok:

        shmat&shmdt:

        shmctl:

1.4共享内存优缺点

1.5共享内存使用注意

2.消息队列

2.1消息队列的概念

2.2消息队列使用指令

2.3消息队列相关函数

        msgget:

        msgsnd&msgrcv:   ​编辑

        msgctl:

3.信号量


1.共享内存

1.1.共享内存的概念

        为什么共享内存可以同管道一样作用与进程间的通信?本质上都是让进程可以看到同一份资源,可以是内核级的内存也可以是命名管道的文件。

        System V 共享内存:
        System V 共享内存允许不同的进程访问同一块物理内存区域,进程可以直接在这块内存中读写数据,而不需要进行繁琐的数据复制操作,这使得数据的传输速度非常快

        操作系统为了对共享内存进行管理,会先描述为 struct shmid_ds 结构体,再用数据结构组织起来,其中会存在引用计数(nattach)来代表共享内存被多少进程使用,后续会进一步讲解 struct shmid_ds 结构体。

        共享内存数据结构:

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

        为什么需要共享内存:

        进程间通过共享内存通信时,先由操作系统分配一块可共享的物理内存区域,映射到各进程的虚拟地址空间。进程直接读写该区域,无需数据拷贝,配合互斥锁等同步机制避免冲突,实现高效数据交换,尤其适合大容量、高频通信场景。

1.2共享内存使用指令

        查看共享内存:

ipcs -m + shmid(共享内存编号)

        删除共享内存:

ipcrm -q + shmid

1.3共享内存相关函数

        shmget:

        获取一个共享内存:shmget() 函数返回与参数 key 关联的 System V 共享内存段的标识符,而key是一个数字,key的值不重要,它在内核中具有唯一性,能够让不同的进程进行唯一性标识

        第一个进程可以通过key创建共享内存,第二个之后的进程只需要拿着同一个key就可以和第一个进程看到同一个共享内存;对于一个已经创建好的共享内存,key储存在共享内存的structXXX结构体中;

       而size是你要创建共享内存的大小,单位字节,shmflg是创建模式:

IPC_CREAT:不存在就创建,存在就返回,若未指定该标志,shmget() 仅查找与 key 关联的段,并检查用户是否有访问权限。

IPC_CREAT | IPC_EXCL:不存在就创建,存在就出错返回,需与 IPC_CREAT 结合使用的目的:确保当段已存在时操作失败,即确保此共享内存始终只有一份!

IPC_CREAT | IPC_EXCL | MODE:还可以设置创建文件权限,一般设为0666。

        如何来创建key?

        ftok:

        获取一个key:这里的 pathname  proj_id 可以是任意设定,如果创建key失败,那么更改一下即可,实质上是一套算法,对pathname和proj_id进行了数值计算

        shmat&shmdt:

        shmat链接共享内存:

        shmid:共享内存段的标识符(由 shmget() 返回)。

        shmaddr:指定附加地址(通常设为 NULL,由系统自动分配)。

        shmflg:标志位,常用值:SHM_RDONLY:以只读方式附加;或 0:以读写方式附加。

        返回值:成功时,shmat () 函数返回附加的共享内存段的地址;失败时,返回 (void *) -1,并设置 errno 以指示错误原因。

        shmdt来去关联共享内存:直接传入shmat返回的地址即可与共享内存去关联。

        shmctl:

        用来控制共享内存:

        shmid:共享内存段的标识符。
        cmd:命令类型,常用值:

  • IPC_STAT:获取共享内存段的状态信息,存入 buf。
  • IPC_SET:设置共享内存段的属性(如权限)。   
  • IPC_RMID:标记共享内存段为待删除状态(实际删除需所有进程分离后)。

        buf:指向 struct shmid_ds 的指针,用于存储或设置共享内存段的信息。(一般设为nullptr

1.4共享内存优缺点

        优点:
        速度快:避免了数据的多次复制,数据传输效率高。

        使用方便:进程可以像操作普通内存一样操作共享内存。

        缺点:
        同步问题:由于多个进程可以同时访问共享内存,可能会出现数据竞争的问题。比如,一个进程正在写入数据,另一个进程同时读取数据,可能会导致数据不一致。这就需要使用其他的同步机制,如信号量,来保证数据的一致性。

        管理复杂:需要手动管理共享内存的创建、连接、分离和删除,容易出现内存泄漏等问题。

1.5共享内存使用注意

        共享内存属于内核,进程终止它仍旧存在,这就是说 共享内存内部的数据,由用户自己维护!需要手动创建、链接、去关联、控制及释放;

        ②shmid是用户级的,key是内核级的,所以用户只需对shmid进行相关函数操作,无需考虑内核中的key;

        ③如果是进程异常退出导致的共享内存没有删除,共享内存的状态仍会设为dest,(待删除状态)【表示这块内存不能再被使用了】。

2.消息队列

2.1消息队列的概念

        消息队列通过内核维护一个有序的数据缓冲区(队列),实现进程间的异步通信。即在内核中创建一个队列,发送方将消息放入队列,接收方从队列中取出消息处理,所有的消息队列先描述为 msqid_ds 结构体再组织起来。

        进程可向队列中添加消息(如结构化数据),也可从队列中读取消息,消息按发送顺序存储和处理。无需接收方实时等待,发送方发送后即可继续执行,适合非实时、松耦合的通信场景,常见于分布式系统、异步任务处理等。

        消息队列数据结构:

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

2.2消息队列使用指令

        查看消息队列:

ipcs -q + msqid(共享内存编号)

        删除消息队列:

ipcrm -q + msqid

2.3消息队列相关函数

        msgget:


        创建消息队列:key与共享内存一样,由ftok生成(具体方式参考共享内存中ftok的使用)

        msgflg:使用与共享内存高度相似:

msgflg取值 描述
IPC_CRAET 创建共享内存时,如果共享内存已经已经存在,获取已经存在的共享内存;不存在则创建并返回
IPC_EXCL 需要与IPC_CREAT组合使用,单独使用没有意义。如果带创建的共享内存存在,则出错返回;如果不存在,则创建并返回对应的共享内存

        msgsnd&msgrcv:   

        msgsnd发送消息:其中,第二个参数需要传入一个形式如下所示的结构体,这个结构体需要用户自己定义:

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

       上述中的mtext表示要发送的数据,因而mtext的数组大小由用户自己指定;而mtype用于标识数据,例如:A进程只读取mtype为1的数据,B进程只读取mtype为2的数据,即mtype标识这个数据要发送给哪些进程。

        msqid:消息队列的标识符(由  msgget() 返回)。

        msgflg:标志位,常用值:IPC_NOWAIT:如果队列已满,不会阻塞等待,而是会返回错误码EAGAIN(设置在errno中);或 0:默认行为,阻塞等待消息队列,直到消息队列中有空间。

        msgrcv读取消息:与上面一样,同样需要自定义一下 struct msgbuf 结构体;

        msqid:消息队列的标识符(由  msgget() 返回)。

        msgsz:用于接收数据的msgbuf中的mtext的大小,即接收缓冲区的大小。

        msgflg:

msgflg 取值 含义
0 默认行为,阻塞等待消息队列,直到消息队列中有满足需求的数据
IPC_NOWAIT 非阻塞等待消息队列,如果队列中没有对应需求的数据,则 errno 设置为 ENOMSG,并返回
MSG_EXCEPT 接收队列中 msgbuf 中的 mtype 不为 msgtyp 的数据
MSG_NOERROR 如果数据的长度长于接收方的 msgbuf 的 mtext 大小,则会发生截断,余下数据被丢弃

        msgctl:

        msqid:消息队列的标识符。
        cmd:命令类型,常用值:

cmd取值 含义
IPC_STAT 获取消息队列的状态
IPC_SET 设置消息队列的属性
IPC_RMID 从系统中移除消息队列

        buf:指向 struct msqid_ds 的指针,用于存储或设置共享内存段的信息。(一般设为nullptr

3.信号量

        本篇信号量不是重点,简单了解即可,先引入问题:       

        对于共享内存来说,一个进程在写,一个进程在读,会引发数据不一致的问题,而管道因为原子写入是安全的,这时候需要加保护,也就是加锁——互斥访问,任何时候,只允许一个执行流访问共享资源,具有锁的资源——临界资源,而访问临界资源的代码——临界区。

        信号量就类似于一个计数器,当进程要访问临界资源需要申请计数器,tips:
1)申请计数器成功,就代表该进程具有访问资源的权限了;计数器减一;

2)申请了计数器资源,并不代表正在访问,是对临界资源的一种预订机制;

3)计数器可以有效保证进入共享资源的执行流的数量;

4)每一个执行流像访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源,如果申请成功,才能进行访问;

5)将1/0两态的计数器称为——锁,其实就是将临界资源当作整体看待,整体申请,整体释放;

6)信号量计数器也是共享资源,直接减一并不安全,自己的安全如何保证?通过原子性操作(使用CAS技术),完成申请——P操作,释放——V操作

7)信号量与其他执行流互相协同,虽然不直接传输数据,但是属于进程间通信

        简单介绍一下接口:
        int semget(key_t key, int nsems, int semflg);
        int semop(int semid, struct sembuf *sops, unsigned nsops);
        int semctl(int semid, int semnum, int cmd, ...);

补充: IPC在内核中数据结构的设计:

        不管是共享内存/消息队列/信号量,都是由XXXid_ds描述的,并且第一个成员都是 struct ipc_perm 类型,事实上,操作系统将 struct ipc_perm 用数组 struct ipc_perm* array[] 组织起来,长度固定,循环访问,下标对应 XXXid(shmid/msqid/semid),因为 struct ipc_perm 中保存着key,并且标志着 struct ipc_perm 属于哪种类型,以方便对 XXXid_ds 的成员进行访问。


网站公告

今日签到

点亮在社区的每一天
去签到