Linux同步互斥9

Linux同步互斥9(基于Linux6.6)---信号量

 


一、 概述

在 Linux 内核和多线程编程中,信号量(Semaphore) 是一种常用的同步机制,旨在控制多个进程或线程对共享资源的访问。信号量可以帮助解决 互斥同步 问题,是内核中用于进程间同步和互斥的基本工具之一。

信号量在 Linux 内核中通常用于:

  • 进程同步:用于确保多个进程或线程按照特定顺序执行。
  • 互斥控制:防止多个进程或线程同时访问共享资源,从而避免数据竞争和不一致。

1.1、信号量的类型

在 Linux 内核中,信号量有两种主要类型:

  1. 二值信号量(Binary Semaphore)

    • 也称为 互斥量(Mutex),它的值通常是 0 或 1。
    • 这种信号量用于实现互斥锁的功能。一个二值信号量要么被 "占用"(值为 0),要么 "空闲"(值为 1)。
    • 它用于控制对共享资源的访问。
  2. 计数信号量(Counting Semaphore)

    • 计数信号量的值可以是任何非负整数。
    • 用于控制对多个相同资源的访问。例如,多个进程可以同时访问同一个类型的共享资源,但不超过一定的限制。

1.2、信号量的基本操作

在 Linux 内核中,信号量通常有两个主要操作:

  1. P 操作(Proberen):也叫 down 操作。

    • 这个操作会尝试减少信号量的值。如果信号量的值大于 0,则该操作会使信号量的值减 1,并允许进程继续执行。
    • 如果信号量的值为 0,进程将会阻塞,直到信号量的值变为正值。
  2. V 操作(Verhogen):也叫 up 操作。

    • 这个操作会增加信号量的值。如果信号量的值为 0,则执行此操作后,如果有其他进程被阻塞,它们将会被唤醒。

这两个操作可以简单地理解为:

  • P 操作(down):请求资源,如果资源不可用(信号量为 0),则阻塞。
  • V 操作(up):释放资源,并通知等待的进程。

1.3、内核中的信号量

在 Linux 内核中,信号量通常由 struct semaphore 类型表示,内核提供了一些标准函数来操作信号量:

  • 初始化信号量

void sema_init(struct semaphore *sem, int val);

用于初始化信号量 sem,并将其初始值设为 val。例如,如果要创建一个初始值为 1 的信号量(通常用于互斥),可以调用:

  • sema_init(&my_semaphore, 1);
    
  • P 操作(down)

void down(struct semaphore *sem);

此函数用于执行 P 操作,进程会阻塞直到信号量的值大于 0。

还有一个变种:

  • int down_interruptible(struct semaphore *sem);
    

    如果信号量不可用时,down_interruptible 会使进程处于可中断的状态,即可以响应信号。

  • V 操作(up)

void up(struct semaphore *sem);

此函数用于执行 V 操作,进程释放资源并通知等待的进程。

还有一个变种:

  • void up_interruptible(struct semaphore *sem);
    

    这是一个安全的版本,可以处理中断信号。

1.4、信号量的使用场景

信号量常常用于以下场景:

1. 互斥

在多个进程或线程需要访问共享资源时,通过信号量可以保证同一时刻只有一个进程能访问该资源,防止竞争条件。例如,一个信号量用于控制对共享文件或内存区域的访问。

static DEFINE_SEMAPHORE(my_semaphore);

void access_shared_resource(void) {
    down(&my_semaphore);  // 请求资源
    // 访问共享资源
    up(&my_semaphore);    // 释放资源
}
2. 资源计数

信号量可以用于限制对有限资源的访问。例如,如果有 3 个可用的打印机设备,信号量的初始值可以设为 3,当一个进程使用完设备后,通过 up 操作释放信号量,允许其他进程使用该设备。

static DEFINE_SEMAPHORE(printer_semaphore);

void use_printer(void) {
    down(&printer_semaphore);  // 等待可用的打印机
    // 使用打印机
    up(&printer_semaphore);    // 释放打印机
}
3. 进程同步

信号量也常用于进程间的同步,例如保证某些操作按顺序进行。比如,两个进程之间可能需要等待彼此完成某个任务才能继续执行。

static DEFINE_SEMAPHORE(sync_semaphore);

