进程间通信之信号量(灯)

来源:互联网 发布:淘宝女士内裤 编辑:程序博客网 时间:2024/05/17 20:24

system V IPC进程间通信之信号量通信机制

信号量的概念由E. W. Dijkstra于1965年首次提出。信号量实际是一个整数。用来标识系统某种可用资源的个数。通常我们所说的创建一个信号量其实是创建一个信号量集合,在这个集合中可能会有多个信号。Linux利用semid_ds结构(信号量集合数数据结构)来表示System V IPC信号量,见图。

 

和消息队列类似,系统中所有的信号量组成了一个semary链表,该链表的每个节点指向一个semid_ds结构。从图可以看出,semid_ds结构的sem_base指向一个信号量数组,每个信号量结构用sem结构定义。允许操作这些信号量数组的进程可以利用系统调用执行操作。系统调用可指定多个操作,每个操作由三个参数指定:信号量索引、操作值和操作标志。信号量索引用来定位信号量数组中的信号量;操作值是要和信号量的当前值相加的数值。首先,Linux按如下的规则判断是否所有的操作都可以成功:操作值和信号量的当前值相加大于0,或操作值和当前值均为0,则操作成功。如果系统调用中指定的所有操作中有一个操作不能成功时,则Linux会挂起这一进程。但是,如果操作标志指定这种情况下不能挂起进程的话,系统调用返回并指明信号量上的操作没有成功,而进程可以继续执行。如果进程被挂起,Linux必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux在堆栈中建立一个sem_queue结构并填充该结构。新的sem_queue结构添加到信号量对象的等待队列中(利用sem_pending和sem_pending_last指针)。当前进程放入sem_queue结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。如果所有的信号量操作都成功了,当前进程可继续运行。在此之前,Linux负责将操作实际应用于信号量队列的相应元素。这时,Linux检查任何等待的或挂起的进程,看它们的信号量操作是否可以成功。如果这些进程的信号量操作可以成功,Linux就会将它们从挂起队列中移去,并将它们的操作实际应用于信号量队列。同时,Linux会唤醒休眠进程,以便可在下次调度程序运行时可以运行这些进程。当新的信号量操作应用于信号量队列之后,Linux会接着检查挂起队列,直到没有操作可成功,或没有挂起进程为止。

    和信号量操作相关的概念还有“死锁”。当某个进程修改了信号量而进入关键段之后,却因为崩溃而没有退出关键段,这时,其他被挂起在信号量上的进程永远得不到运行机会,这就是所谓的死锁。Linux通过维护一个信号量数组的调整链表来避免这一问题。

   信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。

   一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。

   当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

信号量的管理操作

主要有三个系统调用:semget,semctl,semop

1.创建信号量集合
在使用信号量之前,首先需要创建一个信号量集合,该信号量集合中可以包含多个信号量。创建一个信号量集合的函数为 semget,其函数声明如下:

come from /usr/include/sys/sem.h /* Get semaphore. */ extern int semget (key_t __key, int __nsems, int __semflg) __THROW;

第一个参数:为 key_t 类型的 key 值,一般由 ftok 函数产生。
第二个参数:__nsems 为创建的信号量个数,各信号量以数组的方式存储。这个数组用于初始化数组对象。
第三个参数:__semflg 用来标识信号量集合的权限。如 0770,为文件的访问权限类型。

此外,还可以附加以下参数值。这些值可以与基本权限以或的方式一起使用。

 

//come from /usr/include/bit/ipc.h
/* resource get request flags */
#define IPC_CREAT 00001000 //如果 key 不存在,创建
#define IPC_EXCL 00002000 //如果 key 存在,返回失败
#define IPC_NOWAIT 00004000         //如果需要等待,直接返回错误

2.控制信号量集合、信号量
在 Linux 操作系统中,使用 semctl 函数对一个信号量集合以及信号量集合中的信号量进行操作。该函数声明如下:

come from /usr/include/sys/sem.h /* Semaphore control operation. */ extern int semctl (int __semid, int __semnum, int __cmd, ..(union semun arg).) __THROW;


该函数根据cmd最多可有 4 个参数(有可能只有 3 个参数)。

第一个参数:__semid 为要操作的信号量集合标识符,该值一般由 semget函数返回。

第二个参数:是将要操作的信号量个数,从本质上说,它是集合的一个索引,对于集合上的第一个信号量,则该值为0
第三个参数:cmd参数表示在集合上执行的命令,这些命令及解释如表1所示,这些操作在/usr/include/linux/ipc.h 文件中定义。
其操作包括 IPC_RMID、 IPC_SET、 IPC_STAT 和IPC_INFO,具体含义同 msgctl 的相关操作

表1                                                          cmd命令及解释

命令

解   释

IPC_STAT

从信号量集合上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中

IPC_SET

设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值

IPC_RMID

从内核中删除信号量集合

GETALL

从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体成员的一个指针数组arry中

GETNCNT

返回当前等待资源的进程个数

GETPID

返回最后一个执行系统调用semop()进程的PID

GETVAL

返回信号量集合内某个信号量的值

GETZCNT

返回当前等待100%资源利用的进程个数

SETALL

与GETALL正好相反,把集合中所有信号量的值,设置为联合体的array成员所包含的对应值

SETVAL

用联合体中val成员的值设置信号量集合中单个信号量的值


 
 
第四个参数:根据第三个参数的具体操作。其类型为senum的联合体。定义如下:
 
