Day1
- 驱动
- 模块
- 模块传参
- 符号导出
程序的入口:
- 应用程序:main
- 内核:异常向量表
- 中断
1. 驱动
定义:驱使硬件动起来的程序
种类:
- 裸机驱动:需求分析–>查原理图–>查芯片手册–>code
- 系统驱动:需求分析–>查原理图–>查芯片手册–>设备树–>code -->安装到内核中
裸机开发&系统开发的优缺点?
- 裸机开发:成本低 运行效率高 安全性低 单任务
- 系统开发:成本高 运行效率低 安全性低 多任务
应用程序和驱动程序的区别?
区别 | 应用程序 | 驱动程序 |
---|---|---|
加载方式 | 主动加载 | 被动加载 |
运行空间 | 用户空间 | kernel空间 |
执行权限 | 低 | 高 |
影响力 | 局部 | 全局 |
函数来源 | 自定义/库/系统调用 | 内核函数/自定义 |
2. 模块–>驱动模块
模块:能够单独命名并且独立完成一定功能的程序语句的集合(程序代码和数据结构)
驱动模块:能够单独命名并且独立完成特定外设功能驱动的程序语句的集合
注:一个驱动模块就是一个完整的外设驱动程序,驱动程序被安装到操作系统内核中,当该驱动程序对应的外设要工作时,该驱动模块被调用。
2.1 如何写一个驱动模块?
- 模块初始化函数 int 函数名1(void)
- 模块清除函数 void 函数名2(void)
- 模块加载函数 module_init(函数名1) --> sudo insmod hello.ko
- 模块卸载函数 module_exit(函数名2) --> sudo rmmod hello.ko
- 声明该驱动遵守GPL–>MOUDULE_LICENSE("GPL”)
include </linux/init.h>
#define module_init(initfn)
static inline initcall_t __inittest(void)
{ return initfn; }
#define module_exit(exitfn)
static inline exitcall_t __exittest(void)
{ return exitfn; }
2.2 如何编译驱动模块
hello.c-->hello.ko
test.c-->a.out
需要实现一个Makefile:
Makefile:
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
//进入/lib/modules/3.5.0-23-generic/build下执行Makefile,
将PWD路径下的代码编译成一个hello.o
else
obj-m := hello.o //将hello.o链接成hello.ko
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
Module* modules*
2.3 将驱动模块安装到Linux内核中
sudo insmod hello.ko-->将驱动模块hello.ko安装到linux内核中
lsmod-->查看当前系统中所有已加载的驱动模块
dmesg |tail-->查看内核缓存区后10行打印信息
dmesg |tail -20-->查看内核缓存区后20行打印信息
modinfo hello.ko-->查看模块信息描述
sudo rmmod hello.ko-->将hello.ko从内核中移除
2.4 将驱动代码分成两个文件
hello.c-->hello_init.c&hello_exit.c-->hello_init.o&hello_exit.o-->hello.ko
需要实现一个Makefile:
Makefile:
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
//进入/lib/modules/3.5.0-23-generic/build下执行Makefile,
将PWD路径下的代码编译成一个hello.o
else
obj-m := hello.o //将hello.o链接成hello.ko
hello-objs=hello_init.o hello_exit.o//将两个.o文件连接成一个hello.o文件
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
Module* modules*
3. 模块传参
应用程序:
传参:./a.out 1.txt 2.txt
接收传参:int main(int argc,char *argv[])
驱动程序:
传参:安装驱动模块时传 sudo insmod hello.ko gtest=100
接收传参:
1. 用全局变量 int gtest;
2. 声明该全局变量接收传参
如何声明一个全局变量可以接收shell终端的参数传递?用内核函数声明
module_param(name, type, perm)
name:参数名,既是内部参数名,又是外部参数名
type:参数的数据类型
perm:访问权限,0644。0表示该参数在文件系统中不可见
module_param_string(name, string, len, perm)
name:外部参数名
string:内部参数
len:数组的大小
perm:访问权限,0644。0表示该参数在文件系统中不可见
module_param_array(name, type, nump, perm)
name:数组参数名,既是内部参数名,又是外部参数名
type:参数的数据类型(数组成员的数据类型)
nump:用来存放终端传给数组的实际元素个数
perm:访问权限,0644。0表示该参数在文件系统中不可见
测试步骤:
1 sudo insmod hello.ko gtest=100
2 dmesg |tail-->查看gtest的值是否是100?
3 cd /sys/module/hello/paramters
ls -l--->gtest
cat gtest-->100?
sudo chmod 777 gtest
echo 200 > gtest
cat gtest-->200?
4 sudo rmmod hello.ko
5 dmesg |tail-->gtest=200?
4. 符号导出
-
什么是符号?在内核和驱动中主要是指全局变量和函数
-
为什么要导出符号?
linux内核是以模块化形式管理内核代码的。内核中的每个模块之间是相互独立的,也就是说A模块中的全局变量和函数,B模块是无法访问的,若B模块想要使用A模块已有的符号,那么必须将A模块中的符号做导出,导出到模块符号表中,然后B模块才能使用符号表里的符号。 -
如何做符号导出?
linux内核给我们提供了两个宏:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
EXPORT_SYMBOL_GPL(gtest);
EXPORT_SYMBOL_GPL(func);
例如:模块A–>符号导出的模块,hello.c–>EXPORT_SYMBOL_GPL(gtest);EXPORT_SYMBOL_GPL(func); make成功后会生成一个存放符号的本地符号表,本地符号表中存放的就是代码里导出的符号Module.symvers:
Addr | 符号名 | 模块名 | 导出符号的宏 |
---|---|---|---|
0x8eaf8fe3 | gtest | /home/farsight/2022/22101/driver/day1/module_symbol/hello | EXPORT_SYMBOL_GPL |
0xd1a68ac8 | Func | /home/farsight/2022/22091/driver/day1/module_symbol/hello | EXPORT_SYMBOL_GPL |
模块B–>使用导出符号的模块
使用条件:
- 将A模块导出的符号表拷贝到B模块中
- 在B模块的代码里外部声明使用哪个符号,然后才能使用
测试步骤:
1. sudo insmod hello.ko–>符号导出模块
2. sudo insmod world.ko–》使用符号的模块
3. dmesg |tail -->
4. lsmod
5. sudo rmmod world.ko
6. sudo rmmod hello.ko
linux内核提供了两类符号表:
- 用户自定义模块导出的符号表,称为本地符号表Module.symvers
- 内核全局符号表 /proc/kallsyms
sudo cat /proc/kallsyms |grep printk
c15cd833 T printk
函数指针=0xc15cd833;
int printf(const char *format,…)
Day2
1. 字符设备驱动
1.1 字符设备
定义:只能以一个字节一个字节的方式读写设备,不能随机的读取设备中某一段数据,读取数据需要按照先后顺序。字符设备面向字节流的
常见的字符设备:鼠标,键盘,串口,控制台
块设备:可以从设备的任意位置读取一定长度数据的设备
常见的块设备:硬盘,磁盘,光盘,U盘,SD卡,TF卡。
1.2 字符设备驱动框架
init流程:–>HelloModule
- 申请设备号(静态申请,动态申请)
- 创建一个字符设备
- 初始化字符设备
- 将字符号和设备关联起来
exit流程:–>HelloExit
- 删除字符设备
- 删除设备号
1.2.1 设备号
定义:设备在内核中的身份和标识,是内核区分不同设备的唯一信息
构成:设备号是由主设备号和次设备号构成,主设备号表示一类设备,次设备号标识该类设备中的某一个设备
设备号:是有一个32bit位的无符号整形,高12bit位是主设备号,次20bit是次设备号
内核函数和库函数的区别:库函数是语言本身的一部分,而系统函数是内核提供给应用程序的接口,属于系统的一部分。
<linux/kdev_t.h>
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
1.2.2申请设备号
申请设备号有两种方式:静态申请 动态申请
静态申请设备号:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
作用:静态申请设备号
from:设备号(由主次设备号构成)
count:子设备个数
*name:设备名称
返回值:0 成功 非0 失败
void unregister_chrdev_region(dev_t from, unsigned count)
作用:从内核中移除设备号
from:设备号(由主次设备号构成)
count:子设备个数
动态申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
作用:动态申请设备号
*dev:指向设备号的指针
baseminor:子设备的第一个编号
count:子设备个数
*name:设备名称
返回值:0 成功 非0 失败
1.2.3创建字符设备
struct cdev *cdev_alloc(void)
作用:创建一块用于存放字符设备的空间
返回值:是指向创建成功的字符设备的指针
在Linux内核中用struct cdev来描述一个字符设备
struct cdev
{
struct kobject kobj;-->内嵌的内核对象
struct module *owner;-->该字符设备所在的内核模块的对象指针
const struct file_operations *ops;-->指向操作字符设备的方法集
struct list_head list;-->用来将已向内核注册的所有字符设备形成链表
dev_t dev;-->设备号(由主设备号和次设备号构成)
unsigned int count;-->隶属于同一个主设备号的次设备个数
};
void cdev_del(struct cdev *p)
作用:删除字符设备
*p:指向字符设备的指针
1.2.4初始化字符设备–>绑定驱动方法
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
作用:初始化字符设备
*cdev:指向字符设备的指针
*fops:指向操作字符设备的函数集的指针
1.2.5将字符设备和设备号关联
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
作用:将字符设备和设备号关联,并将字符设备添加到内核中
*p:指向字符设备的指针
dev:设备号
count:子设备的个数
返回值:成功为0 失败非0
测试步骤:
- sudo insmod hello.ko
- dmesg |tail -->250 0
- cat /proc/devices–>查看设备号 250 0
- sudo mknod /dev/haha0 c 250 0
- ls -l /dev/haha*—>查看创建字符设备文件
- sudo ./test–>open hahao ok!
- dmesg |tail–>helloopen/helloClose
- sudo rmmod hello.ko
- sudo rm /dev/haha0
区分字符设备驱动框架中使用的三个结构体:
- struct file:代表内核中一个打开的文件。系统中每个打开的文件在内核中都有一个关联的struct file。
- struct inode:用来记录文件在物理上的信息。它和打开文件struct file结构不同,一个文件可以对应多个struct file,但是只有struct inode.
- struct file_operations:是内核给用户提供的驱动接口函数集,用户可以定义函数集中的任何驱动方法。(对于不支持的一般不写)
字符设备文件–>250 0<–字符设备(helloopen/helloclose)
mice----------->鼠标
sudo cat mice
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 *);
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 (*iterate) (struct file *, struct dir_context *);
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 *);
int (*open) (struct inode *, struct 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 实现用户空间和内核空间的数据拷贝
用户代码对字符设备的任何操作,最终都要落实到设备对应的底层操作函数上
内核空间–>用户空间 read–>HelloRead
用户空间–>内核空间 write–>HelloWrite
应用层 :fd=open(“/dev/haha0”) read(fd,) close(fd)
驱动层:增加HelloRead
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
char g_buf[]="hellotest----";
ssize_t HelloRead(struct file *pFile,char __user *buf,size_t count,loff_t *p)
{
copy_to_user(buf,g_buf,count);
}
参数:
- *pFile:为进行读取信息的目标文件
- __user:为对应放置信息的缓冲区(即用户空间内存地址);
- count:为要读取的信息长度;
- p:为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值;
函数原型ssize_t (*write) (struct file * filp, const char __user *buffer, size_t count, loff_t *ppos);
参数含义:
- filp :为目标文件结构体指针;
- buffer :为要写入文件的信息缓冲区;
- count :为要写入信息的长度;
- Ppos :为当前的偏移位置,这个值通常是用来判断写文件是否越界
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
作用:
从内核空间向用户空间拷贝数据
*to:用户空间指针
*from:内核空间指针(数据源)
n:拷贝的字节数
copy_from_user
测试步骤:
1. sudo insmod hello.ko
2. dmesg |tail
3. sudo mknod /dev/haha0 c 250 0
4. ls -l /dev/hah*
5. sudo ./test–>查看是否读到数据?
6. sudo rmmod hello
7. sudo rm /dev/haha0
day2作业
1 用sourceInsight创建linux内核代码工程
2 实现HelloWrite并测试
Day 3
1. 自动创建设备文件
创建设备文件的方式:
- 手动创建 sudo mknod /dev/doglc c 250 0
- 自动创建–>使用内核函数
linux内核为我们提供了一组内核函数,用于在模块加载时自动在 /dev 目录下创建响应的设备文件,并在卸载时删除该设备文件
创建
class_create //创建设备文件类
struct class *class_create(owner, name)
owner:标识模块本身 THIS_MODULE
name:设备文件名
返回值:指向设备文件类的指针(struct class标识设备文件类)
device_create // 创建设备文件
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
*class:指向设备文件类的指针
*parent:指向父设备的指针,一般为NULL
devt:设备号有主次设备号构成
*drvdata:指向设备私有数据,若无给NULL
*fmt:设备文件名
…可变参数
返回值:成功 struct device * 失败err 需要判断
删除:
class_destroy // 删除设备文件类
/**
* class_destroy - destroys a struct class structure
* @cls: pointer to the struct class that is to be destroyed
*
* Note, the pointer to be destroyed must have been created with a call
* to class_create().
*/
void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;
class_unregister(cls);
}
作用:删除设备文件类
*cls:指向设备文件类的指针
device_destroy // 删除设备文件
/**
* device_destroy - removes a device that was created with device_create()
* @class: pointer to the struct class that this device was registered with
* @devt: the dev_t of the device that was previously registered
*
* This call unregisters and cleans up a device that was created with a
* call to device_create().
*/
void device_destroy(struct class *class, dev_t devt)
{
struct device *dev;
dev = class_find_device(class, NULL, &devt, __match_devt);
if (dev) {
put_device(dev);
device_unregister(dev);
}
}
作用:删除设备文件
*class:指向设备文件类的指针
devt:设备号
测试步骤:
- sudo insmod hello.ko
- dmesg | tail
- ls -l /dev/haha* —> 查看是否生成了设备文件
- sudo ./test
- sudo ./test1
- sudo rmmod hello.ko
- ls -l /dev/haha* —>查看设备文件是否被删除
设备:
- 主设备—>一类设备
- 次设备—>该类设备中的某一个设备
设备文件:一个设备文件对应一个次设备
2. ioctl
ioctl—>系统调用函数—>应用程序用它给设备&内核发送控制指令
Linux内核给用户提供了两类系统调用函数:
- 数据操作函数,如read,write
- 非数据操作函数,如ioctl,内核间将对设备操作交给了ioctl接口(换句话说,应用程序可以使用ioctl接口去控制底层设备)
int ioctl(int d,int request,...)
d:文件描述符
request:指令码
…:可变参数,若有那个该参数是传给内核驱动的函数
返回值:0成功,非0失败
命名码:
- 可以自定义 1 0
- 使用标准命名码 int—>4部分
- 30-31:数据的控制方向
- 16–19:数据大小(14bit)
- 8-15:设备类型(8bit)
- 0-7:cmd---->命令码(区分命令的顺序序号,底八位)
- 如何生成标准命名码?用内核提供的宏
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
common.h
#define TEST_CMD 0x1
#define TEST_CMD1 _IO('h',1)
#define TEST_CMD2 _IOW('h',2,int)
应用:test.c
fd = open("/dev/haha0",);
ioctl(fd,TEST_CMD);
ioctl(fd,TEST_CMD1);
ioctl(fd,TEST_CMD2,12345);
驱动:HelloIoctl
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
{
switch(cmd)
{
case TEST_CMD:
{
printk();
break;
};
case TEST_CMD1:
{
printk();
break;
};
case TEST_CMD2:
{
printk();
break;
};
}
}
测试:
- sudo insmod hell.ko
- ls -l /dev/haha*
- sudo ./test
- dmesg |tail ----->查看ioctl发的指令是否被解析
- sudo rmmod hello.ko
3. 驱动的互斥
设备号:主设备 + 次设备
- 主设备:代表一类设备
- 次设备:代表一类设备的某一个设备—>使用驱动程序的终端个体
- 多个子设备公用一个驱动程序,若多个子设备同时访问一个驱动程序时,就产生了竞态访问
- 如何解决驱动的竞态呢?
- Linux内核提供了多种互斥方法
- 互斥锁
- 信号量
- 原子变量
- 自旋锁
- …
- 如何解决驱动的竞态呢?
3.1 互斥锁
mutest_t mutex;–>应用层互斥锁
-
定义互斥锁—>全局
- struct mutex g_Mutex;—>内核互斥锁
-
初始化互斥锁
- mutex_init(&g_Mutex);—>helloModule
-
加锁
- mutex_lock(&g_Mutex);—>helloOpen
-
解锁
- mutex_unlock(&g_Mutex);---->helloClose
测试:
-
test.c --> open(“/dev/haha0”);
-
sleep(5);
-
write();
-
close();
-
-
test1.c – >open(“/dev/haha1”);
- read();
- close();
3.2 信号量
进程线程三种信号量:
- 无名信号量 sem_t sem;
- 有名信号量 sem_open;
- 信号灯集 —>ipc
作用:解决驱动互斥的
驱动中的信号量:
-
定义信号量
- struct semaphore sem;//内核信号量
/* Please don't access any members of this structure directly */ struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };
-
初始化信号量
- sema_init(&sem,1);—>helloModule
static inline void sema_init(struct semaphore *sem, int val) { static struct lock_class_key __key; *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val); lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0); }
-
获取信号量
- down_interruptible(&sem); —> p操作函数 sem > 0:sem–;函数返回 sem = 0:阻塞进程;
- down(&sem);
-
释放信号量— helloClose
- up(&sem);----> v操作 sem_post(&semb) —> 给sem++;
3.3 原子变量
原子变量是不可再分的变量,
-
定义原子变量并初始化
atomic_t a = ATOMIC_INIT(1);
-
使用原子变量
需要通过Linux内核提供的函数来操作原子变量
atomic_t_dec_and_test(&a);:该函数给原子变量a减1,然后测试其值是否等于0,如果为0,返回true,不为0返回false。
atomic_inc(&a);该函数给原子变量加1
helloOpen:
if(!atomic_dec_and_test(&a)) //1->0-->-1
{
atomic_inc(&a); 0
return -EBUSY;
//不能使用驱动
}
//可以使用驱动资源
return 0;
helloClose:
atomic_inc(&a) //0-->1
3.4 自旋锁(忙等锁)
-
定义自旋锁
spinlock_t g; //自旋锁
-
初始化自旋锁
spin_lock_init(&g) //helloModule
-
获取自旋锁
spin_lock(&g) //helloOpen
-
释放自旋锁
spin_unlock(&g) //helloClose
测试步骤:
- sudo insmod hello.ko
- dmesg | tail -20
- ls -l /dev/haha*
- sudo ./test
- sudo ./test1
- sudo rmmod hello.ko
Day4
1. 阻塞操作
定义:是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。
驱动如何实现阻塞操作?
可以定义一个休眠等待队列,如果设备没有数据可读,就将请求读数据的进程放到休眠等待队列中休眠,直到设备准备好数据后,再唤醒休眠进程去读数据。
wait_queue_head_t q;–>休眠等待队列头
init_waitqueue_head(&q);–>初始化休眠等待队列
helloRead():
wait_event_interruptible(q,con)//-->使当前读进程休眠-->休眠期可被中断
wait_event(q,con) //-->使当前读进程休眠-->休眠期不可被中断
q:修吗等待队列头
con:休眠唤醒条件 1:结束休眠 0:继续休眠
helloWrite():
con=1;
wake_up_interruptible(&q) //-->唤醒进程(唤醒可被中断的休眠进程)
wake_up(&q) //-->唤醒进程(唤醒不可被中断的休眠进程)
注意:当进程正常运行时,进程在运行队列中等待被运行
当进程休眠时,进程在休眠等待对列中等待被唤醒运行
2. 非阻塞操作
定义:是指在不能进行设备操作时,并不挂起或休眠该进程,而是给进程一个非正确的值,底层设备驱动通过不停的查询查看操作是否可进行,知道操作可进行后给请求进程一个正确的结果(进程在不能进行设备操作时,并不休眠而是立即返回)
read -->helloRead
应用层
open("/dev/haha0",O_RDWR | O_NONBLOCK); //传入非阻塞标志
helloOpen(struct inode *node,struct file *pFile);
驱动层:需要应用层传入一个O_NONBLOCK标志,之后在helloRead中判断有没有该标志?如果有,并且没有数据可读,则返回一个非正确的值,如果没有,并且没有数据可读则阻塞当前读进程。
helloRead()
if(设备中没有数据可读)
{
if(有O_NONBLOCK标志)
{
return 非正确的值;
}
else
{
阻塞请求读数据的进程;
}
}
//注:int g_charcount; -->表示实际写入到内核中的字符个数
情况一:以非阻塞方式打开
- sudo insmod hello.ko
- ls -l /de/haha*
- sudo ./read -->未读到数据,读进程不会被挂起,而是得到一个非正确的值
- sudo ./write–>写入数据
- sudo ./read -->读到数据了
- sudo rmmod hello.ko
情况二:以阻塞方式打开
- sudo insmod hello.ko
- ls -l /dev/haha*
- sudo ./read -->read进程被阻塞了
- sudo ./write–>写入数据,read进程被唤醒,读数据并正常结束
- sudo rmmod hello.ko
3. poll机制
IO多路复用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lNHp4LKq-1677235729174)(驱动.assets/image-20230221114018620-16769508276421.png)]
hellopoll()
-
将监控文件描述符的进程放入监控队列中
poll_wait(); //将循环等待队列放到轮询表中 static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p) { if (p && p->_qproc && wait_address) p->_qproc(filp, wait_address, p); }
- *file:内核打开的文件
- wait_address:指向休眠等待队列
- *p:指向轮询表的指针
将当前文件的进程加入到休眠等待队列中,将该队列加入到轮询表中
-
系统对监控队列中的进程监控,若被监控的进程中的文件描述符上有IO数据发生,系统会将该系统激活。---->return mask
mask:是用来描述某种或多种操作是否可以立即无阻塞执行的位掩码,mask是一个32bit的整数他的每一个bit代表一个状态(每一个bit可以用一个宏表示)
POLLIN:有数据可读
POLLRDNORM:有普通数据可读
POLLOUT:有数据可写
POLLERR:指定文件描述符发生错误
4. 异步IO
异步IO:读进程去设备中读数据,设备没有数据可读,那么当前进程被阻塞,若内核检查到设备有数据可读之后,内核给读进发送通知信号,读进程会执行该信号对应的处理函数去读数据。
应用层:
A–>读数据
- 注册一个信号处理函数 signal(SIGIO,handler) // handler --> read
- 打开设备文件 open(“/dev/haha0”,);
- 设置文件描述符的宿主fcntl(fd,F_SETOWN,getpid());
- 读取文件描述符标志 int flags = fcntl(fd,F_GETFL);
- 设置文件描述符新标志 fcntl(fd,F_SETFL,flags | FASYNC);—>增加一个新标志FASYNC
while(1);
B–>写数据
- open
- write() --> helloWrite --> kill(A进程)
fcntl:是一个系统调用函数,用来获取或设置文件描述符的属性值
驱动层:
-
HelloWrite
kill_fasync(&fasync,SIGIO,POLLIN)
-
HelloFasync(int fd,struct file *pFile,int on)
{
fasync_helper(fd,pFile,on,&fasync)–>初始化异步通知结构体
}
void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
/* First a quick test without locking: usually
* the list is empty.
*/
if (*fp) {
rcu_read_lock();
kill_fasync_rcu(rcu_dereference(*fp), sig, band);
rcu_read_unlock();
}
}
- 作用:用来发送通知信息
- **fp:指向异步通知结构体的指针
- sig:发送的通知信号
- band:信号携带的指令
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
if (!on)
return fasync_remove_entry(filp, fapp);
return fasync_add_entry(fd, filp, fapp);
}
- 作用:初始化异步通知结构体
- fd:文件描述符
- filp:指向内核设备文件的指针
- on:启动异步通知的开关
- **fapp:指向异步通知的开关
SIGIO–>异步通知结构体–>fd–>A进程
Day 5
1. 设备树
定义:描述设备硬件信息的一种数据结构
exynos4412-fs4412.dtb–>硬件信息
uImage —> 代码框架
将代码逻辑和数据信息分离的思想
硬件:内存 flash gpio uart usb…—>可以用一种数据结构表示—>设备树—>传递给内核的数据信息
软件:uImage —> Linux内核软件 —>逻辑
dt:设备树文件 device tree
dtb:设备树二进制文件 exynos4412-fs4412.dtb
dts:设备树源文件 exynos4412-fs4412.dtb
dtsi:设备树头文件 相当于c的头文件–>多张板卡共有的部分
注意:一张板卡对应一个设备树文件
Linux内核代码支持ARM体系部分,从3.0版本以后就将描述设备数据的文件和描述设备的逻辑代码分开了,描述设备硬件数据的文件成为设备树文件,描述设备的逻辑代码成为内核(kernel)。
一个设备树源文件对应一张板卡的所有外设信息,在文件中描述硬件结点的语法是设备树语法。
设备树文件语法:
- /{} 表示一张板卡的所有硬件信息是根节点
- 节点名称@地址{节点属性} —> 构成一个子节点
- compatible:根节点:制造商和产品型号。子节点:用来关联驱动和被驱动的设备
- reg:可寻址设备用来表示编码地址的信息,是一个列表。reg = <地址1 长度1 地址2 长度2 … >
每一个可编址的设备都有一个reg,他是一个元组表,每一个元组表都表示一个设备地址范围。 - interrupt-parent:终端控制器节点指针,设备节点通过它来指定它所依附的中断控制器
- interrupts:中断指示符列表 (中断号和触发方式)<中断类型 中断号 中断的触发方式>
2. 平台设备驱动框架
适用于字符设备,块设备,网络设备
设备:硬件设备 LED UART SPI I2C USB 网卡… -->设备树节点 —>硬件信息
驱动:驱使硬件动起来的软件
使用虚拟总线可以将设备和他的驱动程序关联起来,虚拟总线就成为平台platfrom。
我们将挂接在虚拟总线上的设备称为平台设备platfrom_device
我们将挂接在虚拟总线上的驱动称为平台驱动platform_drive
当在Linux系统中注册一个设备时,设备会通过虚拟总线去找与之匹配的驱动程序
当在Linux系统中注册一个驱动时,驱动程序会通过虚拟总线会找与之匹配的设备
平台设备驱动框架:
init:
{
1. 注册平台设备
platform_device_register //将设备挂接在平台总线上
/**
* platform_device_register - add a platform-level device
* @pdev: platform device we're adding
*/
int platform_device_register(struct platform_device *pdev)
{
device_initialize(&pdev->dev);
arch_setup_pdev_archdata(pdev);
return platform_device_add(pdev);
}
2. 注册平台驱动
platform_driver_register(struct platform_device *pdev)
//将驱动挂接在平台总线上
}
exit:
{
1. 去注册平台驱动(卸载)
platform_driver_unregister(struct platform_driver *drv) //将驱动从平台总线上移除
/**
* platform_driver_unregister - unregister a driver for platform-level devices
* @drv: platform driver structure
*/
void platform_driver_unregister(struct platform_driver *drv)
{
driver_unregister(&drv->driver);
}
2. 去注册平台设备(卸载)
platform_device_unregister(struct platform_device *pdev)//将设备从平台总线上移除
/**
* platform_device_unregister - unregister a platform-level device
* @pdev: platform device we're unregistering
*
* Unregistration is done in 2 steps. First we release all resources
* and remove it from the subsystem, then we drop reference count by
* calling platform_device_put().
*/
void platform_device_unregister(struct platform_device *pdev)
{
platform_device_del(pdev);
platform_device_put(pdev);
}
}
在Linux系统中用struct platform_device来描述一个平台设备
struct platform_device {
const char *name; //设备名
int id; //设备id
bool id_auto; //是否自动获取id
struct device dev; //代表一个内核设备-->release:释放平台设备
u32 num_resources; //设备资源总数
struct resource *resource; //设备资源
...
}
在Linux内核中用struct platform_driver描述平台驱动
struct platform_driver {
int (*probe)(struct platform_device *);//探测函数
int (*remove)(struct platform_device *);//移除驱动
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;//驱动匹配设备的因素
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
3. 平台设备和平台驱动分开
平台设备–>hello.ko
驱动代码–>driver.ko
注意:
- 安装时,先安装平台设备,再安装设备驱动
- 卸载时,先卸载驱动设备,再卸载平台设备
4. 平台设备添加资源
如何给设备添加资源?使用平台设备中的struct resource来增加资源
/*
* Resources are tree-like, allowing
* nesting etc..
*/
struct resource { //用来描述设备资源
resource_size_t start;//起始地址
resource_size_t end;//结束地址
const char *name;//资源名称
unsigned long flags;//资源类型
struct resource *parent, *sibling, *child;//资源父对象,子对象...
};
struct resource res[]=
{
{
.start=0x11000c40,
.end=0x11000c43,
.flags=IORESOURCE_MEM,//内存资源
},
{
.start=88,
.flags=IORESOURCE_IRQ,
},
}
5. 平台驱动获取资源
当注册平台驱动时,平台驱动的helloprobe函数通过虚拟总线去获取到挂接在虚拟总线上的设备信息
helloprobe(struct platform_device *pdev)
{
pdev->resource;
if(pdev->resource->flags & RESOURCE_MEM)
{
//使用
}
}
6. 内存映射函数(mmap)
mmap --> 系统调用函数
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 作用:将用户空间的地址映射到内核空间
- *addr:被映射的用户空间地址,通常给NULL
- length:映射地址的长度
- prot:内核保护标志PROT_READ PROT_WRITE
- flags:MAP_SHARED->共享 MAP_PRIVATE->私有
- fd:文件描述符
- offset:被映射对象的读写偏移量 0
- 返回值:成功:被映射区的地址(指针),失败:MAP_FAILED
int munmap(void *addr, size_t length);
- 作用:取消映射
- *addr:被映射的地址---->用户空间
- length:映射长度
- 返回值:0:成功,-1 失败
驱动:
int (*mmap) (struct file *, struct vm_area_struct *);
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start;//起始地址/* Our start address within vm_mm. */
unsigned long vm_end;//结束地址/* The first byte after our end address within vm_mm. */
pgprot_t vm_page_prot;//保护标志/* Access permissions of this VMA. */
unsigned long vm_flags; //MAP_SHARED MAP_PRIVATE/* Flags, see mm.h. */
}
HelloMmap(struct file *pFile, struct vm_area_struct *vma));
- pFile:指向内核打开的文件
- *vma:指向应用层调用mmap映射成功后生成的映射结构体对象
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot)
-
作用:将映射结构体中指定的用户内存地址映射到内核虚拟地址,并将该虚拟地址对应到具体的物理地址上
-
*vma:应用层使用mmap后内核生成了映射结构体变量
-
addr: 被映射的用户空间地址的起始值
-
pfn::内核虚拟地址对应的物理地址的页编号
virt_to_phys(g_buf) >> 12;
-
size:被映射区域的大小
-
port:内核保护标志
初始化函数
g_buf = kmalloc(size,GFP_KERNEL); //申请的空间在物理上连续的
vmalloc//申请的空间在物理上不连续的
- 作用:申请一块内核内存空间
kfree(void *p);
vfree(void *p);
- 释放一块内核空间
7. LED驱动
Day 7
1. 用字符设备框架和平台设备驱动框架实现led驱动
1.1 用字符设备驱动框架–>led2
控制led2闪烁
-
应用层:
open("/dev/haha0"); while(1) { ioctl(fd,LED_ON); sleep(1); ioctl(fd,LED_OFF); sleep(1); }
-
驱动层:
helloIoctl(pFile, cmd, arg) { switch(cmd) case LED_ON: { led_on();//点灯 内核函数 设备树文件 break; } case LED_OFF: { led_off();//灭灯 break; } }
-
操作LED–>设备树文件–>增加LED节点–>make dtbs -->新exynos4412-fs4412.dts
-
如何操作LED硬件地址?使用内核提供的函数
struct devive_node *p = of_find_node_by_path("/fs4412-led");//把设备树的节点编译成c结构体所表示的 /** * of_find_node_by_path - Find a node matching a full OF path * @path: The full path to match * * Returns a node pointer with refcount incremented, use * of_node_put() on it when done. */ struct device_node *of_find_node_by_path(const char *path) { struct device_node *np = of_allnodes; unsigned long flags; raw_spin_lock_irqsave(&devtree_lock, flags); for (; np; np = np->allnext) { if (np->full_name && (of_node_cmp(np->full_name, path) == 0) && of_node_get(np)) break; } raw_spin_unlock_irqrestore(&devtree_lock, flags); return np; } gpio管脚编号 = of_get_named_gpio(p,led,0);//获取属性 /** * of_get_named_gpio() - Get a GPIO number to use with GPIO API * @np: device node to get GPIO from * @propname: Name of property containing gpio specifier(s) * @index: index of the GPIO * * Returns GPIO number to use with Linux generic GPIO API, or one of the errno * value on the error condition. */ static inline int of_get_named_gpio(struct device_node *np, const char *propname, int index) { return of_get_named_gpio_flags(np, propname, index, NULL); } gpio_set_value_cansleep(gpio管教编号, #val);
虚拟机:
- 修改设备树文件–>添加led2节点–>make dtbs–>exynos4412-fs4412.dtb
- cp exynos4412-fs4412.dtb /tftpboot
- 修改hello.c–>使用linux3.14下的Makefile编译–>hello.ko
- cp hello.ko /source4/rootfs
- arm-none-linux-gnueabi-gcc test.c -o test
- cp test /source4/tootfs
开发板:
- 启动开发板,进入u-boot模式
- 修改ipaddr,serverip gatewayip,bootargs
- save
- 重启开发板,加载内核设备树,挂接网络文件系统
- root@farsight# insmod hello.ko
- root@farsight# dmesg | tail
- root@farsight# ./test
- 观察LED2是否闪烁
-
1.2 平台设备驱动框架(ioremap)–>led2
ioremap--->内核函数--->内核 & 驱动
//作用:将物理地址映射到内核虚拟地址
void __iomem *ioremap(unsigned long paddr, unsigned long size)
- paddr:需要映射的物理地址
- size:需要映射的字节数
- 返回值:映射成功后生成内核虚拟地址
cpu位IO外设提供了两种编制方式:
- IO映射方式(IO外设独立编址),cpu为外设专门实现了一个单独地址空间,为IO地址空间,cpu通过专门的IO指令来访问这一地址空间
- 内存映射方式(IO外设统一编址),RISC系统的CPU通常会对IO外设做统━编址,通常只实现一个物理地址空间,IO外设端口像内存一样被统一编址,CPU可以像访问内存一样访问IO端口。
static inline void writel(u32 b, volatile void __iomem *addr)
- 作用:将数据写入到内核虚拟地址
- b:通过内核虚拟地址向物理地址写入的值
- *addr指向内核虚拟地址的指针
static inline unsigned int readl(const volatile void __iomem *addr)
- 通过内核虚拟地址读取对应物理地址的值
- *addr:指向内核虚拟地址的指针
- 返回值:从内核虚拟地址读到的对应物理地址的值
驱动层:
helloIoctl(pFile, cmd, arg)
{
switch(cmd)
case LED_ON:
{
led_on();//点灯 内核函数 设备树文件
writel(g_buf,1) //(物理地址)
break;
}
case LED_OFF:
{
led_off();//灭灯
writel(g_buf,0) //(物理地址)
break;
}
}
helloprobe(struct platform_device *pdev)-->pdev->resource
ioremap(pdev->resource[0].start,g_buf)
2. 中断
定义:是指CPU在执行程序的过程中插入了另外一段程序的执行过程
发起中断的方式:
- 软中断 --> 通过软件的方式发起的,可控的。
- 硬件中断 --> 由于硬件故障产生的,不可控的。
系统中断
功能:按下k2按键 触发中断
在Linux设备驱动中,要使用中断的设备需要申请中断和释放中断
request_irq -->给设备请求一个中断
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
作用:请求中断
irq:从设备资源中获取到的中断号
handler:中断处理程序
typedef irqreturn_t (*irq_handler_t)(int, void *);
flags:中断属性,可以指定中断的触发方式和处理方式
*name:请求中断的设备名称
*dev:NULL
free_irq --> 释放中断
void free_irq(unsigned int irq, void *dev_id);
作用:释放中断
irq:从设备资源中获取到的中断号
*dev_id:NULL
软件
硬件中断–>需要配置的–>13jcq
类比:拜访马云
第一阶段:乘坐某种交通工具去杭州–》连线
第二阶段:筛查–》中断控制器(GIC)
第三阶段:cpu处理客户–>CPU处理中断
中断的基本概念:
cpu和外设之间的数据传送方式:
1. 中断方式:当外设需要和cpu进行数据交换时,由外设向CPU发出请求信号,使CPU暂停正在执行的程序,转去执行外设数据的输入和输出操作,数据传送结束后,CPU再继续执行被暂停的程序。
2. 程序控制方式:直接的在程序的控制下进行数据的输入和输出操作
3. DMA方式:外设通过DMA总线直接从内存中拿数据,不通过CPU倒数exynos4412支持160个中断源,这些中断源分三类:
+ SGI:软中断,用于各个核之间的通信
+ PPI:私有外部中断,是每个核私有的外部中断
+ SPI:公共外部中断,是每个核公有的外部中断
程序的入口?main 异常向量表 中断
目标:通过k2按键触发硬件中断
第一阶段:连线
K2–>con4–>con3–>u1a(gpx1_1)–>能发出XEINT9中断–>57号中断
- 配置GPX1_1为外部中断模式
6.2.3.198 GPX1CON 0x11000C20 (4-7)0xF - 配置中断的触发方式–>下降沿触发
6.2.3.211 EXT_INT41CON 0x11000E04 (4-6)0x2 - 取消中断掩码
6.2.3.223 EXT_INT41_MASK 0x11000F04 (1)0
第二阶段:gic
- 使能57号中断
9.5.1.16 ICDISER_CPU 0x10490104 (25)1 - 把57号中断交给cpu0处理
9.5.1.22 ICDIPTR14_CPU0 0x10490838 (8-15)0x1 - 设置中断优先级
9.5.1.21 ICDIPR_CPU 0x10490438 (8-15) 优先级范围0~255 例如 12 - 使能CPU0中断
9.5.1.1 ICCICR_CPU0 0x10480000 (0)1 - 设置cpu0的中断优先级阈值
9.5.1.2 ICCPMR_CPU0 0x10480004 (0-7)0xFF - 使能GIC总开关
9.5.1.12 ICDDCR 0x10490000 (0)1
第三阶段:中断处理
- 得到中断号–》寄存器
9.5.1.4 ICCIAR_CPU0 0x1048000C (0-9)获取中断号处理中断–》led uart - 清除管脚中断标志
6.2.3.227 EXT_INT41_PEND 0x11000F44 (1)1 - 清除GIC控制器的57号中断标志
9.5.1.19 ICDICPR_CPU 0x10490284 (25)1 - 结束中断
9.5.1.5 ICCEOIR_CPU0 0x10480010 (0-9)中断号
Day7
1. 中断下半部
一般来说,操作系统中的中断越短越好,如果系统中的中断处理时间过长,可以将中断处理过程分成两部分进行处理
第一部分,专门接收和响应中断请求(登记中断)–>上半部
第二部分,专门来处理中断的耗时业务逻辑(处理耗时操作)–>下半部
Linux系统中的中断处理也是分成上半部和下半部采完成的。下半部的处理方有:tasklet 工作队列,内核定时器
1.1 tasklet–>小“片”任务
-
声明一个tasklet
DECLARE_TASKLET(name,unfc,data);
-
name:小任务的名称
-
func:小人物对应的处理函数
void (*func)(unsigned long);
-
data:传给func函数的入参
-
-
调用小人物–>handler
tasklet_schedule(&name); static inline void tasklet_schedule(struct tasklet_start *t);
- 作用:调度小任务
- *t:指向小任务名称的指针
1.2 工作队列
是指在中断处理中可以把耗时的操作推迟执行,可以把耗时的操作交到工作队列中等待被执行,内核中有一个线程events会去工作队列中找等待被执行的工作,然后执行这个等待的工作(工作:中断处理程序中耗时的那部分操作)
-
定义一个工作队列
struct work_struct my_wq;
-
初始化工作队列
INIT_WORK(_work,_func);
- _work:指向工作队列的指针
- _func:工作队列对应的处理函数
-
调度工作队列
typedef void (*work_func_t)(struct work_struct *work);
1.3 内核工作队列
-
struct time_list mytimer;定义一个内核定时器
-
helloprobe:
init_timer(mytimer);初始化内核定时器 mytimer.function=my_func;-->用来处理耗时的操作 mytimer.expires=时间值--》表示超时时间 add_timer(&mytimer)-->启动定时
2 块设备驱动
2.1 块设备以及相关术语
块设备:可以从设备的任意位置读取一定长度数据的设备
常见的块设备驱动:硬盘,磁盘,光盘,U盘,sd卡
硬件种类:
- 机械硬盘
- 固态硬盘
- 混合硬盘
磁盘概念:
- 磁头:是磁臂顶端用来指向硬盘的读写数据的指针
- 扇区:是读写数据的最小单位512byte
- 柱面:多张累加的磁盘上同一半径的磁道形成的面
- 磁道:磁头画的同心圆就是磁道,用来存放数据
- 硬盘容量:磁头数 * 柱面数 * 每个磁道包含的扇区个数 * 512byte
磁头:8
扇区:16
柱面:128
2.2 模拟创建一个硬盘,并实现其驱动
init流程:–>helloModule
-
创建一个块设备号
int register_blkdev(unsigned int major, const char *name)
- 作用:创建一个设备号
- major:主设备号。major > 0 静态创建块设备号 major =0 动态创建块设备号
- *name:块设备名称 系统中唯一,不能重复
- 返回值:成功设备号,失败 负数
-
创建并初始化请求队列–>用户对设备的读写请求
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
-
作用:初始化一个请求队列,(用自旋锁来控制队列的访问方向)
-
*rfn:请求处理函数
typedef void(request_fn_proc)(struct request_queue *q)
-
*lock:指向自旋锁的指针
-
返回值:指向请求队列的指针
-
-
分配一个gendisk结构
struct gendisk *alloc_disk(int minors) { return alloc_disk_node(minors, NUMA_NO_NODE); }
- 作用:分配一块存放块设备结构体的内存
- minors:次设备数量(最大分区数)
- 返回值:指向块设备的结构体指针
- 用struct gendisk来描述一个块设备
struct gendisk { /* major, first_minor and minors are input parameters only, * don't use directly. Use disk_devt() and disk_max_parts(). */ int major; // 主设备号 /* major number of driver */ int first_minor;//第一个次设备号 int minors; //次设备号的个数/* maximum number of minors, =1 for* disks that can't be partitioned. */ char disk_name[DISK_NAME_LEN];//块设备名称 /* name of major driver */ char *(*devnode)(struct gendisk *gd, umode_t *mode); unsigned int events; /* supported events */ unsigned int async_events; /* async events, subset of all */ /* Array of pointers to partitions indexed by partno. * Protected with matching bdev lock but stat and other * non-critical accesses use RCU. Always access through * helpers. */ struct disk_part_tbl __rcu *part_tbl; struct hd_struct part0; const struct block_device_operations *fops;//块设备操作方法集 struct request_queue *queue;//IO请求队列 void *private_data; int flags; struct device *driverfs_dev; // FIXME: remove struct kobject *slave_dir; struct timer_rand_state *random; atomic_t sync_io; /* RAID */ struct disk_events *ev; #ifdef CONFIG_BLK_DEV_INTEGRITY struct blk_integrity *integrity; #endif int node_id; };
-
设置gendisk结构体成员
-
为磁盘分配内存
void *vmalloc(unsigned long size)
- size:分配的内存大小
- 返回值:返回指向内存大小
-
设置磁盘容量
static inline void set_capacity(struct gendisk *disk, sector_t size)
- *disk:指向块设备的指针
- size:总的扇区数
-
添加磁盘到内核中
void add_disk(struct gendisk *disk)
- disk:指向设备的指针
exit流程:—>helloeExit
-
释放磁盘扇区缓存
-
释放gendisk
void del_gendisk(struct gendisk *disk)
- *disk:指向块设备结构体的指针
-
清楚内存中的请求队列
void blk_cleanup_queue(struct request_queue *q);
- *q:指向请求队列的指针
-
删除块设备号
void unregister_blkdev(unsigned int major, const char *name)
- major:设备号
- *name:设备名称
struct hd_geometry {-->用来描述磁盘的几何信息
unsigned char heads;-->磁头数
unsigned char sectors;-->每个磁道包含的扇区个数
unsigned short cylinders;-->柱面数
unsigned long start;-->起始地址
};