6.1. ioctl 接口

2018-02-24 15:49 更新

6.1.?ioctl 接口

大部分驅(qū)動(dòng)需要 -- 除了讀寫設(shè)備的能力 -- 通過(guò)設(shè)備驅(qū)動(dòng)進(jìn)行各種硬件控制的能力. 大部分設(shè)備可進(jìn)行超出簡(jiǎn)單的數(shù)據(jù)傳輸之外的操作; 用戶空間必須常常能夠請(qǐng)求, 例如, 設(shè)備鎖上它的門, 彈出它的介質(zhì), 報(bào)告錯(cuò)誤信息, 改變波特率, 或者自我銷毀. 這些操作常常通過(guò) ioctl 方法來(lái)支持, 它通過(guò)相同名子的系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn).

在用戶空間, ioctl 系統(tǒng)調(diào)用有下面的原型:

int ioctl(int fd, unsigned long cmd, ...); 

這個(gè)原型由于這些點(diǎn)而凸現(xiàn)于 Unix 系統(tǒng)調(diào)用列表, 這些點(diǎn)常常表示函數(shù)有數(shù)目不定的參數(shù). 在實(shí)際系統(tǒng)中, 但是, 一個(gè)系統(tǒng)調(diào)用不能真正有變數(shù)目的參數(shù). 系統(tǒng)調(diào)用必須有一個(gè)很好定義的原型, 因?yàn)橛脩舫绦蚩纱嫒∷鼈冎荒芡ㄟ^(guò)硬件的"門". 因此, 原型中的點(diǎn)不表示一個(gè)變數(shù)目的參數(shù), 而是一個(gè)單個(gè)可選的參數(shù), 傳統(tǒng)上標(biāo)識(shí)為 char *argp. 這些點(diǎn)在那里只是為了阻止在編譯時(shí)的類型檢查. 第 3 個(gè)參數(shù)的實(shí)際特點(diǎn)依賴所發(fā)出的特定的控制命令( 第 2 個(gè)參數(shù) ). 一些命令不用參數(shù), 一些用一個(gè)整數(shù)值, 以及一些使用指向其他數(shù)據(jù)的指針. 使用一個(gè)指針是傳遞任意數(shù)據(jù)到 ioctl 調(diào)用的方法; 設(shè)備接著可與用戶空間交換任何數(shù)量的數(shù)據(jù).

ioctl 調(diào)用的非結(jié)構(gòu)化特性使它在內(nèi)核開發(fā)者中失寵. 每個(gè) ioctl 命令, 基本上, 是一個(gè)單獨(dú)的, 常常無(wú)文檔的系統(tǒng)調(diào)用, 并且沒(méi)有方法以任何類型的全面的方式核查這些調(diào)用. 也難于使非結(jié)構(gòu)化的 ioctl 參數(shù)在所有系統(tǒng)上一致工作; 例如, 考慮運(yùn)行在 32-位模式的一個(gè)用戶進(jìn)程的 64-位 系統(tǒng). 結(jié)果, 有很大的壓力來(lái)實(shí)現(xiàn)混雜的控制操作, 只通過(guò)任何其他的方法. 可能的選擇包括嵌入命令到數(shù)據(jù)流(本章稍后我們將討論這個(gè)方法)或者使用虛擬文件系統(tǒng), 要么是 sysfs 要么是設(shè)備特定的文件系統(tǒng). (我們將在 14 章看看 sysfs). 但是, 事實(shí)是 ioctl 常常是最容易的和最直接的選擇,對(duì)于真正的設(shè)備操作.

ioctl 驅(qū)動(dòng)方法有和用戶空間版本不同的原型:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

inode 和 filp 指針是對(duì)應(yīng)應(yīng)用程序傳遞的文件描述符 fd 的值, 和傳遞給 open 方法的相同參數(shù). cmd 參數(shù)從用戶那里不改變地傳下來(lái), 并且可選的參數(shù) arg 參數(shù)以一個(gè) unsigned long 的形式傳遞, 不管它是否由用戶給定為一個(gè)整數(shù)或一個(gè)指針. 如果調(diào)用程序不傳遞第 3 個(gè)參數(shù), 被驅(qū)動(dòng)操作收到的 arg 值是無(wú)定義的. 因?yàn)轭愋蜋z查在這個(gè)額外參數(shù)上被關(guān)閉, 編譯器不能警告你如果一個(gè)無(wú)效的參數(shù)被傳遞給 ioctl, 并且任何關(guān)聯(lián)的錯(cuò)誤將難以查找.

如果你可能想到的, 大部分 ioctl 實(shí)現(xiàn)包括一個(gè)大的 switch 語(yǔ)句來(lái)根據(jù) cmd 參數(shù), 選擇正確的做法. 不同的命令有不同的數(shù)值, 它們常常被給予符號(hào)名來(lái)簡(jiǎn)化編碼. 符號(hào)名通過(guò)一個(gè)預(yù)處理定義來(lái)安排. 定制的驅(qū)動(dòng)常常聲明這樣的符號(hào)在它們的頭文件中; scull.h 為 scull 聲明它們. 用戶程序必須, 當(dāng)然, 包含那個(gè)頭文件來(lái)存取這些符號(hào).

