1. 程式人生 > >教你寫Linux裝置驅動程式:一個簡短的教程

教你寫Linux裝置驅動程式:一個簡短的教程

摘自:http://blog.chinaunix.net/uid-20799298-id-99675.html
原文為 Writing device driver in Linux:A brief tutorial.
該文重點給出了三個例項來講解Linux驅動,使新手快速、從程式碼層瞭解什麼是Linux裝置驅動。
本文算是筆記,大體上是翻譯該文的前兩部分,即前兩個例項,這兩個例子都可能正確成功執行。

檔案: Writing device drivers in Linux.pdf
大小: 216KB
下載: 下載

所需知識

    - C 語言程式設計
    - 微處理器程式設計.對處理器的工作原理有一定的瞭解,如記憶體管理、中斷等

使用者空間和核心空間

寫裝置驅動時,瞭解“使用者空間”和“核心空間”之間的區別是非常重要的。

    - 核心空間。Linux核心簡單並高效地管理著機器的硬體,為使用者提供簡單並
    規範的程式設計介面。同樣地,核心,特別是核心中的驅動,是使用者/程式設計師與硬
    件之間的橋樑或介面。核心的任何例程或函式(比如模組、驅動)都屬於核心
    空間。
    - 使用者空間。使用者程式,比如unix shell或其他的gui應用程式(比如
    kpresenter),都屬於使用者空間。顯然,這些應用程式都要與硬體打交道。但是
    它們不併直接操作硬體,而是通過核心提供的函式來實現。

使用者空間與核心空間之間的介面函式

核心為使用者空間提供了一系列的例程或函式,使用者的應用程式利用這些介面來與硬體互動
。通常,在UNIX或Linux系統中,這種對話是通過函式或子程式來讀寫檔案的。原因是從
使用者的角度來看,UNIX裝置就是檔案。

另一方面,在核心空間中Linux也提供了一系列的函式或子程式來完成底層與硬體的互動
,並允許從核心向用戶空間傳遞資訊。

通常,每個使用者空間的(裝置或檔案允許使用的)函式,都能在核心空間中找到一個類似
的函式,(允許資訊從核心傳遞給使用者空間,反之亦然)

核心空間與硬體裝置之間的介面函式

核心空間中有許多函式用於控制硬體或在核心與硬體之間互動資訊。

第一個驅動:在使用者空間載入和移除驅動

現在將展示如何完成第一個驅動,在核心中將看作模組

新建一個檔案nothing.c如下

include

MODULE_LICENSE(“Dual BSD/GPL”);

2.6.x版本後的核心,編譯模組會略微複雜一點。首先,需要有一份完整的、編譯過的內
核原始碼樹。在下面的文字中,將假設使用2。6。8版本的核心。

其次,需要一個makefile檔案,本例中的makefile檔名為Makefile,內容如下:

obj-m := nothing.o

與之前版本的核心不同,現在編譯模組時使用的核心需要與模組將要載入的核心相同。
編譯上面的檔案,可以使用命令:

make -C /usr/src/kernel-source-2.6.8 M=pwd modules

這個極其簡單的模組就屬於核心空間,一旦其被載入,它就是核心空間的一部分。
在使用者空間,可以使用下面的命令載入它,需要root許可權:

insmod nothing.ko

insmod 這個命令用於為核心載入模組。儘管現在我們已經載入了nothing.ko這個模組,
但是這個模組畢竟沒有任何用處。

可以通過檢視系統裡已載入的模組來檢查是否已經成功載入了nothing.ko

lsmod

最後,需要解除安裝該模組時使用下面的命令:

rmmod nothing

重新使用lsmod,可以發現nothing模組已經不在了。

“Hello world”驅動:在核心空間載入和移除驅動

當一個模組裝置驅動載入到核心,將執行一些初始的工作,如重新設定裝置,reserving
RAM, reserving interrupts, reserving input/output ports, etc.

這些工作得以在核心空間執行,必須要有兩個函式存在:module_init 和
module_exit;它們對應於使用者空間的insmod和rmmod命令。總之,使用者命令insmod和
rmmod使用了核心空間的函式module_init和module_exit.

來看一個經典的程式 HELLO WORLD:

//hello.c
#include 
#include 
#inlucde 

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
        printk("<1> Hello world!\n");
        return 0;
}

static void hello_exit(void)
{
        printk("<1> Bye, cruel world!\n");
}

module_init(hello_init);
module_exit(hello_exit);

