天天看點

Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作

在前面一章我們實作了通過阻塞操作來通路硬體資源,下面我們要通過非阻塞的模式來嘗試一下如何實作這一效果。

使用者态APP

我們在上一章引出非阻塞模式的時候已經說明了,非阻塞IO主要是屬于異步IO的模式。那麼對于使用者态APP來說,有幾個接口我們需要了解

select

Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作

epoll

Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作

還有poll

Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作

其中 select和poll都屬于輪詢機制,而epoll屬于事件驅動機制。這裡要講的内容太多,以後有機會再深入。我們第一步的目的是能夠使用這個輪詢機制實作非阻塞IO的效果。是以這裡隻講select函數的使用。

檔案打開模式

我們前面所有的使用者态APP都是用來通路驅動節點的(/dev路徑下的裝置檔案),是以在程式中需要将檔案打開

f = open(filename, O_RDWR);      

這裡open函數傳入的參數flags是O_RDWR,這種打開方式是預設的阻塞模式。而我們想要使用非阻塞模式通路檔案時,需要加上新的标志

Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作

 也就是這樣打開檔案

f = open(filename, O_RDWR|O_NONBLOCK);      

 在原先的基礎上加上非阻塞的标志。

fd_set

要使用select函數,首先要明白一個資料類型:fd_set,先看看核心裡是怎麼描述這個資料的。

select函數

select函數用來根據IO狀态修改fd_set

/* fd_set for select and pselect.  */
typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;      

追溯到底,fd_set是一個long形式的數組,每一個元素都能和一個檔案句柄建立聯系,建立聯系的過程有我們來完成。具體這個fd_set的具體作用真心沒搞清,但是根據教程上說的大概意思就是值fd_set類型變量的每一位都代表了一個檔案描述符。select函數在監視檔案的時候需要通過fd_set将是否監視該檔案句柄,一般來說這個數組是0和1來描述的,1表示監控,0表示不監控,比如我們需要 從一個裝置檔案中讀取資料,就洗先定義個fd_set變量,然後通過下面幾個宏來操作

void FD_CLR(int fd, fd_set *set);       //将fd_set某一位清零
int  FD_ISSET(int fd, fd_set *set);     //測試某一個檔案是否屬于某個檔案集合
void FD_SET(int fd, fd_set *set);       //将fd_set變量某個bit置一,也就是向fd_set添加一個檔案描述符
void FD_ZERO(fd_set *set);              //将fd_set所有位都清零      

我們要用的時候就這麼做

1 f = open("file",O_RDWR|O_NONBLOCK);     //檔案句柄
2 fd_set readfds;                         //讀操作檔案描述符集
3 
4 FD_SERO(&readfds);                      //readfds清除                      
5 FD_SET(f,&readfds);                     //将打開的檔案句柄f添加到檔案描述符集裡      

在用非阻塞模式打開檔案後,建立一個新的檔案描述符集,清除後将檔案句柄添加到這個描述符裡。就可以通過select調用了。

select函數

當調用select函數時,核心根據IO狀态修改fd_set的内容,由此來通知執行了select的程序哪一個檔案可以讀或寫。先看下select的函數原型

int select(int nfds, fd_set *readfds, fd_set *writefds,
                fd_set *exceptfds, struct timeval *timeout);      

參數nfds 是select見識的檔案句柄數,根據程序中打開的檔案數量而定,一般是我們要監視的檔案的最大檔案号加一(注:nfds并非一定表示監視的檔案句柄數。官方文檔僅指出nfds is the highest-numbered file descriptor in any of the three sets, plus 1. (可在linux環境中通過man select指令查得))

Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作

參數readfds,writefds和exceptfds是select監視的檔案操作類型,分别為讀、寫和異常,可以根據需求按照上面的要求建立對應的檔案描述符集合傳進來就可以了

參數timeout是本次select操作的時長。這個時間是由一個精确到微秒的結構體來描述的

struct timeval
{
  __time_t tv_sec;        /* Seconds.  */
  __suseconds_t tv_usec;    /* Microseconds.  */
};      

我們需要按照需求來構造select的逾時時間