void process_1(void) {
    // 执行某些任务
    up(&sync_semaphore);  // 向信号量发送通知
}

void process_2(void) {
    down(&sync_semaphore);  // 等待信号量
    // 执行后续任务
}
4. 防止死锁

虽然信号量本身不会直接防止死锁,但它们可以通过精确控制资源的分配来减少死锁的发生。为了避免死锁,内核中会使用一些信号量的操作技巧,如避免嵌套锁等。

二、 信号量的初始化

2.1、信号量数据结构

 数据机构struct semaphore用于描述信号量。

include/linux/semaphore.h 

/* Please don't access any members of this structure directly */
struct semaphore {
    raw_spinlock_t        lock;-----------------------------spinlock变量,用于对信号量数据结构里count和wait_list成员的保护。
    unsigned int        count;------------------------------用于表示允许进入临界区的内核执行路径个数。
    struct list_head    wait_list;--------------------------用于管理所有在该信号量上睡眠的进程,没有成功获取锁的进程会睡眠在这个链表上。
};

数据结构struct semaphore_waiter用于描述将在信号量等待队列山等待的进程。

include/linux/semaphore.h 

struct semaphore_waiter {
    struct list_head list;---------------------------------链表项
    struct task_struct *task;------------------------------将要放到信号量等待队列上的进程结构
    bool up;
};

2.2、初始化

 信号量的初始化有两种,一种是通过sema_init()动态初始化一个信号量,另一种是通过DEFINE_SEMAPHORE()静态定义一个信号量。

这两者都通过__SEMAPHORE_INITIALIZER()完成初始化工作。区别是sema_init()提供了lockdep调试跟踪,而且sema_init()可以指定持锁路径个数;而DEFINE_SEMAPHORE()默认为1。

include/linux/semaphore.h 

#define __SEMAPHORE_INITIALIZER(name, n)                \
{                                    \
    .lock        = __RAW_SPIN_LOCK_UNLOCKED((name).lock),    \
    .count        = n,                        \
    .wait_list    = LIST_HEAD_INIT((name).wait_list),        \
}

#define DEFINE_SEMAPHORE(name)    \
    struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)---------------和sema_init()区别在于此处只有1.

static inline void sema_init(struct semaphore *sem, int val)
{
    static struct lock_class_key __key;
    *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
    lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

三、 down()/up()

信号量的使用较简单,down_xxx()持有信号量,up()释放信号量。

down()有很多变种,基本上遵循一致的规则:首先判断sem->count是否大于0,如果大于0,则sem->count--;否则调用__down_xxx()函数。

__down_xxx()最终都会调用__down_common()函数,他们之间的区别就是参数不一样。

down()变种flagtimeout说明
down()TASK_UNINTERRUPTIBLEMAX_SCHEDULE_TIMEOUT争用信号量失败时进入不可中断的睡眠状态。
down_interruptible()TASK_INTERRUPTIBLEMAX_SCHEDULE_TIMEOUT争用信号量失败时进入可中断的睡眠状态。
down_killable()TASK_KILLABLEMAX_SCHEDULE_TIMEOUT争用信号量失败时进入不可中断睡眠状态,但是在收到致命信号时唤醒睡眠进程。
down_timeout()TASK_UNINTERRUPTIBLEtimeout争用信号量失败时进入不可中断的睡眠状态,超时则唤醒当前进程。

down_trylock()是个特例,并不会等待,只是单纯的去获取锁。返回0表示获取锁成功,返回1表示获取锁失败。

kernel/locking/semaphore.c 

void down(struct semaphore *sem)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);-----------------获取spinlock并关本地中断来保护count数据。
    if (likely(sem->count > 0))-------------------------------如果大于0则表明当前进程可以成功获取信号量。
        sem->count--;
    else
        __down(sem);------------------------------------------获取失败,等待。
    raw_spin_unlock_irqrestore(&sem->lock, flags);------------恢复中断寄存器,打开本地中断,并释放spinlock。
}

static noinline void __sched __down(struct semaphore *sem)
{
    __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

int down_interruptible(struct semaphore *sem)
{
    unsigned long flags;
    int result = 0;

    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(sem->count > 0))
        sem->count--;
    else
        result = __down_interruptible(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);

    return result;
}

