Linux环境进程间通讯(四):信号灯

linux下进程间通讯的几种主要手段:linux

  1. 管道(Pipe)及有名管道(named pipe):管道可用于具备亲缘关系进程间的通讯,有名管道克服了管道没有名字的限制,所以,除具备管道所具备的功能外,它还容许无亲缘关系进程间的通讯; 
  2. 信号(Signal):信号是比较复杂的通讯方式,用于通知接受进程有某种事件发生,除了用于进程间通讯外,进程还能够发送信号给进程自己;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又可以统一对外接口,用sigaction函数从新实现了signal函数); 
  3. 报文(Message)队列(消息队列):消息队列是消息的连接表,包括Posix消息队列system V消息队列。有足够权限的进程能够向队列中添加消息,被赋予读权限的进程则能够读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。 
  4. 共享内存:使得多个进程能够访问同一块内存空间,是最快的可用IPC形式。是针对其余通讯机制运行效率较低而设计的。每每与其它通讯机制,如信号量结合使用,来达到进程间的同步及互斥。 
  5. 信号量(semaphore):主要做为进程间以及同一进程不一样线程之间的同步手段。 
  6. 套接口(Socket):更为通常的进程间通讯机制,可用于不一样机器之间的进程间通讯。起初是由Unix系统的BSD分支开发出来的,但如今通常能够移植到其它类Unix系统上:Linux和System V的变种都支持套接字。 

本文讲述进程间通讯方法——信号灯编程

原文:http://www.ibm.com/developerworks/cn/linux/l-ipc/part4/数组

1、信号灯概述安全

信号灯与其余进程间通讯方式不大相同,它主要提供对进程间共享资源访问控制机制。至关于内存中的标志,进程能够根据它断定是否可以访问某些共享资源,同时,进程也能够修改该标志。除了用于访问控制外,还可用于进程同步。信号灯有如下两种类型:网络

  • 二值信号灯:最简单的信号灯形式,信号灯的值只能取0或1,相似于互斥锁。 
    注:二值信号灯可以实现互斥锁的功能,但二者的关注内容不一样。信号灯强调共享资源,只要共享资源可用,其余进程一样能够修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程自己来解锁。
  • 计算信号灯:信号灯的值能够取任意非负值(固然受内核自己的约束)。
2、Linux信号灯

linux对信号灯的支持情况与消息队列同样,在red had 8.0发行版本中支持的是系统V的信号灯。所以,本文将主要介绍系统V信号灯及其相应API。在没有声明的状况下,如下讨论中指的都是系统V信号灯。数据结构

注意,一般所说的系统V信号灯指的是计数信号灯集。ide

3、信号灯与内核

一、系统V信号灯是随内核持续的,只有在内核重起或者显示删除一个信号灯集时,该信号灯集才会真正被删除。所以系统中记录信号灯的数据结构(struct ipc_ids sem_ids)位于内核中,系统中的全部信号灯均可以在结构sem_ids中找到访问入口。函数

二、下图说明了内核与信号灯是怎样创建起联系的:测试

其中:struct ipc_ids sem_ids是内核中记录信号灯的全局数据结构;描述一个具体的信号灯及其相关信息。ui

 

其中,struct sem结构以下:

struct sem{
int semval;		// current value
int sempid		// pid of last operation
}

从上图能够看出,全局数据结构struct ipc_ids sem_ids能够访问到struct kern_ipc_perm的第一个成员:struct kern_ipc_perm;而每一个struct kern_ipc_perm可以与具体的信号灯对应起来是由于在该结构中,有一个key_t类型成员key,而key则惟一肯定一个信号灯集;同时,结构struct kern_ipc_perm的最后一个成员sem_nsems肯定了该信号灯在信号灯集中的顺序,这样内核就可以记录每一个信号灯的信息了。kern_ipc_perm结构参见《Linux环境进程间通讯(三):消息队列》。struct sem_array见附录1。

4、操做信号灯

对消息队列的操做无非有下面三种类型:

一、 打开或建立信号灯 
与消息队列的建立及打开基本相同,再也不详述。

