前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux进程间通信之System V

Linux进程间通信之System V

作者头像
咬咬
发布2024-06-12 14:02:11
1020
发布2024-06-12 14:02:11
举报
文章被收录于专栏:学习笔记

目录

认识system V:

system V共享内存:

共享内存的基本原理:

共享内存的数据结构:

共享内存的建立与释放:

共享内存的建立:

共享内存的释放:

共享内存的关联:

共享内存的去关联:

用共享内存实现serve&client通信:

system V消息队列:

消息队列基本原理:

消息队列数据结构:

消息队列的创建:

消息队列的释放:

向消息队列发送数据:

从消息队列获取数据:

system信号量:

信号量相关概念:

信号量数据结构:

信号量集的创建:

信号量集的删除:

信号量集的操作:

进程互斥


认识system V:

对于进程间通信,想必管道大家再熟悉不过了,对于管道这种通信方式,其实是对底层代码的一种复用,linux工程师借助类似文件缓冲区的内存空间实现了管道,其实也算偷了一个小懒,随着linux的发展,linux正式推出了System V来专门进行进程间通信,它和管道的本质都是一样的,都是让不同的进程看到同一份资源。

system V通信的3种通信方式:

1.system V共享内存 () 2.system V消息队列 () 3.system V信号量 ()

上述中的共享内存和消息队列主要用于传输数据,而信号量则是用于保证进程间的同步与互斥,虽然看起来信号量和通信没关联,但其实它也属于通信的范畴。

system V共享内存:

共享内存的基本原理:

之前说的到了通信的原理都是让不同的进程看到同一份资源,共享内存让进程看到同一份资源的方法就是,在物理内存中申请一块空间,名为共享内存,然后让这块空间与需要通信的进程的页表建立映射,再在进程的虚拟地址的栈区和堆区中间的共享区,开辟一段空间,将该空间的地址页表对应的位置,这样虚拟地址就和物理地址建立了联系,让不同的进程看到了同一份资源。

注意:这里说的开辟物理空间和建立页表映射关系,都是由操作系统来完成。

共享内存的数据结构:

系统中可能不止一对进程需要通信,一块共享内存只能支持两个进程通信,所以操作系统是支持申请多个共享内存的,而多个共享内存被操作系统管理,所以操作系统中一定有管理共享内存的内核数据结构:

代码语言:javascript
复制
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};

当我们申请一块共享内存,system V为了能让不同的进程看到这块共享内存,每个共享内存申请时都会有一个key值,用于系统标志这块共享内存的唯一性。

可以看到上面共享内存数据结构中,第一个成员是shm_permshm_perm是一个ipc_perm类型的结构体变量,ipc_perm中存放了每个共享内存的key,ipc_perm的结构如下:

代码语言:javascript
复制
struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

共享内存的建立与释放:

共享内存的建立大致为以下两步:

1.在物理空间中开辟一块共享内存空间。 2.将该物理空间与进程虚拟地址空间通过页表建立映射关系。(挂载)

共享内存的释放大致为以下两步:

1.将该物理空间和进程虚拟地址空间取关联,取消页表映射。(去挂载) 2.释放共享空间,将物理内存还给操作系统。

共享内存的建立:

共享内存的建立需要使用smhget函数:

smhget参数说明:

key:表示待创建共享内存在系统的唯一标识。 size:表示想要申请的共享内存的大小。(建议4096的整数倍) shmflg:表示创建共享内存的方式。

smhget返回值说明:

若创建成功则返回共享内存的描述符smhid(用户层的,和key不同) 若创建失败则返回 -1

注意key值是需要我们自己传入的,我们可以想传什么就传什么,但key不可重复,所以建议使用ftok函数来取到合适的key:

注意:ftok函数是将一个路径pathname和一个proj_id通过一个特定的函数转换成key值。

传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:

组合方式

作用

IPC_CREAT

如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄

IPC_CREAT|IPC_EXCL

如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回

这两种奇怪的区分到底有什么用呢?

若是第一种方式拿到了一个描述符,则说明该共享内存一定是旧的。

若是第二种方式拿到了一个描述符,则说明该共享内存一定是新的。

所以我们用第二种组合方式来创建共享内存,用第一种组合方式来找到一个共享内存。

共享内存创建好后,我们是可以通过ipcs命令来进行查询的:

ipcs命令选项介绍:

  • -q:列出消息队列相关信息。
  • -m:列出共享内存相关信息。
  • -s:列出信号量相关信息。

不加选项默认全部列出:

图中每列信息如下:

标题

含义

key

系统区别各个共享内存的唯一标识

shmid

共享内存的用户层id(句柄)

owner

共享内存的拥有者

perms

共享内存的权限

bytes

共享内存的大小

nattch

关联共享内存的进程数

status

共享内存的状态

现在我们编写一个简单的程序来创建一个共享内存,并打印出它的key和描述符:

代码语言:javascript
复制
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

const char* pathname = "/home/sxk/linux2/24_6_6";
int proj_id = 0x66;

int main()
{
    //得出key
    key_t key = ftok(pathname,proj_id);
    if(key < 0)
    {
        perror("ftok");
    }
    //创建共享内存
    int shmid = shmget(key,4096,IPC_CREAT);
    if(shmid < 0)
    {
        perror("shmget");
    }

    //打印出共享内存的key和shmid
    printf("key:   %x\n",key);
    printf("shmid: %d\n",shmid);
    sleep(10);
    return 0;
}

运行结果:

共享内存的释放:

先介绍一个共享内存的重要特性:

共享内存不随程序的结束而释放。

所以,当我们的程序结束后共享内存仍然存在:

如果想要释放这个共享内存有两种方法:

1.使用 ipcrm -m 描述符 指令来删除指定的共享内存

2.在代码中使用shmctl函数:

shmctl函数参数选项介绍:

  • 第一个参数shmid,表示所控制共享内存的用户级标识符。
  • 第二个参数cmd,表示具体的控制动作。
  • 第三个参数buf,用于获取或设置所控制共享内存的数据结构

shmctl函数的返回值说明:

  • shmctl调用成功,返回0。
  • shmctl调用失败,返回-1。

第二个参数cmd常用的几个选项如下:

选项

作用

IPC_STAT

获取共享内存的当前关联值,此时参数buf作为输出型参数

IPC_SET

在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值

IPC_RMID

删除共享内存段

修改之前的代码,创建共享内存2秒后删除共享内存:

共享内存的关联:

共享内存在物理空间创建好后,还需将物理内存的地址与进程的虚拟地址空间中的共享区的地址,通过页表映射建立联系,这样之后进程才能访问这片共享内存。

通过shmat函数来建立映射关系

shmat函数的参数说明:

  • 第一个参数shmid,表示待关联共享内存的用户级标识符。
  • 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
  • 第三个参数shmflg,表示关联共享内存时设置的某些属性。

shmat函数的返回值说明:

  • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
  • shmat调用失败,返回(void*)-1。

其中,作为shmat函数的第三个参数传入的常用的选项有以下三个:

选项

作用

SHM_RDONLY

关联共享内存后只进行读取操作

SHM_RND

若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)

0

默认为读写权限

共享内存的去关联:

使用shmdt函数来去关联:

shmat函数参数介绍:

  • shmaddr:表示需要去关联的共享内存

shmat函数的返回值

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

用共享内存实现serve&client通信:

serve端负责创建共享内存,并收消息,client,负责发消息。

serve.cc:

代码语言:javascript
复制
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

const char* pathname = "/home/sxk/linux2/24_6_6";
int proj_id = 0x66;

int main()
{
    //得出key
    key_t key = ftok(pathname,proj_id);
    if(key < 0)
    {
        perror("ftok");
    }
    //创建共享内存
    int shmid = shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);
    if(shmid < 0)
    {
        perror("shmget");
    }
    //打印出共享内存的key和shmid
    printf("key:   %x\n",key);
    printf("shmid: %d\n",shmid);
    sleep(5);
    //与共享内存关联
    char* msg = (char*)shmat(shmid,NULL,0);
    if(msg == (void*)-1)
    {
        perror("shmat");
    }
    //开始读消息
    std::cout<<"serve begin read msg :"<<std::endl;
    while(1)
    {
        std::cout<<msg<<std::endl;
        sleep(1);
    }
    //读完,去关联
    int n = shmdt(msg);
    if(n < 0)
    {
        perror("shmdt");
    }
    //释放共享内存
    int t = shmctl(shmid,IPC_RMID,NULL);
    if(t < 0)
    {
        perror("shmctl");
    }
    return 0;
}