6.1.1.?選擇 ioctl 命令

在為 ioctl 編寫代碼之前, 你需要選擇對(duì)應(yīng)命令的數(shù)字. 許多程序員的第一個(gè)本能的反應(yīng)是選擇一組小數(shù)從0或1開始, 并且從此開始向上. 但是, 有充分的理由不這樣做. ioctl 命令數(shù)字應(yīng)當(dāng)在這個(gè)系統(tǒng)是唯一的, 為了阻止向錯(cuò)誤的設(shè)備發(fā)出正確的命令而引起的錯(cuò)誤. 這樣的不匹配不會(huì)不可能發(fā)生, 并且一個(gè)程序可能發(fā)現(xiàn)它自己試圖改變一個(gè)非串口輸入系統(tǒng)的波特率, 例如一個(gè) FIFO 或者一個(gè)音頻設(shè)備. 如果這樣的 ioctl 號(hào)是唯一的, 這個(gè)應(yīng)用程序得到一個(gè) EINVAL 錯(cuò)誤而不是繼續(xù)做不應(yīng)當(dāng)做的事情.

為幫助程序員創(chuàng)建唯一的 ioctl 命令代碼, 這些編碼已被劃分為幾個(gè)位段. Linux 的第一個(gè)版本使用 16-位數(shù): 高 8 位是關(guān)聯(lián)這個(gè)設(shè)備的"魔"數(shù), 低 8 位是一個(gè)順序號(hào), 在設(shè)備內(nèi)唯一. 這樣做是因?yàn)?Linus 是"無(wú)能"的(他自己的話); 一個(gè)更好的位段劃分僅在后來(lái)被設(shè)想. 不幸的是, 許多驅(qū)動(dòng)仍然使用老傳統(tǒng). 它們不得不: 改變命令編碼會(huì)破壞大量的二進(jìn)制程序,并且這不是內(nèi)核開發(fā)者愿意見(jiàn)到的.

根據(jù) Linux 內(nèi)核慣例來(lái)為你的驅(qū)動(dòng)選擇 ioctl 號(hào), 你應(yīng)當(dāng)首先檢查 include/asm/ioctl.h 和 Documentation/ioctl-number.txt. 這個(gè)頭文件定義你將使用的位段: type(魔數(shù)), 序號(hào), 傳輸方向, 和參數(shù)大小. ioctl-number.txt 文件列舉了在內(nèi)核中使用的魔數(shù),[20]?因此你將可選擇你自己的魔數(shù)并且避免交疊. 這個(gè)文本文件也列舉了為什么應(yīng)當(dāng)使用慣例的原因.

定義 ioctl 命令號(hào)的正確方法使用 4 個(gè)位段, 它們有下列的含義. 這個(gè)列表中介紹的新符號(hào)定義在 .

type

魔數(shù). 只是選擇一個(gè)數(shù)(在參考了 ioctl-number.txt之后)并且使用它在整個(gè)驅(qū)動(dòng)中. 這個(gè)成員是 8 位寬(_IOC_TYPEBITS).

number

序(順序)號(hào). 它是 8 位(_IOC_NRBITS)寬.

direction

數(shù)據(jù)傳送的方向,如果這個(gè)特殊的命令涉及數(shù)據(jù)傳送. 可能的值是 _IOC_NONE(沒(méi)有數(shù)據(jù)傳輸), _IOC_READ, _IOC_WRITE, 和 _IOC_READ|_IOC_WRITE (數(shù)據(jù)在2個(gè)方向被傳送). 數(shù)據(jù)傳送是從應(yīng)用程序的觀點(diǎn)來(lái)看待的; _IOC_READ 意思是從設(shè)備讀, 因此設(shè)備必須寫到用戶空間. 注意這個(gè)成員是一個(gè)位掩碼, 因此 _IOC_READ 和 _IOC_WRITE 可使用一個(gè)邏輯 AND 操作來(lái)抽取.

size

涉及到的用戶數(shù)據(jù)的大小. 這個(gè)成員的寬度是依賴體系的, 但是常常是 13 或者 14 位. 你可為你的特定體系在宏 _IOC_SIZEBITS 中找到它的值. 你使用這個(gè) size 成員不是強(qiáng)制的 - 內(nèi)核不檢查它 -- 但是它是一個(gè)好主意. 正確使用這個(gè)成員可幫助檢測(cè)用戶空間程序的錯(cuò)誤并使你實(shí)現(xiàn)向后兼容, 如果你曾需要改變相關(guān)數(shù)據(jù)項(xiàng)的大小. 如果你需要更大的數(shù)據(jù)結(jié)構(gòu), 但是, 你可忽略這個(gè) size 成員. 我們很快見(jiàn)到如何使用這個(gè)成員.

