找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 4006|回复: 0

转帖:Unix下信号灯和共享内存的使用方法

[复制链接]
发表于 2011-10-25 14:54:51 | 显示全部楼层 |阅读模式
Unix下信号灯和共享内存的使用方法
1互斥的概念
在Unix中经常遇到多个进程需要使用同一资源的情况。共享资源对每个进程而言就是一份完全属于自己的拷贝。但是由于资源是共享的,就会产生问题:在任意两次操作之间,如果其它进程对该资源做了更改,本进程是无法知道其状态的,因此也不可能进行有效的控制。如果对资源的两次操作是有因果关系的,则更不允许其它进程在操作期间改变资源状态。
因此必须提供互斥机制以使一次只能由一个进程使用资源。常用的方法有文件封锁、信号灯等。文件封锁和信号灯都能够用于Unix文件系统读写的互斥,但信号灯主要用于实现不同进程对共享内存的存取。信号灯作为进程间通信(IPC)的一种方式,不象管道、消息队列那样用于传递大量数据,而是提供了一种机制用来协调不同进程间的同步。
本文阐述了Unix System V下信号灯的使用方法,以及如何通过信号灯实现对共享内存的读写操作。
2信号灯的使用
2.1信号灯的概念
信号灯是一个整数变量,用来代表资源计数器,记录了某个时刻可以获得的资源的数目。最简单的情况是一个二值信号灯,它只有两个值:0和1,分别表示一个资源的“可用”和“不可用”状态。
System V下的信号灯是一个复杂的数据结构,它代表一个非负整数的集合,该集合元素的个数以及每个元素的取值范围都只受系统资源的限制。内核为系统的每个信号灯都维持如下数据结构(其定义包含在头文件sys/sem.h中):
struct semid_ds{
                struct ipc_perm sem_perm;        /* operation permission                        */
                struct sem                *sem_base;        /* ptr to first semaphore in set        */
                unsigned short        sem_nsems;        /* number of semaphores in set        */
                time_t                        sem_otime;        /* last semop time                                */
                time_t                        sem_ctime;        /* last change time                                */
};
ipc_perm结构记录了关于信号灯的所有者、同组者和其他用户的PID号,信号灯的读写权限,操作顺序以及信号灯的键值(KEY)等信息。该数据结构适用于所有使用全局唯一标识号KEY的进程通信。其定义参见sys/ipc.h。
sem结构指明了每一个信号灯的值和使用状态,其定义如下(包含在头文件sys/sem.h中):
struct sem{
        unsigned short semval;                /* semaphore value.                        */
        pid_t                   sempid;                /* pid of last operation.        */
        unsigned short semncnt;                /* number of semaphores                */
/* awaiting semval>cval.                */
        unsigned short semzcnt;                /* number of semaphores                */
                                                                /* awaiting semval=0.                */
};
        图1是一个信号灯集合的示意图:

2.2关于信号灯操作的系统调用说明
系统为信号灯操作提供了3个调用:semget, semop, semctl,这些调用都使用了以下头文件:
        #include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
下面是系统调用的详细说明。
2.2.1系统调用semget
作用:创建一个信号灯集合或者访问一个已经存在的信号灯集合。
形式:int semget(key_t key, int nsems, int semflag);
返回值:信号灯集合的标识semid;如果错误返回-1。
描述:
key为信号灯集合的标识号,它是全系统范围内唯一的键值;nsems表示创建的信号灯的个数;semflag指明该信号灯集合的打开方式,它是由一些常数组合(按位或)而成的,见下表:
                表2-1  信号灯调用semget中sem_flg参数选择项
数值(八进制)        符号        描述
0400
0200
0040
0020
0004
0002
01000
02000        SEM_R
SEM_A
SEM_R>>3
SEM_A>>3
SEM_R>>6
SEM_A>>6
IPC_CREAT
IPC_EXCL        拥有者可读
拥有者可写
同组者可读
同组者可写
其他人可读
其他人可写
创建或获取一个信号灯集合
创建一个新的信号灯集合
说明:
1.        设置semflag的IPC_CREAT位,则创建一个信号灯集合,如果该信号灯集合已经存在,则返回其标识符(semid);
2.        设置semflag的IPC_CREAT|IPC_EXCL位,则创建一个新的信号灯集合,如果已经存在则返回错误信息;
3.        只设置IPC_EXCL位而不设置IPC_CREAT位没有任何意义。
2.2.2系统调用semop
作用:对信号灯集合中一个或多个信号灯进行操作
形式:int semop(int semid, struct sembuf *sops, unsigned int nops);
返回值:操作成功返回0,失败返回-1。
描述:
数组sops的每个元素对应一次操作。nops指明执行几次操作。
sembuf是具有如下形式的一个数据结构的数组:
        struct sembuf{
                unsigned short sem_num;                /* semaphore number.                        */
                short                   semop;                /* semaphore operation.                */
                short                   sem_flg;                /* operation flags.                        */
        }
        该结构对应了信号灯的某种操作。
