目的:写一个统计单词个数的简单驱动。
1、驱动的概念
1.1、什么是驱动
无操作系统时函数可以由工程师自己定义。
有操作系统时,把单一的“驱使硬件设备行动”变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的 API。
对设备驱动最通俗的解释就是“驱使硬件设备行动”。设备驱动充当了硬件和应用软件之间的纽带,应用软件时只需要调用系统软件的应用编程接口( API)就可让硬件去完成要求的工作。
1.2 为什么要有驱动
任何一个计算机系统的运转都是系统中软硬件共同努力的结果,但是软硬件工程师不想涉及对方的领域,因此这个中间的连接需要驱动工程师来解决。
1.3 驱动所在位置
linux设备驱动是以内核模块的形式出现。模块的概念,可以不编入内核镜像,在使用的时候动态的加入到内核中,模块被加载后和其他内核完全一样。
2、linux字符设备驱动结构
2.1 重要的两个结构体
2.1.1 cdev结构体
linux内核中,使用cdev(character dev)结构体描述一个字符设备,结构体如下:
-
dev_t:定义了设备号,为32位,12位为主设备号,20位为次设备号。
使用MAJOR(dev_t dev)、MINOR(dev_t dev)可以获得,使用MKDEV(int major, int minor)可以通过主设备号和次设备号生成dev_t:
2.1.2 file_operations 结构体
file_operations:定义了字符设备驱动提供给VFS的接口函数,是 字符设备驱动程序设计的主题内容,会在应用程序进行linus的open/read()等系统调用时最终被内核调用。
struct file_operations {
**struct module *owner;**
//指向module,一般初始化为THIS_MODULE
loff_t (*llseek) (struct file *, loff_t, int);
//修改一个文件的当前读写位置,并将新位置返回;指针参数filp为进行读取信息的目标文件结构体指针,
//loff_t用来维护当前读写位置,代表用户空间的文件光标移动数量值
//int 代表移动光标的参考位置,有3种(即当前光标、文件开头、文件结尾)
//移动设备的文件指针,然后读/写接口就可以对移动后的位置进行读取功能,而不是每次读/写都只能从0开始一次读取全部数据。llseek函数改变了filp->f_pos(position),返回新的位置
//实现的功能是修改驱动中write/read函数的文件指针。
**ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);**
// 从设备中读取数据,成功时返回读取字节数
//PS:这里是_ _user
**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);
// 提供设备相关控制命令的实现。
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
//
int (*mmap) (struct file *, struct vm_area_struct *);
// 将设备内存映射到进程的虚拟地址空间,对帧缓冲等设备特别有意义,
//帧缓冲被映射到用户空间后,应用程序可以直接访问无需在内核和应用间进行内存复制。
//帧缓冲是Linux为显示设备提供的一个接口,帧缓存的每一存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。
** int (*open) (struct inode *, struct file *);**
//打开设备驱动
//inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;
//filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, 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 (*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 **);
long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
2.2 linux字符设备驱动的组成
- 字符设备驱动模块加载与卸载函数
加载函数:设备号的申请和cdev的注册
卸载函数:设备号的释放和cdev的注销
image.png
memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。extern void *memset(void *buffer, int c, int count) buffer:为指针或是数组, c:是赋给buffer的值, count:是buffer的长度。
cdev_add 和cdev_del分别向系统添加和删除一个cdev,完成字符设备的注册和注销。
struct xxx_dev_t{
struct cdev cdev;
...
} xxx_dev;
static int _ _init xxx_init(void)
{
cdev_init(&xxx_dev.cdev, &xxx_fops);
xxx_dev.cdev.owner = THIS_MODULE;
ret = cdev_add(&xxx_dev.cdev,xxx_dev_no,1);
static void _ _exit xxx_exit(void)
{
unregister_chrdev_region{xxx_dev_no,1);
//释放设备号
cdev_del{&xxx_dev.cdev};
}
- 字符设备驱动的file_operations结构体的成员函数
file_operations中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对linux进行系统调用的最终落实者。以read()为例
ssize_t xxx_read(struct file *filp, char _ _ user *buf, size_t count, loff_t *f_pos)
{
...
copy_to_user(buff,...,...);
}
这里:完成内核空间和用户空间内存复制的是
copy_from_user() 、copy_to_user()
完全复制成功,返回值为0.
3、编写一个驱动
3.1 编写linux驱动程序的步骤
- 宏定义与头文件
- 注册和注销设备文件
- 指定与驱动相关的信息
- 指定回调函数
- 编写业务逻辑
- 编写Makefile文件
经过上面的6个步骤,可以安装和卸载linux驱动
3.2 编写word_count驱动
3.2.1 宏定义
#define DEVICE_NAME "wordcount"
#define DEVICE_COUNT 1
#define word_count_MAJOR 0
#define word_count_MINOR 234
#define TRUE -1
#define FALSE 0
static unsigned char mem[10000]
static int major = word_count_MAJOR;
static int minor = word_count_MAJOR;
static dev_t dev_number;
3.2.3 注册和注销设备文件
//
// 定义file_operations结构体
static struct file_operations dev_fops =
{.owner = THIS_MODULE, .read = word_count_read, .write = word_count_write};
// 定义cdev结构体,用于描述字符设备
static struct cdev word_count_cdev;
//
//
// 创建设备文件
static int word_count_create_device(void)
{
int ret = 0;
int err = 0;
//初始化cdev的成员,并建立cdev和file_operations之间的连接
cdev_init(&word_count_cdev,&dev_fops);
wordcount_cdev.owner = THIS_MODULE;
if (major >0)
{
// MKDEV:可通过主设备号和次设备号生成dev_t
dev_number = MKDEV(major,minor);
// #define DEVICE_NAME "wordcount"
// #define DEVICE_COUNT 1
// 注册字符设备区
err = register_chrdev_region(dev_number,DEVICE_COUNT,DEVICE_NAME);
if (err < 0)
{ printk(kern_warning " register_chrdev_region() failed\n");
retun err;
}
else
{
//随机分配设备号,并注册字符设备区
err = alloc_chrdev_region(&word_count_cdev.dev,10,DEVICE_COUNT,DEVICE_NAME);
if (err < 0)
{ printk(kern_warning " register_chrdev_region() failed\n");
retun err;
}
major = MAJOR(word_count_cdev.dev);
minor = MINOR(word_count_cdev.dev);
dev_number = word_count_cdev.dev;
}
//添加字符设备
ret = cdev_add(&word_count_cdev,dev_number,DEVICE_COUNT);
word_count_class = class_create(TIHIS_MODULE,DEVICE_NAME);
//创建设备文件
device_create(word_count_class,NULLndev_number,DEVICE_NAME);
return ret;
}
// 销毁字符设备
static void word_count_destory_decice(void)
{
// 销毁字符设备
device_destroy(word_count_class,dev_number);
// 销毁class 结构体
if (word_count_class)
class_destroy(word_count_class);
// 注销字符设备区
unregister_chrdev_region(dev_number, DEVICE_COUNT);
return;
}
// 初始化linux wordcount驱动
static int word_count_init(void)
{
int ret;
ret = word_count_create_device();
return ret;
}
// 卸载wordcount 驱动
static void word_count_exit(void)
{
word_count_destroy_device();
}
//指定word_count驱动的初始化和卸载
module_init(word_count_init);
module_exit(word_count_exit);
3.2.4 指定与驱动相关的信息
MODULE_AUTHOR("GUQUAN");
MODULE_DESCRIPTION("statistics of word count. ");
MODULE_ALIAS("word count module. ");
MODULE_LICENSE("GPL ");
3.2.5 编写业务逻辑
static char is_spacewhite(char c)
{
if(c == ' ' || c == 9 || c == 13 || c == 10)
return TRUE;
else
return FALSE;
}
//空格、回车、换行分割的字符串作为一个单词
static int get_word_count(const char *buf)
{
int n = 1;
int i = 0;
char c = ' ';
char flag = 0;// 处理多个空格情况:0:正常情况,1:已遇到一个空格
if(*buf == '\0')
return 0;
//第一个字符是空格,从0开始计数
if(is_spacewhite(*buf) == TRUE)
n--;
//扫描字符串中的每一个字符,假设:hello world\0
////空格、回车、换行分割的字符串作为一个单词
for(;(c = *(buf + i)) != '\0'; i++)
{
if(flag == 1 && is_spacewhite(c) == FALSE)
flag = 0;
//多个空格时,忽略
else if(flag == 1 && is_spacewhite(c) == TRUE)
continue;
if(is_spacewhite(c) == TRUE)
{
n++;
flag = 1;
}
}
//如果字符串以一个或多个空格结尾,不计数
if(is_spacewhite(*(buf + i - 1)) == TRUE)
n--;
return n;
}
//从设备文件读取数据
static ssize_t word_count_read(struct file *file, char *buf, size_t count, loff_t *ppos)
{
unsigned char temp[4];
// 将单词数(int类型)分解成4个字节存储在buf中
temp[0] =word_count >> 24;
temp[1] =word_count >> 16;
temp[2] =word_count >> 8;
temp[3] =word_count;
copy_to_user(buf,(void*) temp,4);
printk(" read:word count:%d",(int) count);
return count;
}
//向设备文件写入数据
static ssize_t word_count_write(struct file *file, const char *buf, size_t count, loff_t *ppos)
{
ssize_t written = count;
copy_from_user(mem,buf,count);
mem[count] = '\0';
word_count = get_word_count(mem);
printk("writen:word count :%d",(int)word_count);
return written;
}
3.2.6 编写Makefile文件
obj-m += word_count.o
3.2.7 安装和卸载word_count驱动
安装:insmod word_count.ko
卸载:rmmod word_count
3.2.8 test_main
7 int main(int argc,char *argv[])
8 {
9 int testdev;
10 unsigned char buf[4];
11 testdev = open("/dev/wordcount",O_RDWR);
12
13 if(testdev == -1)
14 {
15 printf("cann't open file,because cann't open /dev \n");
16 return 0;
17 }
18 if(argc >1)
19 {
20 write(testdev,argv[1],strlen(argv[1]));
21 printf("string:%s\n", argv[1]);
22 }
23
24 read(testdev,buf,4);
25
26 int n = 0;
27 n = ((int) buf[0]) << 24 | ((int) buf[1]) << 16 | ((int) buf[2]) << 8 | ((int) buf[3]);
28 printf("word byte display :%d,%d,%d,%d\n", buf[0],buf[1],buf[2],buf[3]);
29
30 printf("word count : %d\n",n);
31 close(testdev);
32 return 0;
33 }
4、遇到的问题
- 挂载:insmod
提示:operation not permitted
解决使用sudo insmod word_count.ko - ./test_word_count时,提示cann't open file,然后使用命令查看权限:ll /dev
可以看到如下信息:
crw --- --- 1 root root 10, 58 8月 13 16:33 wordcount
image.png
文件类型
linux一共由7种文件类型,分别如下:
-:普通文件
d:目录文件
l:软链接(类型Windows的快捷方式)
b:块设备文件(硬盘、光驱)
p:管道文件
c:字符设备文件
s:套接口文件/数据接口文件(例如启动Mysql服务器时产生的mysql.sock文件)
所有者(u表示)权限,组群(group),其他人(other)
文件权限:
| r(read) | 4 | 可读 |
| w(write) | 2 | 可写 |
| x(execute) | 1 | 可执行 |
sudo chmod 777 /dev/wordcount,然后查看,crwxrwxrwx
然后执行 ./test_word_count “hello world",可以得到如下结果