驱动学习笔记

Day1

  • 驱动
  • 模块
  • 模块传参
  • 符号导出

程序的入口:

  • 应用程序:main
  • 内核:异常向量表
  • 中断

1. 驱动

定义:驱使硬件动起来的程序
​种类:

  • 裸机驱动:需求分析–>查原理图–>查芯片手册–>code
  • 系统驱动:需求分析–>查原理图–>查芯片手册–>设备树–>code -->安装到内核中

裸机开发&系统开发的优缺点?

  • 裸机开发:成本低 运行效率高 安全性低 单任务
  • 系统开发:成本高 运行效率低 安全性低 多任务

应用程序和驱动程序的区别?

区别应用程序驱动程序
加载方式主动加载被动加载
运行空间用户空间kernel空间
执行权限
影响力局部全局
函数来源自定义/库/系统调用内核函数/自定义

2. 模块–>驱动模块

模块:能够单独命名并且独立完成一定功能的程序语句的集合(程序代码和数据结构)
驱动模块:能够单独命名并且独立完成特定外设功能驱动的程序语句的集合
注:一个驱动模块就是一个完整的外设驱动程序,驱动程序被安装到操作系统内核中,当该驱动程序对应的外设要工作时,该驱动模块被调用。

2.1 如何写一个驱动模块?

  1. 模块初始化函数 int 函数名1(void)
  2. 模块清除函数 void 函数名2(void)
  3. 模块加载函数 module_init(函数名1) --> sudo insmod hello.ko
  4. 模块卸载函数 module_exit(函数名2) --> sudo rmmod hello.ko
  5. 声明该驱动遵守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:访问权限,06440表示该参数在文件系统中不可见

		module_param_string(name, string, len, perm)
		name:外部参数名
		string:内部参数
		len:数组的大小
		perm:访问权限,06440表示该参数在文件系统中不可见

		module_param_array(name, type, nump, perm)
		name:数组参数名,既是内部参数名,又是外部参数名
		type:参数的数据类型(数组成员的数据类型)
		nump:用来存放终端传给数组的实际元素个数
		perm:访问权限,06440表示该参数在文件系统中不可见

测试步骤:
        1 sudo insmod hello.ko gtest=100
        2 dmesg |tail-->查看gtest的值是否是1003 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. 符号导出

  1. 什么是符号?在内核和驱动中主要是指全局变量和函数

  2. 为什么要导出符号?
    linux内核是以模块化形式管理内核代码的。内核中的每个模块之间是相互独立的,也就是说A模块中的全局变量和函数,B模块是无法访问的,若B模块想要使用A模块已有的符号,那么必须将A模块中的符号做导出,导出到模块符号表中,然后B模块才能使用符号表里的符号。

  3. 如何做符号导出?
    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符号名模块名导出符号的宏
0x8eaf8fe3gtest/home/farsight/2022/22101/driver/day1/module_symbol/helloEXPORT_SYMBOL_GPL
0xd1a68ac8Func/home/farsight/2022/22091/driver/day1/module_symbol/helloEXPORT_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内核提供了两类符号表:

  1. 用户自定义模块导出的符号表,称为本地符号表Module.symvers
  2. 内核全局符号表 /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

  1. 申请设备号(静态申请,动态申请)
  2. 创建一个字符设备
  3. 初始化字符设备
  4. 将字符号和设备关联起来

exit流程:–>HelloExit

  1. 删除字符设备
  2. 删除设备号
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

测试步骤:

  1. sudo insmod hello.ko
  2. dmesg |tail -->250 0
  3. cat /proc/devices–>查看设备号 250 0
  4. sudo mknod /dev/haha0 c 250 0
  5. ls -l /dev/haha*—>查看创建字符设备文件
  6. sudo ./test–>open hahao ok!
  7. dmesg |tail–>helloopen/helloClose
  8. sudo rmmod hello.ko
  9. sudo rm /dev/haha0

区分字符设备驱动框架中使用的三个结构体:

  1. struct file:代表内核中一个打开的文件。系统中每个打开的文件在内核中都有一个关联的struct file。
  2. struct inode:用来记录文件在物理上的信息。它和打开文件struct file结构不同,一个文件可以对应多个struct file,但是只有struct inode.
  3. 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. 自动创建设备文件

创建设备文件的方式:

  1. 手动创建 sudo mknod /dev/doglc c 250 0
  2. 自动创建–>使用内核函数

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:设备号

测试步骤:

  1. sudo insmod hello.ko
  2. dmesg | tail
  3. ls -l /dev/haha* —> 查看是否生成了设备文件
  4. sudo ./test
  5. sudo ./test1
  6. sudo rmmod hello.ko
  7. ls -l /dev/haha* —>查看设备文件是否被删除