信号灯的sem_num值标明它是信号灯集合的第几个元素,第一个信号灯为0,第二个为1,依次类推。semop确定了对信号灯采取什么样的操作,它可以为负数,正数和零。sem_flg指明操作的执行模式,它有两个标志位:
IPC_NOWAIT(04000):指明以非阻塞方式操作信号灯。
SEM_UNDO(010000): 指明内核为信号灯操作保留恢复值。
System V将信号灯的值semval初始化为0。信号灯值为0时可以使用,为非0时不可使用。对信号灯的操作可以用以下原语来描述:
1.If sem_op>0  Then                 /*sem_op>0对应对信号灯的抢占。*/
semval=semval+sem_op;                /*抢占成功,信号灯当前值加上sem_op*/
2.If sem_op==0 Then                            /*sem_op==0对应对信号灯使用权的请求。*/
               {
                                 If semval==0 Then                /*semval==0表明信号灯可以使用,*/
                                        立即返回;                        /*请求成功.                   */
                         Else                                                /*semval!=0表明信号灯不可使用*/
                                 {
                                        If(sem_flg&IPC_NOWAIT) /*IPC_NOWAIT指明如果信号灯不可使用,*/                                        立即返回;                        /*则不等待,立即返回。*/
                                        Else                                                /*如果信号灯不可使用,则一直等待。*/
                                                进程挂起直到:semval==0;
                                 }
                   }
3.If sem_op<0 Then                                /*sem_op<0对应对信号灯的释放。*/
         {
                  If(semval+sem_op)>=0 Then   /*semval+sem_op>=0表明需要释放信号灯。*/
                                 semval=semval+sem_op;    /*信号灯当前值减去|sem_op|,释放信号灯。*/
                    Else
                  {
        If(sem_flg&IPC_NOWAIT)        /*如果无需释放信号灯,                         */
                                        立即返回;                        /*且IPC_NOWAIT置位,则立即返回。*/
        Else                                                /*挂起等待释放信号灯的条件满足。 */
                                        进程挂起直到:(semval+sem_op)>=0;
               }
   }
        以上原语描述的情况比较复杂,在一般应用中实际只使用二值信号灯,即semval只能为1或0,其操作流程就很容易理解了。举个实际的例子来说明对信号灯的操作。我们创建一个二值信号灯,给信号灯加锁前先要等待信号灯的值变为0,获取信号灯后将其值加1。给信号灯解锁的是直接给信号灯减1。
        加锁的sembuf及其操作如下:
        static struct sembuf sem_lock[2]=
        {
                0,0,0,                                /* sem_lock[0] */
                0,1,SEM_UNDO                        /* sem_lock[1] */
        };
        semop(semid, &sem_lock[0], 2);
        说明:
        该调用执行了两次操作:先等待信号灯的值变为0并获取信号灯操作权,然后将信号灯当前值semval加1,表明对信号灯占用成功。其它进程要使用该信号灯必须先等待semval变为0。
注意内核一次将sembuf中多个操作连续执行完,在这个期间,其它进程无法对该信号灯进行操作。比较以下两个操作的组合与以上操作的区别:
        semop(semid, &sem_lock[0], 1);
        semop(semid, &sem_lock[1], 1);
        虽然这两个操作的组合完成了上面的功能,但对内核而言不一定是连续进行的,在两次操作期间会有其它进程使用该信号灯。因此这两个操作是无效的。
        解锁的sembuf如下:
        static struct sembuf sem_unlock[1]=
        {
                0, -1, (IPC_NOWAIT|SEM_UNDO)
        }
        说明:
        如果当前信号灯的值semval=1,则当前值减1为0,表明信号灯再次可用。因为在我们的程序里是先加锁,后解锁,所以semval一定为1。但是为了避免错误使用时导致程序挂起,一般我们将sem_flg的IPC_NOWAIT位置上,当释放失败时立即返回错误信息。
