程序员Nuttx操作系统高薪算法+计算机职称考试

Nuttx信号量机制

2018-12-04  本文已影响8人  Loyen

Nuttx相关的历史文章:

介绍

  • 信号量

在Nuttx中,信号量是同步和互斥的基础,Nuttx支持POSIX信号量。
信号量是获得对资源独占访问的首选机制,尽管sched_lock()sched_unlock()接口也能实现这个功能,但是这两个接口还是会在系统中带来一些副作用,sched_lock()会同时禁止高优先级任务的运行,这些任务不依赖于信号量管理的资源,这会对系统的相应时间产生负面影响。

  • 优先级反转

正确使用信号量可以避免sched_lock()的问题,但是存在以下的情况:

  1. 低优先级任务Task C,获取一个信号量,获得对保护资源的独占使用;
  2. 任务Task C挂起,让高优先级任务Task A执行;
  3. 任务Task A试图获取任务Task C所持有的信号量而被阻塞,直到任务Task C放弃信号量;
  4. 任务Task C允许被再次执行,但是被某个中等优先级的任务Task B挂起。

在这种情况下,高优先级任务Task A在任务Task B(可能还有其他中等优先级的任务)完成和任务Task C释放信号量之前不能执行。表现出来就是任务Task A的优先级好像比任务Task C优先级要低一样,这种现象就叫优先级反转。
在一些操作系统中通过增加低优先级任务Task C来避免优先级反转(这种行为的可操作术语叫优先级继承)。Nuttx在CONFIG_PRIORITY_INHERITANCE被选中时是支持这种行为,否则的话,需要设计人员提供不会发生优先级反转的实现,比如:

  • 优先级继承

上文中提到,当CONFIG_PRIORITY_INHERITANCE被选中时,Nuttx支持优先级继承,但是这个过程比较复杂。

  • Locking信号量 VS Signaling信号量

信号量(互斥锁)有很多种用途。

数据结构及接口

数据结构

/* This structure contains information about the holder of a semaphore */

#ifdef CONFIG_PRIORITY_INHERITANCE
struct tcb_s; /* Forward reference */
struct semholder_s
{
#if CONFIG_SEM_PREALLOCHOLDERS > 0
  struct semholder_s *flink;     /* Implements singly linked list */
#endif
  FAR struct tcb_s *htcb;        /* Holder TCB */
  int16_t counts;                /* Number of counts owned by this holder */
};


/* This is the generic semaphore structure. */

struct sem_s
{
  volatile int16_t semcount;     /* >0 -> Num counts available */
                                 /* <0 -> Num tasks waiting for semaphore */
  /* If priority inheritance is enabled, then we have to keep track of which
   * tasks hold references to the semaphore.
   */

#ifdef CONFIG_PRIORITY_INHERITANCE
  uint8_t flags;                 /* See PRIOINHERIT_FLAGS_* definitions */
# if CONFIG_SEM_PREALLOCHOLDERS > 0
  FAR struct semholder_s *hhead; /* List of holders of semaphore counts */
# else
  struct semholder_s holder;     /* Single holder */
# endif
#endif
};

主要的数据结构分为两部分:

接口

原理

还是来一张图吧:


semaphore原理

信号量整体的框架如上图所示,与之相关的结构如下:

还是从几个关键的函数来分析吧:

sem_wait()

sem_wait()函数主要完成以下几个工作:

  1. 判断是否在中断上下文中,由于sem_wait()可能触发任务调度,造成本身睡眠,因此不能在中断上下文中调用;
  2. 如果信号量可用,将计数值减1,并将调用任务添加到信号量的持有者链表中;
  3. 如果信号量不可用,将计数值减1,将调用任务中waitsem值设置成当前信号量。如果使能了优先级继承,则提升该信号量持有者中比当前调用任务优先级低的任务优先级。最后将调用任务添加到信号量等待队列g_waitingforsemaphore中。
/****************************************************************************
 * Name: sem_wait
 *
 * Description:
 *   This function attempts to lock the semaphore referenced by 'sem'.  If
 *   the semaphore value is (<=) zero, then the calling task will not return
 *   until it successfully acquires the lock.
 *
 * Parameters:
 *   sem - Semaphore descriptor.
 *
 * Return Value:
 *   0 (OK), or -1 (ERROR) is unsuccessful
 *   If this function returns -1 (ERROR), then the cause of the failure will
 *   be reported in 'errno' as:
 *   - EINVAL:  Invalid attempt to get the semaphore
 *   - EINTR:   The wait was interrupted by the receipt of a signal.
 *
 * Assumptions:
 *
 ****************************************************************************/