struct timeval timeout;
/*設定select逾時為500ms*/
timeout.tv_sec = 0;         
timeout.tv_usec = 500000;      

select的傳回值時int形式,如果有錯誤,就會傳回-1,如果逾時就會傳回0,否則傳回值就是可以進行操作都檔案描述符的個數。

寫一個程式來大概示範一下select函數是怎麼使用的

1 void main(void)
 2 {   
 3     /*變量聲明*/
 4     int ret,f;
 5     fd_set readfds;                                     //檔案描述符集
 6     struct timeval timeout;                             //逾時結構體
 7 
 8     f = open("/dev/xxx",O_RDWR|O_NONBLOCK);             //打開檔案擷取檔案句柄
 9     
10     FD_ZERO(&readfds);                                  //将檔案描述符集全部清零
11     FD_SET(f,&readfds);                                 //将檔案句柄添加至描述符集
12 
13     timeout.tv_sec = 0;                                 
14     timeout.tv_usec = 100000;                           //逾時時長100ms
15 
16     ret = select(f+1,&readfds,NULL,NULL,&timeout);      //使用select函數監控檔案read狀态
17     switch(ret){                                        //select傳回值為0 逾時
18         case 0:
19             printf("timeout!\r\n");
20             break;
21         case -1:                                        //select傳回值為-1,錯誤
22             printf("err!\r\n");
23             break;
24         default:                                        //可以read操作
25             if(FD_ISSET(f,&readfds)){                   //判斷是否是f檔案句柄
26                 /*讀取資料等操作*/
27                 printf("read data from dev\r\n");
28             }
29             break;
30     }
31 }      

上面的代碼隻是使用了select一次檢查檔案描述符是否發生變化,代碼的注釋還是很清楚的。因為我們隻需要考慮讀取的情況,是以在select的時候我們隻傳給readfds參數,其餘幾個都是NULL。如果需要進行寫操作則應該對writefds參數也要傳入相應的描述符集。

最後看看APP是怎麼實作的

Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作
Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作
/**
 * @file irqAPP.c
 * @author your name ([email protected])
 * @brief 按鍵中斷APP測試程式——非阻塞版本
 * @version 0.1
 * @date 2022-07-26
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

/**
 * @brief 
 * 
 * @param argc                      //參數個數 
 * @param argv                      //參數
 * @return int 
 */
int main(int argc,char *argv[])
{
    char *filename;                 //檔案名
    filename = argv[1];             //
    int value = 0;
    int ret = 0;                    //初始化操作傳回值
    int f = 0;                      //初始化檔案句柄

    fd_set readfds;
    struct timeval timeout;

    unsigned char data;             //核心傳來的值

    f = open(filename, O_RDWR|O_NONBLOCK);     //打開檔案(非阻塞方式)    
    if(f < 0){
    printf("file open error\r\n");
    return -1;
    }

    while(1){
        FD_ZERO(&readfds);
        FD_SET(f,&readfds);

        timeout.tv_sec = 0;
        timeout.tv_usec = 500000;    //500ms

        ret = select(f+1,&readfds,NULL,NULL,&timeout);
        switch(ret){
            case 0:  //逾時
            break;
            case -1: //錯誤
            break;
            default:
            //可以讀數
            if(FD_ISSET(f,&readfds)){
                ret = read(f,&data,sizeof(data));
                    if(ret<0){
                        //讀取錯誤
                        printf("data read err\r\n");
                    }
                    else{
                        if(data)               //擷取資料有效
                            printf("key value = %#X\r\n",data);
                    }                
            }
            break;
        }
 
        }
    close(f);                       //關閉檔案
    return 0;
}      

APP應用

整個應用程式主要依賴一個while循環,在循環裡先将檔案描述符清零,再導入我們關注的檔案,然後就是不停的使用select函數按照指定的逾時時間去看read的狀态,如果可以read就從驅動讀取資料。

APP完成後下面要針對非阻塞IO來做一下驅動程式

驅動程式構成

在這個驅動建構的過程有太多的問題沒解決,搞了快1周,隻能按照案例把驅動先寫出來。很多函數的使用說實話真沒搞清,網上也沒查到很詳細的說明。隻能先把輪子抄出來,至于為什麼是這樣隻能等以後水準能上來再考慮了。