client.cc:

代码语言:javascript
复制
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

const char* pathname = "/home/sxk/linux2/24_6_6";
int proj_id = 0x66;

int main()
{
    //获取key
    key_t key = ftok(pathname,proj_id);
    if(key < 0)
    {
        perror("ftok");
    }
    //获取共享内存
    int shmid = shmget(key,4096,IPC_CREAT);
    if(shmid < 0)
    {   
        perror("shmget");
    }
    //与共享内存关联指定shmid,不指定地址起始位置,读写权限
    char* msg = (char*)shmat(shmid,NULL,0);
    if(msg == (void*)-1)
    {  
        perror("shmat");
    }
    //开始发送消息
    char a = 'A';
    int i = 0;
    while(a < 'Z')
    {
        msg[i] = a + i;
        i++;
        sleep(1);
    }
    //发送完毕,去关联
    int t = shmdt(msg);
    if(t < 0 )
    {
        perror("shmdt");
    }
    return 0;
}

运行结果:

system V消息队列:

消息队列基本原理:

消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。

总结一下:

  1. 消息队列提供了一个从一个进程向另一个进程发送数据块的方法。
  2. 每个数据块都被认为是有一个类型的,接收者进程接收的数据块可以有不同的类型值。
  3. 和共享内存一样,消息队列的资源也必须自行删除,否则不会自动清除,因为system V IPC资源的生命周期是随内核的。

消息队列数据结构:

当然,系统当中也可能会存在大量的消息队列,系统一定也要为消息队列维护相关的内核数据结构。

消息队列的数据结构如下:

代码语言:javascript
复制
struct msqid_ds {
	struct ipc_perm msg_perm;
	struct msg *msg_first;      /* first message on queue,unused  */
	struct msg *msg_last;       /* last message in queue,unused */
	__kernel_time_t msg_stime;  /* last msgsnd time */
	__kernel_time_t msg_rtime;  /* last msgrcv time */
	__kernel_time_t msg_ctime;  /* last change time */
	unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes; /* ditto */
	unsigned short msg_cbytes;  /* current number of bytes on queue */
	unsigned short msg_qnum;    /* number of messages in queue */
	unsigned short msg_qbytes;  /* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

可以看到消息队列数据结构的第一个成员是msg_perm,它和shm_perm是同一个类型的结构体变量,ipc_perm结构体的定义如下:

代码语言:javascript
复制
struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

消息队列的创建:

创建消息队列我们需要用msgget函数:

msgget函数参数介绍:

key:表示带创建消息队列在系统的唯一标识。(跟共享内存差不多) msgflg:和shmget的第三个参数一样。

msgget函数返回值介绍:

创建消息队列成功则返回该消息队列的描述符。(用户级)

消息队列的释放:

释放消息队列我们需要用msgctl函数:

msgctl和shmctl用法基本相同。

向消息队列发送数据:

向消息队列发送数据我们需要用msgsnd函数:

msgsnd函数的参数说明:

  • 第一个参数msqid,表示消息队列的用户级标识符。
  • 第二个参数msgp,表示待发送的数据块。
  • 第三个参数msgsz,表示所发送数据块的大小
  • 第四个参数msgflg,表示发送数据块的方式,一般默认为0即可。

msgsnd函数的返回值说明:

  • msgsnd调用成功,返回0。
  • msgsnd调用失败,返回-1。

其中msgsnd函数的第二个参数必须为以下结构:

代码语言:javascript
复制
struct msgbuf{
	long mtype;       /* message type, must be > 0 */
	char mtext[1];    /* message data */
};

注意: 该结构当中的第二个成员mtext即为待发送的信息,当我们定义该结构时,mtext的大小可以自己指定。

从消息队列获取数据:

从消息队列获取数据我们需要用msgrcv函数:

msgrcv函数的参数说明:

  • 第一个参数msqid,表示消息队列的用户级标识符。
  • 第二个参数msgp,表示获取到的数据块,是一个输出型参数。
  • 第三个参数msgsz,表示要获取数据块的大小
  • 第四个参数msgtyp,表示要接收数据块的类型。

msgrcv函数的返回值说明:

  • msgsnd调用成功,返回实际获取到mtext数组中的字节数。
  • msgsnd调用失败,返回-1。

