信號量
信號量的使用
信號量(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
- if(down_interruptible(&sem))
- return -ERESTARTSYS;
4.釋放信號
void up(struct semaphore *sem);
該函數釋放信號量sem,喚醒等待者。
用法
- DECLARE_MUTEX(mount_sem);
- down(&mount_sem); //get semaphore
- ...
- critical section
- ...
- up(&mount_sem); //release semaphore
舉例使用信號量實現設備只能被一個進程打開
- 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之後,完成量纔會被釋放,這樣纔會執行讀的動作。
- struct completion com;
- 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;
- }
- int globalmem_init(void)
- {
- int result;
- // spin_lock_init(&lock);
- init_completion(&com);
重新編譯模塊,在加載我們的globalmem模塊,然後進行測試
我們先cat
- jay@jay:/dev$
- jay@jay:/dev$ cat globalmem
發現光標停在那,不動了,因爲在等待完成量,然後我們另開一個終端,去echo
- jay@jay:/dev$ echo "456" > globalmem
當我們往裏面寫數據之後發現之前的終端有反應了
- 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的使用方法和信號量用於互斥的場合完全一樣
- struct mutex my_mutex; //declare mutex
- mutex_init(&mu_mutex); //init mutex
- mutex_lock(&my_mutex);
- ...... //臨界資源
- mutex_unlock(&my_mutex);
修改我們的globalmem字符驅動增加併發控制
定義初始化信號量
- struct globalmem_dev {
- struct miscdevice mdev;
- unsigned char mem[GLOBALMEM_SIZE];
- struct semaphore sem;
- };
- 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 函數中添加信號量
- 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來釋放信號量。
應用程序的測試與之前一樣,自行測試。
信號量與互斥體就介紹到這,結束。