開發 driver 需要的基礎知識

以 Character device driver 為例,學會 user process 將如何透過 device file 與 driver 作溝通。

5-1、執行環境

裝置驅程程式屬於 kernel 程式碼的一部分, driver 在 kernel space 運作。
kernel 的程式分成兩種 context:
Process context (一般行程執行環境)
Interrupt context (中斷執行環境)
Process context 也稱為 user context 或 thread context。
另外還有「kernel context(核心執行環境)」這個稱呼,主要代表在 kernel 內執行的程式碼,可能是 user context 也可能是 interrupt context。
Process context 可以 sleep,可以被 preempt,處理時間可以拖長沒關係。
Interrupt context 不可以 sleep,也不可以被 preempt,處理時間要極力縮短。
但就算是 process context ,如果控制權回到 kernel 之內的話,在 kernel 內的 process context 是不能被 preempt 的。
在 Interrupt context 之內,必須確定用到的 kernel 函式都不會 sleep。
雖然前面提到 kernel space 不能被 preempt,但只要啟動 kernel 配置選項的「CONFIG_PREEMPT」,kernel space 也會變成可以 preempt,切換執行內容的時機僅限於 spin lock 鎖定的地方。
Process type and features -> Preemption Model -> Preemptible Kernel

5-2、資料模型

Data model(資料模型) 指的是 C 語言的 int、long 等等資料型別分別佔用多少 bytes。
在 ANSI C 的規格中, int、long 等資料型別的大小都沒明確定義,完全視執行環境而定。
如果要無視平臺差異、使用固定大小的資料型別的話,就要用 u8 或 u32 之類的 typedef。

typedef signed char s8;
typedef unsigned char u8;
typedef signed short s16;
typedef unsigned short u16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed long s64;
typedef unsigned long u64;

5-3、Endian

有 endian 差異的部分主要包含: CPU Bus(PCI 或 USB 等等) 網路封包 EEPROM 等的資料內容 現在由於 Intel 與 Microsoft 的成功,因此世界的主流是 little endian。
Intel 主導訂定的 PCI bus 規格也採用 little endian。
網路封包由於歷史因素,多半使用 big endian。
EEPROM 之類的 NVRAM(Non Volatile Ram) 儲存資料的格式會在儲存時固定下來,所以 driver 在讀寫 NVRAM 時必須注意 endian 的不同。
在 driver 需要自己應對 endian 差異的,可以用事先定義好的巨集來交換 byte 順序,這些巨集定義在 「include/linux/byteorder/generic.h」標頭檔內:

htonl():將 4 bytes 資料從 host endian 轉成 network byte order(big endian)。
ntohl():將 4 bytes 資料從 network byte order(big endian) 轉成 host endian。
htons():將 2 bytes 資料從 host endian 轉成 network byte order(big endian)。
ntohs():將 2 bytes 資料從 network byte order(big endian) 轉成 host endian。

  • little endian:
int i = 0x12345678
Copy i to char array[4]:
array[0]: 0x7fff929d1c70 = 0x78
array[1]: 0x7fff929d1c71 = 0x56
array[2]: 0x7fff929d1c72 = 0x34
array[3]: 0x7fff929d1c73 = 0x12

5-4、對齊

Page 邊界 有些硬體要求指標一定要指在 page 的開頭,這時 driver 就必須對齊邊界: 新指標 = (記憶體指標 + PAGE_SIZE -1) & ~(PAGE_SIZE-1)

struct 的成員邊界

5-5、連結串列

Stack(堆疊) 類型的 Linked List
Queue(佇列) 類型的 Linked List

5-6、虛擬記憶體

driver 的開發需要瞭解 OS 是如何管理記憶體的,因為 driver 需要與一般應用程式(user space) 交換資料,所以必須知道 kernel space 與 user space 的記憶體有什麼差別才行。

實體記憶體 電腦安裝的記憶體模組稱為「實體記憶體」(physical memory;或稱「物理記憶體」)。 有一些 OS 會直接在物理記憶體內執行 kernel 與一般應用程式。

虛擬記憶體 隨著應用程式功能不斷擴充,直接在物理記憶體上執行應用程式越來越容易影響系統穩定,因此「虛擬記憶體」(Virtual Memory) 的想法就誕生了。 在應用程式載入記憶體時,把它配置在虛擬記憶體內,如此,應用程式執行時,就不會直接影響到物理記憶體了。

Process 在讀寫記憶體時,CPU 會參考 page table 將虛擬位址轉換成物理位址,接著讀寫物理記憶體空間。

如果物理記憶體已經沒有空間的話,將沒辦法再分配記憶體來使用,為了應對這情況,linux 引進了「swap」的觀念。
要為新的 process 分配虛扯記憶體空間時,先將物理記憶體內沒用到的資料移到硬碟內(swap out),等到舊 process 開始再次執行時,再將硬碟裡的資料讀回物理記憶體(swap in)。

