1. 程式人生 > >學習linux字元裝置驅動心得

學習linux字元裝置驅動心得

一、主裝置號和此裝置號
主裝置號表示裝置對應的驅動程式;次裝置號由核心使用,用於正確確定裝置檔案所指的裝置。核心用dev_t型別(<linux/types.h>)來儲存裝置編號,dev_t是一個32位的數,12位表示主裝置號,20為表示次裝置號。在實際使用中,是通過<linux/kdev_t.h>中定義的巨集來轉換格式。

(dev_t)-->主裝置號、次裝置號

MAJOR(dev_t dev)
MINOR(dev_t dev)

主裝置號、次裝置號-->(dev_t)

MKDEV(int major,int minor)

建立一個字元裝置之前,驅動程式首先要做的事情就是獲得裝置編號

。其這主要函式在<linux/fs.h>中宣告:

int register_chrdev_region(dev_t first,unsigned int count,
char *name); //
指定裝置編號

int alloc_chrdev_region(dev_t*dev,unsigned int firstminor,
unsigned int count,char *name); //
動態生成裝置編號

void unregister_chrdev_region(dev_t first,unsigned int count); //
釋放裝置編號


分配之裝置號的最佳方式是:預設採用動態分配,同時保留在載入甚至是編譯時指定主裝置號的餘地。

以下是在scull.c中用來獲取主裝置好的程式碼:

if(scull_major){
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs,"scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,"scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}

在這部分中,比較重要的是在用函式獲取裝置編號後,其中的引數name是和該編號範圍關聯的裝置名稱,它將出現在/proc/devicessysfs中。看到這裡,就可以理解為什麼mdevudev可以動態、自動地生成當前系統需要的裝置檔案。udev就是通過讀取sysfs下的資訊來識別硬體裝置的.
(
請看《理解和認識udev
URL
http://blog.chinaunix.net/u/6541/showart_396425.html)

二、一些重要的資料結構大部分基本的驅動程式操作涉及及到三個重要的核心資料結構,分別是file_operationsfileinode,它們的定義都在<linux/fs.h>

file_operations

是一個函式指標的集合,標記式初始化允許結構成員重新排序; 在某種情況下, 真實的效能提高已經實現, 通過安放經常使用的成員的指標在相同硬體高速儲存行中.

struct module *owner

第一個 file_operations 成員根本不是一個操作; 它是一個指向擁有這個結構的模組的指標. 這個成員用來在它的操作還在被使用時阻止模組被解除安裝. 幾乎所有時間中, 它被簡單初始化為THIS_MODULE, 一個在 <linux/module.h> 中定義的巨集.

loff_t (*llseek) (struct file *, loff_t, int);

llseek 方法用作改變檔案中的當前讀/寫位置, 並且新位置作為(正的)返回值. loff_t 引數是一個"long offset", 並且就算在 32位平臺上也至少 64 位寬. 錯誤由一個負返回值指示. 如果這個函式指標是 NULL, seek 呼叫會以潛在地無法預知的方式修改 file 結構中的位置計數器( 在"file 結構" 一節中描述).

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

用來從裝置中獲取資料. 在這個位置的一個空指標導致 read 系統呼叫以 -EINVAL("Invalid argument") 失敗. 一個非負返回值代表了成功讀取的位元組數( 返回值是一個 "signed size" 型別, 常常是目標平臺本地的整數型別).

ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);

初始化一個非同步讀 -- 可能在函式返回前不結束的讀操作. 如果這個方法是 NULL, 所有的操作會由 read 代替進行(同步地).

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

傳送資料給裝置. 如果 NULL, -EINVAL 返回給呼叫 write 系統呼叫的程式. 如果非負, 返回值代表成功寫的位元組數.

ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);

初始化裝置上的一個非同步寫.

int (*readdir) (struct file *, void *, filldir_t);

對於裝置檔案這個成員應當為 NULL; 它用來讀取目錄, 並且僅對檔案系統有用.

unsigned int (*poll) (struct file *, struct poll_table_struct *);

poll 方法是 3 個系統呼叫的後端: poll, epoll, 和 select, 都用作查詢對一個或多個檔案描述符的讀或寫是否會阻塞. poll 方法應當返回一個位掩碼指示是否非阻塞的讀或寫是可能的, 並且, 可能地, 提供給核心資訊用來使呼叫程序睡眠直到 I/O 變為可能. 如果一個驅動的 poll 方法為 NULL, 裝置假定為不阻塞地可讀可寫.

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

