目录
5.1、父进程子进程通过mmap在内存映射文件中给计数器持续加1
5.2、父子进程共享信号量与计数的结构体,通过内存信号量实现。
12、mlockall()函数/munlockall()函数
1、简介
1.1、共享内存区的优缺点
优点 | 1、共享内存区是IPC形式中最快的。当内存区映射到共享它的进程的地址空间,这些进程间数据的传递就不再涉及内核(进程不再通过执行任何进入内核的系统调用来彼此传递数据)。 2、把显示的I/O操作变换成存取内存单元,可以往往能简化程序,又是还能改善性能。 |
缺点 | 往共享内存区存放信息和从众取走信息的进程间通常需要某种形式的同步。 |
注:对于管道、FIFO和消息队列,进程要交换信息时,这些消息必须经由内核传递。换句话说,数据复制在内核和进程间进行,往往开销很大(比纯粹在内核中或单个进程内复制数据的开销大)。通过让进程共享一个内存区,可以绕过上述问题,所有说共享内存区是IPC形式中最快的。
1.2、对共享内存区的操作
一旦内存映射了一个文件,就不能再使用read,write和lseek来访问该文件,而只是存取已由mmap映射到该文件的内存位置。
注:不是所有的文件都能进行内存映射。例如,把一个访问终端或嵌套字的描述符映射到内存将导致mmap返回一个错误。这些类型的描述符必须使用read和write(或它们的变体)来访问。
1.3、共享内存区的方法
内存映射文件 | 由open函数打开,由mmap函数把得到的描述符映射到当前进程地址空间。 |
共享内存区对象 | 由shm_open函数打开一个Posix1 IPC名字,所返回的描述符由mmap函数映射到当前进程的地址空间 |
注:这两种技术都需要调用mmap,差别在于作为mmap的参数之一的描述符的获取方法:调用open或通过shm_open。
1.4、普通 I/O 与存储映射 I/O 比较
方式 | 缺点 | 优点 |
普通 I/O | 普通 I/O 方式一般是通过调用 read()和 write()函数来实现对文件的读写, 使用 read()和 write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的缓存间倒腾,效率会比较低。 同样使用标准 I/O(库函数 fread()、 fwrite())也是如此,本身标准 I/O 就是对普通 I/O 的一种封装。 | 如果数据量比较小,影响并不大,使用普通的 I/O 方式比较方便。 |
存储映射 I/O | 存储映射 I/O 方式所映射的文件只能是固定大小,因为文件所映射的区域已经在调用mmap()函数时通过 length 参数指定了。另外,文件映射的内存区域的大小必须是系统页大小的整数倍(虽然可以通过映射地址访问剩余的这些字节数据,但不能在映射文件中反应出来)。 | 使用存储映射 I/O 减少了数据的复制操作, 所以在效率上会比普通 I/O 要 高。 |
1)使用存储映射 I/O 在进行大数据量操作时比较有效;对于少量数据,使用普通 I/O 方式更加方便!
2)存储映射 I/O 在处理大量数据时效率高,对于少量数据处理不是很划算,所以存储映射 I/O 会在视频图像处理方面用的比较多,譬如Framebuffer 编程(LCD 编程)。
2、mmap()函数
把一个文件或Posix共享内存区对象映射到调用进程的地址空间(对于同一个共享内存区对象,调用mmap的每个进程所得到的mmap返回值可能不同)。使用该函数有3个目的:
1 | 使用普通文件以提供内存映射IO |
2 | 使用特殊文件以提供匿名内存映射(有缘关系进程间) |
3 | 使用shm_open以提供无亲缘关系进程间的Posix共享内存区 |
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
参数 addr:可以指定描述符fd应被映射到的进程内空间的起始地址(通常被指定为一个空指针,让内核自己去选择起始地址)。
参数 length:映射区的大小(从被映射文件开头起第offset个字节处开始算)。
参数 port:内存映射的保护,常见值为代表可写访问的PORT_READ|PORT_WRITE。
PORT_READ | 数据可读 |
PORT_WRITE | 数据可写 |
PORT_EXEC | 数据可执行 |
PORT_NONE | 数据不能访问 |
注:PORT_NONE的作用。linux - mmap内存保护PROT_NONE的目的是什么? - Thinbug
参数 flags:MAP_SHARED和MAP_PRIVATE这两个标志必须指定一个,并可有选择地或上MAP_FIXED。
MAP_SHARED | 变动是共享的 | 如果指定了MAP_PRIVATE,那么调用进程对被映射数据所做的修改只对该进程可见,而不改变其底层支持对象(或者是一个文件对象,或者是一个共享内存区对象)。 |
MAP_PRIVATE | 变动是私自的 | 如果指定了MAP_SHARED,那么调用进程对被映射数据所做的修改对于共享该对象的所有进程都可见,而且确实改变了其底层支持对象。 |
MAP_FIXED | 准确地解释addr参数 | 从移植性上考虑,MAP_FIXED不应该指定。如果没有指定该标志,但是addr不是一个空指针,那么addr如何处理取决于实现。不为空的addr值通常被当作有关该内存区应如何具体定位的线索。可移植的代码应把addr指定为空指针,并且不指定MAP_FIXED。 |
MAP_ANONYMOUS | 建立匿名映射 | 此时会忽略参数 fd 和 offset,不涉及文件,而且映射区域 |
MAP_ANON | 建立匿名映射 | 与 MAP_ANONYMOUS 标志同义 |
MAP_LOCKED | 对映射区域进行上锁 |
参数 fd:描述符
参数 offset:偏移量,通常设置为0。
返回值:成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1, 通常使用MAP_FAILED 来表示, 并且会设置 errno 来指示错误原因
注:mmap成功返回后,fd参数可以关闭,该操作对于由mmap建立的映射关系没有影响。
注:对于 mmap()函数,参数 addr 和 offset 在不为 NULL 和 0 的情况下, addr 和 offset 的值通常被要求是系统页大小的整数倍,可通过 sysconf()函数获取页大小:
sysconf(_SC_PAGE_SIZE)
或
sysconf(_SC_PAGESIZE)
3、munmap()函数
从某个进程的地址空间删除一个映射关系。如果被映射区时使用MAP_PRIVATE标志映射的,那么调用进程对它所做的变动都会被丢弃。
#include <sys/mman.h>
int munmap(void *addr, size_t length);
参数 addr:由mmap返回的地址,指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍。
参数 length:映射区的大小。
返回值:成功则为0,失败则为-1。
注:再次访问这些地址将导致向调用进程产生一个SIGSEGV信号。
4、mprotect()函数
使用系统调用mprotect()可以更改一个现有映射区的保护要求。
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
参数 addr: 地址范围的起始地址
参数 len:该地址范围的大小。
参数 prot:要设置的地址范围的保护要求的类型
返回值:成功返回 0; 失败将返回-1,并且会设置 errno 来只是错误原因。
4、msync()函数
同步硬盘上的文件内容和内存映射区的内容,使其一致。
注:如果修改了处于内存映射到某个文件的内存区的内容,那么内核将在稍后某个时刻相应地更新文件(前提是它是一个MAP_SHARED内存区)。
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
参数 addr:由mmap返回的地址。
参数 length:映射区的大小。
参数 flags:MS_ASYNC和MS_SYNC中必须指定一个。它们的区别是,一旦写操作已由内核排入队列,MS_ASYNC即返回。而MS_SYNC则要等到写操作完成后才返回。如果指定了MS_INVALIDATE,那么与其最终副本不一致的文件数据的所有内存中副本都失效。
flags | 说明 |
MS_ASYNC | 执行异步写 |
MS_SYNC | 执行同步写 |
MS_INVALIDATE | 使高速缓存的数据失效 |
返回值:成功则为0,失败则为-1。
5、示例
5.1、父进程子进程通过mmap在内存映射文件中给计数器持续加1
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/mman.h>
int count = 0;
#define SEM_NAME "/mysem1"
int main(int argc, char **argv)
{
int fd = 0;
int zero = 0;
int nLoop = 0;
fd = open(argv[1], O_RDWR | O_CREAT, 0666);
write(fd, &zero, sizeof(int));
nLoop = atoi(argv[2]);
int *ptr = NULL;
sem_t *pMutex = NULL;
ptr = (int *)(mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
close(fd);
pMutex = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0666, 1);
sem_unlink(SEM_NAME);
if (NULL == pMutex)
{
printf("NULL,err:%d\n", errno);
}
if (0 == fork())
{
for (int i = 0; i < nLoop; i++)
{
sem_wait(pMutex);
printf("child:%d\n", (*ptr)++);
sem_post(pMutex);
}
exit(0);
}
for (int i = 0; i < nLoop; i++)
{
sem_wait(pMutex);
printf("parent:%d\n", (*ptr)++);
sem_post(pMutex);
}
return 0;
}
注:通过调用sem_unlink从系统中删除该信号量的名字,但是尽管这么一来删除了它的路径名,对于已经打开的信号量却没影响。这样做即使本程序终止了,该路径名也从系统中删除。
5.2、父子进程共享信号量与计数的结构体,通过内存信号量实现。
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
typedef struct{
sem_t mutex;
int count;
}T_Shared;
T_Shared g_shared;
int main(int argc, char **argv)
{
int fd = 0;
int i = 0;
int nLoop = 0;
int *ptr = NULL;
fd = open(argv[1], O_RDWR | O_CREAT, 0666);
write(fd, &g_shared, sizeof(T_Shared));
ptr = (int *)(mmap(NULL, sizeof(T_Shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
close(fd);
nLoop = atoi(argv[2]);
sem_init(&g_shared.mutex, 0, 1);
if (0 == fork())
{
/* child */
for (i = 0; i < nLoop; i++)
{
sem_wait(&g_shared.mutex);
printf("child:%d\n", g_shared.count++);
sem_post(&g_shared.mutex);
}
exit(0);
}
/* parent */
for (i = 0; i < nLoop; i++)
{
sem_wait(&g_shared.mutex);
printf("parent:%d\n", g_shared.count++);
sem_post(&g_shared.mutex);
}
exit(0);
}
6、匿名内存映射
如果调用mmap的目的是提供一个将穿越fork由父子进程共享的映射内存区,那么可以简化上述情况。
匿名内存映射 | 把mmap的flags参数指定为MAP_SHARED |MAP_ANON,把fd参数指定为-1,offset参数被忽略,这样的内存区初始化为0。 |
/dev/zero内存映射 |
6.1、BSD匿名内存映射
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/mman.h>
int count = 0;
#define SEM_NAME "/mysem1"
int main(int argc, char **argv)
{
int nLoop = 0;
nLoop = atoi(argv[1]);
int *ptr = NULL;
sem_t *pMutex = NULL;
ptr = (int *)(mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED
| MAP_ANON, -1, 0));
pMutex = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0666, 1);
sem_unlink(SEM_NAME);
if (NULL == pMutex)
{
printf("NULL,err:%d\n", errno);
}
if (0 == fork())
{
for (int i = 0; i < nLoop; i++)
{
sem_wait(pMutex);
printf("child:%d\n", (*ptr)++);
sem_post(pMutex);
}
exit(0);
}
for (int i = 0; i < nLoop; i++)
{
sem_wait(pMutex);
printf("parent:%d\n", (*ptr)++);
sem_post(pMutex);
}
return 0;
}
6.2、/dev/zero匿名内存映射
对/dev/zero设备文件的任何读,所返回的是所请求数目的全为0的字节。write到该设备的任何数据被直接丢弃掉,就像write到/dev/null设备一样。
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/mman.h>
int count = 0;
#define SEM_NAME "/mysem1"
int main(int argc, char **argv)
{
int nLoop = 0;
int fd;
nLoop = atoi(argv[1]);
int *ptr = NULL;
sem_t *pMutex = NULL;
fd = open("/dev/zero",O_RDWR);
ptr = (int *)(mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED , fd, 0));
pMutex = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0666, 1);
sem_unlink(SEM_NAME);
if (NULL == pMutex)
{
printf("NULL,err:%d\n", errno);
}
if (0 == fork())
{
for (int i = 0; i < nLoop; i++)
{
sem_wait(pMutex);
printf("child:%d\n", (*ptr)++);
sem_post(pMutex);
}
exit(0);
}
for (int i = 0; i < nLoop; i++)
{
sem_wait(pMutex);
printf("parent:%d\n", (*ptr)++);
sem_post(pMutex);
}
return 0;
}
7、访问内存映射对象
内存映射一个普通文件时,内存映射区的大小通常等于该文件的大小。然而文件大小和内存映射区大小可以不同(内核允许给mmap指定一个大于该对象大小的大小参数,但是访问不了该对象以远的部分)。
内核允许我们读写最后一页中映射区以远的地方(内核的内存保护是以页面为单位的),但是写向这部分扩展区的任何内容都不会写到文件中。
注:SIGBUS意味着在内存映射区内访问,但是已超过底层支撑对象的大小。SIGSEGV意味着已在内存映射区以远访问。
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/mman.h>
#include <math.h>
int count = 0;
#define SEM_NAME "/mysem1"
int main(int argc, char **argv)
{
int fd,i;
char* ptr;
size_t filesize,mmapsize,pagesize;
if(argc!=4){
printf("usage:xxx <pathname> <filesize> <mmapsize>\n");
exit(-1);
}
filesize = atoi(argv[2]);
mmapsize = atoi(argv[3]);
fd = open(argv[1], O_RDWR | O_CREAT |O_TRUNC, 0666);
lseek(fd,filesize-1,SEEK_SET);
write(fd,"", 1);
ptr = mmap(NULL, mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
pagesize = sysconf(_SC_PAGESIZE);
printf("PAGESIZE = %d\n",pagesize);
for (int i = 0; i <((filesize>mmapsize)?filesize:mmapsize); i+=pagesize)
{
printf("ptr[%d] = %d\n",i,ptr[i]);
ptr[i] = 1;
printf("ptr[%d] = %d\n",i+pagesize-1,ptr[i+pagesize-1]);
ptr[i+pagesize-1] = 1;
}
printf("ptr[%d]=%d\n",i,ptr[i]);
exit(0);
}
8、shm_open()函数
打开一个共享内存区。
#include <unistd.h>
int shm_open(const char *name, int oflag, mode_t mode);
参数 name:共享内存区名字
参数 oflag:必须或者含有O_RDONLY标志,或者O_RDWR标志,还可以指定如下标志:O_CREAT,O_EXCL或O_TRUNC。
参数 mode:指定权限位。
返回值:若成功则为非负描述符,若出错则为-1。
9、shm_unlink()函数
删除一个共享内存区的名字。
#include <unistd.h>
int shm_unlink(const char *name);
参数 name:共享内存区名字
返回值:成功则为0,出错则为-1。
注:删除一个名字不会影响对于底层支持对象的现有引用,直到对于该对象的引用全部关闭为止。
10、ftruncate()函数
修改普通文件或共享内存区对象的大小。
普通文件 | 如果文件的大小大于length,额外的数据就被丢弃。 如果文件的大小小于length,那么改文件的修改以及其大小是否增长是未加说明的。 对于普通文件,把它的大小扩展到length字节的可移植方法是:先lseek到偏移为length-1处,然后write 1个字节的数据。 |
共享内存区 | 把对象设置为length字节 |
#include <unistd.h>
int ftruncate(int fd, off_t length);
参数 fd:描述符
参数 length:长度
返回值:成功则为0,出错则为-1。
11、fstat()函数
当打开一个已存在的共享内存区对象时,可调用fstat函数获取有关该对象的信息。
#include <unistd.h>
int fstat(int fd,struct stat *buf);
参数 fd:描述符
参数 buf:保存对象信息的结构体指针。
返回值:成功贼0,出错则为-1.
注:当fd指代一个共享内存区对象时,只有四个成员含有信息(st_mode、st_uid、st_gid、st_size)。
12、mlockall()函数/munlockall()函数
mlockall()使调用进程的整个内存空间常驻内存。
munlockall()撤销这种锁定
13、mlock()函数
mlock()会使调用进程地址空间的某个指定范围常驻内存,该函数的参数指定了这个范围的起始地址以及从该地址算起的字节数。
munlock则撤销某个内存区的锁定。
14、System V共享内存区
System V共享内存区在概念是类似Posix共享内存区。对于每个System V共享内存区,内核维护如下的信息结构。
#include <sys/shm.h>
struct ipc_perm
{
uid_t uid; /* 属主用户ID */
gid_t gid; /* 属主组ID */
uid_t cuid; /* 创造者用户ID */
gid_t cgid; /* 创造者组ID */
unsigned short mode; /* 读写权限 */
unsignedshort seq; /* 槽位使用情况序列号 */
key_t key; /* IPC键 */
};
struct shmid_ds{
struct ipc_perm shm_perm; /* 操作权限 */
size_t shm_segsz; /* 段的大小(以字节为单位)*/
pid_t shm_lpid; /* 在该段上操作的最后1个进程的pid */
pid_t shm_cpid; /* 创建该段进程的pid */
shmatt_t shm_npages; /* 段的大小(以页为单位)*/
shmat_t shm_cnpages; /* in-core #attached */
time_t shm_atime; /* 最后一个进程附加到该段的时间 */
time_t shm_dtime; /* 最后一个进程离开该段的时间 */
time_t shm_ctime; /* 最后一个进程修改该段的时间 */
};
14.1、shmget()函数
创建一个共享内存区,或者访问一个已存在的共享内存区。
注:创建新的共享内存区时,该内存区被初始化为size字节的0。
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数 key:ftok的返回值,也可以是IPC_PRIVATE。
参数 size:指定内存区的大小(创建时不为0,访问时为0)。
参数 shmflg:读写权限位,可以与IPC_CREAT或IPC_CREAT|IPC_EXCL按位或。
数字值(八进制) | 共享内存区 | 说明 |
0400 | SHM_R | 由用户(属主)读 |
0200 | SHM_W | 由用户(属主)写 |
0040 | SHM_R>>3 | 由(属)组成员读 |
0020 | SHM_W>>3 | 由(属)组成员写 |
0004 | SHM_R>>6 | 由其他用户读 |
0002 | SHM_W>>6 | 由其他用户写 |
返回值:成功则为共享内存区对象,出错则为-1。
注:Posix共享内存区对象的大小可在任何时刻通过调用ftruncate修改,而System V共享内存区对象的大小是在调用shmget创建时固定的。
14.2、shmat()函数
将共享内存区对象附接到调用进程的地址空间。
#include <sys/shm.h>
void *shmat(int shm_id, const void *shm_addr, int shmflg);
参数 shm_id:共享内存区对象
参数 shm_addr:
空指针 | 那么系统选择地址。 |
非空指针 | 没有指定SHM_RND,那么相应的共享内存区附接到shm_addr参数指定的地址 |
指定了SHM_RND,那么相应的共享内存区附接到shm_addr参数指定的地址向下舍入一个SHMLAB常量(LBA表示“低端边界地址”). |
参数 shmflg:SHM_RND、SHME_RDONLY。
默认情况下,只要调用进程具有某个共享内存的读写权限,它附接该内存区就能够同时读写该内存区。shmflg参数也可以指定SHM_RDONLY值,它限定只读访问。
返回值:成功则为共享内存区对象,出错则为-1。
14.3、shmctl()函数
断接内存区。
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数 shm_addr:共享内存起始地址。
返回值:成功则为0,出错则为-1。
注:当一个进程终止时,当前附接的所有共享内存区都将自动断接。
12.4、shmctl()函数
对共享内存区的多种操作。
#include <sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_ds *buf);
参数 shm_id:共享内存区对象
参数 command:操作命令
IPC_RMID | 从系统中删除由shmid标识的共享内存区并拆除 注:拆除指释放和回收对应的数据结构,包括删除存放在上面的数据(拆除要到该共享内存区的引用计数变为0时才进行)。 |
IPC_SET | 给所指定的共享内存区设置shmid_ds 结构的3个成员:shm_perm.uid、shm_perm.gid、shm_perm.mode。shm_ctime的值也用当前时间替换。 |
IPC_STAT | 返回所指定共享内存区当前的shmid_ds结构。 |
SHM_LOCK | 用于锁定内存,禁止内存交换( 超级用户 )。并不代表共享内存被锁定后禁止其它进程访问。其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。这样做的优势在于让共享内存一直处于内存中,从而提高程序性能。 |
SHM_UNLOCK | 解锁共享内存段。 |
参数 buf:shmid_ds结构体指针。
返回值:成功则为0,出错则为-1。