int sem_wait(FAR sem_t *sem)
{
  FAR struct tcb_s *rtcb = this_task();
  irqstate_t flags;
  int ret  = ERROR;

  /* This API should not be called from interrupt handlers */

  DEBUGASSERT(sem != NULL && up_interrupt_context() == false);

  /* The following operations must be performed with interrupts
   * disabled because sem_post() may be called from an interrupt
   * handler.
   */

  flags = enter_critical_section();

  /* sem_wait() is a cancellation point */

  if (enter_cancellation_point())
    {
#ifdef CONFIG_CANCELLATION_POINTS
      /* If there is a pending cancellation, then do not perform
       * the wait.  Exit now with ECANCELED.
       */

      set_errno(ECANCELED);
      leave_cancellation_point();
      leave_critical_section(flags);
      return ERROR;
#endif
    }

  /* Make sure we were supplied with a valid semaphore. */

  if (sem != NULL)
    {
      /* Check if the lock is available */

      if (sem->semcount > 0)
        {
          /* It is, let the task take the semaphore. */

          sem->semcount--;
          sem_addholder(sem);
          rtcb->waitsem = NULL;
          ret = OK;
        }

      /* The semaphore is NOT available, We will have to block the
       * current thread of execution.
       */

      else
        {
          /* First, verify that the task is not already waiting on a
           * semaphore
           */

          ASSERT(rtcb->waitsem == NULL);

          /* Handle the POSIX semaphore (but don't set the owner yet) */

          sem->semcount--;

          /* Save the waited on semaphore in the TCB */

          rtcb->waitsem = sem;

          /* If priority inheritance is enabled, then check the priority of
           * the holder of the semaphore.
           */

#ifdef CONFIG_PRIORITY_INHERITANCE
          /* Disable context switching.  The following operations must be
           * atomic with regard to the scheduler.
           */

          sched_lock();

          /* Boost the priority of any threads holding a count on the
           * semaphore.
           */

          sem_boostpriority(sem);
#endif
          /* Add the TCB to the prioritized semaphore wait queue */

          set_errno(0);
          up_block_task(rtcb, TSTATE_WAIT_SEM);

          /* When we resume at this point, either (1) the semaphore has been
           * assigned to this thread of execution, or (2) the semaphore wait
           * has been interrupted by a signal or a timeout.  We can detect these
           * latter cases be examining the errno value.
           *
           * In the event that the semaphore wait was interrupted by a signal or
           * a timeout, certain semaphore clean-up operations have already been
           * performed (see sem_waitirq.c).  Specifically:
           *
           * - sem_canceled() was called to restore the priority of all threads
           *   that hold a reference to the semaphore,
           * - The semaphore count was decremented, and
           * - tcb->waitsem was nullifed.
           *
           * It is necesaary to do these things in sem_waitirq.c because a long
           * time may elapse between the time that the signal was issued and
           * this thread is awakened and this leaves a door open to several
           * race conditions.
           */

          if (get_errno() != EINTR && get_errno() != ETIMEDOUT)
            {
              /* Not awakened by a signal or a timeout...
               *
               * NOTE that in this case sem_addholder() was called by logic
               * in sem_wait() fore this thread was restarted.
               */

              ret = OK;
            }

#ifdef CONFIG_PRIORITY_INHERITANCE
          sched_unlock();
#endif
        }
    }
  else
    {
      set_errno(EINVAL);
    }

  leave_cancellation_point();
  leave_critical_section(flags);
  return ret;
}

sem_post()

sem_post()主要完成以下几个任务:

  1. 调用sem_releaseholder()接口来将本任务中持有信号量的次数减1;
  2. 增加信号量计数值;
  3. 当信号量计数值小于等于0时,表明一定有任务正在睡眠等待本信号量,这些任务都在g_waitingforsemaphore队列中,遍历该队列,找到优先级最高的任务,将它添加进信号量的持有者队列中,并调度运行这个任务。
  4. 调用sem_restorebaseprio()接口来恢复之前的优先级(如果有优先级调整的话),在该函数中会去判断任务中持有信号量的计数值,当减到0时,释放该持有者。