在上面的例子里面我们使用了sem_flg的SEM_UNDO位。系统为每个操作信号灯的进程维持了一个操作调整值(semaphore adjustment value),SEM_UNDO标志指示系统在进程退出时恢复该进程使用信号灯之前的信号灯值。SEM_UNDO标志位的作用可以描述如下:
1.        信号灯初始化时调整值一概为0;在信号灯删除时与其相关的调整值一起被删除。
2.        对每个进程,信号灯值增加,调整值等量减小;信号灯值减小,调整值等量增加。
3.        当进程调用exit退出时,内核自动为该进程恢复其操作以前的信号灯值。
        引进SEM_UNDO标志的作用是为了防止当进程异常退出时没有释放信号灯,而其它进程只能永远处于等待状态。
2.2.3 系统调用semctl
作用:提供对信号灯集合中某个信号灯的多种控制功能。
形式:int semctl(int semid, int semnum, int cmd, union semun arg);
返回值:操作成功时根据cmd的不同返回需要的值或0,失败返回-1。
描述:
semid是信号灯集合的标识号,semnum是信号灯号。cmd标识了操作类别。arg用于进行某些操作时存放数据。
semun数据结构如下:
union semnum{
                int semval;                                /*当cmd为SETVAL时有效。                                        */
                struct semid_ds *buf;                /*当cmd为IPC_STAT或IPC_SET时有效。                */
                ushort *array;                                /*当cmd为IPC_GETALL或IPC_SETALL时有效。*/
}
cmd操作包括:
GETVAL:返回信号灯值。
SETVAL:置信号灯值为arg.val。信号灯调整值同时清零。
GETPID:返回sempid。
GETNCNT:返回semncnt。
GETZCNT:返回semzcnt。
GETALL:获取信号灯集合的每个信号灯的值,存入arg.array。
SETALL:置信号灯集合中每个信号灯的值为arg.array的值,对应的调整值同时清零。
IPC_STAT:获取信号灯集合的semid_ds,存入arg.buf。
IPC_SET:将arg.buf数据结构的如下成员赋给信号灯的semid_ds结构:
sem_perm.uid;
sem_perm.gid
sem_perm.mode;                /* 只有低9位有效。 */
                          能够进行此项操作的进程限于超级用户、sem_perm.cuid或sem_perm.uid。
IPC_RMID:删除指定信号灯集合。能够进行此项操作的进程限于超级用户、
   sem_perm.cuid或sem_perm.uid。
3 共享内存的使用
3.1 共享内存的概念
进程间通信(IPC)的几种方法中,管道、命名管道和消息队列传递数据时都需要经过内核缓冲区。而使用共享内存的方法,不同进程之间拷贝数据可以直接从指定内存存取。事实上,共享内存是几种IPC方法中效率最高的一种。
当某个进程正在使用一段共享内存时,其它进程必须等待该操作结束才能使用。这种协调机制可以用信号灯来实现。下面是用信号灯实现共享内存的示意图:

        内核为每个共享内存维护一个如下的数据结构:
        struct shmid_ds{
                struct ipc_perm         shm_perm;        /* 操作权限                                        */
                int                                 shm_segsz;        /* 内存段大小                                */
                struct  tagStruType        shm_Struct;        /* 内存结构和地址                        */
                unsigned short                        shm_lpid;                /* 最后一次操作内存的进程号 */
                unsigned short                        shm_cpid;        /* 创建该内存的进程号                */
                unsigned short                        shm_nattach;        /* 当前连接上内存的进程号        */
                usnigned short                        shm_cnattach;        /* 内核维护的内存连接号                */
                time_t                                shm_atime;        /* 最后一次连接时间                        */
                time_t                                shm_dtime;        /* 最后一次脱离时间                        */
                time_t                                shm_ctime;        /* 最后一次改变时间                        */
};
其中shm_perm与第2节中信号灯的权限结构相同。结构tagStruType是指内核用来指向该共享内存段的指针,它与硬件结构和内核的具体实现有关。
3.2 关于共享内存操作的系统调用
系统为共享内存操作提供了4个调用:shmget, shmat, shmdt, shmctl。这些调用都使用了以下头文件:
        #include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
下面是系统调用的详细说明。
3.2.1 系统调用shmget
作用:创建或打开一个共享内存段,但不访问该段。
形式:int shmget(key_t key, size_t size, int shmflg);
返回值:成功时返回共享内存段的shmid,错误是返回-1;
描述:
        key是该共享内存的键值。size确定其大小。shmflg指明该内存段的创建方式,其含义与信号灯的创建方式相同,见表2-1。注意shmget只是创建或打开一个共享内存段,但并不能够访问该段。