system信号量:

信号量相关概念:

由于进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系叫做进程互斥。 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。 在进程中涉及到临界资源的程序段叫临界区。 IPC资源必须删除,否则不会自动删除,因为system V IPC的生命周期随内核。

信号量数据结构:

在系统当中也为信号量维护了相关的内核数据结构:

代码语言:javascript
复制
struct semid_ds {
	struct ipc_perm sem_perm;       /* permissions .. see ipc.h */
	__kernel_time_t sem_otime;      /* last semop time */
	__kernel_time_t sem_ctime;      /* last change time */
	struct sem  *sem_base;      /* ptr to first semaphore in array */
	struct sem_queue *sem_pending;      /* pending operations to be processed */
	struct sem_queue **sem_pending_last;    /* last pending operation */
	struct sem_undo *undo;          /* undo requests on this array */
	unsigned short  sem_nsems;      /* no. of semaphores in array */
};

信号量数据结构的第一个成员也是ipc_perm类型的结构体变量,ipc_perm结构体的定义如下:

代码语言:javascript
复制
struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

信号量集的创建:

创建信号量集我们需要用semget函数:

创建信号量集也需要使用ftok函数生成一个key值,这个key值作为semget函数的第一个参数。 semget函数的第二个参数nsems,表示创建信号量的个数。 semget函数的第三个参数,与创建共享内存时使用的shmget函数的第三个参数相同。 信号量集创建成功时,semget函数返回的一个有效的信号量集标识符(用户层标识符)。 信号量集的删除

信号量集的删除:

删除信号量集我们需要用semctl函数:

信号量集的操作:

对信号量集进行操作我们需要用semop函数:

进程互斥

进程间通信通过共享资源来实现,这虽然解决了通信的问题,但是也引入了新的问题,那就是通信进程间共用的临界资源,若是不对临界资源进行保护,就可能产生各个进程从临界资源获取的数据不一致等问题。

保护临界资源的本质是保护临界区,我们把进程代码中访问临界资源的代码称之为临界区,信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。

信号量本质是一个计数器,在二元信号量中,信号量的个数为1(相当于将临界资源看成一整块),二元信号量本质解决了临界资源的互斥问题,以下面的伪代码进行解释:

根据以上代码,当进程A申请访问共享内存资源时,如果此时sem为1(sem代表当前信号量个数),则进程A申请资源成功,此时需要将sem减减,然后进程A就可以对共享内存进行一系列操作,但是在进程A在访问共享内存时,若是进程B申请访问该共享内存资源,此时sem就为0了,那么这时进程B会被挂起,直到进程A访问共享内存结束后将sem加加,此时才会将进程B唤起,然后进程B再对该共享内存进行访问操作。

在这种情况下,无论什么时候都只会有一个进程在对同一份共享内存进行访问操作,也就解决了临界资源的互斥问题。

实际上,代码中计数器sem减减的操作就叫做P操作,而计数器加加的操作就叫做V操作,P操作就是申请信号量,而V操作就是释放信号量。

感谢阅读!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 认识system V:
  • system V共享内存:
    • 共享内存的基本原理:
      • 共享内存的数据结构:
        • 共享内存的建立与释放:
          • 共享内存的建立:
          • 共享内存的释放:
        • 共享内存的关联:
          • 共享内存的去关联:
            • 用共享内存实现serve&client通信:
            • system V消息队列:
              • 消息队列基本原理:
                • 消息队列数据结构:
                  • 消息队列的创建:
                    • 消息队列的释放:
                      • 向消息队列发送数据:
                        • 从消息队列获取数据:
                        • system信号量:
                          • 信号量相关概念:
                            • 信号量数据结构:
                              • 信号量集的创建:
                                • 信号量集的删除:
                                  • 信号量集的操作:
                                  • 进程互斥
                                  相关产品与服务
                                  消息队列
                                  腾讯云消息队列 TDMQ 是分布式架构中的重要组件,提供异步通信的基础能力,通过应用解耦降低系统复杂度,提升系统可用性和可扩展性。TDMQ 产品系列提供丰富的产品形态,包含 CKafka、RocketMQ、RabbitMQ、Pulsar、CMQ 五大产品,覆盖在线和离线场景,满足金融、互联网、教育、物流、能源等不同行业和场景的需求。
                                  领券
                                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档