/****************************************************************************
 * Name: sem_post
 *
 * Description:
 *   When a task has finished with a semaphore, it will call sem_post().
 *   This function unlocks the semaphore referenced by sem by performing the
 *   semaphore unlock operation on that semaphore.
 *
 *   If the semaphore value resulting from this operation is positive, then
 *   no tasks were blocked waiting for the semaphore to become unlocked; the
 *   semaphore is simply incremented.
 *
 *   If the value of the semaphore resulting from this operation is zero,
 *   then one of the tasks blocked waiting for the semaphore shall be
 *   allowed to return successfully from its call to sem_wait().
 *
 * Parameters:
 *   sem - Semaphore descriptor
 *
 * Return Value:
 *   0 (OK) or -1 (ERROR) if unsuccessful
 *
 * Assumptions:
 *   This function may be called from an interrupt handler.
 *
 ****************************************************************************/

int sem_post(FAR sem_t *sem)
{
  FAR struct tcb_s *stcb = NULL;
  irqstate_t flags;
  int ret = ERROR;

  /* Make sure we were supplied with a valid semaphore. */

  if (sem)
    {
      /* The following operations must be performed with interrupts
       * disabled because sem_post() may be called from an interrupt
       * handler.
       */

      flags = enter_critical_section();

      /* Perform the semaphore unlock operation, releasing this task as a
       * holder then also incrementing the count on the semaphore.
       *
       * NOTE:  When semaphores are used for signaling purposes, the holder
       * of the semaphore may not be this thread!  In this case,
       * sem_releaseholder() will do nothing.
       *
       * In the case of a mutex this could be simply resolved since there is
       * only one holder but for the case of counting semaphores, there may
       * be many holders and if the holder is not this thread, then it is
       * not possible to know which thread/holder should be released.
       *
       * For this reason, it is recommended that priority inheritance be
       * disabled via sem_setprotocol(SEM_PRIO_NONE) when the semahore is
       * initialixed if the semaphore is to used for signaling purposes.
       */

      ASSERT(sem->semcount < SEM_VALUE_MAX);
      sem_releaseholder(sem);
      sem->semcount++;

#ifdef CONFIG_PRIORITY_INHERITANCE
      /* Don't let any unblocked tasks run until we complete any priority
       * restoration steps.  Interrupts are disabled, but we do not want
       * the head of the read-to-run list to be modified yet.
       *
       * NOTE: If this sched_lock is called from an interrupt handler, it
       * will do nothing.
       */

      sched_lock();
#endif
      /* If the result of of semaphore unlock is non-positive, then
       * there must be some task waiting for the semaphore.
       */

      if (sem->semcount <= 0)
        {
          /* Check if there are any tasks in the waiting for semaphore
           * task list that are waiting for this semaphore. This is a
           * prioritized list so the first one we encounter is the one
           * that we want.
           */

          for (stcb = (FAR struct tcb_s *)g_waitingforsemaphore.head;
               (stcb && stcb->waitsem != sem);
               stcb = stcb->flink);

          if (stcb != NULL)
            {
              /* The task will be the new holder of the semaphore when
               * it is awakened.
               */

              sem_addholder_tcb(stcb, sem);

              /* It is, let the task take the semaphore */

              stcb->waitsem = NULL;

              /* Restart the waiting task. */

              up_unblock_task(stcb);
            }
        }

      /* Check if we need to drop the priority of any threads holding
       * this semaphore.  The priority could have been boosted while they
       * held the semaphore.
       */

#ifdef CONFIG_PRIORITY_INHERITANCE
      sem_restorebaseprio(stcb, sem);
      sched_unlock();
#endif
      ret = OK;

      /* Interrupts may now be enabled. */

      leave_critical_section(flags);
    }
  else
    {
      set_errno(EINVAL);
    }

  return ret;
}

sem_timedwait()

sem_timedwait()机制与sem_wait()大体类似,它们的区别跟消息队列进行消息接收时mq_receive()/mq_timedreceive()区别类似,也是在代码中创建一个watchdog进行计时,当计时结束后还没等到信号量时,此时会回调sem_timeout()接口,在该接口中取消该任务的等待,并重新调度该任务执行。

