ioctl

ioctl

ioctl是設備驅動程序中對設備的I/O通道進行管理的函數。所謂對I/O通道進行管理,就是對設備的一些特性進行控制,例如串口的傳輸波特率、馬達的轉速等等。它的參數個數如下:int ioctl(int fd, int cmd, …);其中fd就是用戶程序打開設備時使用open函數返回的文件標示符,cmd就是用戶程序對設備的控制命令,至於後面的省略號,那是一些補充參數,一般最多一個,有或沒有是和cmd的意義相關的。ioctl函數是文件結構中的一個屬性分量,就是說如果你的驅動程序提供了對ioctl的支持,用戶就能在用戶程序中使用ioctl函數控制設備的I/O通道。

功能


控制I/O設備,提供了一種獲得設備信息和向設備發送控制參數的手段。用於向設備發控制和配置命令,有些命令需要控制參數,這些數據是不能用read / write 讀寫的,稱為Out-of-band數據。也就是說,read / write 讀寫的數據是in-band數據,是I/O操作的主體,而ioctl 命令傳送的是控制信息,其中的數據是輔助的數據。
用 法: int ioctl(int handle, int cmd,[int *argdx, int argcx]);
返回值:成功為0,出錯為-1
usr/include/asm-generic/ioctl.h中定義的宏的註釋:
#define _IOC_NRBITS 8 //序數(number)欄位的字位寬度,8bits
#define _IOC_TYPEBITS 8 //幻數(type)欄位的字位寬度,8bits
#define _IOC_SIZEBITS 14 //大小(size)欄位的字位寬度,14bits
#define _IOC_DIRBITS 2 //方向(direction)欄位的字位寬度,2bits
#define _IOC_NRMASK ((1 << _IOC_NRBITS)-1) //序數欄位的掩碼,0x000000FF
#define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1) //幻數欄位的掩碼,0x000000FF
#define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1) //大小欄位的掩碼,0x00003FFF
#define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1) //方向欄位的掩碼,0x00000003
#define _IOC_NRSHIFT 0 //序數欄位在整個欄位中的位移,0
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS) //幻數欄位的位移,8
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS) //大小欄位的位移,16
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS) //方向欄位的位移,30
#define _IOC_NONE 0U //沒有數據傳輸
#define _IOC_WRITE 1U //向設備寫入數據,驅動程序必須從用戶空間讀入數據
#define _IOC_READ 2U //從設備中讀取數據,驅動程序必須向用戶空間寫入數據
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
//構造無參數的命令編號
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
//構造從驅動程序中讀取數據的命令編號
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
//用於向驅動程序寫入數據命令
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
//用於雙向傳輸
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
//從命令參數中解析出數據方向,即寫進還是讀出
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
//從命令參數中解析出幻數type
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
//從命令參數中解析出序數number
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
//從命令參數中解析出用戶數據大小
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
#define IOC_IN (_IOC_WRITE << _IOC_DIRSHIFT)
#define IOC_OUT (_IOC_READ << _IOC_DIRSHIFT)
#define IOC_INOUT ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
#define IOCSIZE_MASK (_IOC_SIZEMASK << _IOC_SIZESHIFT)
#define IOCSIZE_SHIFT (_IOC_SIZESHIFT)
程序例:
#include
#include
#include
int main(void) {
..int stat;
..stat = ioctl(0, 8, 0, 0);
..if (!stat)
....printf("Drive %c is removable.\n", getdisk() + 'A');
..else
....printf("Drive %c is not removable.\n", getdisk() + 'A');
..return 0;
}
int ioctl( int fd, int request, ... ) 詳解
第三個參數總是一個指針,但指針的類型依賴於request 參數。我們可以把和網路相關的請求劃分為6 類:
套介面操作
文件操作
介面操作
ARP 高速緩存操作
路由表操作
流系統
下表列出了網路相關ioctl請求的request 參數以及arg 地址必須指向的數據類型:
類別Request說明數據類型
SIOCATMARK
SIOCSPGRP
SIOCGPGRP
是否位於帶外標記
設置套介面的進程ID 或進程組ID
獲取套介面的進程ID 或進程組ID
int
int
int
FIONBIO
FIOASYNC
FIONREAD
FIOSETOWN
FIOGETOWN
設置/ 清除非阻塞I/O 標誌
設置/ 清除信號驅動非同步I/O 標誌
獲取接收緩存區中的位元組數
設置文件的進程ID 或進程組ID
獲取文件的進程ID 或進程組ID
int
int
int
int
int
SIOCGIFCONF
SIOCSIFADDR
SIOCGIFADDR
SIOCSIFFLAGS
SIOCGIFFLAGS
SIOCSIFDSTADDR
SIOCGIFDSTADDR
SIOCGIFBRDADDR
SIOCSIFBRDADDR
SIOCGIFNETMASK
SIOCSIFNETMASK
SIOCGIFMETRIC
SIOCSIFMETRIC
SIOCGIFMTU
SIOCxxx
獲取所有介面的清單
設置介面地址
獲取介面地址
設置介面標誌
獲取介面標誌
設置點到點地址
獲取點到點地址
獲取廣播地址
設置廣播地址
獲取子網掩碼
設置子網掩碼
獲取介面的測度
設置介面的測度
獲取介面MTU
(還有很多取決於系統的實現)
struct ifconf
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
struct ifreq
ARP
SIOCSARP
SIOCGARP
SIOCDARP
創建/ 修改ARP 表項
獲取ARP 表項
刪除ARP 表項
struct arpreq
struct arpreq
struct arpreq
SIOCADDRT
SIOCDELRT
SIOCRTMSG
增加路徑
刪除路徑
獲取路由表
struct rtentry
struct rtentry
struct rtentry
I_xxx
套介面操作:
明確用於套介面操作的ioctl請求有三個, 它們都要求ioctl的第三個參數是指向某個整數的一個指針。
SIOCATMARK: 如果本套介面的的度指針當前位於帶外標記,那就通過由第三個參數指向的整數返回一個非0 值;否則返回一個0 值。POSIX 以函數sockatmark 替換本請求。
SIOCGPGRP :通過第三個參數指向的整數返回本套介面的進程ID 或進程組ID ,該ID 指定針對本套介面的SIGIO 或SIGURG 信號的接收進程。本請求和fcntl 的F_GETOWN 命令等效,POSIX 標準化的是fcntl 函數。
SIOCSPGRP :把本套介面的進程ID 或者進程組ID 設置成第三個參數指向的整數,該ID 指定針對本套介面的SIGIO 或SIGURG 信號的接收進程,本請求和fcntl 的F_SETOWN 命令等效,POSIX 標準化的是fcntl 操作。
文件操作:
以下5 個請求都要求ioctl的第三個參數指向一個整數。
FIONBIO :根據ioctl的第三個參數指向一個0 或非0 值分別清除或設置本套介面的非阻塞標誌。本請求和O_NONBLOCK 文件狀態標誌等效,而該標誌通過fcntl 的F_SETFL 命令清除或設置。
FIOASYNC :根據ioctl 的第三個參數指向一個0 值或非0 值分別清除或設置針對本套介面的信號驅動非同步I/O 標誌,它決定是否收取針對本套介面的非同步I/O 信號(SIGIO )。本請求和O_ASYNC 文件狀態標誌等效,而該標誌可以通過fcntl 的F_SETFL 命令清除或設置。
FIONREAD :通過由ioctl的第三個參數指向的整數返回當前在本套介面接收緩衝區中的位元組數。本特性同樣適用於文件,管道和終端。
FIOSETOWN :對於套介面和SIOCSPGRP 等效。
FIOGETOWN :對於套介面和SIOCGPGRP 等效。

必要性


如果不用IOCTL的話,也能實現對設備I/O通道的控制,但那就是蠻擰了。例如,我們可以在驅動程式中實現WRITE的時候檢查一下是否有特別約定的數據流通過,如果有的話,那麼後面就跟著控制命令(一般在SOCKET編程中常常這樣做)。不過如果這樣做的話,會導致代碼分工不明,程式結構混亂,程式員自己也會頭昏眼花的。所以,我們就使用IOCTL來實現控制的功能。要記住,用戶程式所作的只是通過命令碼告訴驅動程式他想做什麼,至於怎麼解釋這些命令和怎麼實現這些命令,這都是驅動程式要做的事情。

實現操作


讀者只要把write換成ioctl,就知道用戶程式的ioctl是怎麼和驅動程式中的ioctl實現聯繫在一起的了。我這裡說一個大概思路,因為我覺得《Linux設備驅動程序》這本書已說的非常清晰了,不過得花一些時間來看。在驅動程式中實現的ioctl函數體內,實際上是有一個switch{case}結構,每一個case對應一個命令碼,做出一些相應的操作。怎麼實現這些操作,這是每一個程式員自己的事情,因為設備都是特定的,這裡也沒法說。關鍵在於怎麼樣組織命令碼,因為在ioctl中命令碼是唯一聯繫用戶程式命令和驅動程式支持的途徑。命令碼的組織是有一些講究的,因為我們一定要做到命令和設備是一一對應的,這樣才不會將正確的命令發給錯誤的設備,或是把錯誤的命令發給正確的設備,或是把錯誤的命令發給錯誤的設備。這些錯誤都會導致不可預料的事情發生,而當程式員發現了這些奇怪的事情的時候,再來調試程式查找錯誤,那將是非常困難的事情。所以在Linux核心中是這樣定義一個命令碼的:
____________________________________
| 設備類型 | 序列號 | 方向 |數據尺寸|
|----------|--------|------|--------|
| 8 bit | 8 bit |2 bit |8~14 bit|
|----------|--------|------|-------|
這樣一來,一個命令就變成了一個整數形式的命令碼。不過命令碼非常的不直觀,所以Linux Kernel中提供了一些宏,這些宏可根據便於理解的字元串生成命令碼,或是從命令碼得到一些用戶能理解的字元串以標明這個命令對應的設備類型、設備序列號、數據傳送方向和數據傳輸尺寸。這些宏我就不在這裡解釋了,具體的形式請讀者察看Linux核心原始碼中的和,文件里給除了這些宏完整的定義。這裡我只多說一個地方,那就是"幻數"。幻數是個字母,數據長度也是8,所以就用一個特定的字母來標明設備類型,這和用一個數字是相同的,只是更加利於記憶和理解。就是這樣,再沒有更複雜的了。更多的說了也沒有,讀者還是看一看原始碼吧,推薦各位閱讀《Linux 設備驅動程序》所帶原始碼中的short一例,因為他比較短小,功能比較簡單,能看明白ioctl的功能和細節。

其他信息


cmd參數怎麼得出?
這裡確實要說一說,cmd參數在用戶程式端由一些宏根據設備類型、序列號、傳送方向、數據尺寸等生成,這個整數通過系統調用傳遞到內核中的驅動程式,再由驅動程式使用解碼宏從這個整數中得到設備的類型、序列號、傳送方向、數據尺寸等信息,然後通過switch{case}結構進行相應的操作。要透徹理解,只能是通過閱讀原始碼,我這篇文章實際上只是個引子。Cmd參數的組織還是比較複雜的,我認為要搞熟他還是得花不少時間的,不過這是值得的,驅動程式中最難的是對中斷的理解。

總結


ioctl其實沒有什麼非常難的東西需要理解,關鍵是理解cmd命令碼是怎麼在用戶程序里生成並在驅動程序里解析的,程序員最主要的工作量在switch{case}結構中,因為對設備的I/O控制都是通過這一部分的代碼實現的。