linux系统将设备分为3类:字符设备、块设备、网络设备。
字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据须要按照前后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
块设备:是指能够从设备的任意位置读取必定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
每个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序经过设备文件(或称设备节点)来使用驱动程序操做字符设备和块设备。node
在Linux内核中:linux
在Linux字符设备驱动中:shell
模块卸载函数经过cdev_del( )来注销cdev,经过 unregister_chrdev_region( )来释放设备号;后端
用户空间访问该设备的程序:数组
对字符设备的访问是经过文件系统内的设备名称进行的,那些名称被称为特殊文件、设备文件,或者简单称之为文件系统树的节点,它们一般位于/dev目录。字符设备驱动程序的设备文件可经过ls -l命令输出的第一列中的"c"来识别。块设备也出如今/dev下,由字符"b"标识。网络
在/dev/下执行ls -l ,可在设备文件项的最后修改日期前看到两个数(用逗号分隔),这个位置一般显示的是文件的长度,而对设备文件,这两个数就是相应设备的主设备号和次设备号,左边红框为主设备号,右边为次设备号数据结构
cdev结构体中dev成员定义了设备号,而dev_t是4个字节,高12位表示主设备号,低20位表示次设备号。app
设备号相关操做 :异步
/*经过主设备号和次设备号获取dev*/ dev = MKDEV(int major,int minor); #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) /*经过dev获取主设备号*/ #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) /*经过dev获取次设备号*/ #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //经过major和minor构建设备号 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))</span>
已知设备编号async
// <linux/fs.h> int register_chrdev_region(dev_t from, unsigned count, const char *name); /* * 成功 -返回 0 ;失败 - 返回错误码,不能使用所请求的编号区域 * first - 要分配的设备编号范围的起始值,first的次设备号常常被置为0,但对该函数来说并非必须的 * count - 是所请求的连续设备编号的个数 * name - 和编号范围关联的设备名称,它将出如今/proc/devices 和 sysfs中。 */
动态分配
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned count, const char *name) /* * dev - 用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号 * firstminor - 要使用的被请求的第一个次设备号,一般是0 * count - 是所请求的连续设备编号的个数 * name - 和编号范围关联的设备名称,它将出如今/proc/devices 和 sysfs中。 */
设备编号释放
void unregister_chrdev_region(dev_t from, unsigned count);
一般在模块的清除函数中调用。
上面的函数为驱动程序的使用分配设备编号,可是它们并无告诉内核关于拿来这些编号要作什么。在用户空间程序可访问上述设备以前,驱动程序须要将设备编号和内部函数链接起来,这些内部函数用来实现设备的操做。
若是使用驱动程序的人只有咱们本身,则选定一个编号的方法永远行的通,然而,一旦驱动程序被普遍使用,随机选定的主设备号可能形成冲突和麻烦。
所以,对于一个新的驱动程序,应该使用动态分配机制获取主设备号。
动态分配的缺点是:因为分配的主设备号不能保证始终一致,因此没法预先建立设备节点。对于驱动的通常用法,能够从/proc/devices中读取获得。
所以,为了加载一个使用动态主设备号的设备驱动程序,对insmod的调用可替换为一个简单的脚本,该脚本在调用insmod以后读取/proc/devices以得到新分配的主设备号,而后建立对应的设备文件。
分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
for example:
if(scull_major){ dev = MKDEV(scull_mgjor,scull_minor); result = register(dev,scull_nr_devs,"scull"); } else{ result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); scull_major = MAJOR(dev); } if(result < 0){ printk(KERN_WARNING "scull : can't get major %d\n",scull_major); return result; }
用户空间使用 open() 函数打开一个字符设备 fd = open("/dev/hello",O_RDWR) , 这一函数会调用两个数据结构 struct inode{...}与struct file{...} ,两者均在虚拟文件系统VFS处
file_operations结构用来创建驱动程序操做和设备编号的链接。每一个打开的文件(在内部由一个file结构表示)和一组函数关联(指向file_operations结构的f_op字段),这些操做主要用来实现系统调用。
file_operations结构或者指向这类结构的指针称为fops,结构中每一个字段都必须指向驱动程序中实现特定操做的函数,对于不支持的操做,对应的字段可置为NULL值。
//<linux/fs.h> struct file_operations { struct module *owner;/*拥有该结构的模块的指针,通常为THIS_MODULES*/ 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); /*只用于读取目录,对于设备文件该字段为NULL*/ unsigned int (*poll) (struct file *, struct poll_table_struct *);/*轮询函数,判断目前是否能够进行非阻塞的读取或写入*/ long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* 不用BLK的文件系统,将使用此函数代替ioctl*/ long (*compat_ioctl) (struct file *, unsigned int, unsigned long); /* 代替ioctl*/ 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 *, int datasync); /*刷新待处理数据*/ int (*aio_fsync) (struct kiocb *, int datasync); /*异步fsync*/ int (*fasync) (int, struct file *, int); /*通知设备FASYNC标志发生变化*/ int (*lock) (struct file *, int, struct file_lock *);/* 实现文件加锁*/ ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /*一般为NULL*/ unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /*在当前的进程地址空间找的一个未映射的内存段*/ int (*check_flags)(int); /*法容许模块检查传递给 fnctl(F_SETFL...) 调用的标志*/ int (*flock) (struct file *, int, struct file_lock *);/**/ ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /*由VFS调用,将管道数据粘贴到文件*/ ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /*由VFS调用,将文件数据粘贴到管道*/ int (*setlease)(struct file *, long, struct file_lock **);/**/ long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len); /**/ };
结构中许多参数包含__user字符串,它是一种形式的文档,代表指针是一个用户空间地址,不能直接引用。对一般的编译来说,__user没有任何效果,可是可由外部检查软件使用,用来寻找对用户空间地址的错误使用
struct module *owner
第一个 file_operations 成员指向拥有这个结构的模块的指针。内核使用这个字段以免在模块的操做正在被使用时卸载该模块。几乎全部的状况下,该成员都会被初始化为THIS_MODULE(<linux/module.h> 中定义的宏.)
loff_t (*llseek) (struct file * filp , loff_t p, int orig);
修改文件的当前 读写位置,并将新位置做为(正的)返回值。 loff_t 是一个长偏移量,即便在32位平台上也至少占用64位数据宽度。出错时返回一个负的返回值。若是这个函数指针是NULL,对seek的调用将会以某种不可预期的方式修改file结构中的位置计数器。
ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p);
用来从设备中读取数据。若被赋为NULL,将致使read系统调用出错并返回-EINVAL("Invalid argument, 非法参数") 。
ssize_t (*aio_read)(struct kiocb * , char __user * buffer, size_t size , loff_t p);
初始化一个异步读取操做 ——即在函数返回前可能不会完成的读操做.若是这个方法是 NULL, 全部的操做会由 read 代替进行(同步地).
ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos)
发送数据给设备.。若是 NULL, -EINVAL 返回给调用 write 系统调用的程序. 若是非负, 返回值表明成功写的字节数。
ssize_t (*aio_write)(struct kiocb *, const char __user * buffer, size_t count, loff_t * ppos);
初始化设备上的一个异步写
int (*readdir) (struct file * filp, void *, filldir_t);
对于设备文件这个成员应当为 NULL; 它用来读取目录, 而且仅对文件系统有用.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
poll 方法是poll, epoll, 和 select系统调用的后端实现 ,用做查询对一个或多个文件描述符的读或写是否会阻塞。poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 而且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 若是一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
系统调用 ioctl 提供了一种设备特定命令的方法。 另外, 内核还能识别一部分ioctl命令,没必要调用fops表中ioctl。若是设备不提供 ioctl 方法, 对于任何未事先定义的请求,ioctl将返回错误(-ENOTTY, "设备无这样的 ioctl")。
int (*mmap) (struct file *, struct vm_area_struct *)
mmap 用来请求将设备内存映射到进程的地址空间。 若是这个方法是 NULL, mmap 系统调用返回 -ENODEV.
int (*open) (struct inode * inode , struct file * filp ) ;
尽管这始终是对设备文件进行的第一个操做, 然而并不要求驱动必定要声明一个对应的方法. 若是这个项是 NULL, 设备的打开操做永远成功, 但系统不会通知驱动程序。
int (*flush) (struct file *);
对flush 操做的调用发生在进程关闭设备文件描述符副本的时候, 它应当执行(而且等待)设备的任何未完成的操做,这个必须不要和用户查询请求的 fsync 操做混淆了. 当前, flush 在不多驱动中使用; 若是 flush 为 NULL, 内核简单地忽略用户应用程序的请求.
int (*release) (struct inode *, struct file *);
在文件结构被释放时引用这个操做. 如同 open, release 能够为 NULL.
int(*fsynch)(struct file *,struct dentry *,int datasync);
刷新待处理的数据,若是没有实现,返回-EINVAL
int (*aio_fsync)(struct kiocb *, int);
fsynch方法的异步版本
int (*fasync) (int, struct file *, int);
用来通知设备它的 FASYNC 标志的改变. 若设备不支持,该字段能够是NULL。
int (*lock) (struct file *, int, struct file_lock *);
lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 可是设备驱动几乎从不实现它.
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
这些方法实现发散/汇聚读和写操做. 应用程序偶尔须要作一个包含多个内存区的单个读或写操做;这些系统调用容许它们这样作而没必要对数据进行额外拷贝. 若是这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另外一个.设备驱动经常使 sendfile 为 NULL.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
sendpage 是 sendfile 的另外一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中。这个任务一般由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动能够置这个方法为 NULL。
int (*check_flags)(int)
这个方法容许模块检查传递给 fnctl(F_SETFL...) 调用的标志.
int (*dir_notify)(struct file *, unsigned long);
这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不须要实现 dir_notify.
file结构体表明一个已经打开的文件,它由内核在open时建立,并传递给在文件上进行操做的全部函数,直至close。在文件的全部实例都被关闭以后,内核就会释放相应的数据结构。
在内核源码中,file指的是struct file自己,而filp是指向这个结构体的指针。
mode_t f_mode;
文件模式。经过FMODE_READ, FMODE_WRITE位来标识文件是否可读或可写。内核在调用驱动程序的read和write前已经检查了访问权限,因此没必要为这两个方法检查权限。在没有得到对应访问权限而打开文件的状况下,对文件的读写操做将被内核拒绝,驱动程序无需为此做额外的判断。
loff_t f_pos;
当前读写文件的位置。若是想知道当前文件当前位置在哪,驱动能够读取这个值而不会改变其位置。对read,write来讲,当其接收到一个loff_t型指针做为其最后一个参数时,他们的读写操做便做更新文件的位置,而不须要直接执行filp ->f_pos操做。而llseek方法的目的就是用于改变文件的位置。
unsigned int f_flags;
文件标志,如O_RDONLY, O_NONBLOCK以及O_SYNC。在驱动中能够检查O_NONBLOCK标志查看是否有非阻塞请求。其它的标志较少使用。特别地注意的是,读写权限的检查是使用f_mode而不是f_flog。全部的标量定义在头文件中
struct file_operations *f_op;
与文件相关的各类操做。内核在执行open操做时对这个指针赋值,之后须要处理这些操做时就读取这个指针。
void *private_data;
在驱动调用open方法以前,open系统调用设置此指针为NULL值。你能够很自由的将其作为你本身须要的一些数据域或者无论它,如,你能够将其指向一个分配好的数据,可是你必须记得在file struct被内核销毁以前在release方法中释放这些数据的内存空间。private_data用于在系统调用期间保存各类状态信息是很是有用的。
struct dentry *f_dentry
文件对应的目录项结构
内核用inode结构体在内部表示一个文件。所以,它与file结构不一样,file结构表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向一个inode结构体。
inodev结构体包含了大量的文件相关的信息,可是就针对驱动代码来讲,咱们只要关心其中的两个域便可:
dev_t i_rdev;
表示设备文件的inode结构,这个字段实际上包含了真正的设备编号。
struct cdev *i_cdev;
表示字符设备的内核内部结构,当inode指向一个字符设备文件时,该字段指向struct cdev结构的指针。
struct inode { struct hlist_node i_hash; struct list_head i_list; struct list_head i_sb_list; struct list_head i_dentry; unsigned long i_ino; atomic_t i_count; unsigned int i_nlink; uid_t i_uid;//inode拥有者id gid_t i_gid;//inode所属群组id dev_t i_rdev;//如果设备文件,表示记录设备的设备号 u64 i_version; loff_t i_size;//inode所表明大少 #ifdef __NEED_I_SIZE_ORDERED seqcount_t i_size_seqcount; #endif struct timespec i_atime;//inode最近一次的存取时间 struct timespec i_mtime;//inode最近一次修改时间 struct timespec i_ctime;//inode的生成时间 unsigned int i_blkbits; blkcnt_t i_blocks; unsigned short i_bytes; umode_t i_mode; spinlock_t i_lock; struct mutex i_mutex; struct rw_semaphore i_alloc_sem; const struct inode_operations *i_op; const struct file_operations *i_fop; struct super_block *i_sb; struct file_lock *i_flock; struct address_space *i_mapping; struct address_space i_data; #ifdef CONFIG_QUOTA struct dquot *i_dquot[MAXQUOTAS]; #endif struct list_head i_devices; union { struct pipe_inode_info *i_pipe; struct block_device *i_bdev; struct cdev *i_cdev;//如果字符设备,对应的为cdev结构体 };
从inode中获取主设备号和次设备号:
unsigned int iminor(struct inode *inode); unsigned int imajor(struct inode *inode);
struct inode{...}中的i_cdev指向字符设备的cdev结构体,而全部字符设备都在chrdevs数组中
#define CHRDEV_MAJOR_HASH_SIZE 255 static DEFINE_MUTEX(chrdevs_lock); static struct char_device_struct { struct char_device_struct *next; // 结构体指针 unsigned int major; // 主设备号 unsigned int baseminor; // 次设备起始号 int minorct; // 次备号个数 char name[64]; struct cdev *cdev; /* will die */ } *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; // 只能挂255个字符主设备
全局数组 chrdevs 包含了255(CHRDEV_MAJOR_HASH_SIZE 的值)个 struct char_device_struct的元素,每个对应一个相应的主设备号。
若是分配了一个设备号,就会建立一个 struct char_device_struct 的对象,并将其添加到 chrdevs 中;这样,经过chrdevs数组,咱们就能够知道分配了哪些设备号。
在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义以下:
struct cdev { struct kobject kobj;/*基于kobject*/ struct module *owner; /*所属模块*/ const struct file_operations *ops; /*设备文件操做函数集*/ struct list_head list; dev_t dev; /*设备号*/ unsigned int count; /*该种类型设备数目*/ };
内核给出的操做struct cdev结构的接口主要有如下几个:
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之间的链接:
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; }
该函数主要分配一个struct cdev结构,动态申请一个cdev内存,并作了cdev_init中所作的前面3步初始化工做(第四步初始化工做须要在调用cdev_alloc后,显式的作初始化即: .ops=xxx_ops).
该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p表明的字符设备已经可使用了。
该函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p表明的字符设备已经不可使用了。
2.6内核以前的注册一个字符设备驱动程序的方式为:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); /* * major - 主设备号 * name - 驱动名称(出如今/porc/devices) * fops - 默认的file_operations结构 */
移除设备:
int unregister_chrdev(unsigned int major, const char *name, struct file_operations *fops);
利用cat /proc/devices查看申请到的设备名,设备号。
1)使用mknod手工建立:mknod filename type major minor (eg: mknod /dev/hello c 250 0)建立设备文件
2)自动建立设备节点:
利用udev(mdev)来实现设备文件的自动建立,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备建立一个class,再为每一个设备调用device_create建立对应的设备。
此后就能够经过系统调用操做设备文件了。
利用udev 设备文件系统(mdev)来实现设备文件的自动建立,主要作的是在驱动初始化的代码里调用调用class_create(...)为该设备建立一个class,再为每一个设备调用device_create(...)建立对应的设备。
内核中定义的struct class结构体对应一个类,class_create(…)函数能够建立一个类,这个类存放于sysfs下面,一旦建立好了这个类,再调用 device_create(…)函数来在/dev目录下建立相应的设备节点。
加载模块的时候,用户空间中的udev会自动响应 device_create()函数,去/sysfs下寻找对应的类从而建立设备节点。
//owner:THIS_MODULE name : 名字 #define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ }) 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); } EXPORT_SYMBOL_GPL(__class_create);
void class_destroy(struct class *cls) { if ((cls == NULL) || (IS_ERR(cls))) return; class_unregister(cls); }
/*struct class *class :类 struct device *parent:NULL dev_t devt :设备号 void *drvdata :null、 const char *fmt :名字*/ 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; } struct device *device_create_vargs(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, va_list args) { return device_create_groups_vargs(class, parent, devt, drvdata, NULL, fmt, args); }
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);
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
测试程序 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); }
编译成功后,使用 insmod 命令加载,这里须要手动建立字符设备结点,也能够自动建立,详见6.自动建立结点
而后用cat /proc/devices 查看,会发现设备号已经申请成功;