## 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);
~~~
對于 2 個方法, filp 是文件指針, count 是請求的傳輸數據大小. buff 參數指向持有被寫入數據的緩存, 或者放入新數據的空緩存. 最后, offp 是一個指針指向一個"long offset type"對象, 它指出用戶正在存取的文件位置. 返回值是一個"signed size type"; 它的使用在后面討論.
讓我們重復一下, read 和 write 方法的 buff 參數是用戶空間指針. 因此, 它不能被內核代碼直接解引用. 這個限制有幾個理由:
-
依賴于你的驅動運行的體系, 以及內核被如何配置的, 用戶空間指針當運行于內核模式可能根本是無效的. 可能沒有那個地址的映射, 或者它可能指向一些其他的隨機數據.
-
就算這個指針在內核空間是同樣的東西, 用戶空間內存是分頁的, 在做系統調用時這個內存可能沒有在 RAM 中. 試圖直接引用用戶空間內存可能產生一個頁面錯, 這是內核代碼不允許做的事情. 結果可能是一個"oops", 導致進行系統調用的進程死亡.
-
置疑中的指針由一個用戶程序提供, 它可能是錯誤的或者惡意的. 如果你的驅動盲目地解引用一個用戶提供的指針, 它提供了一個打開的門路使用戶空間程序存取或覆蓋系統任何地方的內存. 如果你不想負責你的用戶的系統的安全危險, 你就不能直接解引用用戶空間指針.
顯然, 你的驅動必須能夠存取用戶空間緩存以完成它的工作. 但是, 為安全起見這個存取必須使用特殊的, 內核提供的函數. 我們介紹幾個這樣的函數(定義于 <asm/uaccess.h>), 剩下的在第一章"使用 ioctl 參數"一節中. 它們使用一些特殊的, 依賴體系的技巧來確保內核和用戶空間的數據傳輸安全和正確.
scull 中的讀寫代碼需要拷貝一整段數據到或者從用戶地址空間. 這個能力由下列內核函數提供, 它們拷貝一個任意的字節數組, 并且位于大部分讀寫實現的核心中.
~~~
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 函數, 必須加一點小心在從內核代碼中存取用戶空間. 尋址的用戶也當前可能不在內存, 虛擬內存子系統會使進程睡眠在這個頁被傳送到位時. 例如, 這發生在必須從交換空間獲取頁的時候. 對于驅動編寫者來說, 最終結果是任何存取用戶空間的函數必須是可重入的, 必須能夠和其他驅動函數并行執行, 并且, 特別的, 必須在一個它能夠合法地睡眠的位置. 我們在第 5 章再回到這個主題.
這 2 個函數的角色不限于拷貝數據到和從用戶空間: 它們還檢查用戶空間指針是否有效. 如果指針無效, 不進行拷貝; 如果在拷貝中遇到一個無效地址, 另一方面, 只拷貝部分數據. 在 2 種情況下, 返回值是還要拷貝的數據量. scull 代碼查看這個錯誤返回, 并且如果它不是 0 就返回 -EFAULT 給用戶.
用戶空間存取和無效用戶空間指針的主題有些高級, 在第 6 章討論. 然而, 值得注意的是如果你不需要檢查用戶空間指針, 你可以調用 __copy_to_user 和 __copy_from_user 來代替. 這是有用處的, 例如, 如果你知道你已經檢查了這些參數. 但是, 要小心; 事實上, 如果你不檢查你傳遞給這些函數的用戶空間指針, 那么你可能造成內核崩潰和/或安全漏洞.
至于實際的設備方法, read 方法的任務是從設備拷貝數據到用戶空間(使用 copy_to_user), 而 write 方法必須從用戶空間拷貝數據到設備(使用 copy_from_user). 每個 read 或 write 系統調用請求一個特定數目字節的傳送, 但是驅動可自由傳送較少數據 -- 對讀和寫這確切的規則稍微不同, 在本章后面描述.
不管這些方法傳送多少數據, 它們通常應當更新 *offp 中的文件位置來表示在系統調用成功完成后當前的文件位置. 內核接著在適當時候傳播文件位置的改變到文件結構. pread 和 pwrite 系統調用有不同的語義; 它們從一個給定的文件偏移操作, 并且不改變其他的系統調用看到的文件位置. 這些調用傳遞一個指向用戶提供的位置的指針, 并且放棄你的驅動所做的改變.
圖[給 read 的參數](# "圖?3.2.?給 read 的參數")表示了一個典型讀實現是如何使用它的參數.
**圖?3.2.?給 read 的參數**

read 和 write 方法都在發生錯誤時返回一個負值. 相反, 大于或等于 0 的返回值告知調用程序有多少字節已經成功傳送. 如果一些數據成功傳送接著發生錯誤, 返回值必須是成功傳送的字節數, 錯誤不報告直到函數下一次調用. 實現這個傳統, 當然, 要求你的驅動記住錯誤已經發生, 以便它們可以在以后返回錯誤狀態.
盡管內核函數返回一個負數指示一個錯誤, 這個數的值指出所發生的錯誤類型( 如第 2 章介紹 ), 用戶空間運行的程序常常看到 -1 作為錯誤返回值. 它們需要存取 errno 變量來找出發生了什么. 用戶空間的行為由 POSIX 標準來規定, 但是這個標準沒有規定內核內部如何操作.
### 3.7.1.?read 方法
read 的返回值由調用的應用程序解釋:
-
如果這個值等于傳遞給 read 系統調用的 count 參數, 請求的字節數已經被傳送. 這是最好的情況.
-
如果是正數, 但是小于 count, 只有部分數據被傳送. 這可能由于幾個原因, 依賴于設備. 常常, 應用程序重新試著讀取. 例如, 如果你使用 fread 函數來讀取, 庫函數重新發出系統調用直到請求的數據傳送完成.
-
如果值為 0, 到達了文件末尾(沒有讀取數據).
-
一個負值表示有一個錯誤. 這個值指出了什么錯誤, 根據 <linux/errno.h>. 出錯的典型返回值包括 -EINTR( 被打斷的系統調用) 或者 -EFAULT( 壞地址 ).
前面列表中漏掉的是這種情況"沒有數據, 但是可能后來到達". 在這種情況下, read 系統調用應當阻塞. 我們將在第 6 章涉及阻塞.
scull 代碼利用了這些規則. 特別地, 它利用了部分讀規則. 每個 scull_read 調用只處理單個數據量子, 不實現一個循環來收集所有的數據; 這使得代碼更短更易讀. 如果讀程序確實需要更多數據, 它重新調用. 如果標準 I/O 庫(例如, fread)用來讀取設備, 應用程序甚至不會注意到數據傳送的量子化.
如果當前讀取位置大于設備大小, scull 的 read 方法返回 0 來表示沒有可用的數據(換句話說, 我們在文件尾). 這個情況發生在如果進程 A 在讀設備, 同時進程 B 打開它寫, 這樣將設備截短為 0. 進程 A 突然發現自己過了文件尾, 下一個讀調用返回 0.
這是 read 的代碼( 忽略對 down_interruptible 的調用并且現在為 up; 我們在下一章中討論它們):
~~~
ssize_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;
ssize_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 章查看這種情況的確切含義, 那里介紹了阻塞.
-
一個負值表示發生一個錯誤; 如同對于讀, 有效的錯誤值是定義于 <linux/errno.h>中.
不幸的是, 仍然可能有發出錯誤消息的不當行為程序, 它在進行了部分傳送時終止. 這是因為一些程序員習慣看寫調用要么完全失敗要么完全成功, 這實際上是大部分時間的情況, 應當也被設備支持. scull 實現的這個限制可以修改, 但是我們不想使代碼不必要地復雜.
write 的 scull 代碼一次處理單個量子, 如 read 方法做的:
~~~
ssize_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;
ssize_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 個系統調用. 這些 read 和 write 的"矢量"版本使用一個結構數組, 每個包含一個緩存的指針和一個長度值. 一個 readv 調用被期望來輪流讀取指示的數量到每個緩存. 相反, writev 要收集每個緩存的內容到一起并且作為單個寫操作送出它們.
如果你的驅動不提供方法來處理矢量操作, readv 和 writev 由多次調用你的 read 和 write 方法來實現. 在許多情況, 但是, 直接實現 readv 和 writev 能獲得更大的效率.
矢量操作的原型是:
~~~
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 (在用戶空間)并且有 iov_len 字節長. count 參數告訴有多少 iovec 結構. 這些結構由應用程序創建, 但是內核在調用驅動之前拷貝它們到內核空間.
矢量操作的最簡單實現是一個直接的循環, 只是傳遞出去每個 iovec 的地址和長度給驅動的 read 和 write 函數. 然而, 有效的和正確的行為常常需要驅動更聰明. 例如, 一個磁帶驅動上的 writev 應當將全部 iovec 結構中的內容作為磁帶上的單個記錄.
很多驅動, 但是, 沒有從自己實現這些方法中獲益. 因此, scull 省略它們. 內核使用 read 和 write 來模擬它們, 最終結果是相同的.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考