其中hello_init 和 hello_exit 函式可以取任意名,但為了載入和移除功能是更容易識
別,它們作為引數傳遞給函式module_init 和 module_exit.

printk函式與printf函式非常類似,但printk只工作在核心中。<1>表示列印資訊
為最高優先順序(數字越低,優先順序越高)。這樣,不僅可以在核心系統日誌中看到該
列印資訊,還能在系統控制檯接收到該列印資訊。

可以用之前的命令來編譯這個模組,此時只需要將模組名加入到Makefile檔案中即可:

obj-m := nothing.o hello.o

本文的其他部分,將Makefile作為給讀者的練習。一個完整的Makefile檔案可以編譯
本教程中的所有示例模組。

當模組被載入或解除安裝時,通過printk列印的資訊將會出現在系統控制檯。如果列印
資訊沒有出現在終端裡,則可通過dmesg命令或檢視系統日誌檔案(cat
var/log/syslog)看到列印資訊。

一個完整的驅動“memory“:此驅動的初始部分

接著將介紹如何構建一個完整的裝置驅動:memory.c。可以從該裝置中讀取一個字元,
也可向其寫入一個字元。這個裝置並沒有實際意義,只是因為它是一個完整的
驅動程式,遂將其作為一個例項來說明。它很容易實現,因為它並不是一個正真的
硬體裝置的介面(除了電腦本身)。

這個驅動中,要新增幾個在裝置驅動程式中頻繁出現的#inclu#include

include

include

include

include

include

include

include

include

include

include

MODULE_LICENSE(“Dual BSD/GPL”);

int memory_open(struct inode *inode, struct file *filp);
int memory_release(struct inode *inode, struct file *filp);
ssize_t memory_read(struct file *filp, char *buf, size_t count, loff_t
*f_pos);
ssize_t memory_write(struct file *filp, char *buf, size_t count, loff_t
*f_pos);
void memory_exit(void);
int memory_init(void);

struct file_operations memory_fops = {
read: memory_read,
write: memory_write,
open: memory_open,
release: memory_release
};

module_init(memory_init);
module_exit(memory_exit);

int memory_major = 60;r *memory_buffer;件之後,聲明瞭幾個後面要定義的函式。在file_operations結構的定義中聲明瞭
幾個通常用來操作檔案的函式。這些在後面會詳細介紹到。接著,向核心宣告初始化和
退出函式-載入和解除安裝模組時使用的。最後,宣告驅動的全域性變數:memory_major表示驅
動的主驅動號,memory_buffer指向一塊用於儲存驅動資料的記憶體區域。

“memory”驅動:裝置與其檔案的連線

在UNIX和Linux中,從使用者空間訪問裝置與訪問檔案相同。這些裝置檔案通常位於/dev目
錄下。

將一個普通檔案與裝置檔案關聯起來需要使用兩個數字:major number 和 minor
number。核心使用major number將一個檔案連結到它的驅動。而minor number是供裝置內
部使用。

要做到這一點,一個檔案(將用於訪問裝置驅動程式)的建立必須使用root身份鍵入以下
命令:

mknod /dev/memory c 60 0

上面這句命令中,c表示建立一個字元裝置,該裝置的主驅動號major number為60,次驅
動號minor number為0。

對於這個驅動,為了在核心空間將其連結到對應的/dev下的檔案,需要使用
register_chrdev函式。呼叫該函式使用到三個引數:major number,
一個字串用於表示該模組的名字,一個file_operations結構。
在安裝模組時它以下面的方式被呼叫:

int memory_init(void)
{
int result;

    result = register_chrdev(memory_major, "memory", &memory_fops);
    if (result < 0) {
            printk("<1>memory: can't obtain major number %d\n",
                    memory_major);
            return result;
    }

    memory_buffer = kmalloc(1, GFP_KERNEL);
    if (!memroy_buffer) {
            result = -ENOMEM;
            goto fail;
    }
    memset(memory_buffer, 0, 1);

    printk("<1> Inserting memory module\n");
    return 0;

fail:
memory_exit();
return result;
}

注意kmalloc函式的使用。這個函式在核心空間中分配一塊用於裝置驅動緩衝區的記憶體。
它的使用方法與著名的malloc函式類似。最後,如果註冊主驅動號失敗或分配記憶體失敗,
這個模組也將失敗。

“memory”驅動:移除驅動

為了在memory_exit函式中移除模組,需要使用到unregsiter_chrdev函式。它將為核心
釋放相應的主驅
void memory_exit(void)
{
unregister_chrdev(memory_major, “memory”);

    if (memory_buffer) {
            kfree(memory_buffer);
    }

    printk("<1> Removing memory module\n");

}移除驅動時還原一個乾淨的核心,在這個函式中同時釋放了驅動的緩衝區。

“memory”驅動:像開啟檔案一樣開啟裝置

核心空間中與使用者空間中開啟檔案(fopen)相對應的是open:呼叫register_chrdev時
使用到了一個file_operations結構,而open正是這個結構的成員。open函式的引數有:
一個inode結構,它向核心傳遞有關主驅動號major number和次驅動號minor number的相
關資訊;一個file結構,該結構中包括操作檔案的多個不同函式。但本文並不對這些函式
作詳細介紹。

當一個檔案被開啟,通常需要初始化驅動變數或重新設定這個裝置。但在這個例子中這
些沒有做這些工作。

memory_open函式如下:

int memory_open(struct inode *inode, struct file *filp)
{
return 0;
}

“memory”驅動:像關閉檔案一樣關閉裝置

與使用者空間中關閉檔案(fclose)相對應的是release:呼叫register_chrdev時使用到一
個file_operations結構,release正是該結構的成員。在本例中,它對應
memory_release函式,與前面類似,它也有兩個引數:inode結構和file結構。

當一個檔案關閉,通常需要釋放已使用的記憶體和任何開啟檔案時關鏈到的變數。但是,同
樣的因為本例十分簡單,這些工作這裡都沒有做。

memory_release函式如下:

int memory_release(struct inode *inode, struct file *filp)
{
return 0;
}

“memory”驅動:讀裝置

同樣,對應於使用者空間讀檔案的fread,這裡用到read:它也是file_operations結構的成
員。這裡它對應memory_read函式。它的引數有:一個file結構;一個緩衝區buf,使用者
空間從該緩衝區中讀資料;一個計數器count記錄傳輸的位元組;最後,還有f_pos,用來
指示從檔案的哪裡開始讀取。

在本例中,memory_read函式使用函式copy_to_user從驅動緩衝區中傳送一個位元組給使用者
ssize_t memory_read(struct *file filp, char *buf,
size_t count, loff_t *f_pos)
{
copy_to_user(buf, memory_buffer, 1);

    if (*f_pos == 0) {
            *f_pos += 1;
            return 1;
    } else {
            return 0;
    }

}置f_pos同時也會改變。如果從檔案開頭讀起,f_pos會以1遞增,並且返回已正確
讀到的位元組數,即1。如果不是從檔案開頭讀起,檔案的結束標誌0將被返回,因為檔案中
沒有資料。

“memory”驅動:寫裝置

與fwrite類似,核心空間有write:它是file_operations結構的成員。本例中為
memory_write,有下面幾個引數:一個file結構;buf緩衝區,供使用者空間寫入;count,
計數器記錄寫入資料的位元組數;f_pos,寫入的ssize_t memory_write(struct file *filp, char *buf,
size_t count, loff_t *f_pos)
{
char *tmp;

    tmp = buf + count - 1;
    copy_from_user(memory_buffer, tmp, 1);
    return 1;

}
copy_from_user將資料從使用者空間傳送到核心空間。

完整的“memory“驅動

加入之前的所有程式碼後,便組成了完整的memory驅動memory.c:

在使用本驅動之前,當然需要先編譯它,方法與前面類似。載入模組:

insmod memory.ko

很方便即可取消對裝置的保護:

chmod 666 /dev/memory

如果一切順利,將有一個裝置/dev/memory存在,且可以將其中寫字串或字元,它將存
儲字串或多個字元中的最後一個。可以像這樣來操作:

echo -n abcdef > /dev/memory

使用cat來檢查這個裝置的內容:

cat /dev/memory

已存的字元不會改變,除非再寫入覆蓋它或這個模組被解除安裝。

附:
例項2 memory 驅動實驗:

程式碼 memory.c

#include <linux/init.h>
//#include 

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/proc_fs.h>
#include <linux/fcntl.h>
//#include 

#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

int memory_open(struct inode *inode, struct file *filp);
int memory_release(struct inode *inode, struct file *filp);
ssize_t memory_read(struct file *filp, char *buf, size_t count, loff_t *f_pos);
ssize_t memory_write(struct file *filp, char *buf, size_t count, loff_t *f_pos);
void memory_exit(void);
int memory_init(void);

struct file_operations memory_fops = {
        read:memory_read,
        write:memory_write,
        open:memory_open,
        release:memory_release
};

module_init(memory_init);
module_exit(memory_exit);

int memory_major = 60;

char *memory_buffer;

int memory_init(void)
{
        int result;

        result = register_chrdev(memory_major, "memory", &memory_fops);
        if (result < 0) {
                printk("<1>memory: can't obtain major number %d\n", memory_major);
                return result;
        }

        memory_buffer = kmalloc(1, GFP_KERNEL);
        if (!memory_buffer) {
                result = - ENOMEM;
                goto fail;
        }
        memset(memory_buffer, 0, 1);

        printk("<1>Inserting memory module\n");
        return 0;

fail:
        memory_exit();
        return result;
}

void memory_exit(void)
{
        unregister_chrdev(memory_major, "memory");

        if (memory_buffer)
                kfree(memory_buffer);

        printk("<1>Removing memory module\n");
}

int memory_open(struct inode *inode, struct file *filp)
{
        return 0;
}

int memory_release(struct inode *inode, struct file *filp)
{
        return 0;
}

ssize_t memory_read(struct file *filp, char *buf, 
        size_t count, loff_t *f_pos)
{
        copy_to_user(buf, memory_buffer, 1);

        if (*f_pos == 0) {
                *f_pos += 1;
                return 1;
        } else
                return 0;
}

ssize_t memory_write(struct file *filp, char *buf, 
        size_t count, loff_t *f_pos)
{
        char *tmp;

        tmp = buf + count - 1;
        copy_from_user(memory_buffer, tmp, 1);

        return 1;
}

Makefile:
obj-m := memory.o

KERNELDIR := /lib/modules/(shellunamer)/buildPWD:=(shell pwd)

modules:
(MAKE)C(KERNELDIR) M=$(PWD) modules

編譯:make
生成檔案中有 memory.ko, 該檔案即要使用的目標模組

載入:sudo insmod ./memory.ko

檢視dmesg資訊:dmesg | tail -n 1
[10911.945739] Inserting memory module

改變操作裝置檔案許可權:sudo chmod 666 /dev/memory

向驅動中寫入資料:echo -n abcdefg > /dev/memory

檢視驅動中儲存的資料:
[[email protected] ~]cat/dev/memoryg[linux@]

可見其為最後寫入的資料。

解除安裝驅動:
[[email protected] ~]sudormmodmemory[linux@] dmesg | tail -n 2
[10911.945739] Inserting memory module
[11155.809076] Removing memory module

—————————-實驗完畢
分析
上面程式碼中主要有五個函式重點注意下:
register_chrdev
unregister_chrdev
copy_to_user
copy_from_user
kmalloc

/*
* 成功:返回0
* 失敗:-EINVAL表示申請的主裝置號非法(可能是主裝置號大於最大裝置號)
* -EBUSY 表示所申請的主裝置號已為其它裝置使用
* 如果動態分配成功,則此函式將返回主裝置號
*
*/
static inline int register_chrdev(
unsigned int major, //裝置驅動向核心申請主裝置號,若為0則系統動態分配一個主裝置號
const char *name, //裝置名
const struct file_operations *fops //各呼叫的入口點
);

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

arch/x86/lib/usercopy_32.c
/**
* copy_to_user: - Copy a block of data into user space.
* @to: Destination address, in user space.
* @from: Source address, in kernel space.
* @n: Number of bytes to copy.
*
* Context: User context only. This function may sleep.
*
* Copy data from kernel space to user space.
*
* Returns number of bytes that could not be copied.
* On success, this will be zero.
*/
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

/**
* copy_from_user: - Copy a block of data from user space.
* @to: Destination address, in kernel space.
* @from: Source address, in user space.
* @n: Number of bytes to copy.
*
* Context: User context only. This function may sleep.
*
* Copy data from user space to kernel space.
*
* Returns number of bytes that could not be copied.
* On success, this will be zero.
*
* If some data could not be copied, this function will pad the copied
* data to the requested size using zero bytes.
*/
unsigned long _copy_from_user(void *to, const void __user *from, unsigned long n);

在核心中動態開闢記憶體
void *kmalloc(size_t size, int flags);
size:要分配記憶體的大小
flags:分配標誌,以幾個方式控制kmalloc的行為