2014年8月12日 星期二

一個簡單的字元裝置(2)

    接著繼續來看看我們虛擬字元裝置的檔案操作函式吧!在這之前先用一張圖來看看user space是怎麼存取字元裝置

  
    一不小心放了太多東西進去了,不過多看一點也不錯。user space的process會先發出一個system call,例如read, write,好奇system call是怎麼被定義的嗎?其實他們長這樣(參數對應process裡傳給readwrite的參數):

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) 

在system call裡面會根據傳入的fd找到對應的file資料結構,再調用檔案操作結構裡面的readwrite

     以上圖來看,我們目前的位置在charecter device的地方,下面那個other device是...?那個就是依附在bus下的裝置啦,他們沒辦法以一個檔案的方式出現在/dev目錄下,所以只好請求字元裝置來幫忙囉!他們也會提供一些callback functions,字元裝置就可以利用這些callback functions存取依附在bus下的裝置了。

     上圖中的file-system就是常見的ext3,ubifs...這些檔案系統啦,他們跟block device之間的資料傳輸是透過request的方式,以後如果有機會在看看block device的部份。socket-protocol(TCP, UDP, IP)-net_device(網路裝置)之間溝通是透過sk_buf的資料結構,經過一個protocol就在在資料上加上一個頭部,最後透過網路裝置送出去,封包收進來後就一層一層的把頭部摘掉。

    嗯...好像扯太遠了,把話題拉回檔案操作結構,先來看看我們的檔案操作結構長什麼樣吧。

static struct file_operations chrdev_fops = {
    .read = chrdev_read,
    .write = chrdev_write,
    .unlocked_ioctl = chrdev_ioctl,
    .mmap = chrdev_mmap,
    .open = chrdev_open,
    .release = chrdev_release,
};


其實整個完整的結構裡面的callback function有很多,可是有些並不一定會用到,所以我們根據想要的功能只實現其中的某一部份。接下來就一一來探討這些callback function做了哪些事。
    首先來看看chrdev_open()chrdev_release()

int chrdev_open(struct inode *inode, struct file *filp)
{
    int id = iminor(inode);
    struct chrdev_inst *inst;

    if (id == c_dev.id) {
        inst = dev_get_drvdata(c_dev.dev);
        filp->private_data = inst;
        return 0;
    } else {
        return -ENODEV;
    }
   
}

int chrdev_release(struct inode *inode, struct file *filp)
{
    return 0;
}


在linux的世界裡,每個檔案就是一個inode,可以藉由inode區分這個檔案是字元裝置(i_cdev)還是區塊裝置(i_bdev),同時他還會紀錄裝置編號(i_rdev),還記得MKDEV函式產生裝置編號時傳入的次編號是什麼嗎?沒錯!就是字元裝置的ID,所以可以用iminor()來獲取裝置的ID。

    因為沒有把裝置的結構(inst)宣告成全域變數,只有在建立device時把他當成私有資料傳入,dev_get_drvdata()就是用來得到私有資料,接著再把他當成file結構的私有資料,這樣作的原因是之後在readwriteioctl...裡都會傳入file的結構,可以利用這個方法直接取得裝置的結構(inst)。在open()函式裡沒有分配記憶體,也沒有註冊IRQ,所以release()其實沒事可作。

    接下來看看chrdev_read()chrdev_write()

static ssize_t chrdev_read(struct file *filp, char __user *buff, size_t count, loff_t *ppos)
{
    int ret;    
    struct chrdev_inst *inst = filp->private_data;
   
    if(count > inst->size)
        count = inst->size;
    if (copy_to_user(buff, inst->buf, count))
        ret = -EFAULT;
    else      
        ret = count;

    return ret;
}

static ssize_t chrdev_write(struct file *filp, const char __user *buff, size_t count, loff_t *ppos)
{
    int ret;
    struct chrdev_inst *inst = filp->private_data;

    if (count > inst->size)
        count = inst->size;
    if (copy_from_user(inst->buf, buff, count))
        ret = -EFAULT;
    else
        ret = count;

    return ret;
}


這兩個函式會收到一樣的參數,一個指向user space資料的指標(buff),資料的大小(count)和目前的位置(*ppos),目前的位置在這邊並沒有被用到,也就是說每次讀寫都是從第0個byte開始寫或從第0個byte開始讀。因為user space和kernel space之間的記憶體空間不能夠互相直接存取,所以必須藉由copy_to_user()copy_from_user()複製資料。如果成功讀寫就回傳資料的大小。

    接著是chrdev_mmap()的部份

