实现进程间通信最简单也是最直接的方法就是共享内存——为参与通信的多个进程在内存中开辟一个共享区。由于进程可以直接对共享内存进行读写操作,因此这种通信方式效率特别高,但其弱点是,它没有互斥机制,需要信号量之类的手段来配合。
共享内存原理与shm系统
共享内存,顾名思义,就是两个或多个进程都可以访问的同一块内存空间,一个进程对这块空间内容的修改可为其他参与通信的进程所看到的。
显然,为了达到这个目的,就需要做两件事:一件是在内存划出一块区域来作为共享区;另一件是把这个区域映射到参与通信的各个进程空间。
通常在内存划出一个区域的方法是,在内存中打开一个文件,若通过系统调用mmap()把这个文件所占用的内存空间映射到参与通信的各个进程地址空间,则这些进程就都可以看到这个共享区域,进而实现进程间的通信。
为了方便,再把mmap()的原理简述如下:
mmap()原型如下:
void * mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);
其中,参数fd用来指定被映射的文件;offset指定映射的起始位置偏移量(通常为0);len指定文件被映射部分的长度;start用来指定映射到虚地址空间的起始位置(通常为NULL,即由系统确定)。
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
mmap()映射过程示意图如下所示:
那么mmap是怎么形成这个文件映射过程呢?
mmap本身其实是一个很简单的操作,在进程页表中添加一个页表项,该页表项是物理内存的地址。调用mmap的时候,内核会在该进程的地址空间的映射区域查找一块满足需求的空间用于映射该文件,然后生成该虚拟地址的页表项,改页表项此时的有效位(标志是否已经在物理内存中)为0,页表项的内容是文件的磁盘地址,此时mmap的任务已经完成。
简而言之,就是在进程对应的虚存段添加一个段,也就是创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。在创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,引发缺页异常,内核进行请页。
IPC的共享内存通信方式与上面的mmap()方式极为相似,但因为建立一个文件的目的仅是为了通信,于是这种文件没有永久保存的意义,因此IPC并没有使用正规的文件系统,而是在系统初始化时在磁盘交换区建立了一个专门用来实现共享内存的特殊临时文件系统shm,当系统断电后,其中的文件会全部自行销毁。
文章参考:mmap内存映射、认真分析mmap:是什么 为什么 怎么用。
Linux共享内存结构
Linux的一个共享内存区由多个共享段组成。用来描述共享内存段的内核数据结构shmid_kernel如下:
struct shmid_kernel /* private to the kernel */ { struct kern_ipc_perm shm_perm; //描述进程间通信许可的结构 struct file * shm_file; //指向共享内存文件的指针 unsigned long shm_nattch; //挂接到本段共享内存的进程数 unsigned long shm_segsz; //段大小 time_t shm_atim; //最后挂接时间 time_t shm_dtim; //最后解除挂接时间 time_t shm_ctim; //最后变化时间 pid_t shm_cprid; //创建进程的PID pid_t shm_lprid; //最后使用进程的PID struct user_struct *mlock_user; };
shmid_kernel中最重要的域是指针shm_file,它指向临时文件file对象。当进程需要使用这个文件进行通信时,由内核负责将其映射到用户地址空间。
为了便于管理,内核把共享内存区的所有描述结构shmid_kernel都存放在结构ipc_id_ary中的一个数组中。结构ipc_id_ary的定义如下:
struct ipc_id_ary { int size; struct kern_ipc_perm *p[0]; //存放段描述结构的数组 };
同样,为了描述一个共享内存区的概貌,内核使用了数据结构ipc_ids。该结构的定义如下:
struct ipc_ids { int in_use; unsigned short seq; unsigned short seq_max; struct rw_semaphore rw_mutex; struct idr ipcs_idr; struct ipc_id_ary *entries; //指向struct ipc_id_ary的指针 };
由多个共享段组成的共享区的结构如下所示:
共享内存的使用
头文件:
#include <sys/shm.h>
共享内存的打开或创建
进程可以通过调用函数shmget()来打开或创建一个共享内存区。函数shmget()内部由系统调用sys_shmget来实现。函数shmget()的原型如下:
int shmget(key_t key, size_t size, int flag);
其中,参数key为用户给定的键值。
所谓的键值,是在IPC的通信模式下每个IPC对象的名字。进程通过键值识别所有的对象。如果不使用键,进程将无法获取IPC对象,因此IPC对象并不存在于进程本身所使用的的内存中。
因此任何进程都无法为一块共享内存定义一个键值。因此,在调用函数shmget()时,需要key设为IPC_PRIVATE,这样,操作系统将忽略键,建立一个新的共享内存,指定一个键值并返回这块共享内存的IPC标识符ID,然后再设法将这个新的共享内存的标识符ID告诉其他需要使用这个共享内存区的进程。
函数中的参数size为所申请的共享存储段的长度(以页为单位)。
函数中的参数flag为标志,常用的有效标志有IPC_CREAT和IPC_EXCL,它们的功能与文件打开函数open()的O_CREAT和O_EXCL相当。如果用户希望所创建的共享内存区可读,则需要使用标志S_IRUSR;若可读,则需要使用标志S_IWUSR。
函数shmget()调用成功后,返回共享内存区的ID,否则返回-1。
Linux用shmid_ds数据结构表示每个新建的共享内存。当shmget()创建一块新的共享内存后,返回一个可以引用该共享内存的shmid_ds数据结构的标识符。定义在include/linux/shm.h文件中的shmid_ds如下:
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 */ };
例如:调用函数shmget()为当前进程创建一个共享内存区。
代码如下:
int main(void) { int shmid; if((shmid = shmget(IPC_PRIVATE, 10, IPC_CREAT)) < 0) { perror("shmget error!"); exit(1); }else printf("shmget success!"); return 0; }
共享内存与进程的连接
如果一个进程已创建或打开一个共享内存,则在需要使用它时,要调用函数shmat()把该共享内存连接到进程上,即要把待使用的共享内存映射到进程空间。函数shmat()通过系统调用sys_shmat()实现。函数shmat()的原型如下:
void * shmat(int shmid, char __user * shmaddr, int shmflg);
其中,参数shmid为共享内存的标识;参数shmaddr为映射地址,如果该值为0,则由内核决定;参数shmflg为共享内存的标志,如果shmflg的值为SHM_RDONLY,则进程以只读的方式访问共享内存,否则以读写方式访问共享内存。
若函数调用成功,则返回共享存储段地址;若出错,则返回-1。
断开共享内存与进程的连接
调用函数shmdt()可以断开共享内存与进程的连接,其原型如下:
int shmdt(coid * addr);
其中,参数addr为共享存储段的地址,即调用shmat时的返回值。shmdt将使相关shmid_ds结构中的shm_nattch计数器值减1。
共享内存的控制
调用函数shmctl()可以对共享内存进行一些控制,其原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds * buf);
其中,参数shmid为共享存储段的ID;参数cmd为控制命令,常用的值有IPC_STAT(赋值)、IPC_SET(赋值)、IPC_RMID(删除)、SHM_LOCK(上锁)、SHM_UNLOCK(解锁)等等;参数buf为struct shmid_ds类型指针,由buf返回的数值与命令参数cmd表示的操作相关。
共享内存不会随着程序的结束而自动消除,要么调用shmctl()删除,要么手动使用命令ipcrm -m shmid去删除,否则一直保留在系统中,直至系统掉电。
例子:调用函数shmget()为当前进程创建一个共享内存区并使用它。
代码如下:
#include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <sys/stat.h> int main(void) { int shm_id; //定义共享内存键 char* shared_memory; //定义共享内存指针 struct shmid_ds shmbuffer; //定义共享内存缓冲 int shm_size; //定义共享内存大小 shm_id = shmget(IPC_PRIVATE, 0x6400, IPC_CREAT | IPC_EXCL | S_IRUSE | S_IWUSE); //创建一个共享内存区 shared_memory = (char*)shmat(shm_id, 0, 0); //绑定到共享内存 printf("shared memory attached at address %p\n", shared_memory); shmctl(shm_id, IPC_STAT, &shmbuffer); //读共享内存结构struct shmid_ds shm_size = shmbuffer.shm_segsz; //自结构struct shmid_ds获取内存大小 printf("segment size:%d\n", shm_size); sprintf(shared_memory, "Hello,world."); //向共享内存中写入一个字符串 shmdt(shared_memory); //脱离该共享内存 shared_memory = (char*)shmat(shm_id, (void *)0x500000, 0); //重新绑定共享内存 printf("shared memory reattched at address %p\n", shared_memory); printf("%s\n", shared_memory); shmdt(shared_memory); //脱离该共享内存 shmctl(shm_id, IPC_RMID, 0); //释放共享内存 return 0; }
共享内存的互斥
从上面的叙述中可以看到,共享内存是一种低级的通信机制,它没有提供进程间同步和互斥的功能。所以,共享内存通常是要与信号量结合使用。