最近,一直在看書學習linux裝置驅動,從最簡單的字元裝置驅動入門,能夠對驅動的架構和各元素的功能有個宏觀的瞭解。下面詳細分析我寫的第一個字元裝置驅動,整理一些驅動的基礎知識,加深印象。先給出一些宏定義和全域變數:
#define GLB_MEM_SIZE 0x1000
#define HELLO_MAJOR 250
#define MEM_CLEAR 0x1
int hello_major = HELLO_MAJOR;
struct globalmem_dev
{
struct cdev cdev; unsigned char mem[GLB_MEM_SIZE];};
//此處定義一個全域變數,用於構造一個虛擬字元裝置
struct globalmem_dev *devp;
首先,linux裝置驅動屬於核心的一部分,具有明顯的模組特性。字元裝置驅動包括以下幾大塊:
(一)驅動載入和卸載模組
module_init(hello_drv_init);
module_exit(hello_drv_exit);
其中module_init和module_exit是核心定義的宏,用於將自己編寫的模組載入到核心或者從核心載。hello_drv_init和hello_drv_exit是
我們自己定義的函數,hello_drv_init是驅動的初始化函數,主要完成以下幾部分工作:
1. 為字元裝置向系統申請裝置號
dev_t devno = MKDEV(hello_major, 0);
如果手動分配好主裝置號hello_major,則MKDEV宏可產生裝置號,其中次裝置號為0,而且主裝置號佔12位,次裝置號佔20位
register_chrdev_region(devno, 1, "helloworld");
以上函數為向系統手動申請一個裝置號記錄到chardevs數組當中,裝置名稱為“helloworld”,前提是裝置號未被使用
alloc_chrdev_region(devno, 0, 1, "helloworld");
以上函數表示向系統動態申請未被佔用的裝置號,第一個參數devno用於存放獲得的裝置號,第二個參數0表示次裝置號,第三
個參數1表示只申請一個。
hello_major = MAJOR(devno);
hello_minor = MINOR(devno);
以上兩個宏分別表示從裝置號devno中提取主裝置號hello_major和次裝置號hello_major
2. 為字元裝置的結構體分配記憶體並初始化
devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
memset(devp, 0, sizeof(struct globalmem_dev));
上面GFP_KERNEL是核心分配記憶體空間的一個標識,表示無記憶體可用時進入休眠狀態。
3. 初始化cdev
devno = MKDEV(hello_major, 0);
cdev_init(&devp->cdev, &fops);該函數用於初始化cdev的成員,建立cdev和file_operations之間的聯絡
devp->cdev.owner = THIS_MODULE;
4. 註冊cdev
cdev_add(&devp->cdev, devno, 1); 向系統添加一個cdev,完成字元裝置的註冊。
同樣,模組卸載的時候,會進行與註冊時相對應的三個操作
cdev_del(&devp->cdev);//登出cdevkfree(devp);//釋放結構體記憶體unregister_chrdev_region(MKDEV(hello_major, 0), 1);//釋放裝置號
(二)file_operations 資料結構體的填充
本人簡單理解,應用程式對底層裝置的訪問是通過open、close、ioctl等函數操作的,而且應用程式層的這些函數是通過系統調用以及檔案系統的一個橋樑作用,最終調用的正是file_operations中與之相對應的函數,所以file_operations 結構體中的函數實現需要由驅動工程師去實現。
static struct file_operations fops ={.owner = THIS_MODULE,.llseek = hello_llseek,.read = hello_read,.write = hello_write,.ioctl = hello_ioctl,.release = hello_release,}
(三)file_operations 結構體中函數的實現下面以最常見的read和write函數作為例子來分析,
static int hello_read(struct file *file, char __user *buf, size_t size, loff_t *ppos){unsigned long p = *ppos;//讀的位置相對於檔案開頭的位移量int ret = 0;//讀取位移位置是否越界,或者位元組數太大的判斷
if(p >= GLB_MEM_SIZE) return 0;if(size > GLB_MEM_SIZE - p) size = GLB_MEM_SIZE - p;//開始讀取
if(copy_to_user(buf, (void*)(devp->mem+p), size)) ret = -EFAULT;else{ *ppos += size; ret = size; printk(KERN_INFO "read %d bytes from %d\n", size, p);} return ret;}
其中file是檔案結構體指標,buf為使用者空間的記憶體,size是需要讀取的位元組數,因為核心空間和使用者空間的記憶體不能互相訪問,需要藉助於copy_to_user函數完成核心空間到使用者空間的映射(即拷貝),同理在寫的時候,則需要copy_from_user完成使用者空間到記憶體空間的映射,這兩個函數返回的都是不能被複製的位元組數,所以,如果完全映射(複製)成功,則返回0.
static int hello_write(struct file *file, const char __user *buf, size_t count, loff_t *offset){ int ret; unsigned long p = *offset; if(p >= GLB_MEM_SIZE) return 0; if(count> GLB_MEM_SIZE - p) count = GLB_MEM_SIZE - p; if(copy_from_user((devp->mem+p), buf, count)) ret = -EFAULT; else { *ppos += count; ret = count; printk(KERN_INFO "write %d bytes to %d\n", count, p); } return ret;}
(四)標頭檔和宏
驅動模組中常使用到的標頭檔,可參考我轉載的一篇關於linux驅動標頭檔的文章。另外,還有一些宏如下:
MODULE_AUTHOR("W. Yihong <a5131wyh@163.com>");//模組作者的一些資訊聲明MODULE_DESCRIPTION("hello world driver"); //模組功能的一些描述MODULE_LICENSE("GPL");//模組許可證的聲明,如果不聲明LICENSE,則模組載入時會收到核心被汙染的警告MODULE_ALIAS("platform:myfirst_driver"); //模組可以調用此宏為自己定義一個或者若干個別稱
以上是我對字元裝置驅動的一個簡單理解,還有很多驅動的機制和內涵並不清楚,需要在以後的實際項目中去慢慢摸索和總結,本人正在入門,希望大家能指出文章不足之處,共同進步。