static int chrdev_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret = 0;
    unsigned long len = vma->vm_end - vma->vm_start;
    unsigned long offset = vma->vm_pgoff<<PAGE_SHIFT;
    struct chrdev_inst *inst = filp->private_data;

    if (offset > inst->size)
        return -EINVAL;
    if ((len+offset) > inst->size)
        return -EINVAL;

    offset = virt_to_phys(offset + inst->buf) >> PAGE_SHIFT;
    if (remap_pfn_range(vma, vma->vm_start, offset, len, vma->vm_page_prot))
        ret = -EAGAIN;
    return ret;
}


在看這個函式之前,我們先來看看在user space是怎麼呼叫的

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr指的是希望map到的virtual address,不過通常都是0,由kernel自己分配。len是map的檔案長度,以byte為單位。prot是這個區間可讀或可寫,如果有多個process同時map相同的檔案,flag標示如果檔案被一方修改另一方式否可見。offset指的是從檔案的哪裡開始map,必須是PAGE_SIZE大小的整數倍,PAGE_SIZE大小應系統而異,在我的電腦上是4KB。 當user space呼叫mmap()時,會先分配一塊virtual address,實際map到physical address是靠檔案操作函式的mmap()完成。
    回到檔案操作函式的mmap(),vm_area_struct結構紀錄的相關的資訊,結構成員vm_start和vm_end是在virtual address開始和結束的位址,vm_pgoff是offset,不過是以PAGE為單位,所以需要往左shift PAGE_SHIFT(12)的長度。
    接下來就是利用remap_pfn_range()把我們的裝置記憶體從offset的地方map一個len長度的大小給user space了,傳入的offset參數必須是實體記憶體對應的page frame number,所以要透過virt_to_phys()轉成實體記憶體後再往右shift PAGE_SHIFT

    最後就是chrdev_ioctl()

#define CHRDEV_TYPE 'k'
#define CHRDEV_ERASE _IO(CHRDEV_TYPE, 0x50)
#define CHRDEV_RDWR _IO(CHRDEV_TYPE, 0x51)

struct chrdev_msg {
    int flag;
#define MSG_WR 1
#define MSG_RD 2
    unsigned int from;
    unsigned int len;
    char *buf;
};

struct chrdev_ioctl_data {
    int num_msg;
    struct chrdev_msg *msg;
};


static int chrdev_ioctl_rdwr(struct file *filp, unsigned long arg)
{
    int i, j, ret;
    struct chrdev_ioctl_data chrdev_data;
    struct chrdev_msg *msg;
    char *buffer[MAX_NUM_MSG];
    struct chrdev_inst *inst = filp->private_data;

    if (copy_from_user(&chrdev_data, (struct chrdev_ioctl_data*)arg, sizeof(chrdev_data)))
        return -EFAULT;

    msg = kzalloc(chrdev_data.num_msg*sizeof(struct chrdev_msg), GFP_KERNEL);
    if (msg == NULL)
        return -ENOMEM;
    if (copy_from_user(msg, chrdev_data.msg, chrdev_data.num_msg*sizeof(struct chrdev_msg))) {
        kfree(msg);       
        return -EFAULT;
    }

    for (i=0; i<chrdev_data.num_msg; i++) {
        buffer[i] = kzalloc(msg[i].len, GFP_KERNEL);
        if (buffer[i] == NULL)
            break;

        if (msg[i].flag & MSG_WR) {
            if (copy_from_user(buffer[i], msg[i].buf, msg[i].len)) {
                kfree(buffer[i]);
                break;
            }
        }
    }

    for (j = 0; j < i; j++) {
        int from = msg[j].from;
        int len = msg[j].len;

        if (from > inst->size) {
            ret = -EINVAL;
            goto free_all;
        }

        if ((len+from) > inst->size)
            len = inst->size - from;

        if (msg[j].flag & MSG_RD) {
            memcpy(buffer[j], (inst->buf+from), msg[j].len);

            if (copy_to_user(msg[j].buf, buffer[j], msg[j].len))
                break;
        

        } else
            memcpy((inst->buf+from), buffer[j], msg[j].len);
    }
    ret = j;

free_all:
    for (j=0; j<i; j++)
        kfree(buffer[j]);
       
    kfree(msg);
    return ret;
}


static long chrdev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct chrdev_inst *inst = filp -> private_data;
   
    switch(cmd) {

    case CHRDEV_RDWR:
        ret = chrdev_ioctl_rdwr(filp, arg);
        break;
    case CHRDEV_ERASE:
        memset(inst->buf, 0, BUFFER_SIZE);
        break;
    default:
        return -EINVAL;
    }

    return 0;
}


