Linux底层驱动的简单认知

一、什么是底层驱动?

       上一篇博文中,我们提到了底层驱动,但是并没有多做介绍。我的理解是:底层驱动是让设备工作的基本程序,它给用户提供了一个使用这个设备的接口。就拿树莓派来说,如果我们想要用它的那40Pin中的某个GPIO口,但是那个IO口没有相应的驱动程序给我们操作,这时,无论如何我们都无法操作IO口,wiringPi库到了最后也是要通过相应的驱动程序去操作IO的。因为底层驱动会操作CPU的与该设备相关的寄存器,实现驱动功能,这就是底层驱动。
       用户在使用驱动的时候,是要经过一系列复杂的流程,才能实现对设备的操作:
在这里插入图片描述

二、为什么要写驱动?

       当系统不提供相应设备的操作库时或没有驱动时,就需要自己编写驱动。树莓派提供了wiringPi库给我们操作IO口。但是如果换到其他的Linux板子上,2440,RK3399,nanoPi等,没有像wiringPi库这样的函数库时,还是要老老实实写驱动,不然就无法操作它的底层设备了。

三、怎么写驱动?

       在写驱动之前,我首先要了解Linux是怎么调用驱动的,用户应该怎么用驱动。在Linux系统中,一切皆文件,驱动也不例外。

1.驱动文件的存放位置

       Linux的驱动文件同意放在 /dev 目录底下,写好的驱动文件在安装时,应该需要安装在 /dev目录中。我们在使用驱动时,也是使用 /dev底下的驱动文件:
在这里插入图片描述

2.驱动的使用及区分方式

       一个驱动写好并安装之后,可以使用C库中的 openwriteread来操作,因为设备驱动也是文件嘛,这三个文件操作的函数当然也能操作:
       (1)open("/dev/xxx",O_RDWR)函数生成文件描述符 fd;
       (2)write(fd,“ xxx”,size_t);给驱动程序发送指令;
       (3)read(fd,char *,size_t);读取驱动程序发回来的数据;
       操作方式有了,但是要怎么在 /dev 底下找到相应的驱动,系统又是怎么在众多驱动中找到我们写的驱动?有两种方式:
              第一: 通过文件名;就是open打开的 /dev 目录下的某个驱动文件;
              第二: 通过设备号,设备号又有两种:

设备号说明
主设备号用来区分不同的设备,以整数的形式,比如GPIO,IIC,UART等
次设备号用来区分相同设备中的多个设备,比如GPIO中有GPIO.0,GPIO.1等

设备号的查看可用:

cd /dev
ls -l

在这里插入图片描述
       Linux的设备管理和文件系统时紧密结合在一起的,无论字符设备和块设备都会有一个设备号和次设备号。驱动链表负责管理这些设备,系统的设备都注册在驱动链表当中。驱动链表就是根据设备号去查找相应的设备,我们自己写的设备也不会例外。
       驱动既然是需要加入到驱动链表当中,就需要知道该链表的结构体(struct file_operations),也就是说,我们写的驱动文件会有一个框架,这个框架是根据驱动链表的结构体来写。驱动在链表中注册,就是在驱动链表当中插入一个节点,这个节点的位置由设备号来决定。

3.驱动链表 file_operations

       驱动链表的结构体 file_operations 有大量的结构体函数,其中就有open,write,read 这三个函数:

	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(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);//异步读操作
			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); //读取目录
			unsigned int (*poll) (struct file *, struct poll_table_struct *);//查询对一个或多个文件描述符的读或写是否会阻塞
			int (*ioctl) (struct inode *, struct file *, unsigned int,unsigned long);//系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写).
			int (*mmap) (struct file *, struct vm_area_struct *);//用来请求将设备内存映射到进程的地址空间
			int (*open) (struct inode *, struct file *);//打开操作,使用时第一个操作
			int (*flush) (struct file *);//操作在进程关闭它的设备文件描述符的拷贝时调用
			int (*release) (struct inode *, struct file *);//在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.
			int (*fsync) (struct file *, struct dentry *, int datasync);//用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.
			int (*aio_fsync) (struct kiocb *, int datasync);//这是 fsync 方法的异步版本.
			int (*fasync) (int, struct file *, int);//这个操作用来通知设备它的 FASYNC 标志的改变
			int (*lock) (struct file *, int, struct file_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 *);//写汇聚/散发
			ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t,void __user *);//sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个
			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);
	};

我们在写驱动时,应该根据需求给结构体的某个结构体函数写一个函数,比如open,write,read,可以这样:


int pin4_open (struct inode *inode, struct file *file)
{
	/*      实现的操作   */
	return 0;
}
ssize_t pin4_write(struct file *file, const char __user *buf, size_t count, loff_t *loffs)//写操作
{
	/*   实现的操作   */
	return 0;
}
ssize_t pin4_read(struct file *file, char __user *buf, size_t count, loff_t *loff)
{
	/*   实现的操作   */
	return 0;
}

struct file_operations pin_pos ={
	.owner = THIS_MODULE,  
	.open  = pin4_open,
	.write = pin4_write,
	.read  = pin4_read,
};	

上面以 “.open=xxx” 这种结构体的初始化方式只适用于Linux; 做完这些之后,只是把驱动链表中的结构体搭建好了,还需要把这个结构体插入到链表当中。

4.驱动注册及自动创建驱动文件

       建立好驱动的结构体之后,就需要把结构体插入到驱动链表中,先介绍一个函数:
(1) 驱动注册
函数: int register_chrdev(int major,char *module_name,struct file_operations *file_operation );
       这便是驱动链表注册函数,功能是把我们建立好的结构体插入到链表当中,并注册成字符设备,块设备用 register_blkdev
参数说明:
              1. major 驱动的主设备号,用来索引链表中的位置;
              2. module_name这是驱动在链表中的名字,也是驱动的标识;
              3. *file_operation file_operations的结构体指针,建好的驱动结构体就从这里传入。
       前面提到,驱动设备管理和文件系统是紧密结合在一起的,二者缺少其一,驱动都无法生效。
(2)自动创建驱动文件
       自动生成文件需要几个函数的配合,因为最终的函数 (device_create),需要用到dev_t 型的设备号,而设备号又由主设备和次设备号组合而成,所以要用到 MKDEV宏。函数 (device_create)还需要用到一个类这个类由 class_create函数生成;

MKDEV#define dev_t MKDEV(int major,int minor);
功能: 把 主设备号 major 和 次设备号minor整合成设备号并输出。
返回值: 返回一个16位的设备号,高八位为主设备号,第八位为次设备号。
 
类创建函数: struct class class_create(struct module *owner, const char *name);
参数说明:
              1. *owner 通常赋值 THIS_MODULE,表示这个驱动会生成一个单独的驱动模块;
              2. *name 驱动模块的名字,而不是驱动名字,这个参数可以随便起名,但是不能有重复。
功能说明: 生成一个类提供给 device_create以创建相对应的设备模块。

设备创建函数: struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, …)
参数说明:
              1. *class 设备模块的类;
              2. *parent 通常赋值 NULL
              3. devt 设备号,由MKDEV 获得;
              4. *drvdata 设备相关数据,通常赋值NULL
              5. *fmt设备名称。
函数功能: 在文件系统中创建一个设备;
       这些操作通常由一个函数来执行,是为了给module_init 函数对模块进行初始化。

int __init pin4_dev_init(void)
{
        int ret;
        devno =MKDEV(major,minor);
        ret=register_chrdev(major,module_name,&pin4_fops);
        pin4_class=class_create(THIS_MODULE,"myfirstdemo");
        pin4_dev =device_create(pin4_class,NULL,devno,NULL,module_name);
        return 0;
}
module_init(pin4_dev_init);

有驱动文件创建,当然也会有卸载,其中的函数就不过多做介绍:

void __exit pin4_exit(void)
{
        device_destroy(pin4_class,devno);//销毁设备
        class_destroy(pin4_class);		//销毁类
        unregister_chrdev(major,module_name);//在驱动链表中卸载驱动
}
module_exit(pin4_exit);

pin4_exit函数中那三个函数的顺序对应了创建的先后,后创建的先销毁。最后别忘了许可:

MODULE_LICENSE("GPL v2");

“GPL v2” 代表啥意思可以自行百度。