二、 信号灯值操做 
linux能够增长或减少信号灯的值,相应于对共享资源的释放和占有。具体参见后面的semop系统调用。

三、 得到或设置信号灯属性: 
系统中的每个信号灯集都对应一个struct sem_array结构,该结构记录了信号灯集的各类信息,存在于系统空间。为了设置、得到该信号灯集的各类信息及属性,在用户空间有一个重要的联合结构与之对应,即union semun。

 

联合semun数据结构各成员意义参见附录2

信号灯API

一、文件名到键值

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (char*pathname, char proj);

它返回与路径pathname相对应的一个键值,具体用法请参考《Linux环境进程间通讯(三):消息队列》。

二、 linux特有的ipc()调用:

int ipc(unsigned int call, int first, int second, int third, void *ptr, long fifth);

参数call取不一样值时,对应信号灯的三个系统调用: 
当call为SEMOP时,对应int semop(int semid, struct sembuf *sops, unsigned nsops)调用; 
当call为SEMGET时,对应int semget(key_t key, int nsems, int semflg)调用; 
当call为SEMCTL时,对应int semctl(int semid,int semnum,int cmd,union semun arg)调用; 
这些调用将在后面阐述。

注:本人不主张采用系统调用ipc(),而更倾向于采用系统V或者POSIX进程间通讯API。缘由已在Linux环境进程间通讯(三):消息队列中给出。

三、系统V信号灯API

系统V消息队列API只有三个,使用时须要包括几个头文件:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

1)int semget(key_t key, int nsems, int semflg) 

参数key是一个键值,由ftok得到,惟一标识一个信号灯集,用法与msgget()中的key相同;参数nsems指定打开或者新建立的信号灯集中将包含信号灯的数目;semflg参数是一些标志位。参数key和semflg的取值,以及什么时候打开已有信号灯集或者建立一个新的信号灯集与msgget()中的对应部分相同,再也不祥述。 
该调用返回与健值key相对应的信号灯集描述字。 
调用返回:成功返回信号灯集描述字,不然返回-1。 
注:若是key所表明的信号灯已经存在,且semget指定了IPC_CREAT|IPC_EXCL标志,那么即便参数nsems与原来信号灯的数目不等,返回的也是EEXIST错误;若是semget只指定了IPC_CREAT标志,那么参数nsems必须与原来的值一致,在后面程序实例中还要进一步说明。

2)int semop(int semid, struct sembuf *sops, unsigned nsops); 
semid是信号灯集ID,sops指向数组的每个sembuf结构都刻画一个在特定信号灯上的操做。nsops为sops指向数组的大小。 
sembuf结构以下:

struct sembuf {
	unsigned short  	sem_num;		/* semaphore index in array */
	short			sem_op;		/* semaphore operation */
	short			sem_flg;		/* operation flags */
};

sem_num对应信号集中的信号灯,0对应第一个信号灯。sem_flg可取IPC_NOWAIT以及SEM_UNDO两个标志。若是设置了SEM_UNDO标志,那么在进程结束时,相应的操做将被取消,这是比较重要的一个标志位。若是设置了该标志位,那么在进程没有释放共享资源就退出时,内核将代为释放。若是为一个信号灯设置了该标志,内核都要分配一个sem_undo结构来记录它,为的是确保之后资源可以安全释放。事实上,若是进程退出了,那么它所占用就释放了,但信号灯值却没有改变,此时,信号灯值反映的已经不是资源占有的实际状况,在这种状况下,问题的解决就靠内核来完成。这有点像僵尸进程,进程虽然退出了,资源也都释放了,但内核进程表中仍然有它的记录,此时就须要父进程调用waitpid来解决问题了。 

sem_op的值大于0,等于0以及小于0肯定了对sem_num指定的信号灯进行的三种操做。具体请参考linux相应手册页。 
这里须要强调的是semop同时操做多个信号灯,在实际应用中,对应多种资源的申请或释放。semop保证操做的原子性,这一点尤其重要。尤为对于多种资源的申请来讲,要么一次性得到全部资源,要么放弃申请,要么在不占有任何资源状况下继续等待,这样,一方面避免了资源的浪费;另外一方面,避免了进程之间因为申请共享资源形成死锁。 
也许从实际含义上更好理解这些操做:信号灯的当前值记录相应资源目前可用数目;sem_op>0对应相应进程要释放sem_op数目的共享资源;sem_op=0能够用于对共享资源是否已用完的测试;sem_op<0至关于进程要申请-sem_op个共享资源。再联想操做的原子性,更不难理解该系统调用什么时候正常返回,什么时候睡眠等待。 
调用返回:成功返回0,不然返回-1。

