• 信号量


    信号量

    Linux中的信号量是一种睡眠锁

    睡眠机制:如果一个任务试图获取一个不可用(正在被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列的那个任务将被唤醒,并获得该信号量。

    • 由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长期持有的情况

    • 由于执行线程在锁被征用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进行调度的。

    • 等待信号量时会陷入睡眠,因此在占用信号量的同时不能占用自旋锁,因为在等待信号量时可能会睡眠,而持有自旋锁的时候是不允许睡眠的。

    计数信号量

    初始化时设置信号量的数值大于1的非0值,在这种情况下允许在一个时刻同时有多个锁持有者,这种信号量称为计数信号量。

    计数信号量不能用作强制互斥,因为它允许多个执行线程同时访问临界区。内核中使用的不多。

    二值信号量

    顾名思义,与计数信号量相对应,二值信号量即信号量值非0即1,也称为互斥信号量。在使用信号量时大部分用的都是二值信号量。

    创建和初始化信号量

    信号量的实现与体系结构相关,具体定义在<asm/semaphore.h>中。(我只在include/linux下找到了这个文件)

    /* Please don't access any members of this structure directly */
    struct semaphore {
    	spinlock_t		lock;
    	unsigned int		count;
    	struct list_head	wait_list;
    };
    

    静态声明信号量:

    struct semaphore name;
    sema_init(&name , count);
    

    如果是互斥信号量,还可以简化声明:

    static DECLARE_MUTEX(name);
    

    更常见的情况: 信号量作为一个大数据结构的一部分动态创建,此时只有指向该动态创建的信号量的间接指针,可以使用如下函数对它进行初始化:

    sema_init(sem,count);//sem是指针,count是信号量的使用者数量
    

    与前面类似,初始化一个动态创建的互斥信号量也可以使用如下函数:

    init_MUTEX(sem);
    

    使用信号量

    方法 描述
    sema_init( struct semaphore * , int) 以指定的计数值初始化动态创建的信号量
    init_MUTEX( struct semaphore * ) 以计数值1初始化动态创建的信号量
    init_MUTEX_LOCKED(struct semaphore *) 以计数值0初始化动态创建的信号量
    (初始化就为加锁状态)
    down_interruptible(struct semaphore *) 以试图获得指定的信号量,如果信号量已被争用
    则进入可中断睡眠状态
    down(struct semaphore *) 以试图获得指定的信号量,如果信号量已被争用
    则进入不可中断睡眠状态
    down_trylock(struct semaphore *) 以试图获得指定的信号量,如果信号量已被争用
    则立刻返回非0值
    up(struct semaphore *) 以释放指定的信号量,如果睡眠队列不空
    则唤醒其中一个任务

    内核中对down的细化操作

    extern void down(struct semaphore *sem);
    

    具体函数实现如下:

    void down(struct semaphore *sem)
    {
    	unsigned long flags;
    
    	spin_lock_irqsave(&sem->lock, flags);
    	if (likely(sem->count > 0))
    		sem->count--;
    	else
    		__down(sem);
    	spin_unlock_irqrestore(&sem->lock, flags);
    }
    

    显然,信号量的操作使用了自旋锁,这从上面信号量的声明也可以看出,对信号量的操作需要使用自旋锁同步,防止信号量操作产生竞争冒险。

    在spinlock锁区域内部,通过判断当前信号量是否为0进行相应操作,若大于0则自减即可;但是如果信号量被持有,则调用__down函数进一步处理。具体函数如下:

    static noinline void __sched __down(struct semaphore *sem)
    {
    	__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
    }
    
    
    static inline int __sched __down_common(struct semaphore *sem, long state,
    								long timeout)
    {
    	struct task_struct *task = current;
    	struct semaphore_waiter waiter;
    
    	list_add_tail(&waiter.list, &sem->wait_list);
    	waiter.task = task;
    	waiter.up = 0;
    
    	for (;;) {
    		if (signal_pending_state(state, task))
    			goto interrupted;
    		if (timeout <= 0)
    			goto timed_out;
    		__set_task_state(task, state);
    		spin_unlock_irq(&sem->lock);
    		timeout = schedule_timeout(timeout);
    		spin_lock_irq(&sem->lock);
    		if (waiter.up)
    			return 0;
    	}
    
     timed_out:
    	list_del(&waiter.list);
    	return -ETIME;
    
     interrupted:
    	list_del(&waiter.list);
    	return -EINTR;
    }
    

    注意,这里对task的状态赋值state为uninterruptible

    处理过程大概目的就是使当前请求线程进入睡眠等待队列,内核中为信号量等待队列单独设计了结构体,内容如下:

    
    struct semaphore_waiter {
    	struct list_head list;
    	struct task_struct *task;
    	int up;
    };
    

    这个结构体中的up在后面的for循环中一直在被检测,直到被置1的时候才会返回,否则线程依然会重新陷入睡眠,或者调度时间time_out耗尽返回错误。

    内核中对up的细化操作

    extern void up(struct semaphore *sem);
    
    void up(struct semaphore *sem)
    {
    	unsigned long flags;
    
    	spin_lock_irqsave(&sem->lock, flags);
    	if (likely(list_empty(&sem->wait_list)))
    		sem->count++;
    	else
    		__up(sem);
    	spin_unlock_irqrestore(&sem->lock, flags);
    }
    

    同理,up的操作也使用了自旋锁来保持信号量操作的互斥,在spinlock锁区域进行判断,当睡眠等待队列为空时代表没有人在等待该信号量,直接将信号量释放即可。但是当队列非空时要进行一个重要的操作:唤醒睡眠进程

    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->up = 1;
    	wake_up_process(waiter->task);
    }
    
    

    显然,该操作只对等待链表的第一个成员进行操作,避免多个睡眠进程争用信号量。最后将上文提到的up置1,使睡眠进程唤醒。

    读/写信号量

    与自旋锁一样,信号量也有区分读/写访问的功能。与读写自旋锁和普通自旋锁之间的关系差不多,读写信号量也要比普通信号更具优势。

    读写信号量在内核中由rw_semaphore表示,定义在文件<linux/rwsem.h>中。(2.6.34声明在rwsem-spinlock.h中)

    /*
     * the rw-semaphore definition
     * - if activity is 0 then there are no active readers or writers
     * - if activity is +ve then that is the number of active readers
     * - if activity is -1 then there is one active writer
     * - if wait_list is not empty, then there are processes waiting for the semaphore
     */
    struct rw_semaphore {
    	__s32			activity;
    	spinlock_t		wait_lock;
    	struct list_head	wait_list;
    #ifdef CONFIG_DEBUG_LOCK_ALLOC
    	struct lockdep_map dep_map;
    #endif
    };
    

    通过注释可见,读写信号量引入了activity表征读写状态,同时也使用了自旋锁和等待队列。

    同样,读写信号量也可以静态声明:

    #define DECLARE_RWSEM(name) 
    	struct rw_semaphore name = __RWSEM_INITIALIZER(name)
    
    

    或者是动态声明:

    /*
     * Initialize an rwsem:
     */
    void __init_rwsem(struct rw_semaphore *sem, const char *name,
    		  struct lock_class_key *key)
    {
    #ifdef CONFIG_DEBUG_LOCK_ALLOC
    	/*
    	 * Make sure we are not reinitializing a held semaphore:
    	 */
    	debug_check_no_locks_freed((void *)sem, sizeof(*sem));
    	lockdep_init_map(&sem->dep_map, name, key, 0);
    #endif
    	sem->count = RWSEM_UNLOCKED_VALUE;
    	spin_lock_init(&sem->wait_lock);
    	INIT_LIST_HEAD(&sem->wait_list);
    }
    

    注意:所有的读写信号量都是互斥信号量。(从结构体描述来看,它只针对写互斥)

    对其操作也使用up和down:

    extern void __down_read(struct rw_semaphore *sem);
    extern int __down_read_trylock(struct rw_semaphore *sem);
    extern void __down_write(struct rw_semaphore *sem);
    extern void __down_write_nested(struct rw_semaphore *sem, int subclass);
    extern int __down_write_trylock(struct rw_semaphore *sem);
    extern void __up_read(struct rw_semaphore *sem);
    extern void __up_write(struct rw_semaphore *sem);
    extern void __downgrade_write(struct rw_semaphore *sem);
    extern int rwsem_is_locked(struct rw_semaphore *sem);
    

    函数的使用不作赘述。

    但是要注意:这里的trylock操作与普通信号量的返回方式截然相反:读写信号量被争用返回0,而申请成功返回非0值。

    没有必要的时候用普通信号量就足够了。。。

  • 相关阅读:
    作用域 + this指向 的一道没面试题
    找出数组中最大的值
    统计数组中每个值出现的次数, 统计对象中每个字符出现的次数
    uniapp在h5 和 APP 端兼容性 bug 解决方案
    数组去重的常用方法,利用Promise实现函数按序执行
    momentjs实现距离当前时长并且回现中文效果
    SQL server 上机练习题
    JS 9
    JS 8
    JS 7
  • 原文地址:https://www.cnblogs.com/zhaipanger/p/12911852.html
Copyright © 2020-2023  润新知