簡單linux字元裝置驅動程式與程式設計小技巧(上)

心中唯有此願發表於2014-02-15

這幾天開始研究linux下的驅動程式編寫了,遇到的問題也挺多的,好在linux是開源的,很多高人編寫的技巧和思路都會在他們的原始碼中體現,我也在他們的原始碼中學到了很多好東西,我歸納了下貼出來,希望自己的程式碼能幫到別人。

今天就來介紹一下linux的字元裝置驅動程式:

字元驅動應該是驅動學習的第一站了,在《linux裝置驅動程式第三版》這本書的第三章介紹了一個簡單的字元裝置scull的程式設計,這一章很詳細的介紹了字元裝置驅動的一些概念,建議使用的事項和設計時要用到一些結構體和函式,所以我就不需要再吧這些介紹一遍了。我為了學起來方便,簡化了書裡的scull例子,將儲存裝置定義為一塊連續的資料段。

現在讓我們邊看程式碼邊分析這簡單的scull字元裝置驅動設計要做些什麼,怎麼做。

1 - 39行

/* scull V1.0 */
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>           // file_operations
#include <linux/cdev.h>         
#include <linux/errno.h>        // err
#include <linux/string.h>     	// memcpy memset
#include <linux/slab.h>         // kmalloc
#include <linux/kernel.h>       // container_of

#include <asm/uaccess.h>        // copy_xxxx_user

//#include "c_device.h"

#define DEBUG_SWITCH
#ifdef DEBUG_SWITCH
#define P_DEBUG(fmt, args...) printk(KERN_WARNING"KDEBUG-%s[%s]: "fmt, __FILE__,  __FUNCTION__, ##args)
#else
#define P_DEBUG(fmt, args...) printk(KERN_DEBUG"KDEBUG-%s[%s]: "fmt, __FILE__,  __FUNCTION__, ##args)
#endif

#define rw_BUF_SIZE 20      // 每個次裝置分配到的位元組寬度
#define SEEK_SET 0          // llseek
#define SEEK_CUR 1
#define SEEK_END 2
#define D_COUNT  2          // 定義的值與常量d_count一致,存在的理由下面會講

unsigned int major = 0;     // 自定義主裝置號
unsigned int minor = 0;     // 自定義次裝置號
const unsigned int d_count = 2;  // 次裝置數
dev_t devNo;                // dev_t裝置號描述

struct _dev_sct{            // 自定義裝置結構,後面會詳細介紹它
char (*kbuf)[D_COUNT][rw_BUF_SIZE];   // 指向buf主體
int cur_size;               // buf殘留size
unsigned int d_minor;       // 儲存當前開啟裝置的次裝置號
struct cdev c_dev;          // 嵌入cdev結構資訊
};
struct _dev_sct *devp;      // 建立全域性的裝置結構指標
首先來看看包括的頭函式,後面的註釋是這個頭函式包含的作用,其中被我註釋掉的c_device.h這個檔案不存在,我原本的想法是將檔案的一些宣告部分放到c_device.h中的,這樣可以減少原始檔程式碼量,但是為了修改方便所以沒這麼做。

下面的rw_BUF_SIZE是我定義的這個記憶體裝置的長度,每個次裝置將分配到20位元組寬度的記憶體,分配策略會在後面程式碼中實現。

SEEK_xxx將給llseek使用,由於核心空間不能呼叫C庫,所以不能享受C庫的好處了,自己定義吧。

D_COUNT與常量d_count都是為了描述次裝置個數的,本來編寫的時候是沒有D_COUNT,但編譯的時候報錯error: variably modified 'kbuf' at file scope,上網一查發現GCC編譯器不允許常量來做陣列的引數(vc是可以這麼幹的)。

重點來談談_dev_sct這個結構吧,《linux裝置驅動程式第三版》將它解釋為描述裝置的結構,即它代表裝置的主體,這個結構是程式設計師定義的,裡面要新增什麼與具體要編寫的驅動相關,存在這個結構的好處是這個結構包含了裝置所有需要關注的資訊,這使後面的呼叫變得簡單,起碼思路上簡單了很多,不會因為包含了不同重要資訊的變數變得雜亂無章。

我的_dev_sct結構描述了D_COUNT個,每個大小為rw_BUF_SIZE位元組的記憶體buffer,這個buffer的主體是一個[D_COUNT][rw_BUF_SIZE]的陣列,_dev_sct結構儲存了kbuf這個指向陣列的陣列指標。所以在分配完_dev_sct結構後還需分配buffer主體,然後將其與kbuf關聯,這個策略將在後面程式碼中貼出。

_dev_sct結構裡嵌入cdev是一個很nice的做法,這樣_dev_sct就能通過cdev結構被別的函式更方便的呼叫了,策略在後。

165-227

static int __init mycdev_init(void)
{
    if(major)
    {
    	devNo = MKDEV(major, minor);
    	result = register_chrdev_region(devNo, d_count, "mycdev");
    }
    else
    {
     	result = alloc_chrdev_region(&devNo, minor, d_count, "mycdev");
    }
    if(result < 0)
    {
    	P_DEBUG("register devNo errno!\n");
    	goto err0;
    }
    
    devp = (struct _dev_sct *)kmalloc(sizeof(struct _dev_sct), GFP_KERNEL);
    if(!devp)
    {
    	result = -ENOMEM;
    	goto err2;
    }
    memset(devp, 0, sizeof(struct _dev_sct));
    devp->kbuf = (char (*)[d_count][rw_BUF_SIZE])kmalloc(d_count * rw_BUF_SIZE, GFP_KERNEL);
    if(!devp->kbuf)
    {
    	result = -ENOMEM;
    	goto err3;
    }
    memset(devp->kbuf, 0, d_count * rw_BUF_SIZE);
    
    cdev_init(&devp->c_dev, &f_opts);
    devp->c_dev.owner = THIS_MODULE;
    devp->c_dev.ops   = &f_opts;
    
    result = cdev_add(&devp->c_dev, devNo, d_count);
    if(result < 0)
    {
    	P_DEBUG("cdev_add errno!\n");
    	goto err1;
    }
    P_DEBUG("major = [%d]\n", MAJOR(devNo));
    
    
    
    return result;
    
    err0: 
    	return result;
    err2:
    	unregister_chrdev_region(devNo, d_count);
    	return result;
    err3:
    	kfree(devp);
    	unregister_chrdev_region(devNo, d_count);
    	return result;	
    err1:
    	kfree(devp->kbuf);
    	kfree(devp);
    	unregister_chrdev_region(devNo, d_count);
    	return result;	
}

這裡先貼出init程式碼,why? 大家注意,分析核心模組和驅動程式碼都應該從init開始,包括以後看行家裡手寫的核心模組程式碼,如果不從init慢慢開始看,會暈的。

字元裝置首先分配和獲取裝置號,register_chrdev_region代表用自己配置的裝置號,alloc_chrdev_region表示從系統獲取空閒裝置號(推薦)。

為了讓裝置被系統控制就要註冊裝置,cdev相關函式來完成註冊,在註冊以前就要使裝置已在工作狀態,意思就是說呼叫cdev_add以前就要把裝置初始化OK了,這裡就是記憶體裝置,用kmalloc來分配記憶體吧,我首先分配的是_dev_sct結構,kmalloc分配的記憶體是不清零的,需要手動memset,然後分配記憶體裝置主體,接著把它們拼起來:

devp->kbuf = (char (*)[d_count][rw_BUF_SIZE])kmalloc(d_count * rw_BUF_SIZE, GFP_KERNEL);
出錯處理這裡我發現很多資料裡面都用了goto,應該是個不流行的東西,為什麼這裡用的比較多呢,我的理解是:核心和驅動程式碼在某個地方失敗後需要將前面執行成功的比如記憶體分配之類的資源釋放,而這樣的出錯處理如果寫在函式裡面就不直觀,因為這部分出錯處理的程式碼有相似性,釋放順序也很有規律。而且一般都是與資源分配順序相反,這樣goto的程式碼寫在一起就好看很多。

上半部分就講到這裡,下半部分會繼續講解file_opt的相關函式,我的實驗程式碼和操作步驟也會打包到網上。

下半部分《簡單linux字元裝置驅動程式與程式設計小技巧(下)》http://blog.csdn.net/jiebaoabcabc/article/details/19283859

原始碼連結http://download.csdn.net/detail/jiebaoabcabc/6926511

就這樣 米娜桑 貴安。

相關文章