linux驱动概念学习笔记

文章目录

  • 1, 什么是用户空间和内核空间?
  • 2, 为什么要区分用户空间和内核空间?
  • 3, 如何从用户空间进入内核空间?
  • 4, 设备号的具体意义是什么?
  • 5, printk对打印消息的分类有哪些?
  • 6, 如何修改printk的打印等级?
  • 7,linux内核编程中怎么创建线程?
  • 8,linux驱动开发中i2c的开发流程.
  • 9,怎么调试linux驱动程序?如调试i2c的驱动程序
  • 10,linux驱动代码中schedule()函数的作用
  • 11,字符设备驱动中的filp的私有数据的作用是什么?
  • 12,内核模块加载时怎么给它传递参数?
  • 13,驱动程序中休眠与阻塞的区别是什么?

1, 什么是用户空间和内核空间?

对 32 位操作系统而言, 一个进程的最大地址空间为 4G(虚拟地址空间,或叫线性地址空间).
操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。
具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。
针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。

2, 为什么要区分用户空间和内核空间?

在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。

3, 如何从用户空间进入内核空间?

所有的系统资源管理都是在内核空间中完成的。
比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。
应用程序是无法直接进行这样的操作的。
但是我们可以通过内核提供的接口来完成这样的任务。
比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用” 告诉内核:“我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。
具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以开开心心的往下执行了。
概括的说,有三种方式:系统调用、软中断和硬件中断。

4, 设备号的具体意义是什么?

一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。
主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的其他设备。(主设备号和控制这类设备的驱动是一一对应的)
通俗的说就是主设备号标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备(也就是/dev下的设备文件)服务;而次设备号则用来标识具体且唯一的某个设备
在同一个系统中,一类设备的主设备号是唯一的。比如:磁盘这类,次设备号只是在驱动程序内部使用,系统内核直接把次设备号传递给应用程序,由驱动程序管理。为了保证驱动程序的通用性,避免驱动程序移植过程中出现主设备号冲突,系统为设备编了号,每个设备号又分为主设备号和次设备号。
主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号。
注: 主设备号和次设备号,在不同磁盘的工具中都有输出显示,所以对于这个概念的理解还是很重要,否则你看不懂很多命令的输出。
尤其是 cat /proc/devices 输出的意义等

Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
_u32其实就是 unsigned int 类型,是一个 32 位的数据类型。
这 32 位的数据构, 成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。

 #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))

5, printk对打印消息的分类有哪些?

printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件

include/linux/kern_levels.h 里面
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用
KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */

6, 如何修改printk的打印等级?

/proc/sys/kernel/printk该文件有4个数字值,它们根据日志记录消息的重要性,定义将其发送到何处,上面显示的4个数据分别对应如下:

控制台日志级别:优先级高于该值得消息将被打印到到控制台;

默认的消息日志级别:将用该优先级来打印没有优先级的消息;

最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级);

默认的控制台日志级别:控制台日志级别的缺省值。

在终端修改打印等级:

# echo 7       4       1      7 > /proc/sys/kernel/printk

在内核修改打印等级:
在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:

#define CONSOLE_LOGLEVEL_DEFAULT 7

7,linux内核编程中怎么创建线程?

①kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);

#include 
/*************************************************************
*创建一个线程,并开始执行
*fn:线程函数地址
*arg:线程函数的形参,没有,可以是NULL
*flags:标志,一般用CLONE_KERNEL,(定义在linux/sched.h中,注意有的版本中,CLONE_FS | CLONE_FILES | CLONE_SIGHAND),其他标志及含义见uapi/linux/sched.h中
*pid_t:返回线程ID值
*************************************************************/
extern pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);

②pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);



#include 

struct task_struct *kthread_create_on_node(
                   int (*threadfn)(void *data), 
                   void *data,int node,
                   const char namefmt[], ...);

 /*************************************************************
*创建一个线程,线程没有立即运行,需要将返回的值,即线程指针(struct task_struct *),作为参数传入到wake_up_process()唤起线程运行。kthread_stop()停止线程
*threadfn:线程函数地址
*data:线程函数的形参,没有,可以是NULL
*namefmt,arg…:线程函数名字,可以格式化输出名字
*返回值(strcut task_struct *):线程指针
*************************************************************/