/* arg for semctl system calls. */ union semun { int val;                                //用于SETVAL命令,设置信号量的值struct semid_ds *buf;                   //用于IPC_STAT和IPC_SET命令,指向semid_ds结构,                                                 //用于获取或设置信号量控制结构                                          //cmd为IPC_STAT:获取信号量sem_arry并存入指向的buf                                          //cmd 为:IPC_SET:将buf指向的结构semid_ds的成员值                                            //写入相关于该信号集合内核结构*/unsigned short *array;                  //用于GETALL和SETALL命令,获取或者设置信号集的值struct seminfo *__buf;                  //用于IPC_INFO命令,该命令是linux下特有的,                                                   // 用于返回系统内核定义的信号量集合的定义void *__pad;                            //系统内部使用}; 

 
因此,对于第四个参数
l 如果操作为 SETVAL,则第四个参数为 val,是相应信号量的值。
l 如果操作为 IPC_STAT & IPC_SET,则第四个参数为 struct semid_ds 结构体变量。
 

 
信号中表示信号量(set)集合的数据结构
struct semid_ds 结构体定义如下:
 /* Obsolete, used only for backwards compatibility and libc5 compiles */ struct semid_ds { struct ipc_perm sem_perm;               /* permissions .. see ipc.h */ //IPC权限 __kernel_time_t sem_otime;             /* last semop time */ //最后一次对信号量操作 semop 的时间 __kernel_time_t sem_ctime;            /* last change time */ //对此结构最后一次修改的时间struct sem *sem_base;                /* ptr to first semaphore in array *///在信号量数组中指向第一个信号量的指针 struct sem_queue *sem_pending;      /* pending operations to be processed */ 待处理的挂起操作struct sem_queue **sem_pending_last;          /* last pending operation */ 最后一个挂起操作struct sem_undo *undo;                       /* undo requests on this array */ 在这个数组上的undo请求unsigned shortsem_nsems;                    /* no. of semaphores in array */ 在信号数组上的信号量号}; 
 
l 如果操作为 GETALL & SETALL,第四个参数为数组值。 l 如果操作为 IPC_INFO,第四个参数为 struct seminfo 结构体变量。
 
struct seminfo { int semmap;       //信号量的项目数(定义为mns),内核未使用int semmni;       //系统中允许创建的信号量集的最大数目int semmns;       //系统中允许创建的信号量的最大数目int semmnu;       //系统范围内的undo结构最大个数,内核未使用int semmsl;       //一个信号量集合里信号量个数上限int semopm;       //一次semop可以同时执行的信号量的最大操作个数int semume;       //每个进程内undo结构最大个数,内核未使用int semusz;       //结构sem_undo的尺寸int semvmx;       //信号量值的上限int semaem;       //调整出口最大值(未使用或未实现,无特殊说明)

};

 

 

3.信号量操作
除了可以使用 semctl 系统调用访问信号量外,还可以通过semop 系统调用来操作单个信号量,此函数声明如下:
/* Operate on semaphore. */ extern int semop (int __semid, struct sembuf *__sops, size_t __nsops) __THROW;

第一个参数:为要操作的信号量集合标识符,该值一般由 semget 函数返回。
第二个参数:struct sembuf 结构的变量,其定义如下:
//come from /usr/include/linux/sem.h /* semop system calls takes an array of these. */ struct sembuf { unsigned short sem_num;     /* semaphore index in array */ //信号量下标,从0开始 short sem_op;              /* semaphore operation */ //信号量操作 short sem_flg;            /* operation flags */ //操作标识:IPC_NOWAIT:非阻塞方式;                                                  SEM_UNDO:内核为信号量操作保留恢复值}; 


此结构体有 3 个成员变量。
(1)sem_num为操作的信号量编号。
(2)sem_op 为作用于信号量的操作:该值如果为正整数表示增加信号量的值,如果为负整数表示减小信号量的值,如果为 0 表示对信号量的当前值进行是否为 0 的测试。
(3)sem_flg 为操作标识,可选以下各值: 

  •  IPC_NOWAIT:在对信号量集合的操作不能执行的情况下,调用立即返回,
    对某信号量操作,即使其中一个操作失败,也不会导致修改集中的其他信号量
  • SEM_UNDO:如果未设置 IPC_NOWAIT,则允许在被阻塞的操作失败时撤销操作。

关于 sem_op 的具体情况如下,sem_op 用于指定下列 3 种信号量操作之一:

  • 如果 sem_op 是负整数: 
    那么就从信号量的值中减去sem_op的绝对值,这意味着进程要获取资源,这些资源是由信号量控制或监控来存取的。如果没有指定IPC_NOWAIT,那么调用进程睡眠到请求的资源数得到满足(其它的进程可能释放一些资源)
  • 如果 sem_op 是正整数:
    把它的值加到信号量,这意味着把资源归还给应用程序的集合
  • 如果 sem_op 为 0:
     那么调用进程将睡眠到信号量的值也为0,这相当于一个信号量到达了100%的利用

综上所述,Linux按如下的规则判断是否所有的操作都可以成功:操作值和信号量的当前值相加大于 0,或操作值和当前值均为 0,则操作成功。如果系统调用中指定的所有操作中有一个操作不能成功时,则 Linux会挂起这一进程。但是,如果操作标志指定这种情况下不能挂起进程的话,系统调用返回并指明信号量上的操作没有成功,而进程可以继续执行。如果进程被挂起,Linux必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux内核在堆栈中建立一个 sem_queue结构并填充该结构。新的 sem_queue结构添加到集合的等待队列中(利用 sem_pending sem_pending_last指针)。当前进程放入 sem_queue结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。