頭文件 , 它包含在 中, 定義宏來(lái)幫助建立命令號(hào), 如下: _IO(type,nr)(給沒(méi)有參數(shù)的命令), _IOR(type, nre, datatype)(給從驅(qū)動(dòng)中讀數(shù)據(jù)的), _IOW(type,nr,datatype)(給寫數(shù)據(jù)), 和 _IOWR(type,nr,datatype)(給雙向傳送). type 和 number 成員作為參數(shù)被傳遞, 并且 size 成員通過(guò)應(yīng)用 sizeof 到 datatype 參數(shù)而得到.

這個(gè)頭文件還定義宏, 可被用在你的驅(qū)動(dòng)中來(lái)解碼這個(gè)號(hào): _IOC_DIR(nr), _IOC_TYPE(nr), _IOC_NR(nr), 和 _IOC_SIZE(nr). 我們不進(jìn)入任何這些宏的細(xì)節(jié), 因?yàn)轭^文件是清楚的, 并且在本節(jié)稍后有例子代碼展示.

這里是一些 ioctl 命令如何在 scull 被定義的. 特別地, 這些命令設(shè)置和獲得驅(qū)動(dòng)的可配置參數(shù).

/* Use 'k' as magic number */
#define SCULL_IOC_MAGIC 'k'
/* Please use a different 8-bit number in your code */

#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
 * S means "Set" through a ptr,
 * T means "Tell" directly with the argument value
 * G means "Get": reply by setting through a pointer
 * Q means "Query": response is on the return value
 * X means "eXchange": switch G and S atomically
 * H means "sHift": switch T and Q atomically
 */
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)

#define SCULL_IOC_MAXNR 14

真正的源文件定義幾個(gè)額外的這里沒(méi)有出現(xiàn)的命令.

我們選擇實(shí)現(xiàn) 2 種方法傳遞整數(shù)參數(shù): 通過(guò)指針和通過(guò)明確的值(盡管, 由于一個(gè)已存在的慣例, ioclt 應(yīng)當(dāng)通過(guò)指針交換值). 類似地, 2 種方法被用來(lái)返回一個(gè)整數(shù)值:通過(guò)指針和通過(guò)設(shè)置返回值. 這個(gè)有效只要返回值是一個(gè)正的整數(shù); 如同你現(xiàn)在所知道的, 在從任何系統(tǒng)調(diào)用返回時(shí), 一個(gè)正值被保留(如同我們?cè)?read 和 write 中見(jiàn)到的), 而一個(gè)負(fù)值被看作一個(gè)錯(cuò)誤并且被用來(lái)在用戶空間設(shè)置 errno.[21]

"exchange"和"shift"操作對(duì)于 scull 沒(méi)有特別的用處. 我們實(shí)現(xiàn)"exchange"來(lái)顯示驅(qū)動(dòng)如何結(jié)合獨(dú)立的操作到單個(gè)的原子的操作, 并且"shift"來(lái)連接"tell"和"query". 有時(shí)需要象這樣的原子的測(cè)試-和-設(shè)置操作, 特別地, 當(dāng)應(yīng)用程序需要設(shè)置和釋放鎖.

命令的明確的序號(hào)沒(méi)有特別的含義. 它只用來(lái)區(qū)分命令. 實(shí)際上, 你甚至可使用相同的序號(hào)給一個(gè)讀命令和一個(gè)寫命令, 因?yàn)閷?shí)際的 ioctl 號(hào)在"方向"位是不同的, 但是你沒(méi)有理由這樣做. 我們選擇在任何地方不使用命令的序號(hào)除了聲明中, 因此我們不分配一個(gè)返回值給它. 這就是為什么明確的號(hào)出現(xiàn)在之前給定的定義中. 這個(gè)例子展示了一個(gè)使用命令號(hào)的方法, 但是你有自由不這樣做.

除了少數(shù)幾個(gè)預(yù)定義的命令(馬上就討論), ioctl 的 cmd 參數(shù)的值當(dāng)前不被內(nèi)核使用, 并且在將來(lái)也很不可能. 因此, 你可以, 如果你覺(jué)得懶, 避免前面展示的復(fù)雜的聲明并明確聲明一組調(diào)整數(shù)字. 另一方面, 如果你做了, 你不會(huì)從使用這些位段中獲益, 并且你會(huì)遇到困難如果你曾提交你的代碼來(lái)包含在主線內(nèi)核中. 頭文件 是這個(gè)老式方法的例子, 使用 16-位的調(diào)整值來(lái)定義 ioctl 命令. 那個(gè)源代碼依靠調(diào)整數(shù)因?yàn)槭褂媚莻€(gè)時(shí)候遵循的慣例, 不是由于懶惰. 現(xiàn)在改變它可能導(dǎo)致無(wú)理由的不兼容.

6.1.2.?返回值

ioctl 的實(shí)現(xiàn)常常是一個(gè) switch 語(yǔ)句, 基于命令號(hào). 但是當(dāng)命令號(hào)沒(méi)有匹配一個(gè)有效的操作時(shí)缺省的選擇應(yīng)當(dāng)是什么? 這個(gè)問(wèn)題是有爭(zhēng)議的. 幾個(gè)內(nèi)核函數(shù)返回 -ENIVAL("Invalid argument"), 它有意義是因?yàn)槊顓?shù)確實(shí)不是一個(gè)有效的. POSIX 標(biāo)準(zhǔn), 但是, 說(shuō)如果一個(gè)不合適的 ioctl 命令被發(fā)出, 那么 -ENOTTY 應(yīng)當(dāng)被返回. 這個(gè)錯(cuò)誤碼被 C 庫(kù)解釋為"設(shè)備的不適當(dāng)?shù)?ioctl", 這常常正是程序員需要聽到的. 然而, 它仍然是相當(dāng)普遍的來(lái)返回 -EINVAL, 對(duì)于響應(yīng)一個(gè)無(wú)效的 ioctl 命令.

6.1.3.?預(yù)定義的命令

盡管 ioctl 系統(tǒng)調(diào)用最常用來(lái)作用于設(shè)備, 內(nèi)核能識(shí)別幾個(gè)命令. 注意這些命令, 當(dāng)用到你的設(shè)備時(shí), 在你自己的文件操作被調(diào)用之前被解碼. 因此, 如果你選擇相同的號(hào)給一個(gè)你的 ioctl命令, 你不會(huì)看到任何的給那個(gè)命令的請(qǐng)求, 并且應(yīng)用程序獲得某些不期望的東西, 因?yàn)樵?ioctl 號(hào)之間的沖突.

預(yù)定義命令分為 3 類:

  • 可對(duì)任何文件發(fā)出的(常規(guī), 設(shè)備, FIFO, 或者 socket) 的那些.

  • 只對(duì)常規(guī)文件發(fā)出的那些.

  • 對(duì)文件系統(tǒng)類型特殊的那些.

最后一類的命令由宿主文件系統(tǒng)的實(shí)現(xiàn)來(lái)執(zhí)行(這是 chattr 命令如何工作的). 設(shè)備驅(qū)動(dòng)編寫者只對(duì)第一類命令感興趣, 它們的魔數(shù)是 "T". 查看其他類的工作留給讀者作為練習(xí); ext2_ioctl 是最有趣的函數(shù)(并且比預(yù)期的要容易理解), 因?yàn)樗鼘?shí)現(xiàn) append-only 標(biāo)志和 immutable 標(biāo)志.

下列 ioctl 命令是預(yù)定義給任何文件, 包括設(shè)備特殊的文件:

FIOCLEX

設(shè)置 close-on-exec 標(biāo)志(File IOctl Close on EXec). 設(shè)置這個(gè)標(biāo)志使文件描述符被關(guān)閉, 當(dāng)調(diào)用進(jìn)程執(zhí)行一個(gè)新程序時(shí).

FIONCLEX

清除 close-no-exec 標(biāo)志(File IOctl Not CLose on EXec). 這個(gè)命令恢復(fù)普通文件行為, 復(fù)原上面 FIOCLEX 所做的. FIOASYNC 為這個(gè)文件設(shè)置或者復(fù)位異步通知(如同在本章中"異步通知"一節(jié)中討論的). 注意直到 Linux 2.2.4 版本的內(nèi)核不正確地使用這個(gè)命令來(lái)修改 O_SYNC 標(biāo)志. 因?yàn)閮蓚€(gè)動(dòng)作都可通過(guò) fcntl 來(lái)完成, 沒(méi)有人真正使用 FIOASYNC 命令, 它在這里出現(xiàn)只是為了完整性.

FIOQSIZE

這個(gè)命令返回一個(gè)文件或者目錄的大小; 當(dāng)用作一個(gè)設(shè)備文件, 但是, 它返回一個(gè) ENOTTY 錯(cuò)誤.

FIONBIO

"File IOctl Non-Blocking I/O"(在"阻塞和非阻塞操作"一節(jié)中描述). 這個(gè)調(diào)用修改在 filp->f_flags 中的 O_NONBLOCK 標(biāo)志. 給這個(gè)系統(tǒng)調(diào)用的第 3 個(gè)參數(shù)用作指示是否這個(gè)標(biāo)志被置位或者清除. (我們將在本章看到這個(gè)標(biāo)志的角色). 注意常用的改變這個(gè)標(biāo)志的方法是使用 fcntl 系統(tǒng)調(diào)用, 使用 F_SETFL 命令.

列表中的最后一項(xiàng)介紹了一個(gè)新的系統(tǒng)調(diào)用, fcntl, 它看來(lái)象 ioctl. 事實(shí)上, fcntl 調(diào)用非常類似 ioctl, 它也是獲得一個(gè)命令參數(shù)和一個(gè)額外的(可選地)參數(shù). 它保持和 ioctl 獨(dú)立主要是因?yàn)闅v史原因: 當(dāng) Unix 開發(fā)者面對(duì)控制 I/O 操作的問(wèn)題時(shí), 他們決定文件和設(shè)備是不同的. 那時(shí), 有 ioctl 實(shí)現(xiàn)的唯一設(shè)備是 ttys, 它解釋了為什么 -ENOTTY 是標(biāo)準(zhǔn)的對(duì)不正確 ioctl 命令的回答. 事情已經(jīng)改變, 但是 fcntl 保留為一個(gè)獨(dú)立的系統(tǒng)調(diào)用.