#define kthread_create(threadfn, data, namefmt, arg...) \
        kthread_create_on_node(threadfn,
                               data,
                               namefmt, ##arg)
                               
ex:

int my_kernel_thread(void *arg)
{
        int n = 0; 
        while(1){
            printk("%s: %d\n",__func__,n++);
            ssleep(3);
 
//kthread_should_stop判断线程是否应该结束,返回true表示结束,false表示不结束
	        if(kthread_should_stop()){
	             break;
	        }
        }
 
        return 0;
}
strcut task_struct *practice_task_p = kthread_create(my_kernel_thread,NULL,"practice task");
if(!IS_ERR(practice_task_p))
	wake_up_process(practice_task_p);//唤醒practice_task_p指向的线程
kthread_stop(practice_task_p);//停止线程,kthread_should_stop会做出响应

③kthread_run(threadfn, data, namefmt, …)


#include 
/**
 * kthread_run - 创建并唤醒线程
 * @threadfn: 线程函数地址
 * @data: 线程函数形参,没有可以制定为NULL
 * @namefmt: 线程名字,可以格式化输出
 *
 * return: 线程结构体指针(struct task_struct * )或者 ERR_PTR(-ENOMEM)
 */
#define kthread_run(threadfn, data, namefmt, ...)                          \
({                                                                         \
        struct task_struct *__k                                            \
                = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
        if (!IS_ERR(__k))                                                  \
                wake_up_process(__k);                                      \
        __k;                                                               \

8,linux驱动开发中i2c的开发流程.

1, 确定硬件连接: 确定12C总线的连接方式,包括总线上的设备地址、数据线和时钟线的引脚位置等。
2,确定驱动程序接口: 确定驱动程序与应用程序的接口,包括设备的读取和写入操作等3,编写驱动程序框架:编写驱动程序的框架,包括设备的初始化、读取和写入函数等。
4,实现设备的初始化: 实现设备的初始化函数,用于初始化设备并设置相关的寄存器等.
5,实现读取函数:实现设备的读取函数,用于从设备中读取教据
6,实现写入函数:实现设备的写入函数,用于向设备中写入教据
7,测试驱动程序:测试驱动程序的功能是否正常,包括设备的初始化、读取和写入操作等.8.调试驱动程序:如果测试过程中发现问题,可以使用调试工具进行调试,例如使用逻辑分析仪查看总线上的信号波形等。
9,集成驱动程序: 将驱动程序集成到系统中,确保它能够与其他驱动程序和应用程序协同工作
总的来说,12C驱动程序的开发需要深入了解硬件和软件的结合,以及驱动程序的接口和功能。在开发过程中,测试和调试是非常重要的环节,可以帮助确保驱动程序的正确性和稳定性

9,怎么调试linux驱动程序?如调试i2c的驱动程序

1,打印日志:在代码中插入打印语句,将关键信息输出到终端或日志文件中,以便查看和分析。
使用调试器: 使用调试器可以在运行时单步执行程序,观察变量的值和程序执行的流程,2.
定位问题。
3.逻辑分析仪:使用逻辑分析仪可以捕获和分析总线上的信号波形,查看时序和数据的正确性
4,外部工具:有些设备可能提供特定的调试工具或应用程序,例如性能分析工具、仿真器等,可以帮助诊断问题。5,测试用例: 编写测试用例对驱动程序进行测试,可以检测驱动程序的功能是否符合预期并发现潜在的问题。
回归测试: 在修改驱动程序后进行 stop generating6.收不会影响已经测试过的功能,避免引入新的问题.

10,linux驱动代码中schedule()函数的作用

当需要执行实际的调度时,直接调用 shedule(),进程就这样神奇地停止了,而另一个新的进程占据了 CPU. 主动发起进程调度.
在当前进程需要主动放弃cpu时调用, 一般这样调用:

set_current_state(TASK_INTERRUPTIBLE);
schedule();

在 schedle 函数的执行中,current 进程进入睡眠,而一个新的进程被运行,当下一次当前进程被唤醒并得到执行权时,又接着 schedule 后面的代码运行,非常简单的实现,完美地屏蔽了进程切换的内部实现,提供了最简单的接口

11,字符设备驱动中的filp的私有数据的作用是什么?

filp->private_data是void*类型的指针, 通常用于指向用户定义的设备结构体对象, 一般在open函数中指定 filp->private_data = user_devp; 随后在read/write函数中可以通过filp->private_data指针来获取到定义的设备对象

12,内核模块加载时怎么给它传递参数?

可以用“module_param(参数名,参数类型,参数读/ 写权限)”为模块定义一个参数

static char *book_name = "dissecting Linux Device Driver"; 
module_param(book_name, charp, S_IRUGO); 
static int book_num = 4000; 
module_param(book_num, int, S_IRUGO);

用户可以向模块传递参数,形式为“insmode(或modprobe)模块名参数名=参数值”,如果不传递,参数将使用模块内定义的缺省值。
参数数组,形式为“module_param_array(数组名,数组类型,数组长,参数读/写权限)"运行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。

13,驱动程序中休眠与阻塞的区别是什么?

睡眠是程序主动放弃时间片竞争,例如程序调用sleep
阻塞是程序的I/O请求得不到满足,必须等待别的进程释放资源才能继续,属于被动无法得到时间片

你可能感兴趣的:(嵌入式,linux驱动,arm开发,驱动开发,arm)