Linux内核-------同步机制(二)

时间:2022-10-16 08:53:40

信号量的分类

内核信号量,由内核控制路径使用

用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM V信号量。

POSIX信号量又分为有名信号量和无名信号量 。有名信号量,其值保存在文件中, 所以它可以用于线程也可以用于进程间的同步。无名信号量,其值保存在内存中。

信号量操作

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

信号量定义(linux-2.6.4\include\asm-i386\semaphore.h)

struct semaphore {
    atomic_t count;
    int sleepers;
    wait_queue_head_t wait;
#ifdef WAITQUEUE_DEBUG
    long __magic;
#endif
};

内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。只有在资源被释放时,进程才再次变为可运行。 只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。

count:相当于信号量的值,大于0,资源空闲;等于0,资源忙,但没有进程等待这个保护的资源;小于0,资源不可用,并至少有一个进程等待资源

wait :存放等待队列链表的地址,当前等待资源的所有睡眠进程都会放在这个链表中

sleepers: 存放一个标志,表示是否有一些进程在信号量上睡眠。

初始化

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

该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。

互斥锁

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

该函数将count的值初始化为1,从而实现互斥锁的功能。

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

该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。

P操作函数

static inline void down(struct semaphore * sem)
{
#ifdef WAITQUEUE_DEBUG
    CHECK_MAGIC(sem->__magic);
#endif
    might_sleep();
    __asm__ __volatile__(
        "# atomic down operation\n\t"
        LOCK "decl %0\n\t"     /* --sem->count */
        "js 2f\n"
        "1:\n"
        LOCK_SECTION_START("")
        "2:\tcall __down_failed\n\t"
        "jmp 1b\n"
        LOCK_SECTION_END
        :"=m" (sem->count)
        :"c" (sem)
        :"memory");
}

might_sleep()判断当前进程是否需要重新调度。

中间是一段内联汇编:

LOCK "decl %0\n\t" /* --sem->count */ "js 2f\n" "1:\n" LOCK_SECTION_START("") "2:\tcall __down_failed\n\t" "jmp 1b\n" LOCK_SECTION_END :"=m" (sem->count) :"c" (sem) :"memory");

下面详细解释:

LOCK "decl %0\n\t"

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

"js 2f\n"

如果count的值小于0,则跳转到2:。

其中2:对应的代码:

        LOCK_SECTION_START("")
        "2:\tcall __down_failed\n\t"
        "jmp 1b\n"
        LOCK_SECTION_END

LOCK_SECTION_START,LOCK_SECTION_END中间的内容是把这一段的代码汇编到一个叫.text.lock的节中,并且这个节的属性是可重定位和可执行的,这样在代码的执行过程中,因为不同的节会被加载到不同的页去,所以如果前面不出现jmp,就在1: 处结束了。
进程在 __down_failed()中会进入睡眠,一直要等到被唤醒才会返回。其中__down_failed()的具体实现:(linux-2.6.4\arch\i386\kernel\semaphore.c)

asm(
".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 %eax\n\t"
    "pushl %edx\n\t"
    "pushl %ecx\n\t"
    "call __down\n\t"
    "popl %ecx\n\t"
    "popl %edx\n\t"
    "popl %eax\n\t"
#if defined(CONFIG_FRAME_POINTER)
    "movl %ebp,%esp\n\t"
    "popl %ebp\n\t"
#endif
    "ret"
);

上面的__down函数的代码比较复杂:

asmlinkage void __down(struct semaphore * sem)
{
    struct task_struct *tsk = current;
    DECLARE_WAITQUEUE(wait, tsk);
    unsigned long flags;

    tsk->state = TASK_UNINTERRUPTIBLE;
    spin_lock_irqsave(&sem->wait.lock, 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)) {
            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_UNINTERRUPTIBLE;
    }
    remove_wait_queue_locked(&sem->wait, &wait);
    wake_up_locked(&sem->wait);
    spin_unlock_irqrestore(&sem->wait.lock, flags);
    tsk->state = TASK_RUNNING;
}

add_wait_queue_exclusive_locked把代表当前进程的等待队列元素wait链入到由队列头sem_wait代表的等待队列的尾部。

spin_lock_irqsave既禁止本地中断,又禁止内核抢占,保存中断状态寄存器的标志位。

for循环中终止循环的代码:

if (!atomic_add_negative(sleepers - 1, &sem->count)) { sem->sleepers = 0;
            break;
    }

在for循环中,sem->sleepers表示有几个进程正在等待(包括当前进程),atomic_add_negative函数的作用是将(sleepers-1)加到sem_count上。如果相加结果为负数,返回1,否则,返回0。

假设当前进程进入__down时,count的值为0,此时还没调用spin_lock_irqsave函数,那么,由于调度或中断,就有可能被改变为1。sleepers - 1 =-1,count=1。atomic_add_negative的返回值为0,从而终止该循环,使得当前进程不需要休眠就可以进入临界区。

假设没有被修改:sleepers - 1 =-1,count=0。atomic_add_negative的返回值为1。从而继续执行schedule()函数调度当前进程。直到终止条件为真跳出循环。

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

循环终止,恢复中断,恢复内核调度,重新执行当前进程。

:"=m" (sem->count)
:"c" (sem)
:"memory");

第一行表示从内存中读取sem->count的值并存入%0中,第二行表示将sem的地址加载到%ecx中(即sem->count的地址)。

V操作函数

static inline void up(struct semaphore * sem)
{
#ifdef WAITQUEUE_DEBUG
    CHECK_MAGIC(sem->__magic);
#endif
    __asm__ __volatile__(
        "# atomic up operation\n\t"
        LOCK "incl %0\n\t"     /* ++sem->count */
        "jle 2f\n"
        "1:\n"
        LOCK_SECTION_START("")
        "2:\tcall __up_wakeup\n\t"
        "jmp 1b\n"
        LOCK_SECTION_END
        ".subsection 0\n"
        :"=m" (sem->count)
        :"c" (sem)
        :"memory");
}

直接分析中间的内联汇编:

        LOCK "incl %0\n\t" /* ++sem->count */ "jle 2f\n" "1:\n" LOCK_SECTION_START("") "2:\tcall __up_wakeup\n\t" "jmp 1b\n" LOCK_SECTION_END ".subsection 0\n" :"=m" (sem->count) :"c" (sem) :"memory");

具体分析:

 LOCK "incl %0\n\t" /* ++sem->count */

上面的代码比较简单,简单的将count的值加一。

        "jle 2f\n"
        "1:\n"

如果count的值小于等于0,表示有进程在睡眠,因此跳转到2:。

其中的2:。

        LOCK_SECTION_START("")
        "2:\tcall __up_wakeup\n\t"
        "jmp 1b\n"
        LOCK_SECTION_END

唤醒一个进程,然后返回。其中__up_wakeup的具体实现:(linux-2.6.4\arch\i386\kernel\semaphore.c)

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

其中调用了__up函数:

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

上面的代码调用wake_up函数将进程唤醒。

后面三行与前面的down()是一样的。