看了半个月了,但感觉自己这样毫无目标的看效率太低,网上看到大佬十年前就看英文原版书籍了,同时在做读书笔记,博客:(自由泳的青蛙)实在惭愧。我还想着看懂了就看,看不懂就跳过去呢,不行,得定有输出才行。
看了前辈的文章,每篇文章不长,但都有自己的思考,总结,我也不想直接抄书,那样毫无效果,定个目标:每天我看完一些内容,如果这部分内容有代码实现效果的就先列出代码看效果,无法看代码效果的,如果有有价值的代码如源码,结构体,并介绍,如果不懂的就去百度,如果还不懂先放个链接,从今天开始,重新阅读总结第三章《字符设备驱动程序》,之前的简略笔记如下:
《第二章 构造和运行模块》
编写代码多用kalloc,为什么,因为栈空间很小。
1. insmod工具与modprobe工具的区别:modprobe会把与当前module引用到的module也安装上(就是层叠驱动),用于安装标准moduel.相当于调用了多次insmod.
2. 学习编写驱动代码时为什么#include <linux/module.h> (包含需要的大量符号和定义)和 #include <linux/init.h>是必须的。(37)
3. MODULE_LICENSE(“GPL”) 内核能识别通用公用许可证。
4. 内核API中 __ 两个下划线起始的函数标明是底层组件,谨慎使用,后果自负。
5. __init 与 __exit对初始化函数和清除函数的作用:告诉内核运行完就可以释放函数释放的内存了,不是必须但建议使用。
6. EXPORT_SYMBOL 声明的函数或变量就可以被其他模块调用了,可以通过 grep EXPORT_SYMBOL 了解一个驱动程序提供了哪些入口点。
7. 内核代码为什么一般都有返回值需要检查? 因为大部分函数都需要kmalloc申请内存,如果没申请到指针可能指向了一段错误地址,内核将处于不稳定状态。
初始化过程错误处理:
1. 使用goto很方便,其他时候虽然goto尽量少用。(模块初始化遇到错误时,必须自行清除已经注册的设备,否则内核处于不稳定状态)
2. insmod和modprobe可以在装载模块时候为模块输入参数,有例程。
模块参数:
1. 模块在通过insmod输入时可以同时手动传入参数。(实验手动指定打印多少次指定字符串)
快速参考:
将本章涉及到的函数接口以及标准目录等汇总,介绍。
《第三章 字符设备驱动程序》
scull的设计
1. 对scull实现的设备类型做了讲解,不太懂???
主次设备号
1. 如何访问字符设备:通过访问“特殊文件/设备文件”来访问字符设备,通常位于/dev目录,ls -l命令中输出的第一列中的'c'来识别(块设备为b)出这是个字符设备。
2. ls -l 如何看字符设备的主次设备号
3. 次设备号用于获得指向内核设备的直接指针
设备编号的内部表达
1. dev_t 配合两个宏用来存取设备号,这个宏有32位,12位存储主设备好,20位存储次设备号。
分配和释放设备编号
1. int register_chrdev_region(dev_t first, unsigned int count, char *name); 与 int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); 这俩是分配设备编号的接口,之所以有"动态分配设备编号"接口是因为我们往往不知道设备将要使用哪些主设备号。
动态分配主设备号
讲解为什么要使用动态分配主设备号好。及代码。
2重要数据结构-文件操作
重要数据结构-file结构
1. `<linux/fs.h>`中定义的`struct file`与用户空间常用的在C库定义的`FILE`没有任何关系。`struct file`结构代表一个打开的文件,内核在`open`时创建,在`close`时关闭。
2重要数据结构-inode
结构
1. 内核用inode表示文件,对单个文件,可能有多个表示打开文件描述符的file结构,但他们都指向单个inode结构。
2. 其中 dev_t i_rdev 表示设备文件的inode结构体,包含真正的设备编号。
3. struct cdev *i_cdev;struct cdev是表示字符设备的内核的内部结构,当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
3字符设备的注册-新方法
1. 已知struct cdev表示字符设备,故需要先申请,初始化,通知内核,移除。(涉及内核接口函数cdev_alloc,cdev_init, cdev_add,cdev_del);
3字符设备的注册-老方法(不用了快)
1. register_chardev 与 unregister_chardev
4open和release
讲了scull设备的open函数,也有代码,但看完了不明白,很多概念不懂。
本章目标
编写一个模块化的字符设备驱动程序。取自scull(Simple Character Utility for Loading Localities), 我们将在计算机内存之上实现若干设备的抽象,让scull为编写真正的驱动程序提供一个样板。
基础知识
/proc/devices: 列出当前内核已经注册的所有字符设备和块设备的主设备号以及对应的设备名称。提供了内核对设备驱动程序的快照。作用是显示内核已经识别的设备类型。告诉你哪些类型的主设备号是可用的,以及这些设备号对应的设备类型。
/dev:
设备文件,也经常叫设备节点。代表系统中实际的设备实例,可以是字符设备或块设备。
在/proc/devices中列出的设备类型并不意味着在/dev/目录下一定存在对应的设备文件。例如,一个设备类型可能存在,但由于没有对应的硬件接入或驱动程序没有加载,因此没有在/dev/目录下创建相应的设备文件。
大佬博客:https://www.cnblogs.com/johnnyflute/p/3969774.html
1 主设备号与次设备号
linux的设备管理与文件系统关系密切,通过访问文件系统内的设备文件来完成对字符设备的访问,设备文件通常位于/dev目录。
- ls -l 命令执行后列出第一列如果有c则是字符设备
- 在/dev目录执行ls -l 后,列出的时间之前的两列分别是主设备号和次设备号
- 主设备号表示哪一类设备
- 次设备号区分同一类设备中的不同设备,由内核使用
- 对于常用设备,linux有约定俗称的规定,如硬盘的主设备号是3
1.1 设备号的内部表达
内核中用dev_t
类型来保存设备号。dev_t在2.6.0版本内核中用12位来表示主设备号其余位表示次设备号,但其他版本可不一定,我们得用<linux/kdev_t.h>
中的宏。
MAJOR(dev_t dev);//获得主设备号
MINOR(dev_t dev);//获得次设备号
MKDEV(int major, int minor);//将主设备与次设备转化成dev_t类型
1.2 释放和分配设备编号()
设备号是内核为设备分配的唯一标识符。建立字符设备号第一步:给目标设备分个设备号。当然结束时还要释放设备号:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
dev_t first: 自己需要自己知道主设备号,次设备号一般为0
unsigned int count: 请求的连续的设备号的个数
char *name: 该编号范围关联的设备名称,将出现在/proc/devices和sysfs中
return: 成功返回0,否则返回负值
int alloc_chrdev_region(dev_t *dev, unsigned int firstninor, unsigned int count, char *name);
dev_t *dev: 输出参数,返回分配范围第一个编号
unsigned int firstninor: 请求的第一个次设备号,通常为0
return: 成功返回0,否则返回负值
void unregister_chrdev_region(dev_t first, unsigned int count);
上面这些函数只是向内核申请了设备编号,但没有告诉内核拿这些设备号干嘛,想知道干嘛还得继续看。
1.3动态分配主设备号
这节主要讲解要使用动态分配的方法,即用alloc_chrdev_region
而非register_chrdev_region
。 若该驱动module被其他人广泛使用,那么无法保证通过后者注册的设备号是其他人的Linux系统中未分配使用的设备号。
2 一些重要数据结构
2.1 文件操作 file_operations(linux/fs.h)
- 指向这类结构的指针被称为
fops
- 许多包含
__user
字符串的参数指示提醒该参数为用户态指针,无其他作用
struct file_operations {
struct module *owner;
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 *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
} __randomize_layout;
2.2 file结构(linux/fs.h)
- file仅仅出现在内核代码中
- file结构与用户空间FILE无关
- file结构表示一个打开的文件,系统中每个打开的文件在内核中都有一个对应的file结构(由内核在open时创建,在close时释放)
- 指向
struct file
的指针被称为filp
一些重要结构如下:
struct file {
fmode_t f_mode; //标记访问权限
loff_t f_pos; //当前读写位置
unsigned int f_flags; //检查用户是否是非阻塞操作
struct inode *f_inode; /* cached value */
const struct file_operations *f_op; //这个眼熟,open时赋值
/* 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 hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
2.3 inode结构(linux/fs.h)
- 对单个文件可能由许多表示打开文件描述符的file结构,但他们都指向同一个inode结构。
- 从
inode
中获得主次设备号应使用下面两个宏,不要直接操作inode的i_rdev指针。 - 其中的
i_cdev
指针指向字符设备的内核的内部结构struct cdev
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
一些重要结构罗列如下(这个结构体老长了):
struct inode {
dev_t i_rdev;//设备文件的inode结构,包含了真正的设备编号
union {
struct pipe_inode_info *i_pipe;
struct cdev *i_cdev; //字符设备的内核内部结构
char *i_link;
unsigned i_dir_seq;
};
} __randomize_layout;
3 字符设备的注册
- 刚说了内核内核使用
'struct cdev'
表示字符设备,所以需要分配一个这样的结构。 <linux/cdev.h>
中包含了与cdev结构分配相关的辅助函数,cdev结构体如下
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
分配和初始化上述结构体的方法有两种,第一种:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = my_fops;
/* 然后需要用cdev_init初始化 */
void cdev_init(strue cdev *cdev, struct file_operations *fops);
/* 然后初始化struct cdev的owner字段 */
my_cdev->owner = THIS_MODULE;
/* 然后告诉内核该结构的信息 */
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
/* 从系统中删除一个字符设备 */
void cdev_del(struct cdev *dev);
第二种:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
int unregister_chrdev(unsigned int major, const char *name);
//major是主设备号的名字
//name是驱动程序的名字,将出现在/proc/devices中
4 open 和 release
4.1 open 方法
int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */
/* now trim to 0 the length of the device if open was write only */
if ( (filp->flags & O_ACCMODE) == O_WRONLY) {
scull_trim(dev); /* ignore errors */
}
return 0;
}
4.2 release 方法
- 释放由open分配的,保存在filp->private_data中的所有内容
- 在最后一次关闭操作时关闭设备
int scull_release(struct inode *inode, struct file *filp){
return 0;
}
5 scull 的内存使用
5.2 read 和 write
read 和 write可以看file_operations中的成员,确定其格式如下:
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
这两个函数类似,
filp
是文件指针,该结构代表了打开的文件。它包含了文件的状态信息,比如文件的读写位置、文件操作等。buff
是指向用户空间的缓冲区count
是请求传输的数据长度offp
是一个指向loff_t 类型的指针,loff_t 是一个可以表示文件中任意位置的偏移量。
这俩函数需要先检查用户空间指针是否有效。
这两个函数完成内核态与用户态数据的交换,实现他们的时候需要调用下面这俩函数,其实是下面这俩函数帮read和write完成的:
unsigned long copy_to_user(void __usr to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const void __usr *from, unsigned long count);
书中说read和write需要返回正确传输的字节数,如果一开始就传输错误,则返回值来指出错误类型,但当传输过程中出错,需要保存住错误,在下一次才返回错误类型。然后书中说书中说用户态的read当出错时只会接收到-1作为返回值,为找到出错原因需要访问errno变量。时候我就man 2 read
看read系统调用接口,发现果然,man手册已经说的很清楚了呀。
5.2.1 read 方法
本小节介绍read在driver中如何实现,需要考虑哪些情况
- 如果read的返回值等于count参数标明成功
- 如果返回值小于count怎么办
- 如果返回值为0怎么办
- 阻塞了怎么办
下面是参考代码:
6 源码
scull.c 文件代码
#include <linux/init.h>
#include <linux/types.h> //dev_t
#include <linux/kdev_t.h> //MAJOR, MINOR
#include <linux/version.h>
#include <linux/module.h>
#include <linux/fs.h> //register_chrdev_region .etc
#include <linux/kernel.h> //printk
#include <linux/cdev.h>
#include <linux/slab.h> //kmalloc kfree
#include <asm/uaccess.h>
#include "scull.h"
static int scull_major = SCULL_MAJOR;
static int scull_minor = SCULL_MINOR;
static int scull_nr_devs = SCULL_NR_DEVS;
static int scull_quantum = SCULL_QUANTUM;
static int scull_qset = SCULL_QSET;
module_param(scull_major, int, S_IRUGO);
module_param(scull_nr_devs, int, S_IRUGO);
module_param(scull_quantum, int, S_IRUGO);
module_param(scull_qset, int, S_IRUGO);
static dev_t dev_t_first; //设备号,主设备号与次设备号合体
static struct scull_dev *scull_dev_all;
int scull_trim(struct scull_dev *dev)
{
struct scull_qset *next, *dptr;
int qset = dev->qset; /*dev 非空 */
int i;
for (dptr = dev->data; dptr; dptr = next) {
if (dptr->data) {
for (i = 0; i < qset; i++)
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->size = 0;
dev->quantum = SCULL_QUANTUM; /* 该值可以用宏定义,也可也在运行时用ioctl修改 */
dev->qset = SCULL_QSET; /* 该值可以用宏定义,也可也在运行时用ioctl修改 */
dev->data = NULL;
return 0;
}
int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */
/* now trim to 0 the length of the device if open was write only */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
scull_trim(dev); /* ignore errors */
}
return 0;
}
int scull_release(struct inode *inode, struct file *filp){
return 0;
}
struct scull_qset *scull_follow(struct scull_dev *dev, int item)
{
struct scull_qset *next, *dptr;
int i = 0;
for (dptr = dev->data, i = 0; dptr; i++) {
if (dptr->data) {
if (item == i)
return dptr;
} else
break;
dptr = dptr->next;
}
return NULL;
}
ssize_t scull_read(struct file *filp, char __user *buff, size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr; /* 第一个链表项 */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* 该链表项有多少个字节 */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;
if (down_interruptible(&dev->sem)) //第四章介绍,可被中断地获取信号量,如果睡眠被信号中断,返回错误-EINTR
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - *f_pos;
/* 在量子级中寻找链表项, qset索引以及偏移量 */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;
/* 沿着该链表前行,直到正确的位置(在 其他地方定义)*/
dptr = scull_follow(dev, item);
if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */
/* 读取该量子的数据直到结尾 */
if (count > quantum - q_pos)
count = quantum - q_pos;
if (copy_to_user(buff, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
out:
up(&dev->sem);// 释放信号量sem
return retval;
return count;
}
ssize_t scull_write(struct file *filp, const char __user *buff, size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; /* goto out 语句使用的值 */
if (down_interruptible(&dev->sem)) // 可被中断的获取信号量,如果睡眠被信号中断,返回错误-EINTR
return -ERESTARTSYS;
/* 在量子集中寻找链表项,qset 索引以及偏移量 */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;
/* 沿着该链表前行直到找到正确的位置 */
dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
/* 将数据写入该量子直到结尾 */
if (count > quantum - q_pos)
count = quantum - q_pos;
if (copy_from_user(dptr->data[s_pos] + q_pos, buff, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
up(&dev->sem); // 释放信号量sem
return retval;
}
struct file_operations scull_fops = {
.owner = THIS_MODULE,
// .llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
// .ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
/*
* scull charter device registration
*/
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
int err, devno = MKDEV(scull_major, scull_minor + index);//将主设备与次设备转化成 dev_t 类型保存设备号
cdev_init(&dev->cdev, &scull_fops); //初始化cdev的成员,并建立cdev和file_operations之间的连接
//dev->cdev.ops = &scull_fops; //cdev_init 完成了这行代码
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1); //向 kernel 注册struct cedv 结构,参数2为设备号, 参数3为和该设备关联的设备号的数量
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
/*
* Assigned dev number.
*/
static int scull_register_chrdev(void)
{
int result;
if (scull_major) {
dev_t_first = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev_t_first, scull_nr_devs, "scull");
} else {
//"cat /proc/devices"查看申请到的设备名,设备号
result = alloc_chrdev_region(&dev_t_first, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev_t_first); //获取alloc_chrdev_region得到的主设备号
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
return 0;
}
/* 释放所有量子和量子集 */
static void __exit scull_exit(void)
{
int i;
if (scull_dev_all) {
for (i = 0; i < scull_nr_devs; i++) {
scull_trim(&scull_dev_all[i]);
cdev_del(&scull_dev_all[i].cdev); //向 kernel 注销一个struct cdev结构
}
kfree(scull_dev_all);
}
//释放从dev_t_first中指定的主设备号以及第一个参数次设备号开始的指定数量的次设备号
unregister_chrdev_region(dev_t_first, scull_nr_devs);
}
static int __init scull_init(void)
{
int result;
int i;
result = scull_register_chrdev(); //assigned dev number
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
printk(KERN_NOTICE "%s : get device major number %d\n", __func__, scull_major);
scull_dev_all = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_dev_all) {
result = -ENOMEM;
goto fail;
}
for (i = 0; i < scull_nr_devs; i++) {
scull_dev_all[i].data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
memset(scull_dev_all[i].data, 0, sizeof(struct scull_qset));
scull_dev_all[i].qset = scull_qset;
scull_dev_all[i].quantum = scull_quantum;
scull_dev_all[i].size = 0;
scull_dev_all[i].access_key = 0;
sema_init(&scull_dev_all[i].sem, i); //初始化信号量,将信号量计数器值设置val
scull_setup_cdev(&scull_dev_all[i], i); //scull charter device registration
}
fail:
if (result)
scull_exit();
return result;
}
module_init(scull_init);
module_exit(scull_exit);
MODULE_LICENSE("GPL");
MODULE_VERSION("v1.0");
MODULE_AUTHOR("Author Name");
MODULE_DESCRIPTION("A Character Device driver for test!");
scull.h 文件代码
#ifndef __SCULL_H__
#define __SCULL_H__
#include <linux/cdev.h>
#define SCULL_MAJOR 0
#define SCULL_MINOR 0
#define SCULL_NR_DEVS 4
#define SCULL_QUANTUM 4000
#define SCULL_QSET 100
struct scull_qset {
void **data;// 指针数组 保存量子集
struct scull_qset *next;//指向下一个量子集
};
struct scull_dev {
struct scull_qset *data; /* 指向第一个量子集的指针 */
int qset; /* 当前数组的大小, 一个量子集中的量子个数 */
int quantum; /* 一个量子的大小,我们定义为4000字节 */
unsigned long size; /* 保存在其中的数据总量 */
unsigned int access_key; /* 由sculluid & scullpriv 使用*/
struct semaphore sem; /* 互斥信号量 */
struct cdev cdev; /* 字符设备结构 */
};
#endif
手动创建设备节点的脚本:
#!/bin/sh
module='scull'
device='scull'
mode='664'
#使用传入该脚本的i所有参数调用insmod,同时使用路径名来指定模块位置
/sbin/insmod ./$module.ko $* || exit 1
# 删除原有节点
rm -f /dev/${device}[0-3]
major=$(awk "\$2==\"$module\"{print \$1}" /proc/devices)
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3