6.1.4.?使用 ioctl 參數(shù)

在看 scull 驅(qū)動(dòng)的 ioctl 代碼之前, 我們需要涉及的另一點(diǎn)是如何使用這個(gè)額外的參數(shù). 如果它是一個(gè)整數(shù), 就容易: 它可以直接使用. 如果它是一個(gè)指針, 但是, 必須小心些.

當(dāng)用一個(gè)指針引用用戶空間, 我們必須確保用戶地址是有效的. 試圖存取一個(gè)沒(méi)驗(yàn)證過(guò)的用戶提供的指針可能導(dǎo)致不正確的行為, 一個(gè)內(nèi)核 oops, 系統(tǒng)崩潰, 或者安全問(wèn)題. 它是驅(qū)動(dòng)的責(zé)任來(lái)對(duì)每個(gè)它使用的用戶空間地址進(jìn)行正確的檢查, 并且返回一個(gè)錯(cuò)誤如果它是無(wú)效的.

在第 3 章, 我們看了 copy_from_user 和 copy_to_user 函數(shù), 它們可用來(lái)安全地移動(dòng)數(shù)據(jù)到和從用戶空間. 這些函數(shù)也可用在 ioctl 方法中, 但是 ioctl 調(diào)用常常包含小數(shù)據(jù)項(xiàng), 可通過(guò)其他方法更有效地操作. 開始, 地址校驗(yàn)(不傳送數(shù)據(jù))由函數(shù) access_ok 實(shí)現(xiàn), 它定義在 :

int access_ok(int type, const void *addr, unsigned long size); 

第一個(gè)參數(shù)應(yīng)當(dāng)是 VERIFY_READ 或者 VERIFY_WRITE, 依據(jù)這個(gè)要進(jìn)行的動(dòng)作是否是讀用戶空間內(nèi)存區(qū)或者寫它. addr 參數(shù)持有一個(gè)用戶空間地址, size 是一個(gè)字節(jié)量. 例如, 如果 ioctl 需要從用戶空間讀一個(gè)整數(shù), size 是 sizeof(int). 如果你需要讀和寫給定地址, 使用 VERIFY_WRITE, 因?yàn)樗?VERIRY_READ 的超集.

不象大部分的內(nèi)核函數(shù), access_ok 返回一個(gè)布爾值: 1 是成功(存取沒(méi)問(wèn)題)和 0 是失敗(存取有問(wèn)題). 如果它返回假, 驅(qū)動(dòng)應(yīng)當(dāng)返回 -EFAULT 給調(diào)用者.

關(guān)于 access_ok有多個(gè)有趣的東西要注意. 首先, 它不做校驗(yàn)內(nèi)存存取的完整工作; 它只檢查看這個(gè)內(nèi)存引用是在這個(gè)進(jìn)程有合理權(quán)限的內(nèi)存范圍中. 特別地, access_ok 確保這個(gè)地址不指向內(nèi)核空間內(nèi)存. 第2, 大部分驅(qū)動(dòng)代碼不需要真正調(diào)用 access_ok. 后面描述的內(nèi)存存取函數(shù)為你負(fù)責(zé)這個(gè). 但是, 我們來(lái)演示它的使用, 以便你可見(jiàn)到它如何完成.

scull 源碼利用了 ioclt 號(hào)中的位段來(lái)檢查參數(shù), 在 switch 之前:

int err = 0, tmp;
int retval = 0;
/*
 * extract the type and number bitfields, and don't decode
 * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok()
 */
if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC)
        return -ENOTTY;
if (_IOC_NR(cmd) > SCULL_IOC_MAXNR)
        return -ENOTTY;

/*
 * the direction is a bitmask, and VERIFY_WRITE catches R/W
 * transfers. `Type' is user-oriented, while
 * access_ok is kernel-oriented, so the concept of "read" and
 * "write" is reversed
 */
if (_IOC_DIR(cmd) & _IOC_READ)
        err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
else if (_IOC_DIR(cmd) & _IOC_WRITE)
        err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
if (err)
        return -EFAULT;

在調(diào)用 access_ok 之后, 驅(qū)動(dòng)可安全地進(jìn)行真正的傳輸. 加上 copy_from_user 和 copy_touser 函數(shù), 程序員可利用一組為被最多使用的數(shù)據(jù)大小(1, 2, 4, 和 8 字節(jié))而優(yōu)化過(guò)的函數(shù). 這些函數(shù)在下面列表中描述, 它們定義在 :

put_user(datum, ptr)

__put_user(datum, ptr)

