linux同步机制-信号量
一、信号量(semaphore)
1.1 什么是信号量
信号量本质上是一个计数器,它用来记录对某个资源的存取状态。一般来说,为了获取共享资源,进程需要执行下列操作:
- 测试控制该资源的信号量;
- 如果信号量的值为正,则允许操作该资源,并且信号量值减1;
- 如果信号量为0,则资源目前不可用,进程进入休眠状态,直至信号量值大于0,进程被唤醒,转入上一步;
- 当进程不在使用一个信号量控制的资源时,信号量的值加1;
1.2 信号量的工作原理
针对信号量只能进行两种操作,即PV操作,PV操作由P操作原语和V操作原子组成(原子是不可中断的过程),对信号量进行操作,具体定义如下:
- P(s):如果s的值大于零,就给它减1;如果它的值为零,则将该进程设置为等待状态,排入等待队列;
- V(s):如果有其它进程因等待s而被挂起,就让它恢复运行,如果没有进程因等待s而挂起,就给它加1;
1.3 信号量的使用
定义信号量:
struct semaphore sema;
DEFINE_SEAMPHORE宏用于定义一个信号量,并设置信号量的值为1:
DEFINE_SEAMPHORE(name)
sema_init 函数用于初始化信号量,并设置信号量sem的值为val:
void sema_init (struct semaphore *sem, int val);
down函数用于获得信号量sem,它会导致进程睡眠,不能在中断上下文中使用,不然会导致中断处理程序休眠(down函数目前已不建议使用):
int down(struct semaphore * sem);
down_interruptible函数功能与down类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠后,进程状态被设置为TASK_INTERRUPTIBLE,该类型的睡眠是可以被信号打断的。
如果返回0,表示获得信号量;如果被信号打断,返回EINTR。
int down_interruptible(struct semaphore * sem);
down_trylock函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回正数。它不会导致调用者睡眠,可以在中断上下文使用:
int down_trylock(struct semaphore * sem);
up函数释放信号量sem,唤醒等待者。
void up(struct semaphore * sem);
二、信号量的实现源码
2.1 struct semaphore结构体
维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件include/linux/semaphore.h:中看到内核用来维护信号量状态的各个结构的定义。信号量结构体定义:
/* Please don't access any members of this structure directly */ struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };
lock是一个自旋锁结构,其实现原理在后面的章节介绍。count用来保存可用资源的数量。
list_head是一个双向链表,使用该等待列表保存因获取不到信号量而进行睡眠的进程:
struct list_head{ struct list_head *next,*prev; }
再来看一下DEFINE_SEMAPHORE宏定义初始化信号量成员lock、count、wait_list:
#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) 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); }
2.2 down函数
再来看看一下down函数,定义在kernel/locking/semaphore.c:文件中,该函数是用来获取信号量的,也是一个原子操作:
/** * down - acquire the semaphore * @sem: the semaphore to be acquired * * Acquires the semaphore. If no more tasks are allowed to acquire the * semaphore, calling this function will put the task to sleep until the * semaphore is released. * * Use of this function is deprecated, please use down_interruptible() or * down_killable() instead. */ void down(struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(sem->count > 0)) sem->count--; else __down(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); }
这里实际上内部调用raw_spin_lock_irqsave、raw_spin_unlock_irqrestore这个两个函数来实现进程同步的,其实现原理是自旋锁。自旋锁可以保证在这两个函数中的代码可以独占的执行,具体实现原理这块内容我们会在自旋锁里面单独介绍。
如果获取不到信号量,__down函数调用 __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
/* * Because this function is inlined, the 'state' parameter will be * constant, and thus optimised away by the compiler. Likewise the * 'timeout' parameter for the cases without timeouts. */ static inline int __sched __down_common(struct semaphore *sem, long state, long timeout) { struct semaphore_waiter waiter; list_add_tail(&waiter.list, &sem->wait_list); waiter.task = current; waiter.up = false; for (;;) { if (signal_pending_state(state, current)) goto interrupted; if (unlikely(timeout <= 0)) goto timed_out; __set_current_state(state); raw_spin_unlock_irq(&sem->lock); timeout = schedule_timeout(timeout); // CPU调度、无限睡眠,导致进程切换,开销较大 raw_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; }
在__down_common函数中会做以下事情:
- 将当前进程加入信号量等待列表wait_list中;
- 将当前进程设置为睡眠状态,并且进程状态被设置为TASK_UNINTERRUPTIBLE;
- 执行CPU调度,当其它进程调用up函数释放信号量,才会唤醒第一个被等待的进程;
2.3 up函数
最后我们再来看一下up函数:
/** * up - release the semaphore * @sem: the semaphore to release * * Release the semaphore. Unlike mutexes, up() may be called from any * context and even by tasks which have never called down(). */ void up(struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(list_empty(&sem->wait_list))) sem->count++; else __up(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); }
这里同样使用了自旋锁,在临界区代码中, 判断是否有等待的进程,如果有的话,会执行__up函数:
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 = true; wake_up_process(waiter->task); }
这里首先获取第一个信号量等待者,然后将其从信号量等待列表lwait_head中移除,并设置 waiter->up = true,然后唤醒semaphore_waiter ->task指向的进程。
三、信号量示例程序
3.1 驱动程序
#include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/semaphore.h> #define OK (0) #define ERROR (-1) /* 信号量 */ static DEFINE_SEMAPHORE(sema); int hello_open(struct inode *p, struct file *f) { if(down_trylock(&sema) > 0){ printk("device busy,hello_open failed"); return ERROR; } printk("hello_open\n"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk("hello_write\n"); return 0; } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk("hello_read\n"); return 0; } int hello_close(struct inode *inode, struct file *file) { /* 释放信号量 */ up(&sema); return 0; } struct file_operations hello_fops = { .owner = THIS_MODULE, .open = hello_open, .read = hello_read, .write = hello_write, .release = hello_close, }; dev_t devid; // 起始设备编号 struct cdev hello_cdev; // 保存操作结构体的字符设备 struct class *hello_cls; int hello_init(void) // 也可以在这个函数中调用sema_init初始化信号量 { /* 动态分配字符设备: (major,0) */ if(OK == alloc_chrdev_region(&devid, 0, 1,"hello")){ // ls /proc/devices看到的名字 printk("register_chrdev_region ok\n"); }else { printk("register_chrdev_region error\n"); return ERROR; } cdev_init(&hello_cdev, &hello_fops); cdev_add(&hello_cdev, devid, 1); /* 创建类,它会在sys目录下创建/sys/class/hello这个类 */ hello_cls = class_create(THIS_MODULE, "hello"); if(IS_ERR(hello_cls)){ printk("can't create class\n"); return ERROR; } /* 在/sys/class/hello下创建hellos设备,然后mdev通过这个自动创建/dev/hello这个设备节点 */ device_create(hello_cls, NULL, devid, NULL, "hello"); return 0; } void __exit hello_exit(void) { printk("hello driver exit\n"); /* 注销类、以及类设备 /sys/class/hello会被移除*/ device_destroy(hello_cls, devid); class_destroy(hello_cls); cdev_del(&hello_cdev); unregister_chrdev_region(devid, 1); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
3.2 应用程序
#include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main(int argc,char **argv) { int fd; int val = 1; fd = open("/dev/hello",O_RDWR); if(fd == -1){ printf("can't open!\n"); }else{ printf("open success,PID=%d\n",getpid()); sleep(10); // 10s } return 0; }
3.3 测试
如下图所示,3个进程同时访问时,有一个进程访问成功,两外两个进程都访问失败了:
[root@zy:/]# ./main &hello_open open success,PID=61 [root@zy:/]# ./main & [root@zy:/]# can't open! [root@zy:/]# ./main & device busy,hello_open failed can't open! [root@zy:/]#
参考文章