Liunx系统 : 进程间通信【IPC-Shm共享内存】
文章目录
- System V
- 共享内存
- 创建共享内存
- shmget
- 控制共享内存
- shmctl
- shm特性
System V
System V是Liunx中的重要的进程间通信机制,它包括(shm
)共享内存,(msg
)消息队列和(sem
)信号量。本篇博客主要介绍其中最常用的一种方式–共享内存。
共享内存
顾名思义,共享内存是一块进程之间可以共享的内存区域,由于进程之间都是相互独立的,那么这块区域自然而然不是又某个进程开辟的,而应该是由操作系统亲自开辟。
如上图,共享内存会被进程的页表直接映射到自己的进程地址空间共享区,从而通过地址空间与页表直接访问到物理内存(可能有多层),进而对内存操作,这就是多个进程共享一块内存的基本原理。
创建共享内存
shmget
shmget
函数是 shm
中用于创建或获取共享内存段的函数。需要头文件<sys/ipc.h>
和<sys/shm.h>
函数原型如下:
int shmget(key_t key,size_t size,int shmflg);
返回值:
shmget
返回一个整型,这个整型叫做shmid
,用于标识唯一的shm
。
1.
key
: 标识要创建或获取的共享内存段,是System V方式的唯一标识。
注意和shmid
区分,shmid
是用来标识唯一的共享内存(shm
)。
2.
size
: 指定要创建的共享内存段的大小,单位为字节
注意共享内存以4kb
为基本单位开辟内存,因此开辟内存的时候,这个参数最好设置为4096
的倍数,哪怕你只是申请了一个字节的内存,实际上还是会开辟4kb
大小的空间的。
3 .
shmflg
: 用于指定共享内存段访问的权限和其他选项。
这是一个用于控制共享内存的开辟方式,以及各个属性的选项,本质上是一个位图。
IPC_CREAST
: 如果指定的key
不存在,则创建一个新的共享内存段,如果已经存在,则直接获取原先的共享内存。IPC_EXCL
:如果指定的key
已经存在,则创建失败。
要注意IPC_EXCL
只能配合IPC_CREAT
一起使用,不能单独使用。
- 还可以按照权限值的8进制位或到第三个参数之中。
int main()
{
int shmid = shmget(1, 4096, IPC_CREAT | IPC_EXCL | 0666);
return 0;
}
如果只想看共享内存的方式,则ipcs -m
:
一开始存在一个shmid = 0
的共享内存,之后通过指令的方式删除掉了这个共享方式,除了这种方式,我们还可以通过系统接口来删除共享内存,也就是下面要介绍的shmctl
接口。
控制共享内存
shmctl
shmctl
用于控制共享内存的各种属性。
- 获取共享内存段的状态信息
- 修改共享内存段的属性
- 删除共享内存段
shmctl
包含在头文件<sys/ipc.h>
和<sys/shm.h>
中,函数原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 函数返回值
shmctl
函数的返回值用于指示操作是否成功:
- 成功时:返回 0,表示操作已完成且未发生错误。
- 失败时:返回 -1,并设置
errno
变量以指示具体的错误原因。常见的错误包括权限不足、无效的共享内存标识符等。
shmctl
函数有三个主要参数,它们共同决定了函数的行为:
-
shmid:这是要操作的共享内存段的标识符。通过
shmget
函数可以获取该标识符,它是后续所有操作的基础,确保函数作用于正确的共享内存段。 -
cmd:指定要执行的操作类型,它可以是以下值之一:
- IPC_STAT:用于获取共享内存段的状态信息。此操作会将共享内存段的相关属性填充到由
buf
指向的shmid_ds
结构体中,供程序查询和分析。 - IPC_SET:用于设置共享内存段的某些属性。通过
buf
参数传递新的属性值,但需注意只有超级用户或拥有适当权限的进程才能执行此操作。 - IPC_RMID:用于删除共享内存段。执行此操作后,共享内存段将被标记为可销毁,系统会在合适的时候将其从内核中移除。
- IPC_STAT:用于获取共享内存段的状态信息。此操作会将共享内存段的相关属性填充到由
-
buf:这是一个指向
shmid_ds
结构体的指针。当cmd
参数为IPC_STAT
时,该结构体用于接收共享内存段的当前属性;当cmd
参数为IPC_SET
时,该结构体则包含需要设置的新属性值。
- 相关结构体 shmid_ds
在使用 shmctl
函数时,shmid_ds
结构体是至关重要的,它包含了共享内存段的详细信息:
struct shmid_ds {
struct ipc_perm shm_perm; /* 所有者和权限 */
size_t shm_segsz; /* 段的大小(字节) */
time_t shm_atime; /* 最后附加时间 */
time_t shm_dtime; /* 最后分离时间 */
time_t shm_ctime; /* 最后更改时间 */
pid_t shm_cpid; /* 创建者的 PID */
pid_t shm_lpid; /* 最后调用 shmat/shmdt 的 PID */
shmatt_t shm_nattch; /* 当前附加次数 */
...
};
- shm_perm:该字段是一个
ipc_perm
结构体,包含了共享内存段的所有者、组、权限等信息,用于控制进程对共享内存段的访问权限。 - shm_segsz:表示共享内存段的大小,以字节为单位。在创建共享内存段时设置,后续操作中可通过
IPC_STAT
获取该值,或在有适当权限时通过IPC_SET
修改。 - 时间相关字段(shm_atime、shm_dtime、shm_ctime):分别记录了共享内存段最后被附加、分离以及更改的时间,这些信息对于监控共享内存段的使用情况非常有用。
- 进程 ID 相关字段(shm_cpid、shm_lpid):
shm_cpid
是创建共享内存段的进程的 PID,而shm_lpid
是最后调用shmat
或shmdt
函数的进程的 PID,有助于追踪共享内存段的使用历史。 - shm_nattch:表示当前共享内存段被附加的次数。当进程调用
shmat
附加共享内存段时,该值会增加;当进程调用shmdt
分离共享内存段时,该值会减少。当该值为 0 时,表示没有进程正在使用该共享内存段。
当
shmctl
第二个参数cmd
为IPC_STAT
,此时就可以获取一个共享内存的基本信息。
示例 :
int main()
{
int id = shmget(1, 4096, IPC_CREAT | IPC_EXCL | 0666);
struct shmid_ds shm;
shmctl(id, IPC_STAT, &shm);
cout << "atime:" << shm.shm_atime << endl;
cout << "ctime:" << shm.shm_ctime << endl;
cout << "cpid:" << shm.shm_cpid << endl;
return 0;
}
第二个参数为
IPC_SET
时候可以设置共享内存的某些属性。
int main()
{
int id = shmget(1, 4096, IPC_CREAT | IPC_EXCL | 0666);
struct shmid_ds shm;
shmctl(id, IPC_STAT, &shm);
cout << "atime:" << shm.shm_atime << endl;
// 140731859906331
shm.shm_atime = 1 ;
//修改shm信息
shmctl(id, IPC_SET, &shm);
//重新获取shm信息
shmctl(id, IPC_STAT, &shm);
cout << "atime:" << shm.shm_atime << endl;
// 1
return 0;
}
删除共享内存(通过接口的方式)
int main()
{
shmctl(0, IPC_RMID, nullptr);
return 0;
}
一开始存在一个shmid = 0
的共享内存,经过test
之后,这个共享内存就被删除了。
上面我们详细讨论了如何来开辟一个共享内存,还没有真正使用这段共享内存来进行通信。
接下来,假设现在有A
,B
两个进程,他们通过共享内存的方式进行通信,其中A负责发送消息,B负责接收消息。
A进程的代码数据如下:
int main()
{
key_t key = ftok("./test.cpp", 1);
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);
char* ptr = (char*)shmat(shmid, nullptr, 0);
for(int ch = 'A'; ch <= 'Z'; ch++)
{
ptr[ch - 'A'] = ch;
sleep(1);
}
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
B进程的代码数据如下:
int main()
{
key_t key = ftok("./test.cpp", 1);
int shmid = shmget(key, 4096, IPC_CREAT);
char* ptr = (char*)shmat(shmid, nullptr, 0);
while(true)
{
cout << ptr << endl;
sleep(5);
}
return 0;
}
可以看到A向共享内存中写入数据,就被B进程读到了!
shm特性
共享内存有以下一些主要特性:
-
内存共享:多个进程可以同时访问和修改同一块共享内存区域。这种共享内存机制可以让进程之间高效地交换数据,而无需通过系统调用或者其他进程间通信机制。
-
快速访问:相比于其他进程间通信机制,如管道、消息队列等,共享内存的访问速度更快,因为数据直接存储在内存中,不需要进行数据的拷贝和上下文切换。
-
灵活性:共享内存可以在进程之间自由分配和管理,大小和位置都可以灵活设置。这种灵活性使得共享内存非常适合用于复杂的进程间通信场景。
-
同步问题:多个进程可以并发访问和修改共享内存,因此需要使用信号量、互斥锁等同步机制来协调对共享内存的访问,避免数据竞争和不一致性问题。
-
内存分配:共享内存是由内核管理的,进程无法直接分配和释放共享内存,必须通过系统调用如
shmget()
和shmctl()
来完成。
system V 的后两种通信方式 消息队列 msg 和 信号量 sem 都非常不常用了,不深入研究。