這些宏定義寫 datum 到用戶空間; 它們相對(duì)快, 并且應(yīng)當(dāng)被調(diào)用來(lái)代替 copy_to_user 無(wú)論何時(shí)要傳送單個(gè)值時(shí). 這些宏已被編寫來(lái)允許傳遞任何類型的指針到 put_user, 只要它是一個(gè)用戶空間地址. 傳送的數(shù)據(jù)大小依賴 prt 參數(shù)的類型, 并且在編譯時(shí)使用 sizeof 和 typeof 等編譯器內(nèi)建宏確定. 結(jié)果是, 如果 prt 是一個(gè) char 指針, 傳送一個(gè)字節(jié), 以及對(duì)于 2, 4, 和 可能的 8 字節(jié).

put_user 檢查來(lái)確保這個(gè)進(jìn)程能夠?qū)懭虢o定的內(nèi)存地址. 它在成功時(shí)返回 0, 并且在錯(cuò)誤時(shí)返回 -EFAULT. put_user 進(jìn)行更少的檢查(它不調(diào)用 access_ok), 但是仍然能夠失敗如果被指向的內(nèi)存對(duì)用戶是不可寫的. 因此, put_user 應(yīng)當(dāng)只用在內(nèi)存區(qū)已經(jīng)用 access_ok 檢查過(guò)的時(shí)候.

作為一個(gè)通用的規(guī)則, 當(dāng)你實(shí)現(xiàn)一個(gè) read 方法時(shí), 調(diào)用 __put_user 來(lái)節(jié)省幾個(gè)周期, 或者當(dāng)你拷貝幾個(gè)項(xiàng)時(shí), 因此, 在第一次數(shù)據(jù)傳送之前調(diào)用 access_ok 一次, 如同上面 ioctl 所示.

get_user(local, ptr)

__get_user(local, ptr)

這些宏定義用來(lái)從用戶空間接收單個(gè)數(shù)據(jù). 它們象 put_user 和 __put_user, 但是在相反方向傳遞數(shù)據(jù). 獲取的值存儲(chǔ)于本地變量 local; 返回值指出這個(gè)操作是否成功. 再次, __get_user 應(yīng)當(dāng)只用在已經(jīng)使用 access_ok 校驗(yàn)過(guò)的地址.

如果做一個(gè)嘗試來(lái)使用一個(gè)列出的函數(shù)來(lái)傳送一個(gè)不適合特定大小的值, 結(jié)果常常是一個(gè)來(lái)自編譯器的奇怪消息, 例如"coversion to non-scalar type requested". 在這些情況中, 必須使用 copy_to_user 或者 copy_from_user.

6.1.5.?兼容性和受限操作

存取一個(gè)設(shè)備由設(shè)備文件上的許可權(quán)控制, 并且驅(qū)動(dòng)正常地不涉及到許可權(quán)的檢查. 但是, 有些情形, 在保證給任何用戶對(duì)設(shè)備的讀寫許可的地方, 一些控制操作仍然應(yīng)當(dāng)被拒絕. 例如, 不是所有的磁帶驅(qū)動(dòng)器的用戶都應(yīng)當(dāng)能夠設(shè)置它的缺省塊大小, 并且一個(gè)已經(jīng)被給予對(duì)一個(gè)磁盤設(shè)備讀寫權(quán)限的用戶應(yīng)當(dāng)仍然可能被拒絕來(lái)格式化它. 在這樣的情況下, 驅(qū)動(dòng)必須進(jìn)行額外的檢查來(lái)確保用戶能夠進(jìn)行被請(qǐng)求的操作.

傳統(tǒng)上 unix 系統(tǒng)對(duì)超級(jí)用戶帳戶限制了特權(quán)操作. 這意味著特權(quán)是一個(gè)全有-或-全無(wú)的東西 -- 超級(jí)用戶可能任意做任何事情, 但是所有其他的用戶被高度限制了. Linux 內(nèi)核提供了一個(gè)更加靈活的系統(tǒng), 稱為能力. 一個(gè)基于能力的系統(tǒng)丟棄了全有-或全無(wú)模式, 并且打破特權(quán)操作為獨(dú)立的子類. 這種方式, 一個(gè)特殊的用戶(或者是程序)可被授權(quán)來(lái)進(jìn)行一個(gè)特定的特權(quán)操作而不必泄漏進(jìn)行其他的, 無(wú)關(guān)的操作的能力. 內(nèi)核在許可權(quán)管理上排他地使用能力, 并且輸出 2 個(gè)系統(tǒng)調(diào)用 capget 和 capset, 來(lái)允許它們被從用戶空間管理.

全部能力可在 中找到. 這些是對(duì)系統(tǒng)唯一可用的能力; 對(duì)于驅(qū)動(dòng)作者或者系統(tǒng)管理員, 不可能不修改內(nèi)核源碼而來(lái)定義新的. 設(shè)備驅(qū)動(dòng)編寫者可能感興趣的這些能力的一個(gè)子集, 包括下面:

CAP_DAC_OVERRIDE

這個(gè)能力來(lái)推翻在文件和目錄上的存取的限制(數(shù)據(jù)存取控制, 或者 DAC).

CAP_NET_ADMIN

