Linux 字元裝置驅動結構(一)—— cdev 結構體、裝置號相關知識解析[轉載]
一、字元裝置基礎知識
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結構體的定義如下:
核心給出的操作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; //隸屬於同一主裝置號的次裝置號的個數. };
a -- void cdev_init(struct cdev *, const struct file_operations *);
其原始碼如程式碼清單如下:
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;
}
該函式主要對struct cdev結構體做初始化,最重要的就是建立cdev 和 file_operations之間的連線:(1) 將整個結構體清零;
(2) 初始化list成員使其指向自身;
(3) 初始化kobj成員;
(4) 初始化ops成員;
b --struct cdev *cdev_alloc(void);
該函式主要分配一個struct cdev結構,動態申請一個cdev記憶體,並做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在呼叫cdev_alloc後,顯式的做初始化即: .ops=xxx_ops).
其原始碼清單如下:
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;
}
在上面的兩個初始化的函式中,我們沒有看到關於owner成員、dev成員、count成員的初始化;其實,owner成員的存在體現了驅動程式與核心模組間的親密關係,struct module是核心對於一個模組的抽象,該成員在字元裝置中可以體現該裝置隸屬於哪個模組,在驅動程式的編寫中一般由使用者顯式的初始化 .owner = THIS_MODULE, 該成員可以防止裝置的方法正在被使用時,裝置所在模組被解除安裝。而dev成員和count成員則在cdev_add中才會賦上有效的值。
c -- int cdev_add(struct cdev *p, dev_t dev, unsigned count);
該函式向核心註冊一個struct cdev結構,即正式通知核心由struct cdev *p代表的字元裝置已經可以使用了。
當然這裡還需提供兩個引數:
(1)第一個裝置號 dev,
(2)和該裝置關聯的裝置編號的數量。
這兩個引數直接賦值給struct cdev 的dev成員和count成員。
d -- void cdev_del(struct cdev *p);
該函式向核心登出一個struct cdev結構,即正式通知核心由struct cdev *p代表的字元裝置已經不可以使用了。
從上述的介面討論中,我們發現對於struct cdev的初始化和註冊的過程中,我們需要提供幾個東西
(1) struct file_operations結構指標;
(2) dev裝置號;
(3) count次裝置號個數。
但是我們依舊不明白這幾個值到底代表著什麼,而我們又該如何去構造這些值!
三、裝置號相應操作
1 -- 主裝置號和次裝置號(二者一起為裝置號):
一個字元裝置或塊裝置都有一個主裝置號和一個次裝置號。主裝置號用來標識與裝置檔案相連的驅動程式,用來反映裝置型別。次裝置號被驅動程式用來辨別操作的是哪個裝置,用來區分同類型的裝置。
linux核心中,裝置號用dev_t來描述,2.6.28中定義如下:
typedef u_long dev_t;
在32位機中是4個位元組,高12位表示主裝置號,低20位表示次裝置號。
核心也為我們提供了幾個方便操作的巨集實現dev_t:
1) -- 從裝置號中提取major和minor
MAJOR(dev_t dev);
MINOR(dev_t dev);
2) -- 通過major和minor構建裝置號
MKDEV(int major,int minor);
注:這只是構建裝置號。並未註冊,需要呼叫 register_chrdev_region 靜態申請;
//巨集定義:
#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 -- 靜態申請:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
其原始碼清單如下:
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 -- 動態分配:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
其原始碼清單如下:
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 函式,其原始碼如下:
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 的區別,register_chrdev_region直接將Major 註冊進入,而 alloc_chrdev_region從Major = 0 開始,逐個查詢裝置號,直到找到一個閒置的裝置號,並將其註冊進去;二者應用可以簡單總結如下:
register_chrdev_region alloc_chrdev_region
devno = MKDEV(major,minor); ret = register_chrdev_region(devno, 1, "hello"); cdev_init(&cdev,&hello_ops); ret = cdev_add(&cdev,devno,1); | alloc_chrdev_region(&devno, minor, 1, "hello"); major = MAJOR(devno); cdev_init(&cdev,&hello_ops); ret = cdev_add(&cdev,devno,1) | register_chrdev(major,"hello",&hello |
可以看到,除了前面兩個函式,還加了一個register_chrdev 函式,可以發現這個函式的應用非常簡單,只要一句就可以搞定前面函式所做之事;
下面分析一下register_chrdev 函式,其原始碼定義如下:
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, 0, 256, name, fops) 函式:
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 的初始化以及cdev 的註冊;3、登出裝置號:
void unregister_chrdev_region(dev_t from, unsigned count);
4、建立裝置檔案:
利用cat /proc/devices檢視申請到的裝置名,裝置號。
1)使用mknod手工建立:mknod filename type major minor
2)自動建立裝置節點:
利用udev(mdev)來實現裝置檔案的自動建立,首先應保證支援udev(mdev),由busybox配置。在驅動初始化程式碼裡呼叫class_create為該裝置建立一個class,再為每個裝置呼叫device_create建立對應的裝置。
下面看一個例項,練習一下上面的操作:
hello.c
#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);
測試程式 test.c
#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:
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 命令載入: