跳到主要內容

藉由select & poll來學習 Linux device driver programming

最近因為想學習撰寫一隻 Linux device driver,所以先藉由 Linux Device Driver Programming 驅動程式設計,這本書裡的select & poll 的範例程式開始學習。但是因為這本書當時再寫的時候,kernel版本大致分為 2.4 和 2.6,版本比較舊,有些函式已有變動,所以有做一些修改。(我的Kernel版本為4.2版)


驅動程式筆記與程式碼講解

函式加上 static ,讓命名空間限制在檔案內。不過在這裡就算不加static也不會影響kernel整體的符號表。這是只緊限於kernel module的時候,也就是動態載入的驅動程式來說。如果是希望其他module能夠呼叫的話,就必須要使用EXPORT_SYMBOL來明確匯出函式。

驅動程式內部能夠使用到 printf(),因為kernel space沒有直接對應的console (鍵盤,畫面)。但是還是有可代用的function printk()。它輸出的資料會跑到kernel buffer內。kernel buffer 可以用 dmesg 指令查閱,不過空間才只有 128KB(default),而且是環狀的形式(所以心資料會蓋過最舊的資料)。因此不能一直把資料保留在裏面,可以用syslogd 或是 klogd 之類的程式把資料寫到 syslog 裡(var/log/message),不過這種方法還是可能會漏掉訊息。

下面是訊息等級,在 kernel 4.2 版本,預設等級是 log level。但是在 kernel 2.6 版本預設等級是 4。

驅動程式的進入點並不是 main() ,因為驅動程式與一般應用程式是不同的,而且必須準備多個,其中至少要兩個進入點

  • insmod 與 modprobe 呼叫的初始化函式
  • rmmod 呼叫的結束函式
對於這隻driver來說就是 devone_init()與devone_exit()。
其他進入點還包含
  • 系統呼叫
  • 中斷服務程序
  • 計時器程序

驅動程式碼


如果 User process要跟驅動程式交換資料,必須要透過device file(裝置檔),這個檔案可以透過mknod的函式呼叫產生,如 line 153。也可以用指令 mknod 來產生。產生完後會在/dev的資料夾下出現裝置檔。

mknod 裝置檔名 種類 主編號 副編號

刪除的話,只需直接 rm 即可。

mknod 指令可以指定的範圍


種類 範圍
Major number 0~4095
Minor number 0~1048575

Major number 和 Minor number 的功能是當 User process 打開裝置檔的時候,能找到對應的驅動程式來處理。

Kernel 的 open() 系統呼叫介面是 sys_open(),但是要怎麼知道是藥用哪個驅動程式來處理,就要用到 Major number。所以Major number必須是要獨一無二的。Major number與指向驅動程式的指標會存放在 kobj_map對應表中。所以當kernel 發起 sys_open()之後,就會去查詢kobj_map,透過Major number找出對應的驅動程式連結起來。
而Minor number是驅動程式自己管理,kernel不管的,如果有多個裝置,就必須要用Minor number來區分。

User Process 與驅動程式的溝通管道

  1. 通常是以 open()的系統呼叫來打開裝置檔
  2. 透過write() 將資料傳給驅動程式
  3. 透過read() 從驅動程式取資料
  4. 最後要以close() 關閉裝置檔

Major number 登記方法
為了要讓裝置檔和驅動程式連起來,驅動程式要先跟kernel登記 major number。
登記方法可以分為兩種

  1. 靜態(傳統)
    這是kernel 2.4的作法,之後都是採動態登記,但是也是可以使用。
    int register_chrdev(unsigned int major, const char *name, const struct file_operation *fops);
    它是在 fs/char_dev.c的檔案內實作的。登錄成功之後,可以在 /proc/devices 顯示驅動程式名稱。
    major也可以傳入 0,如此kernel會自動分配一個沒用到的major number。
    卸載的時候,用
    void unregister_chrdev(unsigned int major, const char *name);
  2. 動態
    流程:
    以 alloc_chrdev_region() 動態取得major number
    • int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
    cdev_init() 登記系統呼叫 handler
    • void cdev_init(struct cdev *cdev, const struct file_operation *fops);
      初始化cdev結構,並登記系統呼叫handler(fops)。
    cdev_add() 向kernel 登記驅動程式
    • int cdev_add(struct cdev *p, dev_t dev, unsigned count);
      dev 是 alloc_chrdev_region()配置的major number和minor number 交給 MKDEV()造出來的數值。
    卸載:
    cdev_del() 向kernel釋放驅動程式
    • void dcdev_del(struct cdev *p);
    unregister_chrdev_region() 釋放major number
    • void unregister_chrdev_region(dev_t from, unsigned count);

驅動裝置登記過程在 devone_init() function。

系統呼叫處理函式的登記方式


user process對驅動程式下達了系統呼叫,驅動程式也必須要有對應的handler才可以。
驅動程式的handler是透過 file_operations 結構指定的,如下。
其中必須注意的是 不同 kernel版本file_operations的結構也會稍有不同,所以必須留意!


c語言的struct 初始化可以使用gcc擴充語法 :
struct file_operations devone_fops = {
        .open = devone_open,
        .release = devone_close,
};

書上介紹了 open, close, read, write的 handler。

open handler

prototype:

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

return 0 : 成功,否則 錯誤

inode 結構成員
inode 結構成員 意義
bdev_t i_rdev Major/minor number
void *i_private 驅動程式私有指標

i_private 是驅動程式可以自由使用的指標。常見的用途

  • 在open handler 配置記憶體,設給這個指標
  • 在write與read handler運用這塊記憶體
  • 在close handler釋放記憶體
file結構成員

file 結構成員 意義
struct file_operations *f_op 系統呼叫handlers
unsigned int f_flags open 函式第2引數傳入的flag
void *private_data 驅動程式私有資料指標

close handler

prototype:

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

如果開啟時候有配置資源的,就必須要在close handler內釋放。否則會倒置記憶體洩漏。
一個 user process或是多個user process 可能同時重複開啟同一個裝置檔。可以透過inode或是file結構內的私有指標來判斷是誰開啟的。

inode結構是在mknod指令建立裝置檔的時候由kernel建立的,因此

  • inode 結構的個數 : 1個
  • file 結構的個數 : 開檔次數

read handler

prototype:

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

buf 是 user process 呼叫 read()時指定的緩衝區指標。但是因為kernel space與user space的虛擬記憶體空間不同,所以驅動程式不能直接取用buf指標,必須要透過 copy_to_user() 這個function把資料複製過去。

write handler

prototype:

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

user想傳資料給驅動程式使用的話,要用copy_from_user(),從buf讀資料。

udev

為了方便管理裝置檔的機制。
在OS 偵測到新硬體的時候,自動建立裝置檔。
在移除裝置的時候,自動刪除裝置檔 (hotplug)機制

下圖是 udev 機制 :

驅動程式的支援

  • 登記 class
    class *class_create(struct module *owner, const char *name);
  • 卸載驅動程式
    void class_destroy(struct class *cls);
  • 接著要建立 /sys/class/class名稱/裝置名稱
    struct device *device_create(struct class *cls, struct device *parent, dev_t dev, void *drvdata, const char *fmt, ...);
  • 卸載時要先刪除登記的裝置檔
    void device_destroy(struct class *cls, dev_t devt);

open() 系統呼叫,預設會以blocking mode開啟,意思是process等待事情發生,而進入sleep。而read/write也是一樣,如果沒有資料可以供讀寫就會block住。

Linux 應用程式大多是以blocking mode開發,Windows主流是 non-blocking mode。 但是對於應用程式來說,有時read會被block住的話,會造成設計上的問題,因此Linux多了一些功能 :
  • Non-blocking mode
  • 同時執行多個同步IO工作

同時執行多個同步IO工作(synchronous IO multiplexing),使用select系統呼叫的作法。透過select可以監視多個file handle在三種狀態之間變化(可讀,可寫,發生錯誤)。但是select本身會block住,可以設定timeout,最多5秒。 因此可以在呼叫read()之前先呼叫select()判斷資料抵達了沒。

另外還有一個跟select()很像的是poll()。不同的是指定file handle的方式不同,而且在指定多個file handle的時候,poll()走訪所有file handle的速度比較快。

Reference :

  1. Linux Device Driver Programming 驅動程式設計
  2. Linux cross reference



留言

這個網誌中的熱門文章

編譯入門 - gcc toolchain

程式編譯的過程是很複雜的,初學者在學習寫程式的時候,大部份是透過IDE來編譯程式的,所以將內部的編譯流程都隱藏了起來。其實過程是很複雜的,我打算先以觀察gcc 編譯程式的的過程以及中間的產物來開始學習。 以下資料是透過閱讀 「 程式設計師的自我修養 - 連結、載入、程式庫 」並在自己的電腦上驗證結果。 先透過一個最簡單的入門程式開始學起, Hello World !! 接下來我們使用 gcc 來編譯它巴 $gcc hello.c $./a.out Hello World! 上面看似簡單的編譯指令,其實中間包括了4個階段 前置處理 (Preprocessing) 編譯 (Compilation) 組譯 (Assembly) 連結 (Linking) 圖示 : 下面我會使用 gcc 一步一步的編譯 Hello World 這隻程式,將結果呈獻出來。

安裝QT可能會發生的問題,跟解決辦法

參考的官方文件 https://wiki.qt.io/Install_Qt_5_on_Ubuntu 安裝完後,執行QT creator,可能會發生的問題是 garygu@garygu-UX330UA:~/Qt/Tools/QtCreator/bin$ ./qtcreator qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found. This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem. Available platform plugins are: eglfs, linuxfb, minimal, minimalegl, offscreen, vnc, xcb. Aborted (core dumped) 看起來是在run的過程中有缺少shared library,以下網站也有提到。 https://stackoverflow.com/questions/17106315/failed-to-load-platform-plugin-xcb-while-launching-qt5-app-on-linux-without 所以透過ldd 可以知道有哪些shared library是有缺少的。從not found的library下手安裝就行了。 garygu@garygu-UX330UA:~/Qt/Tools/QtCreator/lib/Qt/plugins/platforms$ ldd libqxcb.so  linux-vdso.so.1 (0x00007fff229e2000) libQt5XcbQpa.so.5 => /home/garygu/Qt/Tools/QtCreator/lib/Qt/plugins/platforms/./../../lib/libQt5XcbQpa.so.5 (0x00007ffb4854...