driver 之間的通訊
driver 要呼叫 kernel 之中其它 driver 提供的函式、或是讀寫相關變數時,可以直接透過指標進行。
Kernel space 之間的記憶體空間是共用的,可以把 kernel 想成一個獨立的 process 來看待。
在 Linux kernel 之內想複製記憶體內容時,也可以用 ANSI C 提供的 memcy() 函式,其相關函式都實作在 「lib/string.c」檔案裡。

User Process 與 driver 的通訊
User process 可透過 read() 與 write() 系統呼叫與 driver 交換資料。
User process 與 kernel 分別在不同的(虛擬)記憶體空間內運作,因此兩者之間無法直接讀寫記憶體。
IA-32版的 Linux 在 user context 之內,是把 4GB 虛擬記憶體空間的下半部 3GB 分配給 user process 使用,上半部 1GB 給 kernel 使用,如此就能從 kernel 直接讀寫 user process 的記憶體空間了,但在讀寫之前,須確認目的地:

  • 是不是合法位址
  • 有沒有被 swap out

因此,在 user process 與驅動程式之間搬移資料的時候,光呼叫 memcpy() 是不夠的。
所以,Linux kernel 準備了在 user process 與驅動程式之間搬移資料的 kernel 函式與巨集,讓裝置驅動程式使用:

int get_usr(x, ptr)、int put_usr(x, ptr)
int access_ok(int type, void *addr, unsigned long size)
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
unsigned long copy_to_user(void *to, const void __user *from, unsigned long n);
unsigned long clear_user(void __user *to, unsigned long n)

使用這些函式時,需要 include 「asm/uaccess.h」
這些函式都可能 sleep(在 user process 的記憶體被 swap out 的時候),所以只能在 user context 使用。

5-7、交換資料

說明 user process 與 character 類型的裝置驅動程式交換資料的方法。

device file 的角色 User process 要向 driver 送資料、或是從 driver 取得資料時,需要有「某種東西」把 user process 與 driver 連起來。
扮演這個角色的就是「device file」以及「device special file」。
裝置檔通常放在 /dev 目錄內。
原則上一個裝置就要準備一個 device file。
前面說是「特殊」的檔案,用 ls 就能看出與一般檔案有點差異,最左邊的字元代表裝置檔的種類:

ls -l /dev/ttyS0

b:區塊裝置(block device)
c 或 u:字元裝置(character/unbuffered device)
p:具名管線,或稱 FIFO(named pipe) 平常顯示檔案大小的地方變成兩個數值,分別是 major number 及 minor number。

建立與刪除 device file
裝置檔可透過 mknod 指令建立。
刪除的時候,直接用 rm 即可。
目前 Linux 的 major number 是 12-bit,minor number 是 20-bit,Major/minor number 可以使用的 bit 數定義在「/include/linux/kdev_t.h」裡。

Major / Minor Number 扮演的角色
User process 打開裝置檔時,kernel 看到開檔的是系統呼叫,就算 user process 是呼叫 fopen() 這類函式庫函式,最終還是由 glibc 對 kernel 送出 open() 系統呼叫。
Kernel 的 open() 系統呼叫介面是 sys_open() 函式,但是隻知道開啟 device file 的話,就不知道要找哪個驅動程式處理了。
這邊就要用到 「major number」,驅動程式載入時,可以向 kernel 登記 major number,每個驅動程式都有獨一無二的 major number。
Major number 與指向驅動程式的指標存放在 kobj_map 對應表內。
Kernel 的 sys_open() 函式會在 user process 開啟裝置檔時得知 major number,然後由此查詢 kobj_map,如此,透過 major number 即可把裝置檔與對應的驅動程式結合起來。
Minor number 是由驅動程式自己管理的編號。
驅動程式在同時控制許多個裝置時,通常會用 minor number 來區分各個裝置。
Major/minor number 在系統內必須獨一無二,所以 Linux kernel 在原始檔的 「Documentation/devices.txt」列出所有 major/minor number。
但現在的主流是動態分配 major number。
Linux kernel 是以 「dev_t」這個型別來表現 major/minor number,它被 typedef 成 unsigned int:
typedef u32 kernel_dev_t;
typedef __kernel_dev_t dev_t;
想從 dev_t 變數取出 major / minor number 時,可以使用 MAJOR() 與 MINOR() 巨集。
想結合 major / minor number 建立 dev_t 時,則是用 MKDEV() 巨集。

5-8、User Process 與 driver 的通訊管道

User process 以 open() 系統呼叫打開 device file 之後,可以:
透過 write() 系統呼叫將資料傳給驅動程式。
透過 read() 系統呼叫從驅動程式取得資料。
處理完畢後,再以 close() 系統呼叫關閉 device file。
除了上述幾個系統呼叫外,還有下面這些可以用:

seek()
poll() 或 select()
ioctl()
mmap()
fcntl()

但如果驅動程式沒有為這些系統呼叫準備對應的 handler,user process 就無法使用。

5-9、Major Number 的登記方式

為了將 device file 與驅動程式連起來,驅動程式必須向 kernel 登記「major number」,先前 major number 是靜態的,最近改採動態管理方式為主流。

