信号量 - linux内核锁(三)

信号量又称为信号灯,它是用来鞋套不同进程的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,他用来记录对某个资源(如故乡内存)的存取状况。它负责获取信号量的进程可以睡眠,因此会导致系统调度。

信号signal与信号量semaphore是存在差别的,这个很多时候大家容易混淆。

信号(signal)信号量(semaphore)
使用场景处理异步事件的方式处理同步互斥的机制
作用由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常  调协进程对共享资源的访问,让一个临界区同一时间只有一个进程在访问它

1. 实例与信号量引出

目前我们看一下实际生活中遇到的两个实例,说明信号量实际的应用场景。

  1. 我们要坐火车从上海到新疆,这个任务特别的耗时,只能在车上等着车到站了,这个时候我们没有必要一直睁着眼睛等待车到站,最好的情况是我们直接在车上睡一觉,醒来车就到站了。这种体验对于人(用户)来说是最好的。相比较与线程,程序等待一个耗时的任务,没有必要一直占用CPU资源,可以先暂停当前任务使其进入休眠,等待的事件发生以后在由其他任务唤醒处理这个事件,这种场景采用信号量比较合适。
  2. 我们在等待电梯或者洗手间,这种场景需要等待的事件花费时间并不是很多,如果我们找一个地方睡觉,然后等电梯或者洗手间可以使用了再醒来,这种显然是没有必要的。我们只需要排好队,刷一刷手机就可以了。对比于计算机,如果驱动程序进入中断例程,在等待某个寄存器被置位。这种场景需要等待的时间很短暂,系统开销小于进入休眠的开销,所以这种场景采用自旋锁比较合适。

1.1 真实编码场景

无论是原子操作,还是自旋锁,都不适合长时间等待的情况,因为有很多资源(数据)它有一定的时间性,你想去获取它,CPU 并不能立即返回给你,而是要等待一段时间,才能把数据返回给你。这种情况,你用自旋锁来同步访问这种资源,你会发现这是对 CPU 时间的巨大浪费。下面我们看看另一种同步机制,既能对资源数据进行保护(同一时刻只有一个代码执行流访问),又能在资源无法满足的情况下,让 CPU 可以执行其它任务。如果你翻过操作系统的理论书,应该对信号量这个词并不陌生。信号量是 1965 年荷兰学者 Edsger Dijkstra 提出的,是一种用于资源互斥或者进程间同步的机制。这里我们就来看看如何实现这一机制。你不妨想象这样一个情境:微信等待你从键盘上的输入信息,然后把这个信息发送出去。这个功能我们怎么实现呢?下面我们就来说说实现它的一般方法,当然具体实现中可能不同,但是原理是相通的,具体如下。1. 一块内存,相当于缓冲区,用于保存键盘的按键码。2. 需要一套控制机制,比如微信读取这个缓冲区,而该缓冲区为空时怎么处理;该缓冲区中有了按键码,却没有代码执行流来读取,又该怎么处理。我们期望是这样的,一共有三点。1. 当微信获取键盘输入信息时,发现键盘缓冲区中是空的,就进入等待状态。2. 同一时刻,只能有一个代码执行流操作键盘缓冲区。3. 当用户按下键盘时,我们有能力把按键码写入缓冲区中,并且能看一看微信或者其它程序是否在等待该缓冲区,如果是就重新激活微信和其它的程序,让它们重新竞争读取键盘缓冲区,如果竞争失败依然进入等待状态。其实以上所述无非是三个问题:等待、互斥、唤醒(即重新激活等待的代码执行流)。这就需要一种全新的数据结构来解决这些问题。根据上面的问题,这个数据结构至少需要一个变量来表示互斥,比如大于 0 则代码执行流可以继续运行,等于 0 则让代码执行流进入等待状态。还需要一个等待链,用于保存等待的代码执行流。这个数据结构的实现代码如下所示。

