共享内存(Shared Memory,下简称SHM)是指由一个进程创建并可与其他进程共享的内存 块,在UNIX系统中利用SHM可以实现进程间通信(IPC)。为了对系统共享资源(包括SHM)进行 访问的互斥控制,就要用到信号量(Semaphore)机制。系统要求进程在存取共享内存之前应 该先获得相应的信号量的控制。在编程中,共享内存常常要与信号量产生相应的关系。 一,共享内存的创建与控制 UNIX系统开发软件包提供了一系列的函数来实现共享内存的创建与控制。比如调用sh mget函数用于创建共享内存;调用shmctl函数用于控制共享内存。例程如下: #include #include #include #include #include #include #define SHMKEY Ox1688 #define NODENUM 20 int SHM_id; struct shmid_ds shm_f; if((SHM_id=shmget((ket_t)SHMKEY,NODENUM*sizeof(struct pidtos),IPC_CREAT|IPC _EXCL|0660))<0) prinf("Create Shared Memory Fail!"); shm_f.shm_perm.uid=220;/*有效用户主标识*/ shm_f.shm_perm.gid=110;/*有效用户组标识*/ shm_f.shm_perm.mode=0660;/*操作许可*/ if(shmctl(SHM_id,IPC_SET,&shm_f)<0) printf("Set Shard Memory Error"); 函数shmget的调用格式如下: int shmget(key,size,shmflg) key_t key;/*SHM关键值*/ unsigned int size;/*SHM长度*/ int shmflg;/*标志*/ 作用是创建共享内存或获取已创建的共享内存的标识符。实参shmflg的值必须是一个 整数,且规定下列内容:(1)访问权限,(2)执行方式,(3)控制字段。在创建SHM时,该标志可设 为IPC_CREAT(创建,值是01000)!IPC_EXCL(限制唯一创建,值是02000)!0660(访问权限)。成 功时返回创建的共享内存标识符,失败时返回-1。实参size的值应大于SHMMIN且小于SHMMA X,否则该系统调用失败。当shmflg=0时,用于获取已存在的SHM的标识符。 函数shmctl的调用格式如下: int shmctl(shmid,cmd,sbuf) int shmid;/*由shmget获取的SHM的标识符*/ int cmd;/*将对SHM进行的控制命令*/ struct shmid_ds*sbuf;/*存放操作数*/ 作用是对共享内存进行由cmd指定的控制操作。cmd的值比较有用的是下面两个: IPC_SET 对于规定的shmid,设置有效用户和组标识及操作权限。 IPC_RMID 连同其相关的SHM段数据结构一起,删除规定的shmid。执行IPC_SET或IPC_R MID的进程必须具有Owner/Creator或超级用户的有效用户标识。 系统创建的SHM仅仅是内存中一块连续的区域,本身并没有结构。用户进程对共享内存 不能直接进行存取,需要将共享内存附接在进程的数据段上,进程才能对其进行存取,实现方 法是:用户进程可以根据需要自行定义一个数据结构(如pidtos),然后将共享内存的地址用 函数shmat赋值给指向结构pidtos的指针buf,相当于是给指针变量分配内存,让buf指向共享 内存的起始处。然后就可用数组的方法,按数据结构的长度等分共享内存。这个过程可称之 为共享内存的"结构化"。例程如下: struct pidtos{ char rhostname[10]; long pidsc; }*buf; int if((buf=(struct pidtos*)shmat(SHM_idm,(char*)0,0))<0) printf("Access SHM Error!"); for(i=0;iStrcpy(buf[i].rhostname,""); buf[i].pidsc=0; ) /*如果有必要,就初始化SHM*/ shmdt((char*)buf);/*拆接SHM,释放指针buf*/ 函数shmat及shmdt的调用格式: char*shmat(shmid,shmaddr,shmflg) int shmid;/*SHM标识符*/ char*shmaddr;/*相当于偏移量*/ int shmflg;/*标志*/ 作用是将共享内存附接到进程的数据段上。实际上是将共享内存在主存中的地址+shm addr后赋值给进程中的某一指针。shmaddr相当于偏移量,相对于共享内存在主存中的起始 地址。调用失败时返回(char*)-1.shmflg可取值为0,或者是SHM_RND和SHM_EDONLY中的一个 或两者的或。 int shmdt(shmaddr) char *shmaddr;/*采用shmat函数的返回值*/ 作用是拆接共享内存段,成功时返回0,失败时返回-1。 二,信号量的创建与控制 为了支持并发进程对共享内存的互斥访问,保证共享内存中数据的一致性以及对共享内 存操作的完整性(原子性)。一个进程在SHM操作之前先要将SHM加锁,在操作完成后再将其解 锁,为此引入了信号量机制。信号量可用来控制一个共享资源,当然也包括共享内存。由于 许多应用需要使用一个以上的信号量,因此UNIX系统中具备有创建信号量组的能力。信号量 组可以含有一个或多个信号量。比如我们可以用一个信号量组中编号为0的信号量来控制一 个资源,再用编号为1的信号量来控制另一资源。当我们在编制程序时应该知道它们之间的 对应关系。信号量的创建与控制和共享内存的创建与控制很相似。如下面的例程: #define SEMKEY 0x1680 int SEM_id; struct semid_ds sem_f; union semun{ int val; struct semid_ds *buf; ushort array[1];/*[]中的值应根据信号量数目具体定义*/ }arg; if((SEM_id=semget((key_t)SEMKEY,1,IPC_CREAT|IPC_EXCL;0660)<0) printf("Creat Semaphore Fail!"); arg.val=0; if(semct(SEM_id,0,SETVAL,arg.val)<0) printf("Access Semaphore Error!"); /*信号量的值必须初始化。将编号为0的信号量的值初始化为0*/ arg.buf=&sem_f; sem_f.sem_perm.uid=220;/*有效用户主标识*/ shm_f.shm_perm.gid=110;/*有效用户组标识*/ shm_f.shm_perm.mode=0660;/*操作许可*/ if(semctl(SEM_id,IPC_SET,arg)<0) printf("Set Semaphore Error!"); 上述例程首先设置一个信号量组的关键值(SEMKEY),然后调用semgetr利用该关键值创 建只有一个信号量的信号量组。其中第三个参数的含义与函数shmget第三个参数的含义一 样。 函数semget的调用格式: int semget(key,nsems,semflg) key_t key;/*信号量组的关键值*/ int nsems;/*信号量个数*/ int semflg;/*信号量组的创建标志*/ 用来创建一个信号量组,其中包含nsems个信号量,编号从0至nsems-1;创建方式及访问 权限由semflg指定。成功时初始化相应的控制块信息,并返回创建的信号量组的标识符,出 错时返回-1。当semflg=0时用于获取已存在的信号量的标识符。 函数semctl的调用格式: int semctl(semid,semnum,cmd,arg) int semid;/*信号量组的标识符*/ int semnum;/*信号量的编号*/ int cmd;/*控制命令*/ union semun{ int val; struct semid_ds *buf; ushort array[nsems];/*nsems具体根据信号量的数目定义*/ }arg;/*操作数*/ 作用是对指定的信号量组或组中编号为semnum的信号量进行由cmd指定的控制操作。比 较有用的cmd命令如下: cmd 作用 SETVAL 将信号量(semid,semnm)的当前值置为arg.val的值,常用于初始化某个信号量 IPC_SET 将信号量组的状态信息设置成arg.buf中的内容 IPC_RMID 删除信号量组标识符semid 对信号量的操作由semop函数来实现。当一个进程存取某一共享内存时,先查看相应信 号量的值,若为0,则表示SHM目前空闲,未被其他进程占用,那么就将信号量置1,表示SHM已被 占用,然后进程就可存取SHM;若信号量不为0,表明此时SHM已被其他进程占用了,于是这个进 程开始睡眠,等待其他进程释放SHM,即信号量为0时,才被系统唤醒。如下面的例程: static struct sembuf sem_lock[2]=(0,0,0,0,1,0); if(semop(SEM_id,&sem_lock[0],2)<0) printf("Access Semaphore Error!");/*加锁*/其中结构sembuf由系统定义: struct sembuf{ int sem_num;/*信号量编号*/ int sem_op;/*信号量操作数*/ int sem_flg;}/*操作标志*/ 数组sem_lock[2]可以看作是sem_lock[0]=(0,0,0)及sem_lock[1]=(0,1,0)的组合。 上面的例程实际上是两步合为一步来做,是对同一个信号量(编号为0)做两次不同的操作。 经过这一步后,实际上可以看作是已经将SHM加锁,进程接下来就可以创建或存取SHM了。如 果其他进程此时想占用SHM,就必须等待。当操作完成后,为了其他进程可以存取SHM,就要释 放资源,将信号量的值重新清零,即一个解锁的过程。 static struct sembuf sem_unlock[1]=(0,-1,IPC_NOWAIT); if(semop(SEM_id,&sem_unlock[0],1)<0) printf("Access Semaphore Error!");/*解锁*/ 在此之前,信号量的值为1,小于等于-1的绝对值,于是就将信号量减1,重置为0,表示资 源已经释放。如果信号量的当前值为0,小于-1的绝对值,因为标志设为IPC_NOWAIT,就会立 即返回,此时因为信号量的值为0,表示资源空闲,也就无所谓解锁了。 显然对于共享内存的互斥控制采用的是VP算法。该算法还有另一种实现方式:用信号量 为1表示资源空闲,信号量为0表示资源占用,正好与上面的做法相反。在这种情况下,应将信 号量的值初始化为1。例程如下: static struct sembuf sem_lock[1]=(0,-1,0), sem_unlock[1]=(0,1,0); semop(SEM_id,&sem_lock[0],1);/*解锁*/ 加锁时,如果信号量当前值为1(资源空闲),那么就减1,这时信号量的值变为0,表示资源 已经被占用。如果信号量当前值为0(资源占用),因为0小于-1的绝对值,于是进程开始睡眠 ,直到该信号量变为1时,才被系统唤醒。解锁时,如果信号量当前值为0,那么就加一,这时信 号量的值为1,表示资源空闲。这种方式更加简单一些。 函数semop的调用格式: int semop(semid,sops,nsops) int semid;/*信号量组标识符*/ struct sembuf**sops;/*指向信号操作量数缓冲区*/ unsigned nsops;/*操作的信号量信数*/ 功能是完成对标识符为semid的信号量组中若干信号量的操作,根据sops[i]sem_flg及 操作数sops[i].sem_op,对编号为sops[i]·sem_num的信号量进行操作。一共要做nsops次这 样的操作,这个过程可称之为信号量的"块操作",类似批处理方法。 利用上面的三个函数,可以实现对共享资源的互斥访问,也可设计出具有复杂同步操作 要求的并发程序。不同进程对SHM的互斥访问可用图1表示: @@46P16100.GIF;图1@@ 三,共享内存的实际应用 我们假设一个初始化程序(程序init)已经完成了SHM及信号量组的创建过程。这时不同 的进程可用相同的关键值(SHMKEY)去存取共享内存,互斥访问也用相同的信号量去控制,关 键值是SEMKEY。我们假设进程(程序progl)是一个与远程主机连接的通信进程,其所要连接 的远程主机名由命令行参数argv[1]指定。进程将远程主机名和本身的进程号在SHM中登记 或更新。如: SEM_id=semget((key_t)SEMKEY,1,0); semop(SEM_id,&sem_lock[0],2);/*加锁*/ SHM_id=shmget((key_t)SHMKEY,NODENUM*sizeof(struct pidtos),0); buf=(struct pidtos*)shmat(SHM_id,(char*)0,0); for(i=0;i if(srcmp(buf[i].rhostname,argv[1]==0) break; if(buf[i].pidsc==0) break; if(i==NODENUM) return(-1); strcpy(buf[i].rhostname,argv[1]); buf[i].pidsc=getpid();/*操作部分结束*/ shmdt((char *)buf);/*拆接SHM*/ semop(SEM_id,&sem_unlock[0],1);/*解锁*/ 这样相同的一批进程带不同的命令行参数运行后,就在SHM中登记了一批远程主机名和 与之相连的通信进程的进程号。下面我们就可让进程(程序prog2)依据远程主机名(Remote Host)在SHM中查出与该主机相连的通信进程的进程号,以实现对这些通信进程的管理。只要 将上面例程中的操作部分换成下面的程序段即可。 for(i=0;iif(strcmp(buf[i].rhostname,RemoteHost)==0) break; if(i==NODENUM) pid=-1;else pid=buf[i].pidsc; 总之,不同进程通过相同的关键值SHMKEY及SEMKEY,来获取共享内存及信号量的标识符 ,然后使用标识符分别对它们进行操作。相同的关键值是实现不同进程共享资源的基础与前 提。 四,有关安全性的问题 所谓安全性问题指的是在一个进程将共享内存加锁以后,由于某种原因该进程停止了运 行,没能执行解锁操作,从而使SHM一直处于被锁状态,致使其他进程无法使用SHM。为了尽量 避免出现这种情况,我们可以将一些系统信号忽略掉。具体做法是先定义一些函数指针,将 原来系统对这些信号的处理功能暂放函数指针中,然后设置对这些信号的处理方式为SIG_I GN(忽略)。如: int (*f1)(); int (*f2)(); int (*f3)(); int (*f4)(); f1=signal(SIGINT,SIG_IGN); f2=signal(SIGTERM,SIG_IGN); f3=signal(SIGQUIT,SIG_IGN); f4=signal(SIGHUP,SIG_IGN); 在对SHM操作后,无论成功与否都要将对这些系统信号原来的处理功能恢复过来。如: signal(SIGINT,f1); signal(SIGTERM,f2); signal(SIGQUIT,f3); signal(SIGHUP,f4); |