并发控制 --- 信号量与互斥体
信号量
信号量的使用
信号量(semaphore)是用于保护临界区的一种常用方法,他的用法和自旋锁类似,但是,与自旋锁不同的是,当获取不到信号量时,进程不会原地打转,而是进入休眠等状态。
Linux中信号量的操作主要有
1.定义信号量
struct semaphore sem;
2.初始化信号量
void sema_init(struct semaphore *sem, int val);
该函数初始化信号量,并设置信号量sem的值为val,尽管信号量可以被初始化为大于1的值而成为一个计数信号量,但是通常不建议这么去做。
#define init_MUTEX(sem) sema_init(sem, 1)
这个宏用于初始化一个互斥的信号量,把sem的值设置为1.
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
初始化一个信号量,值设置为0;
也可以使用如下快捷方式初始化
DELCEAR_MUTEX(name)
DELCEAR_MUTEX_LOCKED(name)
3.获得信号量
void down(struct semaphore *sem);
该函数用于获得信号量sem,它会导致睡眠,因此不能再中断上下文中使用;
int down_interruptible(struct semaphore *sem);
该函数与down类似,因为down进入睡眠状态的进程不能被信号打断,但因为down_interruptible而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,返回非0;
int down_trylock(struct semaphone *sem);
该函数尝试获得信号量sem,如果能立即获得返回0,否则返回非0,不会导致调用者睡眠,可以再中断上下文中使用。
在使用down_interruptible()获得信号量时,对返回值一般会进行检查,如果非0,通常立即返回 -ERESTARTSYS
[cpp] view plain copy
- if(down_interruptible(&sem))
- return -ERESTARTSYS;
4.释放信号
void up(struct semaphore *sem);
该函数释放信号量sem,唤醒等待者。
用法
[cpp] view plain copy
- DECLARE_MUTEX(mount_sem);
- down(&mount_sem); //get semaphore
- ...
- critical section
- ...
- up(&mount_sem); //release semaphore
举例使用信号量实现设备只能被一个进程打开
[cpp] view plain copy
- static DECLARE_MUTEX(xxx_lock); //declare mutex lock
- static int xxx_open(struct ...)
- {
- ...
- if(down_trylock(&xxx_lock))
- return -EBUSY;
- ...
- return 0;
- }
- static int xxx_release(struct ...)
- {
- up(&xxx_lock);
- return 0;
- }
信号量用于同步
如果信号量初始化为0, 则它可以用于同步,同步意味着一个执行单元的继续执行需要另外一个执行单元完成某事,保证执行的先后顺序。
下面一图说明此事
执行单元A 执行单元B
struct semphore sem;
init_MUTEX_LOCKED(&sem); 代码区域c
代码区域a
down(&sem); <---------激活------ up(&sem)
代码区域b
在执行单元A中,因为互斥量被设置为0,所以在down的时候试图获得信号量,因为得不到而进入休眠,所以代码区域b一直无法执行到,当执行单元B 发生,执行up来释放互斥量,使得执行单元A被唤醒,从而继续往下执行。
完成量用于同步
完成量的用法
1. 定义完成量
struct completion my_compeltion;
2. 初始化completion
init_completion(&my_completion);
or
DECLARE_COMPLETION(my_completion);
3. 等待完成量
下面函数用于等待一个完成量被唤醒
void wait_for_completion(struct completion *c);
4. 唤醒完成量
void completion(struct completion *c);
void completion_all(struct completion *c);
前者只唤醒一个等待的执行单元,后者释放所有等待同意完成量的执行单元。
完成量的同步功能
执行单元A 执行单元B
struct completion com;
init_completion(&com); 代码区域c
代码区域a
wait_for_completion(&com); <---------激活------ completion(&com);
代码区域b
下面举例说明完成量用于同步。
我们还是沿用我们之前的globalmem字符驱动,我们这么来设计我们的代码,在read回调函数最开始的时候,我们等待完成量被激活,在write回调函数的最后我们释放完成量,这样的话,我们先cat globalmem,会等待完成量,只有当我们write之后,完成量才会被释放,这样才会执行读的动作。
[cpp] view plain copy
- struct completion com;
[cpp] view plain copy
- static ssize_t globalmem_read(struct file *filp, char __user *buf,size_t size,
- loff_t *ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct globalmem_dev *dev = filp->private_data; //get global data pointer
- //wait for write then read go...
- wait_for_completion(&com);
- if(p>=GLOBALMEM_SIZE)
- return 0;
- if(count > GLOBALMEM_SIZE-p)
- count = GLOBALMEM_SIZE-p;
- if(copy_to_user(buf, (void *)(dev->mem + p), count))
- ret = -EFAULT;
- else {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "read %u bytes(s) from %lu\n",count,p);
- }
- return ret;
- }
- static ssize_t globalmem_write(struct file *filp, const char __user *buf,
- size_t size, loff_t *ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct globalmem_dev *dev = filp->private_data; //get global data pointer
- if(p >= GLOBALMEM_SIZE)
- return 0;
- if(count > GLOBALMEM_SIZE - p)
- count = GLOBALMEM_SIZE - p;
- if(copy_from_user(dev->mem+p, buf, count)) {
- printk(KERN_INFO "copy from user error!!\n");
- ret = -EFAULT;
- }
- else {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "written %u bytes(s) from %lu\n",count,p);
- }
- //after write then active completion begin to read ...
- complete(&com);
- return ret;
- }
[cpp] view plain copy
- int globalmem_init(void)
- {
- int result;
- // spin_lock_init(&lock);
- init_completion(&com);
重新编译模块,在加载我们的globalmem模块,然后进行测试
我们先cat
[plain] view plain copy
- jay@jay:/dev$
- jay@jay:/dev$ cat globalmem
发现光标停在那,不动了,因为在等待完成量,然后我们另开一个终端,去echo
[plain] view plain copy
- jay@jay:/dev$ echo "456" > globalmem
当我们往里面写数据之后发现之前的终端有反应了
[plain] view plain copy
- jay@jay:/dev$ cat globalmem
- 456
这样就达到了我们的目的,这里我只是举了一个很简单的例子,个人感觉这个方法设计代码有时候蛮有用处的,特别是当2个驱动之间有一些数据的同步,或者一些设置的同步时,可以利用完成量来设计代码达到同步的效果。
自旋锁 VS 信号量
自旋锁和信号量都是解决互斥问题的基本手段,面对特定的情况,我们要加以选择。
信号量时进程级的,用于多个进程之间对资源的互斥。如果竞争失败,会发生进程上下文切换,因为进程上下文切换的开销比较大,因此,只有当进程占用资源时间较长时,选用信号量才是较好的选择。
所要保护的临界资源访问时间比较短时,用自旋锁是非常方便的,它不会引起进程睡眠而导致上下文切换。
总结:
1. 如果访问临界资源的时间较长,则选用信号量,否则选用自旋锁。
2. 信号量所保护的临界资源区可包含可能引起阻塞的代码,而自旋锁则绝对要避免这样的代码,阻塞意味着需要进程上下文切换,如果进程被切换出去,这个时候如果另外一个进程想获得自旋锁的话,会引起死锁。
3. 信号量存在于进程上下文,因此,如果被保护的资源需要在中断或者软终端情况下使用,则只能选择自旋锁。
互斥体
虽然信号量已经可以实现互斥的功能,而且有一些宏定义可以使用,但在Linux 中还是有一套标准的mutex机制。
定义互斥体并初始化
struct mutex my_mutex;
mutex_init(&my_mutex);
获取互斥体
void inline __sched mutex_lock(struct mutex *lock);
int __sched mutex_lock_interruptible(struct mutex *lock);
int __sched mutex_trylock(struct mutex *lock);
这三个函数与spinlock的几个类似的函数用法相同。
释放互斥体
void __sched mutex_unlock(struct mutex *lock);
mutex的使用方法和信号量用于互斥的场合完全一样
[cpp] view plain copy
- struct mutex my_mutex; //declare mutex
- mutex_init(&mu_mutex); //init mutex
- mutex_lock(&my_mutex);
- ...... //临界资源
- mutex_unlock(&my_mutex);
修改我们的globalmem字符驱动增加并发控制
定义初始化信号量
[cpp] view plain copy
- struct globalmem_dev {
- struct miscdevice mdev;
- unsigned char mem[GLOBALMEM_SIZE];
- struct semaphore sem;
- };
[cpp] view plain copy
- int globalmem_init(void)
- {
- int result;
- globalmem_devp = kmalloc(sizeof(struct globalmem_dev) ,GFP_KERNEL);
- if(!globalmem_devp) {
- result = -ENOMEM;
- goto fail_malloc;
- }
- memset(globalmem_devp, 0, sizeof(struct globalmem_dev));
- globalmem_devp->mdev = mdev_struct;
- result = misc_register(&(globalmem_devp->mdev));
- if(result<0)
- return result;
- else {
- init_MUTEX(&globalmem_devp->sem);
- return 0;
- }
- fail_malloc:
- return result;
- }
在read/write/ioctl 函数中添加信号量
[cpp] view plain copy
- static int globalmem_ioctl(struct inode *inode, struct file *filp,
- unsigned int cmd, unsigned long arg)
- {
- struct globalmem_dev *dev = filp->private_data; //get global data pointer
- switch(cmd) {
- case MEM_CLEAR:
- if(down_interruptible(&dev->sem))
- return -ERESTARTSYS;
- memset(dev->mem, 0, GLOBALMEM_SIZE);
- up(&dev->sem);
- printk(KERN_INFO "clear globalmem!\n");
- break;
- default:
- return -EINVAL;
- }
- return 0;
- }
- static ssize_t globalmem_read(struct file *filp, char __user *buf,size_t size,
- loff_t *ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct globalmem_dev *dev = filp->private_data; //get global data pointer
- if(p>=GLOBALMEM_SIZE)
- return 0;
- if(count > GLOBALMEM_SIZE-p)
- count = GLOBALMEM_SIZE-p;
- if(down_interruptible(&dev->sem))
- return -ERESTARTSYS;
- if(copy_to_user(buf, (void *)(dev->mem + p), count))
- ret = -EFAULT;
- else {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "read %u bytes(s) from %lu\n",count,p);
- }
- up(&dev->sem);
- return ret;
- }
- static ssize_t globalmem_write(struct file *filp, const char __user *buf,
- size_t size, loff_t *ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct globalmem_dev *dev = filp->private_data; //get global data pointer
- if(p >= GLOBALMEM_SIZE)
- return 0;
- if(count > GLOBALMEM_SIZE - p)
- count = GLOBALMEM_SIZE - p;
- if(down_interruptible(&dev->sem))
- return -ERESTARTSYS;
- if(copy_from_user(dev->mem+p, buf, count)) {
- printk(KERN_INFO "copy from user error!!\n");
- ret = -EFAULT;
- }
- else {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "written %u bytes(s) from %lu\n",count,p);
- }
- up(&dev->sem);
- return ret;
- }
在读写之前使用down_interruptible检查是否可以获得信号量,若不能直接返回,在读写完成后使用up来释放信号量。
应用程序的测试与之前一样,自行测试。
信号量与互斥体就介绍到这,结束。