我們在使用者态裡的while不停的調用select查詢檔案可悲通路狀态,每次select的時候對應驅動裡的檔案操作集合會執行poll對應的函數。是以特别要關注的就是poll操作對應的函數。

先看下poll函數的原型

unsigned int (*poll)(struct file *filp, struct poll_table *wait);      

第一個參數filp就是檔案句柄,比較好了解,後面這個指向poll_table的wait指針着實沒搞懂應用程式是怎麼傳過來的。

1 static unsigned int new_dev_poll(struct file *filp,struct poll_table_struct * wait)
 2 {
 3     int mask = 0;
 4     struct new_dev *dev = filp->private_data;
 5     poll_wait(filp,&dev->r_wait,wait);
 6     if(atomic_read(&dev->keyreleased)){
 7         //按鍵被按下,可讀取資料
 8         mask = POLLIN | POLLRDNORM;
 9     }
10     return mask;
11 }      

每一次select的時候,都會在驅動裡執行這個函數。首先在第5行調用poll_wait函數将等待隊列頭添加到poll_table中。

可以看一下poll_wait函數在核心裡的定義include/linux/poll.h

1 /*
 2  * Do not touch the structure directly, use the access functions
 3  * poll_does_not_wait() and poll_requested_events() instead.
 4  */
 5 typedef struct poll_table_struct {
 6     poll_queue_proc _qproc;
 7     unsigned long _key;
 8 } poll_table;
 9 
10 static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
11 {
12     if (p && p->_qproc && wait_address)
13         p->_qproc(filp, wait_address, p);
14 }      

這個poll_wait函數的作用和用法我實在是搞不懂,但是把這個函數屏蔽掉程式竟然還能正常運作,不知道是什麼情況。還有第三個參數wait,應該是從new_dev_poll函數中傳進來的,poll_table結構體的指針。

在Poll_wait函數執行完成後,根據按鍵狀态給定傳回值,如果按鍵被按下過,傳回帶有可被讀的mask狀态。否則傳回mask狀态就是原先狀态。(我這裡列印了一下mask的值,一直都是0,不知道是為什麼)。應用程式看到檔案句柄的可讀狀态發生了變化,就調用read函數從核心讀取資料。

最後把驅動代碼放出來

Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作
Linux驅動開發十一.阻塞與非阻塞IO操作——2.非阻塞IO操作
/**
 * @file unblockIO.c
 * @author your name ([email protected])
 * @brief 非阻塞IO測試驅動
 * @version 0.1
 * @date 2022-08-10
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/ide.h>

#define DEVICE_CNT      1
#define DEVICE_NAME    "imx6uirq"

#define KEY_NUM         1
#define KEY0VALUE       0x01
#define INVALKEYS       0xFF

/**
 * @brief 按鍵中斷結構體 
 * 
 */
struct irq_keydesc {
    int gpio;                           //io編号
    int irqnum;                         //中斷号
    unsigned char value;                //鍵值
    char name[10];                      //按鍵名字
    irqreturn_t (*handler)(int,void*);  //中斷處理函數  
    
};

/**
 * @brief 裝置結構體
 * 
 */
struct new_dev
{
    dev_t dev_id;
    int major;
    int minor;
    struct class *class;
    struct device *device;
    struct cdev cdev;
    struct device_node *dev_nd;
    int dev_gpio;

    struct irq_keydesc irqkey[KEY_NUM];         //按鍵描述數組

    struct timer_list timer;    //定時器
    int timer_per;

    atomic_t keyvalue;          
    atomic_t keyreleased;       //1表示1次有效按鍵并被釋放,0表示未被釋放

    wait_queue_head_t   r_wait;
};

struct new_dev new_dev;

static int new_dev_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &new_dev;     /* 設定私有資料 */
    return 0;
}

static ssize_t new_dev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt)
{
    int ret = 0;
    unsigned char keyvalue;
    unsigned char keyreleased;
    struct new_dev *dev = filp->private_data;       //私有資料
    printk("read\r\n");
    if(filp->f_flags & O_NONBLOCK){
        //非阻塞方式處理
        if(atomic_read(&dev->keyreleased) == 0){
            return -EAGAIN;
        }
    }
   
    else{
        //阻塞方式處理
        wait_event_interruptible(dev->r_wait,atomic_read(&dev->keyreleased));
        }

    keyvalue = atomic_read(&dev->keyvalue);         //擷取按鍵狀态标志及按鍵值
    keyreleased = atomic_read(&dev->keyreleased);

    if(keyreleased){                                //出現按鍵被按下并釋放的過程
        if(keyvalue&0x80){
            //擷取最高位,如果為1表示出現過按鍵被按下後釋放掉狀态,是有效狀态
            keyvalue &= ~0x80;
            ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue));
        }
        else{ 
            goto data_err;                          
        }
        atomic_set(&dev->keyreleased,0);            //恢複keyreleased的狀态
    }
    else{ 
        goto data_err;                              //傳回負數,在使用者态跳出循環
    }

    return ret;
    singal_err:
    data_err:
        return -EINVAL;
};

static unsigned int new_dev_poll(struct file *filp,struct poll_table_struct * wait)
{
    int mask = 0;
    struct new_dev *dev = filp->private_data;
    if(atomic_read(&dev->keyreleased)){
        //按鍵可讀
        mask = POLLIN | POLLWRNORM;
    }
    printk("mask=%d\r\n",mask);
    return mask;
}

/**
 * @brief 檔案操作集合
 * 
 */
static const struct file_operations key_fops = {
    .owner = THIS_MODULE,
    .open =  new_dev_open,
    .read = new_dev_read,
    .poll = new_dev_poll,
};


static irqreturn_t key0_handle_irq(int irq, void *dev_id)
{   
    struct new_dev *dev = dev_id;
    dev->timer.data = dev_id;
    mod_timer(&dev->timer,jiffies + msecs_to_jiffies(10));
    return IRQ_HANDLED;
}


static void timer_func(unsigned long arg){
    int value = 0;
    struct new_dev *dev =(struct new_dev*)arg;
    value = gpio_get_value(dev->irqkey[0].gpio);
    if(value == 0){     
        //按下
        atomic_set(&dev->keyvalue,dev->irqkey[0].value);
    }
    else{
        //釋放
        atomic_set(&dev->keyvalue,0x80|(dev->irqkey[0].value)); //将最高位置一,表示按鈕釋放
        atomic_set(&dev->keyreleased,1);
    }
}

static int dev_gpio_init(struct new_dev *dev)
{
    int ret = 0;
    int i = 0;

    //搜尋裝置樹節點
    dev->dev_nd = of_find_node_by_path("/key");
    if(dev->dev_nd == NULL){
        printk("can't find device key\r\n");
        ret = -EINVAL;
        goto fail_nd;
    }

    for(i=0;i<KEY_NUM;i++)
    {   
        dev->irqkey[i].gpio = of_get_named_gpio(dev->dev_nd,"key-gpios",i); //多個按鍵擷取
        if(dev->irqkey[i].gpio<0){
            ret = -EINVAL;
            goto fail_gpio_num;
        }
        ret = gpio_request(dev->irqkey[i].gpio,dev->irqkey[i].name);
        if(ret){
            ret = -EBUSY;
            goto fail_gpio_request;
        }
        gpio_direction_input(dev->irqkey[i].gpio);
        dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);           //擷取中斷号
        // dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->dev_nd,i)        //方法2擷取中斷号
    }

    dev->irqkey[0].handler = key0_handle_irq;
    dev->irqkey[0].value = KEY0VALUE;

    for(i=0;i<KEY_NUM;i++){
        memset(dev->irqkey[i].name,0,sizeof(dev->irqkey[i].name));
        sprintf(dev->irqkey[i].name,"KEY%d",i);  //将格式化資料寫入字元串中
        ret = request_irq(dev->irqkey[i].irqnum,                            //中斷号
                            key0_handle_irq,                                //中斷處理函數
                            IRQ_TYPE_EDGE_RISING|IRQ_TYPE_EDGE_FALLING,     //中斷處理函數
                            dev->irqkey[i].name,                            //中斷名稱
                            dev                                             //裝置結構體
                            );
        if(ret){
            printk("irq %d request err\r\n",dev->irqkey[i].irqnum);
            goto fail_irq;
        }
    }
    //此處不設定定時值,防止定時器add後直接運作
    init_timer(&dev->timer);
    dev->timer.function = timer_func;

    return 0;
    fail_gpio_request:
    fail_irq:
        for(i=0; i<KEY_NUM;i++){
            gpio_free(dev->irqkey[i].gpio);
        }
    fail_gpio_num:
    fail_nd:
        return ret; 
}

