2015年3月31日 星期二

LDD3 潤飾 3.7 Read and Write

3.7. 讀和寫

讀和寫方法都進行類似的任務, 就是, 從應用程序拷貝數據 或是拷貝數據到應用程序. 因此, 它們的原型相當相似, 可以同時介紹它們:

ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
對於兩個方法, filp 是file pointer, count 是請求的傳輸數據大小. buff 參數指向持有被寫入數據的緩存, 或者放入新數據的空緩存. 最後, offp 是一個pointer指向一個"long offset type"對象, 它指出用戶正在存取的文件位置. 返回值是一個"signed size type"; 它的使用在後面討論.

讓我們重複一下, read 和write 方法的buff 參數是user space pointer. 因此, 它不能被內核代碼直接 解引用. 這個限制有幾個理由:

依賴於你的driver運行的體系, 以及內核被如何配置的, user space pointer當運行於內核模式可能根本是無效的. 可能沒有那個地址的映射, 或者它可能指向一些其他的隨機數據.

就算這個pointer在內核空間是同樣的東西, user space內存是分頁的, 在做system call時這個內存可能沒有在RAM 中. 試圖直接引用user space內存可:能產生一個頁面錯, 這是內核代碼不允許做的事情. 結果可能是一個"oops", 導致進行system call的進程死亡.

置疑中的pointer由一個用戶程序提供, 它可能是錯誤的或者惡意的. 如果你的driver盲目地解引用一個用戶提供的pointer, 它提供了一個打開的門路使user space程序存取或覆蓋系統任何地方的內存. 如果你不想負責你的用戶的系統的安全危險, 你就不能直接解引用user space pointer.

顯然, 你的driver必須能夠存取user space緩存以完成它的工作. 但是, 為安全起見這個存取必須使用特殊的, 內核提供的函數. 我們介紹幾個這樣的函數(定義於<asm/uaccess .h>), 剩下的在第一章"使用ioctl 參數"一節中. 它們使用一些特殊的, 依賴體系的技巧來確保內核和user space的數據傳輸安全和正確.

scull 中的read, write需要拷貝一整段數據到或者從user address space. 這個能力由下列內核函數提供, 它們拷貝一個任意的字節數組, 並且位於大部分讀寫實現的核心中.

unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);
儘管這些函數表現象正常的memcpy 函數, 必須特別小心在從內核代碼中存取user space. 要存取的user pages可能當前不在memory裡, 在這個page被傳送到位時virtual memory子系統會使進程sleep.例如, 這發生在必須從swap space獲取page的時候. 對於driver編寫者來說, 最終結果是任何存取user space的函數必須是可重入的, 必須能夠和其他driver函數並行執行, 並且, 特別的, 必須在一個它能夠合法地sleep的位置. 我們在第5 章再回到這個主題.

這2 個函數的角色不限於拷貝數據到和從user space: 它們還檢查user space pointer是否有效. 如果pointer無效, 不進行拷貝; 如果在拷貝中遇到一個無效地址, 另一方面, 只拷貝部分數據. 在2 種情況下, 返回值是還要拷貝的數據量. scull 代碼查看這個錯誤返回, 並且如果它不是0 就返回-EFAULT 給用戶.

user space存取和無效user space pointer的主題有些進階, 在第6 章討論. 然而, 值得注意的是如果你不需要檢查user space pointer, 你可以調用__copy_to_user 和__copy_from_user 來代替. 這是有用處的, 例如, 如果你知道你已經檢查了這些參數. 但是, 要小心; 事實上, 如果你不檢查你傳遞給這些函數的user space pointer, 那麼你可能造成內核崩潰和/或安全漏洞.

至於實際的設備方法, read 方法的任務是從設備拷貝數據到user space(使用copy_to_user), 而write 方法必須從user space拷貝數據到設備(使用copy_from_user). 每個read 或write system call請求一個特定數目字節的傳送, 但是driver可自由傳送較少數據-- 對讀和寫這確切的規則稍微不同, 在本章後面描述.

