重温 通信编程之二:共享内存
每一个进程都有自己独立的地址空间,把同一块内存,分别映射到不同进程的地址空间的某个地址中去,这样每个进程都可以通过自己映射的地址来访问这块共享内存,优点就是,多个进程交换数据时,不用做任何的拷贝,效率高。遗憾的是,linux共享内存机制并没有提供同步机制,一般我们还需要使用其他进程间的通信方法(如信号量、消息队列、管道等)来配合共享内存机制。
因此开辟共享内存,需要做两件事:一件是在内存划出一块区域来作为共享区;另一件是把这个区域映射到参与通信的各个进程空间。通常在内存划出一个区域的方法是,在内存中打开一个文件,若通过系统调用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本身其实是一个很简单的操作,
a.进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域。即在进程页表中添加一个页表项,该页表项是物理内存的地址。调用mmap的时候,内核会在该进程的物理地址空间的映射区域查找一块满足需求的连续的虚拟空间用于映射该文件,然后生成该虚拟地址的页表项,改页表项此时的有效位(标志是否已经在物理内存中)为0,页表项的内容是文件的磁盘地址,此时mmap的任务已经完成。
简而言之,就是在进程对应的虚存段添加一个段,也就是创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。
b.调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系。即在创建虚拟区间并完成地址映射,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。通过该文件的文件结构体,链接到file_operations模块,调用不同于用户空间的同名内核函数mmap,其原型为:
int mmap(struct file *filp, struct vm_area_struct *vma),这样才通过*filp,实现物理地址和虚拟地址的映射。
c.此时并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,引发缺页异常,内核进行请求调页:
调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中;
之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也就完成了写入到文件的过程(可以调用msync( void *addr, size_t len, int flags)来强制同步, 这样所写的内容就能立即保存到文件里了)。
IPC的共享内存通信方式与上面的mmap()方式极为相似,但因为建立一个文件的目的仅是为了通信,于是这种文件没有永久保存的意义,因此IPC并没有使用正规的文件系统,而是在系统初始化时在磁盘交换区建立了一个专门用来实现共享内存的特殊临时文件系统shm,当系统断电后,其中的文件会全部自行销毁。
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都存放在结构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>
1.共享内存的打开或创建
进程可以通过调用函数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告诉其他需要使用这个共享内存区的进程。
也就是说,它和内核空间打开文件的mmap()的区别是,它创造出的内存不归于某个进程所有,而是系统的内存管理统一管理。另外,即便键值不设置为IPC_PRIVATE,但shmflg包含IPC_CREAT标志,内核也会创建一个新的共享内存。
第二个参数size,是要申请的内存的大小,注意,该值为内存页大小的整数倍,内存页大小的宏定义为PAGE_SIZE,32位操作系统一般设置为4K。
最后一个是标志,常用的有效标志有
IPC_CREAT 创建新的共享内存,否则,会查找跟key关联的、已经存在的、且本进程有访问权的共享内存;
IPC_EXCL 该标志一般与IPC_CREAT 同时使用,作用是,若key关联的共享内存已存在,则shmget返回失败,而不是返回已存在的共享内存shmid;
SHM_HUGETLB 使用huge page来分配内存;
S_IRUSR 共享内存区可读
S_IWUSR 共享内存区可写
调用成功后,返回共享内存区的ID,它代表了一块由shmid_ds结构所描述的共享内存,否则返回-1。
定义在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 */
}
2. 共享内存与进程的连接、断开
void *shmat(int shmid, const void *shmaddr, int shmflg);//映射共享内存到本进程的地址空间
int shmdt(const void *shmaddr);//取消映射
其中,参数shmid为共享内存的标识,是shmget的返回值;
参数shmaddr为映射地址,如果该值为0,则由内核决定;
参数shmflg为共享内存的标志,有三个值SHM_RND、SHM_RDONLY、SHM_REMAP。
SHM_RND, shmaddr值会被自动向下圆整为SHMLBA的整数倍,否则必须保证shmaddr的实参是个页面对齐的地址。
SHM_RDONLY 则进程以只读的方式访问共享内存,否则以读写方式访问共享内存。
SHM_REMAP 把shmid指定的共享内存重新映射到shmaddr指定的地址(大小为size),如果shmaddr指定的地址(整个size范围内)早就被占用了,那么本函数会返回错误,这种情况下,只能把shmaddr设定为NULL。
参数addr为共享存储段的地址,即调用shmat时的返回值。shmdt将使相关shmid_ds结构中的shm_nattch计数器值减1。
若函数调用成功,则返回共享存储段地址;若出错,则返回-1并设置errno。
本函数执行完之后,本共享内存的描述结构shmid_ds的三个成员会被更新:shm_atime更新为当前时间、shm_lpid更新为本进程的PID、shm_nattch加1。
fork出的子进程可以继承父进程映射好的共享内存;通过execve函数启动新程序之后,前面的映射好的共享内存全部都会被detach掉;_exit(2)函数也会把前面的映射好的共享内存全部detach掉。
3.共享内存的控制
调用函数shmctl()可以对共享内存进行一些控制,其原型如下:
int shmctl (int shmid, int cmd, struct shmid_ds * buf);
其中,参数shmid为共享存储段的ID;
参数cmd为控制命令,常用的值有IPC_STAT(读值)、IPC_SET(赋值,只有shm_perm.uid、shm_perm.gid、 shm_perm.mode(低9位))、IPC_RMID(删除,注意的是并不是真正删除,需要等到shm_nattch成员的值会变成0)、SHM_LOCK(上锁禁止该共享内存的交换,shm_perm.mode会被加入SHM_LOCKED标志)、SHM_UNLOCK(解锁允许共享内存交换)、IPC_INFO(读取共享内存的各种限制数、参数,用结构体shminfo存储)、SHM_INFO(用结构体shm_info存储,注意与前命令的结构体名字相差一个下划线)、SHM_STAT(作用等同于IPC_STAT,区别在于传实参时,shmid对应的实参改为一个内核数组索引,而这个数组维护着系统中所有[!]共享内存的信息)等等;
参数buf为struct shmid_ds类型指针,由buf返回的数值与命令参数cmd表示的操作相关。
共享内存不会随着程序的结束而自动消除,要么调用shmctl()删除,要么手动使用命令ipcrm -m shmid去删除,否则一直保留在系统中,直至系统掉电(shm是临时文件系统)。
下面的代码来源于网络
调用函数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;
}
应用实例:父进程占有信号量,然后fork出子进程,然后阻塞一秒,然后往共享内存写入一个字符串,然后释放信号量。子进程等待信号量,等待成功后,读取共享内存中的数据。
如果不使用信号量进行同步的话,在父进程阻塞1秒的时间段内,子进程就会从共享内存读数据,这时父进程还没把数据写进去。
//编译: gcc -o shm_exe sharedMemory.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
#include <errno.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/shm.h>
void V(int semid) //占用资源
{
struct sembuf sops[2];
sops[0].sem_num = 0; /* 要操作的信号量的编号为0 */
sops[0].sem_op = 0; /* Wait for value to equal 0 */
sops[0].sem_flg = 0; /* 既不IPC_NOWAIT,也不IPC_UNDO */
sops[1].sem_num = 0; /* 要操作的信号量的编号为0 */
sops[1].sem_op = 1; /* >0 的作用是,把第sem_num个信号量的当前值+sem_op */
sops[1].sem_flg = SEM_UNDO; /* IPC_NOWAIT,SEM_UNDO. */
if (semop(semid, sops, 2) == -1)
{
printf("V->semop failed, infor:%s\r\n", strerror(errno) );
exit(1);
}
}
void P(int semid) //释放资源
{
struct sembuf sops[5];
sops[0].sem_num = 0; /* 要操作的信号量的编号为0 */
sops[0].sem_op = -1; /* -1 */
sops[0].sem_flg = SEM_UNDO; /* IPC_NOWAIT,SEM_UNDO. */
if (semop(semid, sops, 1) == -1)
{
printf("P->semop failed, infor:%s\r\n", strerror(errno) );
exit(1);
}
}
/*
* 功能: 创建一个二值信号量
* 形参: filePath文件路径,用于ftok函数
* 返回: 成功则返回信号量id,失败则直接令调用本函数的进程退出
*
*/
int creat_semphore(char* filePath)
{
key_t key = ftok(filePath, 'f');
if(-1 == key)
{
printf("ftok failed, infor:%s\r\n", strerror(errno) );
exit(EXIT_FAILURE);
}
else
{
printf("ftok ok, key = %d\r\n", key );
}
int semid = semget(key, 1, IPC_CREAT | 0666 );//灯集中只要一个灯
if(-1 == semid)
{
printf("semget failed, infor:%s\r\n", strerror(errno) );
exit(EXIT_FAILURE);
}
else
{
printf("semget ok, semid = %d\r\n", semid );
}
return semid;
}
/*
* 功能: 创建一个共享内存
* 形参: filePath 文件路径,用于ftok函数;
size 创建的共享内存的字节数
* 返回: 成功则返回共享内存id,失败则直接令调用本函数的进程退出
*
*/
int creat_sharedMemory(char* filePath, size_t size)
{
key_t key = ftok(filePath, 'f');
if(-1 == key)
{
printf("ftok failed, infor:%s\r\n", strerror(errno) );
exit(EXIT_FAILURE);
}
else
{
printf("ftok ok, key = %d\r\n", key );
}
int shmid = shmget(key, size, IPC_CREAT | 0666 );//灯集中只要一个灯
if(-1 == shmid)
{
printf("shmget failed, infor:%s\r\n", strerror(errno) );
exit(EXIT_FAILURE);
}
else
{
printf("shmget ok, shmid = %d\r\n", shmid );
}
return shmid;
}
int main(int argc, char *argv[])
{
printf("exe file path: %s\r\n", argv[0] );
int semid = creat_semphore(argv[0]);
int shmid = creat_sharedMemory(argv[0], 100);
printf("father occupy semaphore!\r\n");
V(semid);//父进程占用资源,只有父进程写完了,才允许子进程读
printf("father occupied semaphore!\r\n");
pid_t pid = fork();
if(pid == 0 )//子进程
{
printf("child process start\r\n");
char *memAddr = shmat(shmid, NULL, SHM_RND);
V(semid);
printf("child occupied semaphore!\r\n");
printf("child read shared memory: %s\r\n", memAddr);
P(semid);
printf("child released semaphore!\r\n");
_exit(0);
}
else if (pid > 0)//父进程
{
char *memAddr = shmat(shmid, NULL, SHM_RND);
char str[] = "this string is from father process!";
memcpy(memAddr, str, sizeof(str));
sleep(1);
P(semid);
printf("father released semaphore!\r\n");
int stat;
if(waitpid(pid, &stat, 0) != pid)
{
printf("child process failed!\r\n");
}
else
{
printf("child process returned value = %d\r\n", stat);
}
printf("father process return\r\n");
}
else
{
printf("fork failed, infor:%s\r\n", strerror(errno) );
return -1;
}
return 0;
}