ioctl 系統呼叫提供了發出裝置特定命令的方法(例如格式化軟盤的一個磁軌, 這不是讀也不是寫). 另外, 幾個 ioctl 命令被核心識別而不必引用 fops 表. 如果裝置不提供 ioctl 方法, 對於任何未事先定義的請求(-ENOTTY, "裝置無這樣的 ioctl"), 系統呼叫返回一個錯誤.

int (*mmap) (struct file *, struct vm_area_struct *);

mmap 用來請求將裝置記憶體對映到程序的地址空間. 如果這個方法是 NULL, mmap 系統呼叫返回 -ENODEV.

int (*open) (struct inode *, struct file *);

儘管這常常是對裝置檔案進行的第一個操作, 不要求驅動宣告一個對應的方法. 如果這個項是 NULL, 裝置開啟一直成功, 但是你的驅動不會得到通知.

int (*flush) (struct file *);

flush 操作在程序關閉它的裝置檔案描述符的拷貝時呼叫; 它應當執行(並且等待)裝置的任何未完成的操作. 這個必須不要和使用者查詢請求的 fsync 操作混淆了. 當前,flush 在很少驅動中使用; SCSI 磁帶驅動使用它, 例如, 為確保所有寫的資料在裝置關閉前寫到磁帶上. 如果 flush 為 NULL, 核心簡單地忽略使用者應用程式的請求.

int (*release) (struct inode *, struct file *);

在檔案結構被釋放時引用這個操作. 如同 open, release 可以為 NULL.

int (*fsync) (struct file *, struct dentry *, int);

這個方法是 fsync 系統呼叫的後端, 使用者呼叫來重新整理任何掛著的資料. 如果這個指標是 NULL, 系統呼叫返回 -EINVAL.

int (*aio_fsync)(struct kiocb *, int);

這是 fsync 方法的非同步版本.

int (*fasync) (int, struct file *, int);

這個操作用來通知裝置它的 FASYNC 標誌的改變. 非同步通知是一個高階的主題, 在第 6 章中描述. 這個成員可以是NULL 如果驅動不支援非同步通知.

int (*lock) (struct file *, int, struct file_lock *);

lock 方法用來實現檔案加鎖; 加鎖對常規檔案是必不可少的特性, 但是裝置驅動幾乎從不實現它.

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

這些方法實現發散/匯聚讀和寫操作. 應用程式偶爾需要做一個包含多個記憶體區的單個讀或寫操作; 這些系統呼叫允許它們這樣做而不必對資料進行額外拷貝. 如果這些函式指標為 NULL, read 和 write 方法被呼叫( 可能多於一次 ).

ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);

這個方法實現 sendfile 系統呼叫的讀, 使用最少的拷貝從一個檔案描述符搬移資料到另一個. 例如, 它被一個需要傳送檔案內容到一個網路連線的 web 伺服器使用. 裝置驅動常常使 sendfile 為 NULL.

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

sendpage 是 sendfile 的另一半; 它由核心呼叫來發送資料, 一次一頁, 到對應的檔案. 裝置驅動實際上不實現 sendpage.

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

這個方法的目的是在程序的地址空間找一個合適的位置來對映在底層裝置上的記憶體段中. 這個任務通常由記憶體管理程式碼進行; 這個方法存在為了使驅動能強制特殊裝置可能有的任何的對齊請求. 大部分驅動可以置這個方法為 NULL.[10 ]

int (*check_flags)(int)

這個方法允許模組檢查傳遞給 fnctl(F_SETFL...) 呼叫的標誌.

int (*dir_notify)(struct file *, unsigned long);

這個方法在應用程式使用 fcntl 來請求目錄改變通知時呼叫. 只對檔案系統有用; 驅動不需要實現 dir_notify.

struct file_operations scull_fops = {

.owner = THIS_MODULE,

.llseek = scull_llseek,

.read = scull_read,

.write = scull_write,

.ioctl = scull_ioctl,

.open = scull_open,

.release = scull_release,

};

檔案結構