靜態(傳統)登記法 靜態登記為 Linux 2.4 的標準作法, Linux 2.6 已不推薦這個作法。
是以使用 register_chrdev() 登記:
int register_chrdev(unsigned int major, const char name, const struct file_operations fops);
這個函式是在 「fs/char_dev.c」檔案內實作的。
major 引數是要使用的 major number,name 引數則是驅動程式名稱,fops 引數則是驅動程式提供的系統呼叫處理介面。
登錄成功後,就會在 /proc/devices 顯示驅動程式名稱及 major number。
major 引數也可傳入 「0」,如此 kernel 會自動分配一個還沒用到的 major number。
卸載驅動程式時,要用 unregister_chrdev() 函式跟 kernel 刪除 major number。
void unregister_chrdev(unsigned int major, const char *name); 第一個引數(major) 是 register_chrdev() 函式分配的 「major number」。
第二個引數(name) 是驅動程式的名稱。

動態登記法 1、以 alloc_chrdev_region() 動態取得 major number。
2、以 cdev_init() 登記系統呼叫 handler。
3、以 cdev_add() 向 kernel 登記驅動程式。
在卸載驅動程式時,則依相反步驟解除登記:
1、以 cdev_del() 向 kernel 釋放驅動程式。
2、以 unregister_chrdev_region() 釋放 major number。

int alloc_chrdev_region(dev_t dev, unsigned baseminor, unsigned count, const char name);

alloc_chrdev_region() 是依驅動程式名稱用來取得 major number,並從指定的起點開始預留指定數目的 minor number。

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

cdev_init() 負責初始化 cdev 結構,並登記系統呼叫 handler(fops),另外,其 cdev 結果變數在卸除驅動程式時還要用到,需定義為全域變數。
在呼叫 cdev_init() 後,還要將 cdev 結構變數的 owner 成員設為 「THIS_MODULE」。
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
cdev_add 向 kernel 登記 cdev_init() 設定好的裝置資訊。
void dcdev_del(struct cdev *p)
cdev_del() 的功能與 cdev_add() 相反,會從 kernel 釋放驅動程式。
void unregister_chrdev_region(dev_t from, unsigned count)
unregister_chrdev_region() 的功能跟 alloc_chrdev_region() 相反,會釋放之前拿到的 major number。

5-10、系統呼叫處理函式的登記方式 說明系統呼叫 handler 的相關細節。

驅動程式的 handler

想讓 user process 能對驅動程式發出 open() 與 read() 之類系統呼叫的話,驅動程式必須先向 kernel 登記相應的 handler 才行。

驅程程式的 handler 是透過 file_operations 結構指定的,其宣告在 include/linux/fs.h 內。
C 語言在初始化 struct 的時候需要記得成員的定義位置,但如果用上 gcc
擴充語法的話,就不必記得這些位置,比如說,可以寫成下面這個樣子:

struct file_operations devone_fops = {
    .open = devone_open,
    .release = devone_close,
}

這樣就會將 open 及 release 成員指定為驅動程式提供的函式指標,並自動把其它成員設為 NULL。

open handler

User process 在操作 device file 時,第一個動作一定是「開啟」
,結束操作之後則會將它「關閉」。
因此,驅動程式至少要提供 open 與 close 這兩個 handler。

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

inode 引數是內含「inode」資料的結構指標,開發驅動程式時會用到的成員如下:
bdev_t i_rdev: Major/minor number
void *i_private: 驅動程式私有指標
驅動程式可以透過 i_rdev 成員判別驅動程式內部使用的 minor
number,想改變控制硬體對象時,可以使用。
取出 minor number 的方法有兩種,如下:

ret = MINOR(inode->i_rdev);
ret = iminor(inode)

考慮到可攜性,還是使用 iminor() 比較好,想得到 major number ,亦是使用同樣的方法:

unsigned iminor(const struct inode *inode); unsigned imajor(const struct inode *inode);
i_private 成員是驅動程式可以自由使用的指標。
file 也是個巨大的資料結構,開發驅動程式常用到的成員如下:
struct file_operations *f_op:系統呼叫 handlers
unsigned int f_flags:open 函式第二引數傳入的旗標
void *private_data:驅動程式私有資料指標
f_op 成員是驅動程式載入時,登記的系統呼叫 handles 指標照對表。
f_flags 成員是 user process 呼叫 open() 時指定的旗標。
private_data 成員是驅動程式可以自由使用的指標。

close Handler

close handler 的 prototype 如下,這邊要注意它的成員名稱是「release」: int (release) (struct inode inode, struct file *file); 引數與 open handler 一樣,被開啟時如果有配置資源的話,一定要在 close handler 之內釋放掉,否則會導致 memory leak 的問題,只要 OS 沒有重開,就無法釋放資源。 就算 user process 忘記明確呼叫 close(),OS 也會在 user process 結束的時候呼叫 close()。 一個或多個 user process 可能同時重複開啟同一個 device file,想在 close handler 被呼叫時,得知目前是對應哪個 open() 開啟的話,可以透過 inode 或 file 結構的驅動程式私有指標來判斷,下個主題會談到。

inode 結構與 file 結構的關係 open 與 close handler 都會從 kernel 接到 inode 結構與 file 結構的指標,而這兩個指標都有給驅動程式的私用指標。 inode 結構在 mknod 指令建立 device file 時由 kernel 建立,而 file 結構是每次開檔時由 kernel 建立。 也就是說,驅動程式想區分 open 系統呼叫的話,應該透過 file 結構的驅動程式私有指標來進行,open handler 配置資源時將之給「file->private_data」,close handler 釋放資源時也是從 「file->private_data」將之釋放。 inode 結構的驅動程式私有指標,可以在存放 device file 相關資訊時使用。 父 process 透過 fork() 系統呼叫產生子 process 時,檔案 handle 會被共用,所以 file 結構只會有一個,但此時因為 file 會被兩個 process 共同使用,所以 reference count 改成「2」,在這兩個 process 呼叫 close() 時,reference count 都會減一,直到 reference 變成「0」時,才會呼叫驅動程式的 close handler,之後 kernel 會釋放不再需要用到的 file 結構。

使用 open 與 close 的範例程式 這隻驅動程式會登記 major number,只支援 open 與 close handler。


/*
 * devone.c - static registration major number with legacy interface
 *
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <asm/current.h>
#include <asm/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

#define DRIVER_NAME "devone"

static unsigned int devone_major = 0;
module_param(devone_major, uint, 0);

static int devone_open(struct inode *inode, struct file *file)
{
    printk("%s: major %d minor %d (pid %d)\n", __func__,
           imajor(inode),
           iminor(inode),
           current->pid
          );

    inode->i_private = inode;
    file->private_data = file;

    printk("  i_private=%p private_data=%p\n",
           inode->i_private, file->private_data);

    return 0;     /* success */
}

static int devone_close(struct inode *inode, struct file *file)
{
    printk("%s: major %d minor %d (pid %d)\n", __func__,
           imajor(inode),
           iminor(inode),
           current->pid
          );
    printk("  i_private=%p private_data=%p\n",
           inode->i_private, file->private_data);

    return 0;     /* success */
}

struct file_operations devone_fops = {
    .open = devone_open,
    .release = devone_close,
};

static int devone_init(void)
{
    int major;
    int ret = 0;

    major = register_chrdev(devone_major, DRIVER_NAME, &devone_fops);
    if ((devone_major > 0 && major != 0) ||        /* static allocation */
            (devone_major == 0 && major < 0) ||    /* dynamic allocation */
            major < 0) {                           /* else */
        printk("%s driver registration error\n", DRIVER_NAME);
        ret = major;
        goto error;
    }
    if (devone_major == 0) {   /* dynamic allocation */
        devone_major = major;
    }

    printk("%s driver(major %d) installed.\n", DRIVER_NAME, devone_major);

error:
    return (ret);
}

static void devone_exit(void)
{
    unregister_chrdev(devone_major, DRIVER_NAME);

    printk("%s driver removed.\n", DRIVER_NAME);

}

module_init(devone_init);
module_exit(devone_exit);

Major number 是以 register_chrdev() 登記的,第一個引數傳入0的話,就會動態分配,但是為了在 insmod 時可以透過引數指定,所以用了 module_param 巨集:

static unsigned int devone_major = 0; 
module_param(devone_major, uint, 0);

module_param 巨集的第二引數是變數的資料型別,用了這個巨集後,就可以下列方式透過引數改變全域變數的初始值:

/sbin/insmod ./sample.ko devone_major=261

open 與 close handler 是在 process context 內運作的,所以可以透過 current 巨集取得呼叫 open 或 close 的 process 資訊,使用 current 巨集,要引入兩個標頭檔:

#include <linux/sched.h>
#include <asm/current.h>

open handler 會把 inode 結構與 file 結構的指標分別放到各自驅動程式私有成員內

  • Makefile
CFILES = devone.c 

obj-m += sample.o
sample-objs := $(CFILES:.c=.o)

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

make 之後得到 sample.ko ,接著載入驅動程式,瀏覽「/proc/devices」可以看到驅動程式分配的 major number,再用這個號碼透過 mknod 建立 device file:

sudo /sbin/insmod ./sample.ko 
/sbin/lsmod | grep sample 
cat /proc/devices | grep devone 
sudo /bin/mknod /dev/devone c 250 0

ls -l /dev/devone 可以看到 device file 的最初權限是「644」,如果要讓一般使用者也可以使用,就必須修改權限:

sudo /bin/insmod --mode=666 /dev/devone c `grep devone /proc/devices | awk '{print $1}'` 0

接著是 user process 的應用程式,只會打開、接著關閉 device file。

  • simple.c
/*
 * # cc simple.c && ./a.out
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define DEVFILE "/dev/devone"

int open_file(void)
{
    int fd;

    fd = open(DEVFILE, O_RDWR);
    if (fd == -1) {
        perror("open");
    }
    return (fd);
}

void close_file(int fd)
{
    if (close(fd) != 0) {
        perror("close");
    }
}

int main(void)
{
    int fd;

    fd = open_file();

    sleep(20);

    close_file(fd);

    return 0;
}

連續執行這個程式兩次,kernel buffer 會顯示驅動程式的訊息,可以發現 inode 結構的指標都是同一個,但 file 結果指標則不同。

接著透過 fork() 複製 process 看看,子 process 關閉之前打開的 file handle,父 process 也會關閉 file handle。

  • fork.c
/*
 * # cc fork.c && ./a.out
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define DEVFILE "/dev/devone"

int open_file(void)
{
    int fd;

    fd = open(DEVFILE, O_RDWR);
    if (fd == -1) {
        perror("open");
    }
    return (fd);
}

void close_file(int fd)
{
    printf("%s called\n", __func__);
    if (close(fd) != 0) {
        perror("close");
    }
}

int main(void)
{
    int fd;
    int status;

    fd = open_file();

    if (fork() == 0) {  /* child process */
        sleep(3);
        close_file(fd);
        exit(1);
    }
    wait(&status);
    sleep(10);
    close_file(fd);

    return 0;
}

編譯執行後,比對 kernel buffer 的訊息,可以發現最初 close() 時,不會呼叫驅動程式的 close handler。 使用 register_chrdev() 時,驅動程式可以自由使用 minor number,就算 mknod 隨意指定 minor number,user process 還是可以開啟成功,但是可以用到的 minor number 範圍僅限 0 ~ 255。 使用 cdev_add() 時,驅動程式需要明確指出想使用的 minor number 範圍。

使用 open 與 close 的範例程式 - cdev_add() [kernel 2.6 推薦] 把上一節的程式改寫成使用 cdev_add() 的形式,不同的地方只有驅動程式的載入、卸除部分,open 與 close handler 直接延用:

/*
 * devone.c
 *
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <asm/current.h>
#include <asm/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

#define DRIVER_NAME "devone"

static int devone_devs = 2;        /* device count */
static int devone_major = 0;       /* dynamic allocation */
module_param(devone_major, uint, 0);
static struct cdev devone_cdev;

static int devone_open(struct inode *inode, struct file *file)
{
    printk("%s: major %d minor %d (pid %d)\n", __func__,
           imajor(inode),
           iminor(inode),
           current->pid
          );

    inode->i_private = inode;
    file->private_data = file;

    printk("  i_private=%p private_data=%p\n",
           inode->i_private, file->private_data);

    return 0;
}

static int devone_close(struct inode *inode, struct file *file)
{
    printk("%s: major %d minor %d (pid %d)\n", __func__,
           imajor(inode),
           iminor(inode),
           current->pid
          );
    printk("  i_private=%p private_data=%p\n",
           inode->i_private, file->private_data);

    return 0;
}

struct file_operations devone_fops = {
    .open = devone_open,
    .release = devone_close,
};


static int devone_init(void)
{
    dev_t dev = MKDEV(devone_major, 0);
    int alloc_ret = 0;
    int major;
    int cdev_err = 0;

    alloc_ret = alloc_chrdev_region(&dev, 0, devone_devs, DRIVER_NAME);
    if (alloc_ret)
        goto error;
    devone_major = major = MAJOR(dev);

    cdev_init(&devone_cdev, &devone_fops);
    devone_cdev.owner = THIS_MODULE;

    cdev_err = cdev_add(&devone_cdev, MKDEV(devone_major, 0), devone_devs);
    if (cdev_err)
        goto error;

    printk(KERN_ALERT "%s driver(major %d) installed.\n", DRIVER_NAME, major);

    return 0;

error:
    if (cdev_err == 0)
        cdev_del(&devone_cdev);

    if (alloc_ret == 0)
        unregister_chrdev_region(dev, devone_devs);

    return -1;
}

static void devone_exit(void)
{
    dev_t dev = MKDEV(devone_major, 0);

    cdev_del(&devone_cdev);
    unregister_chrdev_region(dev, devone_devs);

    printk(KERN_ALERT "%s driver removed.\n", DRIVER_NAME);
}

module_init(devone_init);
module_exit(devone_exit);

Makefile 同上例

devone_devs 全域變數為使用的 minor number 個數,此程式以 2 為初始值,所以只有「0」與「1」這兩個 minor number 可以使用。 即使空用 mknod 建立了 minor number 0 ~ 3 的裝置檔,user process 也只能 open 「0」或「1」的裝置檔,對於「2」及「3」的裝置檔會產生 No such device or address 的錯誤訊息。 user process 的範例程式碼如下(simple.c):