static int __init new_dev_init(void){

    int ret = 0; 
    // unsigned int val = 0;
    //申請裝置号
    new_dev.major = 0;
    if(new_dev.major){
        //手動指定裝置号,使用指定的裝置号
        new_dev.dev_id = MKDEV(new_dev.major,0);
        ret = register_chrdev_region(new_dev.dev_id,DEVICE_CNT,DEVICE_NAME);
    }
    else{
        //裝置号未指定,申請裝置号
        ret = alloc_chrdev_region(&new_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME);
        new_dev.major = MAJOR(new_dev.dev_id);
        new_dev.minor = MINOR(new_dev.dev_id);
    }
    printk("dev id geted!\r\n");

    if(ret<0){
        //裝置号申請異常,跳轉至異常處理
        goto faile_devid;
    }

    //字元裝置cdev初始化
    new_dev.cdev.owner = THIS_MODULE;

    cdev_init(&new_dev.cdev,&key_fops);                 //檔案操作集合映射

    ret = cdev_add(&new_dev.cdev,new_dev.dev_id,DEVICE_CNT);
    if(ret<0){
        //cdev初始化異常,跳轉至異常處理
        goto fail_cdev;
    }

    printk("chr dev inited!\r\n");


    //自動建立裝置節點
    new_dev.class = class_create(THIS_MODULE,DEVICE_NAME);
    if(IS_ERR(new_dev.class)){
        //class建立異常處理
        printk("class err!\r\n");
        ret = PTR_ERR(new_dev.class);
        goto fail_class;
    }
    printk("dev class created\r\n");
    new_dev.device = device_create(new_dev.class,NULL,new_dev.dev_id,NULL,DEVICE_NAME);
    if(IS_ERR(new_dev.device)){
        //裝置建立異常處理
        printk("device err!\r\n");
        ret = PTR_ERR(new_dev.device);
        goto fail_device;
    }
    printk("device created!\r\n");


    ret = dev_gpio_init(&new_dev);
    if(ret<0){
        goto fail_gpio_init;
    }

    //初始化原子變量
    atomic_set(&new_dev.keyvalue,INVALKEYS);
    atomic_set(&new_dev.keyreleased,0);
    return ret;


fail_gpio_init:
fail_device:
    //device建立失敗,意味着class建立成功,應該将class銷毀
    printk("device create err,class destroyed\r\n");
    class_destroy(new_dev.class);
fail_class:
    //類建立失敗,意味着裝置應該已經建立成功,此刻應将其釋放掉
    printk("class create err,cdev del\r\n");
    cdev_del(&new_dev.cdev);
fail_cdev:
    //cdev初始化異常,意味着裝置号已經申請完成,應将其釋放
    printk("cdev init err,chrdev register\r\n");
    unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT);
faile_devid:
    //裝置号申請異常,由于是第一步操作,不需要進行其他處理
    printk("dev id err\r\n");
    return ret;
}

static void __exit new_dev_exit(void){
    int i = 0;
    //釋放中斷
    for(i=0;i<KEY_NUM;i++){
        free_irq(new_dev.irqkey[i].irqnum,&new_dev);
    }

    for(i=0;i<KEY_NUM;i++){
        gpio_free(new_dev.irqkey[i].gpio);
    }

    del_timer_sync(&new_dev.timer);
    cdev_del(&new_dev.cdev);
    unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT);

    device_destroy(new_dev.class,new_dev.dev_id);
    class_destroy(new_dev.class);

}

module_init(new_dev_init);
module_exit(new_dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZeqiZ");      

unblockIO測試