设备:

  1. 主设备—>一类设备
  2. 次设备—>该类设备中的某一个设备

设备文件:一个设备文件对应一个次设备

2. ioctl

ioctl—>系统调用函数—>应用程序用它给设备&内核发送控制指令

Linux内核给用户提供了两类系统调用函数:

  1. 数据操作函数,如read,write
  2. 非数据操作函数,如ioctl,内核间将对设备操作交给了ioctl接口(换句话说,应用程序可以使用ioctl接口去控制底层设备)
int ioctl(int d,int request,...)

d:文件描述符

request:指令码

…:可变参数,若有那个该参数是传给内核驱动的函数

返回值:0成功,非0失败

命名码:

  1. 可以自定义 1 0
  2. 使用标准命名码 int—>4部分
    1. 30-31:数据的控制方向
    2. 16–19:数据大小(14bit)
    3. 8-15:设备类型(8bit)
    4. 0-7:cmd---->命令码(区分命令的顺序序号,底八位)
  3. 如何生成标准命名码?用内核提供的宏
#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;
            };
    }
}

测试:

  1. sudo insmod hell.ko
  2. ls -l /dev/haha*
  3. sudo ./test
  4. dmesg |tail ----->查看ioctl发的指令是否被解析
  5. sudo rmmod hello.ko

3. 驱动的互斥

设备号:主设备 + 次设备

  • 主设备:代表一类设备
  • 次设备:代表一类设备的某一个设备—>使用驱动程序的终端个体
  • 多个子设备公用一个驱动程序,若多个子设备同时访问一个驱动程序时,就产生了竞态访问
    • 如何解决驱动的竞态呢?
      • Linux内核提供了多种互斥方法
      • 互斥锁
      • 信号量
      • 原子变量
      • 自旋锁

3.1 互斥锁

mutest_t mutex;–>应用层互斥锁

  1. 定义互斥锁—>全局

    • struct mutex g_Mutex;—>内核互斥锁
  2. 初始化互斥锁

    • mutex_init(&g_Mutex);—>helloModule
  3. 加锁

    • mutex_lock(&g_Mutex);—>helloOpen
  4. 解锁

    • mutex_unlock(&g_Mutex);---->helloClose

测试:

  1. test.c --> open(“/dev/haha0”);

    • sleep(5);

    • write();

    • close();

  2. test1.c – >open(“/dev/haha1”);

    • read();
    • close();

3.2 信号量

进程线程三种信号量:

  • 无名信号量 sem_t sem;
  • 有名信号量 sem_open;
  • 信号灯集 —>ipc

作用:解决驱动互斥的

驱动中的信号量:

  1. 定义信号量

    • 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;
    };
    
  2. 初始化信号量

    • 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);
    }
    
  3. 获取信号量

    • down_interruptible(&sem); —> p操作函数 sem > 0:sem–;函数返回 sem = 0:阻塞进程;
    • down(&sem);
  4. 释放信号量— helloClose

    • up(&sem);----> v操作 sem_post(&semb) —> 给sem++;

3.3 原子变量

原子变量是不可再分的变量,

  1. 定义原子变量并初始化

    atomic_t a = ATOMIC_INIT(1);

  2. 使用原子变量

    需要通过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 自旋锁(忙等锁)

  1. 定义自旋锁

    spinlock_t g; //自旋锁

  2. 初始化自旋锁

    spin_lock_init(&g) //helloModule

  3. 获取自旋锁

    spin_lock(&g) //helloOpen

  4. 释放自旋锁

    spin_unlock(&g) //helloClose

测试步骤:

  1. sudo insmod hello.ko
  2. dmesg | tail -20
  3. ls -l /dev/haha*
  4. sudo ./test
  5. sudo ./test1
  6. 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; -->表示实际写入到内核中的字符个数

情况一:以非阻塞方式打开

  1. sudo insmod hello.ko
  2. ls -l /de/haha*
  3. sudo ./read -->未读到数据,读进程不会被挂起,而是得到一个非正确的值
  4. sudo ./write–>写入数据
  5. sudo ./read -->读到数据了
  6. sudo rmmod hello.ko

情况二:以阻塞方式打开

  1. sudo insmod hello.ko
  2. ls -l /dev/haha*
  3. sudo ./read -->read进程被阻塞了
  4. sudo ./write–>写入数据,read进程被唤醒,读数据并正常结束
  5. sudo rmmod hello.ko

3. poll机制

IO多路复用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lNHp4LKq-1677235729174)(驱动.assets/image-20230221114018620-16769508276421.png)]