/*
 * # cc simple.c && ./a.out
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define DEVFILE "/dev/devone"
#define DEVCOUNT 4

int open_file(char *filename)
{
    int fd;

    fd = open(filename, O_RDWR);
    if (fd == -1) {
        perror("open");
    }
    return (fd);
}

void close_file(int fd)
{
    if (close(fd) != 0) {
        perror("close");
    }
}

int main(void)
{
    int fd[DEVCOUNT];
    int i;
    char file[BUFSIZ];

    for (i = 0 ; i < DEVCOUNT ; i++) {
        snprintf(file, sizeof(file), "%s%d",DEVFILE, i);
        printf("%s\n", file);
        fd[i] = open_file(file);
    }

    sleep(5);

    for (i = 0 ; i < DEVCOUNT ; i++) {
        printf("closing fd[%d]\n", i);
        close_file(fd[i]);
    }

    return 0;
}

read Handler 與 write Handler 這邊要介紹 read 與 write handler 的實作方式:

ssize_t *read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);

只要這樣定義 handler,驅動程式就能把資料傳給 user process。 filp 引數指向開啟裝置檔時 kernel 建立的 file 結構,跟 open 收到的指標是同一個,因此,open handler 設給「filp->private_data」成員的指標,在 read handler 裡也可以拿來用。 buf 引數是 user process 呼叫 read() 時指定的緩衝區指標,驅動程式無法直接取用 buf 指標,必須透過 copy_to_user() 這個 kernel 提供的函式把資料複製過去,原因是之前所提有關「kernel space」與「user space」的不同。 count 引數是 user process 呼叫 read() 時提供的緩衝區空間。 f_pos 引數是 offset,驅動程式可以視需要將之更新。

ssize_t *write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);

write handler 的 prototype 與 read handler 大致相同。 write handler 是在 user process 要傳資料給驅動程式時使用的,驅動程式需透過 kernel 提供的 copy_from_user() 函式從 buf 引數讀入資料,緩衝區的大小是 count bytes。 用 read 與 write 寫個驅動程式測試看看,User Process 在一開始在讀檔時,會全讀到 0xff,之後再寫入 1 byte 資料,再讀出寫入後的資料。

  • devone.c
/*
 * devone.c
 *
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <asm/current.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

MODULE_LICENSE("Dual BSD/GPL");

#define DRIVER_NAME "devone"

static int devone_devs = 1;        /* device count */
static int devone_major = 0;       /* dynamic allocation */
module_param(devone_major, uint, 0);
static struct cdev devone_cdev;

struct devone_data {
    unsigned char val;
    rwlock_t lock;
};

ssize_t devone_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct devone_data *p = filp->private_data;
    unsigned char val;
    int retval = 0;

    printk("%s: count %d pos %lld\n", __func__, count, *f_pos);

    if (count >= 1) {
        if (copy_from_user(&val, &buf[0], 1)) {
            retval = -EFAULT;
            goto out;
        }
        //printk("%02x ", val);

        write_lock(&p->lock);
        p->val = val;
        write_unlock(&p->lock);
        retval = count;
    }

out:
    return (retval);
}

ssize_t devone_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct devone_data *p = filp->private_data;
    int i;
    unsigned char val;
    int retval;

    read_lock(&p->lock);
    val = p->val;
    read_unlock(&p->lock);

    printk("%s: count %d pos %lld\n", __func__, count, *f_pos);

    for (i = 0 ; i < count ; i++) {
        if (copy_to_user(&buf[i], &val, 1)) {
            retval = -EFAULT;
            goto out;
        }
    }

    retval = count;
out:
    return (retval);
}

static int devone_open(struct inode *inode, struct file *file)
{
    struct devone_data *p;

    printk("%s: major %d minor %d (pid %d)\n", __func__,
           imajor(inode),
           iminor(inode),
           current->pid
          );

    p = kmalloc(sizeof(struct devone_data), GFP_KERNEL);
    if (p == NULL) {
        printk("%s: Not memory\n", __func__);
        return -ENOMEM;
    }

    p->val = 0xff;
    rwlock_init(&p->lock);

    file->private_data = p;

    return 0;
}

static int devone_close(struct inode *inode, struct file *file)
{
    printk("%s: major %d minor %d (pid %d)\n", __func__,
           imajor(inode),
           iminor(inode),
           current->pid
          );

    if (file->private_data) {
        kfree(file->private_data);
        file->private_data = NULL;
    }

    return 0;
}

struct file_operations devone_fops = {
    .open = devone_open,
    .release = devone_close,
    .read = devone_read,
    .write = devone_write,
};


static int devone_init(void)
{
    dev_t dev = MKDEV(devone_major, 0);
    int alloc_ret = 0;
    int major;
    int cdev_err = 0;

    alloc_ret = alloc_chrdev_region(&dev, 0, devone_devs, DRIVER_NAME);
    if (alloc_ret)
        goto error;
    devone_major = major = MAJOR(dev);

    cdev_init(&devone_cdev, &devone_fops);
    devone_cdev.owner = THIS_MODULE;

    cdev_err = cdev_add(&devone_cdev, MKDEV(devone_major, 0), devone_devs);
    if (cdev_err)
        goto error;

    printk(KERN_ALERT "%s driver(major %d) installed.\n", DRIVER_NAME, major);

    return 0;

error:
    if (cdev_err == 0)
        cdev_del(&devone_cdev);

    if (alloc_ret == 0)
        unregister_chrdev_region(dev, devone_devs);

    return -1;
}

