<acronym id="xonnx"></acronym>
      <td id="xonnx"></td>
    1. <pre id="xonnx"></pre>

      1. 專注電子技術學習與研究
        當前位置:單片機教程網 >> MCU設計實例 >> 瀏覽文章

        Linux驅動總結3- unlocked_ioctl和堵塞(waitqueue)讀寫函數的實現

        作者:公平   來源:本站原創   點擊數:  更新時間:2014年03月14日   【字體:

        學習了驅動程序的設計,感覺在學習驅動的同時學習linux內核,也是很不錯的過程哦,做了幾個實驗,該做一些總結,只有不停的作總結才能印象深刻。
        我的平臺是虛擬機,fedora14,內核版本為2.6.38.1.其中較之前的版本存在較大的差別,具體的實現已經在上一次總結中給出了。今天主要總結的是ioctl和堵塞讀寫函數的實現。

        一、ioctl函數的實現
        首先說明在2.6.36以后ioctl函數已經不再存在了,而是用unlocked_ioctl和compat_ioctl兩個函數實現以前版本的ioctl函數。同時在參數方面也發生了一定程度的改變,去除了原來ioctl中的struct inode參數,同時改變了返回值。
        但是驅動設計過程中存在的問題變化并不是很大,同樣在應用程序設計中我們還是采用ioctl實現訪問,而并不是unlocked_ioctl函數,因此我們還可以稱之為ioctl函數的實現。
        ioctl函數的實現主要是用來實現具體的硬件控制,采用相應的命令控制硬件的具體操作,這樣就能使得硬件的操作不再是單調的讀寫操作。使得硬件的使用更加的方便。
        ioctl函數實現主要包括兩個部分,首先是命令的定義,然后才是ioctl函數的實現,命令的定義是采用一定的規則。
        ioctl的命令主要用于應用程序通過該命令操作具體的硬件設備,實現具體的操作,在驅動中主要是對命令進行解析,通過switch-case語句實現不同命令的控制,進而實現不同的硬件操作。

        ioctl函數的命令定義方法:
        int (*unlocked_ioctl)(struct file*filp,unsigned int cmd,unsigned long arg)
        雖然其中沒有指針的參數,但是通常采用arg傳遞指針參數。cmd是一個命令。每一個命令由一個整形數據構成(32bits),將一個命令分成四部分,每一部分實現具體的配置,設備類型(幻數)8bits,方向2bits,序號8bits,數據大小13/14bits。命令的實現實質上就是通過簡單的移位操作,將各個部分組合起來而已。
        一個命令的分布的大概情況如下:

        |---方向位(31-30)|----數據長度(29-16)----------------|---------設備類型(15-8)------|----------序號(7-0)----------|
        |----------------------------------------------------------------------------------------------------------------------------------------|

        其中方向位主要是表示對設備的操作,比如讀設備,寫設備等操作以及讀寫設備等都具有一定的方向,2個bits只有4種方向。
        數據長度表示每一次操作(讀、寫)數據的大小,一般而已每一個命令對應的數據大小都是一個固定的值,不會經常改變,14bits說明可以選擇的數據長度最大為16k。
        設備類型類似于主設備號(由于8bits,剛好組成一個字節,因此經常采用字符作為幻數,表示某一類設備的命令),用來區別不同的命令類型,也就是特定的設備類型對應特定的設備。序號主要是這一類命令中的具體某一個,類似于次設備號(256個命令),也就是一個設備支持的命令多達256個。

        同時在內核中也存在具體的宏用來定義命令以及解析命令。
        但是大部分的宏都只是定義具體的方向,其他的都需要設計者定義。
        主要的宏如下:
        #include<linux/ioctl.h>

        _IO(type,nr)                    表示定義一個沒有方向的命令,
        _IOR(type,nr,size)            表示定義一個類型為type,序號為nr,數據大小為size的讀命令
        _IOW(type,nr,size)           表示定義一個類型為type,序號為nr,數據大小為size的寫命令
        _IOWR(type,nr,size)         表示定義一個類型為type,序號為nr,數據大小為size的寫讀命令

        通常的type可采用某一個字母或者數字作為設備命令類型。
        是實際運用中通常采用如下的方法定義一個具體的命令:

            //頭文件
            #include<linux/ioctl.h>

            /*定義一系列的命令*/
            /*幻數,主要用于表示類型*/
            #define MAGIC_NUM 'k'
            /*打印命令*/
            #define MEMDEV_PRINTF _IO(MAGIC_NUM,1)
            /*從設備讀一個int數據*/
            #define MEMDEV_READ _IOR(MAGIC_NUM,2,int)
            /*往設備寫一個int數據*/
            #define MEMDEV_WRITE _IOW(MAGIC_NUM,3,int)

            /*最大的序列號*/
            #define MEM_MAX_CMD 3

        還有對命令進行解析的宏,用來確定具體命令的四個部分(方向,大小,類型,序號)具體如下所示:

            /*確定命令的方向*/
            _IOC_DIR(nr)                   
            /*確定命令的類型*/
            _IOC_TYPE(nr)                    
            /*確定命令的序號*/
            _IOC_NR(nr)                          
            /*確定命令的大小*/
            _IOC_SIZE(nr)   

        上面的幾個宏可以用來命令,實現命令正確性的檢查。

        ioctl的實現過程主要包括如下的過程:
        1、命令的檢測
        2、指針參數的檢測
        3、命令的控制switch-case語句

        1、命令的檢測主要包括類型的檢查,數據大小,序號的檢測,通過結合上面的命令解析宏可以快速的確定。

                    /*檢查類型,幻數是否正確*/
                    if(_IOC_TYPE(cmd)!=MAGIC_NUM)
                            return -EINVAL;
                    /*檢測命令序號是否大于允許的最大序號*/
                    if(_IOC_NR(cmd)> MEM_MAX_CMD)
                            return -EINVAL;

        2、主要是指針參數的檢測。指針參數主要是因為內核空間和用戶空間的差異性導致的,因此需要來自用戶空間指針的有效性。使用copy_from_user,copy_to_user,get_user,put_user之類的函數時,由于函數會實現指針參量的檢測,因此可以省略,但是采用__get_user(),__put_user()之類的函數時一定要進行檢測。具體的檢測方法如下所示:

            if(_IOC_DIR(cmd) & _IOC_READ)
                    err = !access_ok(VERIFY_WRITE,(void *)args,_IOC_SIZE(cmd));
            else if(_IOC_DIR(cmd) & _IOC_WRITE)
                    err = !access_ok(VERIFY_READ,(void *)args,_IOC_SIZE(cmd));
            if(err)/*返回錯誤*/
                    return -EFAULT;

        當方向是讀時,說明是從設備讀數據到用戶空間,因此要檢測用戶空間的指針是否可寫,采用VERIFY_WRITE,而當方向是寫時,說明是往設備中寫數據,因此需要檢測用戶空間中的指針的可讀性VERIFY_READ。檢查通常采用access_ok()實現檢測,第一個參數為讀寫,第二個為檢測的指針,第三個為數據的大小。
        3、命名的控制:
        命令的控制主要是采用switch和case相結合實現的,這于window編程中的檢測各種消息的實現方式是相同的。

            /*根據命令執行相應的操作*/
                    switch(cmd)
                    {
                            case MEMDEV_PRINTF:
                                    printk("<--------CMD MEMDEV_PRINTF Done------------>\n\n");
                                    ...
                                    break;
                            case MEMDEV_READ:
                                    ioarg = &mem_devp->data;
                                    ...
                                    ret = __put_user(ioarg,(int *)args);
                                    ioarg = 0;
                                    ...
                                    break;
                            case MEMDEV_WRITE:
                                    ...
                                    ret = __get_user(ioarg,(int *)args);
                                    printk("<--------CMD MEMDEV_WRITE Done ioarg = %d--------->\n\n",ioarg);
                                    ioarg = 0;
                                    ...
                                    break;
                            default:
                                    ret = -EINVAL;
                                    printk("<-------INVAL CMD--------->\n\n");
                                    break;
                    }

        這只是基本的框架結構,實際中根據具體的情況進行修改。這樣就實現了基本的命令控制。
        文件操作支持的集合如下:

            /*添加該模塊的基本文件操作支持*/
            static const struct file_operations mem_fops =
            {
                    /*結尾不是分號,注意其中的差別*/
                    .owner = THIS_MODULE,
                    .llseek = mem_llseek,
                    .read = mem_read,
                    .write = mem_write,
                    .open = mem_open,
                    .release = mem_release,
                    /*添加新的操作支持*/
                    .unlocked_ioctl = mem_ioctl,
            };

        需要注意不是ioctl,而是unlocked_ioctl。


        二、設備的堵塞讀寫方式實現,通常采用等待隊列。
        設備的堵塞讀寫方式,默認情況下的讀寫操作都是堵塞型的,具體的就是如果需要讀數據,當設備中沒有數據可讀的時候應該等待設備中有設備再讀,當往設備中寫數據時,如果上一次的數據還沒有被讀完成,則不應該寫入數據,就會導致進程的堵塞,等待數據可讀寫。但是在應用程序中也可以采用非堵塞型的方式進行讀寫。只要在打開文件的時候添加一個O_NONBLOCK,這樣在不能讀寫的時候就會直接返回,而不會等待。
        因此我們在實際設計驅動設備的同時需要考慮讀寫操作的堵塞方式。堵塞方式的設計主要是通過等待隊列實現,通常是將等待隊列(實質就是一個鏈表)的頭作為設備數據結構的一部分。在設備初始化過程中初始化等待隊列的頭。最后在設備讀寫操作的實現添加相應的等待隊列節點,并進行相應的控制。

        等待隊列的操作基本如下:
        1、等待隊列的頭定義并初始化的過程如下:
        方法一:
        struct wait_queue_head_t mywaitqueue;
        init_waitqueue_head(&mywaitqueue);
        方法二:
        DECLARE_WAIT_QUEUE_HEAD(mywaitqueue);
        以上的兩種都能實現定義和初始化等待隊列頭。

        2、創建、移除一個等待隊列的節點,并添加、移除相應的隊列。
        定義一個等待隊列的節點:DECLARE_WAITQUEUE(wait,tsk)
        其中tsk表示一個進程,可以采用current當前的進程。
        添加到定義好的等待隊列頭中。
        add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);
        即:add_wait_queue(&mywaitqueue,&wait);

        移除等待節點
        remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);
        即:remove_wait_queue(&mywaitqueue,&wait);

        3、等待事件
        wait_event(queue,condition);當condition為真時,等待隊列頭queue對應的隊列被喚醒,否則繼續堵塞。這種情況下不能被信號打斷。
        wait_event_interruptible(queue,condition);當condition為真時,等待隊列頭queue對應的隊列被喚醒,否則繼續堵塞。這種情況下能被信號打斷。
        4、喚醒等待隊列
        wait_up(wait_queue_head_t *q),喚醒該等待隊列頭對應的所有等待。
        wait_up_interruptible(wait_queue_head_t *q)喚醒處于TASK_INTERRUPTIBLE的等待進程。
        應該成對的使用。即wait_event于wait_up,而wait_event_interruptible與wait_up_interruptible。


            wait_event和wait_event_interruptible的實現都是采用宏的方式,都是一個重新調度的過程,如下所示:

            #define wait_event_interruptible(wq, condition)                \
            ({                                    \
                int __ret = 0;                            \
                if (!(condition))                        \
                    __wait_event_interruptible(wq, condition, __ret);    \
                __ret;                                \
            })

            #define __wait_event_interruptible(wq, condition, ret)            \
            do {                                    \
                 /*此處存在一個聲明等待隊列的語句,因此不需要再重新定義一個等待隊列節點*/
                DEFINE_WAIT(__wait);                        \
                                                \
                for (;;) {                            \
                    /*此處就相當于add_wait_queue()操作,具體參看代碼如下所示*/
                    prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);    \
                    if (condition)                        \
                        break;                        \
                    if (!signal_pending(current)) {                \
                        /*此處是調度,丟失CPU,因此需要wake_up函數喚醒當前的進程
            根據定義可知,如果條件不滿足,進程就失去CPU,能夠跳出for循環的出口只有
                            1、當條件滿足時2、當signal_pending(current)=1時。
                            1、就是滿足條件,也就是說wake_up函數只是退出了schedule函數,
                            而真正退出函數還需要滿足條件 
                            2、說明進程可以被信號喚醒。也就是信號可能導致沒有滿足條件時就喚醒當前的進程。
                           這也是后面的代碼采用while判斷的原因.防止被信號喚醒。  
               */
                        schedule();                    \
                        continue;                    \
                    }                            \
                    ret = -ERESTARTSYS;                    \
                    break;                            \
                }                                \
                finish_wait(&wq, &__wait);                    \
            } while (0)

        #define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
        #define DEFINE_WAIT_FUNC(name, function) \
        wait_queue_t name = { \
        .private = current, \
        .func = function, \
        .task_list = LIST_HEAD_INIT((name).task_list), \
        }

            void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
            {
            unsigned long flags;

            wait->flags &= ~WQ_FLAG_EXCLUSIVE;
            spin_lock_irqsave(&q->lock, flags);
            if (list_empty(&wait->task_list))
                           /*添加節點到等待隊列*/
            __add_wait_queue(q, wait);
            set_current_state(state);
            spin_unlock_irqrestore(&q->lock, flags);
            }
            喚醒的操作也是類似的。
            #define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

         
          void __wake_up(wait_queue_head_t *q, unsigned int mode,
        int nr_exclusive, void *key)
        {
        unsigned long flags;

        spin_lock_irqsave(&q->lock, flags);
        __wake_up_common(q, mode, nr_exclusive, 0, key);
        spin_unlock_irqrestore(&q->lock, flags);
        }

        static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
        int nr_exclusive, int wake_flags, void *key)
        {
        wait_queue_t *curr, *next;

        list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
        unsigned flags = curr->flags;

        if (curr->func(curr, mode, wake_flags, key) &&
        (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
        break;
        }
        }
        等待隊列通常用在驅動程序設計中的堵塞讀寫操作,并不需要手動的添加節點到隊列中,直接調用即可實現,具體的實現方法如下:
        1、在設備結構體中添加等待隊列頭,由于讀寫都需要堵塞,所以添加兩個隊列頭,分別用來堵塞寫操作,寫操作。

            #include<linux/wait.h>

            struct mem_dev
            {
                    char *data;
                    unsigned long size;
                    /*添加一個并行機制*/
                    spinlock_t lock;

                    /*添加一個等待隊列t頭*/
                    wait_queue_head_t rdqueue;
                    wait_queue_head_t wrqueue;
            };

        2、然后在模塊初始化中初始化隊列頭:

            /*初始化函數*/
            static int memdev_init(void)
            {
                   ....
                    for(i = 0; i < MEMDEV_NR_DEVS; i)
                    {
                            mem_devp[i].size = MEMDEV_SIZE;
                            /*對設備的數據空間分配空間*/
                            mem_devp[i].data = kmalloc(MEMDEV_SIZE,GFP_KERNEL);
                            /*問題,沒有進行錯誤的控制*/
                            memset(mem_devp[i].data,0,MEMDEV_SIZE);

                            /*初始化定義的互信息量*/
                            //初始化定義的自旋鎖ua
                            spin_lock_init(&(mem_devp[i].lock));
                            /*初始化兩個等待隊列頭,需要注意必須用括號包含起來,使得優先級正確*/
                            init_waitqueue_head(&(mem_devp[i].rdqueue));
                            init_waitqueue_head(&(mem_devp[i].wrqueue));
                    }
                  ...
            }

        3、確定一個具體的條件,比如數據有無,具體的條件根據實際的情況設計。
        /*等待條件*/
        static bool havedata = false;

        4、在需要堵塞的讀函數,寫函數中分別實現堵塞,首先定義等待隊列的節點,并添加到隊列中去,然后等待事件的喚醒進程。但是由于讀寫操作的兩個等待隊列都是基于條件havedata的,所以在讀完成以后需要喚醒寫,寫完成以后需要喚醒讀操作,同時更新條件havedata,最后還要移除添加的等待隊列節點。

            /*read函數的實現*/
            static ssize_t mem_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 mem_dev *dev = filp->private_data;

                    /*參數的檢查,首先判斷文件位置*/
                    if(p >= MEMDEV_SIZE)
                            return 0;
                    /*改正文件大小*/
                    if(count > MEMDEV_SIZE - p)
                            count = MEMDEV_SIZE - p;

                 #if 0

                    /*添加一個等待隊列節點到當前進程中*/
                    DECLARE_WAITQUEUE(wait_r,current);

                    /*將節點添加到等待隊列中*/
                    add_wait_queue(&dev->rdqueue,&wait_r);

                    /*添加等待隊列,本來采用if即可,但是由于信號等可能導致等待隊列的喚醒,因此采用循環,確保不會出現誤判*/
                    #endif

                    while(!havedata)
                    {
                            /*判斷用戶是否設置為非堵塞模式讀,告訴用戶再讀*/
                            if(filp->f_flags & O_NONBLOCK)
                                    return -EAGAIN;

                            /*依據條件havedata判斷隊列的狀態,防止進程被信號喚醒*/
                            wait_event_interruptible(dev->rdqueue,havedata);
                    }

                    spin_lock(&dev->lock);
                    /*從內核讀數據到用戶空間,實質就通過private_data訪問設備*/
                    if(copy_to_user(buf,(void *)(dev->data p),count))
                    {
                            /*出錯誤*/
                            ret = -EFAULT;
                    }
                    else
                    {
                            /*移動當前文件光標的位置*/

                            *ppos = count;
                            ret = count;

                            printk(KERN_INFO "read %d bytes(s) from %d\n",count,p);
                    }
                 
                    spin_unlock(&dev->lock);

         #if 0

                    /*將等待隊列節點從讀等待隊列中移除*/
                    remove_wait_queue(&dev->rdqueue,&wait_r);
            #endif

                    /*更新條件havedate*/
                    havedata = false;
                    /*喚醒寫等待隊列*/
                    wake_up_interruptible(&dev->wrqueue);

                    return ret;
            }


            /*write函數的實現*/
            static ssize_t mem_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 mem_dev *dev = filp->private_data;

                    /*檢查參數的長度*/
                    if(p >= MEMDEV_SIZE)
                            return 0;
                    if(count > MEMDEV_SIZE - p)
                            count = MEMDEV_SIZE - p;

          #if 0

                    /*定義并初始化一個等待隊列節點,添加到當前進程中*/
                    DECLARE_WAITQUEUE(wait_w,current);
                    /*將等待隊列節點添加到等待隊列中*/
                    add_wait_queue(&dev->wrqueue,&wait_w);
                    #endif

                    /*添加寫堵塞判斷*/
                    /*為何采用循環是為了防止信號等其他原因導致喚醒*/
                    while(havedata)
                    {
                            /*如果是以非堵塞方式*/
                            if(filp->f_flags & O_NONBLOCK)
                                    return -EAGAIN;
                    /*分析源碼發現,wait_event_interruptible 中存在DECLARE_WAITQUEUE和add_wait_queue的操作,因此不需要手動添加等待隊列節點*/
                            wait_event_interruptible(&dev->wrqueue,(!havedata));
                    }

                    spin_lock(&dev->lock);

                    if(copy_from_user(dev->data p,buf,count))
                            ret = -EFAULT;
                    else
                    {
                            /*改變文件位置*/
                            *ppos = count;
                            ret = count;
                            printk(KERN_INFO "writted %d bytes(s) from %d\n",count,p);
                    }

                    spin_unlock(&dev->lock);
            #if 0
                    /*將該等待節點移除*/
                    remove_wait_queue(&dev->wrqueue,&wait_w);
            #endif

                    /*更新條件*/
                    havedata = true;
                    /*喚醒讀等待隊列*/
                    wake_up_interruptible(&dev->rdqueue);

                    return ret;
            }

        5、應用程序采用兩個不同的進程分別進行讀、寫,然后檢測順序是否可以調換,檢查等待是否正常。

        關閉窗口

        相關文章

        欧美性色欧美精品视频,99热这里只有精品mp4,日韩高清亚洲日韩精品一区二区,2020国自产拍精品高潮