字符设备驱动程序
在这里首先需要说明,这一系列的文章可能跨度比较大,比如按照正常的逻辑,写完怎样搭建开发环境之后,就该接着讲一个helloworld的内核模块,然 后才是这一篇字符驱动的文章,但是我不想重复的去写那么多东西,至于有这方面需求的博友,可以随便google一下就能查到很多类似的文章,因为我是希望 linux的博客和Mac专题的一起同时对比着写,所以理论上的东西不会太多,主要还是讲解每一个能真正运行起来的小实例,用这种方式来阐述内核开发的方 方面面。而且需要重点说明的是,这一个阶段涉及的比较多的内容是关于网络方面的扩展,也就是主要介绍的内容是Netfilter,因为在Mac上我也主要 是介绍Net Kernel Extensions。当然为了阐述NetFilter,也必须介绍一些基本的知识,例如内存管理,同步机制,中断等等。本文主要介绍的是如何实现一个简 单的字符驱动程序,来达到内核和用户空间之间的内存数据交换。
一、设备分类
在Linux系统中设备分为三种类型:字符驱动、块设备驱动和网络接口设备。字符设备是指那些只能一个字符一个字符按照先后顺序进行读写的设备。而块设备是指那些可以从设备的任意位置读取指定长度数据的设备,不必按照先后顺序进行读取。
常见的字符设备有鼠标、键盘、串口等,常见的块设备有硬盘、U盘、SD卡、光盘等。在Linux中用ls
-l命令可以看到第一列的开头字母为c的就是字符设备(character),开头字母为b的就是块设备(block),下图就是我的电脑中/dev目录
下的设备部分信息:
二、设备版本号
Linux中无论块设备还是字符设备都对应有一个唯一的设备号,而这个设备号又是又主设备号和次设备号构成,
主设备号表示一类设备,次设备号用以表示这一类设备中某一个具体的设备。设备号用dev_t类型表示,定义如下:
点击(此处)折叠或打开
- #include <linux/types.h>
- typedef u_long dev_t;
在32位操作系统中,高12位为主设备号,低20位为次设备号。通过下面定义的宏可以获取主设备号和次设备号:
点击(此处)折叠或打开
- #define MINORBITS 20
- #define MINORMASK ((1U << MINORBITS) -1)
- // 设备号右移20位得到主设备号
- #define MAJOR(dev) ((unsigned int)) ((dev) >> MINORBITS)
- // 设备号取第20位即将高20位清0作为次设备号
- #define MINOR(dev) ((unsigned int)) ((dev) & MINORMASK)
同样的可以用定义的MKDEV宏去根据主设备号和次设备号组成一个设备号
点击(此处)折叠或打开
- // ma为主设备号,mi为次设备号
- #define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi))
以 上介绍的是如何手动的静态分配设备号,但是如果有好几个人在写不同的驱动程序,就会有可能导致设备号的重复,以至于产生冲突,为了防止这种情况产 生,Linux内核提供了另外一种分配设备号的方式就是动态分配,所谓动态分配就是由linux内核自己判断分配给注册的设备一个什么样的设备号。这个分 配设备号的函数定义如下:
点击(此处)折叠或打开
- #include <linux/fs.h>
- /**
- * @from 设备号的起始值,一般之设置主设备号,次设备号自动分配
- * @count 申请的次设备个数
- * @name 设备名称
- * return 成功返回0,否则返回错误码
- **/
- int register_chdev_region(dev_t from, unsigned int count, const char *name);
另外一个更新的更好的申请设备号的函数定义如下:
点击(此处)折叠或打开
- #include <linux/fs.h>
- /**
- * @dev 将用于返回申请成功的第一个设备号,通常次设备号是0
- * @count 申请的次设备个数
- * @name 设备名称
- * return 成功返回0,否则返回错误码
- **/
- int alloc_chrdev_region(dev_t *dev, unsigned int count, char *name);
释放已经申请到的设备号的函数定义如下:
点击(此处)折叠或打开
- #include <linux/fs.h>
- /**
- * @from 要释放的设备号序列中第一个设备号
- * @count 释放的设备个数
- * return 成功返回0,否则返回错误码
- **/
- int unregister_chrdev_region(dev_t from, unsigned int count);
通常我们需要在模块加载函数中分配设备号,在模块卸载函数中释放设备号。
三、字符设备之cdev结构和file_operations结构
Linux内核中不管是字符驱动还是块设备驱动还是混杂驱动,都有一个模型,就拿字符设备驱动来说,其模型就是struct cdev,在内核中cdev结构就代表着一个驱动程序。其定义如下:
点击(此处)折叠或打开
- struct cdev {
- struct kobject kobj; // 每个cdev都有一个kobject对象的引用
- struct module *owner; // 所属模块,y\一般用THIS_MODULE
- const struct file_operations *ops; // 文件操作结构,在写驱动时,
- // 其结构体内的大部分函数要被实现
- struct list_head list; // 与cdev 对应的字符设备文件的 inode->i_devices 的链表头
- dev_t dev; // 设备号,int 类型,高12位为主设备号,低20位为次设备号
- unsigned int count; // 设备引用计数
- };
cdev结构也有两种初始化方式:静态内存初始化和动态内存初始化。
静态内存定义初始化:
点击(此处)折叠或打开
- struct cdev my_cdev;
- cdev_init(&my_cdev, &fops);
- my_cdev.owner = THIS_MODULE;
动态内存定义初始化:
点击(此处)折叠或打开
- struct cdev *my_cdev = cdev_alloc();
- my_cdev->ops = &fops;
- my_cdev->owner = THIS_MODULE;
下面给出cdev_init函数和cdev_alloc()函数的定义。
cdev_init:
点击(此处)折叠或打开
- 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;
- }
cdev_alloc:
点击(此处)折叠或打开
- 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;
- }
两种方式功能基本上是一样的,在不同的场合选择不同的方式,只是cdev_init还多赋了一个 cdev->ops 的值。
初始化 cdev 后,需要把它添加到系统中去。为此可以调用 cdev_add() 函数。传入 cdev 结构的指针,起始设备编号,以及设备编号范围。
cdev_add:
点击(此处)折叠或打开
- 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);
- }
当一个字符设备驱动不再需要的时候(比如模块卸载),就可以用 cdev_del() 函数来释放 cdev 占用的内存。
cdev_del:
点击(此处)折叠或打开
- void cdev_del(struct cdev *p)
- {
- cdev_unmap(p->dev, p->count);
- kobject_put(&p->kobj);
- }
其中 cdev_unmap() 调用 kobj_unmap() 来释放 cdev_map 散列表中的对象。kobject_put() 释放 cdev 结构本身。
综上可以看出整个cdev的使用流程如下图所示:
struct file_operations结构里面包含了所有对设备操作的函数,这些函数会对应到应用层的open,read,write,lseek,close等函数上,这之间是如何关联的呢?
linux系统中,当在用户空间调用open打开一个文件,此时返回一个file descriptor,对应的在内核中创建了一个struct file结构,struct file结构定义如下:
点击(此处)折叠或打开
- struct file {
- union {
- struct list_head fu_list; // 文件对象链表指针linux/include/linux/list.h
- struct rcu_head fu_rcuhead; // RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
- } f_u;
- struct path f_path; // 包含dentry和mnt两个成员,用于确定文件路径
- #define f_dentry f_path.dentry // f_path的成员之一,当前文件的dentry结构
- #define f_vfsmnt f_path.mnt // 表示当前文件所在文件系统的挂载根目录
- const struct file_operations *f_op; //与该文件相关联的操作函数
- atomic_t f_count; // 文件的引用计数(有多少进程打开该文件)
- unsigned int f_flags; //对应于open时指定的flag
- mode_t f_mode; // 读写模式:open的mod_t mode参数
- off_t f_pos; // 该文件在当前进程中的文件偏移量
- struct fown_struct f_owner; // 该结构的作用是通过信号进行I/O时间通知的数据。
- unsigned int f_uid, f_gid; // 文件所有者id,所有者组id
- struct file_ra_state f_ra; // 在linux/include/linux/fs.h中定义,文件预读相关
- unsigned long 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;
- spinlock_t f_ep_lock;
- #endif /* #ifdef CONFIG_EPOLL */
- struct address_space *f_mapping;
- };
struct file说明:
1). struct dentry
dentry的中文名称是目录项,是Linux文件系统中某个索引节点(inode)的链接。这个索引节点可以是文件,也可以是目录。
inode(可理解为ext2
inode)对应于物理磁盘上的具体对象,dentry是一个内存实体,其中的d_inode成员指向对应的inode。也就是说,一个inode可以在
运行的时候链接多个dentry,而d_count记录了这个链接的数量。定义如下:
点击(此处)折叠或打开
- struct dentry {
- atomic_t d_count; // 目录项对象使用计数器,可以有未使用态,使用态和负状态
- unsigned int d_flags; // 目录项标志
- struct inode * d_inode; // 与文件名关联的索引节点
- struct dentry * d_parent; // 父目录的目录项对象
- struct list_head d_hash; // 散列表表项的指针
- struct list_head d_lru; // 未使用链表的指针
- struct list_head d_child; // 父目录中目录项对象的链表的指针
- struct list_head d_subdirs; // 对目录而言,表示子目录目录项对象的链表
- struct list_head d_alias; // 相关索引节点(别名)的链表
- int d_mounted; // 对于安装点而言,表示被安装文件系统根项
- struct qstr d_name; // 文件名
- unsigned long d_time; /* used by d_revalidate */
- struct dentry_operations *d_op; // 目录项方法
- struct super_block * d_sb; // 文件的超级块对象
- vunsigned long d_vfs_flags;
- void * d_fsdata; // 与文件系统相关的数据
- unsigned char d_iname [DNAME_INLINE_LEN]; // 存放短文件名
- };
2). struct files_struct
对于每个进程,包含一个files_struct结构,用来记录文件描述符的使用情况,定义在include/linux/file.h中:
点击(此处)折叠或打开
- struct files_struct {
- atomic_t count; // 使用该表的进程数
- struct fdtable *fdt;
- struct fdtable fdtab;
- spinlock_t file_lock ____cacheline_aligned_in_smp;
- int next_fd; // 数值最小的最近关闭文件的文件描述符,下一个可用的文件描述符
- struct embedded_fd_set close_on_exec_init; // 执行exec时需要关闭的文件描述符初值集合
- struct embedded_fd_set open_fds_init; // 文件描述符的屏蔽字初值集合
- struct file * fd_array[NR_OPEN_DEFAULT]; // 默认打开的fd队列
- };
- struct fdtable {
- unsigned int max_fds;
- struct file ** fd; // 指向打开的文件描述符列表的指针,开始的时候指向fd_array,
- // 当超过max_fds时,重新分配地址
- fd_set *close_on_exec; // 执行exec需要关闭的文件描述符位图(fork,exec即不被子进
- // 程继承的文件描述符)
- fd_set *open_fds; // 打开的文件描述符位图
- struct rcu_head rcu; // rcu锁
- struct fdtable *next;
- };
3). struct fs_struct
点击(此处)折叠或打开
- struct fs_struct {
- atomic_t count; // 计数器
- rwlock_t lock; // 读写锁
- int umask;
- struct dentry * root, * pwd, * altroot; // 根目录("/"),当前目录以及替换根目录
- struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
- };
4). struct inode
索引节点对象由inode结构体表示,定义文件在linux/fs.h中。
点击(此处)折叠或打开
- struct inode {
- struct hlist_node i_hash; // 哈希表
- struct list_head i_list; // 索引节点链表
- struct list_head i_dentry; // 目录项链表
- unsigned long i_ino; // 节点号
- atomic_t i_count; // 引用记数
- umode_t i_mode; // 访问权限控制
- unsigned int i_nlink; // 硬链接数
- uid_t i_uid; // 使用者id
- gid_t i_gid; // 使用者id组
- kdev_t i_rdev; // 实设备标识符
- loff_t i_size; // 以字节为单位的文件大小
- struct timespec i_atime; // 最后访问时间
- struct timespec i_mtime; // 最后修改(modify)时间
- struct timespec i_ctime; // 最后改变(change)时间
- unsigned int i_blkbits; // 以位为单位的块大小
- unsigned long i_blksize; // 以字节为单位的块大小
- unsigned long i_version; // 版本号
- unsigned long i_blocks; // 文件的块数
- unsigned short i_bytes; // 使用的字节数
- spinlock_t i_lock; // 自旋锁
- struct rw_semaphore i_alloc_sem; // 索引节点信号量
- struct inode_operations *i_op; // 索引节点操作表
- 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; // 设备地址映射
- struct dquot *i_dquot[MAXQUOTAS];// 节点的磁盘限额
- struct list_head i_devices; // 块设备链表
- struct pipe_inode_info *i_pipe; // 管道信息
- struct block_device *i_bdev; // 块设备驱动
- unsigned long i_dnotify_mask;// 目录通知掩码
- struct dnotify_struct *i_dnotify; // 目录通知
- unsigned long i_state; // 状态标志
- unsigned long dirtied_when;// 首次修改时间
- unsigned int i_flags; // 文件系统标志
- unsigned char i_sock; // 套接字
- atomic_t i_writecount; // 写者记数
- void *i_security; // 安全模块
- __u32 i_generation; // 索引节点版本号
- union {
- void *generic_ip; // 文件特殊信息
- } u;
- };
进程中打开一个文件F,实际上就是要在内存中建立F的dentry,和inode结构,并让它们与进程结构联系来,把VFS中定义的接口给接起来。进而关联到具体设备的操作上。linux文件操作结构定义如下:
点击(此处)折叠或打开
- struct file_operations{
- struct module *owner;
- // 指向拥有该结构的模块的指针,避免正在操作时被卸载,一般为初始化为THIS_MODULES
- loff_t (*llseek) (struct file *, loff_t, int);
- // llseek用来修改文件当前的读写位置,返回新位置
- // loff_t为一个”长偏移量”。当此函数指针为空,seek调用将会以不可预期的方式修改file结构中的位置计数器。
- ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
- // 从设备中同步读取数据。读取成功返回读取的字节数。设置为NULL,调用时返回-EINVAL
- ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
- // 初始化一个异步的读取操作,为NULL时全部通过read处理
- ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
- // 向设备发送数据。
- ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
- // 初始化一个异步的写入操作。
- int (*readdir) (struct file *, void *, filldir_t);
- // 仅用于读取目录,对于设备文件,该字段为 NULL
- unsigned int (*poll) (struct file *, struct poll_table_struct *);
- // 返回一个位掩码,用来指出非阻塞的读取或写入是否可能。
- // 将pool定义为 NULL,设备会被认为即可读也可写。
- int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
- // 提供一种执行设备特殊命令的方法。不设置入口点,返回-ENOTTY
- long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
- // 不使用BLK的文件系统,将使用此种函数指针代替ioctl
- long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
- // 在64位系统上,32位的ioctl调用,将使用此函数指针代替
- int (*mmap) (struct file *, struct vm_area_struct *);
- // 用于请求将设备内存映射到进程地址空间。如果无此方法,将访问-ENODEV。
- int (*open) (struct inode *, struct file *);
- // 如果为空,设备的打开操作永远成功,但系统不会通知驱动程序
- // 由VFS调用,当VFS打开一个文件,即建立了一个新的”struct file”,之后调用open方法分配文件结构。open属于struct inode_operations。
- int (*flush) (struct file *);
- // 发生在进程关闭设备文件描述符副本,执行并等待,若设置为NULL,内核将忽略用户应用程序的请求。
- int (*release) (struct inode *, struct file *);
- // file结构释放时,将调用此指针函数,release与open相同可设置为NULL
- int (*fsync) (struct file *, struct dentry *, int datasync);
- // 刷新待处理的数据,如果驱动程序没有实现,fsync调用将返回-EINVAL
- int (*aio_fsync) (struct kiocb *, int datasync);
- // 异步fsync
- 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 *);
- // readv和writev 分散/聚集型的读写操作,实现进行涉及多个内存区域的单次读或写操作。
- ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
- // 实现sendfile调用的读取部分,将数据从一个文件描述符移到另一个,设备驱动通常将其设置为 NULL
- ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
- // 实现sendfile调用的另一部分,内核调用将其数据发送到对应文件,每次一个数据页,设备驱动通常将其设置为NULL
- unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
- // 在进程地址空间找到一个合适的位置,以便将底层设备中的内存段映射到该位置。大部分驱动可将其设置为NULL
- int (*check_flags)(int);
- // 允许模块检查传递给fcntl(F_SETEL…)调用的标志
- int (*dir_notify)(struct file *filp, unsigned long arg);
- // 应用程序使用fcntl来请求目录改变通知时,调用该方法。仅对文件系统有效,驱动程序不必实现。
- int (*flock) (struct file *, int, struct file_lock *);
- // 实现文件锁
- };
file_oprations结构中包含了所有我们可能会用到的操作,但是实际情况下,可能我们不必实现全部的函数,将根据需要,实现
file_operations结构中定义的操作函数,下面这个实例,我们将简单实现open, release, read, write,
ioctl这几个函数。这几个函数对应到用户态的posix函数就是open,close,read,write,ioctl。
如何把file_operations的实例关联到cdev设备上呢?前面已经提到了,这里就不再赘述。
字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申
请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、
write()、ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。
四、字符设备内核模块源程序分析
1). memdev.h
点击(此处)折叠或打开
- View Code
- #ifndef _MEMDEV_H_
- #define _MEMDEV_H_
- #ifndef MEMDEV_MAJOR
- #define MEMDEV_MAJOR 251 /*预设的mem的主设备号*/
- #endif
- #ifndef MEMDEV_NR_DEVS
- #define MEMDEV_NR_DEVS 2 /*设备数*/
- #endif
- #ifndef MEMDEV_SIZE
- #define MEMDEV_SIZE 4096
- #endif
- /*mem设备描述结构体*/
- struct mem_dev
- {
- char *data;
- unsigned long size;
- };
- #endif /* _MEMDEV_H_ */
2). memdev.c
点击(此处)折叠或打开
- #include <linux/module.h>
- #include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include"memdev.h" - static int mem_major = MEMDEV_MAJOR;
- module_param(mem_major, int, S_IRUGO);
- struct mem_dev *mem_devp; /*设备结构体指针*/
- struct cdev cdev;
- /*文件打开函数*/
- int mem_open(struct inode *inode, struct file *filp)
- {
- struct mem_dev *dev;
- /*获取次设备号*/
- int num = MINOR(inode->i_rdev);
- if (num >= MEMDEV_NR_DEVS)
- return -ENODEV;
- dev = &mem_devp[num];
- /*将设备描述结构指针赋值给文件私有数据指针*/
- filp->private_data = dev;
- return 0;
- }
- /*文件释放函数*/
- int mem_release(struct inode *inode, struct file *filp)
- {
- return 0;
- }
- /*读函数*/
- static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
- {
- unsigned long p = *ppos; /*记录文件指针偏移位置*/
- unsigned int count = size; /*记录需要读取的字节数*/
- int ret = 0; /*返回值*/
- struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
- /*判断读位置是否有效*/
- if (p >= MEMDEV_SIZE) /*要读取的偏移大于设备的内存空间*/
- return 0;
- if (count > MEMDEV_SIZE - p) /*要读取的字节大于设备的内存空间*/
- count = MEMDEV_SIZE - p;
- /*读数据到用户空间:内核空间->用户空间交换数据*/
- if (copy_to_user(buf, (void*)(dev->data + p), count))
- {
- ret = - EFAULT;
- }
- else
- {
- *ppos += count;
- ret = count;
- printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
- }
- return ret;
- }
- /*写函数*/
- static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
- {
- unsigned long p = *ppos;
- unsigned int count = size;
- int ret = 0;
- struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
- /*分析和获取有效的写长度*/
- if (p >= MEMDEV_SIZE)
- return 0;
- if (count > MEMDEV_SIZE - p) /*要写入的字节大于设备的内存空间*/
- count = MEMDEV_SIZE - p;
- /*从用户空间写入数据*/
- if (copy_from_user(dev->data + p, buf, count))
- ret = - EFAULT;
- else
- {
- *ppos += count; /*增加偏移位置*/
- ret = count; /*返回实际的写入字节数*/
- printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
- }
- return ret;
- }
- /* seek文件定位函数 */
- static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
- {
- loff_t newpos;
- switch(whence) {
- case 0: /* SEEK_SET */ /*相对文件开始位置偏移*/
- newpos = offset; /*更新文件指针位置*/
- break;
- case 1: /* SEEK_CUR */
- newpos = filp->f_pos + offset;
- break;
- case 2: /* SEEK_END */
- newpos = MEMDEV_SIZE -1 + offset;
- break;
- default: /* can't happen */
- return -EINVAL;
- }
- if ((newpos<0) || (newpos>MEMDEV_SIZE))
- return -EINVAL;
- filp->f_pos = newpos;
- return newpos;
- }
- /*文件操作结构体*/
- static const struct file_operations mem_fops =
- {
- .owner = THIS_MODULE,
- .llseek = mem_llseek,
- .read = mem_read,
- .write = mem_write,
- .open = mem_open,
- .release = mem_release,
- };
- /*设备驱动模块加载函数*/
- static int memdev_init(void)
- {
- int result;
- int i;
- dev_t devno = MKDEV(mem_major, 0);
- /* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/
- /* 静态申请设备号*/
- if (mem_major)
- result = register_chrdev_region(devno, 2, "memdev");
- else /* 动态分配设备号 */
- {
- result = alloc_chrdev_region(&devno, 0, 2, "memdev");
- mem_major = MAJOR(devno); /*获得申请的主设备号*/
- }
- if (result < 0)
- return result;
- /*初始化cdev结构,并传递file_operations结构指针*/
- cdev_init(&cdev, &mem_fops);
- cdev.owner = THIS_MODULE; /*指定所属模块*/
- cdev.ops = &mem_fops;
- /* 注册字符设备 */
- cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
- /* 为设备描述结构分配内存*/
- mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
- if (!mem_devp) /*申请失败*/
- {
- result = - ENOMEM;
- goto fail_malloc;
- }
- memset(mem_devp, 0, sizeof(struct mem_dev));
- /*为设备分配内存*/
- for (i=0; i < MEMDEV_NR_DEVS; i++)
- {
- mem_devp[i].size = MEMDEV_SIZE;
- mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
- memset(mem_devp[i].data, 0, MEMDEV_SIZE);
- }
- return 0;
- fail_malloc:
- unregister_chrdev_region(devno, 1);
- return result;
- }
- /*模块卸载函数*/
- static void memdev_exit(void)
- {
- cdev_del(&cdev); /*注销设备*/
- kfree(mem_devp); /*释放设备结构体内存*/
- unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
- }
- MODULE_AUTHOR("David Xie");
- MODULE_LICENSE("GPL");
- module_init(memdev_init);
- module_exit(memdev_exit);
3). Makefile
点击(此处)折叠或打开
- ifneq ($(KERNELRELEASE),)
- # call from kernel build system
- obj-m := memdev.o
- else
- KERNELDIR ?= /lib/modules/$(shell uname -r)/build
- PWD := $(shell pwd)
- modules:
- $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
- endif
- clean:
- rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
编译安装与卸载:
1. 在命令行下切换到Makefile和源文件所在目录,输入make;
2. 安装模块,切换到root用户,然后输入insmod memdev.ko
3. 卸载模块,切换到root用户,然后输入rmmod memdev.ko
五、用户空间操作此字符设备
test_memdev.c
点击(此处)折叠或打开
- #include <stdio.h>
- int main()
- {
- FILE *fp0 = NULL;
- char Buf[4096];
- /*初始化Buf*/
- strcpy(Buf,"Mem is char dev!");
- printf("BUF: %s\n",Buf);
- /*打开设备文件*/
- fp0 = fopen("/dev/memdev0","r+");
- if (fp0 == NULL)
- {
- printf("Open Memdev0 Error!\n");
- return -1;
- }
- /*写入设备*/
- fwrite(Buf, sizeof(Buf), 1, fp0);
- /*重新定位文件位置(思考没有该指令,会有何后果)*/
- fseek(fp0,0,SEEK_SET);
- /*清除Buf*/
- strcpy(Buf,"Buf is NULL!");
- printf("BUF: %s\n",Buf);
- /*读出设备*/
- fread(Buf, sizeof(Buf), 1, fp0);
- /*检测结果*/
- printf("BUF: %s\n",Buf);
- return 0;
- }
编译运行:
1. 编译,在命令行源文件所在目录输入:gcc -o test-memdev test_memdev.c;
2. 运行,在命令行test-memdev所在目录输入:./test-memdev
六、程序测试截图
待完成