/****************************************************************************
 * Name: sem_timedwait
 *
 * Description:
 *   This function will lock the semaphore referenced by sem as in the
 *   sem_wait() function. However, if the semaphore cannot be locked without
 *   waiting for another process or thread to unlock the semaphore by
 *   performing a sem_post() function, this wait will be terminated when the
 *   specified timeout expires.
 *
 *   The timeout will expire when the absolute time specified by abstime
 *   passes, as measured by the clock on which timeouts are based (that is,
 *   when the value of that clock equals or exceeds abstime), or if the
 *   absolute time specified by abstime has already been passed at the
 *   time of the call.
 *
 * Parameters:
 *   sem - Semaphore object
 *   abstime - The absolute time to wait until a timeout is declared.
 *
 * Return Value:
 *   Zero (OK) is returned on success.  On failure, -1 (ERROR) is returned
 *   and the errno is set appropriately:
 *
 *   EINVAL    The sem argument does not refer to a valid semaphore.  Or the
 *             thread would have blocked, and the abstime parameter specified
 *             a nanoseconds field value less than zero or greater than or
 *             equal to 1000 million.
 *   ETIMEDOUT The semaphore could not be locked before the specified timeout
 *             expired.
 *   EDEADLK   A deadlock condition was detected.
 *   EINTR     A signal interrupted this function.
 *
 ****************************************************************************/

int sem_timedwait(FAR sem_t *sem, FAR const struct timespec *abstime)
{
  FAR struct tcb_s *rtcb = this_task();
  irqstate_t flags;
  int        ticks;
  int        errcode;
  int        ret = ERROR;

  DEBUGASSERT(up_interrupt_context() == false && rtcb->waitdog == NULL);

  /* sem_timedwait() is a cancellation point */

  (void)enter_cancellation_point();

  /* Verify the input parameters and, in case of an error, set
   * errno appropriately.
   */

#ifdef CONFIG_DEBUG_FEATURES
  if (!abstime || !sem)
    {
      errcode = EINVAL;
      goto errout;
    }
#endif

  /* Create a watchdog.  We will not actually need this watchdog
   * unless the semaphore is unavailable, but we will reserve it up
   * front before we enter the following critical section.
   */

  rtcb->waitdog = wd_create();
  if (!rtcb->waitdog)
    {
      errcode = ENOMEM;
      goto errout;
    }

  /* We will disable interrupts until we have completed the semaphore
   * wait.  We need to do this (as opposed to just disabling pre-emption)
   * because there could be interrupt handlers that are asynchronously
   * posting semaphores and to prevent race conditions with watchdog
   * timeout.  This is not too bad because interrupts will be re-
   * enabled while we are blocked waiting for the semaphore.
   */

  flags = enter_critical_section();

  /* Try to take the semaphore without waiting. */

  ret = sem_trywait(sem);
  if (ret == OK)
    {
      /* We got it! */

      goto success_with_irqdisabled;
    }

  /* We will have to wait for the semaphore.  Make sure that we were provided
   * with a valid timeout.
   */

  if (abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000)
    {
      errcode = EINVAL;
      goto errout_with_irqdisabled;
    }

  /* Convert the timespec to clock ticks.  We must have interrupts
   * disabled here so that this time stays valid until the wait begins.
   */

  errcode = clock_abstime2ticks(CLOCK_REALTIME, abstime, &ticks);

  /* If the time has already expired return immediately. */

  if (errcode == OK && ticks <= 0)
    {
      errcode = ETIMEDOUT;
      goto errout_with_irqdisabled;
    }

  /* Handle any time-related errors */

  if (errcode != OK)
    {
      goto errout_with_irqdisabled;
    }

  /* Start the watchdog */

  (void)wd_start(rtcb->waitdog, ticks, (wdentry_t)sem_timeout, 1, getpid());

  /* Now perform the blocking wait */

  ret = sem_wait(sem);
  if (ret < 0)
    {
      /* sem_wait() failed.  Save the errno value */

      errcode = get_errno();
    }

  /* Stop the watchdog timer */

  wd_cancel(rtcb->waitdog);

  if (errcode != OK)
    {
      goto errout_with_irqdisabled;
    }

  /* We can now restore interrupts and delete the watchdog */

  /* Success exits */

success_with_irqdisabled:
  leave_critical_section(flags);
  wd_delete(rtcb->waitdog);
  rtcb->waitdog = NULL;
  leave_cancellation_point();
  return OK;

  /* Error exits */

errout_with_irqdisabled:
  leave_critical_section(flags);
  wd_delete(rtcb->waitdog);
  rtcb->waitdog = NULL;

errout:
  set_errno(errcode);
  leave_cancellation_point();
  return ERROR;
}

总结

总体来说,在Nuttx中信号量既可用于同步和互斥处理,当任务等不到信号量时,便添加到相关的任务队列中进行阻塞睡眠,当释放信号量时,再去该任务队列中进行查询,重新调度该任务执行。如果遇到优先级反转的情况,优先级继承是一种解决方法。

上一篇下一篇

猜你喜欢

热点阅读