1. 程式人生 > >Linux 字元裝置驅動結構(一)—— cdev 結構體、裝置號相關知識解析[轉載]

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結構體的定義如下:

<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結構的介面主要有以下幾個:

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 命令載入: