首页 > 代码库 > system v信号量的深入剖析

system v信号量的深入剖析

最近看了linux的SYSTEM V信号量的部分,同时对于信号量的数据结构以及系统调用函数的具体实现进行了分析,现将这部分资料进行一个整理,以便于自己理清思路,同时便于以后的查看,若里面有写的不当之处,还望大家指教。

需要说明的是本报告是基于linux-2.6.11版本

第一部分  信号量的数据结构

1、信号量的分类:

(1)内核级信号量,及内核函数采用的信号量。

(2)用户级信号量(只是用户态使用)可分为POSIX信号量和SYSTEM V信号量

2、本文只对system v信号量进行分析

为了让大家对system v的信号量有个大致的了解,我们首先认识一下它的数据结构关系。

                                                                图   1

 由于system v信号量是伴随着内核的启动而生成,我们可以在源码文件sem.c中看到static struct ipc_ids sem_ids;它是system v信号量的入口,因此在系统运行过程中是一直存在的。它所保存的信息是资源(在sem中是信号量集,也可以是msg,shm)的信息。如:

struct ipc_ids {
 int in_use;//说明已分配的资源个数
 int max_id;/在使用的最大的位置索引
 unsigned short seq;//下一个分配的位置序列号
 unsigned short seq_max;//最大位置使用序列
 struct semaphore sem; //保护 ipc_ids的信号量
 struct ipc_id_ary nullentry;//如果IPC资源无法初始化,则entries字段指向伪数据结构
 struct ipc_id_ary* entries;//指向资源ipc_id_ary数据结构的指针
};

它的最后一个元素 entries指向struct ipc_id_ary这样一个数据结构,它有两个成员:

 struct ipc_id_ary {
 int size;//保存的是数组的长度值
 struct kern_ipc_perm *p[0];//它是个指针数组 ,数组长度可变,内核初始化后它的值为128
};

正如我们在图1看到的,sem_ids.entries->p指向sem_array这个数据结构,为什么呢?

我们看信号量集sem_array这个数据结构:

/* One sem_array data structure for each set of semaphores in the system. */
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_ids.entries就跟信号量集sem_array关联起来了,但是为什么要通过kern_ipc_perm关联呢,为什么不直接由sem_ids指向sem_array呢,这是因为信号量,消息队列,共享内存实现的机制基本差不多,所以他们都是通过ipc_id_ary这个数据结构管理,而通过kern_ipc_perm,他们与各自的数据结构关联起来。这样就清楚了!在后面我们来看内核函数sys_semget()是如何进行创建信号量集,并将其加入到sem_ids.entries中的。

第二部分 semget(),semop(),semctl()系统调用函数简介

(1)semget()    

 

 可以使用系统调用semget()创建一个新的信号量集,或者存取一个已经存在的信号量集:系统调用:semget();原型:intsemget(key_t key,int nsems,int semflg);返回值:如果成功,则返回信号量集的IPC标识符。如果失败,则返回-1     

errno=EACCESS(没有权限)    

EEXIST(信号量集已经存在,无法创建)       

EIDRM(信号量集已经删除)       

ENOENT(信号量集不存在,同时没有使用IPC_CREAT)ENOMEM(没有足够的内存创建新的信号量集)ENOSPC(超出限制)

 

系统调用semget()的第一个参数是关键字值(一般是由系统调用ftok()返回的)。系统内核将此值和系统中存在的其他的信号量集的关键字值进行比较。打开和存取操作与参数semflg中的内容相关。

IPC_CREAT如果信号量集在系统内核中不存在,则创建信号量集。IPC_EXCL当和 IPC_CREAT一同使用时,如果信号量集已经存在,则调用失败。如果单独使用IPC_CREAT,则semget()要么返回新创建的信号量集的标识符,要么返回系统中已经存在的同样的关键字值的信号量的标识符。如果IPC_EXCLIPC_CREAT一同使用,则要么返回新创建的信号量集的标识符,要么返回-1IPC_EXCL单独使用没有意义。参数nsems指出了一个新的信号量集中应该创建的信号量的个数

(2)semop()

 

系统调用:semop();调用原型:int semop(int semid,struct sembuf*sops,unsign ednsops);返回值:0,如果成功。-1,如果失败:

errno=E2BIG(nsops大于最大的ops数目)

EACCESS(权限不够)

EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行)

EFAULT(sops指向的地址无效)

EIDRM(信号量集已经删除)

EINTR(当睡眠时接收到其他信号)

EINVAL(信号量集不存在,或者semid无效)

ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构)

ERANGE(信号量值超出范围)  

第一个参数是关键字值。第二个参数是指向将要操作的数组的指针。第三个参数是数组中的操作的个数。参数sops指向由sembuf组成的数组

 

(3)semctl()

 

系统调用:semctl();原型:int semctl(int semid,int semnum,int cmd,union semunarg);返回值:如果成功,则为一个正数。如果失败,则为-1errno=EACCESS(权限不够)EFAULT(arg指向的地址无效)EIDRM(信号量集已经删除)EINVAL(信号量集不存在,或者semid无效)EPERM(EUID没有cmd的权利)ERANGE(信号量值超出范围)    

 