3) int semctl(int semid,int semnum,int cmd,union semun arg) 
该系统调用实现对信号灯的各类控制操做,参数semid指定信号灯集,参数cmd指定具体的操做类型;参数semnum指定对哪一个信号灯操做,只对几个特殊的cmd操做有意义;arg用于设置或返回信号灯信息。 
该系统调用详细信息请参见其手册页,这里只给出参数cmd所能指定的操做。

IPC_STAT 获取信号灯信息,信息由arg.buf返回;
IPC_SET 设置信号灯信息,待设置信息保存在arg.buf中(在manpage中给出了能够设置哪些信息);
GETALL 返回全部信号灯的值,结果保存在arg.array中,参数sennum被忽略;
GETNCNT 返回等待semnum所表明信号灯的值增长的进程数,至关于目前有多少进程在等待semnum表明的信号灯所表明的共享资源;
GETPID 返回最后一个对semnum所表明信号灯执行semop操做的进程ID;
GETVAL 返回semnum所表明信号灯的值;
GETZCNT 返回等待semnum所表明信号灯的值变成0的进程数;
SETALL 经过arg.array更新全部信号灯的值;同时,更新与本信号集相关的semid_ds结构的sem_ctime成员;
SETVAL 设置semnum所表明信号灯的值为arg.val;

调用返回:调用失败返回-1,成功返回与cmd相关:

Cmd return value
GETNCNT Semncnt
GETPID Sempid
GETVAL Semval
GETZCNT Semzcnt
5、信号灯的限制

一、 一次系统调用semop可同时操做的信号灯数目SEMOPM,semop中的参数nsops若是超过了这个数目,将返回E2BIG错误。SEMOPM的大小特定与系统,redhat 8.0为32。

二、 信号灯的最大数目:SEMVMX,当设置信号灯值超过这个限制时,会返回ERANGE错误。在redhat 8.0中该值为32767。

三、 系统范围内信号灯集的最大数目SEMMNI以及系统范围内信号灯的最大数目SEMMNS。超过这两个限制将返回ENOSPC错误。redhat 8.0中该值为32000。

四、 每一个信号灯集中的最大信号灯数目SEMMSL,redhat 8.0中为250。 SEMOPM以及SEMVMX是使用semop调用时应该注意的;SEMMNI以及SEMMNS是调用semget时应该注意的。SEMVMX同时也是semctl调用应该注意的。

6、竞争问题

第一个建立信号灯的进程同时也初始化信号灯,这样,系统调用semget包含了两个步骤:建立信号灯;初始化信号灯。由此可能致使一种竞争状态:第一个建立信号灯的进程在初始化信号灯时,第二个进程又调用semget,而且发现信号灯已经存在,此时,第二个进程必须具备判断是否有进程正在对信号灯进行初始化的能力。在参考文献[1]中,给出了绕过这种竞争状态的方法:当semget建立一个新的信号灯时,信号灯结构semid_ds的sem_otime成员初始化后的值为0。所以,第二个进程在成功调用semget后,可再次以IPC_STAT命令调用semctl,等待sem_otime变为非0值,此时可判断该信号灯已经初始化完毕。下图描述了竞争状态产生及解决方法:

 

实际上,这种解决方法也是基于这样一个假定:第一个建立信号灯的进程必须调用semop,这样sem_otime才能变为非零值。另外,由于第一个进程可能不调用semop,或者semop操做须要很长时间,第二个进程可能无限期等待下去,或者等待很长时间。

7、信号灯应用实例

本实例有两个目的:一、获取各类信号灯信息;二、利用信号灯实现共享资源的申请和释放。并在程序中给出了详细注释。

