淺析Linux字符設備驅動程序內核機制
前段時間在學習linux設備驅動的時候,看了陳學松著的《深入Linux設備驅動程序內核機制》一書。
說實話。這是一本非常好的書,作者不但給出了在設備驅動程序開發過程中的所須要的知識點(如對應的函數和數據結構),還深入到linux內核裏去分析了這些函數或數據結構的原理。對設備驅動開發的整個過程和原理都分析的非常到位。但可能是因為知識點太多。原理也比較深的原因,這本書在知識點的排版上跨度有些大。所以讀起來顯得有點吃力,可是假設第一遍看的比較認真的話,再回頭看第二次就真的可以非常好地理解作者的寫作思路了。
第二章字符設備驅動程序我也是看了兩遍才理解過來。趁著這熱度,就依照自己的思路總結一下,以便下次再看的話,就行依照自己比較好理解的方式去看了。
1、字符設備驅動程序框架:
在深入討論字符設備驅動程序之前,先給出一個設備驅動程序典型框架結構,以便於對字符設備驅動程序有個初步的理解。
<span style="background-color: rgb(255, 255, 255);">/*字符設備驅動程序源碼*/ /*demo_chr_dev.c*/ #include<linux/module.h> #include<linux/kernel.h> #include<linux/fs.h> #include<linux/cdev.h> </span> static struct cdev chr_dev;//定義一個字符設備對象 static dev_t ndev;//字符設備節點的設備號 static int chr_open(struct inode *nd,struct file *filp) //打開設備 { int major=MAJOR(nd->i_rdev); int minor=MINOR(nd->i_rdev); printk("chr_open,major=%d,minor=%d\n",major,minor); return 0; } static ssize_t chr_read(struct file *f,char __user *u,size_t sz,loff_t *off) //讀取設備文件內容 { printk("In the chr_read() function!\n"); return 0; } //重要數據結構 struct file_operations chr_ops= { .owner=THIS_MODULE, .open=chr_open, .read=chr_read, }; static int demo_init(void) //模塊初始化函數 { int ret; cdev_init(&chr_dev,&chr_ops);//初始化字符設備對象。chr_ops定義在上面 ret=alloc_chardev_region(&ndev,0,1,"char_dev");//分配設備號 if(ret<0) return ret; printk("demo_init():major=%d,minor=%d\n",MAJOR(ndev),MINOR(ndev)); ret=cdev_add(&chr_dev,ndev,1);//將字符設備對象chr_dev註冊到系統中 if(ret<0) return ret; return 0; } static void demo_exit(void) { printk("Removing chr_dev module...\n"); cdev_del(&chr_dev);//將字符設備對象chr_dev從系統中註銷 unregister_chr_region(ndev,1);//釋放分配的設備號 } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL");
編譯後能夠內核模塊demo_chr_dev.ko
對驅動程序框架的整體理解:
(1)在linux系統中。各種設備都是以文件的形式存在。因此設備驅動程序包括了用於操作字符設備文件的函數。如打開,讀、寫等操作函數。如chr_open(),chr_read()等。這些函數都要由程序猿自己實現。
(2)驅動程序中包括了類型為struct file_operations的結構體對象如chr_ops,該結構體對象用於存放針對字符設備的各種操作函數。
(3)設備驅動程序作為內核模塊.ko安裝到系統中,因此在程序框架中,必需要調用module_init()函數完畢模塊的安裝。調用module_exit()函數完畢模塊的卸載。
(4)在模塊初始化函數中完畢字符設備對象的初始化。這個初始化過程中調用了程序猿定義的數據結構chr_ops作為參數。同一時候在初始化函數中還完畢了分配設備號,設備對象註冊等工作。
(5)在模塊的卸載函數中,會將相應的字符設備對象從系統中註銷掉。並釋放已分配的設備號。
2、字符設備驅動程序內核機制具體解釋
為了更easy理解驅動程序,我們結合前一步中給出的框架驅動程序中相應的函數和數據結構進行分析與解釋。
(1)結構體structfile_operations
該結構體定義在文件<include/linux/fs.h>中,詳細例如以下:
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **); };
能夠看到struct file_operations的成員變量計劃全是函數指針。
現實中,字符設備驅動程序的編寫。基本上是環繞著怎樣實現struct file_operations中的那些函數指針成員而展開的。
應用程序對文件類函數的調用如read()/open()等,在linux內核的機制下,終於會轉到structfile_operations中相應的函數指針成員上。
(2)THIS_MODULE
這是個宏定義#defineTHIS_MODULE(&__this_module)
__this_module內核模塊的編譯工具鏈為當前模塊產生 的struct module 類對象。所以THIS_MODULE實際上是當前模塊對象的指針。
(3)字符設備的抽象struct cdev
字符設備驅動程序管理的核心對象是字符設備,內核為字符設備抽象出了一個詳細的數據結構struct cdev,它定義在文件<include/linux/cdev.h>中。例如以下:
struct cdev { struct kobject kobj; //內嵌的內核對象 struct module *owner; //驅動程序所在的內核模塊對象指針 const struct file_operations *ops; //存放各種操作函數的結構體對象 struct list_head list; //字符設備鏈表 dev_t dev; //字符設備的設備號,由主設備號和次設備號組成 unsigned int count; //隸屬於同一個主設備號的次設備號的個數 };
須要註意的是,內核引入struct cdev數據結構作為字符設備的抽象,不過為了滿足系統 對字符設備驅動程序框架結構設計的須要,現實中一個詳細的字符硬件設備的數據結構可能更復雜,在這樣的情況下。struct cdev經常作為一種內嵌的成員變量出如今設備的數據結構中,如:
struct my_keypad_dev { //硬件相關的成員變量 int a ,*p; ... //內嵌的struct cdev結構對象 struct cdev c_dev; }
設備驅動程序中能夠使用兩種方式來產生struct cdev對象:
靜態方式:static struct cdev chr_dev;
動態方式:static struct cdev *p=kmalloc(sizeof(struct cdev),GFP_KERNEL);
(4)初始化函數cdev_init
在(3)中討論了怎樣產生一個struct cdev對象。接下來就討論一下怎樣初始化一個cdev對象。為此。內核提供了對應的初始化函數cdev_init。定義在<fs/char_dev.c>中,例如以下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops) { memset(cdev, 0, sizeof *cdev); INIT_LIST_HEAD(&cdev->list); kobject_init(&cdev->kobj, &ktype_cdev_default); cdev->ops = fops; }
參數說明:
*cdev:指向須要初始化的設備對象
*fops:包括了針對該字符設備的操作函數的結構體指針
(5)設備號
在linux系統中,一個設備號由主設備號和次設備號構成。內核使用主設備號來定位相應的設備驅動程序。而次設備號則由驅動程序使用,用於標識它所管理的若幹同類設備。
設備號是系統管理設備的有效資源。Linux中使用 dev_t(32位無符號整數)來表示一個設備號。
A、內核提供了下面三個宏用於操作設備號:<include/linux/kdev_t.h>
#define MAJOR(dev) ((unsignedint)((dev)>>MINORBITS)) //提取主設備號 #define MINOR(dev) ((unsignedint)((dev)&MINORBITS)) //提取次設備號 #define MKDEV(ma,mi)(((ma)<<MINORBITS)|(mi)) //合成設備號
B、為了有效的管理字符設備的設備號,內核定義了一個全局性指針數組chrdevs,該數組中的每一項都是一個指向struct char_device_struct類型的指針。系統中已分配的字符設備號都存放在該數組中。該指針數組定義例如以下:<fs/char_dev.c>
static struct char_device_struct { struct char_device_struct *next; unsigned int major; unsigned int baseminor; int minorct; char name[64]; struct cdev *cdev; /* will die */ } *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
C、另外內核還提供了兩個函數用於分配和管理設備號,定義在<fs/char_dev.c>中
alloc_chrdev_region()函數:該函數用於分配設備號,分配的主設備號範圍將在1~254之間。定義例如以下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) { struct char_device_struct *cd; cd = __register_chrdev_region(0, baseminor, count, name); if (IS_ERR(cd)) return PTR_ERR(cd); *dev = MKDEV(cd->major, cd->baseminor); return 0; }
這個函數的核心是調用__register_chr_dev_region,並且第一個參數為0。這樣將導致
__register_chr_dev_region運行以下的邏輯:
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) {jkjkhujhklkljkljmn struct char_device_struct *cd, **cp; int ret = 0; int i; cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); if (cd == NULL) return ERR_PTR(-ENOMEM); mutex_lock(&chrdevs_lock); /* temporary */ if (major == 0) { for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) { if (chrdevs[i] == NULL) break; } if (i == 0) { ret = -EBUSY; goto out; } major = i; ret = major; } ... }
這段代碼的原理是:它在for循環中,從chrdevs數字的最後一項一次向前掃描,假設發現該數組中的某項,比方第i項相應的數值為NULL。那麽就把該項相應的索引值i作為分配的主設備號返回給驅動程序,並將其增加到chrdevs[i]相應的哈希鏈表中。假設分配成功,所分配的主設備號將記錄在structchar_device_struct對象cd中(數組存放的都是這樣的對象),並將cd返回給alloc_chrdev_region函數。後者通過*dev=MKDEV(cd->major,cd->baseminor) 將新分配的設備號返回給函數掉用者。
register_chrdev_region()函數:函數原型例如以下:
int register_chrdev_region(dev_t from,unsigned count,const char *name){}
參數說明:
from:表示設備號;count:連續設備編號的個數。name:設備或者驅動的名稱。
該函數的作用就是將當前設備驅動程序要使用的設備號記錄到chrdev數組中。用於跟蹤系統設備號的使用情況,從而避免設備號的沖突。在使用這個函數時,要事先知道它所使用的設備號。
(6)字符設備的註冊
在一個字符設備初始化完之後,就能夠把它增加系統中,這樣別的模塊才幹夠使用它。把一個字符增加系統中須要調用函數cdev_add。其定義例如以下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count) { p->dev = dev; p->count = count; return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); }
cdev_add的核心功能通過kobj_map()函數來實現。調用cdev_add後,把要註冊的字符設備對象的指針嵌入到了一個類型為struct probe的節點之中。然後再把該節點增加到cdev_map所實現的哈希鏈表中。有關struct probe和cdev_map的定義例如以下:
<fs/char_dev.c> static sturct kobj_map *cdev; //這是一個struct kobj_map指針類型的全局變量。
在Linux系統啟動期間由chrdev_init函數負責初始化。
struct kobj_map定義例如以下:
struct kobj_map { struct probe { struct probe *next; dev_t dev; unsigned long range; struct module *owner; kobj_probe_t *get; int (*lock)(dev_t, void *); void *data; } *probes[255]; struct mutex *lock; };
kobj_map()函數過程:通過要增加系統的設備的主設備號major來獲得probes數組的索引值i,然後把一個類型為struct probe的節點對象增加到probe[i]所管理的鏈表中。當中probe節點中包括了設備的主設備號。以及指向字符設備對象的指針。
例如以下圖:
通過調用cdev_add後,就意味著一個字符設備對象已經增加到了系統中。在須要的時候,系統就能夠找到它了。
在cdev_add()函數中,動態分配了struct probe類型的節點。
當設備對象從系統中移除時,須要將它們從鏈表中刪除並釋放節點所占用的內存空間。這就是cdev_del()函數的作用。定義例如以下:
void cdev_del(struct cdev *p) { cdev_unmap(p->dev, p->count); kobject_put(&p->kobj); }
對於以內核模塊形式存在的驅動程序。作為通用規則。模塊的卸載函數應負責調用這個函數來將所管理的設備對象從系統中移除。
(7)設備文件節點的生成
在linux系統中,硬件設備都是以文件的形式存在於/dev/下的。即相應/dev/下的每一個文件節點都代表了一個設備。在linux系統中,每一個文件都有兩種不同的表示方式。對於隨意一個文件,在用戶空間一般用文件名稱來識別如demodev。在內核空間中。一般用inode來表示。
如168。它們實際上指向的都是同一個文件。對於設備文件,有:
inode->i_fop=&def_chr_fops;
inode->i_rdev=rdev;
(8)字符設備文件的打開操作
作為樣例,這裏假定前面相應於/dev/demodev設備節點的驅動程序已經實現了例如以下的struct file_operations對象chr_fops和打開函數chr_open。
struct file_operations chr_ops=
{
.owner=THIS_MODULE,
.open=chr_open,
.read=chr_read,
};
static int chr_open(struct inode *nd,structfile *filp)
{
intmajor=MAJOR(nd->i_rdev);
intminor=MINOR(nd->i_rdev);
printk("chr_open,major=%d,minor=%d\n",major,minor);
return0;
}
用戶空間應用程序的open函數原型為:
int open(constchar *filename,int flages,mode_t mode);
位於內核空間的驅動程序中open函數的原型為:
structfile_operations
{ ...
int(*open)(struct inode *,struct file *) ;
…
}
接下來我們見描寫敘述用戶態的open函數是怎樣一步步調用到驅動程序提供的open函數的(在本樣例中即chr_open函數)。
由前面的三個函數能夠看出:用戶態open函數返回的是文件描寫敘述符fd(整形)。
而驅動程序中的參數類型為struct file*file。顯然內核須要在打開設備文件時為fd與file建立某種關系。而且為file與驅動程序中的fops建立關聯。
用戶空間調用open函數,將發起一個系統調用,通過sys_open函數進入內核空間。調用關系例如以下:
1)do_sys_open函數首先通過get_unused_flags為本次的open操作 分配一個未使用過的文件描寫敘述符fd。
2)do_sys_open函數隨後調用do_filp_open函數。該函數會查找 “/dev/demodev”設備文件相應的inode。
查找到inode之後,接著調用函數get_empty_filp函數為打開的文件分配一個新的struct file類型的內存空間(返回指針)。內核用struct file對象來描寫敘述進程打開的每一個文件。
struct file的定義例如以下 :
struct file
{ ....
Const structfile_operations *f_op ;
….
}
從struct file的定義能夠看出,struct file對象中包括了struct file_operations類型的指針。
3)linux系統為每個進程都維護了一個文件描寫敘述符表。進程已打開文件的文件描寫敘述符(fd)就是文件描寫敘述符表的索引值。
文件描寫敘述符表中的每個表項都有一個指向已打開文件的指針。這個指針就是struct file 類型的指針。即:在描寫敘述符表中。通過fd索引僅僅能夠找到相應的表項,該表項的值就是filp,它指向了內核為剛剛打開的文件所分配的struct file類型空間。
4)在do_sys_open函數的後半部分,調用函數__dentry_open函數將“/dev/demodev”相應節點的inode中的i_fop賦值給filp->f_op。
由(7)中節點創建能夠知道,inode->i_fop=&def_chr_fops;因此進行賦值操作filp->fop=inode->i_fop後,filp->fop=&def_chr_fops;即file結構成員*fop指向了設備驅動程序中的struct file_operations型數據結構。從而能夠調用驅動程序的open函數。
3、總結
待續。
。。
淺析Linux字符設備驅動程序內核機制