系统调用semctl用来执行在信号量集上的控制操作。这和在消息队列中的系统调用msgctl是十分相似的。但这两个系统调用的参数略有不同。因为信号量一般是作为一个信号量集使用的,而不是一个单独的信号量。所以在信号量集的操作中,不但要知道IPC关键字值,也要知道信号量集中的具体的信号量。这两个系统调用都使用了参数cmd,它用来指出要操作的具体命令。两个系统调用中的最后一个参数也不一样。在系统调用msgctl中,最后一个参数是指向内核中使用的数据结构的指针。我们使用此数据结构来取得有关消息队列的一些信息,以及设置或者改变队列的存取权限和使用者。但在信号量中支持额外的可选的命令,这样就要求有一个更为复杂的数据结构。系统调用semctl()的第一个参数是关键字值。第二个参数是信号量数目。

参数cmd中可以使用的命令如下:    ·

IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。    ·

IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。    

·IPC_RMID将信号量集从内存中删除。    

·GETALL用于读取信号量集中的所有信号量的值。    

·GETNCNT返回正在等待资源的进程数目。    

·GETPID返回最后一个执行semop操作的进程的PID   

 ·GETVAL返回信号量集中的一个单个的信号量的值。    

·GETZCNT返回这在等待完全空闲的资源的进程数目。   

 ·SETALL设置信号量集中的所有的信号量的值。   

 ·SETVAL设置信号量集中的一个单独的信号量的值。

第三部分 内核函数sys_semget(),sys_semop(),sys_semctl()的剖析

 semget(),semop(),semctl()系统调用函数在内核中分别由sys_semget(),sys_semop(),sys_semctl()来进行实现的

1、asmlinkage long sys_semget (key_t key, int nsems, int semflg) { 

    struct sem_array *sma;

   if (key == IPC_PRIVATE) {  

          err = newary(key, nsems, semflg);  //创建一个信号量集

   } else if ((id = ipc_findkey(&sem_ids, key)) == -1) {  /* key not used */   

        if (!(semflg & IPC_CREAT))    err = -ENOENT;   

        else   err = newary(key, nsems, semflg);//关键字key未被使用,且sem_flag有IPC_CREAT标志,则创建信号量集

   } else if (semflg & IPC_CREAT && semflg & IPC_EXCL) {

         err = -EEXIST;

   } else {  

         if (nsems > sma->sem_nsems)    err = -EINVAL;

         else if (ipcperms(&sma->sem_perm, semflg))   err = -EACCES;

         else {    

                   int semid = sem_buildid(id, sma->sem_perm.seq);   //若信号量集已经存在,则返回信号量集的标识符id

                   err = security_sem_associate(sma, semflg);   

                   if (!err)     err = semid;  

               }   

          sem_unlock(sma);

  }

}

我们来跟踪newary()这个调用函数:

我可以发现:struct sem_array *sma;

size = sizeof (*sma) + nsems * sizeof (struct sem);
 sma = ipc_rcu_alloc(size);创建了信号量集

后面又调用了id = ipc_addid(&sem_ids, &sma->sem_perm, sc_semmni);这个函数

我们进入int ipc_addid(struct ipc_ids* ids, struct kern_ipc_perm* new, int size)看发生了什么

首先调用size = grow_ary(ids,size);对动态数组进行调整

后面是这些语句,他们将信号量集插入到sem_ids的entries的p数组里

for (id = 0; id < size; id++) {//查找空的数组项

     if(ids->entries->p[id] == NULL)  

    goto found;  

}  return -1;

found:  ids->in_use++;//使用资源数加1

          if (id > ids->max_id)  

                  ids->max_id = id;

    new->cuid = new->uid = current->euid;

    new->gid = new->cgid = current->egid;

      new->seq = ids->seq++; //每分配个资源,位置序列号加1,它用来计算信号量集标识符

     if(ids->seq > ids->seq_max)  

         ids->seq = 0;

     spin_lock_init(&new->lock);  

    new->deleted = 0;  

    rcu_read_lock();

     spin_lock(&new->lock);  

    ids->entries->p[id] = new;//这个语句将它插入到sem_ids.entries->p中

  return id;

这样我们就清楚了,内核函数是如何创建信号量集,并加入到sem_ids中的。

这里得说明的是信号量集的标志的计算公式,在newary()这个函数的最后调用了return sem_buildid(id, sma->sem_perm.seq);,进入sem_buildid我们可以看到其实具体计算方法:

SEQ_MULTIPLIER*seq + id;

SEQ_MULTIPLIER是可分配的最大资源数,seq为位置序列号,id为在ids->entries->p[id]的位置索引值。

而每分配一个资源,seq加1,这样就保证后面分配的标志符号总是比前面的要大,除非是seq超过最大值seq_max,最大限度的减小了错误引用资源的概率。

 2、sys_semop内核函数

