一、字符設(shè)備基礎(chǔ)知識
1、設(shè)備驅(qū)動分類
linux系統(tǒng)將設(shè)備分為3類:字符設(shè)備、塊設(shè)備、網(wǎng)絡(luò)設(shè)備。使用驅(qū)動程序:

字符設(shè)備:是指只能一個字節(jié)一個字節(jié)讀寫的設(shè)備,不能隨機(jī)讀取設(shè)備內(nèi)存中的某一數(shù)據(jù),讀取數(shù)據(jù)需要按照先后數(shù)據(jù)。字符設(shè)備是面向流的設(shè)備,常見的字符設(shè)備有鼠標(biāo)、鍵盤、串口、控制臺和LED設(shè)備等。
塊設(shè)備:是指可以從設(shè)備的任意位置讀取一定長度數(shù)據(jù)的設(shè)備。塊設(shè)備包括硬盤、磁盤、U盤和SD卡等。
每一個字符設(shè)備或塊設(shè)備都在/dev目錄下對應(yīng)一個設(shè)備文件。linux用戶程序通過設(shè)備文件(或稱設(shè)備節(jié)點(diǎn))來使用驅(qū)動程序操作字符設(shè)備和塊設(shè)備。
2、字符設(shè)備、字符設(shè)備驅(qū)動與用戶空間訪問該設(shè)備的程序三者之間的關(guān)系

如圖,在Linux內(nèi)核中:
a -- 使用cdev結(jié)構(gòu)體來描述字符設(shè)備;
b -- 通過其成員dev_t來定義設(shè)備號(分為主、次設(shè)備號)以確定字符設(shè)備的唯一性;
c -- 通過其成員file_Operations來定義字符設(shè)備驅(qū)動提供給VFS的接口函數(shù),如常見的open()、read()、write()等;
在Linux字符設(shè)備驅(qū)動中:
a -- 模塊加載函數(shù)通過 register_chrdev_region( ) 或 alloc_chrdev_region( )來靜態(tài)或者動態(tài)獲取設(shè)備號;
b -- 通過 cdev_init( ) 建立cdev與 file_operations之間的連接,通過 cdev_add( ) 向系統(tǒng)添加一個cdev以完成注冊;
c -- 模塊卸載函數(shù)通過cdev_del( )來注銷cdev,通過 unregister_chrdev_region( )來釋放設(shè)備號;
用戶空間訪問該設(shè)備的程序:
a -- 通過Linux系統(tǒng)調(diào)用,如open( )、read( )、write( ),來“調(diào)用”file_operations來定義字符設(shè)備驅(qū)動提供給VFS的接口函數(shù);
3、字符設(shè)備驅(qū)動模型

