您的位置:首页 > 运维架构 > Linux

linux字符设备驱动开发基础知识

2013-02-26 21:42 513 查看
这章将介绍Linux系统的设 备,这样我们才能清楚的知道应用程序和设备驱动程序是如何的工作的,或者说应用程序是如何控制驱动程序的,进而知道应用程序是如何通过驱动程序操作设备 的,另外会详细的介绍设备号及设备文件。

 

Linux设备分类
Linux下的设备通常分为三类,字符设备,块设备和网络设 备。

字符设备

一个字符设 备是一种字节流设备,对设备的存取只能按顺序按字节的存取而不能随机访问,字符设备没有请求缓冲区,所有的访问请求都是按顺序执行的。Linux下的大多设备都是字符设备。应用程序是通过字符设备节点来访问
字符设备的。设备节点一般都由mknod命令都创 建在/dev目录下,下
面的例子显示了串口设备的设备节点。字符设备文件的第一个标志是前面的“c”标志。

root#ls -l /dev/ttyS[0-3]

crw-rw----  1 root  root 4, 64 Feb 18 23:34 /dev/ttyS0

crw-r-----  1 root  root 4, 65 Nov 17 10:26 /dev/ttyS1

crw-rw----  1 root  root 4, 66 Jul  5  2000 /dev/ttyS2

crw-rw----  1 root  root 4, 67 Jul  5  2000 /dev/ttyS3
字符设备是指那些只能按顺序一个字节一个字节读取的设备,但事实上现在一些高级 字符设备也可以从指定位置一次读取一块数据。字符设备是面向数据流的设备,每个字符设备都有一个设备号,设备号由主设备号和次设备号组成。同时Linux使用管理文件相同的方法来管理字符设备,所以每个字符设备在/dev/目录下都有一个对应的设备文件,即设备节点,它们包含了设备的
类型、主/次设备号以 及设备的访问权限控制等,系统通过设备文件来对字符设备进行操作,每个字符设备文件都有自己的与普通文件相同的文件操作函数组结构(struct
file_operations)。字符设 备驱动通常至少需要实现文件操作函数组中的open、release、read和write四种操作方法。常见的字符设备有鼠标、键盘、串口、控制台等。

块设备

存储设备一 般属于块设备,块设备有请求缓冲区,并且支持随机访问而不必按照顺序去存取数据,比如你可以 先存取后面的数据,然后在存取前面的数据,这对字符设备来说是不可能的。Linux下的磁盘
设备都是块设备,尽管在Linux下有块设 备节点,但应用程序一般是通过文件系统及其高速缓存来访问块设备的,而不是直 接通过设备节点来读写块设备上的数据。块设备文件的第一个标志是前面的“b”标志。

root# ls -l /dev/hda[1-3]

brw-rw----  1 root  root  3, 1 Jul  5  2000 /dev/hda1

brw-rw----  1 root  root  3, 2 Jul  5  2000 /dev/hda2

brw-rw----  1 root  root  3, 3 Jul  5  2000 /dev/hda3
块设备是指那些可以从设备的任意位置读取任意长度数据的设备。每个块设备同样有 一个设备号,设备号由主设备号和次设备号组成。同时Linux也使用管 理文件相同的方法来管理块设备,每个块设备在/dev/目录下都
有一个对应的设备文件,即设备节点,它们包含了设备的类型、主/次设备号 以及设备的访问权限控制等,系统通过设备文件来对块设备进行操作,每个块设备文件都有自己的与普通文件相同的文件操作函数组结构(struct
file_operations)。但块设 备需要实现的操作方法远比字符设备的操作方法多得多,也难得多。块设备既可以作为普通的裸设备用来存放任意数据,也可以将块设备按某种文件系统类型的格式 进行格式化,然后按照该文件系统类型的格式来读取块设备上的数据,但不管哪种方式,最后访问设备上的数据都必须通过调用设备本身的操作方法实现,区别在于 前者直接调用块设备的操作方法,而后者则间接调用块设备的操作方法。常见的块设备有各种硬盘、flash磁盘、RAM磁盘等。

网络设备

网络设备不 同于字符设备和块设备,它是面向报文的而不是面向流的,它不支持随机访问,也没有请求缓冲区。在Linux里一个网络设备也可以叫做一个网络接口,如eth0,应用程序是通过Socket而不是设备节点来访问网络设备,在系统里根本就不存在网络设备节点。

网络接口用来与其他设备交换数据,它可以是硬件设备,也可以是纯软件设备,如loopback接口就是一个纯软件设备。网络接口由内核中的网络 子系统驱动,负责发送和接收数据包,但它不需要了解每项事务如何映射到实际传送的数据包,许多网络连接(尤其是使用TCP协议的连接)是面向流的,但网络设备围绕数据包的传输和接收设
计。网络驱动程序不需要知道各个连接的相关信息,它只需处理数据包。网络接口没有像字符设备和块设备一样的设备号,只有一个唯一的名字,如eth0、eth1等,而这个名字也不需要与设备文件节点对应。内核使用一套与数据
包传输相关的函数来与网络设备驱动程序通信,它们不同于字符设备和块设备的read()和write()方法。

设备节点、设备驱动及设备的关联
     当我们访问 一个设备节点是,系统是如果知道使用哪个设备驱动及访问哪个设备的呢?这个是通过设备号来实现的。当我们创建一个设备节点时需要指定主设备号和次设备号。 对于设备节点来说,名字不是重要的,设备号才是最重要的,它实际指定了对应的驱动程序和对应的设备。

Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存 放在/dev目录下,称 为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备
号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。

 

      Linux为所有的
设备文件都提供了统一的操作函数接口,方法是使用数据结构struct file_operations。这个数据结构中包括许多操作函数的指针,如open()、close()、read()和write()等,但由于外设的种类较多,操作方式各不相同。Struct
file_operations结构体中的 成员为一系列的接口函数,如用于读/写的read/write函数和用于控制的ioctl等。打开一个文件就是调用这个文件file_operations中的open操作。不同类型的文件有不同的file_operations成员函数,
如普通的磁盘数据文件,接口函数完成磁盘数据块读写操作;而对于各种设备文件,则最终调用各自驱动程序中的I/O函数进行具体设备的操作。这样,应用程序根本不必考虑操作的是设 备还是普通文件,可一律当作文件处理,具有非常清晰统一的I/O接口。所
以file_operations是文件层 次的I/O接口。

 

主设备号

驱动程序在 初始化时,会注册它的驱动及对应主设备号到系统中,这样当应用程序访问设备节点时,系统就知道它所访问的驱动程序了。你可以通过/proc/devices文件来驱动 系统设备的主设备号。

次设备号

驱动程序遍 历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。这样当应用程序访问设备节点时驱动程序就可以根据次 设备号知道它说访问的设备了。

系统中的每一个字符设备和块设备(网络接口没有设备号)都有一个设备号,传统的UNIX以及早期版本Linux中的设备号是16位的,主次设备号都是8位的,低8位为次设备号,高8位为主设备号,因此系统最多分别支持65536个字符设备和65536个块设备,这个限制已经不能满足当
前层出不穷的各种新设备的需要,所以Linux2.6中对设备号已经进行了扩展,一个设备 号为32位,主设备号为12位,次设备号为20位,但是这32位设备号的编码方式有新旧两种,旧的
设备编号格式为:最高12位为主设备号,最低20位为次设备号;新的设备编号格式为:bit[19:8]是主设备号,bit[31:20]是次设备号的高12位,bit[7:0]是次设备号的低8位。如果知道了一个设备的主设备号major和次设备号minor,那么用MKDEV(major,minor)生成是该设备的旧格式的设备号,用new_encode_dev(MKDEV(major,minor))生成的则是新格式的设备号。Linux支持的各种设备的主设备号定义在include/linux/major.h文件中,而已经在官方注册的主设备
号和次设备号在Documentation/devices.txt文件中可以找到。

老式16位设备 号、32位旧格式设 备号以及32位新格式设
备号的转换操作函数如下:

new_encode_dev(dev_t dev)函数

将32位旧格式 设备号dev转换成32位新格式设备号。

new_decode_dev(u32 dev)函数

将32位新格式 设备号转换成32位旧格式设 备号。

old_encode_dev(dev_t dev)函数

将32位旧格式 设备号转换成老式16位设备号。

dev_t old_decode_dev(u16 val)函数

将老式16位设备号转换成32位旧格式设备号。

Linux中设备节点是通过“mknod”命令来创建
的。一个设备节点其实就是一个文件,Linux中称为设 备文件。有一点必要说明的是,在Linux中,所有 的设备访问都是通过文件的方式,一般的数据文件程序普通文件,设备节点称为设备文件。在Linux内核中网络设备也是通过文件操作的,称为网络设备文件,在用户空间是通过socket接口来访问的。socket号就是网络设备文件描述符。

如:mknod /dev/mydevice c 254 0

(c代表子都设备,254为主设备号,0为次设备号)

Open,close等操作/dev/下设备文件,内核根据文件的主设备号找到对应的设备驱动

主设备号可以分为动态和静态申请。

设备文件

Linux使用对文 件一样管理方式来管理设备,所以对于系统中的每个字符设备或者块设备都必须为其创建一个设备文件,这个设备文件就是放在/dev/目录下的设备节点,它包含了该设备的设备类型(块设备或字符设
备)、设备号(主设备号和次设备号)以及设备访问控制属性等。设备文件可以通过手工用mknod命令生成也可以由udev用户工具 软件在系统启动后根据/sys目录下每个
设备的实际信息创建,使用后一种方式可以为每个设备动态分配设备号,而不必分配固定的设备号,如果系统中的设备不多,而且设备类型又是常见的,可以使用手 工方式生成设备文件,为常用设备创建一个已经分配号的设备号对应的设备文件,这样比较方便。如果系统很大,系统中的设备太多,那么最好动态分配设备号,由udev在系统启动之后根据设备实际信息自动创建设备文件。

Linux下的大部分驱动程序都是字符设备驱动程序,通过下面的学习我们将 会了解到字符设备是如何注册到系统中的,应用程序是如何访问驱动程序的数据的,及字符驱动程序是如何工作的。

设备号

通过前面的 学习我们知道应用程序是通过设备节点来访问驱动程序及设备的,其根本是通过设备节点的设备号(主设备号及从设备号)来关联驱动程序及设备的,字符设备也不 例外(其实字符设备只能这样访问)。这里我们详细讨论Linux内部如何管 理设备号的。

设备号类型

Linux内核里用“dev_t”来表示设备 号,它是一个32位的无符号
数,其高12位用来表示主 设备号,低20位用来表示从 设备号。它被定义在<linux/types.h>头文件里。
内核里提供了操作“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))
MAJOR(dev)用于获取主设备号,MINOR(dev)用于获取从设备号,而MKDEV(ma,mi)用于通过主设备号和从设备号构造"dev_t"数据。

另一点需要 说明的是,dev_t数据类型支持2^12个主设备号,每个主设备号(通常是一个设备驱动)可以支持2^20个设备,目前来说这已经足够大了,但谁又能说将来还能满足要求
呢?一个良好的编程习惯是不要依赖dev_t这个数据类 型,切记必须使用内核提供的操作设备号的函数。

字符设备号注册

内核提供了字符设备号管理的函数接口,作为一个良好的编程习惯,字符设备驱动程 序应该通过这些函数向系统注册或注销字符设备号。

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

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,

            const char *name)

void unregister_chrdev_region(dev_t from, unsigned count)
register_chrdev_region用于向内核 注册已知可用的设备号(次设备号通常是0)范围。由 于历史的原因一些设备的设备号是固定的,你可以在内核源代码树的Documentation/devices.txt文件中找到
这些静态分配的设备号。

alloc_chrdev_region用于动态分 配的设备号并注册到内核中,分配的设备号通过dev参数返回。 作为一个良好的内核开发习惯,我们推荐你使用动态分配的方式来生成设备号。
unregister_chrdev_region用于注销一 个不用的设备号区域,通常这个函数在驱动程序卸载时被调用。

字符设备
Linux2.6内核使用“struct cdev”来记录字符设 备的信息,内核也提供了相关的函数来操作“struct
cdev”对象,他们 定义在<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;

};

void cdev_init(struct cdev *, const 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 *);
对于Linux 2.6内核来说,struct cdev是内核字符设备的基础结构,用来表示一个字符设备, 包含了字符设备需要的全部信息。

kobj:struct kobject对象数据,用
来描述设备的引用计数,是Linux设备模型的 基础结构。我们在后面的“Linux设备模型”在做详细的介绍。

owner:struct module对象数据,描
述了模块的属主,指向拥有这个结构的模块的指针,显然它只有对编译为模块方式的驱动才由意义。一般赋值位“THIS_MODULE”。

ops:struct file_operations对象数据,描
述了字符设备的操作函数指针。对于设备驱动来说,这是一个很重要的数据成员,几乎所有的驱动都要用到这个对象,我们会在下面做详细介绍。

dev:dev_t对象数据,描述了字符设备的设备号。

内核提供了操作字符设备对象“struct cdev”的函数,我们 只能通过这些函数来操作字符设备,例如:初始化、注册、添加、移除字符设备。

cdev_alloc:用于动态 分配一个新的字符设备 cdev 对象,并对其
进行初始化。采用cdev_alloc分配的cdev对象需要显示的初始化owner和ops对象。

// 参考drivers/scsi/st.c:st_probe 函数
struct cdev *cdev = NULL;

cdev = cdev_alloc();

// Error Processing

cdev->owner = THIS_MODULE;

cdev->ops = &st_fops;
 

cdev_init:用于初始 化一个静态分配的cdev对象,一般这
个对象会嵌入到其他的对象中。cdev_init会自动初始 化ops数据,因此应
用程序只需要显示的给owner对象赋值。cdev_init的功能与cdev_alloc基本相同,唯
一的区别是cdev_init初始化一个 已经存在的cdev对象,并且这
个初始化会影响到字符设备删除函数(cdev_del)的行为, 请参考cdev_del函数。

cdev_add:向内核系 统中添加一个新的字符设备cdev,并且使它立
即可用。

cdev_del:从内核系 统中移除cdev字符设备。如
果字符设备是由cdev_alloc动态分配的, 则会释放分配的内存。

cdev_put:减少模块 的引用计数,一般很少会有驱动程序直接调用这个函数。

文件操作对象
Linux中的所有设备都是文件,内核中用“struct file”结构来表示一 个文件。尽管我们的驱动不会直接使用这个结构中的大部分对象,其中的一些数据成员还是很重要的,我们有必要在这里做一些介绍,具体的内容请参考内核源代码
树<linux/fs.h>头文件。

// struct file 中的一些重要数据成员
const struct file_operations    *f_op;

unsigned int         f_flags;

mode_t            f_mode;

loff_t            f_pos;

struct address_space    *f_mapping;
这里我们不对struct file做过多的介绍,另一篇struct file将做详细介绍。这个结构中的f_ops成员是我们的驱动所关心的,它是一个struct
file_operations结构。Linux里的struct file_operations结构描述了一 个文件操作需要的所有函数,它定义在<linux/fs.h>头文件中。

struct file_operations {

    struct module *owner;

    loff_t (*llseek) (struct file *, loff_t, int);

    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

    int (*readdir) (struct file *, void *, filldir_t);

    unsigned int (*poll) (struct file *, struct poll_table_struct *);

    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

    int (*mmap) (struct file *, struct vm_area_struct *);

    int (*open) (struct inode *, struct file *);

    int (*flush) (struct file *, fl_owner_t id);

    int (*release) (struct inode *, struct file *);

    int (*fsync) (struct file *, struct dentry *, int datasync);

    int (*aio_fsync) (struct kiocb *, int datasync);

    int (*fasync) (int, struct file *, int);

    int (*lock) (struct file *, int, struct file_lock *);

    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

    int (*check_flags)(int);

    int (*dir_notify)(struct file *filp, unsigned long arg);

    int (*flock) (struct file *, int, struct file_lock *);

    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);

    int (*setlease)(struct file *, long, struct file_lock **);

};
这是一个很大的结构,包含了所有的设备操作函数指针。当然,对于 一个驱动,不是所有的接口都需要来实现的。对于一个字符设备来说,一般实现open、release、read、write、mmap、ioctl这几个函数就足够了。

这里需要指 出的是,open和release函数的第一个参数是一个struct
inode对象。这是一个内核文件系统索引节点对象,它包含 了内核在操作文件或目录是需要的全部信息。对于字符设备驱动来说,我们关心的是从struct inode对象中获取设备号(inode的i_rdev成员)内核提供了两个函数来做这件事。

static inline unsigned iminor(const struct inode *inode)

{

    return MINOR(inode->i_rdev);

}

static inline unsigned imajor(const struct inode *inode)

{

    return MAJOR(inode->i_rdev);

}
尽管我们可 以直接从inode->i_rdev获取设备 号,但是尽量不要这样做。我们推荐你调用内核提供的函数来获取设备号,这样即使将来inode->i_rdev有所变化,
我们的程序也会工作的很好。

 

字符设备驱 动可以参考Linux 设备驱动程序 第三版和linux设备驱动开发 详解,其中linux设备驱动程序
第三版中讲的:

主次编号

一些重要数据结构

字符设备注册

Open和release

读和写

一些头文件和结构体;

都非常经典, 都理解字符驱动设备很重要,很值得参考!
http://blog.csdn.net/shanzhizi
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