【嵌入式环境下linux内核及驱动学习笔记-(5-驱动的并发控制机制)】
目录
- 1、上下文和并发
-
- 1.1 上下文
- 1.2 共享与临界
-
- 内核中并发控制机制分为以下几类:
- 并发控制机制的使用场景:
- 2、 并发控制机制--中断屏蔽
- 3、并发控制机制--原子变量
- 4、并发控制机制--自旋锁
-
- 4.1 概念
- 4.2 函数
-
- a.定义自旋锁
- b.初始化自旋锁
- c.获得自旋锁(即P操作,置资源为被用)
- d.释放自旋锁(即V操作,置资源为可用)
- 使用示范
- 4.3 实例
- 5、并发控制机制--信号量
-
- 5.1 函数
-
- a.定义信号量
- b.初始化信号量
- c.获得信号量P
- d.释放信号量V
- 5.2 实例
- 6、互斥锁
-
- 6.1 函数
-
- a.初始化
- b.获取互斥体
- c.释放互斥体
- 7 、选择并发控制机制的原则
1、上下文和并发
执行流:有开始有结束总体顺序执行的一段代码 又称上下文。
1.1 上下文
- 任务上下文:普通的,具有五种状态(就绪态、运行态、睡眠态、暂停态、僵死态),可被阻塞的上下文。
- 异常上下文:异常时的中断处理上下文。不能进入阻塞状态。
对于应用层编程而言,只有任务上下文。
对于内核编程而言,即有任务上下文也有异常上下文。
1.2 共享与临界
竞态:多任务并行执行时,如果在一个时刻同时操作同一个资源,会引起资源的错乱,这种错乱情形被称为竞态
共享资源:可能会被多个任务同时使用的资源。
临界区:操作共享资源的代码段。
并发控制机制:为了解决竞态,需要提供一种控制机制,来避免在同一时刻使用共享资源,这种机制被称为并发控制机制
内核中并发控制机制分为以下几类:
- 原子操作类 :不会被系统包括异常打断的操作。
- 忙等待类 :通过循环轮询的方式,等待资源可用的操作。
- 阻塞类:资源不可用时,进入睡眠态等待资源可用。
并发控制机制的使用场景:
- 互斥场景:多个任务不能同时使用同一资源,一个在用时,其它的处于等待状态,互斥体现的是一种排它性。一般操作过程如下:
对互斥锁初始化为可用 --> 对互斥锁进行P操作锁定 --> 临界区 --> 对互斥锁进行V操作放开
- 同步场景:多个并行任务对资源的有序访问,同步体现的是一种协作性。
对互斥锁初始化为不可用 --> 先行任务完成所有操作后 --> 先行方进行V操作释放资源 --> 后行方P操作直到锁定资源 -->后行方执行操作。
2、 并发控制机制–中断屏蔽
当一个中断服务程序ISR与被打断的任务可能使用相同的资源时。这时,就需要在被打断的任务在进入临界区之前先进行中断屏蔽,等执行完成后再恢复中断。
中断屏蔽相关的函数如下:
中断屏蔽相关函数 | 使能中断相关函数 | |
---|---|---|
local_irq_disable() | loacal_irq_enable() | |
local_irq_save(flags) | loacal_irq_restore(flags) | 涉及cpu的中断屏蔽字相关 |
local_bh_disable() | local_bh_enable() | 与中断低半部有关,操作软中断 |
中断屏蔽后的临界区代码不能占用太长时间,需要尽快完成。否则中被屏蔽会引起系统调度等一系列问题。
适用场合:中断上下文与某任务共享资源时,或多个不同优先级的中断上下文间共享资源时。
3、并发控制机制–原子变量
3.1 相关函数
原子变量: 存取时不可被打断的特殊整型变量。
适用场合: 共享资源为单个整型变量的互斥场合。
对原子变量的操作必须用下面这些专用宏或函数:
原子量类型
/include/linux/types.h
typedef struct {
int counter;
} atomic_t;
该类型本质是一个数据结构。
a.设置原子量的值
头文件 /arch/arm/include/asm/atomic.h
原码:
#define ATOMIC_INIT(i) { (i) }
#define atomic_set(v,i) (((v)->counter) = (i))
所以,可以如下定义原子量的初始值
atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为0
void atomic_set(atomic_t *v,int i); //设置原子量的值为i
这样做是错误的,v = 10; // X 因为不能对原子变量赋值操作,所以这里是错误的
b.获取原子量的值
头文件 /arch/arm/include/asm/atomic.h
原码:
#define atomic_read(v) (*(volatile int *)&(v)->counter)
返回值:实质是 int类型
所以可以这样用函数
atomic_read(atomic_t *v); //返回原子量的值
c.原子变量加减
头文件 /arch/arm/include/asm/atomic.h
原码:
#define atomic_add(i, v) (void) atomic_add_return(i, v)
static inline int atomic_add_return(int i, atomic_t *v)
{
unsigned long flags;
int val;
raw_local_irq_save(flags);
val = v->counter;
v->counter = val += i;
raw_local_irq_restore(flags);
return val;
}
#define atomic_sub(i, v) (void) atomic_sub_return(i, v)
static inline int atomic_sub_return(int i, atomic_t *v)
{
unsigned long flags;
int val;
raw_local_irq_save(flags);
val = v->counter;
v->counter = val -= i;
raw_local_irq_restore(flags);
return val;
}
所以可以这样用:
void atomic_add(int i,atomic_t *v);//原子变量增加I,返回V的Int类型值
void atomic_sub(int i,atomic_t *v);//原子变量减少I,返回V的Int类型值
d.原子变量自增自减
头文件 /arch/arm/include/asm/atomic.h
原码
#define atomic_inc(v) atomic_add(1, v)
#define atomic_dec(v) atomic_sub(1, v)
所以可以这样用:
void atomic_inc(atomic_t *v);//原子变量增加1,返回V的Int类型值
void atomic_dec(atomic_t *v);//原子变量减少1,返回V的Int类型值
e.操作并测试
头文件 /arch/arm/include/asm/atomic.h
原码
#define atomic_inc_and_test(v) (atomic_add_return(1, v) == 0)
#define atomic_dec_and_test(v) (atomic_sub_return(1, v) == 0)
#define atomic_sub_and_test(i, v) (atomic_sub_return(i, v) == 0)
所以可以这样使用函数:
运算后结果为0则返回真,否则返回假
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i,atomic_t *v);
f.原子位操作方法:
头文件:/arch/arm/include/asm/bitops.h
原码:
#define ATOMIC_BITOP(name,nr,p) (__builtin_constant_p(nr) ? __atomic##name(nr, p) : ##name(nr,p))
====================================================================================
这个宏定义的意思是:
如果nr(代表bit number,即位号)是一个常数,则使用____atomic##name形式的位操作函数。
否则,使用##name形式的位操作函数。
举个例子,如果定义如下宏:
c
#define SET_BIT(nr, p) ATOMIC_BITOP(set_bit, nr, p)
那么当调用SET_BIT(5, ptr)时,如果5是一个常数,会展开为:
c
____atomic_set_bit(5, ptr)
否则,会展开为:
c
_set_bit(nr, ptr)
在宏定义中使用##运算符可以将两个符号连接成一个符号。这被称为宏连接(Macro Concatenation)。
例如在这个宏定义中:
c
#define ATOMIC_BITOP(name,nr,p) (__builtin_constant_p(nr) ? ___atomic##name(nr, p) : _##name(nr,p))
##运算符被用于:
___atomic##name:将name和____atomic_连接为一个符号,例如____atomic_set_bit
_##name:将_和name连接为一个符号,例如_set_bit
所以如果定义:
c
#define SET_BIT(nr, p) ATOMIC_BITOP(set_bit, nr, p)
那么这个宏可以展开为:
c
(__builtin_constant_p(nr) ? ____atomic_set_bit(nr, p) : _set_bit(nr,p))
=============================================================
其下为使用函数:
#define set_bit(nr,p) ATOMIC_BITOP(set_bit,nr,p)
#define clear_bit(nr,p) ATOMIC_BITOP(clear_bit,nr,p)
#define change_bit(nr,p) ATOMIC_BITOP(change_bit,nr,p)
#define test_and_set_bit(nr,p) ATOMIC_BITOP(test_and_set_bit,nr,p)
#define test_and_clear_bit(nr,p) ATOMIC_BITOP(test_and_clear_bit,nr,p)
#define test_and_change_bit(nr,p) ATOMIC_BITOP(test_and_change_bit,nr,p)
所以可以这么用:
- 设置位
void set_bit(nr, void *addr); //设置addr所指向的数据的第nr位为1 - 清除位
void clear_bit(nr , void *addr); //清除addr所指向的数据的第nr位为0 - 改变位
void change_bit(nr , void *addr); //改变addr所指向的数据的第nr位为1 - 测试位
void test_bit(nr , void *addr); //测试addr所指向的数据的第nr位是否为1
3.2 实例
要求:字符驱动只能被一个应用进程使用。即只能被一个任务open,其它任务在同一时间要打开该设备时会出错提示。
/*> File Name: atomic-only.c> 作用:以原子变量做为并发控制的手段,使本驱动同时只能被一个应用层进程所open/#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/types.h>/*1、定义重要的变量及结构体*/
struct x_dev_t {struct cdev my_dev; //cdev设备描述结构体变量atomic_t have_open; //记录驱动是否被打开的原子变量
};struct x_dev_t *pcdev;/*所有驱动函数声明*/
int open (struct inode *, struct file *);
int close (struct inode *, struct file *);//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={.open = open,.release = close,
};static int __init my_init(void){int unsucc =0;dev_t devno;int major,minor;pcdev = kzalloc(sizeof(struct x_dev_t), GFP_KERNEL);/*2、创建 devno */unsucc = alloc_chrdev_region(&devno , 0 , 1 , "atomic-char");if (unsucc){printk(" creating devno faild\\n");return -1;}major = MAJOR(devno);minor = MINOR(devno);printk("devno major = %d ; minor = %d;\\n",major , minor);/*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*//*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/cdev_init(&pcdev->my_dev , &fops);pcdev->my_dev.owner = THIS_MODULE;/*4、注册cdev结构体到内核链表中*/unsucc = cdev_add(&pcdev->my_dev,devno,1);if (unsucc){printk("cdev add faild \\n");return 1;}//初始化原子量have_open为1atomic_set(&pcdev->have_open,1);printk("the driver atomic-char initalization completed\\n");return 0;
}static void __exit my_exit(void)
{cdev_del(&pcdev->my_dev);unregister_chrdev_region(pcdev->my_dev.dev , 1);printk("*the driver atomic-char exit\\n");
}
/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int open(struct inode *pnode , struct file *pf){struct x_dev_t *p = container_of(pnode->i_cdev,struct x_dev_t , my_dev);pf->private_data = (void *)p;//在open函数中对原子量have_open进行减1并检测。=0,允许打开文件,<0则不允许打开if (atomic_dec_and_test(&p->have_open)){printk("atomic-char is opened\\n");return 0;}else{printk("device atomic-char can't be opened again\\n");atomic_inc(&p->have_open);//原子量=-1,记得这里要把原子量加回到0return -1;}
}
/*file_operations结构全成员函数.release的具体实现*/
int close(struct inode *pnode , struct file *pf){struct x_dev_t *p = (struct x_dev_t *)pf->private_data;printk("atomic-char is closed \\n");atomic_set(&p->have_open,1);return 0;
}module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");
4、并发控制机制–自旋锁
4.1 概念
自旋锁是基于忙等待的并发控制机制,由于在获取不到资源锁时会进入忙等待,不会进入睡眠状态。忙等待是占时间片的,因此这个机制不要用于过长的临界区执行。也因此,这个互斥锁可用于异常上下文,不会使异常进入阻塞状态。
适用场合:
- 异常上下文之间或异常上下文与任务上下文之间共享资源时
- 任务上下文之间且临界区执行时间很短时
- 互斥问题
4.2 函数
#include <linux/spinlock.h>
a.定义自旋锁
原码:
typedef struct spinlock {
union {
struct raw_spinlock rlock;
…
};
};
} spinlock_t;
所以使用前先定义自旋锁:
spinlock_t lock;
b.初始化自旋锁
spin_lock_init(spinlock_t *);
c.获得自旋锁(即P操作,置资源为被用)
spin_lock(spinlock_t *); //成功获得自旋锁立即返回,否则自旋在那里,直到该自旋锁的保持者释放
spin_trylock(spinlock_t *); //成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转”
d.释放自旋锁(即V操作,置资源为可用)
spin_unlock(spinlock_t *);
使用示范
#include <linux/spinlock.h>
定义spinlock_t类型的变量lock
spin_lock_init(&lock)后才能正常使用spinlockspin_lock(&lock);
临界区
spin_unlock(&lock);
4.3 实例
要求:用自旋锁实现上节中一个驱动同一时只能被打开一次。
/*> File Name: spinlock-only.c> 作用:以自旋锁做为并发控制的手段,使本驱动同时只能被一个应用层进程所open/#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/spinlock.h>/*1、定义重要的变量及结构体*/
struct x_dev_t {struct cdev my_dev; //cdev设备描述结构体变量spinlock_t lock; //自旋锁int have_open; //开启标志,0为已开启,1为未开启};struct x_dev_t *pcdev;/*所有驱动函数声明*/
int open (struct inode *, struct file *);
int close (struct inode *, struct file *);//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={.open = open,.release = close,
};static int __init my_init(void){int unsucc =0;dev_t devno;int major,minor;pcdev = kzalloc(sizeof(struct x_dev_t), GFP_KERNEL);/*2、创建 devno */unsucc = alloc_chrdev_region(&devno , 0 , 1 , "spinlock-char");if (unsucc){printk(" driver: creating devno faild\\n");return -1;}major = MAJOR(devno);minor = MINOR(devno);printk("driver : devno major = %d ; minor = %d;\\n",major , minor);/*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*//*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/cdev_init(&pcdev->my_dev , &fops);pcdev->my_dev.owner = THIS_MODULE;/*4、注册cdev结构体到内核链表中*/unsucc = cdev_add(&pcdev->my_dev,devno,1);if (unsucc){printk("driver : cdev add faild \\n");return 1;}//初始化自旋锁have_open 和 开启标志 spin_lock_init(&pcdev->lock);pcdev->have_open = 1;printk("driver : the driver spinlock-char initalization completed\\n");return 0;
}static void __exit my_exit(void)
{cdev_del(&pcdev->my_dev);unregister_chrdev_region(pcdev->my_dev.dev , 1);printk("*the driver spinlock-char exit\\n");
}
/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int open(struct inode *pnode , struct file *pf){struct x_dev_t *p = container_of(pnode->i_cdev,struct x_dev_t , my_dev);pf->private_data = (void *)p;//在open函数中对原子量have_open进行减1并检测。=0,允许打开文件,<0则不允许打开spin_lock(&p->lock);if (p->have_open == 1){p->have_open = 0;printk("driver : spinlock-char is opened\\n");spin_unlock(&p->lock);return 0;}else{ //已被打开printk("driver : device spinlock-char Could not opend again\\n");spin_unlock(&p->lock);return -1;}
}
/*file_operations结构全成员函数.release的具体实现*/
int close(struct inode *pnode , struct file *pf){printk("driver : spinlock-char is closed \\n");return 0;
}module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");
5、并发控制机制–信号量
基于阻塞的并发控制机制,当要取资源时,如遇资源不足,则会使本任务进入阻塞状态。即P操作不成功时会进入阻塞睡眠状态。
适用场合:只能用于任务上下文之间且临界区执行时间较长时的互斥或同步问题
5.1 函数
#include <linux/semaphore.h>
a.定义信号量
原码:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
使用方法:
struct semaphore sem;
b.初始化信号量
void sema_init(struct semaphore *sem, int val);
c.获得信号量P
int down(struct semaphore *sem);//深度睡眠
int down_interruptible(struct semaphore *sem);//浅度睡眠
一旦信号量sem的值为0时,则该函数会进入睡眠状态。直到sem值大于0后。
d.释放信号量V
void up(struct semaphore *sem);
5.2 实例
要求:一个内存缓冲虚拟一个设备。该设备允许应用层多个进程同时对该设备进行读写操作。这样就需要驱动对并发场景进行控制。
/*> File Name: semaphore-memory.c/#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>/*1、定义重要的变量及结构体*/#define MEM_SIZE 500struct mem_dev_t{dev_t devno;struct cdev my_dev; //cdev设备描述结构体变量char mem[MEM_SIZE]; //内存池,当成虚拟设备struct semaphore sem; //信号量
};struct mem_dev_t *mem_dev;/*驱动函数声明*/ssize_t read (struct file *, char __user *, size_t, loff_t *);
ssize_t write (struct file *, const char __user *, size_t, loff_t *);
int open (struct inode *, struct file *);
int release (struct inode *, struct file *);
//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={.open = open,.release = release,.read = read,.write = write,
};/*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*/
/*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
static int cdev_setup(struct mem_dev_t *mem_dev ){int unsucc =0;cdev_init(&mem_dev->my_dev , &fops);mem_dev->my_dev.owner = THIS_MODULE;/*4、注册cdev结构体到内核链表中*/unsucc = cdev_add(&mem_dev->my_dev,mem_dev->devno,1);if (unsucc){printk("cdev add faild \\n");return -1;}sema_init( &mem_dev->sem,1); //初始化信号量,为1return 0;}static int __init my_init(void){int major , minor;int unsucc =0;mem_dev = kzalloc(sizeof(struct mem_dev_t) , GFP_KERNEL);if (!mem_dev){printk(" allocating memory is failed");return -1;}/*2、创建 devno */unsucc = alloc_chrdev_region(&mem_dev->devno , 0 , 1 , "operate_memory");if (unsucc){printk(" creating devno is failed\\n");return -1;}else{major = MAJOR(mem_dev->devno);minor = MINOR(mem_dev->devno);printk("major = %d ; minor = %d\\n",major,minor);}/*3、 初始化cdev结构体,并联cdev结构体与file_operations.*//*4、注册cdev结构体到内核链表中*/if (cdev_setup(mem_dev) == 0){printk("the driver operate_memory initalization completed\\n");return 0; } elsereturn -1;
}static void __exit my_exit(void)
{cdev_del(&mem_dev->my_dev);unregister_chrdev_region(mem_dev->devno , 1);printk("*the driver operate_memory exit\\n");
}/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/int open(struct inode *pnode , struct file *pf){pf->private_data = (void*)mem_dev; //把全局变量指针放入到struct file结构体里printk("operate_memory is opened\\n");return 0;}
/*file_operations结构全成员函数.release的具体实现*/
int release(struct inode *pnode , struct file *pf){printk("operate_memory is closed \\n");return 0;
}/*file_operations结构全成员函数.read的具体实现*/
ssize_t read (struct file * pf, char __user * buf, size_t size , loff_t * ppos){struct mem_dev_t *pdev = pf->private_data;int count = 0;//判断偏移量的有效性if (*ppos >= MEM_SIZE){return 0;}//判断能够读到的字节数量if (size > MEM_SIZE - *ppos){count = MEM_SIZE - *ppos;}else{count = size;}//copy_from_user返回值大于0失败down(&pdev->sem); //信号量P操作if ( copy_to_user(buf , &pdev->mem[*ppos] , count )){up(&pdev->sem); //退出前释放信号量,V操作return 0;}else{*ppos += count;up(&pdev->sem); //退出前释放信号量,V操作return count;}}/*file_operations结构全成员函数.write的具体实现*/
ssize_t write (struct file * pf, const char __user *buf, size_t size , loff_t *ppos){struct mem_dev_t *pdev = pf->private_data;int count = 0;//判断偏移量的有效性if (*ppos >=MEM_SIZE ){return 0;}//判断能够写入的字节数量if (size > MEM_SIZE-*ppos){count = MEM_SIZE-*ppos;}else{count = size;}//copy_from_user返回值大于0失败down(&pdev->sem); //信号量P操作if ( copy_from_user(&pdev->mem[*ppos] , buf , count)){up(&pdev->sem); //退出前释放信号量,V操作return 0;}else{*ppos +=count;up(&pdev->sem); //退出前释放信号量,V操作return count;}}module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");
6、互斥锁
基于阻塞的互斥机制。
=适用场合:任务上下文之间且临界区执行时间较长时的互斥问题
6.1 函数
#include <linux/mutex.h>
a.初始化
struct mutex my_mutex;
mutex_init(&my_mutex);
b.获取互斥体
void mutex_lock(struct mutex *lock);
c.释放互斥体
void mutex_unlock(struct mutex *lock);
使用步骤:
- 定义对应类型的变量
- 初始化对应变量
P/加锁
临界区
V/解锁
7 、选择并发控制机制的原则
-
不允许睡眠的上下文(异常上下文)需要采用忙等待类(自旋锁,原子变量),可以睡眠的上下文可以采用阻塞类(信号量,互斥锁)。在异常上下文中访问的竞争资源一定采用忙等待类。
-
临界区操作较长的应用建议采用阻塞类,临界区很短的操作建议采用忙等待类。
-
中断屏蔽仅在有与中断上下文共享资源时使用。
-
共享资源仅是一个简单整型量时用原子变量