linux驱动概念学习笔记
迪丽瓦拉
2024-06-02 18:04:11
0

文章目录

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

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的开发流程.

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

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

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

set_current_state(TASK_INTERRUPTIBLE);
schedule();

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

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

相关内容