struct file, 定義於 <linux/fs.h>, 是裝置驅動中第二個最重要的資料結構. 注意 file 與使用者空間程式的 FILE 指標沒有任何關係. 一個 FILE 定義在 C 庫中, 從不出現在核心程式碼中. 一個 struct file, 另一方面, 是一個核心結構, 從不出現在使用者程式中.

在核心原始碼中, struct file 的指標常常稱為 file 或者 filp("file pointer"). 我們將一直稱這個指標為 filp 以避免和結構自身混淆. 因此, file 指的是結構, 而 filp 是結構指標.


struct file 的最重要成員在這展示. 如同在前一節, 第一次閱讀可以跳過這個列表. 但是, 在本章後面, 當我們面對一些真實 C 程式碼時, 我們將更詳細討論這些成員.

mode_t f_mode;

檔案模式確定檔案是可讀的或者是可寫的(或者都是), 通過位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函式中檢查這個成員的讀寫許可, 但是你不需要檢查讀寫許可, 因為核心在呼叫你的方法之前檢查. 當檔案還沒有為那種存取而開啟時讀或寫的企圖被拒絕, 驅動甚至不知道這個情況.

loff_t f_pos;

當前讀寫位置. loff_t 在所有平臺都是 64 位( 在 gcc 術語裡是 long long ). 驅動可以讀這個值, 如果它需要知道檔案中的當前位置, 但是正常地不應該改變它; 讀和寫應當使用它們作為最後引數而收到的指標來更新一個位置, 代替直接作用於 filp->f_pos. 這個規則的一個例外是在 llseek 方法中, 它的目的就是改變檔案位置.

unsigned int f_flags;

這些是檔案標誌, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驅動應當檢查 O_NONBLOCK 標誌來看是否是請求非阻塞操作( 我們在第一章的"阻塞和非阻塞操作"一節中討論非阻塞 I/O ); 其他標誌很少使用. 特別地, 應當檢查讀/寫許可, 使用 f_mode 而不是 f_flags. 所有的標誌在標頭檔案 <linux/fcntl.h> 中定義.

struct file_operations *f_op;

和檔案關聯的操作. 核心安排指標作為它的 open 實現的一部分, 接著讀取它當它需要分派任何的操作時. filp->f_op 中的值從不由核心儲存為後面的引用; 這意味著你可改變你的檔案關聯的檔案操作, 在你返回呼叫者之後新方法會起作用. 例如, 關聯到主編號 1 (/dev/null, /dev/zero, 等等)的 open 程式碼根據開啟的次編號來替代 filp->f_op 中的操作. 這個做法允許實現幾種行為, 在同一個主編號下而不必在每個系統呼叫中引入開銷. 替換檔案操作的能力是面向物件程式設計的"方法過載"的核心對等體.

void *private_data;

open 系統呼叫設定這個指標為 NULL, 在為驅動呼叫 open 方法之前. 你可自由使用這個成員或者忽略它; 你可以使用這個成員來指向分配的資料, 但是接著你必須記住在核心銷燬檔案結構之前, 在 release 方法中釋放那個記憶體.private_data 是一個有用的資源, 在系統呼叫間保留狀態資訊, 我們大部分例子模組都使用它.

struct dentry *f_dentry;

關聯到檔案的目錄入口( dentry )結構. 裝置驅動編寫者正常地不需要關心 dentry 結構, 除了作為 filp->f_dentry->d_inode 存取 inode 結構.


真實結構有多幾個成員, 但是它們對裝置驅動沒有用處. 我們可以安全地忽略這些成員, 因為驅動從不建立檔案結構; 它們真實存取別處建立的結構.

inode 結構


inode 結構由核心在內部用來表示檔案. 因此, 它和代表開啟檔案描述符的檔案結構是不同的. 可能有代表單個檔案的多個開啟描述符的許多檔案結構, 但是它們都指向一個單個 inode 結構.


inode 結構包含大量關於檔案的資訊. 作為一個通用的規則, 這個結構只有 2 個成員對於編寫驅動程式碼有用:

dev_t i_rdev;

對於代表裝置檔案的節點, 這個成員包含實際的裝置編號.

struct cdev *i_cdev;

struct cdev 是核心的內部結構, 代表字元裝置; 這個成員包含一個指標, 指向這個結構, 當節點指的是一個字元裝置檔案時.