asmlinkage long sys_semop (int semid, struct sembuf __user *tsops, unsigned nsops)
{
 return sys_semtimedop(semid, tsops, nsops, NULL);
}

 传递的是struct sembuf的数组,nsops是数组的大小,semid是信号量集的标识符

进入asmlinkage long sys_semtimedop(int semid, struct sembuf __user *tsops,
   unsigned nsops, const struct timespec __user *timeout)这个函数

我们首先说明这句:

if(nsops > SEMOPM_FAST) {
  sops = kmalloc(sizeof(*sops)*nsops,GFP_KERNEL);//在内核内分配缓冲区
  if(sops==NULL)
   return -ENOMEM;
 }
 if (copy_from_user (sops, tsops, nsops * sizeof(*tsops))) {
  error=-EFAULT;
  goto out_free;
 }

我们可以看到由于操作是在内核态进行,所以首先在系统内核内开辟一段空间,然后 将要操作的struct sembuf由用户空间复制到内核空间

for (sop = sops; sop < sops + nsops; sop++) {
  if (sop->sem_num >= max)
   max = sop->sem_num;
  if (sop->sem_flg & SEM_UNDO)
   undos++;
  if (sop->sem_op < 0)
   decrease = 1;
  if (sop->sem_op > 0)
   alter = 1;
 }
 alter |= decrease;

上面的这个语句是判断是否sop->sem_op都为0,也即看有没有操作,有的话alter=1,否则alter=0

我们再看这段程序:

error = try_atomic_semop (sma, sops, nsops, un, current->tgid);
 if (error <= 0) {
  if (alter && error == 0)
   update_queue (sma);
  goto out_unlock_free;
 }

我们看到它调用了try_atomic_semop()这个函数,那么如果我们对于系统调用函数semop()的功能比较了解的话,大概也能猜个一二,它就是将每个sem_buf的信号量的值semval与sem_op相减,(也即若sem_op>0为P操作,sem_op<0为V操作),若结果<0,则返回1,满足要求的话返回0,我们之后进入该函数,看具体实现。

当alter && error == 0,也即传进来的sem_buf的semop不是全为0且try_atomic_semop()函数成功返回,则进行更新等待队列update_queue (sma);也就是若刚才有V操作,看是否有可以唤醒的进程。若有的话进行唤醒,并移出等待队列。后面我们再来介绍这个函数,下面我们我们先对try_atomic_semop()进行分析:

static int try_atomic_semop (struct sem_array * sma, struct sembuf * sops,         int nsops, struct sem_undo *un, int pid) {  int result, sem_op;  struct sembuf *sop;  struct sem * curr;

 for (sop = sops; sop < sops + nsops; sop++) {   //传进来的是 struct sembuf数组,所以我们得一个个的试探

    curr = sma->sem_base + sop->sem_num;   //找到要进行操作的信号量

    sem_op = sop->sem_op;   

    result = curr->semval;     

    if (!sem_op && result)       //sop->sem_op==0且信号量的值curr->semval!=0,则进程被阻塞

      goto would_block;//如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0

     result += sem_op;   

    if (result < 0)    

      goto would_block;   //信号量值小于0时进行阻塞

    if (result > SEMVMX)    

      goto out_of_range;   

    if (sop->sem_flg & SEM_UNDO) {      //进行保存调整数组,这个在进程异常退出后,用来释放其持有的信号量

      int undo = un->semadj[sop->sem_num] - sem_op;    /*      * Exceeding the undo range is an error.     */   

       if (undo < (-SEMAEM - 1) || undo > SEMAEM)     

        goto out_of_range;   //范围越界,跳出循环并对已经进行的操作进行还原,还原的代码这这儿就不列出了,读者可            自己查看源代码

      }  

       curr->semval = result; //将相减的结果保存到信号量中

   }

可以看到如果sem_op其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0

然后再看update_queue (sma)

while(q) {  

   error = try_atomic_semop(sma, q->sops, q->nsops,       q->undo, q->pid); /* Does q->sleeper still need to sleep? */   //查看是否有可以唤醒的进程,若有的话返回0,并将其移出等待队列,并唤醒

  if (error <= 0) {

       struct sem_queue *n;   

     remove_from_queue(sma,q);   

     q->status = IN_WAKEUP;  

      if (q->alter)     

     n = sma->sem_pending;   

   else     

    n = q->next;    

   wake_up_process(q->sleeper);    /* hands-off: q will disappear immediately after     * writing q->status.     */    

    q->status = error;

       q = n;

} else {   

     q = q->next;   

  }  

}

我们再回到 sys_semtimedop(semid, tsops, nsops, NULL);这个函数,看到若try_atomic_semop (sma, sops, nsops, un, current->tgid);返回值为1时,进程将会被阻塞,进行睡眠知道有别的进程释放资源,并在update_queue (sma)将其唤醒。

这样的话我们对于sys_semop()这个内核实现函数就有了一个清晰的认识。

 

 

 

system v信号量的深入剖析