static noinline int __sched __down_interruptible(struct semaphore *sem)
{
    return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

int down_killable(struct semaphore *sem)
{
    unsigned long flags;
    int result = 0;

    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(sem->count > 0))
        sem->count--;
    else
        result = __down_killable(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);

    return result;
}

static noinline int __sched __down_killable(struct semaphore *sem)
{
    return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT);
}

int down_trylock(struct semaphore *sem)
{
    unsigned long flags;
    int count;

    raw_spin_lock_irqsave(&sem->lock, flags);
    count = sem->count - 1;
    if (likely(count >= 0))-------------------------------判断当前sem->count的减1后是否大于等于0。如果小于0,则表示无法获取信号量;如果大于等于0,表示可以成功获取信号量,并更新sem->count的值。
        sem->count = count;
    raw_spin_unlock_irqrestore(&sem->lock, flags);

    return (count < 0);-----------------------------------如果count<0,表示无法获取信号量;如果count<0不成立,则表示获取信号量失败。
}

int down_timeout(struct semaphore *sem, long timeout)
{
    unsigned long flags;
    int result = 0;

    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(sem->count > 0))
        sem->count--;
    else
        result = __down_timeout(sem, timeout);
    raw_spin_unlock_irqrestore(&sem->lock, flags);

    return result;
}

static noinline int __sched __down_timeout(struct semaphore *sem, long timeout)
{
    return __down_common(sem, TASK_UNINTERRUPTIBLE, timeout);
}

kernel/locking/semaphore.c 

static inline int __sched __down_common(struct semaphore *sem, long state,
                                long timeout)
{
    struct task_struct *task = current;-------------------得到当前进程结构
    struct semaphore_waiter waiter;-----------------------struct semaphore_waiter数据结构用于描述获取信号量失败的进程,每个进程会有一个semaphore_waiter数据结构,并把当前进程放到信号量sem的成员变量wait_list链表中。

    list_add_tail(&waiter.list, &sem->wait_list);---------将waiter加入到信号量sem->waiter_list尾部
    waiter.task = task;-----------------------------------waiter.task指向当前正在运行的进程。
    waiter.up = false;

    for (;;) {
        if (signal_pending_state(state, task))------------根据不同state和当前信号pending情况,决定是否进入interrupted处理。
            goto interrupted;
        if (unlikely(timeout <= 0))-----------------------timeout设置错误
            goto timed_out;
        __set_task_state(task, state);--------------------设置当前进程task->state。
        raw_spin_unlock_irq(&sem->lock);------------------下面即将睡眠,这里释放了spinlock锁,和down()中的获取spinlock锁对应。
        timeout = schedule_timeout(timeout);--------------主动让出CPU,相当于当前进程睡眠。
        raw_spin_lock_irq(&sem->lock);--------------------重新获取spinlock锁,在down()会重新释放锁。这里保证了schedule_timeout()不在spinlock环境中。
        if (waiter.up)------------------------------------waiter.up为true时,说明睡眠在waiter_list队列中的进程被该信号量的up操作唤醒。
            return 0;
    }

 timed_out:
    list_del(&waiter.list);
    return -ETIME;

 interrupted:
    list_del(&waiter.list);
    return -EINTR;
}

static inline int signal_pending_state(long state, struct task_struct *p){	  if (!(state & (TASK_INTERRUPTIBLE | TASK_WAKEKILL)))---------------------对于TASK_UNINTERRUPTIBLE,返回0,继续睡眠。TASK_INTERRUPTIBLE和TASK_WAKEKILL则往下继续判断。		    return 0;	  if (!signal_pending(p))--------------------------------------------------TASK_INTERRUPTIBLE和TASK_WAKEKILL情况,如果没有信号pending,则返回0,继续睡眠.		    return 0;

   return (state & TASK_INTERRUPTIBLE) || __fatal_signal_pending(p);--------如果是TASK_INTERRUPTIBLE或有SIGKILL信号未处理,则返回1,中断睡眠等待。}

