
🏠关于专栏:Linux的浅学到熟知专栏用于记录Linux系统编程、网络编程等内容。
🎯每天努力一点点,技术变化看得见
文章目录
共享内存引入及原理
在前面的文章中,我们介绍了匿名管道和命名管道,下面通过对比这两种通信方式,以引出共享内存通信方式。↓↓↓
匿名管道方式
其中匿名管道由操作系统在内核帮助用户维护一份匿名管道文件,在若子进程与父进程通信时,子进程将数据写入printf缓冲区,再调用系统调用将printf缓冲区内容拷贝到内核中的匿名管道文件;而父进程读取数据时,不能直接读取内核维护的匿名管道文件,需要将匿名管道文件拷贝到父进程中的临时缓冲区。因而,匿名管道在每次通信时,都会发生两次拷贝。同时,匿名管道通信时,进程读取或写入数据时,需要进行用户态到内核态的相互转换,而这种转换会产生额外开销。

命名管道通信方式
为了避免访问公享资源时需要转换状态(用户/内核态),命名管道方式应运而生。命名管道通过在内存创建一个具备单向通信的信道文件,当A、B进程通信时可以直接对内存中的FIFO文件进行操作,不需要用户/内核态转换;整个通信过程中,FIFO文件不会与磁盘进行任何交互(落盘),故命名管道比普通文件快。但A、B进程无法直接将数据写入该文件或直接从该文件进行读取,故写入进程需要维护一个写入缓冲区,写入完毕后,再将缓冲区的内容拷贝到FIFO文件中;读取进程需要维护一个读取缓冲区,将FIFO文件中的内容拷贝到缓冲区,再进行读取,需要两次拷贝。

共享内存的提出
可不可以实现通信的两个进程A、B,A进程直接向内存中写入,另一个进程直接从该内存中读取,避免上述的两次拷贝呢?
共享内存方式,就是让两个通信进程直接访问同一块内存空间,直接对该空间执行读取或写入,避免了拷贝。共享的建立过程是:①在内存中申请一块空间;②将该内存挂接(映射)到通信进程的进程地址空间的共享区。
当A进程需要与B进程通信时,只需要通过共享区虚拟地址映射到物理地址,对该物理地址直接进行写入;而B进程则是通过象取虚拟地址映射到物理地址,从该物理地址直接进行读取。这样就可以避免2次拷贝。

那共享内存的提供者是谁呢?由于内存属于系统硬件资源,故共享内存的提供者必然是操作系统。由于通信的进程可能很多,申请的共享内存也可能很多,故操作系统需要对各个共享内存先描述再组织。即操作系统需要创建对应的内核数据结构,对系统中的共享内存的各个属性进行描述,再通过管理这些内核数据结构,实现对共享内存的管理。由此,我们可以得出结论:共享内存=共享内存块+对应的共享内存的内核数据结构。
//共享内存对应的内核数据结构
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 */
...
};
★ps:共享内存也存在用户态与内核态的转换,但一旦对应内存映射到进程的地址空间,则进程间通信就不会涉及到用户态与内核态的转换。换句话说,进程后序不再通过执行内核的系统调用来转递彼此的数据。
★ps:共享内存是进程间通信最快的方式,一方面用它通信时,拷贝没有缓冲区拷贝问题;另一方面,共享内存通信时,仅涉及开始时的一次系统调用(即一次用户态和内核态的转换)。
共享内存相关函数及使用实例
获取唯一key值——ftok
在命名管道中,通过文件路径的唯一性,以保证多个通信进程能够看到同一个共享资源。在共享内存也需要保证多个通信进程看到同一份共享资源,即使用key来保证资源的唯一性。key需要使用ftok函数自动生成↓↓↓

第一个参数pathname是值文件路径,第二个参数是项目id(用户自己指定,没有明确要求)。返回计算得到的key值,但如果传入的路径不存在,则会生成失败,返回-1。
如果多个进程需要通信,只需要传入同一个文件目录及项目id就能确定唯一的key值,即能够找到同一份共享内存资源。
★ps:谈谈key:
1.key是一个数字,这个数字是多少并不重要,关键是它必须在内核中具有唯一性,就能让不同的进程进行唯一性标识;
2.第一个进程可以通过key创建共享内存,第二个及以后的进程只要拿着同一个key值,就可以和第一个进程看到同一个共享内存;
3.对于一个已经创建好的共享内存,key在哪呢?答案是:key在共享内存的内核数据结构中(共享内存的描述对象);
4.为了保证多个进程能够看到同一个共享内存,需要约定唯一的pathname和项目id,避免找到不是同一个共享内存;
5.key和路径一样,具有唯一性。
下面给出ftok创建key值的实例代码↓↓↓
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main()
{
key_t key1 = ftok("/home/xiaoming", 5);
key_t key2 = ftok("/home/xiaoming", 5);
key_t key3 = ftok("/home/xiaoming/gitcode", 5);
key_t key4 = ftok("/home/xiaoming/", 666);
printf("key1 = %u\n", key1);
printf("key2 = %u\n", key2);
printf("key3 = %u\n", key3);
printf("key4 = %u\n", key4);
return 0;
}

创建共享内存——shmget

创建共享内存第一个参数key用于保证共享内存的唯一性,第二个参数标识共享内存的大小(单位:字节),下面表格给出了第三个参数的说明↓↓↓
| shmflg取值 | 描述 |
|---|---|
| IPC_CRAET | 创建共享内存时,如果共享内存已经已经存在,获取已经存在的共享内存;不存在则创建并返回 |
| IPC_EXCL | 需要与IPC_CREAT组合使用,单独使用没有意义。如果带创建的共享内存存在,则出错返回;如果不存在,则创建并返回对应的共享内存 |
shmget返回值类似于文件描述符(但并不是文件描述符),我们可以像读写文件一样操纵该返回值(但共享内存创建失败则会返回-1)。由于传入了具有唯一性的key值,故共享内存的唯一性得以保证。下面给出代码示例。
使用IPC_CREAT选项创建共享内存(两次创建均成功)↓↓↓
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok("/home/xiaoming", 88);
int shmid = shmget(key, 128, IPC_CREAT);
printf("%d\n", shmid);
shmid = shmget(key, 128, IPC_CREAT);
printf("%d\n", shmid);
return 0;
}

使用IPC_CREAT | IPC_EXCL选项创建共享内存(第一次创建成功,第二次失败)↓↓↓
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok("/home/xiaoming", 88);
int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL);
printf("%d\n", shmid);
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL);
printf("%d\n", shmid);
return 0;
}

我们可以使用ipcs -m查看系统中的共享内存情况↓↓↓

其中,shmid是共享内存的编号,bytes表示共享内存的大小(单位字节),perms表示权限,我们怎么指定共享内存的权限呢?在shmget的第三个参数或运算上对应的权限即可↓↓↓
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok("/home/xiaoming", 88);
int shmid = shmget(key, 128, IPC_CREAT | 0666);
printf("%d\n", shmid);
return 0;
}

我们要删除某个共享内存时,可以使用ipcrm -m [shmid]↓↓↓

挂接共享内存——shmat

创建完共享内存后,需要将该共享内存挂接到进程的地址空间中。shmat用于将共享内存挂接到当前进程中,第一个参数为共享内存标识符(shmget正确时的返回值);第二个参数表示要将共享内存挂接到进程的哪个地址中,我们只需要填写NULL即可,操作系统会选择合适的位置进行挂接;第三个参数可以填写NULL、SHM_RND或SHM_RDONLY,选项的具体描述如下表(通常情况下填NULL)↓↓↓
| shmflg选项 | 描述 |
|---|---|
| NULL | 为空时,操作系统会自动选择合适的位置挂接共享内存 |
| SHM_RND | 连接的地址将会是shmaddr向下取整到最接近的SHMLBA的倍数的地址。这可以增加系统的安全性,同时也可以帮助避免攻击者利用固定地址进行攻击。 |
| SHM_RDONLY | 将共享内存段连接到进程地址空间时,以只读模式进行连接。这意味着进程只能读取共享内存段中的数据,不能对其进行写入操作。这有助于确保共享内存段的数据不会被意外修改。 |
shmat将与shmdt共用一个示例代码,这里先不给出示例代码。
解除挂接共享内存——shmdt

shmdt用于解除挂接,当使用shmat挂接完后,会返回一个指向共享内存第一节的指针。shmdt需要传入该指针才能完成解除挂接。
下面代码中,挂接后,sleep 5秒,再解除挂接。我们使用while :; do ipcs -m; sleep 1; done;每隔一秒监控一次共享内存的情况↓↓↓
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok("/home/xiaoming", 800);
int shmid = shmget(key, 256, IPC_CREAT | 0666);
sleep(2);
void* ptr = shmat(shmid, NULL, 0);
printf("attach %d\n",shmid);
sleep(2);
shmdt(ptr);
printf("dettach %d\n",shmid);
return 0;
}
监控结果如下,nattch表示当前共享内存挂接数。前2秒,挂接数为0;第2秒开始,由于上述程序对应的进程挂接了该共享内存,故nattch变为1;第4秒时,解除挂接,故后序挂接数变为0。↓↓↓

删除共享内存——shmctl

shmctl用于控制共享内存,第一个参数为共享内存的唯一性标识符shmid,第三个参数是共享内存在内核中对应的数据结构,第二个参数具体取值即各取值说明如下表↓↓↓
| 命令 | 说明 |
|---|---|
| IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的 当前相关值 |
| IPC_SET | 在进程有足够权限的前提下,把共享内存的关联值设置为第三个参数传入的shmid_ds数据结构给出的值 |
| IPC_RMID | 删除共享内存(即使当前有进程挂接,也会删除) |
下面代码演示如何删除共享内存↓↓↓
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key = ftok("/home/xiaoming", 800);
int shmid = shmget(key, 256, PIC_CREAT | 0666);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}

共享内存的特性
- 共享没有没有同步和互斥机制。即读写双方可以同时访问共享内存,这会导致数据不一致问题,这个问题的解决方案将在后序文章中介绍;
- 共享内存是所有的进程通信中,速度最快的;
- 共享内存内部的数据由用户自己维护(读完要自己清空);
- 共享内存的生命周期是随内核的,用户不主动删除,共享内存会一直存在(除非内核重启或用户释放);
- 共享内存的大小一般建议是4096的整数倍,内存管理的一页大小为4096字节(4KB)。若申请4097,则系统会分配4096 * 2,但用户还是只能使用4097的空间,会存在4095字节空间的浪费。
共享内存应用——Sever&Client通信
由于共享内存不存在同步互斥机制,而目前阶段,我们暂为学习锁、信号量等同步互斥机制,这里可以借助之前文章介绍的管道来实现。
Com.hpp
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <fcntl.h>
const char* FIFO_FILE_NAME = "con-fifo";
const char* pathname = "/home/xiaoming";
const int proj_id = 555;
const int size = 4096;
enum
{
FIFO_CREAT_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR,
PIPE_CREAT_ERR,
KEY_CRATE_ERR,
SHM_CRATE_ERR
};
key_t GetKey()
{
key_t key = ftok(pathname, proj_id);
if(key == -1)
{
perror("crate key error");
exit(KEY_CRATE_ERR);
}
return key;
}
int GetShareMemHelper(int flags)
{
key_t key = GetKey();
int shmid = shmget(key, size, flags);
if(shmid < 0)
{
perror("create shm error");
exit(SHM_CRATE_ERR);
}
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
class Init
{
public:
Init()
{
int n = mkfifo(FIFO_FILE_NAME, 0666);
if(n == -1)
{
perror("create fifo error");
exit(PIPE_CREAT_ERR);
}
}
~Init()
{
int n = unlink("con-fifo");
if(n == -1)
{
perror("delete fifo error");
exit(FIFO_DELETE_ERR);
}
}
};
ProcessA.cc
#include "Com.hpp"
int main()
{
Init init;
int shmid = CreateShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
int fd = open(FIFO_FILE_NAME, O_RDONLY);
if(fd < 0)
{
perror("fifo open error");
exit(FIFO_OPEN_ERR);
}
while(true)
{
char c;
int n = read(fd, &c, sizeof(c));
if(n <= 0) break;
std::cout << "Client says @ " << shmaddr << std::endl;
sleep(1);
}
shmdt(shmaddr);
return 0;
}
ProcessB.cc
#include "Com.hpp"
int main()
{
int shmid = GetShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
int fd = open(FIFO_FILE_NAME, O_WRONLY);
if(fd < 0)
{
perror("fifo open error");
exit(FIFO_OPEN_ERR);
}
while(true)
{
std::cout << "Say#";
fgets(shmaddr, size, stdin);
write(fd, "c", 1);
}
return 0;
}

🎈欢迎进入从浅学到熟知Linux专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d
&spm=1001.2101.3001.5002&articleId=138164163&d=1&t=3&u=1f2c5490698c48aca60a3d902fde07d3)
2794

被折叠的 条评论
为什么被折叠?



