【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
一、字符设备基础知识
1、设备驱动分类
linux系统将设备分为3类:字符设备、块设备、网络设备。
字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。
2、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
在Linux内核中:
- 使用cdev结构体来描述字符设备;
- 通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;
- 通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;
在Linux字符设备驱动中:
- 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;
- 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;
-
模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;
用户空间访问该设备的程序:
- 通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;
3. 字符设备驱动模型
二、设备号
1. 主设备号和次设备号
对字符设备的访问是通过文件系统内的设备名称进行的,那些名称被称为特殊文件、设备文件,或者简单称之为文件系统树的节点,它们通常位于/dev目录。字符设备驱动程序的设备文件可通过ls -l命令输出的第一列中的"c"来识别。块设备也出现在/dev下,由字符"b"标识。
在/dev/下执行ls -l ,可在设备文件项的最后修改日期前看到两个数(用逗号分隔),这个位置通常显示的是文件的长度,而对设备文件,这两个数就是相应设备的主设备号和次设备号,左边红框为主设备号,右边为次设备号
- 主设备号标识设备对应的驱动程序。例如,/dev/sda和/dev/sda1由驱动程序8管理
- 次设备号由内核使用,用于正确确定设备文件所指的设备,我们可以通过次设备号获得一个指向内核设备的直接指针,也可以将次设备号当做设备本地数组的索引。
cdev结构体中dev成员定义了设备号,而dev_t是4个字节,高12位表示主设备号,低20位表示次设备号。
设备号相关操作 :
/*通过主设备号和次设备号获取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>
2.分配和释放设备编号
已知设备编号
// <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);
通常在模块的清除函数中调用。
上面的函数为驱动程序的使用分配设备编号,但是它们并没有告诉内核关于拿来这些编号要做什么。在用户空间程序可访问上述设备之前,驱动程序需要将设备编号和内部函数连接起来,这些内部函数用来实现设备的操作。
3. 动态分配主设备号
如果使用驱动程序的人只有我们自己,则选定一个编号的方法永远行的通,然而,一旦驱动程序被广泛使用,随机选定的主设备号可能造成冲突和麻烦。
因此,对于一个新的驱动程序,应该使用动态分配机制获取主设备号。
动态分配的缺点是:由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点。对于驱动的一般用法,可以从/proc/devices中读取得到。
因此,为了加载一个使用动态主设备号的设备驱动程序,对insmod的调用可替换为一个简单的脚本,该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号,然后创建对应的设备文件。
分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
for example:
- 使用一个全局变量scull_major保存所选设备号,scull_minor保存次设备号
- scull_major初始化值是SCULL_MAJOR,若为0,则为动态分配,若不为0,则选择某个特定的设备号
- 用户可以在编译前修改宏定义,也可以通过insmod命令行指定scull_major的值。
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;
}
4.重要的数据结构
用户空间使用 open() 函数打开一个字符设备 fd = open("/dev/hello",O_RDWR) , 这一函数会调用两个数据结构 struct inode{...}与struct file{...} ,二者均在虚拟文件系统VFS处
4.1 file_operations 结构体
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.
4.2 file 结构
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
文件对应的目录项结构
4.3 inode结构体
内核用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);
4.4 chardevs数组
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结构的接口主要有以下几个:
1. 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之间的连接:
- 将整个结构体清零;
- 初始化list成员使其指向自身;
- 初始化kobj成员;
- 初始化ops成员;
2. struct cdev *cdev_alloc(void)
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).
3. int cdev_add(struct cdev *p, dev_t dev, unsigned count)
该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了。
4. void cdev_del(struct cdev *p)
该函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了。
5.早期函数
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创建对应的设备。
此后就可以通过系统调用操作设备文件了。
1.自动创建设备节点
利用udev 设备文件系统(mdev)来实现设备文件的自动创建,主要做的是在驱动初始化的代码里调用调用class_create(...)为该设备创建一个class,再为每个设备调用device_create(...)创建对应的设备。
内核中定义的struct class结构体对应一个类,class_create(…)函数可以创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点。
加载模块的时候,用户空间中的udev会自动响应 device_create()函数,去/sysfs下寻找对应的类从而创建设备节点。
1.1 创建一个类class_create(...) 函数
//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);
1.2 销毁函数:void class_destroy(struct class *cls)
void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;
class_unregister(cls);
}
1.3 创建一个字符设备文件device_create(...) 函数
/*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 查看,会发现设备号已经申请成功;