i_rdev 型別在 2.5 開發系列中改變了, 破壞了大量的驅動. 作為一個鼓勵更可移植程式設計的方法, 核心開發者已經增加了 2 個巨集, 可用來從一個 inode 中獲取主次編號:

unsigned int iminor(struct inode *inode);

unsigned int imajor(struct inode *inode);


為了不要被下一次改動抓住, 應當使用這些巨集代替直接操作 i_rdev.

三、字元裝置的註冊核心內部使用struct cdev結構來表示字元裝置。在核心呼叫裝置的操作之前,必須分配並註冊一個或多個struct cdev。程式碼應包含<linux/cdev.h>,它定義了struct cdev以及與其相關的一些輔助函式。

註冊一個獨立的cdev裝置的基本過程如下:

1、為struct cdev 分配空間(如果已經將struct cdev 嵌入到自己的裝置的特定結構體中,並分配了空間,這步略過!)

struct cdev *my_cdev = cdev_alloc();

my_cdev->ops=&my_ops;

2、初始化struct cdev

void cdev_init(struct cdev *cdev, const struct file_operations *fops)

3、初始化cdev.owner

cdev.owner = THIS_MODULE;

4、cdev設定完成,通知核心struct cdev的資訊(在執行這步之前必須確定你對struct cdev的以上設定已經完成!

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

這裡, dev 是 cdev 結構, num 是這個裝置響應的第一個裝置號, count 是應當關聯到裝置的裝置號的數目. 常常 count 是 1, 但是有多個裝置號對應於一個特定的裝置的情形. 例如, 設想 SCSI 磁帶驅動, 它允許使用者空間來選擇操作模式(例如密度), 通過安排多個次編號給每一個物理裝置.

在使用 cdev_add 是有幾個重要事情要記住. 第一個是這個呼叫可能失敗. 如果它返回一個負的錯誤碼, 你的裝置沒有增加到系統中. 它幾乎會一直成功, 但是, 並且帶起了其他的點: cdev_add 一返回, 你的裝置就是"活的"並且核心可以呼叫它的操作.除非你的驅動完全準備好處理裝置上的操作, 你不應當呼叫 cdev_add.

5、從系統中移除一個字元裝置:void cdev_del(struct cdev *p)

以下是scull中的初始化程式碼(之前已經為struct scull_dev分配了空間):

/*
* Set up the char_dev structure for this device.
*/
static void scull_setup_cdev(struct scull_dev*dev,int index)
{
int err, devno= MKDEV(scull_major, scull_minor+ index);

cdev_init(&dev->cdev,&scull_fops);
dev->cdev.owner= THIS_MODULE;
dev->cdev.ops= &scull_fops; //
這句可以省略,在cdev_init中已經做過
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be
這步值得注意*/
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}

老方法

如果你深入瀏覽 2.6 核心的大量驅動程式碼, 你可能注意到有許多字元驅動不使用我們剛剛描述過的 cdev 介面. 你見到的是還沒有更新到 2.6 核心介面的老程式碼. 因為那個程式碼實際上能用, 這個更新可能很長時間不會發生. 為完整, 我們描述老的字元設備註冊介面, 但是新程式碼不應當使用它; 這個機制在將來核心中可能會消失.

註冊一個字元裝置的經典方法是使用:

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

這裡, major 是感興趣的主編號, name 是驅動的名子(出現在 /proc/devices), fops 是預設的 file_operations 結構. 一個對 register_chrdev 的呼叫為給定的主編號註冊 0 - 255 的次編號, 並且為每一個建立一個預設的 cdev 結構. 使用這個介面的驅動必須準備好處理對所有 256 個次編號的 open 呼叫( 不管它們是否對應真實裝置 ), 它們不能使用大於 255 的主或次編號.

如果你使用 register_chrdev, 從系統中去除你的裝置的正確的函式是:

int unregister_chrdev(unsigned int major, const char *name);

major 和 name 必須和傳遞給 register_chrdev 的相同, 否則呼叫會失敗.

四、scull模型的記憶體使用

以下是scull模型的結構體:

/*
* Representation of scull quantum sets.
*/
struct scull_qset {
void **data;
struct scull_qset *next;
};

struct scull_dev {
struct scull_qset *data;/* Pointer to first quantum set */
int quantum;/* the current quantum size */
int qset;/* the current array size */
unsigned long size;/* amount of data stored here */
unsigned int access_key;/* used by sculluid and scullpriv */
struct semaphore sem;/* mutual exclusion semaphore */
struct cdev cdev;/* Char device structure */
};

scull驅動程式引入了兩個Linux核心中用於記憶體管理的核心函式,它們的定義都在<linux/slab.h>:

void*kmalloc(size_t size,int flags);
void kfree(void*ptr);

以下是scull模組中的一個釋放整個資料區的函式(類似清零),將在scull以寫方式開啟和scull_cleanup_module中被呼叫:

int scull_trim(struct scull_dev*dev)
{
struct scull_qset *next,*dptr;
int qset = dev->qset;/*
量子集中量子的個數*/
int i;
for (dptr = dev->data; dptr; dptr = next){ /*
迴圈scull_set個數次,直到dptrNULL為止。*/
if (dptr->data){
for (i = 0; i< qset; i++)
/* 迴圈一個量子集中量子的個數次*/
kfree(dptr->data[i]);
/* 釋放其中一個量子的空間*/

kfree(dptr->data);
/* 釋放當前的scull_set的量子集的空間*/
dptr->data = NULL;
/* 釋放一個scull_set中的void**data指標*/
}
next = dptr->next;
/* 準備下個scull_set的指標*/
kfree(dptr);
/* 釋放當前的scull_set*/
}
dev->size = 0;
/* 當前的scull_device所存的資料為0位元組*/
dev->quantum = scull_quantum;
/* 初始化一個量子的大小*/
dev->qset = scull_qset;
/* 初始化一個量子集中量子的個數*/
dev->data = NULL;
/* 釋放當前的scull_device的struct scull_qset *data指標*/
return 0;
}

以下是scull模組中的一個沿連結串列前行得到正確scull_set指標的函式,將在read和write方法中被呼叫:

/*Follow the list*/
struct scull_qset *scull_follow(struct scull_dev*dev,int n)
{
struct scull_qset *qs = dev->data;
/* Allocate first qset explicitly if need be */
if (! qs){
qs = dev->data= kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs ==NULL)
return NULL; /* Never mind */
memset(qs, 0,sizeof(struct scull_qset));
}
/* Then follow the list */
while (n--){
if (!qs->next){
qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs->next== NULL)
return NULL; /* Never mind */
memset(qs->next, 0,sizeof(struct scull_qset));
}
qs = qs->next;
continue;
}
return qs;
}

