Linux进程间通信之System V

目录

认识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共享内存:

 共享内存的基本原理:

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

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

共享内存的数据结构:

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

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的结构如下:

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和描述符:

#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:

#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:

#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资源的生命周期是随内核的。

消息队列数据结构:

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

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

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结构体的定义如下:

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函数的第二个参数必须为以下结构:

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的生命周期随内核。

信号量数据结构:

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

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结构体的定义如下:

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操作就是释放信号量。

感谢阅读!

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:腾讯云自媒体同步曝光计划 - 腾讯云开发者社区-腾讯云

  • 31
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咬_咬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值