Linux設備驅動併發控制

Linux設備驅動併發控制

本文共包含三部分:1.併發及競態的概念及發生的場合;2.併發控制的具體處理機制;3.相關的具體應用實例。

1. 併發及競態的概念

1.1 併發與競態的概念

併發:指多個執行單元同時、併發地被執行;

        競態:併發執行單元的運行,很容易會對共享資源(包含硬件、軟件的全局變量靜態變量等)的訪問造成競態。

1.2 競態發生的幾種情況

在Linux內核中,有如下幾種競態:

1)對稱多處理器的多個CPU;

2)單CPU內的進程與搶佔它的進程之間;

3)進程與中斷(硬中斷、軟中斷、tasklet、底半部)之間。

2.併發控制的具體機制

2.1 中斷屏蔽

在linux內核中避免競態的一種簡單有效的方法:在進入臨界區錢將中斷屏蔽。中斷屏蔽不僅避免了中斷與進程間的競態;而且在Linux內核中,由於進程調度、異步IO等都是通過中斷來完成的,因而這樣也可以有效的避免進程間的調度。
中斷屏蔽的方法:

1) 中斷屏蔽的普通方法
local_irq_disable();/*屏蔽中斷*/
critical section/*臨界區*/
local_irq_enable();/*打開中斷*/

2)中斷屏蔽(保存中斷位的信息)
local_irq_save(flags);  /*屏蔽中斷*/
critical section/*臨界區*/
local_irq_restor(flags); /*打開中斷*/

3)中斷屏蔽的方法--只操作底半部
local_bh_disable();/*屏蔽中斷*/
critical section/*臨界區*/
local_bh_enable();/*打開中斷*/

注意:中斷不可長時間屏蔽,應儘快處理完臨界區內容,儘快恢復中斷,否則可能會導致數據丟失乃至整個系統崩潰。

2.2  原子操作

原子操作指在代碼的執行過程中,不會被別的代碼路徑所中斷的操作。

原子操作有兩種:位和整型,和cpu架構有關係。

1. 整型原子操作
1)設置原子變量的值:
void atomic_set(atomic_t *v, int i);  /*設置原子變量的值爲i*/
atomic_t v = ATOMIC_INIT(0);/*定義原子變量v並初始化爲0*/

2)獲取原子變量的值
atomic_read(atomic_t *v);/*返回原子變量的值*/

3)原子變量加/減
void atomic_add(int i,  atomic_t *v); /*原子變量增加i*/
void atomic_sub(int i, atomic_t *v); /*原子變量減少i*/

4)原子變量自增/自減
void atomic_inc(atomic_t *v); /*原子變量自增1*/
void atomic_dec(tomic_t *v); /*原子變量自減1*/

5)操作並測試
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);

6) 操作並返回
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);

2.3 位作原子操作

1) 設置位
void set_bit(nr, void *addr);

2) 清除位
void clear_bit(nr, void *addr);

3)改變位
void change_bit(nr, void *addr);

4)測試位
test_bit(nr, void * addr);

5)測試並操作位
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);

2.3 自旋鎖

自旋鎖包含以下幾種:自旋鎖、讀寫自旋鎖、順序鎖、讀-拷貝-更新;

1. 自旋鎖

理解自選鎖的最簡單的方法是把自旋鎖看成是一個變量。該變量把一個臨界區標記爲“我當前在運行,請稍等一會”或標記爲“我當前不在運行,可以被使用”。
        自旋鎖的相關操作由如下4種:
        1)定義自旋鎖
spinlock_t lock;
2)初始化自旋鎖

spin_lock_init(lock);

3)獲得自旋鎖

spin_lock(lock);<span style="white-space:pre">	</span>/*如果能獲得自旋鎖將立即返回,否則將自旋在那裏。*/
spin_trylock(lock);<span style="white-space:pre">	</span>/*如果獲得自旋鎖將返回真,否則將返回假*/


        4)釋放自旋鎖

spin_unlock(lock);

自旋鎖主要是針對SMP或單CPU內內核可被將佔的情況,無法避免中斷和底半部的影響,因而在自旋鎖的基礎上有了衍生。

spin_lock_irq();
spin_lock_irqsave();
spin_lock_bh();

注意:

1)自旋鎖實際上爲忙等待,只使用於較短時間的佔用,當臨界區很大,或臨界區內有共享設備時,長時間的佔用可能或降低系統的性能;

2)自旋鎖可能會導致死鎖,例如對自旋鎖的遞歸調用,當一個CPU試圖調用一個已經佔用的自旋鎖時就會產生死鎖;

3)自旋鎖期間不能調用系統調用的函數,如copy_from_user()、copy_to_user()、kmalloc()、msleep()等。


2. 讀寫鎖

在自旋鎖的基礎上粒度更細,在資源共享方面,運行有1個寫的執行單元,可以有多個讀的共享單元。

讀寫所一般這樣被使用:

/*定義讀寫所*/
rwlock_t lock;
/*初始化讀寫鎖*/
rwlock_init(&lock);

/*讀時獲取鎖*/
read_lock(&lock);
.../*臨界資源*/
read_unlock(&lock);

/*寫時獲取鎖*/
write_lock_irqsave(&lock, flags);
.../*臨界資源*/
write_unlock_irqrestore(&lock, flags);

3. 順序鎖

順序鎖是對讀寫鎖的一種優化,在使用順序鎖時,讀執行單元不會被寫執行單元阻塞;即讀時可以寫,寫時也可以讀,但是寫與寫之間仍是互斥的;如果在讀期間發生寫,則爲了數據的準確性和完整性需要重新讀。

示例:
/*讀開始*/
unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags);

/*釋放鎖*/
read_seqretry_irqrestore(lock, iv, flags);

/*重讀*/
do
{
     seqnum = read_seqbegin(&seqlock_a);
} while(read_seqretry(&seqlock_a, seqnum) )

4. RCU(讀-拷貝-更新)


2.4 信號量 

1. 信號量的概念

信號量的是在保護臨界區一種較爲常見的方法,使用同自旋鎖;與自旋鎖不同的是,自旋鎖無法獲取時系統進入忙等待,而信號量是進入休眠狀態。

信號量的使用方法:

1)定義:

<span style="font-weight: normal;">struct semaphore sem;</span>

2)  信號量的初始化

<span style="font-weight: normal;">/*信號量初始化*/
sema_init(struct semaphore *sem, int val);	/*將信號量初始化爲val*/

/*一般的使用方法被定義爲宏*/
#define init_MUTEX(sem)	sema_init(sem, 1)	/*初始化信號量爲1*/
#define init_MUTEX_LOCKED(sem) sem_init(sem, 0)	/*初始化信號量爲0*/</span>
3) 信號量的獲取

<span style="font-weight: normal;">/*用於獲取信號量sem, 會導致睡眠,因此不能在中斷上下文中使用,不能被信號打斷*/
void down(struct semaphore *sem);

/*會被信號打斷;有信號打斷則返回非0*/
int down_interruptible(struct semaphore *sem);

if(down_interruptible(&sem) )
	return - ERESTARTSYS;

/*如果可以獲取信號量則立即獲取並返回0,如果不可以獲取則立即返回非0值。不會導致調用者睡眠,可被用在中斷上下文*/
int down_trylock(struct semaphore *sem);</span>
4) 信號量的釋放
<span style="font-weight: normal;">void up(struct semaphore *sem);</span>

一般使用方法:

<span style="font-weight: normal;">DECLARE_MUTEX(mount_sem);
down(&mount_sem);	/*獲取信號量,包含臨界區*/
...
critical section	/*臨界區*/
...
up(&mount_sem);	/*釋放信號量*/</span>


2. 信號量用於同步

信號量的同步意味着代碼的順序執行,使用方法上可以將信號量初始化爲0;如下圖,執行單元A在執行代碼b前,需在執行單元b執行完代碼c之後。


3. 完成量用於同步

代碼的同步也可以用完成量來實現,功能需求同2中信號量的同步,實現過程如下圖所示。



4. 信號量VS自旋鎖