hellopoll()

  1. 将监控文件描述符的进程放入监控队列中

    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:指向轮询表的指针​

    将当前文件的进程加入到休眠等待队列中,将该队列加入到轮询表中

  2. 系统对监控队列中的进程监控,若被监控的进程中的文件描述符上有IO数据发生,系统会将该系统激活。---->return mask

    mask:是用来描述某种或多种操作是否可以立即无阻塞执行的位掩码,mask是一个32bit的整数他的每一个bit代表一个状态(每一个bit可以用一个宏表示)

    POLLIN:有数据可读

    POLLRDNORM:有普通数据可读

    POLLOUT:有数据可写

    POLLERR:指定文件描述符发生错误

4. 异步IO

异步IO:读进程去设备中读数据,设备没有数据可读,那么当前进程被阻塞,若内核检查到设备有数据可读之后,内核给读进发送通知信号,读进程会执行该信号对应的处理函数去读数据。

应用层:

A–>读数据

  1. 注册一个信号处理函数 signal(SIGIO,handler) // handler --> read
  2. 打开设备文件 open(“/dev/haha0”,);
  3. 设置文件描述符的宿主fcntl(fd,F_SETOWN,getpid());
  4. 读取文件描述符标志 int flags = fcntl(fd,F_GETFL);
  5. 设置文件描述符新标志 fcntl(fd,F_SETFL,flags | FASYNC);—>增加一个新标志FASYNC
while(1);

B–>写数据

  1. open
  2. write() --> helloWrite --> kill(A进程)

fcntl:是一个系统调用函数,用来获取或设置文件描述符的属性值

驱动层:

  1. HelloWrite

    kill_fasync(&fasync,SIGIO,POLLIN)

  2. 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)。

一个设备树源文件对应一张板卡的所有外设信息,在文件中描述硬件结点的语法是设备树语法。
设备树文件语法:

  1. /{} 表示一张板卡的所有硬件信息是根节点
  2. 节点名称@地址{节点属性} —> 构成一个子节点
  3. compatible:根节点:制造商和产品型号。子节点:用来关联驱动和被驱动的设备
  4. reg:可寻址设备用来表示编码地址的信息,是一个列表。reg = <地址1 长度1 地址2 长度2 … >
    每一个可编址的设备都有一个reg,他是一个元组表,每一个元组表都表示一个设备地址范围。
  5. interrupt-parent:终端控制器节点指针,设备节点通过它来指定它所依附的中断控制器
  6. 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闪烁

  1. 应用层:

    open("/dev/haha0");
    while(1)
    {
        ioctl(fd,LED_ON);
        sleep(1);
        ioctl(fd,LED_OFF);
        sleep(1);
    }
    
  2. 驱动层:

    helloIoctl(pFile, cmd, arg)
    {
        switch(cmd)
            case LED_ON:
        	{
                led_on();//点灯 内核函数 设备树文件
                break;
            }
        	case LED_OFF:
        	{
                led_off();//灭灯
                break;
            }
    }
    
    1. 操作LED–>设备树文件–>增加LED节点–>make dtbs -->新exynos4412-fs4412.dts

    2. 如何操作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);
      
      

      虚拟机:

      1. 修改设备树文件–>添加led2节点–>make dtbs–>exynos4412-fs4412.dtb
      2. cp exynos4412-fs4412.dtb /tftpboot
      3. 修改hello.c–>使用linux3.14下的Makefile编译–>hello.ko
      4. cp hello.ko /source4/rootfs
      5. arm-none-linux-gnueabi-gcc test.c -o test
      6. cp test /source4/tootfs

      开发板:

      1. 启动开发板,进入u-boot模式
      2. 修改ipaddr,serverip gatewayip,bootargs
      3. save
      4. 重启开发板,加载内核设备树,挂接网络文件系统
      5. root@farsight# insmod hello.ko
      6. root@farsight# dmesg | tail
      7. root@farsight# ./test
      8. 观察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在执行程序的过程中插入了另外一段程序的执行过程

发起中断的方式:

  1. 软中断 --> 通过软件的方式发起的,可控的。
  2. 硬件中断 --> 由于硬件故障产生的,不可控的。

系统中断

功能:按下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号中断

  1. 配置GPX1_1为外部中断模式
    6.2.3.198 GPX1CON 0x11000C20 (4-7)0xF
  2. 配置中断的触发方式–>下降沿触发
    6.2.3.211 EXT_INT41CON 0x11000E04 (4-6)0x2
  3. 取消中断掩码
    6.2.3.223 EXT_INT41_MASK 0x11000F04 (1)0