#include <linux/sem.h>
#include <stdio.h>
#include <errno.h>
#define SEM_PATH "/unix/my_sem"
#define max_tries 3 
int semid;
main()
{
int flag1,flag2,key,i,init_ok,tmperrno;
struct semid_ds sem_info;
struct seminfo sem_info2;
union semun arg;       //union semun: 请参考附录2
struct sembuf askfor_res, free_res;
flag1=IPC_CREAT|IPC_EXCL|00666;
flag2=IPC_CREAT|00666;
key=ftok(SEM_PATH,'a');
//error handling for ftok here;
init_ok=0;
semid=semget(key,1,flag1);
//create a semaphore set that only includes one semphore.
if(semid<0)
{
  tmperrno=errno;
  perror("semget");
if(tmperrno==EEXIST)
//errno is undefined after a successful library call( including perror call) 
//so it is saved  in tmperrno.
    {
    semid=semget(key,1,flag2);
//flag2 只包含了IPC_CREAT标志, 参数nsems(这里为1)必须与原来的信号灯数目一致
    arg.buf=&sem_info;
    for(i=0; i<max_tries; i++)
    {
      if(semctl(semid, 0, IPC_STAT, arg)==-1)
      {  perror("semctl error"); i=max_tries;}
      else
      { 
        if(arg.buf->sem_otime!=0){ i=max_tries;  init_ok=1;}
        else   sleep(1);  
      }
    }
    if(!init_ok)
  // do some initializing, here we assume that the first process that creates the sem
  //  will finish initialize the sem and run semop in max_tries*1 seconds. else it will  
  // not run semop any more.
    {
      arg.val=1;
      if(semctl(semid,0,SETVAL,arg)==-1) perror("semctl setval error");
    } 
  }
  else
  {perror("semget error, process exit");  exit();  }
}
else //semid>=0; do some initializing   
{
  arg.val=1;
  if(semctl(semid,0,SETVAL,arg)==-1)
    perror("semctl setval error");
}
//get some information about the semaphore and the limit of semaphore in redhat8.0
  arg.buf=&sem_info;
  if(semctl(semid, 0, IPC_STAT, arg)==-1)
    perror("semctl IPC STAT");    
  printf("owner's uid is %d\n",   arg.buf->sem_perm.uid);
  printf("owner's gid is %d\n",   arg.buf->sem_perm.gid);
  printf("creater's uid is %d\n",   arg.buf->sem_perm.cuid);
  printf("creater's gid is %d\n",   arg.buf->sem_perm.cgid);
  arg.__buf=&sem_info2;
  if(semctl(semid,0,IPC_INFO,arg)==-1)
    perror("semctl IPC_INFO");
  printf("the number of entries in semaphore map is %d \n",  arg.__buf->semmap);
  printf("max number of semaphore identifiers is %d \n",    arg.__buf->semmni);
  printf("mas number of semaphores in system is %d \n",   arg.__buf->semmns);
  printf("the number of undo structures system wide is %d \n",  arg.__buf->semmnu);
  printf("max number of semaphores per semid is %d \n",   arg.__buf->semmsl);
  printf("max number of ops per semop call is %d \n",  arg.__buf->semopm);
  printf("max number of undo entries per process is %d \n",  arg.__buf->semume);
  printf("the sizeof of struct sem_undo is %d \n",  arg.__buf->semusz);
  printf("the maximum semaphore value is %d \n",  arg.__buf->semvmx);
  
//now ask for available resource:  
  askfor_res.sem_num=0;
  askfor_res.sem_op=-1;
  askfor_res.sem_flg=SEM_UNDO;    
    
    if(semop(semid,&askfor_res,1)==-1)//ask for resource
      perror("semop error");
  
  sleep(3); 
  //do some handling on the sharing resource here, just sleep on it 3 seconds
  printf("now free the resource\n");  
  
//now free resource  
  free_res.sem_num=0;
  free_res.sem_op=1;
  free_res.sem_flg=SEM_UNDO;
  if(semop(semid,&free_res,1)==-1)//free the resource.
    if(errno==EIDRM)
      printf("the semaphore set was removed\n");
//you can comment out the codes below to compile a different version:      
  if(semctl(semid, 0, IPC_RMID)==-1)
    perror("semctl IPC_RMID");
  else printf("remove sem ok\n");
}