這個函式會接收一個命令(cmd)和一個參數(arg),參數的部份可以自行定義,不過process和裝置驅動都要對這個參數有共識,型態轉換的時候才不會出錯。命令的部份就不能隨便定義了,因為有可能會跟系統中已有的命令相衝突,來看看kernel裡面是怎麼做的吧!

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
    struct file *filp;
    int error = -EBADF;
    int fput_needed;

    filp = fget_light(fd, &fput_needed);
    if (!filp)
        goto out;

    error = security_file_ioctl(filp, cmd, arg);
    if (error)
        goto out_fput;

    error = do_vfs_ioctl(filp, fd, cmd, arg);
 out_fput:
    fput_light(filp, fput_needed);
 out:
    return error;
}


int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd, unsigned long arg)
{
    int error = 0;
    int __user *argp = (int __user *)arg;

    switch (cmd) {
    case FIOCLEX:
        set_close_on_exec(fd, 1);
        break;

    case FIONCLEX:
        set_close_on_exec(fd, 0);
        break;

    case FIONBIO:
        error = ioctl_fionbio(filp, argp);
        break;

    case FIOASYNC:
        error = ioctl_fioasync(fd, filp, argp);
        break;

    case FIOQSIZE:
        if (S_ISDIR(filp->f_path.dentry->d_inode->i_mode) ||
            S_ISREG(filp->f_path.dentry->d_inode->i_mode) ||
            S_ISLNK(filp->f_path.dentry->d_inode->i_mode)) {
            loff_t res =
                inode_get_bytes(filp->f_path.dentry->d_inode);
            error = copy_to_user((loff_t __user *)arg, &res,
                         sizeof(res)) ? -EFAULT : 0;
        } else
            error = -ENOTTY;
        break;

    case FIFREEZE:
        error = ioctl_fsfreeze(filp);
        break;

    case FITHAW:
        error = ioctl_fsthaw(filp);
        break;

    case FS_IOC_FIEMAP:
        return ioctl_fiemap(filp, arg);

    case FIGETBSZ:
    {
        struct inode *inode = filp->f_path.dentry->d_inode;
        int __user *p = (int __user *)arg;
        return put_user(inode->i_sb->s_blocksize, p);
    }

    default:
        if (S_ISREG(filp->f_path.dentry->d_inode->i_mode))
            error = file_ioctl(filp, cmd, arg);
        else
            error = vfs_ioctl(filp, cmd, arg);
        break;
    }
    return error;
}

chrdev_ioctl()要到vfs_ioctl()這邊才會被調用,在這之前命令會被用來判斷是否是FIOCLEX,FIONCLEX,...,FIGETBSZ這些情況,至少我們的命令不可以跟這些衝突,不過保險起見還是參考一下Documentation/ioctl/ioctl-number.txt裡面所列已經被使用的命令。要怎麼設定一個命令呢?kernel提供了幾個方法:

_IO(type, nr)
_IOR(typr, nr, size)
_IOW(type, nr, size)
_IORW(type, nr, size)

可是...這樣在process裡面怎麼知道命令是什麼?嗯...這真是個好問題,今天就先看看ioctl()裡面在做些什麼,這個問題就留到下次探討吧!

     chrdev_ioctl()裡面只做兩件事,一個是user space的process透過chrdev_ioctl_data這個資料結構存取裝置的記憶體,令一個就是清除裝置的記憶體內容。chrdev_ioctl_data這是自己定義的一個結構,成員有chrdev_msg結構的數量(num_msg)和chrdev_msg結構的指標(msg),也可以看成一個陣列。而chrdev_msg結構可以把他看成一個message,他的成員有flag(這個message是讀或寫),from(從裝置記憶體哪裡開始讀或寫),len(讀或寫的大小)和一個指向user space的記憶體指標(buf)。

    實際實現這個方法是在chrdev_ioctl_rdwr()這個函式裡面,他會根據chrdev_ioctl_data和chrdev_msg裡面的欄位分配message和buffer的記憶體大小,並且把需要的資料從user space複製到kernel space來,最後就是對裝置的記憶體作讀寫了。如果在處理某個message上出錯了,之前成功的message還是可以被用來讀寫,並且回傳成功的message的數量。

    那如果process同時傳來兩個message都是寫資料到一樣的裝置記憶體位址,有沒有需要作錯誤處理呢?這個問題就牽涉到策略(policy)的部份,而linux開發者希望把有關策略的部份放到user space處理,kernel裡面只留能力(ability)或功能(functionality)的部份。udev也是因為這個原因才被開發出來的。

    有關這個字元裝置的檔案作函式大概就介紹到這邊啦!如果覺得意猶未盡,想要了解更多其他的操作函式,可以參考kernel裡面其他driver的作法,或是上google找找囉!


沒有留言:

張貼留言