操作系统内的并发执行进程可以是独立的也可以是协作的:
-
如果一个进程不能影响其他进程或受其他进程影响,那么该进程是独立的,换句话说,不与任何其他进程共享数据的进程是独立的;
-
如果一个进程能影响其他进程或受其他进程所影响,那么该进程是协作的。换句话说,与其他进程共享数据的进程为协作进程。
提供环境允许进程协作,具有许多理由:
-
信息共享:由于多个用户可能对同样的信息感兴趣(例如共享文件),所以应提供环境以允许并发访问这些信息。
-
计算加速:如果希望一个特定任务快速运行,那么应将它分成子任务,而每个子任务可以与其他子任务一起并行执行。注意,如果要实现这样的加速,那么计算机需要有多个处理核。
-
模块化:可能需要按模块化方式构造系统,即将系统功能分成独立的进程或线程。
协作进程需要有一种进程间通信机制(简称 IPC) interprocess communication (IPC),以允许进程相互交换数据与信息。进程间通信有两种基本模型:共享内存 shared memory和消息传递(消息队列) message passing:
-
共享内存模型会建立起一块供协作进程共享的内存区域,进程通过向此共享区域读出或写入数据来交换信息。
-
消息队列模型通过在协作进程间交换消息来实现通信。
图 1 给出了这两种模型的对比。
图 1 通信模型 (a)消息队列 (b)共享内存
上述两种模型在操作系统中都常见,而且许多系统也实现了这两种模型。消息传递对于交换较少数量的数据很有用,因为无需避免冲突。对于分布式系统,消息传递也比共享内存更易实现。共享内存可以快于消息传递,这是因为消息传递的实现经常采用系统调用,因此需要消耗更多时间以便内核介入。与此相反,共享内存系统仅在建立共享内存区域时需要系统调用;一旦建立共享内存,所有访问都可作为常规内存访问,无需借助内核。
对具有多个处理核系统的最新研究表明,在这类系统上,消息传递的性能要优于共享内存。共享内存会有高速缓存一致性问题,这是由共享数据在多个高速缓存之间迁移而引起的。随着系统的处理核的数量的日益增加,可能导致消息传递作为 IPC 的首选机制。
共享内存系统 IPC in Shared-Memory Systems
采用共享内存的进程间通信,需要通信进程建立共享内存区域。通常,共享内存区域驻留在创建共享内存段的进程地址空间内。其他希望使用这个共享内存段进行通信的进程应将其附加到自己的地址空间。
回忆一下,通常操作系统试图阻止一个进程访问另一进程的内存。共享内存需要两个或更多的进程同意取消这一限制,这样它们通过在共享区域内读出或写入来交换信息。数据的类型或位置取决于这些进程,而不是受控于操作系统。另外,进程负责确保它们不向同一位置同时写入数据。
为了说明协作进程的概念,我们来看一看生产者-消费者问题,这是协作进程的通用范例。生产者进程生成信息,以供消费者进程消费。例如,编译器生成的汇编代码可供汇编程序使用,而且汇编程序又可生成目标模块以供加载程序使用。
生产者-消费者问题同时还为客户机-服务器范例提供了有用的比喻。通常,将服务器当作生产者,而将客户机当作消费者。例如,一个 Web 服务器生成(提供)HTML 文件和图像,以供请求资源的 Web 客户浏览器使用(读取)。
解决生产者-消费者问题的方法之一是采用共享内存。为了允许生产者进程和消费者进程并发执行,应有一个可用的缓冲区,以被生产者填充和被消费者清空。这个缓冲区驻留在生产者进程和消费者进程的共享内存区域内。当消费者使用一项时,生产者可产生另一项。生产者和消费者必须同步,这样消费者不会试图消费一个尚未生产出来的项。
缓冲区类型可分两种:
-
无界缓冲区没有限制缓冲区的大小。消费者可能不得不等待新的项,但生产者总是可以产生新项。
-
有界缓冲区假设固定大小的缓冲区。对于这种情况,如果缓冲区空,那么消费者必须等待;并且如果缓冲区满,那么生产者必须等待。
Linux和所有的UNIX操作系统都允许通过共享内存在应用程序之间共享存储空间。有两类基本的API函数用于在进程间共享内存:System v和POSIX。
-
这两类函数上使用相同的原则,核心思想就是任何要被共享的内存都必须经过显示的分配。
-
因为所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。
-
内核没有对访问共享内存进行同步,所以必须提供自己的同步措施,比如数据在写入之前,不允许其它进程对其进行读写.可以用wait来解决这个问题。
下面分别通过介绍POSIX共享内存API和System V共享内存API详细了解共享内存的机制。
POSIX共享内存API
函数shm_open和shm_unlink非常类似于为普通文件所提供的open和unlink系统调用。如果要编写一个可移植的程序,那么shm_open和shm_unlink是最好的选择。
-
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);
-
参数:
name: 共享内存名字;
oflag: 与open函数类型, 可以是O_RDONLY, O_WRONLY, O_RDWR, 还可以按位或上O_CREAT, O_EXCL, O_TRUNC.
mode: 此参数总是需要设置, 如果oflag没有指定O_CREAT, 则mode可以设置为0;
返回值:
成功: 返回一个文件描述符;
失败: 返回-1;
-
ftruncate:修改共享内存大小。
int ftruncate(int fd, off_t length);
-
参数:
fd:文件描述符
length:长度
返回值:
成功返回0,失败返回-1
-
shm_unlink:类似于unlink系统调用对文件进行操作,直到所有的进程不再引用该内存区后才对其进行释放。
int shm_unlink(const char *name);
-
参数:
name:共享内存对象的名字
返回值:
成功返回0,失败返回-1
-
mmap:用于将一个文件映射到某一内存区中,其中也使用了shm_open函数返回的文件描述符。
-
munmap:用于释放mmap所映射的内存区域。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
参数:
addr: 要映射的起始地址, 通常指定为NULL, 让内核自动选择;
length: 映射到进程地址空间的字节数, 通常是先前已经创建的共享内存的大小;
prot: 映射区保护方式(见下);
- PROT_EXEC表示映射的内存页可执行
- PROT_READ表示映射的内存可被读
- PROT_WRITE表示映射的内存可被写
- PROT_NONE表示映射的内存不可访问
flags: 标志(通常指定为MAP_SHARED, 用于进程间通信);
- MAP_SHARED表示共享这块映射的内存,读写这块内存相当于直接读写文件,这些操作对其他进程可见,由于OS对文件的读写都有缓存机制,所以实际上不会立即将更改写入文件,除非带哦用msync()或mumap()
- MAP_PRIVATE表示创建一个私有的copy-on-write的映射, 更新映射区对其他映射到这个文件的进程是不可见的
- MAP_32BIT把映射区的头2GB个字节映射到进程的地址空间,仅限域x86-64平台的64位程序,在早期64位处理器平台上,可以用来提高上下文切换的性能。当设置了MAP_FIXED时此选项自动被忽略
- MAP_ANONYMOUS映射不会备份到任何文件,fd和offset参数都被忽略,通常和MAP_SHARED连用
- MAP_DENYWRITE ignored.
- MAP_EXECUTABLE ignored
- MAP_FILE用来保持兼容性,ignored
- MAP_FIXED不要对addr参数进行处理确确实实的放在addr指向的地址,此时addr一定时页大小的整数倍,
- MAP_GROWSDOWN用在栈中,告诉VMM映射区应该向低地址扩展
- MAP_HUGETLB (since Linux 2.6.32)用于分配"大页"
fd: 文件描述符(填为shm_open返回的共享内存ID);
offset: 从文件头开始的偏移量(一般填为0);
mmap返回值:
成功: 返回映射到的内存区的起始地址;
失败: 返回-1;
-
msync:同步存取一个映射区域并将高速缓存的数据回写到物理内存中,以便其他进程可以监听这些改变。
#include <sys/mman.h>
int msync(void *start, size_t length, int flags);
flags值可为 MS_ASYNC,MS_SYNC,MS_INVALIDATE
- MS_ASYNC的作用是,不管映射区是否更新,直接冲洗返回。
- MS_SYNC的作用是,如果映射区更新了,则冲洗返回,如果映射区没有更新,则等待,知道更新完毕,就冲洗返回。
- MS_INVALIDATE的作用是,丢弃映射区中和原文件相同的部分。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/wait.h>
void error_out(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
int main (int argc, char *argv[])
{
int r;
const char *memname = "/mymem";
const size_t region_size = sysconf(_SC_PAGE_SIZE);
int fd = shm_open(memname, O_CREAT|O_TRUNC|O_RDWR, 0666);
if (fd == -1)
error_out("shm_open");
r = ftruncate(fd, region_size);
if (r != 0)
error_out("ftruncate");
void *ptr = mmap(0, region_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
error_out("MMAP");
close(fd);
pid_t pid = fork();
if (pid == 0)
{
u_long *d = (u_long *)ptr;
*d = 0xdeadbeef;
exit(0);
}
else
{
int status;
waitpid(pid, &status, 0);
printf("child wrote %#lx\n", *(u_long *)ptr);
}
sleep(50);
r = munmap(ptr, region_size);
if (r != 0)
error_out("munmap");
r = shm_unlink(memname);
if (r != 0)
error_out("shm_unlink");
return 0;
}
编译
gcc -pthread posix_shm.c -lrt -o posix_shm.o
-l表示链接指定库
rt应该是库名
POSIX.1b Realtime Extensions library
程序分析:
-
程序执行shm_open函数创建了共享内存区域,此时会在/dev/shm/创建mymem文件.
-
通过ftruncate函数改变shm_open创建共享内存的大小为页大小(sysconf(_SC_PAGE_SIZE)),如果不执行ftruncate函数的话,会报Bus error的错误. (其实大小指定成多少都可以,1024也行,2048也行(page size的倍数?),但是一定要用ftruncate来将文件改成指定的大小,后面mmap要用的)
-
通过mmap函数将创建的mymem文件映射到内存.
-
通过fork派生出子进程,而共享区域映射通过fork调用而被继承.
-
程序通过wait系统调用来保持父进程与子进程的同步.
-
在非父子进程也可以通过共享内存区域的方式进行通讯.
mmap的机制如:就是在磁盘上建立一个文件,每个进程存储器里面,单独开辟一个空间来进行映射。如果多进程的话,那么不会对实际的物理存储器(主存)消耗太大。
System V共享内存API
System V API广泛应用于X windows系统及其扩展版本中,许多X应用程序也使用它。
-
shmget:创建一个新的共享区域或者附加在已有的共享区域上(同shm_open)。
int shmget(key_t key, size_t size, int shmflg);
-
参数:
key:这个共享内存段名字;
size:共享内存大小(bytes);
shmflg:用法类似msgget中的msgflg参数;
返回值:
成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
-
shmat:用于将一个文件映射到内存区域中(同mmap)。
void *shmat(int shmid, const void *shmaddr, int shmflg);
-
参数
shmid:由shmget返回的共享内存标识码
shmaddr:
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址
shmaddr不为NULL且shmaddr设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
-
shmdt:用于释放所映射的内存区域(同munmap)。
int shmdt(const void *shmaddr);
-
参数:shmaddr: 由shmat所返回的指针
注意:将共享内存段与当前进程脱离不等于删除共享内存段
-
shmctl:对于多个用户,断开其对共享区域的连接(同shm_unlink)。
int shnctl(int shnid, int cmd, struct shmid_ds* buf);
-
参数:shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(由三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限数据结构
返回值:成功返回0,;失败返回-1
shm的机制:每个进程的共享内存都直接映射到实际物理存储器里面。
我们将从两种进程范例来演示System V共享内存
父子进程通信范例
/* comm.h */
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <errno.h>
#include <sys/shm.h>
#define PATHNAME "." // ftok函数 生成key使用
#define PROJ_ID 66 // ftok 函数生成key使用
int create_shm( int size);// 分配指定大小的共享内存块
int destroy_shm( int shmid); // 释放指定id的共享内存块
int get_shmid(); // 获取已经存在的共享内存块
#endif /*_COMM_H_*/
//parent_child.c
#include "comm.h"
int main()
{
int shmid;// 创建共享内存块
char *shmaddr ;
struct shmid_ds buf ;
int i=0,index = 0,flag = 0 ;
int pid ;
shmid = shmget(IPC_PRIVATE,4096,IPC_CREAT|IPC_EXCL|0666);
if(shmid < 0){
perror("get shm ipc_id error") ;
return -2;
}
pid = fork() ;
if ( pid == 0 )
{
#if 1
shmaddr = shmat(shmid,NULL, 0 );
if ( (int)shmaddr == -1 )
{
perror("shmat addr error") ;
return -1 ;
}
#endif
while( index < 4096)
{
#if 0
shmaddr = shmat(shmid,NULL, 0 );
if ( (int)shmaddr == -1 )
{
perror("shmat addr error") ;
return -1 ;
}
#endif
printf("share memory@0x%x: %s\n", shmaddr , shmaddr );
sleep(1);
index++;
if( index == 26)
break; // 让程序结束
}
return 0;
} else if ( pid > 0) {
flag = shmctl( shmid, IPC_STAT, &buf) ;
if ( flag == -1 )
{
perror("shmctl shm error") ;
return -1 ;
}
printf("shm_segsz =%d bytes\n", buf.shm_segsz ) ;
printf("parent pid=%d, shm_cpid = %d \n", (int)getpid(), buf.shm_cpid ) ;
printf("chlid pid=%d, shm_lpid = %d \n",pid , buf.shm_lpid ) ;
shmaddr = (char *) shmat(shmid, NULL, 0 ) ;
if ( (int)shmaddr == -1 )
{
perror("shmat addr error") ;
return -1 ;
}
while( i < 4096)
{
shmaddr[i] = 'A'+i ;
i++;
sleep(1);
if(i == 26)
break; // 让程序结束,去释放该共享内存
}
destroy_shm(shmid);
}else{
perror("fork error") ;
destroy_shm(shmid);
}
return 0;
}
key值为IPC_PRIVATE
IPC_PRIVATE会保证创建唯一的共享内存,多个进程都可以用IPC_PRIVATE创建自己特有的共享内存。
在进程中一旦使用shmat访问共享对象“nattch”将会增加。“nattch”表示共享对象被访问多少人访问。一旦使用shmdt访问共享对象“nattch”将会减少。
多进程通信范例
//client1024.c
#include "comm.h"
int main()
{
int shmid = get_shmid(1024),current_shmid;
char *buf;
int index = 1;
buf = shmat(shmid,NULL, 0 );
while(1)
{
current_shmid=get_shmid(1024);
if(current_shmid < 0)
return -1 ;
if(current_shmid!=shmid)
{
shmdt(shmid);
buf = shmat(current_shmid,NULL, 0 );
if ( (int)buf == -1 )
{
perror("shmat addr error") ;
return -1 ;
}
shmid = current_shmid;
}
index = *(buf + 0);
printf("share memory@0x%x: buf[%d]=%c\n", buf,index, *(buf+index));
sleep(1);
if( index == 27)
break; // 让程序结束
}
return 0;
}
// server1024.c
#include "comm.h"
int main()
{
char *buf;
int i = 1;
int shmid = create_shm(1024),current_shmid;// 创建共享内存块
if(shmid < 0){
return shmid;
}
buf = shmat(shmid,NULL, 0 );
if ( (int)buf == -1 )
{
perror("shmat addr error") ;
return -1 ;
}
while(1)
{
current_shmid = get_shmid(1024);
if(current_shmid!=shmid)
{
buf = shmat(current_shmid,NULL, 0 );
if ( (int)buf == -1 )
{
perror("shmat addr error") ;
return -1 ;
}
shmid = current_shmid;
}
buf[i] = '@'+i ;
buf[0] = i;
i++;
sleep(1);
if(i == 27)
break; // 让程序结束,去释放该共享内存
}
destroy_shm(get_shmid());
return 0;
}
server1024.o 进程创建了1024bytes 的共享内存,并且往里写了26个字符。 client1024.o进程获取当前写入的字符。知道server1024.o 进程结束。
用ipcs -m 反复查询共享内存资源的状态。你会发现“nattch”由1变为2。 表示目前有两个进程正在使用共享内存。
server4096.o进程key与server1024.o进程相同,创建大小为4096共享内存,发现已存在共享内存(server1024.o进程创建)容量太小,则删除旧共享内存,并重新创建。
用ipcs -m 反复查询共享内存资源的状态。你会发现 ./server1024.o 执行结束后 shmid “1441792” 的key 由 0x4220047b 变为 0x00000000。因为server1024.o 共享内存被释放。并且"status"为dest表示已经被删除。其实这情况下server1024.o还在运行。但是共享内存资源已经被重新申请更大的共享内存。并且key 0x4220047b 重新申请的shmid为1474561。
结论:
1、mmap保存到实际硬盘,实际存储并没有反映到主存上。优点:储存量可以很大(多于主存);缺点:进程间读取和写入速度要比主存的要慢。
2、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优点,进程间访问速度(读写)比磁盘要快;缺点,储存量不能非常大(多于主存)
使用上看:如果分配的存储量不大,那么使用shm;如果存储量大,那么使用mmap。