其實這個函式的實質是:如果已經存在這個scull_set,就返回這個scull_set的指標。如果不存在這個scull_set,一邊沿連結串列為scull_set分配空間一邊沿連結串列前行,直到所需要的scull_set被分配到空間並初始化為止,就返回這個scull_set的指標。

openrelease

open方法提供給驅動程式以初始化的能力,為以後的操作作準備。應完成的工作如下:

(1)檢查裝置特定的錯誤(如裝置未就緒或硬體問題);

(2)如果裝置是首次開啟,則對其進行初始化;

(3)如有必要,更新f_op指標;

(4)分配並填寫置於filp->private_data裡的資料結構。

int (*open)(struct inode *inode, struct file *filp);

inode 引數有我們需要的資訊,以它的 i_cdev 成員的形式, 裡面包含我們之前建立的 cdev 結構.唯一的問題是通常我們不想要 cdev 結構本身, 我們需要的是包含 cdev 結構的 scull_dev 結構. C 語言使程式設計師玩弄各種技巧來做這種轉換; 但是, 這種技巧程式設計是易出錯的, 並且導致別人難於閱讀和理解程式碼. 幸運的是, 在這種情況下, 核心 hacker 已經為我們實現了這個技巧, 以 container_of 巨集的形式, 在 <linux/kernel.h> 中定義。

而根據scull的實際情況,他的open函式只要完成第四步(將初始化過的struct scull_dev dev的指標傳遞到filp->private_data裡,以備後用)就好了,所以open函式很簡單。但是其中用到了定義在<linux/kernel.h>中的container_of巨集,原始碼如下:

#define container_of(ptr, type, member)({ \
const typeof( ((type *)0)->member) *__mptr =(ptr); \
(type *)( (char*)__mptr - offsetof(type,member));})