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_EXCL和IPC_CREAT一同使用,则要么返回新创建的信号量集的标识符,要么返回-1。IPC_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);返回值:如果成功,则为一个正数。如果失败,则为-1:errno=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()这个内核实现函数就有了一个清晰的认识。

时间: 2024-10-17 10:16:02

system v信号量的深入剖析的相关文章

System V信号量

引言 当我们谈论System V信号量的时候,所指的是计数信号量集(posix信号量就是单个的).内核为每个信号量集维护一个数据结构.为什么说是一个信号量集呢?可以看看下面的数据结构. 数据结构示意图

Linux IPC实践(11) --System V信号量(1)

信号量API #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg); int semctl(int semid, int semnum, int cmd, ...); int semop(int semid, struct sembuf *sops, unsigned nsops); semget int

Linux系统编程——进程同步与互斥:System V 信号量

信号量概述 信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问. 编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞.PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1. 在实际应用中两个进程间通信可能会使用多个信号量,因此 System V 的信号量以集合的概念来管理,具体操作和Posix 信号量大同小异,详情请点此链接:http://blog.cs

Linux互斥与同步应用(五):system V信号量的互斥与同步

[版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet 或 .../gentleliu,文章仅供学习交流,请勿用于商业用途] system V信号量操作类似于posix信号量,但system V信号量的操作要复杂得多,posix信号量使用步骤为sem_init(sem_open)-->sem_wait(sem_post) --> sem_close详见上一节,system V使用不同的函数. 1. 创建和打开信号量函数:semget(). #include <

System V信号量(3)

使用HttpClient进行网络处理的基本步骤如下: 1.通过get的方式获取到Response对象. CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet("http://www.baidu.com/"); CloseableHttpResponse response = httpClient.execute(httpGet); 2.获取Response对

System V信号量(2)

Lua可以调用C函数的能力将极大的提高Lua的可扩展性和可用性. 对于有些和操作系统相关的功能,或者是对效率要求较高的模块,我们完全可以通过C函数来实现,之后再通过Lua调用指定的C函数. 对于那些可被Lua调用的C函数而言,其接口必须遵循Lua要求的形式,即typedef int (*lua_CFunction)(lua_State* L). 简单说明一下,该函数类型仅仅包含一个表示Lua环境的指针作为其唯一的参数,实现者可以通过该指针进一步获取Lua代码中实际传入的参数.返回值是整型,表示该

System V信号量(1)

原创整理不易,转载请注明出处:使用Memcached.Spring AOP构建数据库前端缓存框架 代码下载地址:http://www.zuidaima.com/share/1781569917635584.htm 数 据库访问可能是很多网站的瓶颈.动不动就连接池耗尽.内存溢出等.前面已经讲到如果我们的网站是一个分布式的大型站点,那么使用memcached实现数 据库的前端缓存是个很不错的选择:但如果网站本身足够小只有一个服务器,甚至是vps的那种,不推荐使用memcached,使用Hiberna

linux进程通信之SYSTEM V信号量

信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有.信号量的值为正的时候,说明它空闲.所测试的线程可以锁定而使用它.若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒. 一.信号量的分类: 在学习信号量之前,我们必须先知道--Linux提供两种信号量: (1) 内核信号量,由内核控制路径使用. (2) 用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM V信号量. POSIX信号量又分为有名信号量和无名信号量.有名信号量,其值保存在文件

System V 信号量使用相关函数

System V 信号量 在提到Posix 信号量时,指的是二值信号量或计数信号量,而System V信号量指的是入了计数信号量集 二值信号量:其值为0或1,类似于互斥锁,资源被锁住时为0,资源可用为1计数信号量:其值在0和某个限制值之间的信号量,信号量的值就是可用资源数计数信号量集:一个或多个信号量构成一个集合,集合中每个元素都是计数信号量(每个集合的信号量数存在一个限制) semid_ds结构: 内核为每个信号量集维护的信息结构 #include <sys/sem.h> struct se