文章目录
共享内存是所有IPC中最快的,因为大多数数据读写都要经历读数据(数据从内核复制到进程地址空间)->发送数据(数据从用户地址空间复制到内核空间)->接收端收到数据(从内核空间将数据复制到进程地址空间),此间最少需要多次系统调用。而进程使用共享内存传递数据不再涉及内核,但是需要进行数据的同步,一般使用互斥锁,条件变量,读写锁,记录锁,信号量。
再详细一点儿就是:假设场景有一个客户端和服务端,客户端读取一个文件将文件经过IPC发给服务端,服务端收到数据后将数据写入输出文件。
- IPC使用非共享内存IPC:
- 客户端通过
read
读取文件,数据由内核复制到客户端进程空间,1次; - 客户端通过非共享内存IPC写入数据,数据由客户端进程空间复制到内核,1次;
- 服务端经过IPC读取数据,数据从内核复制到服务端进程空间,1次;
- 服务端通过
write
写入文件,数据有用户进程空间拷贝到内核空间,1次;
- 客户端通过
- IPC使用共享内存:
- 客户端通过信号量获得访问共享内存区的权限;
- 客户端将数据从输入文件读入到共享内存对象,1次;
- 客户端读入完成通知服务端;
- 客户端将共享内存对象中的数据写出到输出文件,1次。
1 内存映射
1.1 内存映射
内存映射文件(Memory-mapped file),或称“文件映射”、“映射文件”,是一段虚内存逐字节对应于一个文件或类文件的资源,使得应用程序处理映射部分如同访问主内存。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
int msync(void *addr, size_t length, int flags);
mmap
:将一个文件或者一个共享内存对象映射到调用进程地址空间,能够保证父进程中的映射关系能够存留到子进程;addr
:希望映射的进程空间的起始空间,一般设为空指针,由内核自动寻找该起始地址;length
:希望映射到进程地址空间的字节数;prot
:指定读写访问权限:可读(PROT_READ
),可写(PROT_WRITE
),可执行(PROT_EXEC
),不可访问(PROT_NONE
);flags
:可选为MAP_SHARED,MAP_PRIVATE,MAP_FIXED
,其中MAP_SHARED,MAP_PRIVATE
必须指定其一,MAP_FIXED
可选;MAP_SHARED
:表示进程对被映射数据的修改共享该对象的进程都可见,会改变底层支持的对象;MAP_PRIVATE
:表示进程对被映射数据的修改只有当前进程可见,不改变底层支持的对象;- 如果为了可移植性
addr
设为NULL
,不指定MAP_FIXED
;
fd
:希望映射文件的文件描述符,函数调用成功后关闭描述符fd
不影响映射关系;offset
:映射的开始位置为从映射文件的开头offset
处;
munmap
:删除进程地址空间的一个映射关系;addr
:该地址为mmap
返回的地址;len
:映射区大小;
msync
:将文件内容和内存映射区的内容进行同步;addr
:需要进程同步的映射区起始地址;length
:需要映射的内存大小;flags
:可选为异步写(MS_ASYNC
),同步写(MS_SYNC
),高速缓存失效(MS_INVALIDATE
),异步写和同步写必须指定其中一个;MS_ASYNC
:等写操作进入队列直接返回;MS_SYNC
:等写操作完成才会返回;MS_INVALIDATE
:与其最终副本不一致的文件数据所占内存中的副本都会失效。
1.2 内存映射共享数据例子
下面的例子就是父子进程通过共享内存访问数据的情况:
typedef struct mmap_shared
{
sem_t mutex;
int count;
}mmap_shared;
void mmap_test(int argc, char **argv)
{
int fd = open("./mmap", O_RDWR | O_CREAT, FILE_MODE);
mmap_shared *ptr = NULL;
mmap_shared ele;
srand(time((void*)0));
lwrite(fd, &ele, sizeof(ele));
ptr = lmmap(NULL, sizeof(ele), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
lclose(fd);
lsem_init(&ptr->mutex, 1, 1);
int lops = 20;
pid_t pid = lfork();
if(pid == 0)
{
for(int i = 0;i < lops;i ++)
{
lsem_wait(&ptr->mutex);
printf("child count = %3d\n", ptr->count++);
//sleep(rand()%2);
lsem_post(&ptr->mutex);
}
}
else
{
for(int i = 0;i < lops;i ++)
{
lsem_wait(&ptr->mutex);
printf("father count = %3d\n", ptr->count++);
//sleep(rand()%2);
lsem_post(&ptr->mutex);
}
}
//waitpid(pid, 0, 0);
}
执行结果:
➜ build git:(master) ✗ ./main
father count = 0
father count = 1
father count = 2
father count = 3
father count = 4
father count = 5
child count = 6
child count = 7
child count = 8
child count = 9
child count = 10
father count = 11
father count = 12
child count = 13
child count = 14
child count = 15
child count = 16
child count = 17
father count = 18
father count = 19
father count = 20
father count = 21
father count = 22
father count = 23
father count = 24
father count = 25
father count = 26
father count = 27
father count = 28
father count = 29
child count = 30
child count = 31
child count = 32
child count = 33
child count = 34
child count = 35
child count = 36
child count = 37
child count = 38
child count = 39
1.3 内存映射空间增长的问题
因为一般情况下都会将内存映射的大小设置为和文件大小相同或者小,但是如果内存映射空间大于文件大小会如何。
void mmap_size_test(int argc, char **argv)
{
if(argc != 3)
return;
int filesize = atoi(argv[1]);
int mmapsize = atoi(argv[2]);
int fd = lopen("./mmap", O_RDWR | O_CREAT | O_TRUNC);
lseek(fd, filesize - 1, SEEK_SET);
lwrite(fd, " ", 1);
char *ptr = lmmap(NULL, mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
int pagesize = sysconf(_SC_PAGESIZE); //获取页面大小
printf("PAGE_SIZE=%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;
int j = i + pagesize - 1;
printf("ptr[%d] = %d\n", j, ptr[j]);
ptr[j] = 1;
}
}
从下面的测试可以看出,对于内存映射区的访问,由于大小为5000,占用两个页面,第二个页面中[5000,8191]并未使用,对这段超出的内存访问是没有问题的,但是写会导致错误。当内存映射区的大小大于文件大小时,以文件大小为依据,对于5000的文件大小,15000的内存映射区大小,文件占用2个页面,第二个页面只使用了前半段,对前两个页面访问无问题,但是对于相对于页面大小多出的内存映射空间的访问会触发SIGBUS
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JUAgsWh0-1600093564875)(img/mmap.drawio.svg)]
➜ build git:(master) ✗ ./main 5000 5000
PAGE_SIZE=4096
ptr[0] = 0
ptr[4095] = 0
ptr[4096] = 0
ptr[8191] = 0
ptr[8192] = 0
[1] 9986 segmentation fault (core dumped) ./main 5000 5000
➜ build git:(master) ✗ ./main 5000 15000
PAGE_SIZE=4096
ptr[0] = 0
ptr[4095] = 0
ptr[4096] = 0
ptr[8191] = 0
[1] 9996 bus error (core dumped) ./main 5000 15000
2 Posix共享内存
2.1 Posix 共享内存
无亲缘关系进程间共享内存的两种方法:
- 内存映射文件:由
open
函数打开,由mmap
函数将相应的描述符映射到当前进程地址空间的一个文件; - 内存映射对象:由
shm_open
打开一个IPC名字,返回的描述符由mmap
函数映射到当前进程的地址空间。
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
int ftruncate(int fd, off_t length);
int fstat(int fd, struct stat *buf);
shm_open
:创建一个或者打开一个共享内存对象;name
:IPC名字;oflag
:可以有O_RDONLY,O_RDWR,O_CREATE,O_EXCL,O_TRUNC
,如果同时指定了O_RDWR,O_TRUNC
标志,所需的共享内存区对象已经存在则长度被截断为0;mode
:权限位,当oflag
指定O_CREAT
时有效,否则可指定0;
shm_unlink
:删除一个共享内存区对象;name
:IPC名字;
ftruncate
:修改文件或者共享内存区对象大小;fd
:文件描述符;length
:长度:- 普通文件:如果该文件的大小大于
length
参数,额外的数据就被丢弃;如果该文件的大小小于length
,那么噶文件是否修改以及其大小是否增长是未加说明的。而对于实际的普通文件,可移植的方法:先使用lseek
偏移到lenth-1
,然后write
1个字节的数据; - 共享内存区对象:直接把该对象的大小设置为
length
字节;
- 普通文件:如果该文件的大小大于
fstat
:获取指定描述符的相关信息;fd
:文件描述符;buf
:具体信息结构。
2.2 简单的共享计数
服务器创建共享内存和信号量,客户端对共享内存中的数据进行读写。
//服务端程序创建共享内存和信号量
void count_server(char *share_name, char *sem_name)
{
//共享内存
share_name = lpx_ipc_name(share_name);
sem_name = lpx_ipc_name(sem_name);
shm_unlink(share_name);
int fd = lshm_open(share_name, O_RDWR | O_CREAT | O_EXCL, FILE_MODE);
lftruncate(fd, sizeof(int));
char *ptr = lmmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
lclose(fd);
//信号量
sem_unlink(sem_name);
sem_t *mutex = lsem_open(sem_name, O_CREAT | O_EXCL, FILE_MODE, 1);
lsem_close(mutex);
}
//客户端程序对共享内存区的内容进行修改
void count_client(char *share_name, char *sem_name)
{
int fd = lshm_open(lpx_ipc_name(share_name), O_RDWR, FILE_MODE);
char *ptr = lmmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
lclose(fd);
sem_t *mutex = sem_open(lpx_ipc_name(sem_name), 0);
int *count = ptr;
pid_t pid = getpid();
int lops = 5;
for(int i = 0;i < lops;i ++)
{
lsem_wait(mutex);
printf("pid = %d, count = %d\n", pid, (*ptr)++);
lsem_post(mutex);
}
}
void mmap_count_test(int argc, char **argv)
{
if(argc != 4)
return;
switch (argv[1][0])
{
case 'c':
count_client(argv[2], argv[3]);break;
case 's':
count_server(argv[2], argv[3]);break;
default:
break;
}
}
➜ build git:(master) ✗ ./main s s2 sem2
➜ build git:(master) ✗ ./main c s2 sem2 && ./main c s2 sem2
pid = 23365, count = 0
pid = 23365, count = 1
pid = 23365, count = 2
pid = 23365, count = 3
pid = 23365, count = 4
pid = 23366, count = 5
pid = 23366, count = 6
pid = 23366, count = 7
pid = 23366, count = 8
pid = 23366, count = 9
2.3 生产者消费者
消费者接受1个参数为共享内存的名字,生产者接受一个共享内存的名字,一个循环次数,一个延时。
//消费者
void cp_server(int argc, char **argv)
{
if(argc != 3)
err_exit("argc is not 3", -1);
char *name = argv[2];
shm_unlink(lpx_ipc_name(name));
int fd = lshm_open(lpx_ipc_name(name), O_RDWR | O_CREAT | O_EXCL, FILE_MODE);
cp_share *ptr = lmmap(NULL, sizeof(cp_share), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
lftruncate(fd, sizeof(cp_share));
lclose(fd);
//initialize the offset
for(int i = 0;i < MESSAGE_NO;i ++)
{
ptr->msg_off[i] = i * MESSAGESIZE;
}
lsem_init(&ptr->nempty, 1, MESSAGE_NO);
lsem_init(&ptr->nstored, 1, 0);
lsem_init(&ptr->mutex, 1, 1);
lsem_init(&ptr->flowmutex, 1, 1);
int i = 0, lastflow = 0, tmp = 0;
for(;;)
{
lsem_wait(&ptr->nstored);
lsem_wait(&ptr->mutex);
int offset = ptr->msg_off[i];
char *data = &ptr->msg_data[offset];
printf("消费者:i = %3d, msg=%s\n", i, data);
i = (i + 1) % MESSAGE_NO;
lsem_post(&ptr->mutex);
lsem_post(&ptr->nempty);
lsem_wait(&ptr->flowmutex);
tmp = ptr->nflowing;
lsem_post(&ptr->flowmutex);
if(tmp != lastflow)
{
printf("noverflow = %d\n", tmp);
lastflow = tmp;
}
}
}
//生产者
void cp_client(int argc, char **argv)
{
if(argc != 5)
err_exit("argc is not 5", -1);
int loops = atoi(argv[3]);
int nus = atoi(argv[4]);
char *name = argv[2];
int fd = lshm_open(lpx_ipc_name(name), O_RDWR, FILE_MODE);
cp_share *ptr = lmmap(NULL, sizeof(cp_share), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
lclose(fd);
pid_t pid = getpid();
char msg[MESSAGESIZE];
for(int i = 0;i < loops;i ++)
{
usleep(nus);
memset(msg, 0, sizeof(char) * MESSAGESIZE);
snprintf(msg, MESSAGESIZE, "pid = %d, message = %d", pid, i);
int ret = sem_trywait(&ptr->nempty);
if(ret == -1)
{
if(errno == EAGAIN)
{
lsem_wait(&ptr->flowmutex);
ptr->nflowing ++;
lsem_post(&ptr->flowmutex);
continue;
}
else
{
err_exit("sem_trywait error", -1);
}
}
lsem_wait(&ptr->mutex);
int off = ptr->msg_off[ptr->i];
ptr->i = (ptr->i + 1) % MESSAGE_NO;
lsem_post(&ptr->mutex);
strcpy(&ptr->msg_data[off], msg);
lsem_post(&ptr->nstored);
}
}
void mmap_cp_test(int argc, char **argv)
{
if(argc < 2)
err_exit("argc < 2", -1);
switch (argv[1][0])
{
case 'c':
cp_client(argc, argv);break;
case 's':
cp_server(argc, argv);break;
default:
break;
}
}
结果如下:
生产者:
➜ build git:(master) ✗ ./main c sem 10 10
消费者:
➜ build git:(master) ✗ ./main s sem
消费者:i = 0, msg=pid = 29162, message = 0
消费者:i = 1, msg=pid = 29162, message = 1
消费者:i = 2, msg=pid = 29162, message = 2
消费者:i = 3, msg=pid = 29162, message = 3
消费者:i = 4, msg=pid = 29162, message = 4
消费者:i = 5, msg=pid = 29162, message = 5
消费者:i = 6, msg=pid = 29162, message = 6
消费者:i = 7, msg=pid = 29162, message = 7
消费者:i = 8, msg=pid = 29162, message = 8
消费者:i = 9, msg=pid = 29162, message = 9
3 System V共享内存
System V共享内存和Posix共享内存类似,区别是先调用shmget
再调用shmat
。内核中会维护以下结构:
struct shmid_ds
{
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmget
:创建或者打开一个共享内存对象;key
:可以为IPC_PRIVATE
或者ftok
的返回值;size
:共享内存的大小,如果是打开一个存在的共享内存则设为0即可;shmflg
:权限组合;
shmat
:将一个共享内存对象附接到调用进程的地址空间;shmid
:共享内存对象的标识符;shmaddr
:- 如果为空指针则系统自动寻找地址返回;
- 如果为非空指针,返回值取决于
flag
是否指定SHM_RND
:- 指定
SHM_RND
:则相应的共享内存附接到指定的地址向下舍入一个SHMLBA
常值; - 未指定
SHM_RND
:则相应的共享内存直接附接到指定的地址;
- 指定
shmflg
:指定读写权限;
shmdt
:解除共享内存映射;shmctl
:对共享内存进行操作;IPC_RMID
:从系统中删除标识符指定的共享内存并拆除它;IPC_SET
:将指定的shmid_ds
结构中的三个成员shm_perm.uid,shm_perm.gid,shm_per.mode
设置共享内存对象,同时设置shm_ctime
;IPC_STAT
:将共享内存的状态写入shmid_ds
结构中。
4 共享内存的限制
shmmax
:共享内存区的最大字节;shmmnb
:共享内存区的最小字节;shmmni
:系统范围内最大共享内存区标识符数;shmseg
:每个进程附接的最大共享内存区数;