#define SEM_FLG_MUTEX 0
#define SEM_FLG_MULTI 1
#define SEM_MUTEX_ONE_LOCK 1
#define SEM_MULTI_LOCK 0
//等待链数据结构,用于挂载等待代码执行流(线程)的结构,里面有用于挂载代码执行流的链表和计数器变量,这里我们先不深入研究这个数据结构。
typedef struct s_KWLST
{   
    spinlock_t wl_lock;
    uint_t   wl_tdnr;
    list_h_t wl_list;
}kwlst_t;
//信号量数据结构
typedef struct s_SEM
{
    spinlock_t sem_lock;//维护sem_t自身数据的自旋锁
    uint_t sem_flg;//信号量相关的标志
    sint_t sem_count;//信号量计数值
    kwlst_t sem_waitlst;//用于挂载等待代码执行流(线程)结构
}sem_t;

1.2 底层框架

信号量不同类型的硬件实现详细的底层实现原理是不停的,ARM芯片查看链接。信号量与互斥体 - ARM汇编同步机制实现(六)_生活需要深度的博客-CSDN博客

搞懂了信号量的结构,我们再来看看信号量的一般用法,注意信号量在使用之前需要先进行初始化。这里假定信号量数据结构中的 sem_count 初始化为 1,sem_waitlst 等待链初始化为空。使用信号量的步骤:

第一步,获取信号量。

  1. 首先对用于保护信号量自身的自旋锁 sem_lock 进行加锁。
  2. 对信号值 sem_count 执行“减 1”操作,并检查其值是否小于 0。
  3. 上步中检查 sem_count 如果小于 0,就让进程进入等待状态并且将其挂入 sem_waitlst 中,然后调度其它进程运行。否则表示获取信号量成功。当然最后别忘了对自旋锁 sem_lock 进行解锁。

第二步,代码执行流开始执行相关操作,例如读取键盘缓冲区。

第三步,释放信号量。

  1. 首先对用于保护信号量自身的自旋锁 sem_lock 进行加锁。
  2. 对信号值 sem_count 执行“加 1”操作,并检查其值是否大于 0。
  3. 上步中检查 sem_count 值如果大于 0,就执行唤醒 sem_waitlst 中进程的操作,并且需要调度进程时就执行进程调度操作,不管 sem_count 是否大于 0(通常会大于 0)都标记信号量释放成功。当然最后别忘了对自旋锁 sem_lock 进行解锁。

这里我给你额外分享一个小技巧,写代码之前我们常常需要先想清楚算法步骤,建议你像我这样分条列出,因为串联很容易含糊其辞,不利于后面顺畅编码。好,下面我们来看看实现上述这些功能的代码,按照理论书籍上说,信号量有两个操作:down,up,代码如下。


//获取信号量
void krlsem_down(sem_t* sem)
{
    cpuflg_t cpufg;
start_step:    
    krlspinlock_cli(&sem->sem_lock,&cpufg);
    if(sem->sem_count<1)
    {//如果信号量值小于1,则让代码执行流(线程)睡眠
        krlwlst_wait(&sem->sem_waitlst);
        krlspinunlock_sti(&sem->sem_lock,&cpufg);
        krlschedul();//切换代码执行流,下次恢复执行时依然从下一行开始执行,所以要goto开始处重新获取信号量
        goto start_step; 
    }
    sem->sem_count--;//信号量值减1,表示成功获取信号量
    krlspinunlock_sti(&sem->sem_lock,&cpufg);
    return;
}
//释放信号量
void krlsem_up(sem_t* sem)
{
    cpuflg_t cpufg;
    krlspinlock_cli(&sem->sem_lock,&cpufg);
    sem->sem_count++;//释放信号量
    if(sem->sem_count<1)
    {//如果小于1,则说数据结构出错了,挂起系统
        krlspinunlock_sti(&sem->sem_lock,&cpufg);
        hal_sysdie("sem up err");
    }
    //唤醒该信号量上所有等待的代码执行流(线程)
    krlwlst_allup(&sem->sem_waitlst);
    krlspinunlock_sti(&sem->sem_lock,&cpufg);
    krlsched_set_schedflgs();
    return;
}

