本文主要参考《unix网络编程卷2:进程间通信》
另外可以参考以下文章:
System V共享内存: Linux环境进程间通信(五): 共享内存(下)
共享内存区是可用IPC方式中最快的。一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据传递就不再涉及内核(这里的“不再涉及内核”含义是:进程不再通过执行任何进入内核的系统调用来彼此传递数据。显然,内核必须建立允许各个进程共享该内存区的内存映射关系,然后一直管理该内存区,如处理页面故障等)。然而往该共享内存区放信息或从中取信息的进程间通常需要某种形式的同步(同步的几种方式参考文章:Linux进程间通信IPC的几种方式简介)。
示例
通过考虑一个客户-服务器文件复制程序中涉及的通常步骤:
图1 从服务器到客户的文件数据流
(1)服务器从输入文件读。该文件的数据由内核读入自己的内存空间,然后从内核复制到服务器进程。
(2)服务器往管道、FIFO或消息队列以一条消息的形式写入这些数据。这些IPC通常要把这些数据从进程复制到内核。
这里的“通常”是因为POSIX消息队列可使用内存映射I/O(mmap函数)实现,然而管道、FIFO和System V消息队列的write或msgsnd都设计从进程到内核的数据复制,它们的read或msgrcv都设计从内核到进程的数据复制。这些IPC的问题在于,两个进程交换信息时,这些信息都必须经由内核传递。
(3)客户从IPC通道读出这些数据,通常需要把数据从内核复制到进程。
(4)最后,把这些数据从由write函数的第二个参数指定的客户缓冲区复制到输出文件。
这个过程涉及四次数据复制,而且这四次复制都是在内核和某个进程间进行的(四次内核/进程切换),往往开销很大(比内核中或进程内复制数据开销大)。
通过让多个进程共享内存区
(1)服务器使用一个信号量取得访问某个共享内存区对象的权利。
(2)服务器将数据从输入文件读入到该共享内存区对象。read函数的第二个参数所指定的数据缓冲区地址指向这个共享内存区对象。
(3)服务器读入完毕,使用一个信号量通知客户。
(4)客户将这些数据从该共享内存区对象写出到输出文件中。
这里仅涉及两次复制:一次从输入文件到共享内存区,一次从共享内存区到输出文件。默认情况下,通过fork派生的子进程不与父进程共享内存区。
mmap munmap msync函数
mmap将一个磁盘文件或一个POSIX共享内存区对象映射到调用进程的地址空间(虚地址空间)。使用该函数的目的:
1)使用普通文件以提供内存映射I/O;
内存映射文件:通过open打开文件后调用mmap把它映射到调用进程地址空间的某个文件,所有的I/O都在内核的掩盖下完成,只需辨析存取内存映射区中各个值的代码,我们决不调用read、write或lseek,这样简化了我们的代码。
2)使用特殊文件以提供匿名内存映射;
3)使用shm_open以提供无亲缘关系进程间的POSIX共享内存区。
这种情况下,所映射文件的实际内容成了被共享内存区的初始内容,而且这些进程对该共享内存所作的任何改动都复制回所映射的文件(以提供随文件系统的持续性);要设定MAP_SHARED标志,这是进程间共享内存所需要的。
以下分别介绍这三类:
存储映射I/O
《APUE》第二版392页:
如下程序映射I/O复制一个文件:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/stat.h>
#define BUF_SIZE 4096
#define FILE_IN "test.pcap"
int main() {
time_t t1 = time((time_t *)NULL);
printf("ssss %d\n", t1);
void *src, *dst;
int fdin = open(FILE_IN, O_RDONLY);
if (fdin < 0) {
perror("open file read error");
exit(-1);
}
int fdout = open("test2.pcap", O_RDWR | O_CREAT);
if (fdout < 0) {
perror("open file write error");
exit(-1);
}
struct stat statbuf;
if (fstat(fdin, &statbuf) < 0) {
perror("fstat error");
exit(-1);
}
if (lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1) {
perror("lseek error");
exit(-1);
}
if (write(fdout, "", 1) != 1) {
perror("write error");
exit(-1);
}
if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED) {
perror("mmap in error");
exit(-1);
}
if ((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, 0)) == MAP_FAILED) {
perror("mmap write error");
exit(-1);
}
memcpy(dst, src, statbuf.st_size);
time_t t2 = time((time_t *)NULL);
printf("sssdd %d\n", t2);
printf("%d\n", t2 - t1);
return 0;
}
匿名映射
可以不用进行在文件系统中创建文件、打开文件、然后通过mmap把它映射到调用进程地址空间等操作,达到这个目的:提供一个将穿越fork由父子进程共享的映射内存区。两种方式:
通过设置MAP_ANON标志
mmap第二个参数SIZE由用户指定,设置MAP_ANON标志,将倒数第二个参数(文件描述符)设置为-1.
//不用专门创建一个文件,打开它,然后mmap映射
//直接:
int *ptr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
/dev/zero内存映射
open打开/dev/zero设备文件后,在mmap调用中使用open得到的描述符
int fd = open("/dev/zero", O_RDWR)
int *ptr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHAED, fd, 0);
访问内存映射的对象
内存映射一个普通文件时,内存中映射区的大小(mmap的第二个参数)通常等于该文件的大小(然而文件大小可以不等于内存映射区大小)。关于文件大小和内存映射区大小以及产生结果参考文章:Linux环境进程间通信:对mmap返回地址的访问。
如下的程序验证了该文章中Linux环境进程间通信:对mmap返回地址的访问提到的SIGBUS问题:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/stat.h>
#define BUF_SIZE 4096
#define FILEPATH "test.pcap"
void err_exit(char *err) {
perror(err);
exit(-1);
}
int main(int argc, char **argv) {
int fd, i;
char *ptr;
size_t filesize, mmapsize, pagesize;
//命令行参数分别表示映射到内存的文件大小、内存映射区的大小
if (argc != 3)
err_exit("usage: <filesize> <mmapsize>");
filesize = atoi(argv[1]);
mmapsize = atoi(argv[2]);
//创建、打开并截断文件;设置文件大小
//文件不存在则创建,否则打开文件并将其截断为0.接着把该文件大小设置为
//命令行参数指定的大小,办法是把文件读写指针移动到这个大小减去1的字节
//位置,然后写入一个字节
//也可以使用ftruncate函数来设置文件的长度
fd = open(FILEPATH, O_RDWR | O_CREAT | O_TRUNC, 0660);
if (fd == -1)
err_exit("open file error");
if (lseek(fd, filesize - 1, SEEK_SET) == -1)
err_exit("lseek error");
write(fd, " ", 1); //写入一字节大小以便确定文件长度
//内存映射文件,内存映射区大小由命令行参数给出了
ptr = mmap(NULL, mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
err_exit("mmap error");
if (close(fd) == -1)
err_exit("close file error");
//使用sysconf获取系统实现的页面大小并输出
if ((pagesize = sysconf(_SC_PAGESIZE)) == -1)
err_exit("sysconf error");
printf("PAGESIZE = %ld\n", (long)pagesize);
//读出和存入内存映射区
//读出内存映射区中每个页面的首字节和尾字节,并输出它们的值,预期这些值全为0.
//同时将每个页面的这两个字节设置为1。我们预期某个引用会最终引发一个信号,它将
//终止程序。当for循环结束时,输出下一页的首字节并预期这会失败。
size_t maxsize = (filesize > mmapsize ? filesize : mmapsize);
for (i = 0; i < maxsize; 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);
}
/*
int main(int argc, char **argv) {
time_t t1 = time((time_t *)NULL);
printf("ssss %d\n", t1);
void *src, *dst;
int fdin = open(FILE_IN, O_RDONLY);
if (fdin < 0) {
perror("open file read error");
exit(-1);
}
int fdout = open("test2.pcap", O_RDWR | O_CREAT);
if (fdout < 0) {
perror("open file write error");
exit(-1);
}
struct stat statbuf;
if (fstat(fdin, &statbuf) < 0) {
perror("fstat error");
exit(-1);
}
if (lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1) {
perror("lseek error");
exit(-1);
}
if (write(fdout, "", 1) != 1) {
perror("write error");
exit(-1);
}
if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED) {
perror("mmap in error");
exit(-1);
}
if ((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, 0)) == MAP_FAILED) {
perror("mmap write error");
exit(-1);
}
memcpy(dst, src, statbuf.st_size);
time_t t2 = time((time_t *)NULL);
printf("sssdd %d\n", t2);
printf("%d\n", t2 - t1);
return 0;
}
*/
运行结果:
SIGBUS错误其在Shell输出为“Bus Error(总线错误)",该信号意味着我们是在内存映射区内访问,但是已经超出了底层支撑对象(这里是映射到内存的文件)的大小,即使该对象的描述符已经关闭也一样。内核允许给mmap指定一个大于该对象大小的大小参数,但是我们访问不了该对象以外的部分(最后一页上该对象以外的那些字节除外,它们的下标为5000-8191)。
POSIX共享内存区
POSIX提供了两种在无缘关系进程间共享内存区的方法:
(1)内存映射文件:由open函数打开,由mmap函数把得到的描述符映射到当前进程地址空间中的一个文件。
(2)共享内存区对象步骤:
a. 指定 一个POSIX.1 IPC名字(或许为文件系统的一个路径名),由shm_open,以创建一个新的共享内存区对象或打开一个已存在的共享内存区对象。b. 所返回的描述符由mmap函数将这个共享内存区映射到当前进程的地址空间。
这两种技术都用到mmap,其差别在于作为mmap参数之一的描述符获得的手段:前者通过open、后者头通过shm_open。
函数原型:
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
System V共享内存区
System V共享内存: Linux环境进程间通信(五): 共享内存(下)
创建步骤:
a. 由shmget函数创建一个新的共享内存区或访问一个已存在的共享内存区。该函数创建或打开一个共享内存区但是并没有给调用进程提供访问该内存区的手段。
b. 调用shmat把它连接到调用进程的地址空间。
c. 当一个进程完成某个共享内存区的使用时,可调用shmdt断开与这个内存区的链接。
d. 但是上一步中并没有从系统删除其标示符以及其数据结构,直到某个进程调用shmctl(带命令IPC_RMID)特地删除它。