第二阶段:gic

  1. 使能57号中断
    9.5.1.16 ICDISER_CPU 0x10490104 (25)1
  2. 把57号中断交给cpu0处理
    9.5.1.22 ICDIPTR14_CPU0 0x10490838 (8-15)0x1
  3. 设置中断优先级
    9.5.1.21 ICDIPR_CPU 0x10490438 (8-15) 优先级范围0~255 例如 12
  4. 使能CPU0中断
    9.5.1.1 ICCICR_CPU0 0x10480000 (0)1
  5. 设置cpu0的中断优先级阈值
    9.5.1.2 ICCPMR_CPU0 0x10480004 (0-7)0xFF
  6. 使能GIC总开关
    9.5.1.12 ICDDCR 0x10490000 (0)1

第三阶段:中断处理

  1. 得到中断号–》寄存器
    9.5.1.4 ICCIAR_CPU0 0x1048000C (0-9)获取中断号处理中断–》led uart
  2. 清除管脚中断标志
    6.2.3.227 EXT_INT41_PEND 0x11000F44 (1)1
  3. 清除GIC控制器的57号中断标志
    9.5.1.19 ICDICPR_CPU 0x10490284 (25)1
  4. 结束中断
    9.5.1.5 ICCEOIR_CPU0 0x10480010 (0-9)中断号

Day7

1. 中断下半部

一般来说,操作系统中的中断越短越好,如果系统中的中断处理时间过长,可以将中断处理过程分成两部分进行处理

第一部分,专门接收和响应中断请求(登记中断)–>上半部
第二部分,专门来处理中断的耗时业务逻辑(处理耗时操作)–>下半部

Linux系统中的中断处理也是分成上半部和下半部采完成的。下半部的处理方有:tasklet 工作队列,内核定时器

1.1 tasklet–>小“片”任务

  1. 声明一个tasklet

    DECLARE_TASKLET(name,unfc,data);

    • name:小任务的名称

    • func:小人物对应的处理函数

      void (*func)(unsigned long);
      
    • data:传给func函数的入参

  2. 调用小人物–>handler

    tasklet_schedule(&name);
    static inline void tasklet_schedule(struct tasklet_start *t);
    
    • 作用:调度小任务
    • *t:指向小任务名称的指针

    1.2 工作队列

    是指在中断处理中可以把耗时的操作推迟执行,可以把耗时的操作交到工作队列中等待被执行,内核中有一个线程events会去工作队列中找等待被执行的工作,然后执行这个等待的工作(工作:中断处理程序中耗时的那部分操作)

    1. 定义一个工作队列

      struct work_struct my_wq;
      
    2. 初始化工作队列

      INIT_WORK(_work,_func);
      
      • _work:指向工作队列的指针
      • _func:工作队列对应的处理函数
    3. 调度工作队列

      typedef void (*work_func_t)(struct work_struct *work);
      

    1.3 内核工作队列

  3. struct time_list mytimer;定义一个内核定时器

  4. 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

  1. 创建一个块设备号

    int register_blkdev(unsigned int major, const char *name)
    
    • 作用:创建一个设备号
    • major:主设备号。major > 0 静态创建块设备号 major =0 动态创建块设备号
    • *name:块设备名称 系统中唯一,不能重复
    • 返回值:成功设备号,失败 负数
  2. 创建并初始化请求队列–>用户对设备的读写请求

    struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
    
    • 作用:初始化一个请求队列,(用自旋锁来控制队列的访问方向)

    • *rfn:请求处理函数

      typedef void(request_fn_proc)(struct request_queue *q)
      
    • *lock:指向自旋锁的指针

    • 返回值:指向请求队列的指针

  3. 分配一个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;
    };
    
  4. 设置gendisk结构体成员

  5. 为磁盘分配内存

    void *vmalloc(unsigned long size)
    
    • size:分配的内存大小
    • 返回值:返回指向内存大小
  6. 设置磁盘容量

    static inline void set_capacity(struct gendisk *disk, sector_t size)
    
    • *disk:指向块设备的指针
    • size:总的扇区数
  7. 添加磁盘到内核中

    void add_disk(struct gendisk *disk)
    
    • disk:指向设备的指针

exit流程:—>helloeExit

  1. 释放磁盘扇区缓存

  2. 释放gendisk

    void del_gendisk(struct gendisk *disk)
    
    • *disk:指向块设备结构体的指针
  3. 清楚内存中的请求队列

    void blk_cleanup_queue(struct request_queue *q);
    
    • *q:指向请求队列的指针
  4. 删除块设备号

    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;-->起始地址
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

居合啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值