進(jìn)行網(wǎng)絡(luò)管理任務(wù)的能力, 包括那些能夠影響網(wǎng)絡(luò)接口的.

CAP_SYS_MODULE

加載或去除內(nèi)核模塊的能力.

CAP_SYS_RAWIO

進(jìn)行 "raw" I/O 操作的能力. 例子包括存取設(shè)備端口或者直接和 USB 設(shè)備通訊.

CAP_SYS_ADMIN

一個(gè)捕獲-全部的能力, 提供對(duì)許多系統(tǒng)管理操作的存取.

CAP_SYS_TTY_CONFIG

進(jìn)行 tty 配置任務(wù)的能力.

在進(jìn)行一個(gè)特權(quán)操作之前, 一個(gè)設(shè)備驅(qū)動(dòng)應(yīng)當(dāng)檢查調(diào)用進(jìn)程有合適的能力; 不這樣做可能導(dǎo)致用戶進(jìn)程進(jìn)行非法的操作, 對(duì)系統(tǒng)的穩(wěn)定和安全有壞的后果. 能力檢查是通過(guò) capable 函數(shù)來(lái)進(jìn)行的(定義在 ):

 int capable(int capability); 

在 scull 例子驅(qū)動(dòng)中, 任何用戶被許可來(lái)查詢 quantum 和 quantum 集的大小. 只有特權(quán)用戶, 但是, 可改變這些值, 因?yàn)椴贿m當(dāng)?shù)闹悼赡芎軌牡赜绊懴到y(tǒng)性能. 當(dāng)需要時(shí), ioctl 的 scull 實(shí)現(xiàn)檢查用戶的特權(quán)級(jí)別, 如下:

 if (! capable (CAP_SYS_ADMIN))
 return -EPERM;

在這個(gè)任務(wù)缺乏一個(gè)更加特定的能力時(shí), CAP_SYS_ADMIN 被選擇來(lái)做這個(gè)測(cè)試.

6.1.6.?ioctl 命令的實(shí)現(xiàn)

ioctl 的 scull 實(shí)現(xiàn)只傳遞設(shè)備的配置參數(shù), 并且象下面這樣容易:

switch(cmd)
{
case SCULL_IOCRESET:
        scull_quantum = SCULL_QUANTUM;
        scull_qset = SCULL_QSET;
        break;

case SCULL_IOCSQUANTUM: /* Set: arg points to the value */
        if (! capable (CAP_SYS_ADMIN))

                return -EPERM;
        retval = __get_user(scull_quantum, (int __user *)arg);
        break;

case SCULL_IOCTQUANTUM: /* Tell: arg is the value */
        if (! capable (CAP_SYS_ADMIN))

                return -EPERM;
        scull_quantum = arg;
        break;

case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */
        retval = __put_user(scull_quantum, (int __user *)arg);
        break;

case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */
        return scull_quantum;

case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */
        if (! capable (CAP_SYS_ADMIN))

                return -EPERM;
        tmp = scull_quantum;
        retval = __get_user(scull_quantum, (int __user *)arg);
        if (retval == 0)

                retval = __put_user(tmp, (int __user *)arg);
        break;

case SCULL_IOCHQUANTUM: /* sHift: like Tell + Query */
        if (! capable (CAP_SYS_ADMIN))
                return -EPERM;
        tmp = scull_quantum;
        scull_quantum = arg;
        return tmp;

default: /* redundant, as cmd was checked against MAXNR */
        return -ENOTTY;
}
return retval;

scull 還包含 6 個(gè)入口項(xiàng)作用于 scull_qset. 這些入口項(xiàng)和給 scull_quantum 的是一致的, 并且不值得展示出來(lái).

從調(diào)用者的觀點(diǎn)看(即從用戶空間), 這 6 種傳遞和接收參數(shù)的方法看來(lái)如下:

int quantum;
ioctl(fd,SCULL_IOCSQUANTUM, &quantum);  /* Set by pointer */
ioctl(fd,SCULL_IOCTQUANTUM, quantum);  /* Set by value */
ioctl(fd,SCULL_IOCGQUANTUM, &quantum);  /* Get by pointer */
quantum = ioctl(fd,SCULL_IOCQQUANTUM);  /* Get by return value */
ioctl(fd,SCULL_IOCXQUANTUM, &quantum);  /* Exchange by pointer */

quantum = ioctl(fd,SCULL_IOCHQUANTUM, quantum); /* Exchange by value */

當(dāng)然, 一個(gè)正常的驅(qū)動(dòng)不可能實(shí)現(xiàn)這樣一個(gè)調(diào)用模式的混合體. 我們這里這樣做只是為了演示做事情的不同方式. 但是, 正常地, 數(shù)據(jù)交換將一致地進(jìn)行, 通過(guò)指針或者通過(guò)值, 并且要避免混合這 2 種技術(shù).

6.1.7.?不用 ioctl 的設(shè)備控制

