環境:board:JZ2440 arch:arm CPU:arm920t kernel:linux2.6
基本框架
本篇作爲linux驅動練習第一篇,理應從零開始,優先將基本框架準備好,而後根據需要往框架中填充需要的功能。
驅動作爲linux內核功能的補充,在已有的linux內核框架基礎上,驅動可以作爲模塊很容易地加入到內核併發揮其預期的功能。一般驅動可以以build-in和modules的方式加入到內核中,前者在內核編譯時完成,與內核一體。而後者則以modules這一模塊的方式存在,可以在內核運行時,動態加載或者卸載自身,這種應該算是支持熱拔插了吧。以module的方式極大地豐富了linux內核的靈活性,同時也方便了驅動開發者,使得驅動開發不再需要修改內核、編譯內核、加載內核這一冗長的流程。
爲了實現這一靈活性,驅動和內核之間必然有協商好的約定,從而加載驅動時,內核能夠知道這是一個驅動,並作出相應的措施來使能或者初始化加載的驅動。以下爲一個驅動最基本需要的框架,雖然沒什麼用處,但是仍然可以作爲一個驅動被內核識別並加載。
#include <linux/module.h>
#include <linux/init.h>
/* 驅動加載到內核時的初始化函數,__init標識用於將該函數放入對應的段中(數據段,代碼段等,
* 由鏈接腳本規劃,同時加上__init代表完成加載後可以釋放的區域)。
* 返回值類型int,用於表示加載是否成功。
*/
static int __init XXX_init()
{
}
/* 基本概念如init沒有太大區別,只是__exit標識放置的段會有所差異,同時調用時機也是有差別,
* 在驅動要卸載時調用。
*/
static void __exit XXX_exit()
{
}
module_init(XXX_init); //標識入口,驅動加載到內核時,由內核調用,一般爲初始化資源
module_exit(XXX_exit); //標識出口,驅動從內核中卸載時,由內核調用,一般爲資源回收
在描述其他實際功能之前,不妨再看看能給這個框架增加信息但是實際上並沒什麼用處的其他描述(這裏沒啥用只是針對最終期望實現的功能而言,存在必定存在其意義,只是這裏不做展開)。
MODULE_AUTHOR("CryptonymAMS"); //作者名稱
MODULE_VERSION("1.0.0"); //版本號
MODULE_DESCRIPTION("hello world!"); //相關描述
MODULE_LICENSE("GPL"); //GPL許可
註冊設備
基本框架有了,現在要開始往這個架子填充相關功能了。首先需要做的是把設備節點申請並註冊好,這是驅動自身在內核中存在的一個"證明",同時也是向用戶空間暴露入口的機會,linux一切皆文件的背景下,最終的設備節點期望是以/dev/xxx這一文件存在在文件系統中。
通過 ls -l /dev 指令可以看到/dev目錄下已有的字符設備驅動,行首的 c 表示該文件是字符型設備,4 和 9x分別爲該設備的主設備號和次設備號,英文表示分別爲major和minor,這是該驅動在內核中的身份證,而名字ttyxx只是針對用戶空間做的一個代名,在內核中並沒有實際的作用。
major minor
crw-rw---- 1 root dialout 4, 91 5月 2 17:00 ttyS27
crw-rw---- 1 root dialout 4, 92 5月 2 17:00 ttyS28
crw-rw---- 1 root dialout 4, 93 5月 2 17:00 ttyS29
crw-rw---- 1 root dialout 4, 67 5月 2 17:00 ttyS3
crw-rw---- 1 root dialout 4, 94 5月 2 17:00 ttyS30
crw-rw---- 1 root dialout 4, 95 5月 2 17:00 ttyS31
當用戶空間調用C庫函數如open和read等對文件進行操作時,操作的是文件名,而系統將根據文件名找到對應的主次設備號,依次經過系統調用層、VFS分派到對應的設備驅動中處理。關於VFS和系統調用這裏展開過於龐大,放個TODO,之後另篇分析。
從上述可知,註冊設備第一步需要做的是註冊主次設備號,佔好坑。然後把處理函數綁定到該設備上。
註冊設備號並綁定設備根據linux內核版本不同有兩個版本,以linux2.6作爲分水嶺,當然新版本內核會兼容舊版本,但是推薦使用新的方法,一方面新方法提供了更高的靈活性,另一方面是防止日後不兼容。
舊方法
舊方法涉及兩個方法如下,從命名可以看出兩者對立,一個用於分配,另一個負責回收,這裏嘗試將內核中的描述翻譯,發現怎麼都不對味,因此附上原文,基本上重要的點是:major傳入非0時,即我們指定系統分配該major給我們的驅動,系統會進行嘗試分配,成功返回0,失敗返回對應錯誤碼,不推薦,當傳入爲0時,由系統自動分配空閒主設備號,此時成功將返回對應分配的設備號。使用該方法申請設備號會將所有的次設備號也佔住,且全部指向同一個file_operation,即使用相同的處理函數,如果次設備需要不同處理,需要在對應的函數中做對應的區分。
/**
* register_chrdev() - Register a major number for character devices.
* @major: major device number or 0 for dynamic allocation
* @name: name of this range of devices
* @fops: file operations associated with this devices
*
* If @major == 0 this functions will dynamically allocate a major and return
* its number.
*
* If @major > 0 this function will attempt to reserve a device with the given
* major number and will return zero on success.
*
* Returns a -ve errno on failure.
*
* The name of this device has nothing to do with the name of the device in
* /dev. It only helps to keep track of the different owners of devices. If
* your module name has only one type of devices it's ok to use e.g. the name
* of the module here.
*
* This function registers a range of 256 minor numbers. The first minor number
* is 0.
*/
int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops);
int unregister_chrdev(unsigned int major, const char *name);
例子
這裏暫且忽略錯誤情況,正常情況下需要對每一次返回值進行判斷,出現錯誤時及時回收資源並return,這樣內核才能穩定。
int major;
const char* device_name = "xxx_device";
static const struct file_operations fops= {
.owner =THIS_MODULE,
};
static int __init xxx_init(void)
{
major = register_chrdev(0, device_name, &fops);
return 0;
}
static void __exit xxx_exit(void)
{
unregister_chrdev(unsigned int major, device_name);
}
新方法
新方法中使用的接口較多,但是實際上將舊方法中的兩個接口展開,就會發現,新方法只是將舊方法拆開,細節化而已。同樣包含分配和回收兩部分,分配分爲設備號分配,cdev綁定fops和註冊這兩步,同樣存在動態分配和指定分配的區別,拆開後的好處是同一個主設備號,可以綁定不同的fops到次設備號中,增加了設備容量,同時靈活性up。
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name);
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
/**
* unregister_chrdev_region() - return a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count);
/**
* cdev_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
struct cdev *cdev_alloc(void);
/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*/
void cdev_del(struct cdev *p);
例子
同樣暫時不對錯誤情況處理。
#include <linux/fs.h>
#include <linux/cdev.h>
static dev_t dev;
static struct cdev *cdev_p;
const char* device_name = "xxx_device";
static const struct file_operations fops= {
.owner =THIS_MODULE,
};
static int __init xxx_init(void)
{
alloc_chrdev_region(&dev, 0, 1, device_name);
cdev_p = cdev_alloc();
cdev_init(cdev_p, &fops);
cdev_add(cdev_p, dev, 1);
return 0;
}
static void __exit xxx_exit(void)
{
cdev_del(cdev_p);
unregister_chrdev_region(dev,1);
}
至此,申請好了主次設備號並且綁定好了fops,可以編譯並加載進內核看看了。編譯使用make,依賴kernel的編譯樹,編寫makefile如下:
kernel_dir = "/work/system/linux-2.6.22.6" #內核所在路徑,需要提前編譯好
All:
make -C $(kernel_dir) M=`pwd` modules
clean:
make -C $(kernel_dir) M=`pwd` modules clean
rm Module.symvers
obj-m += final_test.o
編譯後產物爲final_test.ko,將其上傳至開發板中,執行加載相關操作
insmod final_test.ko #加載ko模塊
lsmod #列出所有已加載的模塊
rmmod #卸載已加載模塊
動態生成設備節點
通過 cat /proc/devices 可以看到已經將設備創建並加載完成。並分配到了252這一主設備號。但是可以看到/dev目錄下並未生成我們需要的供用戶空間操作的設備節點。
此時,可以通過mknod指令手動創建該設備節點。
mknod /dev/test c 252 0
但是這樣並不方便,因爲手動創建必須知道該設備的主次設備號,此時需要mdev機制,將我們hotplug的設備自動創建設備節點出來。這裏需要mdev和/sys文件系統的共同作用。需要使用到的接口如下:
#include <linux/device.h>
/**
* class_create - create a struct class structure
* @owner: pointer to the module that is to "own" this struct class
* @name: pointer to a string for the name of this class.
*
* This is used to create a struct class pointer that can then be used
* in calls to class_device_create().
*
* Note, the pointer created here is to be destroyed when finished by
* making a call to class_destroy().
*/
struct class *class_create(struct module *owner, const char *name);
/**
* class_device_create - creates a class device and registers it with sysfs
* @cls: pointer to the struct class that this device should be registered to.
* @parent: pointer to the parent struct class_device of this new device, if any.
* @devt: the dev_t for the char device to be added.
* @device: a pointer to a struct device that is assiociated with this class device.
* @fmt: string for the class device's name
*
* This function can be used by char device classes. A struct
* class_device will be created in sysfs, registered to the specified
* class.
* A "dev" file will be created, showing the dev_t for the device, if
* the dev_t is not 0,0.
* If a pointer to a parent struct class_device is passed in, the newly
* created struct class_device will be a child of that device in sysfs.
* The pointer to the struct class_device will be returned from the
* call. Any further sysfs files that might be required can be created
* using this pointer.
*
* Note: the struct class passed to this function must have previously
* been created with a call to class_create().
*/
struct class_device *class_device_create(struct class *cls,
struct class_device *parent,
dev_t devt,
struct device *device,
const char *fmt, ...);
void class_device_unregister(struct class_device *class_dev);
/**
* class_destroy - destroys a struct class structure
* @cls: pointer to the struct class that is to be destroyed
*
* Note, the pointer to be destroyed must have been created with a call
* to class_create().
*/
void class_destroy(struct class *cls);
例子
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define BASE_MINOR 0
#define MAX_DEVICE_NUM 1
int major;
static dev_t dev;
static struct cdev *cdev_p;
const char* device_name = "xxx_device";
static struct class* xxx_clsp;
static struct class_device* xxx_cls_devp[MAX_DEVICE_NUM];
static const struct file_operations fops= {
.owner =THIS_MODULE,
};
static int __init xxx_init(void)
{
int i=0;
alloc_chrdev_region(&dev, BASE_MINOR, MAX_DEVICE_NUM, device_name);
cdev_p = cdev_alloc();
cdev_init(cdev_p, &fops);
cdev_add(cdev_p, dev, MAX_DEVICE_NUM);
xxx_clsp = class_create(THIS_MODULE,"xxx_class");
major=MAJOR(dev);
for(i=BASE_MINOR;i < MAX_DEVICE_NUM + BASE_MINOR;++i)
xxx_cls_devp[i] = class_device_create(xxx_clsp ,NULL,MKDEV(major,i), NULL, "xxx%d",i);
return 0;
}
static void __exit xxx_exit(void)
{
int i;
for(i=BASE_MINOR;i < MAX_DEVICE_NUM + BASE_MINOR;++i)
class_device_unregister(xxx_cls_devp[i]);
class_destroy(xxx_clsp);
cdev_del(cdev_p);
unregister_chrdev_region(dev,MAX_DEVICE_NUM);
}
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_AUTHOR("CryptonymAMS");
MODULE_VERSION("1.0.0");
MODULE_DESCRIPTION("hello world!");
MODULE_LICENSE("GPL");
至此,可以看到設備節點已經創建完成。後續將在此基本框架上增加需要的功能。