static void devone_exit(void)
{
    dev_t dev = MKDEV(devone_major, 0);

    cdev_del(&devone_cdev);
    unregister_chrdev_region(dev, devone_devs);

    printk(KERN_ALERT "%s driver removed.\n", DRIVER_NAME);
}

module_init(devone_init);
module_exit(devone_exit);

Makefile 同上例

user process program

  • sample.c
/*
 * # cc test.c && ./a.out
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define DEVFILE "/dev/devone"

int open_file(char *filename)
{
    int fd;

    fd = open(filename, O_RDWR);
    if (fd == -1) {
        perror("open");
    }
    return (fd);
}

void close_file(int fd)
{
    if (close(fd) != 0) {
        perror("close");
    }
}

void read_file(int fd)
{
    unsigned char buf[8], *p;
    ssize_t ret;

    ret = read(fd, buf, sizeof(buf));
    if (ret > 0) {
        p = buf;
        while (ret--)
            printf("%02x ", *p++);
    } else {
        perror("read");
    }

    printf("\n");
}

void write_file(int fd, unsigned char val)
{
    ssize_t ret;

    ret = write(fd, &val, 1);
    if (ret <= 0) {
        perror("write");
    }
}

int main(void)
{
    int fd;
    int i;

    for (i = 0 ; i < 2 ; i++) {
        printf("No. %d\n", i + 1);

        fd = open_file(DEVFILE);

        read_file(fd);

        write_file(fd, 0x00);
        read_file(fd);

        write_file(fd, 0xC0);
        read_file(fd);

        close_file(fd);
    }

    return 0;
}

此範此的精髓在於 struct devone_data 的應用,包含記憶體的配置、指檔的運用…等。 open handler 及 close handler 因為使用了 BKL(Big Kernel Lock) ,所以目前的版本無法同時執行多個呼叫。 read handler 與 write handler 會從 file 結構參考 private_date 成員,從而得到數值。 不管是 read handler 或 write handler 都可能同時呼叫多次,所以需要寫成 reentrant(可重進入) 的形式,有需要的話,就得用上 semaphore 做鎖定。 此範例透過 reader/writer lock 來控制 devone_data 結構成員的修改動作,就是為了預防同時執行 p->val = val; 時會發生衝突的問題。

透過 Minor Number 切換 Handler

Minor number 是驅動程式自己管理的,如果希望依不同的 minor number 提供不同功能時,若在 handler 內以 if 判斷 minor number 的話,會寫出不易閱讀的套疊程式碼。 open handler 會拿到 file 結構指標,如果能在驅動程式這邊更換 f_op 成員的話,也能依照 minor number 切換 handler,程式碼較為易讀。

static int devone_open(struct inode *inode, struct file *file) {
    swtich (iminor(inode)) {
    case 0:
        file->f_op = &zero_fops;
        break;

    case 1:
        file->f_op = &one_fops;
        break;
    default:
        return -ENXIO;
    }

    if (file->f_op && file->f_op->open)
        return file->f_op->open(inode, file);

    return 0;
}

其中 zero_fops, one_fops 是 struct file_operations 的變數,各自定義自己的 handler。

5-11、udev

將 character 或 block 類型的裝置驅動程式載入 kernel 後,要在 /dev 目錄下建立對應的裝置檔,隨著硬體種類不斷增加,這個方法的破綻也逐漸顯現。 因此出現了希望在 OS 偵測到新硬體時,可以自動建立裝置檔,透過 hotplug 之類的機制,也希望能做到在移除裝置時,自動刪除裝置檔。 /dev 是在 RAM 裡面的檔案系統,所以用 mknod 建立的裝置檔,重開機之後就會消失。 在 OS 啟動時,因為要動態產生 /dev 內容的關係,所以需要稍花時間,kernel 在進入 runlevel 3 之後的 「Starting udev」就是建立裝置檔的地方。 在 init daemon 開始執行之前,/dev 是磁碟上的目錄,要在 RAM 裡頭建立 udev 之前,須先 unmount 才行。 啟動時 init daemon 的 rc script(/etc/rc.d/rc.sysinit) 會呼叫「/sbin/start_udev」 script 建立裝置檔。 為了讓 udev 能夠執行,/dev 目錄下還是要有一些最基本的裝置檔,這些裝置檔要用 MAKEDEV 手動建立。 手動建立的裝置檔清單可在 /etc/udev/makedev.d 目錄內找到。 啟動 OS 後載入驅動程式的話,會對系統新增機置,udevd daemon 會偵側到這個事件,而後去檢查 /sys 目錄。 如果驅動程式建立了「dev」標案的話,檔案裡會有 major/minor number,如此 udevd 就能以它建立裝置檔。 建立裝置檔的規則設定位於 /etc/udev/rules.d/ 內。

驅動程式的 udev 支援 要讓驅動程式支援 udev 的話,必須登錄驅動程式的 class,並在 /sys/class 目錄下建立驅動程式資訊,此外,還要配合需求提供規則檔。 舉例要新建「/sys/class/devone/devone0/dev」。 首先是登記 class,登記 class 是使用 class_create() 這個 kernel 函式,其 prototype 定義在 linux/device.h 內:

class *class_create(struct module *owner, const char *name);

第二引數 name 用來指定 class 的名稱,按上面的例子來說,就是要指定成 "devone"。 函式呼叫的成功與否,可透過 IS_ERR() 來判斷,不能直接判斷是否為 NULL。 卸載驅程程式時,必須一併刪除先前登記的 class,這時用的是 class_destroy():

void class_destroy(struct class *cls);

建立完 /sys/class/devone 資料夾後,接著要建立「/sys/class/devone/devone0」這個裝置名稱,用的是 class_device_create():

struct device *device_create(struct class *cls, struct class_device *parent, dev_t devt, struct device *device, const char *fmt, ...);

第一引數 cls 是 class_create() 傳回的 class。 第二引數 parent 是指定上層 class 時使用的,也可以指定成 NULL。 第三引數 devt 是要在 dev 檔顯示的 major/minor number,用 MKDEV 巨集指定即可。 第四引數 device 在想為 class 驅動程式連結 「struct device」時可以指定,傳入 NULL 也可。 第五引數 fmt 是裝置名稱,以上例來說就是 devone0 ,可以使用可變引數。 卸載驅動程式時,須刪除先前登記的裝置名,可以使用 class_device_destroy():

void class_device_destroy(struct class *cls, dev_t devt);

支援 udev 的驅動程式範例:

/*
 * devone.c
 *
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

static int devone_devs = 1;        /* device count */
static int devone_major = 0;       /* MAJOR: dynamic allocation */
static int devone_minor = 0;       /* MINOR: static allocation */
static struct cdev devone_cdev;
static struct class *devone_class = NULL;
static dev_t devone_dev;

ssize_t devone_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    int i;
    unsigned char val = 0xc0;
    int retval;
    //dump_stack();

    for (i = 0 ; i < count ; i++) {
        if (copy_to_user(&buf[i], &val, 1)) {
            retval = -EFAULT;
            goto out;
        }
    }

    retval = count;
out:
    printk(KERN_ALERT "0x%08X\n", __builtin_return_address(0));
    return (retval);
}

struct file_operations devone_fops = {
    .read = devone_read,
};

static int devone_init(void)
{
    dev_t dev = MKDEV(devone_major, 0);
    int alloc_ret = 0;
    int major;
    int cdev_err = 0;
    struct class_device *class_dev = NULL;

    alloc_ret = alloc_chrdev_region(&dev, 0, devone_devs, "devone");
    if (alloc_ret)
        goto error;
    devone_major = major = MAJOR(dev);

    cdev_init(&devone_cdev, &devone_fops);
    devone_cdev.owner = THIS_MODULE;
    devone_cdev.ops = &devone_fops;
    cdev_err = cdev_add(&devone_cdev, MKDEV(devone_major, devone_minor), 1);
    if (cdev_err)
        goto error;

    /* register class */
    devone_class = class_create(THIS_MODULE, "devone");
    if (IS_ERR(devone_class)) {
        goto error;
    }
    devone_dev = MKDEV(devone_major, devone_minor);
    class_dev = device_create(
                    devone_class,
                    NULL,
                    devone_dev,
                    NULL,
                    "devone%d",
                    devone_minor);

    printk(KERN_ALERT "devone driver(major %d) installed.\n", major);

    return 0;

error:
    if (cdev_err == 0)
        cdev_del(&devone_cdev);

    if (alloc_ret == 0)
        unregister_chrdev_region(dev, devone_devs);

    return -1;
}

static void devone_exit(void)
{
    dev_t dev = MKDEV(devone_major, 0);

    /* unregister class */
    device_destroy(devone_class, devone_dev);
    class_destroy(devone_class);

    cdev_del(&devone_cdev);
    unregister_chrdev_region(dev, devone_devs);

    printk(KERN_ALERT "devone driver removed.\n");

}

module_init(devone_init);
module_exit(devone_exit);

範例的規則檔: /etc/udev/rules.d/51-devone.rules

KERNEL=="devone[0-9]*", GROUP="root", MODE="0644"

這邊設定的權限讓一般使用者也能打開裝置檔。

  • ps
1, 檔名似乎是隨意取的,只要不與同資料夾的其它檔案產生衝突即可。 
2, 試著移除後,發現以 root 的權限還是能打開裝置檔,但以 user 的權限就不行。

Makefile 同上例 編譯、測試過程:

insmod ./sample.ko 
lsmod | grep sample 
dmesg | tail 
cat /proc/devices | grep devone 
ls -l /dev/devone0 
ls -l /sys/class/devone/devone0/ 
cat /sys/class/devone/devone0/dev 
hexdump -C -v -n 32 /dev/devone0 
rmmod sample 
ls -l /dev/devone*

5-12、結語

驅動程式多半要與 user process 通訊才有辦法實現功能,而其通訊方式多半遵循 UNIX 的傳統手法,透過檔案來進行。