不管這些方法傳送多少數據, 它們通常應當更新*offp 中的文件位置來表示在system call成功完成後當前的文件位置. 內核接著在適當時候傳播文件位置的改變到文件結構. pread 和pwrite system call有不同的語義; 它們從一個給定的文件偏移操作, 並且不改變其他的system call看到的文件位置. 這些調用傳遞一個指向用戶提供的位置的pointer, 並且放棄你的driver所做的改變.

圖給read的參數表示了一個典型讀實現是如何使用它的參數.
給read 的參數
圖 3.2. 給read 的參數

給read 的參數
read 和write 方法都在發生錯誤時返回一個負值. 相反, 大於或等於0 的返回值告知調用程序有多少字節已經成功傳送. 如果一些數據成功傳送接著發生錯誤, 返回值必須是成功傳送的字節數, error不會報告直到函數下一次調用. 當然, 為了實現這個慣例,要求你的driver記住錯誤已經發生, 以便它們可以在以後返回錯誤狀態.

儘管內核函數返回一個負數指示一個錯誤, 這個數的值指出所發生的錯誤類型( 如第2 章介紹), user space運行的程序常常看到-1 作為錯誤返回值. 它們需要存取errno 變量來找出發生了什麼. user space的行為由POSIX 標準來規定, 但是這個標準沒有規定內核內部如何操作.

3.7.1. read 方法

read 的返回值由調用的應用程序解釋:

如果這個值等於傳遞給read system call的count 參數, 代表請求的字節數已經被傳送. 這是最好的情況.

如果是正數, 但是小於count, 則只有部分數據被傳送. 這可能由於幾個原因, 與設備有關. 一般而言,應用程序重新試著讀取. 例如, 假如你使用fread 函數來讀取, library function重新發出system call直到請求的數據傳送完成.

如果值為0, 代表到達了文件末尾(沒有讀取數據).

一個負值表示有一個錯誤. , 根據<linux/errno.h>這個值指出了什麼錯誤. 出錯的典型返回值包括-EINTR( 被打斷的system call) 或者-EFAULT( 錯誤地址).

前面列表中漏掉的是這種情況"沒有數據, 但是可能之後會有". 在這種情況下, read system call應當blocking. 我們將在第6 章涉及blocking.

scull 代碼利用了這些規則. 特別是它利用了partial-read規則. 每個scull_read 調用只處理單個數據quantum, 不實現一個循環來收集所有的數據; 這使得代碼更短更易讀. 如果確實需要更多數據, 它重複重新調用. 如果標準I/O library(例如, fread)用來讀取設備, 應用程序甚至不會注意到數據傳送的量子化.

如果當前讀取位置大於設備大小, scull 的read 方法返回0 來表示沒有可用的數據(換句話說, 我們在文件尾). 這個情況發生在如果進程A 在讀取設備, 同時進程B 打開它來寫入, 這樣將設備截短為0. 進程A 突然發現自己過了文件尾, 下一個read call返回0.

這是read 的代碼( 忽略對down_interruptible 的調用並且現在為up; 我們在下一章中討論它們):

ssiz??e_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
        struct scull_dev *dev = filp->private_data;
        struct scull_qset *dptr; /* the first listitem */
        int quantum = dev->quantum, qset = dev->qset;
        int itemsize = quantum * qset; /* how many bytes in the listitem */
        int item, s_pos, q_pos, rest;
        ssiz??e_t retval = 0;

        if (down_interruptible(&dev->sem))
                return -ERESTARTSYS;
        if (*f_pos >= dev->size)
                goto out;
        if (*f_pos + count > dev->size)
                count = dev->size - *f_pos;

        /* find listitem, qset index, and offset in the quantum */
        item = (long)*f_pos / itemsize;
        rest = (long)*f_pos % itemsize;
        s_pos = rest / quantum;
        q_pos = rest % quantum;

        /* follow the list up to the right position (defined elsewhere) */
        dptr = scull_follow(dev, item);
        if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
                goto out; /* don't fill holes */

        /* read only up to the end of this quantum */
        if (count > quantum - q_pos)
                count = quantum - q_pos;

        if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count))
        {
                retval = -EFAULT;
                goto out;

        }
        *f_pos += count;
        retval = count;