注:读者能够尝试一下注释掉初始化步骤,进程在运行时会出现何种状况(进程在申请资源时会睡眠),同时能够像程序结尾给出的注释那样,把该程序编译成两个不一样版本。下面是本程序的运行结果(操做系统redhat8.0):

owner's uid is 0
owner's gid is 0
creater's uid is 0
creater's gid is 0
the number of entries in semaphore map is 32000 
max number of semaphore identifiers is 128 
mas number of semaphores in system is 32000 
the number of undo structures system wide is 32000 
max number of semaphores per semid is 250 
max number of ops per semop call is 32 
max number of undo entries per process is 32 
the sizeof of struct sem_undo is 20 
the maximum semaphore value is 32767 
now free the resource
remove sem ok

Summary:信号灯与其它进程间通讯方式有所不一样,它主要用于进程间同步。一般所说的系统V信号灯其实是一个信号灯的集合,可用于多种共享资源的进程间同步。每一个信号灯都有一个值,能够用来表示当前该信号灯表明的共享资源可用(available)数量,若是一个进程要申请共享资源,那么就从信号灯值中减去要申请的数目,若是当前没有足够的可用资源,进程能够睡眠等待,也能够当即返回。当进程要申请多种共享资源时,linux能够保证操做的原子性,即要么申请到全部的共享资源,要么放弃全部资源,这样可以保证多个进程不会形成互锁。Linux对信号灯有各类各样的限制,程序中给出了输出结果。另外,若是读者想对信号灯做进一步的理解,建议阅读sem.h源代码,该文件不长,但给出了信号灯相关的重要数据结构。

附录1: struct sem_array以下:

/*系统中的每一个信号灯集对应一个sem_array 结构 */
struct sem_array {
  struct kern_ipc_perm  sem_perm;    /* permissions .. see ipc.h */
  time_t      sem_otime;      /* last semop time */
  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 */
  unsigned long    sem_nsems;    /* no. of semaphores in array */
};

其中,sem_queue结构以下:

/* 系统中每一个由于信号灯而睡眠的进程,都对应一个sem_queue结构*/
 struct sem_queue {
  struct sem_queue *  next;     /* next entry in the queue */
  struct sem_queue **  prev; 
  /* previous entry in the queue, *(q->prev) == q */
  struct task_struct*  sleeper;   /* this process */
  struct sem_undo *  undo;     /* undo structure */
  int   pid;             /* process id of requesting process */
  int   status;           /* completion status of operation */
  struct sem_array *  sma;       /* semaphore array for operations */
  int  id;               /* internal sem id */
  struct sembuf *  sops;       /* array of pending operations */
  int  nsops;             /* number of operations */
  int  alter;             /* operation will alter semaphore */
};

附录2:union semun是系统调用semctl中的重要参数:

union semun {
	int val;					/* value for SETVAL */
	struct semid_ds *buf;		/* buffer for IPC_STAT & IPC_SET */
	unsigned short *array;		/* array for GETALL & SETALL */
	struct seminfo *__buf;		/* buffer for IPC_INFO */   //test!!
	void *__pad;
};
struct  seminfo {
	int semmap;
	int semmni;
	int semmns;
	int semmnu;
	int semmsl;
	int semopm;
	int semume;
	int semusz;
	int semvmx;
	int semaem;
};

参考资料

[1] UNIX网络编程第二卷:进程间通讯,做者:W.Richard Stevens,译者:杨继张,清华大学出版社。对POSIX以及系统V信号灯都有阐述,对Linux环境下的程序开发有极大的启发意义。

[2] linux内核源代码情景分析(上),毛德操、胡希明著,浙江大学出版社,给出了系统V信号灯相关的源代码分析,尤为在阐述保证操做原子性方面,以及阐述undo标志位时,讨论的很深入。

[3]GNU/Linux编程指南,第二版,Kurt Wall等著,张辉译

[4]semget、semop、semctl手册

相关文章
相关标签/搜索