2. 实例

2.1 信号量接口说明

 // 1)定义一个信号量   
 	struct semaphore btn_sem;
 	
 // 2) 初始化信号量          
 	void sema_init(&btn_sem, 5);    //该信号量可以被5个执行单元持有
    //还可以通过以下宏完成信号量的定义和赋值为1             
    DEFINE_SEMAPHORE(btn_sem);
    
 // 3) 获取信号量,本质就是给其中的计数-1(获取权利)
   	//成功立即返回,失败使用调用者进程进入睡眠状态(深度睡眠kiii -9都杀不死) ,
  	//直到可以获取信号量成功才被唤醒、返回
    //该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq 
    //上下文)使用该函数。该函数首先判断sem->count的值是否大于0,如果true则sem->count–,否者 
    //调用者将被挂起,直到别的任务释放该信号量才能继续运行。
	void down(struct semaphore *sem);

  	//成功立即返回,失败进入可中断的睡眠状态(潜睡眠,可被ctrl+c打断)
    //可以获取信号量 + 收到信号(ctrl+c)
    //该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible 
    //能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得 
    //信号量正常返回,如果被信号打断,返回-EINTR。
  	int down_interruptible(struct semaphore *sem); //关注返回值

    //失败立即返回一个错误信息,不会导致睡眠
    //可以在中断上下文中使用
    //该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获 
    //得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用。
 	int down_trylock(struct semaphore *sem);

	//失败进入可以kill的睡眠状态 
	int down_killable(struct semaphore *sem);     
	
	//获取信号量,指定超时时间为x
    //如果获取信号量不成功,对应的进程进入睡眠状态
    //可能因为信号量可用而被唤醒/也可能因为定时时间到而被唤醒
	int down_timeout(struct semaphore *sem, long jiffies);
	
 // 4) 执行临界区代码,访问共享资源
 
 // 5)释放信号量,本质就是给计数器+1
    void up(struct semaphore *sem);

2.2 实例 

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/delay.h>

MODULE_LICENSE("GPL");

dev_t dev;
struct cdev btn_cdev;
struct class *cls = NULL;

int count = 1; //共享资源

spinlock_t btn_lock;
struct semaphore btn_sem;

int btn_open(struct inode *inode, struct file *filp)
{ 
    down(&btn_sem);      //3.获取信号量

    count--;             //4.执行临界区代码,访问共享资源
    if(count !=0 )       
    {
        count++;  
        /*释放信号量*/
        up(&btn_sem);
        return -EBUSY;
    }

    //msleep(1000* 20);
    //mdelay(1000*20);
   
    up(&btn_sem);          /*5.释放信号量*/
    
    return 0;
}

int btn_close(struct inode *inode, struct file *filp)
{
    down(&btn_sem);
    count++;
    up(&btn_sem);
    return 0;
}

struct file_operations btn_fops =
{
    .owner = THIS_MODULE,
    .open  = btn_open,
    .release = btn_close,
};

int __init btn_drv_init(void)
{
    /*设备号的申请注册*/
    alloc_chrdev_region(&dev, 100, 1, "mybuttons");
    /*初始化cdev*/
    cdev_init(&btn_cdev, &btn_fops);
    /*注册cdev*/
    cdev_add(&btn_cdev, dev, 1);
    /*设备文件的创建*/
    cls = class_create(THIS_MODULE, "buttons");
    device_create(cls, NULL, dev, NULL,"mybuttons");
  
    
    spin_lock_init(&btn_lock);      /*2.初始化自旋锁*/
    sema_init(&btn_sem, 1);

    return 0;
}

void __exit btn_drv_exit(void)
{
    /*销毁设备文件*/
    device_destroy(cls, dev);
    class_destroy(cls);

    /*注销cdev*/
    cdev_del(&btn_cdev);

    /*注销设备号*/
    unregister_chrdev_region(dev, 1);
}
module_init(btn_drv_init);
module_exit(btn_drv_exit);
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值