linux設備驅動中的併發控制

併發指的是多個執行單元同時、並行被執行,而併發的執行單元對共享資源(硬件資源和軟件上的全局變量,靜態變量等)的訪問很容易導致競態。在linux內核中,競態發生主要有以下幾種情況:

  1. 對稱多處理器(SMP)的多個CPU之間
  2. 單CPU內核進程與搶佔他的進程
  3. 中斷(硬中斷、軟終端、Tasklet、底半部)與進程之間

解決競態問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問是指一個執行單元在訪問共享資源的時候,其他的執行單元被禁止訪問。訪問共享資源的代碼區域稱爲臨界區,臨界區需要被某種互斥機構加以保護。中斷屏蔽、原子操作、自旋鎖、信號量、互斥體等是linux設備驅動中可採用的互斥途徑。

針對這個問題,做一個不正確,但可以形象說明這個問題例子:

現在對之前寫的簡單字符設備驅動的代碼做一個簡單測試,在一個打開的字符設備驅動中,一個線程一直不停的往驅動中寫aaaa,另一個線程不停的讀取設備中的內容,按照所寫的驅動代碼可知,讀取線程會一直讀到寫線程所寫的內容;但是如果在開啓一個寫線程一直不停的寫bbbb,那麼讀線程讀到的內容還會一直是aaaa嗎?

測試代碼如下:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <string.h>

void func_pth1(void *arg)
{
    int *fd = arg;
    while(1)
        write(*fd,"aaaa",sizeof("aaaa"));
}

void func_pth2(void *arg)
{
    int *fd = arg;
    while(1)
        write(*fd,"bbbb",sizeof("bbbb"));
}

void func_pth3(void *arg)
{
    int *fd = arg;
    char buf[4];
    while(1){
        read(*fd,buf,4);
        for(int i=0;i<4;i++)
            printf("%c",buf[i]);
        printf("\n");
    }
}

int main()
{
    pthread_t pth1,pth2,pth3;
    int ret_pth1,ret_pth2,ret_pth3;

    int fd = open("/dev/mymodules",O_RDWR);
    if(fd <0)
        printf("fd open error\n");

    ret_pth1 = pthread_create(&pth1,NULL,(void *)func_pth1,(void *)&fd);
    ret_pth2 = pthread_create(&pth2,NULL,(void *)func_pth2,(void *)&fd);
    ret_pth3 = pthread_create(&pth3,NULL,(void *)func_pth3,(void *)&fd);
    
    if(ret_pth1 != 0)
        printf("pth1 create error\n");
    if(ret_pth2 != 0)
        printf("pth2 create error\n");
    if(ret_pth3 != 0)
        printf("pth3 create error\n");

    int tmp1 = pthread_join(pth1,NULL);
    int tmp2 = pthread_join(pth2,NULL);
    int tmp3 = pthread_join(pth3,NULL);

    return 0;
}

交叉編譯,移植到虛擬開發版中運行:

打印中若出現aabb的存在,while(1)的去寫,到底是哪個線程寫入了設備驅動了,這時我們想到的是內核的調度機制,時間片輪轉,內核最小執行單元(線程)的執行狀態,線程的執行機制等等。但是,如果把寫入aaaa的線程換成是往銀行卡存錢,寫入bbbb的線程換成從銀行卡取錢,讀線程爲櫃檯顯示卡餘額,那最後顯示的餘額怎麼算了?

當然,都不希望這種現象的發生,linux內核也不希望哈,於是就有了防止這種單CPU進程搶佔等競態的方法。這些方法中,中斷屏蔽很少單獨使用,原子操作只能針對整型數進行,信號量已基本不用,因此自旋鎖和互斥體應用最爲廣泛。自旋鎖或導致死循環(死鎖),鎖定期間不允許阻塞,因此要求鎖定的臨界區小。互斥體允許臨界區阻塞,可以使用臨界區大的情況。這裏給出自旋鎖和互斥體選用的3項原則:

  1. 當鎖不能被獲取到時,使用互斥體的開銷是進程上下文切換的時間,使用自旋鎖的開銷是等待獲取自旋鎖(由臨界區執行時間決定)。若臨界區較大,宜使用互斥體,反之使用自旋鎖。
  2. 互斥體所保護的臨界區克包含可能引起阻塞的代碼,而自旋鎖則絕對要避免用來保護這樣代碼的臨界區。應爲阻塞意味着進程的切換,如果進程切換出去後,另一個進程企圖過去本自旋鎖,死鎖就會發生。
  3. 互斥體存在於進程上下文,因此,如果被保護的資源要再中斷或軟中斷情況下使用,只能使用自旋鎖。當然使用互斥體代碼mutex_trylock()方式,不能獲取就立即返回以避免阻塞。

互斥體的使用

//定義並初始化互斥體my_mutex
struct mutex my_nutex;
mutex_init(&my_mutex);

//獲取互斥體
void mutex_lock(&my_mutex); //引起的睡眠不能被中斷打斷
int mutex_lock_interruptible(&my_mutex);//睡眠可以被中斷打斷
int mutex_trylock(&my_mutex);//嘗試獲取mutex,獲取不到不會引起進程睡眠

//釋放互斥體
void mutex_unlock(&my_mutex);

自旋鎖的使用

//定義與初始化自旋鎖
spinlock_t lock;
spin_lock_init(lock);

//獲取自旋鎖
spin_lock(lock);

//釋放自旋鎖
spin_unlock(lock);

自旋鎖還有一些衍生的鎖,有興趣可以多瞭解一些,在之前寫的簡單字符設備驅動代碼中使用了copy_to_user()等會導致阻塞的函數,因此使用互斥體來防止競態,修改代碼如下:

//添加頭文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/device.h>
 
static int major=0,minor=0;
 
//定義cdev結構體
struct mymodule_dev{
    struct cdev cdev;
    unsigned char buf[512];
    struct mutex mutex;
};
 
struct mymodule_dev *mydev;
struct class *my_class;
struct device *my_device;
 
static int mymodule_open(struct inode *inode,struct file *filp)
{
    filp->private_data = mydev;
    return 0;
}
 
static int mymodule_release(struct inode *inode,struct file *filp)
{
    return 0;
}
 
static ssize_t mymodule_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 mymodule_dev *dev = filp->private_data;
    nutex_lock(&dev->mutex);
    if(copy_to_user(buf,dev->buf,count))
        return -EFAULT;
    else
    {
        *ppos += count;
        ret = count;
        printk(KERN_INFO"read %d bytes from %ld\n",count,p);
    }
    mutex_unlock(&dev->mutex);
    return ret;
}
 
static ssize_t mymodule_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 mymodule_dev *dev = filp->private_data;
    mutex_lock(&dev->mutex);
    if(copy_from_user(dev->buf,buf,count))
        return -EFAULT;
    else
    {
        *ppos += count;
        ret = count;
        printk(KERN_INFO"write %d bytes from %ld\n",count,p);
    }
    mutex_unlock(&dev->mutex);
    return ret;
}
 
//file_operation設備驅動文件操作結構體
static struct file_operations mymodule_fops = {
    .owner = THIS_MODULE,
    .open = mymodule_open,
    .release = mymodule_release,
    .read = mymodule_read,
    .write = mymodule_write,
};
 
//初始化並添加cdev結構體
static void mymodule_cdev_setup(struct mymodule_dev *dev)
{
    int err,devno=MKDEV(major,minor);
    //初始化
    cdev_init(&dev->cdev,&mymodule_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &mymodule_fops;
    //註冊,添加
    err = cdev_add(&dev->cdev,devno,1);
    if(err)
        printk(KERN_NOTICE"error %d adding mymodule",err);
}
 
//模塊加載
int __init mymodule_init(void)
{
    //申請設備號
    int result;
    dev_t devno = MKDEV(major,minor);
    if(major)
        result = register_chrdev_region(devno,1,"mymodule");
    else{
        result = alloc_chrdev_region(&devno,minor,1,"mymodule");
        major = MAJOR(devno);
        minor = MINOR(devno);
    }
    if(result<0)
        return result;
    //動態申請設備結構體內存
    mydev = kmalloc(sizeof(struct mymodule_dev),GFP_KERNEL);
    if(!mydev){
        result=-ENOMEM;
        goto fail_malloc;
    }
    memset(mydev,0,sizeof(struct mymodule_dev));
    //初始化互斥體
    mutex_init(&mydev->mutex); 
   //cdev字符設備的初始化和添加
    mymodule_cdev_setup(mydev);
 
    //註冊設備節點
    my_class = class_create(THIS_MODULE,"mymodule_t");
    my_device = device_create(my_class,NULL,MKDEV(major,minor),NULL,"mymodules");
 
    return 0;
fail_malloc:unregister_chrdev_region(devno,1);
return result;  
}
//模塊卸載
void __exit mymodule_exit(void)
{
    device_destroy(my_class,MKDEV(major,minor));
    class_destroy(my_class);
    //刪除cdev結構體
    cdev_del(&mydev->cdev);
    kfree(mydev);
    //註銷設備號
    unregister_chrdev_region(MKDEV(major,minor),1);
}
 
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_AUTHOR("HaoRan");
MODULE_LICENSE("GPL");

重新編譯驅動並加載,在測試上述測試代碼看結果,並想爲什麼?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章