目錄
1.引言.....................................................................................................................1
2.Linux 內核模塊...................................................................................................2
3.字符設備驅動程序.............................................................................................4
4.設備驅動中的並發控制...................................................................................10
5.設備的阻塞與非阻塞操作...............................................................................16
6.設備驅動中的異步通知...................................................................................25
7.設備驅動中的中斷處理...................................................................................26
8.定時器...............................................................................................................30
9.內存與I/O 操作................................................................................................32
10.結構化設備驅動程序.....................................................................................39
11.復雜設備驅動.................................................................................................40
12.總結.................................................................................................................52
1
深入淺出Linux 設備驅動編程
宋寶華 21cnbao@21cn.com
1.引言
目前,Linux 軟件工程師大致可分為兩個層次:
(1)Linux 應用軟件工程師(Application Software Engineer):主要利用C 庫函數和Linux
API 進行應用軟件的編寫;
(2)Linux 固件工程師(Firmware Engineer):主要進行Bootloader、Linux 的移植及
Linux 設備驅動程序的設計。
一般而言,固件工程師的要求要高於應用軟件工程師的層次,而其中的Linux 設備驅動
編程又是Linux 程序設計中比較復雜的部分,究其原因,主要包括如下幾個方面:
(1)設備驅動屬於Linux 內核的部分,編寫Linux 設備驅動需要有一定的Linux 操作
系統內核基礎;
(2)編寫Linux 設備驅動需要對硬件的原理有相當的了解,大多數情況下我們是針對
一個特定的嵌入式硬件平台編寫驅動的;
(3)Linux 設備驅動中廣泛涉及到多進程並發的同步、互斥等控制,容易出現bug;
(4)由於屬於內核的一部分,Linux 設備驅動的調試也相當復雜。
目前,市面上的Linux 設備驅動程序參考書籍非常稀缺,少有的經典是由Linux 社區的
三位領導者Jonathan Corbet、Alessandro Rubini、Greg Kroah-Hartman 編寫的《Linux Device
Drivers》(目前該書已經出版到第3 版,中文譯本由中國電力出版社出版)。該書將Linux 設
備驅動編寫技術進行了較系統的展現,但是該書所列舉實例的背景過於復雜,使得讀者需要
將過多的精力投放於對例子背景的理解上,很難完全集中精力於Linux 驅動程序本身。往往
需要將此書翻來覆去地研讀許多遍,才能有較深的體會。
(《Linux Device Drivers》中英文版封面)
本文將仍然秉承《Linux Device Drivers》一書以實例為主的風格,但是實例的背景將非
常簡單,以求使讀者能將集中精力於Linux 設備驅動本身,理解Linux 內核模塊、Linux 設
備驅動的結構、Linux 設備驅動中的並發控制等內容。另外,與《Linux Device Drivers》所
不同的是,針對設備驅動的實例,本文還給出了用戶態的程序來訪問該設備,展現設備驅動
的運行情況及用戶態和內核態的交互。相信閱讀完本文將為您領悟《Linux Device Drivers》
一書中的內容打下很好的基礎。
本文中的例程除引用的以外皆由筆者親自調試通過,主要基於的內核版本為Linux 2.4,
例子要在其他內核上運行只需要做少量的修改。
構建本文例程運行平台的一個較好方法是:在 Windows 平台上安裝VMWare 虛擬機,
並在VMWare 虛擬機上安裝Red Hat。注意安裝的過程中應該選中“開發工具”和“內核開
發”二項(如果本文的例程要在特定的嵌入式系統中運行,還應安裝相應的交叉編譯器,並
2
包含相應的Linux 源代碼),如下圖:
2.Linux 內核模塊
Linux 設備驅動屬於內核的一部分,Linux 內核的一個模塊可以以兩種方式被編譯和加
載:
(1)直接編譯進Linux 內核,隨同Linux 啟動時加載;
(2)編譯成一個可加載和刪除的模塊,使用insmod 加載(modprobe 和insmod 命令類
似,但依賴於相關的配置文件),rmmod 刪除。這種方式控制了內核的大小,而模塊一旦被
插入內核,它就和內核其他部分一樣。
下面我們給出一個內核模塊的例子:
#include <linux/module.h> //所有模塊都需要的頭文件
#include <linux/init.h> // init&exit 相關宏
MODULE_LICENSE("GPL");
static int __init hello_init (void)
{
printk("Hello module init\n");
return 0;
}
static void __exit hello_exit (void)
{
printk("Hello module exit\n");
3
}
module_init(hello_init);
module_exit(hello_exit);
分析上述程序,發現一個Linux 內核模塊需包含模塊初始化和模塊卸載函數,前者在
insmod 的時候運行,后者在rmmod 的時候運行。初始化與卸載函數必須在宏module_init
和module_exit 使用前定義,否則會出現編譯錯誤。
程序中的 MODULE_LICENSE("GPL")用於聲明模塊的許可證。
如果要把上述程序編譯為一個運行時加載和刪除的模塊,則編譯命令為:
gcc –D__KERNEL__ -DMODULE –DLINUX –I /usr/local/src/linux2.4/include -c –o hello.o
hello.c
由此可見,Linux 內核模塊的編譯需要給gcc 指示–D__KERNEL__ -DMODULE
–DLINUX 參數。-I 選項跟着Linux 內核源代碼中Include 目錄的路徑。
下列命令將可加載 hello 模塊:
insmod ./hello.o
下列命令完成相反過程:
rmmod hello
如果要將其直接編譯入Linux 內核,則需要將源代碼文件拷貝入Linux 內核源代碼的相
應路徑里,並修改Makefile。
我們有必要補充一下 Linux 內核編程的一些基本知識:
內存
在 Linux 內核模式下,我們不能使用用戶態的malloc()和free()函數申請和釋放內存。進
行內核編程時,最常用的內存申請和釋放函數為在include/linux/kernel.h 文件中聲明的
kmalloc()和kfree(),其原型為:
void *kmalloc(unsigned int len, int priority);
void kfree(void *__ptr);
kmalloc 的priority 參數通常設置為GFP_KERNEL,如果在中斷服務程序里申請內存則
要用GFP_ATOMIC 參數,因為使用GFP_KERNEL 參數可能會引起睡眠,不能用於非進程
上下文中(在中斷中是不允許睡眠的)。
由於內核態和用戶態使用不同的內存定義,所以二者之間不能直接訪問對方的內存。而
應該使用Linux 中的用戶和內核態內存交互函數(這些函數在include/asm/uaccess.h 中被聲
明):
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
unsigned long copy_to_user (void * to, void * from, unsigned long len);
copy_from_user、copy_to_user 函數返回不能被復制的字節數,因此,如果完全復制成
功,返回值為0。
include/asm/uaccess.h 中定義的put_user 和get_user 用於內核空間和用戶空間的單值交互
(如char、int、long)。
這里給出的僅僅是關於內核中內存管理的皮毛,關於 Linux 內存管理的更多細節知識,
我們會在本文第9 節《內存與I/O 操作》進行更加深入地介紹。
輸出
在內核編程中,我們不能使用用戶態 C 庫函數中的printf()函數輸出信息,而只能使用
printk()。但是,內核中printk()函數的設計目的並不是為了和用戶交流,它實際上是內核的
一種日志機制,用來記錄下日志信息或者給出警告提示。
4
每個printk 都會有個優先級,內核一共有8 個優先級,它們都有對應的宏定義。如果未
指定優先級,內核會選擇默認的優先級DEFAULT_MESSAGE_LOGLEVEL。如果優先級數
字比int console_loglevel 變量小的話,消息就會打印到控制台上。如果syslogd 和klogd 守護
進程在運行的話,則不管是否向控制台輸出,消息都會被追加進/var/log/messages 文件。klogd
只處理內核消息,syslogd 處理其他系統消息,比如應用程序。
模塊參數
2.4 內核下,include/linux/module.h 中定義的宏MODULE_PARM(var,type) 用於向模塊
傳遞命令行參數。var 為接受參數值的變量名, type 為采取如下格式的字符串
[min[-max]]{b,h,i,l,s}。min 及max 用於表示當參數為數組類型時,允許輸入的數組元素的個
數范圍;b:byte;h:short;i:int;l:long;s:string。
在裝載內核模塊時,用戶可以向模塊傳遞一些參數:
insmod modname var=value
如果用戶未指定參數,var 將使用模塊內定義的缺省值。
3.字符設備驅動程序
Linux 下的設備驅動程序被組織為一組完成不同任務的函數的集合,通過這些函數使得
Windows 的設備操作猶如文件一般。在應用程序看來,硬件設備只是一個設備文件,應用程
序可以象操作普通文件一樣對硬件設備進行操作,如open ()、close ()、read ()、write () 等。
Linux 主要將設備分為二類:字符設備和塊設備。字符設備是指設備發送和接收數據以
字符的形式進行;而塊設備則以整個數據緩沖區的形式進行。字符設備的驅動相對比較簡單。
下面我們來假設一個非常簡單的虛擬字符設備:這個設備中只有一個4 個字節的全局變
量int global_var,而這個設備的名字叫做“gobalvar”。對“gobalvar”設備的讀寫等操作即
是對其中全局變量global_var 的操作。
驅動程序是內核的一部分,因此我們需要給其添加模塊初始化函數,該函數用來完成對
所控設備的初始化工作,並調用register_chrdev() 函數注冊字符設備:
static int __init gobalvar_init(void)
{
if (register_chrdev(MAJOR_NUM, " gobalvar ", &gobalvar_fops))
{
//…注冊失敗
}
else
{
//…注冊成功
}
}
其中,register_chrdev 函數中的參數MAJOR_NUM 為主設備號,“gobalvar”為設備名,
gobalvar_fops 為包含基本函數入口點的結構體,類型為file_operations。當gobalvar 模塊被
加載時,gobalvar_init 被執行,它將調用內核函數register_chrdev,把驅動程序的基本入口點
指針存放在內核的字符設備地址表中,在用戶進程對該設備執行系統調用時提供入口地址。
與模塊初始化函數對應的就是模塊卸載函數,需要調用 register_chrdev()的“反函數”
unregister_chrdev():
static void __exit gobalvar_exit(void)
5
{
if (unregister_chrdev(MAJOR_NUM, " gobalvar "))
{
//…卸載失敗
}
else
{
//…卸載成功
}
}
隨着內核不斷增加新的功能,file_operations 結構體已逐漸變得越來越大,但是大多數
的驅動程序只是利用了其中的一部分。對於字符設備來說,要提供的主要入口有:open ()、
release ()、read ()、write ()、ioctl ()、llseek()、poll()等。
open()函數 對設備特殊文件進行open()系統調用時,將調用驅動程序的open () 函數:
int (*open)(struct inode * ,struct file *);
其中參數inode 為設備特殊文件的inode (索引結點) 結構的指針,參數file 是指向這一
設備的文件結構的指針。open()的主要任務是確定硬件處在就緒狀態、驗證次設備號的合法
性(次設備號可以用MINOR(inode-> i - rdev) 取得)、控制使用設備的進程數、根據執行情況
返回狀態碼(0 表示成功,負數表示存在錯誤) 等;
release()函數 當最后一個打開設備的用戶進程執行close ()系統調用時,內核將調用驅
動程序的release () 函數:
void (*release) (struct inode * ,struct file *) ;
release 函數的主要任務是清理未結束的輸入/輸出操作、釋放資源、用戶自定義排他標
志的復位等。
read()函數 當對設備特殊文件進行read() 系統調用時,將調用驅動程序read() 函數:
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
用來從設備中讀取數據。當該函數指針被賦為NULL 值時,將導致read 系統調用出錯
並返回-EINVAL(“Invalid argument,非法參數”)。函數返回非負值表示成功讀取的字節數
(返回值為“signed size”數據類型,通常就是目標平台上的固有整數類型)。
globalvar_read 函數中內核空間與用戶空間的內存交互需要借助第2 節所介紹的函數:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
…
copy_to_user(buf, &global_var, sizeof(int));
…
}
write( ) 函數 當設備特殊文件進行write () 系統調用時,將調用驅動程序的write () 函
數:
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
向設備發送數據。如果沒有這個函數,write 系統調用會向調用程序返回一個-EINVAL。
如果返回值非負,則表示成功寫入的字節數。
globalvar_write 函數中內核空間與用戶空間的內存交互需要借助第2 節所介紹的函數:
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t
*off)
6
{
…
copy_from_user(&global_var, buf, sizeof(int));
…
}
ioctl() 函數 該函數是特殊的控制函數,可以通過它向設備傳遞控制信息或從設備取得
狀態信息,函數原型為:
int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);
unsigned int 參數為設備驅動程序要執行的命令的代碼,由用戶自定義,unsigned long
參數為相應的命令提供參數,類型可以是整型、指針等。如果設備不提供ioctl 入口點,則
對於任何內核未預先定義的請求,ioctl 系統調用將返回錯誤(-ENOTTY,“No such ioctl
fordevice,該設備無此ioctl 命令”)。如果該設備方法返回一個非負值,那么該值會被返回
給調用程序以表示調用成功。
llseek()函數該函數用來修改文件的當前讀寫位置,並將新位置作為(正的)返回值返
回,原型為:
loff_t (*llseek) (struct file *, loff_t, int);
poll()函數poll 方法是poll 和select 這兩個系統調用的后端實現,用來查詢設備是否
可讀或可寫,或是否處於某種特殊狀態,原型為:
unsigned int (*poll) (struct file *, struct poll_table_struct *);
我們將在“設備的阻塞與非阻塞操作”一節對該函數進行更深入的介紹。
設備“gobalvar”的驅動程序的這些函數應分別命名為gobalvar_open、gobalvar_ release、
gobalvar_read、gobalvar_write、gobalvar_ioctl,因此設備“gobalvar”的基本入口點結構變量
gobalvar_fops 賦值如下:
struct file_operations gobalvar_fops = {
read: gobalvar_read,
write: gobalvar_write,
};
上述代碼中對gobalvar_fops 的初始化方法並不是標准C 所支持的,屬於GNU 擴展語
法。
完整的 globalvar.c 文件源代碼如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");
#define MAJOR_NUM 254 //主設備號
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
//初始化字符設備驅動的file_operations 結構體
struct file_operations globalvar_fops =
7
{
read: globalvar_read, write: globalvar_write,
};
static int global_var = 0; //“globalvar”設備的全局變量
static int __init globalvar_init(void)
{
int ret;
//注冊設備驅動
ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
if (ret)
{
printk("globalvar register failure");
}
else
{
printk("globalvar register success");
}
return ret;
}
static void __exit globalvar_exit(void)
{
int ret;
//注銷設備驅動
ret = unregister_chrdev(MAJOR_NUM, "globalvar");
if (ret)
{
printk("globalvar unregister failure");
}
else
{
printk("globalvar unregister success");
}
}
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
//將global_var 從內核空間復制到用戶空間
if (copy_to_user(buf, &global_var, sizeof(int)))
{
return - EFAULT;
8
}
return sizeof(int);
}
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t
*off)
{
//將用戶空間的數據復制到內核空間的global_var
if (copy_from_user(&global_var, buf, sizeof(int)))
{
return - EFAULT;
}
return sizeof(int);
}
module_init(globalvar_init);
module_exit(globalvar_exit);
運行
gcc –D__KERNEL__ -DMODULE –DLINUX –I /usr/local/src/linux2.4/include -c –o
globalvar.o globalvar.c
編譯代碼,運行
inmod globalvar.o
加載globalvar 模塊,再運行
cat /proc/devices
發現其中多出了“254 globalvar”一行,如下圖:
9
接着我們可以運行:
mknod /dev/globalvar c 254 0
創建設備節點,用戶進程通過/dev/globalvar 這個路徑就可以訪問到這個全局變量虛擬設
備了。我們寫一個用戶態的程序globalvartest.c 來驗證上述設備:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
{
int fd, num;
//打開“/dev/globalvar”
fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != -1 )
{
//初次讀globalvar
read(fd, &num, sizeof(int));
printf("The globalvar is %d\n", num);
//寫globalvar
printf("Please input the num written to globalvar\n");
scanf("%d", &num);
write(fd, &num, sizeof(int));
//再次讀globalvar
read(fd, &num, sizeof(int));
printf("The globalvar is %d\n", num);
//關閉“/dev/globalvar”
close(fd);
}
else
{
printf("Device open failure\n");
}
}
編譯上述文件:
gcc –o globalvartest.o globalvartest.c
運行
./globalvartest.o
可以發現“globalvar”設備可以正確的讀寫。
10
4.設備驅動中的並發控制
在驅動程序中,當多個線程同時訪問相同的資源時(驅動程序中的全局變量是一種典型
的共享資源),可能會引發“競態”,因此我們必須對共享資源進行並發控制。Linux 內核中
解決並發控制的最常用方法是自旋鎖與信號量(絕大多數時候作為互斥鎖使用)。
自旋鎖與信號量“類似而不類”,類似說的是它們功能上的相似性,“不類”指代它們在
本質和實現機理上完全不一樣,不屬於一類。
自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環
查看是否該自旋鎖的保持者已經釋放了鎖,“自旋”就是“在原地打轉”。而信號量則引起調
用者睡眠,它把進程從運行隊列上拖出去,除非獲得鎖。這就是它們的“不類”。
但是,無論是信號量,還是自旋鎖,在任何時刻,最多只能有一個保持者,即在任何時
刻最多只能有一個執行單元獲得鎖。這就是它們的“類似”。
鑒於自旋鎖與信號量的上述特點,一般而言,自旋鎖適合於保持時間非常短的情況,它
可以在任何上下文使用;信號量適合於保持時間較長的情況,會只能在進程上下文使用。如
果被保護的共享資源只在進程上下文訪問,則可以以信號量來保護該共享資源,如果對共享
資源的訪問時間非常短,自旋鎖也是好的選擇。但是,如果被保護的共享資源需要在中斷上
下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖。
與信號量相關的 API 主要有:
定義信號量
struct semaphore sem;
初始化信號量
void sema_init (struct semaphore *sem, int val);
該函數初始化信號量,並設置信號量sem 的值為val
void init_MUTEX (struct semaphore *sem);
該函數用於初始化一個互斥鎖,即它把信號量sem 的值設置為1,等同於sema_init (struct
semaphore *sem, 1);
void init_MUTEX_LOCKED (struct semaphore *sem);
該函數也用於初始化一個互斥鎖,但它把信號量sem 的值設置為0,等同於sema_init
(struct semaphore *sem, 0);
獲得信號量
void down(struct semaphore * sem);
該函數用於獲得信號量sem,它會導致睡眠,因此不能在中斷上下文使用;
int down_interruptible(struct semaphore * sem);
該函數功能與down 類似,不同之處為,down 不能被信號打斷,但down_interruptible
能被信號打斷;
int down_trylock(struct semaphore * sem);
該函數嘗試獲得信號量sem,如果能夠立刻獲得,它就獲得該信號量並返回0,否則,
返回非0 值。它不會導致調用者睡眠,可以在中斷上下文使用。
釋放信號量
void up(struct semaphore * sem);
該函數釋放信號量sem,喚醒等待者。
與自旋鎖相關的 API 主要有:
定義自旋鎖
spinlock_t spin;
11
初始化自旋鎖
spin_lock_init(lock)
該宏用於動態初始化自旋鎖lock
獲得自旋鎖
spin_lock(lock)
該宏用於獲得自旋鎖lock,如果能夠立即獲得鎖,它就馬上返回,否則,它將自旋在那
里,直到該自旋鎖的保持者釋放;
spin_trylock(lock)
該宏嘗試獲得自旋鎖lock,如果能立即獲得鎖,它獲得鎖並返回真,否則立即返回假,
實際上不再“在原地打轉”;
釋放自旋鎖
spin_unlock(lock)
該宏釋放自旋鎖lock,它與spin_trylock 或spin_lock 配對使用;
除此之外,還有一組自旋鎖使用於中斷情況下的 API。
下面進入對並發控制的實戰。首先,在 globalvar 的驅動程序中,我們可以通過信號量
來控制對int global_var 的並發訪問,下面給出源代碼:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <asm/semaphore.h>
MODULE_LICENSE("GPL");
#define MAJOR_NUM 254
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
struct file_operations globalvar_fops =
{
read: globalvar_read, write: globalvar_write,
};
static int global_var = 0;
static struct semaphore sem;
static int __init globalvar_init(void)
{
int ret;
ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
if (ret)
{
printk("globalvar register failure");
}
12
else
{
printk("globalvar register success");
init_MUTEX(&sem);
}
return ret;
}
static void __exit globalvar_exit(void)
{
int ret;
ret = unregister_chrdev(MAJOR_NUM, "globalvar");
if (ret)
{
printk("globalvar unregister failure");
}
else
{
printk("globalvar unregister success");
}
}
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
//獲得信號量
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}
//將global_var 從內核空間復制到用戶空間
if (copy_to_user(buf, &global_var, sizeof(int)))
{
up(&sem);
return - EFAULT;
}
//釋放信號量
up(&sem);
return sizeof(int);
}
ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t
13
*off)
{
//獲得信號量
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}
//將用戶空間的數據復制到內核空間的global_var
if (copy_from_user(&global_var, buf, sizeof(int)))
{
up(&sem);
return - EFAULT;
}
//釋放信號量
up(&sem);
return sizeof(int);
}
module_init(globalvar_init);
module_exit(globalvar_exit);
接下來,我們給globalvar 的驅動程序增加open()和release()函數,並在其中借助自旋鎖
來保護對全局變量int globalvar_count(記錄打開設備的進程數)的訪問來實現設備只能被
一個進程打開(必須確保globalvar_count 最多只能為1):
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <asm/semaphore.h>
MODULE_LICENSE("GPL");
#define MAJOR_NUM 254
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
static int globalvar_open(struct inode *inode, struct file *filp);
static int globalvar_release(struct inode *inode, struct file *filp);
struct file_operations globalvar_fops =
{
14
read: globalvar_read, write: globalvar_write, open: globalvar_open, release:
globalvar_release,
};
static int global_var = 0;
static int globalvar_count = 0;
static struct semaphore sem;
static spinlock_t spin = SPIN_LOCK_UNLOCKED;
static int __init globalvar_init(void)
{
int ret;
ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
if (ret)
{
printk("globalvar register failure");
}
else
{
printk("globalvar register success");
init_MUTEX(&sem);
}
return ret;
}
static void __exit globalvar_exit(void)
{
int ret;
ret = unregister_chrdev(MAJOR_NUM, "globalvar");
if (ret)
{
printk("globalvar unregister failure");
}
else
{
printk("globalvar unregister success");
}
}
static int globalvar_open(struct inode *inode, struct file *filp)
{
//獲得自選鎖
15
spin_lock(&spin);
//臨界資源訪問
if (globalvar_count)
{
spin_unlock(&spin);
return - EBUSY;
}
globalvar_count++;
//釋放自選鎖
spin_unlock(&spin);
return 0;
}
static int globalvar_release(struct inode *inode, struct file *filp)
{
globalvar_count--;
return 0;
}
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t
*off)
{
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}
if (copy_to_user(buf, &global_var, sizeof(int)))
{
up(&sem);
return - EFAULT;
}
up(&sem);
return sizeof(int);
}
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len,
loff_t *off)
{
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
16
}
if (copy_from_user(&global_var, buf, sizeof(int)))
{
up(&sem);
return - EFAULT;
}
up(&sem);
return sizeof(int);
}
module_init(globalvar_init);
module_exit(globalvar_exit);
為了上述驅動程序的效果,我們啟動兩個進程分別打開/dev/globalvar。在兩個終端中調
用./globalvartest.o 測試程序,當一個進程打開/dev/globalvar 后,另外一個進程將打開失敗,
輸出“device open failure”,如下圖:
5.設備的阻塞與非阻塞操作
阻塞操作是指,在執行設備操作時,若不能獲得資源,則進程掛起直到滿足可操作的條
件再進行操作。非阻塞操作的進程在不能進行設備操作時,並不掛起。被掛起的進程進入
sleep 狀態,被從調度器的運行隊列移走,直到等待的條件被滿足。
在 Linux 驅動程序中,我們可以使用等待隊列(wait queue)來實現阻塞操作。wait queue
很早就作為一個基本的功能單位出現在Linux 內核里了,它以隊列為基礎數據結構,與進程
17
調度機制緊密結合,能夠用於實現核心的異步事件通知機制。等待隊列可以用來同步對系統
資源的訪問,上節中所講述Linux 信號量在內核中也是由等待隊列來實現的。
下面我們重新定義設備“globalvar”,它可以被多個進程打開,但是每次只有當一個進
程寫入了一個數據之后本進程或其它進程才可以讀取該數據,否則一直阻塞。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <asm/semaphore.h>
MODULE_LICENSE("GPL");
#define MAJOR_NUM 254
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
struct file_operations globalvar_fops =
{
read: globalvar_read, write: globalvar_write,
};
static int global_var = 0;
static struct semaphore sem;
static wait_queue_head_t outq;
static int flag = 0;
static int __init globalvar_init(void)
{
int ret;
ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
if (ret)
{
printk("globalvar register failure");
}
else
{
printk("globalvar register success");
init_MUTEX(&sem);
init_waitqueue_head(&outq);
}
return ret;
}
18
static void __exit globalvar_exit(void)
{
int ret;
ret = unregister_chrdev(MAJOR_NUM, "globalvar");
if (ret)
{
printk("globalvar unregister failure");
}
else
{
printk("globalvar unregister success");
}
}
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t
*off)
{
//等待數據可獲得
if (wait_event_interruptible(outq, flag != 0))
{
return - ERESTARTSYS;
}
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}
flag = 0;
if (copy_to_user(buf, &global_var, sizeof(int)))
{
up(&sem);
return - EFAULT;
}
up(&sem);
return sizeof(int);
}
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len,
loff_t *off)
19
{
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}
if (copy_from_user(&global_var, buf, sizeof(int)))
{
up(&sem);
return - EFAULT;
}
up(&sem);
flag = 1;
//通知數據可獲得
wake_up_interruptible(&outq);
return sizeof(int);
}
module_init(globalvar_init);
module_exit(globalvar_exit);
編寫兩個用戶態的程序來測試,第一個用於阻塞地讀/dev/globalvar,另一個用於寫
/dev/globalvar。只有當后一個對/dev/globalvar 進行了輸入之后,前者的read 才能返回。
讀的程序為:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
{
int fd, num;
fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != - 1)
{
while (1)
{
read(fd, &num, sizeof(int)); //程序將阻塞在此語句,除非有針對globalvar 的輸入
printf("The globalvar is %d\n", num);
//如果輸入是0,則退出
if (num == 0)
{
close(fd);
break;
20
}
}
}
else
{
printf("device open failure\n");
}
}
寫的程序為:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
{
int fd, num;
fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != - 1)
{
while (1)
{
printf("Please input the globalvar:\n");
scanf("%d", &num);
write(fd, &num, sizeof(int));
//如果輸入0,退出
if (num == 0)
{
close(fd);
break;
}
}
}
else
{
printf("device open failure\n");
}
}
打開兩個終端,分別運行上述兩個應用程序,發現當在第二個終端中沒有輸入數據時,
第一個終端沒有輸出(阻塞),每當我們在第二個終端中給globalvar 輸入一個值,第一個終
端就會輸出這個值,如下圖:
21
關於上述例程,我們補充說一點,如果將驅動程序中的 read 函數改為:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t
*off)
{
//獲取信號量:可能阻塞
if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}
//等待數據可獲得:可能阻塞
if (wait_event_interruptible(outq, flag != 0))
{
return - ERESTARTSYS;
}
flag = 0;
//臨界資源訪問
if (copy_to_user(buf, &global_var, sizeof(int)))
{
up(&sem);
return - EFAULT;
}
22
//釋放信號量
up(&sem);
return sizeof(int);
}
即交換wait_event_interruptible(outq, flag != 0)和down_interruptible(&sem)的順序,這個
驅動程序將變得不可運行。實際上,當兩個可能要阻塞的事件同時出現時,即兩個wait_event
或down 擺在一起的時候,將變得非常危險,死鎖的可能性很大,這個時候我們要特別留意
它們的出現順序。當然,我們應該盡可能地避免這種情況的發生!
+還有一個與設備阻塞與非阻塞訪問息息相關的論題,即select 和poll,select 和poll 的
本質一樣,前者在BSD Unix 中引入,后者在System V 中引入。poll 和select 用於查詢設備
的狀態,以便用戶程序獲知是否能對設備進行非阻塞的訪問,它們都需要設備驅動程序中的
poll 函數支持。
驅動程序中 poll 函數中最主要用到的一個API 是poll_wait,其原型如下:
void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);
poll_wait 函數所做的工作是把當前進程添加到wait 參數指定的等待列表(poll_table)
中。下面我們給globalvar 的驅動添加一個poll 函數:
static unsigned int globalvar_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = 0;
poll_wait(filp, &outq, wait);
//數據是否可獲得?
if (flag != 0)
{
mask |= POLLIN | POLLRDNORM; //標示數據可獲得
}
return mask;
}
需要說明的是,poll_wait 函數並不阻塞,程序中poll_wait(filp, &outq, wait)這句話的意
思並不是說一直等待outq 信號量可獲得,真正的阻塞動作是上層的select/poll 函數中完成的。
select/poll 會在一個循環中對每個需要監聽的設備調用它們自己的poll 支持函數以使得當前
進程被加入各個設備的等待列表。若當前沒有任何被監聽的設備就緒,則內核進行調度(調
用schedule)讓出cpu 進入阻塞狀態,schedule 返回時將再次循環檢測是否有操作可以進行,
如此反復;否則,若有任意一個設備就緒,select/poll 都立即返回。
我們編寫一個用戶態應用程序來測試改寫后的驅動。程序中要用到BSD Unix 中引入的
select 函數,其原型為:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval
*timeout);
其中readfds、writefds、exceptfds 分別是被select()監視的讀、寫和異常處理的文件描述
符集合,numfds 的值是需要檢查的號碼最高的文件描述符加1。timeout 參數是一個指向struct
timeval 類型的指針,它可以使select()在等待timeout 時間后若沒有文件描述符准備好則返回。
23
struct timeval 數據結構為:
struct timeval
{
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};
除此之外,我們還將使用下列API:
FD_ZERO(fd_set *set)――清除一個文件描述符集;
FD_SET(int fd,fd_set *set)――將一個文件描述符加入文件描述符集中;
FD_CLR(int fd,fd_set *set)――將一個文件描述符從文件描述符集中清除;
FD_ISSET(int fd,fd_set *set)――判斷文件描述符是否被置位。
下面的用戶態測試程序等待/dev/globalvar 可讀,但是設置了5 秒的等待超時,若超過5
秒仍然沒有數據可讀,則輸出“No data within 5 seconds”:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
main()
{
int fd, num;
fd_set rfds;
struct timeval tv;
fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != - 1)
{
while (1)
{
//查看globalvar 是否有輸入
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
//設置超時時間為5s
tv.tv_sec = 5;
tv.tv_usec = 0;
select(fd + 1, &rfds, NULL, NULL, &tv);
//數據是否可獲得?
if (FD_ISSET(fd, &rfds))
{
read(fd, &num, sizeof(int));
24
printf("The globalvar is %d\n", num);
//輸入為0,退出
if (num == 0)
{
close(fd);
break;
}
}
else
printf("No data within 5 seconds.\n");
}
}
else
{
printf("device open failure\n");
}
}
開兩個終端,分別運行程序:一個對globalvar 進行寫,一個用上述程序對globalvar 進
行讀。當我們在寫終端給globalvar 輸入一個值后,讀終端立即就能輸出該值,當我們連續5
秒沒有輸入時,“No data within 5 seconds”在讀終端被輸出,如下圖:
25
6.設備驅動中的異步通知
結合阻塞與非阻塞訪問、poll 函數可以較好地解決設備的讀寫,但是如果有了異步通知
就更方便了。異步通知的意思是:一旦設備就緒,則主動通知應用程序,這樣應用程序根本
就不需要查詢設備狀態,這一點非常類似於硬件上“中斷”地概念,比較准確的稱謂是“信
號驅動(SIGIO)的異步I/O”。
我 們 先 來 看 一 個 使 用 信 號 驅 動 的 例 子 , 它 通 過 signal(SIGIO, input_handler) 對
STDIN_FILENO 啟動信號機制,輸入可獲得時input_handler 被調用,其源代碼如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#define MAX_LEN 100
void input_handler(int num)
{
char data[MAX_LEN];
int len;
//讀取並輸出STDIN_FILENO 上的輸入
len = read(STDIN_FILENO, &data, MAX_LEN);
data[len] = 0;
printf("input available:%s\n", data);
}
main()
{
int oflags;
//啟動信號驅動機制
signal(SIGIO, input_handler);
fcntl(STDIN_FILENO, F_SETOWN, getpid());
oflags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
//最后進入一個死循環,程序什么都不干了,只有信號能激發input_handler 的運行
//如果程序中沒有這個死循環,會立即執行完畢
while (1);
}
程序的運行效果如下圖:
26
為了使設備支持該機制,我們需要在驅動程序中實現 fasync()函數,並在write()函數中
當數據被寫入時,調用kill_fasync()函數激發一個信號,此部分工作留給讀者來完成。
7.設備驅動中的中斷處理
與 Linux 設備驅動中中斷處理相關的首先是申請與釋放IRQ 的API request_irq()和
free_irq(),request_irq()的原型為:
int request_irq(unsigned int irq,
void (*handler)(int irq, void *dev_id, struct pt_regs *regs),
unsigned long irqflags,
const char * devname,
void *dev_id);
irq 是要申請的硬件中斷號;
handler 是向系統登記的中斷處理函數,是一個回調函數,中斷發生時,系統調用這個
函數,dev_id 參數將被傳遞;
irqflags 是中斷處理的屬性,若設置SA_INTERRUPT,標明中斷處理程序是快速處理程
序,快速處理程序被調用時屏蔽所有中斷,慢速處理程序不屏蔽;若設置SA_SHIRQ,則多
個設備共享中斷,dev_id 在中斷共享時會用到,一般設置為這個設備的device 結構本身或
者NULL。
free_irq()的原型為:
void free_irq(unsigned int irq,void *dev_id);
另外,與Linux 中斷息息相關的一個重要概念是Linux 中斷分為兩個半部:上半部
(tophalf)和下半部(bottom half)。上半部的功能是“登記中斷”,當一個中斷發生時,它進
行相應地硬件讀寫后就把中斷例程的下半部掛到該設備的下半部執行隊列中去。因此,上半
27
部執行的速度就會很快,可以服務更多的中斷請求。但是,僅有“登記中斷”是遠遠不夠的,
因為中斷的事件可能很復雜。因此,Linux 引入了一個下半部,來完成中斷事件的絕大多數
使命。下半部和上半部最大的不同是下半部是可中斷的,而上半部是不可中斷的,下半部幾
乎做了中斷處理程序所有的事情,而且可以被新的中斷打斷!下半部則相對來說並不是非常
緊急的,通常還是比較耗時的,因此由系統自行安排運行時機,不在中斷服務上下文中執行。
Linux 實現下半部的機制主要有tasklet 和工作隊列。
tasklet 基於Linux softirq,其使用相當簡單,我們只需要定義tasklet 及其處理函數並將
二者關聯:
void my_tasklet_func(unsigned long); //定義一個處理函數:
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); // 定義一個tasklet 結構
my_tasklet,與my_tasklet_func(data)函數相關聯
然后,在需要調度 tasklet 的時候引用一個簡單的API 就能使系統在適當的時候進行調
度運行:
tasklet_schedule(&my_tasklet);
此外,Linux 還提供了另外一些其它的控制tasklet 調度與運行的API:
DECLARE_TASKLET_DISABLED(name,function,data); // 與DECLARE_TASKLET 類
似,但等待tasklet 被使能
tasklet_enable(struct tasklet_struct *); //使能tasklet
tasklet_disble(struct tasklet_struct *); //禁用tasklet
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); // 類似
DECLARE_TASKLET()
tasklet_kill(struct tasklet_struct *); // 清除指定tasklet 的可調度位,即不允許調度該tasklet
我們先來看一個tasklet 的運行實例,這個實例沒有任何實際意義,僅僅為了演示。它
的功能是:在globalvar 被寫入一次后,就調度一個tasklet,函數中輸出“tasklet is executing”:
#include <linux/interrupt.h>
…
//定義與綁定tasklet 函數
void test_tasklet_action(unsigned long t);
DECLARE_TASKLET(test_tasklet, test_tasklet_action, 0);
void test_tasklet_action(unsigned long t)
{
printk("tasklet is executing\n");
}
…
ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t
*off)
{
…
if (copy_from_user(&global_var, buf, sizeof(int)))
{
28
return - EFAULT;
}
//調度tasklet 執行
tasklet_schedule(&test_tasklet);
return sizeof(int);
}
由於中斷與真實的硬件息息相關,脫離硬件而空談中斷是毫無意義的,我們還是來舉一
個簡單的例子。這個例子來源於SAMSUNG S3C2410 嵌入式系統實例,看看其中實時鍾的
驅動中與中斷相關的部分:
static struct fasync_struct *rtc_async_queue;
static int __init rtc_init(void)
{
misc_register(&rtc_dev);
create_proc_read_entry("driver/rtc", 0, 0, rtc_read_proc, NULL);
#if RTC_IRQ
if (rtc_has_irq == 0)
goto no_irq2;
init_timer(&rtc_irq_timer);
rtc_irq_timer.function = rtc_dropped_irq;
spin_lock_irq(&rtc_lock);
/* Initialize periodic freq. to CMOS reset default, which is 1024Hz */
CMOS_WRITE(((CMOS_READ(RTC_FREQ_SELECT) &0xF0) | 0x06),
RTC_FREQ_SELECT);
spin_unlock_irq(&rtc_lock);
rtc_freq = 1024;
no_irq2:
#endif
printk(KERN_INFO "Real Time Clock Driver v" RTC_VERSION "\n");
return 0;
}
static void __exit rtc_exit(void)
{
remove_proc_entry("driver/rtc", NULL);
misc_deregister(&rtc_dev);
release_region(RTC_PORT(0), RTC_IO_EXTENT);
if (rtc_has_irq)
free_irq(RTC_IRQ, NULL);
29
}
static void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
/*
* Can be an alarm interrupt, update complete interrupt,
* or a periodic interrupt. We store the status in the
* low byte and the number of interrupts received since
* the last read in the remainder of rtc_irq_data.
*/
spin_lock(&rtc_lock);
rtc_irq_data += 0x100;
rtc_irq_data &= ~0xff;
rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) &0xF0);
if (rtc_status &RTC_TIMER_ON)
mod_timer(&rtc_irq_timer, jiffies + HZ / rtc_freq + 2 * HZ / 100);
spin_unlock(&rtc_lock);
/* Now do the rest of the actions */
wake_up_interruptible(&rtc_wait);
kill_fasync(&rtc_async_queue, SIGIO, POLL_IN);
}
static int rtc_fasync (int fd, struct file *filp, int on)
{
return fasync_helper (fd, filp, on, &rtc_async_queue);
}
static void rtc_dropped_irq(unsigned long data)
{
unsigned long freq;
spin_lock_irq(&rtc_lock);
/* Just in case someone disabled the timer from behind our back... */
if (rtc_status &RTC_TIMER_ON)
mod_timer(&rtc_irq_timer, jiffies + HZ / rtc_freq + 2 * HZ / 100);
rtc_irq_data += ((rtc_freq / HZ) << 8);
rtc_irq_data &= ~0xff;
rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) &0xF0); /* restart */
30
freq = rtc_freq;
spin_unlock_irq(&rtc_lock);
printk(KERN_WARNING "rtc: lost some interrupts at %ldHz.\n", freq);
/* Now we have new data */
wake_up_interruptible(&rtc_wait);
kill_fasync(&rtc_async_queue, SIGIO, POLL_IN);
}
RTC 中斷發生后,激發了一個異步信號,因此本驅動程序提供了對第6 節異步信號的
支持。並不是每個中斷都需要一個下半部,如果本身要處理的事情並不復雜,可能只有一個
上半部,本例中的RTC 驅動就是如此。
8.定時器
Linux 內核中定義了一個timer_list 結構,我們在驅動程序中可以利用之:
struct timer_list {
struct list_head list;
unsigned long expires; //定時器到期時間
unsigned long data; //作為參數被傳入定時器處理函數
void (*function)(unsigned long);
};
下面是關於timer 的API 函數:
增加定時器
void add_timer(struct timer_list * timer);
刪除定時器
int del_timer(struct timer_list * timer);
修改定時器的expire
int mod_timer(struct timer_list *timer, unsigned long expires);
使用定時器的一般流程為:
(1)timer、編寫function;
(2)為timer 的expires、data、function 賦值;
(3)調用add_timer 將timer 加入列表;
(4)在定時器到期時,function 被執行;
(5)在程序中涉及timer 控制的地方適當地調用del_timer、mod_timer 刪除timer 或修
改timer 的expires。
我們可以參考 drivers\char\keyboard.c 中鍵盤的驅動中關於timer 的部分:
…
#include <linux/timer.h>
…
static struct timer_list key_autorepeat_timer =
31
{
function: key_callback
};
static void
kbd_processkeycode(unsigned char keycode, char up_flag, int autorepeat)
{
char raw_mode = (kbd->kbdmode == VC_RAW);
if (up_flag) {
rep = 0;
if(!test_and_clear_bit(keycode, key_down))
up_flag = kbd_unexpected_up(keycode);
} else {
rep = test_and_set_bit(keycode, key_down);
/* If the keyboard autorepeated for us, ignore it.
* We do our own autorepeat processing.
*/
if (rep && !autorepeat)
return;
}
if (kbd_repeatkeycode == keycode || !up_flag || raw_mode) {
kbd_repeatkeycode = -1;
del_timer(&key_autorepeat_timer);
}
…
/*
* Calculate the next time when we have to do some autorepeat
* processing. Note that we do not do autorepeat processing
* while in raw mode but we do do autorepeat processing in
* medium raw mode.
*/
if (!up_flag && !raw_mode) {
kbd_repeatkeycode = keycode;
if (vc_kbd_mode(kbd, VC_REPEAT)) {
if (rep)
key_autorepeat_timer.expires = jiffies + kbd_repeatinterval;
else
key_autorepeat_timer.expires = jiffies + kbd_repeattimeout;
add_timer(&key_autorepeat_timer);
}
}
…
32
}
9.內存與I/O 操作
對於提供了 MMU(存儲管理器,輔助操作系統進行內存管理,提供虛實地址轉換等硬
件支持)的處理器而言,Linux 提供了復雜的存儲管理系統,使得進程所能訪問的內存達到
4GB。
進程的 4GB 內存空間被人為的分為兩個部分——用戶空間與內核空間。用戶空間地址
分布從0 到3GB(PAGE_OFFSET,在0x86 中它等於0xC0000000),3GB 到4GB 為內核空間,
如下圖:
內核空間中,從3G 到vmalloc_start 這段地址是物理內存映射區域(該區域中包含了內
核鏡像、物理頁框表mem_map 等等),比如我們使用的VMware 虛擬系統內存是160M,那
么3G~3G+160M 這片內存就應該映射物理內存。在物理內存映射區之后,就是vmalloc 區
域。對於160M 的系統而言,vmalloc_start 位置應在3G+160M 附近(在物理內存映射區與
vmalloc_start 期間還存在一個8M 的gap 來防止躍界),vmalloc_end 的位置接近4G(最后位
置系統會保留一片128k 大小的區域用於專用頁面映射),如下圖:
kmalloc 和get_free_page 申請的內存位於物理內存映射區域,而且在物理上也是連續的,
它們與真實的物理地址只有一個固定的偏移,因此存在較簡單的轉換關系,virt_to_phys()
可以實現內核虛擬地址轉化為物理地址:
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
{
33
return __pa(address);
}
上面轉換過程是將虛擬地址減去3G(PAGE_OFFSET=0XC000000)。
與之對應的函數為 phys_to_virt(),將內核物理地址轉化為虛擬地址:
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void * phys_to_virt(unsigned long address)
{
return __va(address);
}
virt_to_phys()和phys_to_virt()都定義在include\asm-i386\io.h 中。
而 vmalloc 申請的內存則位於vmalloc_start~vmalloc_end 之間,與物理地址沒有簡單的
轉換關系,雖然在邏輯上它們也是連續的,但是在物理上它們不要求連續。
我們用下面的程序來演示 kmalloc、get_free_page 和vmalloc 的區別:
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");
unsigned char *pagemem;
unsigned char *kmallocmem;
unsigned char *vmallocmem;
int __init mem_module_init(void)
{
//最好每次內存申請都檢查申請是否成功
//下面這段僅僅作為演示的代碼沒有檢查
pagemem = (unsigned char*)get_free_page(0);
printk("<1>pagemem addr=%x", pagemem);
kmallocmem = (unsigned char*)kmalloc(100, 0);
printk("<1>kmallocmem addr=%x", kmallocmem);
vmallocmem = (unsigned char*)vmalloc(1000000);
printk("<1>vmallocmem addr=%x", vmallocmem);
return 0;
}
void __exit mem_module_exit(void)
{
free_page(pagemem);
kfree(kmallocmem);
vfree(vmallocmem);
}
34
module_init(mem_module_init);
module_exit(mem_module_exit);
我們的系統上有160MB 的內存空間,運行一次上述程序,發現pagemem 的地址在
0xc7997000(約3G+121M)、kmallocmem 地址在0xc9bc1380(約3G+155M)、vmallocmem
的地址在0xcabeb000(約3G+171M)處,符合前文所述的內存布局。
接下來,我們討論 Linux 設備驅動究竟怎樣訪問外設的I/O 端口(寄存器)。
幾乎每一種外設都是通過讀寫設備上的寄存器來進行的,通常包括控制寄存器、狀態寄
存器和數據寄存器三大類,外設的寄存器通常被連續地編址。根據CPU 體系結構的不同,
CPU 對IO 端口的編址方式有兩種:
(1)I/O 映射方式(I/O-mapped)
典型地,如 X86 處理器為外設專門實現了一個單獨的地址空間,稱為“I/O 地址空間”
或者“I/O 端口空間”,CPU 通過專門的I/O 指令(如X86 的IN 和OUT 指令)來訪問這一
空間中的地址單元。
(2)內存映射方式(Memory-mapped)
RISC 指令系統的CPU(如ARM、PowerPC 等)通常只實現一個物理地址空間,外設
I/O 端口成為內存的一部分。此時,CPU 可以象訪問一個內存單元那樣訪問外設I/O 端口,
而不需要設立專門的外設I/O 指令。
但是,這兩者在硬件實現上的差異對於軟件來說是完全透明的,驅動程序開發人員可以
將內存映射方式的I/O 端口和外設內存統一看作是“I/O 內存”資源。
一般來說,在系統運行時,外設的I/O 內存資源的物理地址是已知的,由硬件的設計決
定。但是CPU 通常並沒有為這些已知的外設I/O 內存資源的物理地址預定義虛擬地址范圍,
驅動程序並不能直接通過物理地址訪問I/O 內存資源,而必須將它們映射到核心虛地址空間
內(通過頁表),然后才能根據映射所得到的核心虛地址范圍,通過訪內指令訪問這些I/O
內存資源。Linux 在io.h 頭文件中聲明了函數ioremap(),用來將I/O 內存資源的物理地址
映射到核心虛地址空間(3GB-4GB)中,原型如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
iounmap 函數用於取消ioremap()所做的映射,原型如下:
void iounmap(void * addr);
這兩個函數都是實現在mm/ioremap.c 文件中。
在將 I/O 內存資源的物理地址映射成核心虛地址后,理論上講我們就可以象讀寫RAM
那樣直接讀寫I/O 內存資源了。為了保證驅動程序的跨平台的可移植性,我們應該使用Linux
中特定的函數來訪問I/O 內存資源,而不應該通過指向核心虛地址的指針來訪問。如在x86
平台上,讀寫I/O 的函數如下所示:
#define readb(addr) (*(volatile unsigned char *) __io_virt(addr))
#define readw(addr) (*(volatile unsigned short *) __io_virt(addr))
#define readl(addr) (*(volatile unsigned int *) __io_virt(addr))
#define writeb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b))
#define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b))
#define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b))
#define memset_io(a,b,c) memset(__io_virt(a),(b),(c))
#define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))
35
#define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))
最后,我們要特別強調驅動程序中mmap 函數的實現方法。用mmap 映射一個設備,意
味着使用戶空間的一段地址關聯到設備內存上,這使得只要程序在分配的地址范圍內進行讀
取或者寫入,實際上就是對設備的訪問。
筆者在 Linux 源代碼中進行包含“ioremap”文本的搜索,發現真正出現的ioremap 的地
方相當少。所以筆者追根索源地尋找I/O 操作的物理地址轉換到虛擬地址的真實所在,發現
Linux 有替代ioremap 的語句,但是這個轉換過程卻是不可或缺的。
譬如我們再次摘取 S3C2410 這個ARM 芯片RTC(實時鍾)驅動中的一小段:
static void get_rtc_time(int alm, struct rtc_time *rtc_tm)
{
spin_lock_irq(&rtc_lock);
if (alm == 1) {
rtc_tm->tm_year = (unsigned char)ALMYEAR & Msk_RTCYEAR;
rtc_tm->tm_mon = (unsigned char)ALMMON & Msk_RTCMON;
rtc_tm->tm_mday = (unsigned char)ALMDAY & Msk_RTCDAY;
rtc_tm->tm_hour = (unsigned char)ALMHOUR & Msk_RTCHOUR;
rtc_tm->tm_min = (unsigned char)ALMMIN & Msk_RTCMIN;
rtc_tm->tm_sec = (unsigned char)ALMSEC & Msk_RTCSEC;
}
else {
read_rtc_bcd_time:
rtc_tm->tm_year = (unsigned char)BCDYEAR & Msk_RTCYEAR;
rtc_tm->tm_mon = (unsigned char)BCDMON & Msk_RTCMON;
rtc_tm->tm_mday = (unsigned char)BCDDAY & Msk_RTCDAY;
rtc_tm->tm_hour = (unsigned char)BCDHOUR & Msk_RTCHOUR;
rtc_tm->tm_min = (unsigned char)BCDMIN & Msk_RTCMIN;
rtc_tm->tm_sec = (unsigned char)BCDSEC & Msk_RTCSEC;
if (rtc_tm->tm_sec == 0) {
/* Re-read all BCD registers in case of BCDSEC is 0.
See RTC section at the manual for more info. */
goto read_rtc_bcd_time;
}
}
spin_unlock_irq(&rtc_lock);
BCD_TO_BIN(rtc_tm->tm_year);
BCD_TO_BIN(rtc_tm->tm_mon);
BCD_TO_BIN(rtc_tm->tm_mday);
BCD_TO_BIN(rtc_tm->tm_hour);
BCD_TO_BIN(rtc_tm->tm_min);
BCD_TO_BIN(rtc_tm->tm_sec);
/* The epoch of tm_year is 1900 */
36
rtc_tm->tm_year += RTC_LEAP_YEAR - 1900;
/* tm_mon starts at 0, but rtc month starts at 1 */
rtc_tm->tm_mon--;
}
I/O 操作似乎就是對ALMYEAR、ALMMON、ALMDAY 定義的寄存器進行操作,那這
些宏究竟定義為什么呢?
#define ALMDAY bRTC(0x60)
#define ALMMON bRTC(0x64)
#define ALMYEAR bRTC(0x68)
其中借助了宏bRTC,這個宏定義為:
#define bRTC(Nb) __REG(0x57000000 + (Nb))
其中又借助了宏__REG,而__REG 又定義為:
# define __REG(x) io_p2v(x)
最后的io_p2v 才是真正“玩”虛擬地址和物理地址轉換的地方:
#define io_p2v(x) ((x) | 0xa0000000)
與__REG 對應的有個__PREG:
# define __PREG(x) io_v2p(x)
與io_p2v 對應的有個io_v2p:
#define io_v2p(x) ((x) & ~0xa0000000)
可見有沒有出現ioremap 是次要的,關鍵問題是有無虛擬地址和物理地址的轉換!
下面的程序在啟動的時候保留一段內存,然后使用 ioremap 將它映射到內核虛擬空間,
同時又用remap_page_range 映射到用戶虛擬空間,這樣一來,內核和用戶都能訪問。如果
在內核虛擬地址將這段內存初始化串"abcd",那么在用戶虛擬地址能夠讀出來:
/************mmap_ioremap.c**************/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/wrapper.h> /* for mem_map_(un)reserve */
#include <asm/io.h> /* for virt_to_phys */
#include <linux/slab.h> /* for kmalloc and kfree */
MODULE_PARM(mem_start, "i");
MODULE_PARM(mem_size, "i");
static int mem_start = 101, mem_size = 10;
static char *reserve_virt_addr;
static int major;
int mmapdrv_open(struct inode *inode, struct file *file);
int mmapdrv_release(struct inode *inode, struct file *file);
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma);
37
static struct file_operations mmapdrv_fops =
{
owner: THIS_MODULE, mmap: mmapdrv_mmap, open: mmapdrv_open, release:
mmapdrv_release,
};
int init_module(void)
{
if ((major = register_chrdev(0, "mmapdrv", &mmapdrv_fops)) < 0)
{
printk("mmapdrv: unable to register character device\n");
return ( - EIO);
}
printk("mmap device major = %d\n", major);
printk("high memory physical address 0x%ldM\n", virt_to_phys(high_memory) /
1024 / 1024);
reserve_virt_addr = ioremap(mem_start *1024 * 1024, mem_size *1024 * 1024);
printk("reserve_virt_addr = 0x%lx\n", (unsigned long)reserve_virt_addr);
if (reserve_virt_addr)
{
int i;
for (i = 0; i < mem_size *1024 * 1024; i += 4)
{
reserve_virt_addr[i] = 'a';
reserve_virt_addr[i + 1] = 'b';
reserve_virt_addr[i + 2] = 'c';
reserve_virt_addr[i + 3] = 'd';
}
}
else
{
unregister_chrdev(major, "mmapdrv");
return - ENODEV;
}
return 0;
}
/* remove the module */
void cleanup_module(void)
{
if (reserve_virt_addr)
38
iounmap(reserve_virt_addr);
unregister_chrdev(major, "mmapdrv");
return ;
}
int mmapdrv_open(struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT;
return (0);
}
int mmapdrv_release(struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT;
return (0);
}
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
if (size > mem_size *1024 * 1024)
{
printk("size too big\n");
return ( - ENXIO);
}
offset = offset + mem_start * 1024 * 1024;
/* we do not want to have this area swapped out, lock it */
vma->vm_flags |= VM_LOCKED;
if (remap_page_range(vma, vma->vm_start, offset, size, PAGE_SHARED))
{
printk("remap page range failed\n");
return - ENXIO;
}
return (0);
}
remap_page_range 函數的功能是構造用於映射一段物理地址的新頁表,實現了內核空間
與用戶空間的映射,其原型如下:
39
int remap_page_range(vma_area_struct *vma, unsigned long from, unsigned long to,
unsigned long size, pgprot_tprot);
使用mmap 最典型的例子是顯示卡的驅動,將顯存空間直接從內核映射到用戶空間將可
提供顯存的讀寫效率。
10.結構化設備驅動程序
在 1~9 節關於設備驅動的例子中,我們沒有考慮設備驅動程序的結構組織問題。實際上,
Linux 設備驅動的開發者習慣於一套約定俗成的數據結構組織方法和程序框架。
設備結構體
Linux 設備驅動程序的編寫者喜歡把與某設備相關的所有內容定義為一個設備結構體,
其中包括設備驅動涉及的硬件資源、全局軟件資源、控制(自旋鎖、互斥鎖、等待隊列、定
時器等),在涉及設備的操作時,僅僅操作這個結構體就可以了。
對於“globalvar”設備,這個結構體就是:
struct globalvar_dev
{
int global_var = 0;
struct semaphore sem;
wait_queue_head_t outq;
int flag = 0;
};
open()和release()
一般來說,較規范的 open( )通常需要完成下列工作:
1. 檢查設備相關錯誤,如設備尚未准備好等;
2. 如果是第一次打開,則初始化硬件設備;
3. 識別次設備號,如果有必要則更新讀寫操作的當前位置指針f_ops;
4. 分配和填寫要放在file->private_data 里的數據結構;
5. 使用計數增1。
release( )的作用正好與open( )相反,通常要完成下列工作:
1. 使用計數減1;
2. 釋放在file->private_data 中分配的內存;
3. 如果使用計算為0,則關閉設備。
我們使用 LDD2 中scull_u 的例子:
int scull_u_open(struct inode *inode, struct file *filp)
{
Scull_Dev *dev = &scull_u_device; /* device information */
int num = NUM(inode->i_rdev);
if (!filp->private_data && num > 0)
return -ENODEV; /* not devfs: allow 1 device only */
spin_lock(&scull_u_lock);
if (scull_u_count &&
(scull_u_owner != current->uid) && /* allow user */
(scull_u_owner != current->euid) && /* allow whoever did su */
40
!capable(CAP_DAC_OVERRIDE)) { /* still allow root */
spin_unlock(&scull_u_lock);
return -EBUSY; /* -EPERM would confuse the user */
}
if (scull_u_count == 0)
scull_u_owner = current->uid; /* grab it */
scull_u_count++;
spin_unlock(&scull_u_lock);
/* then, everything else is copied from the bare scull device */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)
scull_trim(dev);
if (!filp->private_data)
filp->private_data = dev;
MOD_INC_USE_COUNT;
return 0; /* success */
}
int scull_u_release(struct inode *inode, struct file *filp)
{
scull_u_count--; /* nothing else */
MOD_DEC_USE_COUNT;
return 0;
}
上面所述為一般意義上的設計規范,應該說是option(可選的)而非強制的。
11.復雜設備驅動
這里所說的復雜設備驅動涉及到 PCI、USB、網絡設備、塊設備等(嚴格意義而言,這
些設備在概念上並不並列,例如與塊設備並列的是字符設備,而PCI、USB 設備等都可能屬
於字符設備),這些設備的驅動中又涉及到一些與特定設備類型相關的較為復雜的數據結構
和程序結構。本文將不對這些設備驅動的細節進行過多的介紹,僅僅進行輕描淡寫的敘述。
PCI 是The Peripheral Component Interconnect –Bus 的縮寫,CPU 使用PCI 橋chipset 與
PCI 設備通信,PCI 橋chipset 處理了PCI 子系統與內存子系統間的所有數據交互,PCI 設備
完全被從內存子系統分離出來。下圖呈現了PCI 子系統的原理:
41
每個 PCI 設備都有一個256 字節的設備配置塊,其中前64 字節作為設備的ID 和基本
配置信息,Linux 中提供了一組函數來處理PCI 配置塊。在PCI 設備能得以使用前,Linux
驅動程序需要從PCI 設備配置塊中的信息決定設備的特定參數,進行相關設置以便能正確
操作該PCI 設備。
一般的 PCI 設備初始化函數處理流程為:
(1)檢查內核是否支持PCI-Bios;
(2)檢查設備是否存在,獲得設備的配置信息;
1~2 這兩步的例子如下:
int pcidata_read_proc(char *buf, char **start, off_t offset, int len, int *eof,
void *data)
{
int i, pos = 0;
int bus, devfn;
if (!pcibios_present())
return sprintf(buf, "No PCI bios present\n");
/*
* This code is derived from "drivers/pci/pci.c". This means that
* the GPL applies to this source file and credit is due to the
* original authors (Drew Eckhardt, Frederic Potter, David
* Mosberger-Tang)
*/
for (bus = 0; !bus; bus++)
{
/* only bus 0 :-) */
for (devfn = 0; devfn < 0x100 && pos < PAGE_SIZE / 2; devfn++)
42
{
struct pci_dev *dev = NULL;
dev = pci_find_slot(bus, devfn);
if (!dev)
continue;
/* Ok, we've found a device, copy its cfg space to the buffer*/
for (i = 0; i < 256; i += sizeof(u32), pos += sizeof(u32))
pci_read_config_dword(dev, i, (u32*)(buf + pos));
pci_release_device(dev); /* 2.0 compatibility */
}
}
*eof = 1;
return pos;
}
其中使用的pci_find_slot()函數定義為:
struct pci_dev *pci_find_slot (unsigned int bus,
unsigned int devfn)
{
struct pci_dev *pptr = kmalloc(sizeof(*pptr), GFP_KERNEL);
int index = 0;
unsigned short vendor;
int ret;
if (!pptr) return NULL;
pptr->index = index; /* 0 */
ret = pcibios_read_config_word(bus, devfn, PCI_VENDOR_ID, &vendor);
if (ret /* == PCIBIOS_DEVICE_NOT_FOUND or whatever error */
|| vendor==0xffff || vendor==0x0000) {
kfree(pptr); return NULL;
}
printk("ok (%i, %i %x)\n", bus, devfn, vendor);
/* fill other fields */
pptr->bus = bus;
pptr->devfn = devfn;
pcibios_read_config_word(pptr->bus, pptr->devfn,
PCI_VENDOR_ID, &pptr->vendor);
pcibios_read_config_word(pptr->bus, pptr->devfn,
PCI_DEVICE_ID, &pptr->device);
return pptr;
}
(3)根據設備的配置信息申請I/O 空間及IRQ 資源;
(4)注冊設備。
43
USB 設備的驅動主要處理probe(探測)、disconnect(斷開)函數及usb_device_id(設
備信息)數據結構,如:
static struct usb_device_id sample_id_table[] =
{
{
USB_INTERFACE_INFO(3, 1, 1), driver_info: (unsigned long)"keyboard"
} ,
{
USB_INTERFACE_INFO(3, 1, 2), driver_info: (unsigned long)"mouse"
}
,
{
0, /* no more matches */
}
};
static struct usb_driver sample_usb_driver =
{
name: "sample", probe: sample_probe, disconnect: sample_disconnect, id_table:
sample_id_table,
};
當一個USB 設備從系統拔掉后,設備驅動程序的disconnect 函數會自動被調用,在執
行了disconnect 函數后,所有為USB 設備分配的數據結構,內存空間都會被釋放:
static void sample_disconnect(struct usb_device *udev, void *clientdata)
{
/* the clientdata is the sample_device we passed originally */
struct sample_device *sample = clientdata;
/* remove the URB, remove the input device, free memory */
usb_unlink_urb(&sample->urb);
kfree(sample);
printk(KERN_INFO "sample: USB %s disconnected\n", sample->name);
/*
* here you might MOD_DEC_USE_COUNT, but only if you increment
* the count in sample_probe() below
*/
return;
}
當驅動程序向子系統注冊后,插入一個新的USB 設備后總是要自動進入probe 函數。
驅動程序會為這個新加入系統的設備向內部的數據結構建立一個新的實例。通常情況下,
probe 函數執行一些功能來檢測新加入的USB 設備硬件中的生產廠商和產品定義以及設備
所屬的類或子類定義是否與驅動程序相符,若相符,再比較接口的數目與本驅動程序支持設
備的接口數目是否相符。一般在probe 函數中也會解析USB 設備的說明,從而確認新加入
44
的USB 設備會使用這個驅動程序:
static void *sample_probe(struct usb_device *udev, unsigned int ifnum,
const struct usb_device_id *id)
{
/*
* The probe procedure is pretty standard. Device matching has already
* been performed based on the id_table structure (defined later)
*/
struct usb_interface *iface;
struct usb_interface_descriptor *interface;
struct usb_endpoint_descriptor *endpoint;
struct sample_device *sample;
printk(KERN_INFO "usbsample: probe called for %s device\n",
(char *)id->driver_info /* "mouse" or "keyboard" */ );
iface = &udev->actconfig->interface[ifnum];
interface = &iface->altsetting[iface->act_altsetting];
if (interface->bNumEndpoints != 1) return NULL;
endpoint = interface->endpoint + 0;
if (!(endpoint->bEndpointAddress & 0x80)) return NULL;
if ((endpoint->bmAttributes & 3) != 3) return NULL;
usb_set_protocol(udev, interface->bInterfaceNumber, 0);
usb_set_idle(udev, interface->bInterfaceNumber, 0, 0);
/* allocate and zero a new data structure for the new device */
sample = kmalloc(sizeof(struct sample_device), GFP_KERNEL);
if (!sample) return NULL; /* failure */
memset(sample, 0, sizeof(*sample));
sample->name = (char *)id->driver_info;
/* fill the URB data structure using the FILL_INT_URB macro */
{
int pipe = usb_rcvintpipe(udev, endpoint->bEndpointAddress);
int maxp = usb_maxpacket(udev, pipe, usb_pipeout(pipe));
if (maxp > 8) maxp = 8; sample->maxp = maxp; /* remember for later */
FILL_INT_URB(&sample->urb, udev, pipe, sample->data, maxp,
sample_irq, sample, endpoint->bInterval);
}
45
/* register the URB within the USB subsystem */
if (usb_submit_urb(&sample->urb)) {
kfree(sample);
return NULL;
}
/* announce yourself */
printk(KERN_INFO "usbsample: probe successful for %s (maxp is %i)\n",
sample->name, sample->maxp);
/*
* here you might MOD_INC_USE_COUNT; if you do, you'll need to unplug
* the device or the devices before being able to unload the module
*/
/* and return the new structure */
return sample;
}
在網絡設備驅動的編寫中,我們特別關心的就是數據的收、發及中斷。網絡設備驅動程
序的層次如下:
網絡設備接收到報文后將其傳入上層:
/*
* Receive a packet: retrieve, encapsulate and pass over to upper levels
*/
void snull_rx(struct net_device *dev, int len, unsigned char *buf)
{
struct sk_buff *skb;
struct snull_priv *priv = (struct snull_priv *) dev->priv;
/*
* The packet has been retrieved from the transmission
* medium. Build an skb around it, so upper layers can handle it
*/
skb = dev_alloc_skb(len+2);
if (!skb) {
printk("snull rx: low on mem - packet dropped\n");
priv->stats.rx_dropped++;
46
return;
}
skb_reserve(skb, 2); /* align IP on 16B boundary */
memcpy(skb_put(skb, len), buf, len);
/* Write metadata, and then pass to the receive level */
skb->dev = dev;
skb->protocol = eth_type_trans(skb, dev);
skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
priv->stats.rx_packets++;
#ifndef LINUX_20
priv->stats.rx_bytes += len;
#endif
netif_rx(skb);
return;
}
在中斷到來時接收報文信息:
void snull_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int statusword;
struct snull_priv *priv;
/*
* As usual, check the "device" pointer for shared handlers.
* Then assign "struct device *dev"
*/
struct net_device *dev = (struct net_device *)dev_id;
/* ... and check with hw if it's really ours */
if (!dev /*paranoid*/ ) return;
/* Lock the device */
priv = (struct snull_priv *) dev->priv;
spin_lock(&priv->lock);
/* retrieve statusword: real netdevices use I/O instructions */
statusword = priv->status;
if (statusword & SNULL_RX_INTR) {
/* send it to snull_rx for handling */
snull_rx(dev, priv->rx_packetlen, priv->rx_packetdata);
}
if (statusword & SNULL_TX_INTR) {
/* a transmission is over: free the skb */
priv->stats.tx_packets++;
priv->stats.tx_bytes += priv->tx_packetlen;
47
dev_kfree_skb(priv->skb);
}
/* Unlock the device and we are done */
spin_unlock(&priv->lock);
return;
}
而發送報文則分為兩個層次,一個層次是內核調用,一個層次完成真正的硬件上的發送:
/*
* Transmit a packet (called by the kernel)
*/
int snull_tx(struct sk_buff *skb, struct net_device *dev)
{
int len;
char *data;
struct snull_priv *priv = (struct snull_priv *) dev->priv;
#ifndef LINUX_24
if (dev->tbusy || skb == NULL) {
PDEBUG("tint for %p, tbusy %ld, skb %p\n", dev, dev->tbusy, skb);
snull_tx_timeout (dev);
if (skb == NULL)
return 0;
}
#endif
len = skb->len < ETH_ZLEN ? ETH_ZLEN : skb->len;
data = skb->data;
dev->trans_start = jiffies; /* save the timestamp */
/* Remember the skb, so we can free it at interrupt time */
priv->skb = skb;
/* actual deliver of data is device-specific, and not shown here */
snull_hw_tx(data, len, dev);
return 0; /* Our simple device can not fail */
}
/*
* Transmit a packet (low level interface)
*/
void snull_hw_tx(char *buf, int len, struct net_device *dev)
{
48
/*
* This function deals with hw details. This interface loops
* back the packet to the other snull interface (if any).
* In other words, this function implements the snull behaviour,
* while all other procedures are rather device-independent
*/
struct iphdr *ih;
struct net_device *dest;
struct snull_priv *priv;
u32 *saddr, *daddr;
/* I am paranoid. Ain't I? */
if (len < sizeof(struct ethhdr) + sizeof(struct iphdr)) {
printk("snull: Hmm... packet too short (%i octets)\n",
len);
return;
}
if (0) { /* enable this conditional to look at the data */
int i;
PDEBUG("len is %i\n" KERN_DEBUG "data:",len);
for (i=14 ; i<len; i++)
printk(" %02x",buf[i]&0xff);
printk("\n");
}
/*
* Ethhdr is 14 bytes, but the kernel arranges for iphdr
* to be aligned (i.e., ethhdr is unaligned)
*/
ih = (struct iphdr *)(buf+sizeof(struct ethhdr));
saddr = &ih->saddr;
daddr = &ih->daddr;
((u8 *)saddr)[2] ^= 1; /* change the third octet (class C) */
((u8 *)daddr)[2] ^= 1;
ih->check = 0; /* and rebuild the checksum (ip needs it) */
ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);
if (dev == snull_devs)
PDEBUGG("%08x:%05i --> %08x:%05i\n",
ntohl(ih->saddr),ntohs(((struct tcphdr *)(ih+1))->source),
ntohl(ih->daddr),ntohs(((struct tcphdr *)(ih+1))->dest));
else
49
PDEBUGG("%08x:%05i <-- %08x:%05i\n",
ntohl(ih->daddr),ntohs(((struct tcphdr *)(ih+1))->dest),
ntohl(ih->saddr),ntohs(((struct tcphdr *)(ih+1))->source));
/*
* Ok, now the packet is ready for transmission: first simulate a
* receive interrupt on the twin device, then a
* transmission-done on the transmitting device
*/
dest = snull_devs + (dev==snull_devs ? 1 : 0);
priv = (struct snull_priv *) dest->priv;
priv->status = SNULL_RX_INTR;
priv->rx_packetlen = len;
priv->rx_packetdata = buf;
snull_interrupt(0, dest, NULL);
priv = (struct snull_priv *) dev->priv;
priv->status = SNULL_TX_INTR;
priv->tx_packetlen = len;
priv->tx_packetdata = buf;
if (lockup && ((priv->stats.tx_packets + 1) % lockup) == 0) {
/* Simulate a dropped transmit interrupt */
netif_stop_queue(dev);
PDEBUG("Simulate lockup at %ld, txp %ld\n", jiffies,
(unsigned long) priv->stats.tx_packets);
}
else
snull_interrupt(0, dev, NULL);
}
塊設備也以與字符設備register_chrdev、unregister_ chrdev 函數類似的方法進行設備的
注冊與釋放。但是,register_chrdev 使用一個向 file_operations 結構的指針,而register_blkdev
則使用 block_device_operations 結構的指針,其中定義的open、release 和 ioctl 方法和字
符設備的對應方法相同,但未定義 read 或者 write 操作。這是因為,所有涉及到塊設備的
I/O 通常由系統進行緩沖處理。
塊驅動程序最終必須提供完成實際塊 I/O 操作的機制,在 Linux 中,用於這些 I/O 操
作的方法稱為“request(請求)”。在塊設備的注冊過程中,需要初始化request 隊列,這一
動作通過blk_init_queue 來完成,blk_init_queue 函數建立隊列,並將該驅動程序的 request 函
數關聯到隊列。在模塊的清除階段,應調用 blk_cleanup_queue 函數。看看mtdblock 的例子:
static void handle_mtdblock_request(void)
{
struct request *req;
struct mtdblk_dev *mtdblk;
unsigned int res;
50
for (;;) {
INIT_REQUEST;
req = CURRENT;
spin_unlock_irq(QUEUE_LOCK(QUEUE));
mtdblk = mtdblks[minor(req->rq_dev)];
res = 0;
if (minor(req->rq_dev) >= MAX_MTD_DEVICES)
panic("%s : minor out of bound", __FUNCTION__);
if (!IS_REQ_CMD(req))
goto end_req;
if ((req->sector + req->current_nr_sectors) > (mtdblk->mtd->size >> 9))
goto end_req;
// Handle the request
switch (rq_data_dir(req))
{
int err;
case READ:
down(&mtdblk->cache_sem);
err = do_cached_read (mtdblk, req->sector << 9,
req->current_nr_sectors << 9,
req->buffer);
up(&mtdblk->cache_sem);
if (!err)
res = 1;
break;
case WRITE:
// Read only device
if ( !(mtdblk->mtd->flags & MTD_WRITEABLE) )
break;
// Do the write
down(&mtdblk->cache_sem);
err = do_cached_write (mtdblk, req->sector << 9,
req->current_nr_sectors << 9,
req->buffer);
up(&mtdblk->cache_sem);
if (!err)
res = 1;
51
break;
}
end_req:
spin_lock_irq(QUEUE_LOCK(QUEUE));
end_request(res);
}
}
int __init init_mtdblock(void)
{
int i;
spin_lock_init(&mtdblks_lock);
/* this lock is used just in kernels >= 2.5.x */
spin_lock_init(&mtdblock_lock);
#ifdef CONFIG_DEVFS_FS
if (devfs_register_blkdev(MTD_BLOCK_MAJOR, DEVICE_NAME, &mtd_fops))
{
printk(KERN_NOTICE "Can't allocate major number %d for Memory Technology
Devices.\n",
MTD_BLOCK_MAJOR);
return -EAGAIN;
}
devfs_dir_handle = devfs_mk_dir(NULL, DEVICE_NAME, NULL);
register_mtd_user(¬ifier);
#else
if (register_blkdev(MAJOR_NR,DEVICE_NAME,&mtd_fops)) {
printk(KERN_NOTICE "Can't allocate major number %d for Memory Technology
Devices.\n",
MTD_BLOCK_MAJOR);
return -EAGAIN;
}
#endif
/* We fill it in at open() time. */
for (i=0; i< MAX_MTD_DEVICES; i++) {
mtd_sizes[i] = 0;
mtd_blksizes[i] = BLOCK_SIZE;
}
init_waitqueue_head(&thr_wq);
/* Allow the block size to default to BLOCK_SIZE. */
52
blksize_size[MAJOR_NR] = mtd_blksizes;
blk_size[MAJOR_NR] = mtd_sizes;
BLK_INIT_QUEUE(BLK_DEFAULT_QUEUE(MAJOR_NR), &mtdblock_request,
&mtdblock_lock);
kernel_thread (mtdblock_thread, NULL,
CLONE_FS|CLONE_FILES|CLONE_SIGHAND);
return 0;
}
static void __exit cleanup_mtdblock(void)
{
leaving = 1;
wake_up(&thr_wq);
down(&thread_sem);
#ifdef CONFIG_DEVFS_FS
unregister_mtd_user(¬ifier);
devfs_unregister(devfs_dir_handle);
devfs_unregister_blkdev(MTD_BLOCK_MAJOR, DEVICE_NAME);
#else
unregister_blkdev(MAJOR_NR,DEVICE_NAME);
#endif
blk_cleanup_queue(BLK_DEFAULT_QUEUE(MAJOR_NR));
blksize_size[MAJOR_NR] = NULL;
blk_size[MAJOR_NR] = NULL;
}
12.總結
至此,我們可以對 Linux 設備驅動程序的編寫作一總結。驅動的編寫涉及如下主題:
(1)內核模塊、驅動程序的結構;
(2)驅動程序中的並發控制;
(3)驅動程序中的中斷處理;
(4)內核模塊、驅動程序的結構;
(5)驅動程序中的定時器;
(6)驅動程序中的I/O 與內存訪問;
(7)驅動程序與用戶程序的通信。
實際內容相當錯綜復雜,掌握起來有相當地難度。而本質上,這些內容僅分為兩類:(1)
設備的訪問;(2)設備訪問的控制。前者是目的,而為了達到訪問的目的,又需要借助並發
控制等輔助手段。