Fork Me on GitHub

源码开放学ARM

LASO - Learn ARM with Source Open

首页         目录索引         资料下载         代码下载         联系作者        
下载PDF打印版本

主设备号和次设备号

主次设备号

字符设备通过文件系统中的名字来存取. 那些名字称为文件系统的特殊文件, 或者设备文件, 或者文件系统的简单结点; 惯例上它们位于 /dev 目录. 字符驱动的特殊文件由使用 ls -l 的输出的第一列的”c”标识. 块设备也出现在 /dev 中, 但是它们由”b”标识. 本章集中在字符设备, 但是下面的很多信息也适用于块设备.

如果你发出 ls -l 命令, 你会看到在设备文件项中有 2 个数(由一个逗号分隔)在最后修改日期前面, 这里通常是文件长度出现的地方. 这些数字是给特殊设备的主次设备编号. 下面的列表显示了一个典型系统上出现的几个设备. 它们的主编号是 1, 4, 7, 和 10, 而次编号是 1, 3, 5, 64, 65, 和 129.

crw-rw-rw- 1 root  root  1,  3 Apr 11  2002 null 
crw------- 1 root  root  10, 1 Apr 11  2002 psaux 
crw------- 1 root  root  4,  1 Oct 28 03:04 tty1 
crw-rw-rw- 1 root  tty   4, 64 Apr 11  2002 ttys0 
crw-rw---- 1 root  uucp  4, 65 Apr 11  2002 ttyS1 
crw--w---- 1 vcsa  tty   7,  1 Apr 11  2002 vcs1 
crw--w---- 1 vcsa  tty   7,129 Apr 11  2002 vcsa1 
crw-rw-rw- 1 root  root  1,  5 Apr 11  2002 zero  

传统上, 主编号标识设备相连的驱动. 例如, /dev/null 和 /dev/zero 都由驱动 1 来管理, 而虚拟控制台和串口终端都由驱动 4 管理; 同样, vcs1 和 vcsa1 设备都由驱动 7 管理. 现代 Linux 内核允许多个驱动共享主编号, 但是你看到的大部分设备仍然按照一个主编号一个驱动的原则来组织.

次编号被内核用来决定引用哪个设备. 依据你的驱动是如何编写的(如同我们下面见到的), 你可以从内核得到一个你的设备的直接指针, 或者可以自己使用次编号作为本地设备数组的索引. 不论哪个方法, 内核自己几乎不知道次编号的任何事情, 除了它们指向你的驱动实现的设备.

主设备号是由 include/linux/major.h 定义的。 设备号和设备名可在内核源代码的 Documentation/devices.txt 里查到, mknod 可为这些指定的设备创建节点,当然节点的位置不是一定要在/dev下,但是为了便于管理一般都是指定/dev。

mknod 命令

mknod - make block or character special files

mknod [OPTION]... NAME TYPE [MAJOR MINOR]  	
    option 有用的就是 -m 了  
    name   自定义  
    type   有 b 和 c 还有 p  
    主设备号  
    次设备号  	
	b	表示特殊文件是面向块的设备(磁盘、软盘或磁带)。
	c	表示特殊文件是面向字符的设备(其他设备)。
	p	创建 FIFO(已命名的管道)。

举例
$ mknod mylcd c 29 0 (创建一个自己的lcd设备,主设备号为 29,等价于 /dev/fb0 )

实验一下,如果用 cp 命令和 mv 命令分别对一个设备文件进行操作,会有什么结果? (结论是 cp 不可用,mv 可以)

注册设备

注册一个字符设备的经典方法是使用:

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

这里, major 是感兴趣的主编号, name 是驱动的名字(出现在 /proc/devices), fops 是缺省的 file_operations 结构. 一个对 register_chrdev 的调用为给定的主编号, 并且为每一个建立一个缺省的 cdev 结构.

如果你使用 register_chrdev, 从系统中去除你的设备的正确的函数是:

void unregister_chrdev(unsigned int major, const char *name);

major 和 name 必须和传递给 register_chrdev 的相同, 否则调用会失败.

这两个通用方法的声明在 include/linux/fs.h 中,它们的实现主要体现在 fs.h 文件的内联函数和 fs/char_dev.c 中.

调用范例

struct file_operations xxx_fops = 
{
	.owner = THIS_MODULE,
	.open = xxx_open,
	.read = xxx_read,
	.write = xxx_write,
	.release = xxx_release,
};

xxx_init() 中注册设备
rc = register_chrdev(XXX_MAJOR, "akae", &xxx_fops);

xxx_release() 中卸载设备
unregister_chrdev(XXX_MAJOR, "akae");

当前已经注册使用的设备可以通过 cat /proc/devices 文件得到:

Character devices:
 1 mem
 2 pty
 3 ttyp
 4 ttyS
 6 lp
 7 vcs
 10 misc
 13 input
 14 sound 
 21 sg
 180 usb

Block devices:
 2 fd
 8 sd
 11 sr
 65 sd
 66 sd 

设备编号的内部表示

在内核中, dev_t 类型(在 linux/types.h 中定义)用来持有设备编号 – 主次部分都包括. 对于 2.6.0 内核, dev_t 是 32 位的量, 12 位用作主编号, 20 位用作次编号. 你的代码应当, 当然, 对于设备编号的内部组织从不做任何假设; 相反, 应当利用在 linux/kdev_t.h 中的一套宏定义. 为获得一个 dev_t 的主或者次编号, 使用:

MAJOR(dev_t dev); 
MINOR(dev_t dev);

相反, 如果你有主次编号, 需要将其转换为一个 dev_t, 使用:

MKDEV(int major, int minor); 


/* linux/fs.h */
struct inode {
	...
	dev_t                   i_rdev;
	...
};

/* linux/types.h */
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t		dev_t;

/* linux/kdev_t.h *?
#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))

cdev结构体

在 Linux 2.6 内核中使用 cdev结构体描述字符设备, cdev 结构体的定义在 include/linux/cdev.h 中,内容如下:

struct cdev  
{ 
	struct kobject kobj;   /*所属模块*/ 
	struct module *owner;    
	struct file_operations  /*文件操作结构体*/ 
	struct list_head list; 
	dev_t dev;           /*设备号*/ 
	unsigned int count; 
}; 

Linux 2.6 内核提供了一组函数用于操作 cdev 结构体,它们的实现在 fs/char_dev.c 中,声明如下所示:

void cdev_init(struct cdev *, struct file_operations *); 
struct cdev *cdev_alloc(void); 
void cdev_put(struct cdev *p); 
int cdev_add(struct cdev *, dev_t, unsigned); 
void cdev_del(struct cdev *); 

cdev_init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接。

cdev_init()函数内部实现

void cdev_init(struct cdev *cdev, struct file_operations *fops) 
{ 
	memset(cdev, 0, sizeof *cdev); 
	INIT_LIST_HEAD(&cdev->list); 
	cdev->kobj.ktype = &ktype_cdev_default; 
	kobject_init(&cdev->kobj); 
	cdev->ops = fops;    /*将传入的文件操作结构体指针赋值给cdev的ops*/ 
}  

cdev_alloc()函数用于动态申请一个cdev内存。

struct cdev *cdev_alloc(void) 
{ 
	struct  cdev  *p=kmalloc(sizeof(struct  cdev),GFP_KERNEL);  /*分配cdev的内存*/ 
	if (p) { 
		memset(p, 0, sizeof(struct cdev)); 
		p->kobj.ktype = &ktype_cdev_dynamic; 
		INIT_LIST_HEAD(&p->list); 
		kobject_init(&p->kobj); 
	} 
	return p; 
} 

分配和释放设备号

在 调用 cdev_add() 函数向系统注册字符设备之前 , 应首先调用 register_chrdev_region() 函数向系统申请设备号。

相反地 ,在调用 cdev_del() 函数从系统注销字符设备之后,应该调用 unregister_chrdev_region() 以释放原先申请的设备号,

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);
}

void cdev_del(struct cdev *p) 
{ 
	cdev_unmap(p->dev, p->count); 
	kobject_put(&p->kobj); 
}

其中用到的申请区域的两个函数的原型如下,它们的实现也在 fs/char_dev.c 中:

int  register_chrdev_region(dev_t  from,  unsigned  count,  const  char *name); 	
void unregister_chrdev_region(dev_t from, unsigned count); 

当 register_chrdev_region 返回值为0,则表示申请成功。
如果 from 参数是0,代表要求系统自动分配一个可用的 major 并返回这个 major 。

Examples

struct cdev uart_cdev;
dev_t uart_dev_no;

int uart_drv_init(void)
{
	printk("uart_drv init ok \n");

	//register_chrdev(UART_MAJOR, "myttyS3", &uart_drv_fops);

	// use cdev
	uart_dev_no = MKDEV(UART_MAJOR, 3);
	register_chrdev_region(uart_dev_no, 1, "myttyS3");
	cdev_init(&uart_cdev, &uart_drv_fops);
	cdev_add(&uart_cdev, uart_dev_no, 1);

	return 0;
}

void uart_drv_exit(void)
{
	printk("uart_drv exit ok \n");

	//unregister_chrdev(UART_MAJOR, "myttyS3");

	// use cdev 
	unregister_chrdev_region(uart_dev_no, 1);
	cdev_del(&uart_cdev);

	return;
}

上一节 | 目录索引 | 下一节

blog comments powered by Disqus