"My poor lost soul"
上章花了不少的篇幅讲了讲基于管道((匿名、命名))技术实现的进程间通信。进程为什么需要通信?目的是为了完成进程间的"协同",提高处理数据的能力、优化业务逻辑的实现等等,在linux中我们已经谈过了一个通信的大类——管道。根据System V标准的基础上提出了,另一套进程通信的标准。
-----前言
一、System V简介
System V,曾经也被称为AT&TSystem V,是Unix操作系统众多版本中的一支。
System V的第一个版本,发布于1983年。它引进了一些特性,例如vi编辑器和curses库。其中也包括了对DEC VAX机器的支持。同时也支持使用消息进行进程间通信,信号量和共享内存。 取自这里
为什么这么"隆重"地介绍System V呢?因为这是一套通信标准。当在后面学了共享内存的多个API后,你会发现消息队列、信号量的接口函数极为相似。
二、共享内存
共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。 取自这里
文字描述显然很恼人,我们直接上图。
通信的本质,就是让不同能够看到同一份资源。
由此,我们对共享内存的理解是:其本质就是开辟在物理内存里的一块内存块,用来进行IPC通信的。
让需要通信的进程能够看到这块内存块,并在上面进行通信行为。
三、实现共享内存
(1)创建共享内存块
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
shmget() returns the identifier of the System V shared memory segment associated with the value of the argument key. A new shared memory segment, with size equal to the value of size rounded up to a multiple of PAGE_SIZE, is created if key has the value IPC_PRIVATE or key isn't IPC_PRIVATE, no shared memory segment corresponding to key exists, and IPC_CREAT is specified in shmflg.
这里也就简单说说shmget的参数。
key:这个参数至关重要。它是连接共享内存,找到共享内存块的关键。也就是说,一个进程只要拿到了在这个key值,就可以访问这一块内存块。
size:申请共享内存块的大小。这个size会按照PAGE_SIZE大小(向上对齐)。
shmflg:我们常用的参数就是 IPC_CREATE \ IPC_EXCL:
IPC_CREATE:如果不存在就创建、如果存在就获取
IPC_EXCL:不能单独使用。IPC_CREATE | IPC_EXCL 如果不存在就创建,存在就返回错误(保证给用户的共享内存块一定是新创建的)。
如果创建成功,shmget会返回一个有效标记内存块的标识符
那么怎么形成key这个唯一标识的关键字呢?库里给我们提供了一个函数,可以让生成的key是一个唯一标识的数字key_t类型。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
convert a pathname and a project identifier to a System V IPC key:用pathname + proj_id转换为key
并且这个pathname不是随便乱取的。而是一个"现有"、"可访问"的文件。
如何让两个进程看到一块共享内存?只需要知道key就行了。key是怎么生成的?ftok(pathname,proj_id)。这两个参数一样,不久可以生成相同的key了嘛?
如何理解key?
在操作系统中,一定会存在多个key_t类型的 有效标识符。那么这么多标识符一定会被操作系统管理。管理的本质:先描述、再组织。
我们举个例子:
我们可以看到,OS会为共享内存块维护一份结构体struct shimid_ds 用来管理共享内存块。
共享内存的生命周期随OS:
我们在学习管道通信时,一旦进程有一方结束,那么双方的通信管道也会随之关闭。为什么现如今,进程结束了,它们申请的共享内存块仍然存在呢??
管道的生命周期随进程,共享内存的生命周期随OS。
为此,我们不得不手动去关闭掉,我们编写的进程所打开的共享内存块。
ipcrm -m + shmid
(2)挂接与去关联
我们现如今拿到了共享内存块的标识符shmid,那么如何通过这个shmid找到共享内存的位置呢?
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmat() attaches the System V shared memory segment identified by shmid to the address space of the calling process. The attaching address is specified by shmaddr with one of the following criteria.
通过这个函数可以通过shmid 与 共享内存块挂接,并返回该共享内存块的地址。
参数: shmaddr 、shmflag
If shmaddr is NULL, the system chooses a suitable (unused) address at which to attach the segment.
这里我们通常设置为 nullptr,那么OS就会为我们选择一个适合的挂接到了这个内存块的地址。
shmflag通常为设置为0.
返回参数:
On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set to indicate the cause of the error.
如果成功,shmat()返回挂接这个共享内存块的地址,返回-1是error的
void *attachShm(int shmid)
{
void *mem = shmat(shmid, nullptr, 0);
if ((long long)mem == -1) // Linux下 是64位系统 一个指针大小为8字节
{
std::cerr << "attachShm: " << mem << std::endl;
exit(3);
}
return mem;
}
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
shmdt() detaches the shared memory segment located at the address specified by shmaddr from the address space of the calling process. The to-be-detached segment must be currently attached with shmaddr equal to the value returned by the attaching shmat() call.
shmdt()会将传入的共享内存块地址去挂接(关联)。调用shmdt(),必须传入通过shmat()获取的地址。
On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.
0为去关联成功,-1位失败。
void detachShm(void* start)
{
int ret = shmdt(start);
if(ret == -1)
{
std::cerr << "attachShm: " << ret << std::endl;
exit(4);
}
}
(3)释放共享内存块
共享内存块的生命周期随OS,我们每申请共享内存空间,都得"ipcrm -m + shmid"释放内存空间,当我们不再使用时,这未免太过麻烦了。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl() performs the control operation specified by cmd on the System V shared memory segment whose identifier is given in shmid.
shmctl()通过cmd传入的命令操作,对shmid表示的共享内存块进行处理。
要在进程结束时,关闭共享内存,我们需要传入的参数是:
IPC_RMID Mark the segment to be destroyed.
void delShm(int shmid)
{
if (shmctl(shmid, IPC_RMID, nullptr) == -1)
{
std::cerr << "delShm: " << shmid << std::endl;
exit(5);
}
}
当然,shmctl不仅仅可以归还共享内存,当cmd传入的参数是IPC_STAT时,我们可以获取由shmid填充的结构体内容。
像这样:
int main()
{
int shmid = shmget(key);
struct shmid_ds ds;
shmctl(shmid,IPC_STAT,&ds);
ds.shm_perm.__key;
ds.shm_atime;
//..
return 0;
}
(4)测试
有了上面对函数的理解和实现,我们可以进行一定的通信了。
我们想让Sever读取client发送的内容: "Hello Server! pid + 发送次数"。
Client:
int main()
{
// 1.申请key
key_t key = getKey(PATH_NAME, PROJ_ID);
std::cout << "Client key: " << key << std::endl;
// 2.申请内存块
int shmid = getShm(key);
std::cout << "Client shmid: " << shmid << std::endl;
// 3.挂接
void *start = attachShm(shmid);
printf("attach success, address start: %p\n", start);
// 真正的通信区域
const char* msg = "Hello Server!";
int cnt = 0;
while (true)
{
snprintf((char*)start,MAX_PAGE,"%s[pid:%d]编号信息:[%d]\n",msg,getpid(),cnt++);
sleep(1);
}
// 4.去关联
detachShm(start);
return 0;
}
Server:
int main()
{
// 1.申请key
key_t key = getKey(PATH_NAME, PROJ_ID);
std::cout << "Server key: " << key << std::endl;
// 2.申请内存块
int shmid = createShm(key);
std::cout << "Server shmid: " << shmid << std::endl;
// 3.挂接
void *start = attachShm(shmid);
printf("attach success, address start: %p\n", start);
// 真正的通信区域
while (true)
{
printf("client say : %s\n", start);
sleep(1);
}
// 4.去关联
detachShm(start);
printf("detach success");
// 5.关闭共享内存
delShm(shmid);
return 0;
}
我们来看看效果吧~
这是怎么回事??噢,原来是权限问题。
我们在创建共享内存的时候,需要附上权限大小。
这样就完成了双方进程间的通信。
四、共享内存 vs 管道
但是,管道自带同步与互斥!如果写端不写入,读端会成阻塞状态,直到写端写入,此时有数据可读。但是共享内存不一样,它没有同步与互斥操作,即便写端没有写入数据,读端照样会向内存块中读取。
总结:
①共享内存是一块存在物理内存的,由操作系统管理的内存块。它属于IPC通信方式的一种,在所有的通信方式中速度是最快的。
②共享内存的创建:1.申请(shmget) 2.挂接(shmat) 3.去关联(shmdt) 4.关闭空间(shmctl).
③管道自带同步与互斥,共享内存不支持。
本篇也就告一段落了,感谢你的阅读。
祝你好运~向阳而生。