信號量和自旋鎖都是較爲常用的互斥手段,只是適用的層次不同,信號量的實現依賴於自旋鎖,爲了保證信號量結構存取的原子性,在多CPU中需要用自旋鎖來互斥;而信號量適用於進程間的競爭,因而這樣進程上下文的切換開銷很大。因此,當佔用資源時間較長時適用信號量,較短時適用自旋鎖。選擇哪個要看臨界區的性質和系統特點。
1)使用信號量的開銷是進程上下文切換時間Tsw,使用自旋鎖的開銷是等待自旋鎖Tcs,如果Tcs較小則使用自旋鎖,否則使用信號量;
2)當臨界區可能存在阻塞時,因爲阻塞會引起進程切換,如果使用自旋鎖會導致死鎖,因此這是使用信號量;
3)如果共享資源是在中斷或軟中斷下使用的,則使用自旋鎖。

5. 讀寫信號量
    讀寫信號量與信號量的關係類似於讀寫自旋鎖與自旋鎖的關係;讀寫信號量可能引起進程阻塞,它可允許N個讀執行單元同時訪問共享資源,而最多只能有1個寫執行單元。因此,讀寫信號量比信號量的粒度稍粗。
使用方法:
struct rw_semaphore my_rws;	/*定義讀寫信號量*/
init_rwsem(struct rw_semaphore *sem);	/*初始化讀寫信號量*/

/*讀時獲取信號量*/
down_read(&rw_sem);
...
up_read(&rw_sem);

/*寫時獲取信號量*/
down_write(&rw_sem);
...
up_write(&rw_sem);

2.5 互斥體

使用方法:
struct mutex my_mutex;		/*定義互斥體*/
mutex_init(&my_mutex);		/*初始化互斥體*/


mutex_lock(&my_mutex);		/*獲取互斥體*/
...
mutex_unlock(&my_mutex);	/*釋放互斥體*/

3. 應用實例

1) 在globalmem中因爲大量涉及到cup_from_user()、cpu_to_user()容易導致阻塞的函數,因此使用信號量而不能是用自旋鎖;
2)一般驅動工程師習慣將信號也放在設備結構體中。
3)實例源碼如下
/************************************************************************
*   Filename:          globalmem.c
*   File Description:  A virtual mem char driver
*   Author:            Vinvian Cheng
*   Emaile:
*   Ver:               1.0.0
*   Date:               2014.11.11
*   History:
*       1.
************************************************************************/
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>


/*struct mem*/
#define GLOBALMEM_SIZE<span style="white-space:pre">	</span>0x1000<span style="white-space:pre">	</span>/*全局內存最大4K字節*/


#define MEM_CLEAR 0x1  /*清0全局內存*/
#define GLOBALMEM_MAJOR 254    /*預設的globalmem的主設備號*/


static globalmem_major = GLOBALMEM_MAJOR;


/*globalmem struct*/
struct globalmem_dev{
    struct cdev cdev;   /*cdev struct*/
    unsigned char mem[GLOBALMEM_SIZE];  /*全局內存,私有數據*/
    struct semaphore sem;           /*信號量*/
};


/*dev struct piont*/
struct globalmem_dev *globalmem_devp = NULL;


/*文件打開函數*/
int globalmem_open(struct inode *inode, struct file *filp)
{
  /*將設備結構體指針賦值給文件私有數據指針*/
  filp->private_data = globalmem_devp;
  return 0;
}
/*文件釋放函數*/
int globalmem_release(struct inode *inode, struct file *filp)
{
  return 0;
}
/* ioctl設備控制函數 */
static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned
  int cmd, unsigned long arg)
{
  struct globalmem_dev *dev = filp->private_data;/*獲得設備結構體指針*/


  switch (cmd)
  {
    case MEM_CLEAR:
    {
        if (down_interruptible(&dev->sem))
        {
            return  - ERESTARTSYS;
        }
        memset(dev->mem, 0, GLOBALMEM_SIZE);
        printk(KERN_INFO "globalmem is set to zero\n");
        up(&dev->sem);
        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; /*獲得設備結構體指針*/


  /*分析和獲取有效的寫長度*/
  if (p >= GLOBALMEM_SIZE)
    return count ?  - ENXIO: 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 %d bytes(s) from %ld\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; /*獲得設備結構體指針*/


  /*分析和獲取有效的寫長度*/
  if (p >= GLOBALMEM_SIZE)
    return count ?  - ENXIO: 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) )
    ret =  - EFAULT;
  else
  {
    *ppos += count;
    ret = count;


    printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
  }


  up(&dev->sem);


  return ret;
}
/* seek文件定位函數 */
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
  loff_t ret = 0;
  switch (orig)
  {
    case 0:   /*相對文件開始位置偏移*/
      if (offset < 0)
      {
        ret =  - EINVAL;
        break;
      }
      if ((unsigned int)offset > GLOBALMEM_SIZE)
      {
        ret =  - EINVAL;
        break;
      }
      filp->f_pos = (unsigned int)offset;
      ret = filp->f_pos;
      break;
    case 1:   /*相對文件當前位置偏移*/
      if ((filp->f_pos + offset) > GLOBALMEM_SIZE)
      {
        ret =  - EINVAL;
        break;
      }
      if ((filp->f_pos + offset) < 0)
      {
        ret =  - EINVAL;
        break;
      }
      filp->f_pos += offset;
      ret = filp->f_pos;
      break;
    default:
      ret =  - EINVAL;
      break;
  }


  return ret;
}


/*文件操作結構體*/
static const struct file_operations globalmem_fops =
{
    .owner    = THIS_MODULE,
    .llseek   = globalmem_llseek,
    .read     = globalmem_read,
    .write    = globalmem_write,
    .ioctl    = globalmem_ioctl,
    .open     = globalmem_open,
    .release  = globalmem_release,
};


/***********************************************************************
*   Function Name:  globalmem_setup_cdev
*   Paramter:
*           struct globalmem_dev *dev [INOUT];
*           int index   [IN];
*   Function Descrition:
*                   cdev init and register.
*   Return: void
*   Author: Vinvian 2014/11/11
***********************************************************************/
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
  int err = 0;
  int devno = MKDEV(globalmem_major, index);


  cdev_init(&dev->cdev, &globalmem_fops);       //dev init
  dev->cdev.owner = THIS_MODULE;
  dev->cdev.ops = &globalmem_fops;


  err = cdev_add(&dev->cdev, devno, 1);
  if (err)
    printk(KERN_NOTICE "Error %d adding LED%d", err, index);
}
/***********************************************************************
*   Function Name:  globalmem_init
*   Paramter:       type name [IN]: void
*   Function Descrition:
*                   Module init
*   Return: 0
*   Error:
*   Author: Vinvian 2014/11/11
***********************************************************************/
static int globalmem_init(void)
{
   printk(KERN_INFO " Globlamem Driver for init!\n");


    int result;


    dev_t devno = MKDEV(globalmem_major, 0); //Create det_t with major and minor devNo


    /*Regsiter devNo*/
    if (globalmem_major)    //has major no
        result = register_chrdev_region(devno, 1, "globalmem");
    else                    //no major no
    {
        result = alloc_chrdev_region(&devno, 0, 1, "globalmem");
        globalmem_major = MAJOR(devno);
    }
    if (result < 0)
        return result;


    /*malloc dev struct mem*/
    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));


    /*init cdev*/
    globalmem_setup_cdev(globalmem_devp, 0);


    /*init sema*/
    sema_init(&globalmem_devp->sem, 1);
    return 0;


    fail_malloc: unregister_chrdev_region(devno, 1);


    return result;


}
/***********************************************************************
*   Function Name:  globalmem_exit
*   Paramter:       type name [IN]: void
*   Function Descrition:
*                   Module exit
*   Return: void
*   Error:
*   Author: Vinvian 2014/11/11
***********************************************************************/
void globalmem_exit(void)
{
    /*release cdev*/
    cdev_del(&globalmem_devp->cdev);


    /*release struct dev mem*/
    kfree(globalmem_devp);


    /*release devNo*/
    unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}


module_init(globalmem_init);
module_exit(globalmem_exit);


module_param(globalmem_major, int, S_IRUGO);


MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Vinvian");
MODULE_DESCRIPTION("A globalmem driver Module");
MODULE_ALIAS("A globalmem driver");



4. 總結

1)自旋鎖和信號量最爲常用;自旋鎖適用於臨界資源佔用時間較短,且不存在阻塞;信號量適用於臨界資源佔用時間較長,可能存在阻塞的情況下;
2)原子操作只適用於整數;
3)中斷屏蔽適用較少。

5. 參考文獻

1) Linux設備開發驅動詳解 宋寶華著
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章