3.2.2系统调用shmat
作用:连接(attach)共享内存段。
形式:char* shmat(int shmid, const void *shmaddr,  int shmflg);
返回值:成功时返回共享内存首地址,失败返回-1;
描述:
        shmid是共享内存号。shmaddr确定内存地址。shmflg确定连接方式,有如下2个标志:SHM_RND和SHM_RDONLY。连接规则如下:
1.        如果shmflg=0则内核选择当前第一个可用地址作为共享内存地址;
2.        如果shmflg!=0且设置了SHM_RND,共享内存地址为shmaddr;
3.        如果shmflg!=0且没有设置SHM_RND,共享内存地址为shmaddr且使用SHMLBA表示地址循环。
在实际应用中,由于直接指定内存地址存在许多问题,一般由系统自动分配,即指定shmaddr=0。
共享内存创建后缺省是可读写的,如果置shmflg的SHM_RDONLY位,则该段共享内存就是只读的。
shmat的返回值是指向某个地址的指针,因此我们可以强制它返回我们需要的结构的数据地址,例如我们可以定义一个数据结构struMesg,在连接内存时返回该结构的地址:
struMesg *pMesg;
pMesg=(struMesg*)shmat(shmid, (const void*)0, 0);
也就是说共享内存操作的可以是结构化的数据,不仅仅限于一个字符串。
一旦连接上了共享内存,就可以通过返回的地址指针对它进行任何操作,就象使用本进程自己的数据一样。
3.2.3 系统调用shmdt
作用:脱离(detach)共享内存段。
形式:int shmdt( const void *shmaddr);
返回值:成功时返回0,失败返回-1;
描述:
        使该进程脱离该共享内存段,但并不删除该段共享内存。
3.2.4 系统调用shmctl
作用:提供了对共享内存段的多种控制。
形式:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:成功时返回0,失败返回-1;
描述:
        将cmd和buf结合使用,对共享内存的操作根据cmd的以下值而定:
        IPC_RMID:删除共享内存shmid。操作权限是超级用户、shm_perm.cuid或shm_per.uid。
        IPC_SET:将buf中以下成员的值赋给共享内存的shmid_ds:
                                shm_perm.uid
                                shm_perm.gid
                                shm_perm.mode                /* 低9位有效                */
操作权限是超级用户、shm_perm.cuid或shm_per.uid。
        IPC_STAT:获取共享内存的shmid_ds值赋给buf指向的数据结构。
        SHM_LOCK:锁住共享内存shmid。操作权限是超级用户。
        SHM_UNLOCK:解锁共享内存shmid。操作权限是超级用户。
4 例子
        下面我们通过一套程序来演示信号灯对操作共享内存的作用。create.out用于创建信号灯和共享内存。delete.out用于删除信号灯和共享内存。write.out向共享内存写两次数据,中间停顿10秒。read.out从共享内存读数据并显示出来。
我们先以后台方式运行write.out,在其停顿期间运行read.out。结果是read.out立即挂起,直到write.out停顿结束并写入第二次数据后read.out被唤醒,接着read.out读取并显示了第二次数据。
为了验证信号灯的作用,我们可以将程序中所有涉及到信号灯的操作注释掉并重新编译。我们仍然先以后台方式运行write.out,在其停顿期间运行read.out。结果显示read.out立即执行并显示第一次数据;等到write.out停顿结束并写入第二次数据以后,再次运行read.out,显示了第二次数据。
该例子的源文件如下所示,包括:shm_sem.h, semoper.h, create.c, delete.c, read.c, write.c。
该套程序在SCO Unix 5.0.4上编译并运行通过。
/****************************************************************************
*
*        文件名:        shm_sem.h
*        说明:        包含信号灯和共享内存需要的头文件和自定义宏、类型。
*
****************************************************************************/
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<sys/shm.h>

#define sem_key 6998l
#define shm_key 6999l
#define SEM_PERM 0600
#define SHM_PERM 0600

typedef struct
{
        int iMesgLen;
        char* MesgTxt[30];
}struMesg;
        
/****************************************************************************
*
*        文件名:semoper.h
*        说明:        定义对信号灯操作的基本函数。
*
****************************************************************************/
void LockSem();
void UnlkSem();

static struct sembuf LockOp[2]=
{
        0,0,0,
        0,1,SEM_UNDO
};
static struct sembuf UnlckOp[1]=
{
        0,-1,(IPC_NOWAIT|SEM_UNDO)
};
struMesg *pMesg;
int shmid,semid;

/*        加锁信号灯                */
void LockSem()
{
        if((semid=semget(sem_key,1,SEM_PERM|IPC_CREAT))==-1)
        {
                perror("LockSem() Get semaphore error");
        }
        if(semop(semid,&LockOp[0],2)==-1)
        {
                perror("LockSem() Lock semaphore error");
        }
}
/*        解锁信号灯                */
void UnlkSem()
{
        if((semid=semget(sem_key,1,SEM_PERM|IPC_CREAT))==-1)
        {
                perror("UnlkSem() Get semaphore error");
        }
        if(semop(semid,&UnlckOp[0],1)==-1)
        {
                perror("UnlkSem() Unlock semaphore error");
        }
}
/****************************************************************************
*
*        文件名:create.c
*        说明:        创建信号灯和共享内存。
*
****************************************************************************/
#include"shm_sem.h"

main()
{
        struMesg *pMesg;
        
        /*        创建信号灯                */
        if(semget(sem_key,1,SEM_PERM|IPC_CREAT|IPC_EXCL)==-1)
        {
                perror("Create semaphore error");
        }
        /*        创建共享内存                */
        if(shmget(sem_key,sizeof(struMesg),SEM_PERM|IPC_CREAT|IPC_EXCL)==-1)
        {
                perror("Create share memory error");
        }
}

/****************************************************************************
*
*        文件名:        delete.c
*        说明:        删除指定信号灯和共享内存。
*
****************************************************************************/
#include"shm_sem.h"

main()
{
        struMesg *pMesg;
        int shmid,semid;

        /*        获取共享内存shmid                */
        if((shmid=shmget(sem_key,sizeof(struMesg),SEM_PERM|IPC_CREAT))==-1)
        {
                perror("Get share memory error");
        }
        /*        删除共享内存shmid
        if(shmctl(shmid,IPC_RMID,0)==-1)
        {
                perror("Remove share memory error");
        }
        
        /*        获取信号灯semid                */
        if((semid=semget(sem_key,1,SEM_PERM|IPC_CREAT))==-1)
        {
                perror("Get semaphore error");
        }
        /*        删除信号灯semid                */
        if(semctl(semid,0,IPC_RMID,0)==-1)
        {
                perror("Remove semaphore memory error");
        }
}
/****************************************************************************
*
*        文件名:write.c
*        说明:        申请信号灯并向共享内存写两次数据,中间间隔10秒。
*
****************************************************************************/
#include<string.h>
#include"shm_sem.h"
#include"semoper.h"

main()
{
        /*        获取共享内存shmid                */
        if((shmid=shmget(sem_key,sizeof(struMesg),SEM_PERM|IPC_CREAT))==-1)
        {
                perror("write.out Get share memory error");
        }
        /*        连接共享内存shmid                */
        if((pMesg=(struMesg*)shmat(shmid,(const void*)0,0))==(struMesg*)-1)
        {
                perror("write.out Attach memory error");
        }

        /*        加锁信号灯                                */
        LockSem();                        

        /* 往共享内存写第一次数据。        */
        memcpy(pMesg->MesgTxt,"This is the first copy.",30);        
        pMesg->iMesgLen=23;
        printf(”First data written.\n”)

        sleep(10);                        /*        停顿10秒                */

        /* 往共享内存写第一次数据。        */
        memcpy(pMesg->MesgTxt,"This is the second copy.",30);
        pMesg->iMesgLen=24;
        printf(”Second data written.\n”)

/*        解锁信号灯                                */
UnlkSem();
        
        /*        脱离共享内存                                */
if(shmdt(pMesg)==-1)
        {
                perror("write.out detach memory error");
        }
}



/****************************************************************************
*
*        文件名:read.c
*        说明:        申请信号灯并从共享内存读取数据。
*
****************************************************************************/
#include<string.h>
#include"shm_sem.h"
#include"semoper.h"

main()
{
        /*        获取共享内存shmid                        */
        if((shmid=shmget(sem_key,sizeof(struMesg),SEM_PERM|IPC_CREAT))==-1)
        {
                perror("write.out Get share memory error");
        }
        /*        连接共享内存                                        */
        if((pMesg=(struMesg*)shmat(shmid,(const void*)0,0))==(struMesg*)-1)
        {
                perror("write.out Attach memory error");
        }

        /*        加锁信号灯                                        */
        LockSem();
        
        printf("read.out get from memory:\n");
        printf("---MesgTxt is:%s\n",pMesg->MesgTxt);
        printf("---iMesgLen is %d\n",pMesg->iMesgLen);

        /*        解锁信号灯                                        */
        UnlkSem();

        /*        脱离共享内存                                        */
        if(shmdt(pMesg)==-1)
        {
                perror("write.out detach memory error");
        }
}
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-5-3 07:23 , Processed in 0.018872 second(s), 7 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表