semaphore的实现机制详解

 

semaphore是内核中比较重要和常用的同步方式之一,他主要的特点是实现了Sleep机制下的同步。也就是当获取一个semaphore但是又不能立刻获取的时候,他使当前的执行进程进入到Sleep状态中等待,当semaphore可以获取的时候,从新开始运行,而不像splin lock在获取锁的时候是BusyWait。

首先看其定义:

struct semaphore {
                atomic_t count; // 原子变量,是后续的实际代码中,我们能看到其即为我们在初始化时所设置的信号量。
                int sleepers; // 有几个等待者。
                wait_queue_head_t wait; // 等待队列
        };

初始化函数:
        static inline void sema_init (struct semaphore *sem, int val)
        {
                atomic_set(&sem->count, val); // 原子操作,把信号量的值设为原子操作的值。
                sem->sleepers = 0; // 设为等待者为0。
                init_waitqueue_head(&sem->wait); // 初始化等待队列。
        }

好看完了初始化函数,我们来看一下一个特例PV操作:

static inline void init_MUTEX (struct semaphore *sem)
        {
                sema_init(sem, 1);
        }

static inline void init_MUTEX_LOCKED (struct semaphore *sem)
        {
                sema_init(sem, 0);
        }

从中我们可以看到,其实我们所说的PV操作就是调用sema_init来把其中的原子变量分别置0或者1。

下面我们来看下具体的操作函数:

static inline void down(struct semaphore * sem)
        {
                might_sleep();
                __asm__ __volatile__(
                "# atomic down operation/n/t"
                LOCK_PREFIX "decl %0/n/t" /* --sem->count */
                "jns 2f/n"
                "/tlea %0,%%eax/n/t"
                "call __down_failed/n"
                "2:"
                :"+m" (sem->count)
                :
                :"memory","ax");
        }

关于might_sleep():
        #define might_sleep() /
                do { __might_sleep(__FILE__, __LINE__); might_resched(); } while (0)

从中可以看到其根本是调用might_resched():
        #define might_resched() cond_resched()

其调用了cond_resched():

int __sched cond_resched(void)
        {
                if (need_resched() && !(preempt_count() & PREEMPT_ACTIVE) &&
                system_state == SYSTEM_RUNNING) {
                        __cond_resched();
                        return 1;
                 }
                return 0;
        }

其中:

static inline int need_resched(void)
        {
                return unlikely(test_thread_flag(TIF_NEED_RESCHED));
                // TIF_NEED_RESCHED 这个值在arm是2,在i386中是3。
        }

其是判断进程的状态是否是需要调度。

其具体实现是:

#define test_thread_flag(flag) /
        test_ti_thread_flag(current_thread_info(), flag)

得到当前进程的状态,然后和flag做位&操作,判断是否可以做调度。

看下关于抢占机制的判断:
        #define preempt_count() (current_thread_info()->preempt_count)

调用preempt_count()得到当前进程关于抢占的状态,preempt_count & PREEMPT_ACTIVE 为TRUE,取反为当前不可以抢占,就不能调度,否则为可调度。

看些系统状态:system_state 为运行状态。

综合上面的几个条件,我们看出当前进程可以被调度或者说抢占,于是当前进程被调度放弃CPU资源。
might_sleep宏就是检查是否需要重新调度。

下面我们进入汇编代码区:

__asm__ __volatile__(
        "# atomic up operation/n/t"
        LOCK_PREFIX "decl %0/n/t" /* --sem->count */
        "jns 2f/n"        /* 当符号标志位为0的时候跳转到标号2地方,标志获取锁,结束本次操作。 */
        "/tlea %0,%%eax/n/t"
        "call __down_failed/n"        /* 获取不到的话,压栈,调用 __down_failed */
        "2:"
        :"+m" (sem->count)
        :
        :"memory","ax");

LOCK_PREFIX宏:

在多处理器环境中 LOCK_PREFIX 实际被定义为 “lock”前缀。

x86 处理器使用“lock”前缀的方式提供了在指令执行期间对总线加锁的手段。芯片上有一条引线 LOCK,如果在一条汇编指令(ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG)前加上“lock” 前缀,经过汇编后的机器代码就使得处理器执行该指令时把引线 LOCK 的电位拉低,从而把总线锁住,这样其它处理器或使用DMA的外设暂时无法通过同一总线访问内存。

注意:从 P6 处理器开始,如果指令访问的内存区域已经存在于处理器的内部缓存中,则“lock” 前缀并不将引线 LOCK 的电位拉低,而是锁住本处理器的内部缓存,然后依靠缓存一致性协议保证操作的原子性。

在比较新的内核版本中Version >2.6.20, i386平台找不到关于__down_failed的具体定义:

linux-2.6.13.4>arch>i386>kernel>semaphore.c
        asm(
        ".section .sched.text/n"
        ".align 4/n"
        ".globl __down_failed/n"
        "__down_failed:/n/t"
        #if defined(CONFIG_FRAME_POINTER)
                "pushl %ebp/n/t"
                "movl %esp,%ebp/n/t"
        #endif
                "pushl %edx/n/t"
                "pushl %ecx/n/t"
                "call __down/n/t"
                "popl %ecx/n/t"
                "popl %edx/n/t"
        #if defined(CONFIG_FRAME_POINTER)
                "movl %ebp,%esp/n/t"
                "popl %ebp/n/t"
        #endif
                "ret"
        );

从汇编代码中我们可以看到其调用了__down()来实现其等待获取锁的过程。

fastcall void __sched __down(struct semaphore * sem)
        {
                struct task_struct *tsk = current; // 当前进程task_struct。
                DECLARE_WAITQUEUE(wait, tsk); // 定义等待队列。
                unsigned long flags;

        tsk->state = TASK_UNINTERRUPTIBLE; // 设置当前进程状态为不可中断状态。
                spin_lock_irqsave(&sem->wait.lock, flags);
                // spin_loc_irqsave 在获得自旋锁之前禁止中断(只在当前处理器);之前的中断状态保存在 flags 里。
                add_wait_queue_exclusive_locked(&sem->wait, &wait);
                        // 将当前的进程添加到等待队列的队尾。操作是不需要锁,因为此函数一般是在获取锁之后调用。

        sem->sleepers++;
                for (;;) {
                        int sleepers = sem->sleepers;

                /*
                        * Add "everybody else" into it. They aren't
                        * playing, because we own the spinlock in
                        * the wait_queue_head.
                        */
                        if (!atomic_add_negative(sleepers - 1, &sem->count)) {
                                        // 将(sleepers - 1) + ( &sem->count),如果当结果值为负数时返回真。
                                        /* First:初始化的时候,count = val, sleeper = 0,
                                        * down()中, count -- 成为 val - 1,以PV操作为例,其val值为1,count = 0,
                                        * __down()中,sleeper ++ 为 1,
                                        * 此时(sleepers - 1, &sem->count) 为0,不为负值,故此为FALSE,跳出循环。
                                        *
                                        * Two:由于在跳出的时候sleepers设置为0,故此Other进程尝试想再次获取锁资源的时候,
                                        * (sleepers - 1, &sem->count) 为 ( 0 - 1, 0 )为负值,此时为真,进入下次循环。
                                        */
                               sem->sleepers = 0; //
                               break;
                       }
                       sem->sleepers = 1;        /* us - see -1 above */
                                                                     /* 设置sleeper为1,标识有进程在等待。*/
                       spin_unlock_irqrestore(&sem->wait.lock, flags);
                                                                     /* 解开锁,开始准备被调度。 */

                schedule();

                spin_lock_irqsave(&sem->wait.lock, flags);
                        tsk->state = TASK_UNINTERRUPTIBLE;
                }
                remove_wait_queue_locked(&sem->wait, &wait);
                                /* 获取到了锁资源,把当前进程从等待队列中删掉。 */
                wake_up_locked(&sem->wait);
                                /* 唤醒等待队列 */
                spin_unlock_irqrestore(&sem->wait.lock, flags);
                tsk->state = TASK_RUNNING;
        }

这里要注意的是对sleeper成员的理解结合后面的代码:

进程在没有获得锁时会将其设为1,在获得了锁的时候将其设为0。

所以,sleeper = 1时表示有进程在等待,sleeper = 0时,有进程获得信号量退出,或者没有进程在等待此信号量。

如果在等待队列中进程被唤醒并获得了锁,则将sleeper设为0,然后用break退出循环,再用wake_up_locked(&sem->wait)唤醒等待队列中的一个进程。
这个进程下次被调度的时候从shedule()后面开始运行,更改进程状态。

调用if (!atomic_add_negative(sleepers - 1, &sem->count))判断,此时sleeper = 0,sleep - 1 = -1,经过atomic_add_negative()使sem->count - 1了。

因此前面论述过,进程进来取信号量 down()里count有减1,然后如果有等待线程在获取到这个信号,刚又会将count + 1,使其变到了原值。

现在在我们这个情景中,已经有线程释放信号了,应该要 + 1。

那思考一下:

为什么要对count sleeper做这样的处理呢?直接使用使用计数不就完了吗?进程等待获取信号量的时候sleep+1 count-1.释放信号量的时候sleep-1.count+1不可以吗?

表面上上述的方法也能工作的很好,但是如果进程数目一多就要考虑到溢出问题了。

