一、字符設備基礎知識
1、設備驅動分類
linux系統將設備分為3類:字符設備、塊設備、網絡設備。使用驅動程序:

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

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

二、cdev 結構體解析
在Linux內核中,使用cdev結構體來描述一個字符設備,cdev結構體的定義如下:
[cpp] view plain copy 
<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%20cdev結構的接口主要有以下幾個: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該函數主要對struct%20cdev結構體做初始化,最重要的就是建立cdev%20和%20file_operations之間的連接:(1)%20將整個結構體清零;
(2)%20初始化list成員使其指向自身;
(3)%20初始化kobj成員;
(4)%20初始化ops成員;
b%20--struct%20cdev%20*cdev_alloc(void);
%20 %20 該函數主要分配一個struct%20cdev結構,動態申請一個cdev內存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在調用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 在上面的兩個初始化的函數中,我們沒有看到關于owner成員、dev成員、count成員的初始化;其實,owner成員的存在體現了驅動程序與內核模塊間的親密關系,struct%20module是內核對于一個模塊的抽象,該成員在字符設備中可以體現該設備隸屬于哪個模塊,在驅動程序的編寫中一般由用戶顯式的初始化%20.owner%20=%20THIS_MODULE,%20該成員可以防止設備的方法正在被使用時,設備所在模塊被卸載。而dev成員和count成員則在cdev_add中才會賦上有效的值。
c%20--%20int%20cdev_add(struct%20cdev%20*p,%20dev_t%20dev,%20unsigned%20count);
%20 %20 %20 該函數向內核注冊一個struct%20cdev結構,即正式通知內核由struct%20cdev%20*p代表的字符設備已經可以使用了。
當然這里還需提供兩個參數:
(1)第一個設備號%20dev,
(2)和該設備關聯的設備編號的數量。
這兩個參數直接賦值給struct%20cdev%20的dev成員和count成員。
d%20--%20void%20cdev_del(struct%20cdev%20*p);
%20 %20 該函數向內核注銷一個struct%20cdev結構,即正式通知內核由struct%20cdev%20*p代表的字符設備已經不可以使用了。
%20 %20 從上述的接口討論中,我們發現對于struct%20cdev的初始化和注冊的過程中,我們需要提供幾個東西
(1)%20struct%20file_operations結構指針;
(2)%20dev設備號;
(3)%20count次設備號個數。
但是我們依舊不明白這幾個值到底代表著什么,而我們又該如何去構造這些值!
三、設備號相應操作
1%20--%20主設備號和次設備號(二者一起為設備號):
%20 %20 %20一個字符設備或塊設備都有一個主設備號和一個次設備號。主設備號用來標識與設備文件相連的驅動程序,用來反映設備類型。次設備號被驅動程序用來辨別操作的是哪個設備,用來區分同類型的設備。
linux內核中,設備號用dev_t來描述,2.6.28中定義如下:
typedef%20u_long%20dev_t;
在32位機中是4個字節,高12位表示主設備號,低20位表示次設備號。
內核也為我們提供了幾個方便操作的宏實現dev_t:
1)%20-- 從設備號中提取major和minor
MAJOR(dev_t%20dev);
MINOR(dev_t%20dev);
2)%20-- 通過major和minor構建設備號
MKDEV(int%20major,int%20minor);
注:這只是構建設備號。并未注冊,需要調用 register_chrdev_region 靜態申請;
[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、分配設備號(兩種方法):
a%20--%20靜態申請:
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動態分配: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; } 可以看到二者都是調用了__register_chrdev_region%20函數,其源代碼如下:
[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); } 通過這個函數可以看出 register_chrdev_region和 alloc_chrdev_region%20的區別,register_chrdev_region直接將Major%20注冊進入,而 alloc_chrdev_region從Major%20=%200%20開始,逐個查找設備號,直到找到一個閑置的設備號,并將其注冊進去;二者應用可以簡單總結如下:
%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 可以看到,除了前面兩個函數,還加了一個register_chrdev%20函數,可以發現這個函數的應用非常簡單,只要一句就可以搞定前面函數所做之事;
下面分析一下register_chrdev%20函數,其源代碼定義如下:
[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); } 調用了 __register_chrdev(major,%200,%20256,%20name,%20fops)%20函數:[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; } 可以看到這個函數不只幫我們注冊了設備號,還幫我們做了cdev%20的初始化以及cdev%20的注冊;3、注銷設備號:
void%20unregister_chrdev_region(dev_t%20from,%20unsigned%20count);
4、創建設備文件:
%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 查看,會發現設備號已經申請成功;