目录
认识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_perm
,shm_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消息队列:
消息队列基本原理:
消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。
总结一下:
- 消息队列提供了一个从一个进程向另一个进程发送数据块的方法。
- 每个数据块都被认为是有一个类型的,接收者进程接收的数据块可以有不同的类型值。
- 和共享内存一样,消息队列的资源也必须自行删除,否则不会自动清除,因为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操作就是释放信号量。
感谢阅读!
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:腾讯云自媒体同步曝光计划 - 腾讯云开发者社区-腾讯云