共享内存方式
最快的IPC形式,这样的内存区域映射到共享它的进程的地址空间,这些进程的数据传输就不再涉及内核(进程不再通过任何进入内核的系统调用来彼此传递数据,内核必须允许各个进程共享内存区域的内存映射关系然后一直处理该内存区域),但是在共享内存中存放或读取信息需要进程间的同步方式。
客户——服务器交互信息的步骤
使用共享内存方式:(共享内存区对象同时出现在客户和服务器的地址空间中)
- 服务器使用一个信号量来获取某个共享内存对象的权利
- 服务器将数据从输入文件读到共享内存区对象,read的第二个参数指定的数据缓冲区地址指向这个共享内存区对象
- 服务区器读入完毕使用一个信号量通知客户
- 客户将这些数据从该共享内存区对象写到输出文件中
具有至少随内核持续特性
内存映射文件
由open函数打开,由mmap函数把得到的描述符映射到调用进程地址空间的文件。
mmap函数把一个文件或Posix共享内存区对象映射到调用进程的地址空间。使得进程之间通过映射同一个普通文件实现共享内存。使用该函数的三个目的:
- 使用普通文件以提供内存映射I/O
- 使用特殊文件以提供匿名内存映射
- 使用shm_open以提供无亲缘关系间的进程Posix共享内存区
内存映射文件(普通文件):在open之后调用mmap把他映射到调用进程地址空间的某个文件。使用内存映射文件的一个特性是:所有的I/O都在内核的掩盖下完成,我们只需编写存取内存映射区中各个值的代码(就是不调用read和write执行I/O时那样有内核直接参与I/O的完成,而是有内核在背后通过操纵页表等方式间接参与,这样用户进程看来I/O不再涉及系统调用),绝不再使用read,write,lseek。但是不是所有的文件都能进行内存映射的(终端或套接字映射到内存吗就会导致mmap返回一个错误,这些类型的描述符必须使用read和write)。
在无亲缘关系的进程间共享内存区(特殊文件匿名映射):所映射文件的实际内容成了被共享内存区的初始内容,而且这些进城对该共享内存区所做的任何变动都复制会映射文件(提供随文件系统的持续性,要使用MAP_SHARED)。
不能对终端或套接字描述符进行mmap,这类描述符必须使用read、write来访问。
#include <sys/mman.h> void *mmap(void *addr,size_t len,int port,int flags,int fd,off_t offset); //返回值:成功返回被映射区的起始地址,成功调用后fd可关闭,该操作对mmap建立映射关系无影响,失败返回MAP_FAILED,[其值为(void *)-1],munmap返回-1。
- addr:指定描述fd被映射到的进程内空间的起始地址,通常为空指针,让内核自己去选择起始地址,无论哪种情况下返回值都是描述符fd所映射到内存的起始地址
- fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。
- len:映射到调用进程地址空间的字节数,他被映射文件开头起第offset个字节处开始算,offest通常为0
- port:内存映射保护;PROT_READ:数据可读;PROT_WEITE:数据可写;PROT_EXEC:数据可执行;PROT_NONE:数据不可访问
- flags:
1>MAP_PRIVATE:变动自私的,调用进程对被映射数据所修改只对改进程可见,而不改变其底层支持对象(或是一个文件对象或是一个共享内存区对象);
2>MAP_SHARED:变动共享的,调用进程对被映射数据所修改对于共享该对象所有的进程可见,改变其底层支持对象(或是一个文件对象或是一个共享内存区对象)以上这二者必须指定一个;
3>MAP_FIXED:从移植方面来讲不应该指定该标志,但addr不是一个空指针,那么addr如何处置取决于实现,不为空的addr值通常被当作有关该内存区应是如何具体定位的线索,可移植性应该把addr指定一个空指针而且不指定该标志,使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
4> MAP_NORESERVE :不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
5>MAP_LOCKED:锁定映射区的页面,从而防止页面被交换出内存。
6>MAP_GROWSDOWN:用于堆栈,告诉内核VM系统,映射区可以向下扩展。
7>MAP_ANONYMOUS:匿名映射,映射区不与任何文件关联。
8>MAP_ANON:MAP_ANONYMOUS的别称,不再被使用。
9>MAP_FILE:兼容标志,被忽略。
10>MAP_32BIT:将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
11>MAP_POPULATE:为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞
12>MAP_NONBLOCK:仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
函数返回值,errno被设为以下的某个值
- EACCES:访问出错
- EAGAIN:文件已被锁定,或者太多的内存已被锁定
- EBADF:fd不是有效的文件描述词
- EINVAL:一个或者多个参数无效
- ENFILE:已达到系统对打开文件的限制
- ENODEV:指定文件所在的文件系统不支持内存映射
- ENOMEM:内存不足,或者进程已超出最大内存映射数量
- EPERM:权能不足,操作不允许
- ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
- SIGSEGV:试着向只读区写入
- SIGBUS:试着访问不属于进程的内存区
在fork之前可以先指定MAP_SHARED调用mmap。mmap成功返回后可关闭fd,该操作对mmap建立的映射关系无影响。
munmap
#include <sys/mman.h> int munmap(void *addr,size_t len); //返回值:成功返回0,出错返回-1
- addr:由mmap返回的地址
- len:映射区大小
- 再次访问这些地址将会导致SIGSEGV信号,如果被映射区域是MAP_PRIVATE标志,那么调用进程对他的所有变动都会被丢弃
msync
内核的虚拟内存算法保持映射文件(一般在硬盘上)与内存映射区(一般在内存中)的同步,前提他是一个MAP_SHARED内存区,也就是我们改了处于内存映射到某个文件的内存区中某个位置的内容,那么内核将在稍后某个时刻相应的更新文件,然而我们需要确信硬盘上的文件内容与内存区中的内容一致,于是调用该函数来执行同步
#include <sys/mman.h> int msync(void *addr,size_t len,int flags); //返回值:成功返回0,出错返回-1
flags:
- MS_ASYNC:执行异步写,一旦写操作已由内核排入队列,该标志指定的函数立即返回
- MS_SYNC:执行同步写(与ASYNC选其一,不能同时指定),该标志是等到写操作完成后再返回
- MS_INVALIDATE:使高速缓存的数据丢失,与其最终副本不一致的文件数据的所有内存中副本都失效,后续的引用将从文件中取得数据
信号量父子进程中各有一份
/************************************************************************* > File Name: mmap.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月16日 星期二 16时53分40秒 ************************************************************************/ #include <iostream> #include <sys/mman.h> #include <sys/types.h> #include <semaphore.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <error.h> using namespace std; string name("/tmp"); #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) const int loop=10; int main(int argc,char **argv) { //信号量在内核中 sem_t *sem=sem_open(name.c_str(),O_CREAT|O_EXCL,FILE_MODE,1); if(sem==SEM_FAILED) { cerr<<strerror(errno)<<endl; exit(-1); } sem_unlink(name.c_str()); //int value; //sem_getvalue(sem,&value); //cout<<value<<endl; int fd=open("1.txt",O_RDWR|O_CREAT,FILE_MODE); int zero=0; write(fd,&zero,sizeof(int)); //子父进程都有自己的ptr副本,每个副本都指向共享内存区域的一个整数 int *ptr=(int *)mmap(nullptr,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(ptr==MAP_FAILED) { cerr<<strerror(errno)<<endl; exit(1); } close(fd); setbuf(stdout,nullptr); if((fork()==0))//子进程 { cout<<"child: "<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(sem); cout<<(*ptr)++<<endl; sem_post(sem); } exit(0); } cout<<"parent :"<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(sem); cout<<(*ptr)++<<endl; sem_post(sem); } exit(0); }
信号量在共享内存中
/************************************************************************* > File Name: mmap.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月16日 星期二 16时53分40秒 ************************************************************************/ #include <iostream> #include <sys/mman.h> #include <sys/types.h> #include <semaphore.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <error.h> using namespace std; typedef struct Shared { int count; sem_t sem; }Shared; const int loop=10; #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) int main(int argc,char **argv) { Shared s; int fd=open("1.txt",O_RDWR|O_CREAT,FILE_MODE); write(fd,&s,sizeof(Shared)); //子父进程都有自己的ptr副本,每个副本都指向共享内存区域的一个整数 Shared *ptr=(Shared *)mmap(nullptr,sizeof(Shared),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(ptr==MAP_FAILED) { cerr<<strerror(errno)<<endl; exit(1); } close(fd); //此时信号量在共享内存中 int res=sem_init(&ptr->sem,1,1); if(res==-1) { cerr<<strerror(errno)<<endl; exit(1); } //取消缓冲 setbuf(stdout,nullptr); if((fork()==0))//子进程 { cout<<"child: "<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(&ptr->sem); cout<<(*ptr).count++<<endl; sem_post(&ptr->sem); } exit(0); } cout<<"parent :"<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(&ptr->sem); cout<<(*ptr).count++<<endl; sem_post(&ptr->sem); } exit(0); }
匿名内存映射
/************************************************************************* > File Name: anon.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月16日 星期二 20时19分50秒 ************************************************************************/ #include <iostream> #include <sys/mman.h> #include <unistd.h>//fork #include <fcntl.h>//O_RDWR #include <semaphore.h> #include <string.h>//strerror #include <error.h> using namespace std; string name("/tmp2"); const int loop=10; int main() { sem_t *sem=sem_open(name.c_str(),O_CREAT|O_EXCL,1); if(sem==SEM_FAILED) { cerr<<strerror(errno)<<endl; exit(1); } //内存区初始化为0 int *ptr=(int *)mmap(nullptr,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0); if(ptr==MAP_FAILED) { cerr<<strerror(errno)<<endl; exit(1); } if(fork()==0) { cout<<"child: "<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(sem); cout<<(*ptr)++<<endl; sem_post(sem); } sem_unlink(name.c_str()); sem_close(sem); exit(0);//不显示的调用 } cout<<"parent: "<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(sem); cout<<(*ptr)++<<endl; sem_post(sem); } sem_unlink(name.c_str()); sem_close(sem); munmap(ptr,sizeof(int)); exit(0); }
使用dev/zero映射
/************************************************************************* > File Name: dev.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月16日 星期二 21时40分03秒 ************************************************************************/ #include <iostream> #include <fcntl.h> #include <unistd.h> #include <semaphore.h> #include <sys/mman.h> using namespace std; int main() { int fd=open("dev/zero",O_RDWR); int *ptr=(int *)mmap(nullptr,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); close(fd); return 0; }
代码 github连接:https://github.com/tianzengBlog/test/tree/master/ipc2/mmap
访问内存映射区对象
文件大小等于内存映射区大小,但这个大小不是页面大小的整数倍。比如内存映射区大小我5000,页面的大小为4096,在程序中仍能访问第二页(4096-8191),但访问第三页引发SIGESGV信号。所以内核允许读写最后一页中映射区以远的部分(内核的内存保护以页为单位),但向这部分扩展区写的任何内容都不会写到文件中,
共享内存区对象
由shm_open打开一个IPC的名字(也许是在文件系统中的某个路径),所返回的描述符由mmap函数映射到当前进程的地址空间。
- 指定一个参数名字调用shm_open,以创建一个新的共享内存区对象或打开一个已存在的共享内存区对象
- 调用mmap把这个共享内存区映射到调用进程的地址空间,传递给shm_open的名字参数随后由希望共享该内存区的任何其他进程调用
#include <sys/mman.h> int shm_open(const char *name,int oflag,mode_t mode); //成功返回非负描述符,若出则为-1 int shm_unlink(const char *name); //成功返回0,出错返回-1
- olfag:必须或者含有O_RDONLY,或者含有O_RDWR,还可以指定O_CREAT,O_EXCL,或O_TRUNC。如果随O_RDWR指定O_TRUNC,且所需的共享内存已存在,那么长度将会被截为0
- mode:在指定O_CREAT标志前提下使用,如果没有使用O_CREAT该参数置0,该标志必须指定。
- 删除一个共享内存区的对象名,跟其他所有unlink一样(删除文件系统的一个路径名,删除一个posix消息队列的mq_unlink,删除一个有名信号量sem_unlink)一样,删除一个名字不会影响对其底层支持对象的现有引用,直到对该对象的全部引用关闭为止,删除一个名字仅仅为防止后续的open,mq_oepn,sem_open调用取得成功
- 共享内存在不同的进程中可以出现不同的地址
- 新创建的共享内存区对象大小应该为0,可用ftrucate改变共享内存对象大小,用fstat查看共享内存对象大小
- 共享内存对象名不应该和消息队列名相同
代码连接:https://github.com/tianzengBlog/test/tree/master/ipc2/mmap/shm
ftruncate
普通文件或共享内存区的大小都可以通过该函数修改,并且调用fstat获得对象的信息。
#include <unistd.h> int ftruncate(int fd,off_t leght); //成功返回0失败返回-1
- 对于一个普通文件,如果该文件的大小大于lenght参数,额外的数据就会被丢弃,如果该文件的大小小于lenght,那么该文件是否修改及其大小是否增长是未加说明的。实际上对于普通文件来说,把他的大小扩展到length字节的可移植方法是:先lseek到偏移字节lenght-1处,然后write 1个字节的数据,如果大小被扩展,那么扩展的部分显得好像已用0填写过
- 对于一个共享内存区对象:ftruncate把该对象的大小设置为length字节,如果共享内存区被扩展,那么扩展部分全为0
共享内存区对象与内存映射文件区别
- 内存映射文件的数据载体是物理文件。
- 共享内存区对象,也就是共享的数据载体是物理内存。