out:
        up(&dev->sem);
        return retval;
}
3.7.2. write 方法

write, 像read, 可以傳送少於要求的數據, 根據返回值的下列規則:

如果值等於count, 要求的字節數已被傳送.

如果正值, 但是小於count, 只有部分數據被傳送. 程序最可能會重試寫入剩下的數據.

如果值為0, 什麼都沒有寫入. 這個結果不是一個錯誤, 因此沒有理由返回一個錯誤碼. 再一次, 標準庫重試寫入調用. 我們將在第6 章查看這種情況的確切含義, 那裡介紹了blocking.

一個負值表示發生一個錯誤; 如同對於讀, 有效的錯誤值是定義於<linux/errno.h>中.

不幸的是, 仍然可能有些不當行為程序, 它在進行了部分傳送時,發出error message 並且終止程序. 這是因為一些程序員習慣看寫調用要么完全失敗要么完全成功, 這實際上是大部分時間的情況, 應當也被設備支持. scull 實現的這個限制可以修改, 但是我們不想使代碼不必要地複雜.

write 的scull 代碼一次處理單個quantum, 如read 方法做的:

ssiz??e_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
        struct scull_dev *dev = filp->private_data;
        struct scull_qset *dptr;
        int quantum = dev->quantum, qset = dev->qset;
        int itemsize = quantum * qset;
        int item, s_pos, q_pos, rest;
        ssiz??e_t retval = -ENOMEM; /* value used in "goto out" statements */
        if (down_interruptible(&dev->sem))
                return -ERESTARTSYS;

        /* find listitem, qset index and offset in the quantum */
        item = (long)*f_pos / itemsize;
        rest = (long)*f_pos % itemsize;
        s_pos = rest / quantum;
        q_pos = rest % quantum;
        /* follow the list up to the right position */
        dptr = scull_follow(dev, item);
        if (dptr == NULL)
                goto out;
        if (!dptr->data)
        {
                dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
                if (!dptr->data)
                        goto out;
                memset(dptr->data, 0, qset * sizeof(char *));
        }
        if (!dptr->data[s_pos])
        {
                dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
                if (!dptr->data[s_pos])

                        goto out;
        }
        /* write only up to the end of this quantum */
        if (count > quantum - q_pos)

                count = quantum - q_pos;
        if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count))
        {
                retval = -EFAULT;
                goto out;

        }
        *f_pos += count;
        retval = count;

        /* update the size */
        if (dev->size < *f_pos)
                dev->size = *f_pos;

out:
        up(&dev->sem);
        return retval;

}
3.7.3. readv 和writev

Unix 系統已經長時間支持名為readv 和writev 的2 個system call. 這些read 和write 的"vector"版本使用一個array structure, 每個包含一個緩存的pointer和一個長度值. 一個readv 調用被期望來輪流讀取指示的數量到每個緩存. 相反, writev 要收集每個緩存的內容到一起並且作為單個write操作送出它們.

如果你的driver不提供方法來處理vector操作, readv 和writev 由多次調用你的read 和write 方法來實現. 但是在許多情況, 直接實現readv 和writev 能獲得更大的效率.

vector操作的原型是:

ssize_t (*readv) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
ssize_t (*writev) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
這裡, filp 和ppos 參數與read 和write 的相同. iovec 結構, 定義於<linux/uio.h>, 如同:

struct iovec
{
    void __user *iov_base; __kernel_size_t iov_len;
};
每個iovec 描述了一塊要傳送的數據; 它開始於iov_base (在user space)並且有iov_len 字節長. count 參數告訴有多少iovec 結構. 這些結構由應用程序創建, 但是內核在調用driver之前拷貝它們到內核空間.

矢量操作的最簡單實現是一個直接的循環, 只是傳遞出去每個iovec 的地址和長度給driver的read 和write 函數. 然而, 有效的和正確的行為常常需要driver更聰明. 例如, 一個磁帶driver上的writev 應當將全部iovec 結構中的內容作為磁帶上的單個記錄.

但是很多driver 沒有從自己實現這些方法中獲益. 因此, scull 省略它們. 內核使用read 和write 來模擬它們, 最終結果是相同的.

沒有留言: