linux设备驱动–字符设备驱动程序
目录
简介
字符设备是Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节
流进行读写操作的设备,读写数据是分先后顺序的。常见的字符设备包括鼠标、键盘、显示器、串口等等。当我们执行ls -l /dev的时候,就能看到大量的设备文件,c就是字符设备,b就是块设备,网络设备没有对应的设备文件。编写一个外部模块的字符设备驱动,除了要实现编写一个模块所需要的代码之外,还需要编写作为一个字符设备的代码。
设备号
对字符设备的访问是通过文件系统内的设备名称进行的。那些名称被称为特殊文件、设备文件,或者简单称之为文件系统树的节点,它们通常位于/dev目录.字符设备驱动程 序的设备文件可通过ls -I命令输出的第一列中的“c"来识别.块设备也出现在/dev下, 但它们由字符“b"标识。
设备编号的内部表达
在内核中,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);
分配和释放设备编号
在建立一个字符设备之前,我们的驱动程序首先要做的事情就是获得 一个或者多个设备 编号。完成该工作的必要函数是register_chrdev_region,该函数在<linux/fs.h>中声明
- 静态分配设备号的函数原型
int register_chrdev_region(dev_t first, unsigned int count, char *name);
参数 | 说明 |
---|---|
first | 要分配的设备编号范围的初始值, 这组连续设备号的起始设备号, 相当于register_chrdev()中主设备号 |
count | 连续编号范围, 是这组设备号的大小(也是次设备号的个数) |
name | 编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称 |
register_chrdev _region的返回值在分配成功时为0。在错误情况下,将返回一个负的错误码, 并且不能使用所请求的编号区域。
适用于:提前明确知道所需要的设备编号
- 动态分配设备号的函数原型
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
参数 | 说明 |
---|---|
dev | 输出型参数,获得一个分配到的设备号 |
firstminor | 次设备号的基准,从第几个次设备号开始分配 |
count | 次设备号的个数 |
name | 驱动的名字 |
- 释放设备号的函数原型
void unregister_chrdev_region(dev_t from, unsigned count);
参数 | 说明 |
---|---|
from | 设备号 |
count | 次设备号的个数 |
重要的数据结构
file_operations结构
Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。
用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。
字符设备驱动程序经常用到的5种操作
详细的解析可以参考:file_operations 说明
struct file_operations
{
ssize_t (*read)(struct file *,char *, size_t, loff_t *);//从设备同步读取数据
ssize_t (*write)(struct file *,const char *, size_t, loff_t *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);//执行设备IO控制命令
int (*open) (struct inode *, struct file *);//打开
int (*release)(struct inode *, struct file *);//关闭
...
};
这个结构定义在<linux/fs.h>中,其中包含了一组函数指针。每个打开的文件(在内部由一个file结构表示)和一组函数关联(通过包含指向一个file_operations结构的f_op字段)。这些操作主要用来实现系统调用,命名为open、read等等。
可以认为文件是一个“对象”,而操作它的函数是“方法”,如果采用面向对象编程的术语来表达就是, 对象声明的动作将作用千其本身。
在file_operations方法中,有许多参数包含有__user字符串,它其实是一种形式的文档而已,表明指针是一个用户空间地址,因此不能被直接引用
owner:它是指向 “拥有" 该结构的模块的指针。 内核使用这个字段以避免在校块的操作正在被使用时卸载该楼块。 儿乎在所有的情况下, 该成员都会被初始化为THIS_MODULE, 它是定义在<linux/module.h>中的一个宏.
file结构
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
在<linux/fs.h>中定义的structfile是设备驱动程序所使用的第二个最重要的数据结构。struct file是一个内核结构,它不会出现在用户程序中。
file结构代表一个打开的文件(它并不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的file结构)。它由内核在open时创建,井传递给在该 文件上进行操作的所有函数,直到最后的close函数。在文件的所有实例都被关闭之后, 内核会释放这个数据结构。
- mode_t f_mode
文件模式。它通过FMODE_READ和FMODE_ERITE位来标识文件是否可读或可写(或可读写)。读者可能会认为要在自己的open或ioctl函数中查看这个字段,以便检查是否拥有读/写访问权限,但由千内核在调用驱动程序的read和write前已经检查了访问权限,所以不必为这两个方法检查权限。在没有获得对应访问权限而打开文件的情况下,对文件的读写操作将被内核拒绝,驱动程序无需为此而作额外的判断。 - loff_t f_pos
当前的读/写位置。loff_t是一个64位的数(用gee的术语说就是longlong)。 如果驱动程序需要知道文件中的当前位置,可以读取这个值,但不要去修改它。 readlwri1e会使用它们接收到的最后那个指针参数来更新这一位置,而不是直接对 filp->f_pos进行操作。这一规则的一个例外是llseek方法,该方法的目的本身就是为了修改文件位置。 - unsigned int f_flags
文件标志、如O_RDONLY、O_NONBLOCK和O_SYNC。为了检查用户请求的是否是非阻塞式的操作驱动程序需要桧查O_NONBLOCK标志,而其他标志很少用到。注意,检查读/写权限应该查看f_mode而不是f_flags。所有这些标志都定义在<linux/fcntl.h>中。 - struct file_operations *f_op
与文件相关的操作。内核在执行open操作时对这个指针赋值,以后需要处理这些操作时就读取这个指针。filp->f_op中的值决不会为方便引用而保存起来;也就是说,我们可以在任何需要的时候修改文件的关联操作,在返回给调用者之后, 新的操作方法就会立即生效。例如,对应千主设备号l(/dev/null、/dev/zero等等) 的open代码根据要打开的次设备号替换filp->f_op中的操作。这种技巧允许相同主设备号下的设备实现多种操作行为,而不会增加系统调用的负担。这种替换文件操作的能力在面向对象编程技术中称为"方法重载"。 - void *priva七e_data
open系统调用在调用驱动程序的open方法前将这个指针置为NULL。驱动程序可 以将这个字段用千任何目的或者忽峈这个字段.驱动程序可以用这个字段指向已分配的数据,但是一定要在内核销毁file结构前在release方法中释放内存。 private_data是跨系统调用时保存状态信息的非常有用的资源,我们的大部分示 例都使用了它。 - struct dentry *f_dentry
件对应的目录项(dentry)结构。除了用filp->f_dentry->d_inode的方式来 访问索引节点结构之外,设备驱动程序的开发者们一般无需关心dentry结构。
inode结构
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
unsigned int i_blkbits;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct mutex i_mutex;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash;
struct list_head i_wb_list; /* backing dev IO list */
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
u64 i_version;
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
#ifdef CONFIG_IMA
atomic_t i_readcount; /* struct files open RO */
#endif
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head i_fsnotify_marks;
#endif
void *i_private; /* fs or device private pointer */
};
内核用inode结构在内部表示文件, 因此它和file结构不同、 后者表示打开的文件描述符。 对单个文件, 可能会有许多个表示打开的文件描述符的file结构, 但它们都指向单个inode结构。
inode结构中包含了大量有关文件的信息。作为常规,只有下面两个字段对编写驱动程 序代码有用:
- dev_t i_rdev
对表示设备文件的inode结构, 该字段包含了真正的设备编号。 - struct cdev *i_cdev
struct cdev是表示字符设备的内核的内部结构。当inode指向一个字符设备文件时 , 该字段包含了指向struct cdev结构的指针。
内核增加了两个新的宏, 可用来从一个 inode中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
字符设备注册和删除
字符设备的注册
内核内部使用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;
};
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops =&my_fops;
需要用下面的代码来初始化已分配到的结构
void cdev_init(struct cdev *cdev, struct file_operations *fops);
owner字段, 应被设置为THIS_MODULE。
在cdev结构设置好之后 , 最后的步骤是通过下面的调用告诉内核该结构的信息:
nt cdev_add(struct cdev *dev, dev_t num, unsigned int count);
字段 | 说明 |
---|---|
dev | cdev结构 |
num | 该设备对应的第一个设备编号 |
count | 和该设 备关联的设备编号的数量 |
这个调用可能会失败。如果它返回一 个负的错误码, 则设备不会被添加到系统中
移除一个字符设备
void cdev_del(struct cdev *dev);
cdev_del 和 unregister_chrdev_region 这两个函数合起来的功能相当于 unregister_chrdev 函
数。
一个方便使用的宏
<linux/kernel.h>中的container_of宏
container_of(pointer, container_type, container_field);
它可用于从包含在某个结构中的指针获得结构本身的指针。
内核代码和用户空间之间移动数据的函数
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
unsigned long copy_to_user(void *to, const void *from, unsigned long count);
自动创建设备节点
在前面说的添加完字符设备之后,如何让用户看到并能够使用这个设备,根据linux一切皆文件的原则,需要创建一个设备节点文件,一般有两种方法:
手动创建设备节点
[root@localhost ~]# mknod /dev/myled c 2 1 //创建一个设备
以上命令表示创建一个名为"myled"的设备,c 表示字符设备 后续2和1则表示主设备号和次设备号
自动创建设备节点
创建类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添
加自动创建设备节点相关代码。首先要创建一个 class 类, class 是个结构体,定义在文件
include/linux/device.h 里面
/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
/**
* class_create - create a struct class structure
* @owner: pointer to the module that is to "own" this struct class
* @name: pointer to a string for the name of this class.
* @key: the lock_class_key for this class; used by mutex lock debugging
*
* This is used to create a struct class pointer that can then be used
* in calls to device_create().
*
* Returns &struct class pointer on success, or ERR_PTR() on error.
*
* Note, the pointer created here is to be destroyed when finished by
* making a call to class_destroy().
*/
struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)
{
struct class *cls;
int retval;
cls = kzalloc(sizeof(*cls), GFP_KERNEL);
if (!cls) {
retval = -ENOMEM;
goto error;
}
cls->name = name;
cls->owner = owner;
cls->class_release = class_create_release;
retval = __class_register(cls, key);
if (retval)
goto error;
return cls;
error:
kfree(cls);
return ERR_PTR(retval);
}
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
返回值是个指向结构体 class 的指针,也就是创建的类。
关于结构体class,也定义在device.h 中
/**
* struct class - device classes
* @name: Name of the class.
* @owner: The module owner.
* @class_attrs: Default attributes of this class.
* @dev_groups: Default attributes of the devices that belong to the class.
* @dev_kobj: The kobject that represents this class and links it into the hierarchy.
* @dev_uevent: Called when a device is added, removed from this class, or a
* few other things that generate uevents to add the environment
* variables.
* @devnode: Callback to provide the devtmpfs.
* @class_release: Called to release this class.
* @dev_release: Called to release the device.
* @suspend: Used to put the device to sleep mode, usually to a low power
* state.
* @resume: Used to bring the device from the sleep mode.
* @ns_type: Callbacks so sysfs can detemine namespaces.
* @namespace: Namespace of the device belongs to this class.
* @pm: The default device power management operations of this class.
* @p: The private data of the driver core, no one other than the
* driver core can touch this.
*
* A class is a higher-level view of a device that abstracts out low-level
* implementation details. Drivers may see a SCSI disk or an ATA disk, but,
* at the class level, they are all simply disks. Classes allow user space
* to work with devices based on what they do, rather than how they are
* connected or how they work.
*/
struct class {
const char *name;
struct module *owner;
struct class_attribute *class_attrs;
const struct attribute_group **dev_groups;
struct kobject *dev_kobj;
int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
char *(*devnode)(struct device *dev, umode_t *mode);
void (*class_release)(struct class *class);
void (*dev_release)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct kobj_ns_type_operations *ns_type;
const void *(*namespace)(struct device *dev);
const struct dev_pm_ops *pm;
struct subsys_private *p;
};
删除类
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下(class.c)
/**
* class_destroy - destroys a struct class structure
* @cls: pointer to the struct class that is to be destroyed
*
* Note, the pointer to be destroyed must have been created with a call
* to class_create().
*/
void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;
class_unregister(cls);
}
创建设备
使用 device_create 函数在类下面创建设备, device_create 函数原型如下:
drivers/base/core.c
/**
* device_create - creates a device and registers it with sysfs
* @class: pointer to the struct class that this device should be registered to
* @parent: pointer to the parent struct device of this new device, if any
* @devt: the dev_t for the char device to be added
* @drvdata: the data to be added to the device for callbacks
* @fmt: string for the device's name
*
* This function can be used by char device classes. A struct device
* will be created in sysfs, registered to the specified class.
*
* A "dev" file will be created, showing the dev_t for the device, if
* the dev_t is not 0,0.
* If a pointer to a parent struct device is passed in, the newly created
* struct device will be a child of that device in sysfs.
* The pointer to the struct device will be returned from the call.
* Any further sysfs files that might be required can be created using this
* pointer.
*
* Returns &struct device pointer on success, or ERR_PTR() on error.
*
* Note: the struct class passed to this function must have previously
* been created with a call to class_create().
*/
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
device_create 是个可变参数函数,
参数 class 就是设备要创建哪个类下面;
参数 parent 是父设备,一般为 NULL,也就是没有父设备;
参数 devt 是设备号;
参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;
参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。
返回值就是创建好的设备。
删除设备
卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原
型如下:
/**
* device_destroy - removes a device that was created with device_create()
* @class: pointer to the struct class that this device was registered with
* @devt: the dev_t of the device that was previously registered
*
* This call unregisters and cleans up a device that was created with a
* call to device_create().
*/
void device_destroy(struct class *class, dev_t devt)
{
struct device *dev;
dev = class_find_device(class, NULL, &devt, __match_devt);
if (dev) {
put_device(dev);
device_unregister(dev);
}
}
参数 class 是要删除的设备所处的类
参数 devt 是要删除的设备号