signed long __sched schedule_timeout(signed long timeout)
{
    struct timer_list timer;
    unsigned long expire;

    switch (timeout)
    {
    case MAX_SCHEDULE_TIMEOUT:
        schedule();-------------------------------------------------------MAX_SCHEDULE_TIMEOUT并不设置一个具体的时间,仅是睡眠。
        goto out;
    default:
        if (timeout < 0) {
            printk(KERN_ERR "schedule_timeout: wrong timeout "
                "value %lx\n", timeout);
            dump_stack();
            current->state = TASK_RUNNING;
            goto out;
        }
    }

    expire = timeout + jiffies;

    setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
    __mod_timer(&timer, expire, false, TIMER_NOT_PINNED);----------------这时一个timer,超时函数为process_timeout(),超时后wake_up_process()唤醒当前进程current。
    schedule();
    del_singleshot_timer_sync(&timer);-----------------------------------删除timer

    /* Remove the timer from the object tracker */
    destroy_timer_on_stack(&timer);--------------------------------------销毁timer

    timeout = expire - jiffies;------------------------------------------还剩多少jiffies达到超时点。

 out:
    return timeout < 0 ? 0 : timeout;------------------------------------timeout<0表示已超过超时点;timeout>0表示提前了timeout个jiffies唤醒了。
}

kernel/locking/semaphore.c 

void up(struct semaphore *sem)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(list_empty(&sem->wait_list)))---------------------------如果信号量上的等待队列sem->wait_list为空,说明没有进程在等待该信号来那个,那么直接sem->count加1。
        sem->count++;
    else
        __up(sem);-----------------------------------------------------如果不为空,说明有进程在等待队列里睡眠,调用__up()唤醒。
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}

static noinline void __sched __up(struct semaphore *sem)
{
    struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
                        struct semaphore_waiter, list);
    list_del(&waiter->list);--------------------------------------------将waiter从信号量等待队列列表删除。
    waiter->up = true;--------------------------------------------------修改该信号量等待队列上waiter->up变量。
    wake_up_process(waiter->task);--------------------------------------唤醒该信号量等待队列上的进程。
}

int wake_up_process(struct task_struct *p)
{
    WARN_ON(task_is_stopped_or_traced(p));
    return try_to_wake_up(p, TASK_NORMAL, 0);
}

四、 举例应用

在 Linux 内核或用户空间中,信号量(Semaphore)广泛应用于进程间和线程间的同步与互斥操作。以下是几个常见的信号量应用实例,这些示例展示了如何使用信号量来管理资源、实现同步以及避免死锁等问题。


4.1、互斥(Mutex)——实现进程间互斥访问共享资源

互斥量(Mutex)是一种特殊的二值信号量,用于在多个进程或线程之间实现互斥访问。它的初始值通常为 1,代表资源可用。当进程访问共享资源时,信号量的值会被设置为 0,直到资源被释放。

示例:

假设我们有一个共享资源(例如,打印机),多个进程需要访问该资源。为了避免多个进程同时访问打印机导致数据冲突,我们使用信号量来实现互斥。

#include <linux/semaphore.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

static struct semaphore printer_mutex;  // 定义信号量

// 模拟访问打印机的函数
void access_printer(void)
{
    printk(KERN_INFO "Requesting to use printer\n");

    down(&printer_mutex);  // 请求资源(信号量 P 操作)

    printk(KERN_INFO "Printer is in use\n");
    // 执行打印操作
    printk(KERN_INFO "Printing done\n");

    up(&printer_mutex);  // 释放资源(信号量 V 操作)
}

static int __init semaphore_example_init(void)
{
    // 初始化信号量
    sema_init(&printer_mutex, 1);  // 初始值为 1,表示可用

    printk(KERN_INFO "Module loaded, ready to use printer\n");

    access_printer();  // 模拟一次打印操作

    return 0;
}

static void __exit semaphore_example_exit(void)
{
    printk(KERN_INFO "Module unloaded\n");
}

module_init(semaphore_example_init);
module_exit(semaphore_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Semaphore Example for Mutex");

解释

  • sema_init(&printer_mutex, 1):初始化一个信号量 printer_mutex,初始值为 1,表示资源可用。
  • down(&printer_mutex):请求资源,如果信号量值为 0,进程会阻塞,直到资源可用。
  • up(&printer_mutex):释放资源,信号量值增加,允许其他进程访问资源。

使用场景

  • 在多进程环境中,通过信号量确保同一时间只有一个进程能够访问共享资源,从而避免竞态条件。

4.2、资源计数(Counting Semaphore)——限制并发访问共享资源

当有多个相同类型的资源时,使用计数信号量可以控制对这些资源的访问。例如,有 3 台打印机,最多允许 3 个进程同时使用打印机。

示例:

假设有 3 台打印机,我们可以使用计数信号量来控制并发进程对打印机的访问。

#include <linux/semaphore.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

#define NUM_PRINTERS 3  // 定义可用的打印机数量

static struct semaphore printer_semaphore;  // 定义计数信号量

// 模拟访问打印机的函数
void use_printer(void)
{
    printk(KERN_INFO "Attempting to use a printer\n");

    down(&printer_semaphore);  // 请求资源

    printk(KERN_INFO "Using printer\n");
    // 执行打印操作
    printk(KERN_INFO "Printing done\n");

    up(&printer_semaphore);  // 释放资源
}

static int __init semaphore_example_init(void)
{
    // 初始化信号量,初始值为 NUM_PRINTERS,表示有 3 台打印机
    sema_init(&printer_semaphore, NUM_PRINTERS);

    printk(KERN_INFO "Module loaded, printers available\n");

    use_printer();  // 模拟一次打印操作
    use_printer();  // 再次模拟打印操作

    return 0;
}

static void __exit semaphore_example_exit(void)
{
    printk(KERN_INFO "Module unloaded\n");
}

module_init(semaphore_example_init);
module_exit(semaphore_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Semaphore Example for Counting Semaphore");

解释

  • sema_init(&printer_semaphore, NUM_PRINTERS):初始化信号量 printer_semaphore,初始值为 NUM_PRINTERS(即 3),表示有 3 台打印机可用。
  • down(&printer_semaphore):请求资源。如果有可用的打印机,信号量减 1,允许访问。如果没有可用的打印机,进程会阻塞,直到有打印机被释放。
  • up(&printer_semaphore):释放资源,将信号量值加 1,允许其他等待的进程访问打印机。

使用场景

  • 限制对有限资源的并发访问。例如,有多个相同的设备或网络连接,可以用计数信号量来限制并发数量。

4.3、进程同步——保证操作的顺序执行

在多个进程或线程之间,有时需要确保某些操作按特定的顺序进行。这种同步需求可以通过信号量来实现。例如,确保进程 A 完成某个任务后,进程 B 才能开始执行。

示例:

假设有两个进程:进程 A 执行任务 1,进程 B 执行任务 2。我们希望进程 B 只能在进程 A 完成任务 1 后才能开始执行。

#include <linux/semaphore.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

static struct semaphore sync_semaphore;  // 定义信号量

// 进程 A 执行的任务
void process_A(void)
{
    printk(KERN_INFO "Process A: Starting task 1\n");
    // 执行任务 1
    printk(KERN_INFO "Process A: Task 1 complete\n");

    up(&sync_semaphore);  // 任务 1 完成后通知进程 B
}

// 进程 B 执行的任务
void process_B(void)
{
    down(&sync_semaphore);  // 等待进程 A 完成任务 1

    printk(KERN_INFO "Process B: Starting task 2\n");
    // 执行任务 2
    printk(KERN_INFO "Process B: Task 2 complete\n");
}

static int __init semaphore_example_init(void)
{
    sema_init(&sync_semaphore, 0);  // 初始化信号量,初始值为 0,表示进程 B 会被阻塞

    printk(KERN_INFO "Module loaded\n");

    process_A();  // 执行进程 A 的任务
    process_B();  // 执行进程 B 的任务

    return 0;
}

static void __exit semaphore_example_exit(void)
{
    printk(KERN_INFO "Module unloaded\n");
}

module_init(semaphore_example_init);
module_exit(semaphore_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Semaphore Example for Process Synchronization");

解释

  • sema_init(&sync_semaphore, 0):初始化信号量 sync_semaphore,初始值为 0,表示进程 B 会被阻塞,直到进程 A 执行完任务并调用 up() 来通知。
  • down(&sync_semaphore):进程 B 会在信号量为 0 时阻塞,直到进程 A 调用 up() 释放信号量,通知进程 B 开始执行任务。

使用场景

  • 用于确保多个进程按顺序执行某些操作。例如,保证一个进程的任务在另一个进程之前完成。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值