四、总结

       最后总结一下写驱动的基本框架:
       1. 构建驱动的 file_operations 结构体;
       2. 写一个初始化函数,里面包括了:
              MKDEV:生成16位设备号;
              register_chrdev():把驱动在链表中注册成字符设备;
              class_create():创建一个设备类;
              device_create():在文件系统中创建驱动。
       3. 初始化驱动模块:module_init();
       4. 写一个卸载驱动的函数,里面有:卸载驱动函数、卸载类函数和在链表中移除驱动函数;
       5. 驱动模块卸载:module_exit();
       6. 添加许可:MODULE_LICENSE(“GPL v2”);

  • 7
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 对于Linux底层驱动面试,以下是我个人的回答: 首先,Linux底层驱动是指与硬件设备交互的一组软件组件。它们允许操作系统与硬件之间的通信和交互。在Linux系统中,驱动程序通常作为内核模块加载到内核中,以支持设备的正常运行。 在面试中,考官可能会关注以下几个方面: 1. 驱动程序的开发 - 面试官可能会问您有关驱动程序开发的经验和技能。他们可能会要求您解释您在开发过程中使用的工具、技术和流程,并问您如何处理常见的驱动程序开发问题。 2. 设备和驱动程序的关系 - 面试官可能会问您对设备和驱动程序之间关系的理解。您需要解释设备如何与驱动程序进行通信以及操作系统如何使用驱动程序来访问设备的功能。 3. 内核模块的加载和卸载 - 面试官可能会问您有关内核模块加载和卸载的知识。您需要解释如何在Linux系统中加载和卸载驱动程序,并讨论在加载和卸载过程中可能遇到的问题和解决方法。 4. 设备树(DTS) - Linux系统中的设备树是描述硬件设备的数据结构。面试官可能会问您对设备树的理解以及如何在驱动程序中使用设备树来配置硬件设备。 5. 调试和故障排除 - 面试官可能会问您关于驱动程序调试和故障排除的问题。您需要解释您在调试和故障排除过程中使用的工具和技术,并描述一些常见的驱动程序问题及其解决方法。 总的来说,面试官希望了解您在Linux底层驱动开发方面的知识和经验。除了理论知识,他们可能还会考察您的实际操作和解决问题的能力。因此,在面试前应该准备并熟悉相关概念和技术,同时也要对常见的驱动程序开发问题进行思考和准备。 ### 回答2: Linux底层驱动面试涉及到Linux内核,对于驱动程序开发有一定的要求。以下是对Linux底层驱动面试的回答: 首先,Linux底层驱动面试通常会要求面试者对Linux内核的工作原理和架构有一定的了解。面试官可能会问及Linux内核的组成、内存管理、进程调度、文件系统等方面的问题,以检验面试者对于Linux内核的掌握程度。 其次,面试者应具备C语言编程技能和驱动开发经验。面试官可能会询问面试者在驱动开发方面的项目经历,对于驱动程序的编写、调试和优化等方面的经验和能力。 另外,面试者还需要了解Linux设备模型和驱动框架。Linux设备模型包括字符设备、块设备、网络设备等,面试者需要了解设备的注册、初始化、IO操作等流程。面试者还需要了解Linux驱动框架,如Platform驱动、PCI驱动、USB驱动等,面试者需要知道如何编写针对特定设备的驱动程序,并能够解释驱动程序的加载、绑定和解绑过程。 最后,面试者应具备问题解决能力和团队合作精神。Linux底层驱动开发涉及到复杂的问题和困难的调试过程,面试者需要展示自己解决问题的能力,并能够与团队合作进行系统的调试与优化。 综上所述,Linux底层驱动面试需要具备对Linux内核的深入理解、熟练的C语言编程和驱动开发经验、对Linux设备模型和驱动框架的熟悉,以及问题解决能力和团队合作精神。希望以上回答对您有所帮助。 ### 回答3: Linux底层驱动面试主要聚焦于以下几个方面: 首先,需要掌握Linux操作系统的基本原理和体系结构。这包括Linux内核的基本组成、内核模块以及驱动的加载和卸载机制等。熟悉Linux系统的启动流程以及内核的初始化过程也是必备的知识。 其次,熟悉Linux设备驱动的框架和模型。Linux的设备驱动模型将驱动程序分为字符设备、块设备和网络设备等类型,并提供了相应的框架和接口。面试中需要解释和展示对这些框架和接口的理解和使用经验。 接下来,需要了解如何编写和调试Linux驱动程序。这包括如何使用标准的Linux API来开发设备驱动、如何调试和优化驱动程序以及如何处理常见的错误和异常情况等。面试官可能会要求候选人描述自己的开发经验和解决问题的能力。 此外,对于特定设备的驱动开发经验也是面试中的加分项。例如,有经验开发网络驱动、存储设备驱动或声卡驱动等的候选人会更受面试官的关注。在回答问题时,可以结合自身的经验和项目来具体说明自己的能力和技术深度。 最后,沟通能力和团队合作精神也是Linux底层驱动面试中需要考察的因素。Linux驱动开发往往需要与硬件工程师、内核开发者和应用程序开发者进行密切合作。面试官可能会通过场景题或者工作经验来考察候选人的沟通和协作能力。 综上所述,Linux底层驱动面试需要候选人掌握Linux操作系统的基本原理和体系结构、熟悉设备驱动模型和编写调试驱动程序的方法、具备特定设备驱动开发经验、具备良好的沟通和团队合作能力。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值