有時(shí)控制設(shè)備最好是通過(guò)寫控制序列到設(shè)備自身來(lái)實(shí)現(xiàn). 例如, 這個(gè)技術(shù)用在控制臺(tái)驅(qū)動(dòng)中, 這里所謂的 escape 序列被用來(lái)移動(dòng)光標(biāo), 改變?nèi)笔〉念伾? 或者進(jìn)行其他的配置任務(wù). 這樣實(shí)現(xiàn)設(shè)備控制的好處是用戶可僅僅通過(guò)寫數(shù)據(jù)控制設(shè)備, 不必使用(或者有時(shí)候?qū)?只為配置設(shè)備而建立的程序. 當(dāng)設(shè)備可這樣來(lái)控制, 發(fā)出命令的程序甚至常常不需要運(yùn)行在和它要控制的設(shè)備所在的同一個(gè)系統(tǒng)上.

例如, setterm 程序作用于控制臺(tái)(或者其他終端)配置, 通過(guò)打印 escape 序列. 控制程序可位于和被控制的設(shè)備不同的一臺(tái)計(jì)算機(jī)上, 因?yàn)橐粋€(gè)簡(jiǎn)單的數(shù)據(jù)流重定向可完成這個(gè)配置工作. 這是每次你運(yùn)行一個(gè)遠(yuǎn)程 tty 會(huì)話時(shí)所發(fā)生的事情: escape 序列在遠(yuǎn)端被打印但是影響到本地的 tty; 然而, 這個(gè)技術(shù)不局限于 ttys.

通過(guò)打印來(lái)控制的缺點(diǎn)是它給設(shè)備增加了策略限制; 例如, 它僅僅當(dāng)你確信在正常操作時(shí)控制序列不會(huì)出現(xiàn)在正被寫入設(shè)備的數(shù)據(jù)中. 這對(duì)于 ttys 只是部分正確的. 盡管一個(gè)文本顯示意味著只顯示 ASCII 字符, 有時(shí)控制字符可潛入正被寫入的數(shù)據(jù)中, 并且可能, 因此, 影響控制臺(tái)的配置. 例如, 這可能發(fā)生在你顯示一個(gè)二進(jìn)制文件到屏幕時(shí); 產(chǎn)生的亂碼可能包含任何東西, 并且最后你常常在你的控制臺(tái)上出現(xiàn)錯(cuò)誤的字體.

通過(guò)寫來(lái)控制是當(dāng)然的使用方法了, 對(duì)于不用傳送數(shù)據(jù)而只是響應(yīng)命令的設(shè)備, 例如遙控設(shè)備.

例如, 被你們作者當(dāng)中的一個(gè)編寫來(lái)好玩的驅(qū)動(dòng), 移動(dòng)一個(gè) 2 軸上的攝像機(jī). 在這個(gè)驅(qū)動(dòng)里, 這個(gè)"設(shè)備"是一對(duì)老式步進(jìn)電機(jī), 它們不能真正讀或?qū)? 給一個(gè)步進(jìn)電機(jī)"發(fā)送數(shù)據(jù)流"的概念沒(méi)有任何意義. 在這個(gè)情況下, 驅(qū)動(dòng)解釋正被寫入的數(shù)據(jù)作為 ASCII 命令并且轉(zhuǎn)換這個(gè)請(qǐng)求為脈沖序列, 來(lái)操縱步進(jìn)電機(jī). 這個(gè)概念類似于, 有些, 你發(fā)給貓的 AT 命令來(lái)建立通訊, 主要的不同是和貓通訊的串口必須也傳送真正的數(shù)據(jù). 直接設(shè)備控制的好處是你可以使用 cat 來(lái)移動(dòng)攝像機(jī), 而不必寫和編譯特殊的代碼來(lái)發(fā)出 ioctl 調(diào)用.

當(dāng)編寫面向命令的驅(qū)動(dòng), 沒(méi)有理由實(shí)現(xiàn) ioctl 命令. 一個(gè)解釋器中的額外命令更容易實(shí)現(xiàn)并使用.

有時(shí), 然而, 你可能選擇使用其他的方法:不必轉(zhuǎn)變 write 方法為一個(gè)解釋器和避免 ioctl, 你可能選擇完全避免寫并且專門使用 ioctl 命令, 而實(shí)現(xiàn)驅(qū)動(dòng)為使用一個(gè)特殊的命令行工具來(lái)發(fā)送這些命令到驅(qū)動(dòng). 這個(gè)方法轉(zhuǎn)移復(fù)雜性從內(nèi)核空間到用戶空間, 這里可能更易處理, 并且?guī)椭3烛?qū)動(dòng)小, 而拒絕使用簡(jiǎn)單的 cat 或者 echo 命令.


[20]?但是, 這個(gè)文件的維護(hù)在后來(lái)有些少見(jiàn)了.

[21]?實(shí)際上, 所有的當(dāng)前使用的 libc 實(shí)現(xiàn)(包括 uClibc) 僅將 -4095 到 -1 的值當(dāng)作錯(cuò)誤碼. 不幸的是, 能夠返回大的負(fù)數(shù)而不是小的, 沒(méi)有多大用處.

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)