二、cdev 結(jié)構(gòu)體解析
在Linux內(nèi)核中,使用cdev結(jié)構(gòu)體來描述一個字符設(shè)備,cdev結(jié)構(gòu)體的定義如下:
[cpp] view plain copy 
<include/linux/cdev.h> struct cdev { struct kobject kobj; //內(nèi)嵌的內(nèi)核對象. struct module *owner; //該字符設(shè)備所在的內(nèi)核模塊的對象指針. const struct file_operations *ops; //該結(jié)構(gòu)描述了字符設(shè)備所能實(shí)現(xiàn)的方法,是極為關(guān)鍵的一個結(jié)構(gòu)體. struct list_head list; //用來將已經(jīng)向內(nèi)核注冊的所有字符設(shè)備形成鏈表. dev_t dev; //字符設(shè)備的設(shè)備號,由主設(shè)備號和次設(shè)備號構(gòu)成. unsigned int count; //隸屬于同一主設(shè)備號的次設(shè)備號的個數(shù). }; 內(nèi)核給出的操作struct%20cdev結(jié)構(gòu)的接口主要有以下幾個:a%20--%20void%20cdev_init(struct%20cdev%20*,%20const%20struct%20file_operations%20*);
其源代碼如代碼清單如下:
[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
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; } %20 %20 %20該函數(shù)主要對struct%20cdev結(jié)構(gòu)體做初始化,最重要的就是建立cdev%20和%20file_operations之間的連接:(1)%20將整個結(jié)構(gòu)體清零;
(2)%20初始化list成員使其指向自身;
(3)%20初始化kobj成員;
(4)%20初始化ops成員;
b%20--struct%20cdev%20*cdev_alloc(void);
%20 %20 該函數(shù)主要分配一個struct%20cdev結(jié)構(gòu),動態(tài)申請一個cdev內(nèi)存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在調(diào)用cdev_alloc后,顯式的做初始化即:%20.ops=xxx_ops).
其源代碼清單如下:
[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
struct cdev *cdev_alloc(void) { struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); if (p) { INIT_LIST_HEAD(&p->list); kobject_init(&p->kobj, &ktype_cdev_dynamic); } return p; } %20 %20 在上面的兩個初始化的函數(shù)中,我們沒有看到關(guān)于owner成員、dev成員、count成員的初始化;其實(shí),owner成員的存在體現(xiàn)了驅(qū)動程序與內(nèi)核模塊間的親密關(guān)系,struct%20module是內(nèi)核對于一個模塊的抽象,該成員在字符設(shè)備中可以體現(xiàn)該設(shè)備隸屬于哪個模塊,在驅(qū)動程序的編寫中一般由用戶顯式的初始化%20.owner%20=%20THIS_MODULE,%20該成員可以防止設(shè)備的方法正在被使用時,設(shè)備所在模塊被卸載。而dev成員和count成員則在cdev_add中才會賦上有效的值。
c%20--%20int%20cdev_add(struct%20cdev%20*p,%20dev_t%20dev,%20unsigned%20count);
%20 %20 %20 該函數(shù)向內(nèi)核注冊一個struct%20cdev結(jié)構(gòu),即正式通知內(nèi)核由struct%20cdev%20*p代表的字符設(shè)備已經(jīng)可以使用了。
當(dāng)然這里還需提供兩個參數(shù):
(1)第一個設(shè)備號%20dev,
(2)和該設(shè)備關(guān)聯(lián)的設(shè)備編號的數(shù)量。
這兩個參數(shù)直接賦值給struct%20cdev%20的dev成員和count成員。
d%20--%20void%20cdev_del(struct%20cdev%20*p);
%20 %20 該函數(shù)向內(nèi)核注銷一個struct%20cdev結(jié)構(gòu),即正式通知內(nèi)核由struct%20cdev%20*p代表的字符設(shè)備已經(jīng)不可以使用了。
%20 %20 從上述的接口討論中,我們發(fā)現(xiàn)對于struct%20cdev的初始化和注冊的過程中,我們需要提供幾個東西
(1)%20struct%20file_operations結(jié)構(gòu)指針;
(2)%20dev設(shè)備號;
(3)%20count次設(shè)備號個數(shù)。
但是我們依舊不明白這幾個值到底代表著什么,而我們又該如何去構(gòu)造這些值!
三、設(shè)備號相應(yīng)操作
1%20--%20主設(shè)備號和次設(shè)備號(二者一起為設(shè)備號):
%20 %20 %20一個字符設(shè)備或塊設(shè)備都有一個主設(shè)備號和一個次設(shè)備號。主設(shè)備號用來標(biāo)識與設(shè)備文件相連的驅(qū)動程序,用來反映設(shè)備類型。次設(shè)備號被驅(qū)動程序用來辨別操作的是哪個設(shè)備,用來區(qū)分同類型的設(shè)備。
linux內(nèi)核中,設(shè)備號用dev_t來描述,2.6.28中定義如下:
typedef%20u_long%20dev_t;
在32位機(jī)中是4個字節(jié),高12位表示主設(shè)備號,低20位表示次設(shè)備號。
內(nèi)核也為我們提供了幾個方便操作的宏實(shí)現(xiàn)dev_t:
1)%20-- 從設(shè)備號中提取major和minor
MAJOR(dev_t%20dev);
MINOR(dev_t%20dev);
2)%20-- 通過major和minor構(gòu)建設(shè)備號
MKDEV(int%20major,int%20minor);
注:這只是構(gòu)建設(shè)備號。并未注冊,需要調(diào)用 register_chrdev_region 靜態(tài)申請;
[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
//宏定義: #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))</span> 2、分配設(shè)備號(兩種方法):
a%20--%20靜態(tài)申請:
int%20register_chrdev_region(dev_t%20from,%20unsigned%20count,%20const%20char%20*name);
其源代碼清單如下:
[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
int register_chrdev_region(dev_t from, unsigned count, const char *name) { struct char_device_struct *cd; dev_t to = from + count; dev_t n, next; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n)+1, 0); if (next > to) next = to; cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name); if (IS_ERR(cd)) goto fail; } return 0; fail: to = n; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n)+1, 0); kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); } return PTR_ERR(cd); } b%20--%20動態(tài)分配:int%20alloc_chrdev_region(dev_t%20*dev,%20unsigned%20baseminor,%20unsigned%20count,%20const%20char%20*name);
其源代碼清單如下:
[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
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; } 可以看到二者都是調(diào)用了__register_chrdev_region%20函數(shù),其源代碼如下:
[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) { 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; } cd->major = major; cd->baseminor = baseminor; cd->minorct = minorct; strlcpy(cd->name, name, sizeof(cd->name)); i = major_to_index(major); for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ((*cp)->major > major || ((*cp)->major == major && (((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)))) break; /* Check for overlapping minor ranges. */ if (*cp && (*cp)->major == major) { int old_min = (*cp)->baseminor; int old_max = (*cp)->baseminor + (*cp)->minorct - 1; int new_min = baseminor; int new_max = baseminor + minorct - 1; /* New driver overlaps from the left. */ if (new_max >= old_min && new_max <= old_max) { ret = -EBUSY; goto out; } /* New driver overlaps from the right. */ if (new_min <= old_max && new_min >= old_min) { ret = -EBUSY; goto out; } } cd->next = *cp; *cp = cd; mutex_unlock(&chrdevs_lock); return cd; out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); } 通過這個函數(shù)可以看出 register_chrdev_region和 alloc_chrdev_region%20的區(qū)別,register_chrdev_region直接將Major%20注冊進(jìn)入,而 alloc_chrdev_region從Major%20=%200%20開始,逐個查找設(shè)備號,直到找到一個閑置的設(shè)備號,并將其注冊進(jìn)去;二者應(yīng)用可以簡單總結(jié)如下:
%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 register_chrdev_region %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 alloc_chrdev_region
| %20devno = MKDEV(major,minor); ret = register_chrdev_region(devno, 1, "hello"); cdev_init(&cdev,&hello_ops); ret = cdev_add(&cdev,devno,1); | %20 alloc_chrdev_region(&devno, minor, 1, "hello"); %20 %20major = MAJOR(devno); %20 %20cdev_init(&cdev,&hello_ops); %20 %20ret = cdev_add(&cdev,devno,1) | register_chrdev(major,"hello",&hello |
%20 %20 可以看到,除了前面兩個函數(shù),還加了一個register_chrdev%20函數(shù),可以發(fā)現(xiàn)這個函數(shù)的應(yīng)用非常簡單,只要一句就可以搞定前面函數(shù)所做之事;
下面分析一下register_chrdev%20函數(shù),其源代碼定義如下:
[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); } 調(diào)用了 __register_chrdev(major,%200,%20256,%20name,%20fops)%20函數(shù):[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
int __register_chrdev(unsigned int major, unsigned int baseminor, unsigned int count, const char *name, const struct file_operations *fops) { struct char_device_struct *cd; struct cdev *cdev; int err = -ENOMEM; cd = __register_chrdev_region(major, baseminor, count, name); if (IS_ERR(cd)) return PTR_ERR(cd); cdev = cdev_alloc(); if (!cdev) goto out2; cdev->owner = fops->owner; cdev->ops = fops; kobject_set_name(&cdev->kobj, "%s", name); err = cdev_add(cdev, MKDEV(cd->major, baseminor), count); if (err) goto out; cd->cdev = cdev; return major ? 0 : cd->major; out: kobject_put(&cdev->kobj); out2: kfree(__unregister_chrdev_region(cd->major, baseminor, count)); return err; } 可以看到這個函數(shù)不只幫我們注冊了設(shè)備號,還幫我們做了cdev%20的初始化以及cdev%20的注冊;3、注銷設(shè)備號:
void%20unregister_chrdev_region(dev_t%20from,%20unsigned%20count);
4、創(chuàng)建設(shè)備文件:
%20 %20 利用cat%20/
#include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> static int major = 250; static int minor = 0; static dev_t devno; static struct cdev cdev; static int hello_open (struct inode *inode, struct file *filep) { printk("hello_open /n"); return 0; } static struct file_operations hello_ops= { .open = hello_open, }; static int hello_init(void) { int ret; printk("hello_init"); devno = MKDEV(major,minor); ret = register_chrdev_region(devno, 1, "hello"); if(ret < 0) { printk("register_chrdev_region fail /n"); return ret; } cdev_init(&cdev,&hello_ops); ret = cdev_add(&cdev,devno,1); if(ret < 0) { printk("cdev_add fail /n"); return ret; } return 0; } static void hello_exit(void) { cdev_del(&cdev); unregister_chrdev_region(devno,1); printk("hello_exit /n"); } MODULE_LICENSE("GPL"); module_init(hello_init); module_exit(hello_exit); 測試程序%20test.c
[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> main() { int fd; fd = open("/dev/hello",O_RDWR); if(fd<0) { perror("open fail /n"); return ; } close(fd); } makefile:[cpp]%20view%20plain%20copy%20![在CODE上查看代碼片]()
ifneq ($(KERNELRELEASE),) obj-m:=hello.o $(info "2nd") else KDIR := /lib/modules/$(shell uname -r)/build PWD:=$(shell pwd) all: $(info "1st") make -C $(KDIR) M=$(PWD) modules clean: rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order endif 編譯成功后,使用 insmod 命令加載:
然后用cat /proc/devices 查看,會發(fā)現(xiàn)設(shè)備號已經(jīng)申請成功;