static inline void up(struct semaphore * sem)
        {
        __asm__ __volatile__(
                "# atomic up operation/n/t"
                LOCK_PREFIX "incl %0/n/t" /* ++sem->count */
                "jg 1f/n/t"
                "lea %0,%%eax/n/t"
                "call __up_wakeup/n"
                "1:"
                :"+m" (sem->count)
                :
                :"memory","ax");
        }

        asm(
        ".section .sched.text/n"
        ".align 4/n"
        ".globl __up_wakeup/n"
        "__up_wakeup:/n/t"
                "pushl %edx/n/t"
                "pushl %ecx/n/t"
                "call __up/n/t"
                "popl %ecx/n/t"
                "popl %edx/n/t"
                "ret"
        );

fastcall void __up(struct semaphore *sem)
        {
                wake_up(&sem->wait);
        }

释放锁资源就比较简单了,首先是利用汇编判断其是否是有符号大于,我们这里count是为0,不是大于则call __up_wakeup(),最终执行__up(),唤醒等待队列。

至于相应信号的分析如下:

static inline int down_interruptible(struct semaphore * sem)
        {
                int result;

        might_sleep();
                __asm__ __volatile__(
                        "# atomic interruptible down operation/n/t"
                        "xorl %0,%0/n/t"
                        LOCK_PREFIX "decl %1/n/t"        /* --sem->count */
                        "jns 2f/n/t"
                        "lea %1,%%eax/n/t"
                        "call __down_failed_interruptible/n"
                        "2:"
                        :"=&a" (result), "+m" (sem->count)
:
                        :"memory");
                return result;
        }

fastcall int __sched __down_interruptible(struct semaphore * sem)
        {
                int retval = 0; /* 返回值 */
                struct task_struct *tsk = current;
                DECLARE_WAITQUEUE(wait, tsk);
                unsigned long flags;

        tsk->state = TASK_INTERRUPTIBLE;
                spin_lock_irqsave(&sem->wait.lock, flags);
                add_wait_queue_exclusive_locked(&sem->wait, &wait);

        sem->sleepers++;
                for (;;) {
                        int sleepers = sem->sleepers;

                /*
                        * With signals pending, this turns into
                        * the trylock failure case - we won't be
                        * sleeping, and we* can't get the lock as
                        * it has contention. Just correct the count
                        * and exit.
                        */
                        if (signal_pending(current)) {
                                /* 判断是否有中断触发,如果有则返回非0值,否则返回0。*/
                                retval = -EINTR;
                                sem->sleepers = 0;
                                /* 重置sleepers = 0 */
                                atomic_add(sleepers, &sem->count);
                                /* 让等待者等于count。*/
                                break;
                        }

                /*
                        * Add "everybody else" into it. They aren't
                        * playing, because we own the spinlock in
                        * wait_queue_head. The "-1" is because we're
                        * still hoping to get the semaphore.
                        */
                        if (!atomic_add_negative(sleepers - 1, &sem->count)) {
                                sem->sleepers = 0;
                                break;
                        }
                        sem->sleepers = 1; /* us - see -1 above */
                        spin_unlock_irqrestore(&sem->wait.lock, flags);

                schedule();

                spin_lock_irqsave(&sem->wait.lock, flags);
                        tsk->state = TASK_INTERRUPTIBLE;
                }
                remove_wait_queue_locked(&sem->wait, &wait);
                wake_up_locked(&sem->wait);
                spin_unlock_irqrestore(&sem->wait.lock, flags);

        tsk->state = TASK_RUNNING;
                return retval;
        }

static inline int down_trylock(struct semaphore * sem)
       {
                  int result;

         __asm__ __volatile__(
                           "# atomic interruptible down operation/n/t"
                           "xorl %0,%0/n/t"
                           LOCK_PREFIX "decl %1/n/t" /* --sem->count */
                           "jns 2f/n/t" /* 可以获取锁,锁之。*/
                           "lea %1,%%eax/n/t"
                           "call __down_failed_trylock/n/t"
                           "2:/n"
                           :"=&a" (result), "+m" (sem->count)
                           :
                           :"memory");
                return result;
        }

fastcall int __down_trylock(struct semaphore * sem)
        {
                int sleepers;
                unsigned long flags;

        spin_lock_irqsave(&sem->wait.lock, flags);
                sleepers = sem->sleepers + 1;
                        /* 不能获取锁,把sleepers + 1,判断是否有等待进程,如果有唤醒其。*/
                sem->sleepers = 0;

        /*
                * Add "everybody else" and us into it. They aren't
                * playing, because we own the spinlock in the
                * wait_queue_head.
                */
                if (!atomic_add_negative(sleepers, &sem->count)) {
                wake_up_locked(&sem->wait);
                }

        spin_unlock_irqrestore(&sem->wait.lock, flags);
                return 1;
        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值