Linux驱动
并发与竞争
并发:
并发的意思是 CPU 在同一时间内只能执行一个任务,但是通过将CPU的使用权分配给不同的任务,CPU 的执行速度极快,多任务切换的时间也极短,所以多个任务看起来是一起执的。

并行:
并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的,多核 CPU 可以真正实现同时执行多个任务,比起单核 CPU 执行效率更高。

在实际场景中,处于运行状态的任务是非常多的,比如 windows 系统在开机之后会运行几十个任务,而CPU 往往只有 4 核、8 核等,远远低于任务的数量,这个时候就会同时存在并发和并行两种情况
竞争:
并发可能会造成多个程序同时访问一个共享资源,这种情况就叫做竞争。
所以我们要对共享资源进行保护,常见的方法有原子操作,自旋锁,信号量,互斥量。
原子操作
原子操作可以理解为“不可被拆分的操作”,就是不能被更高优先级的任务中断。所以能确保在一个操作执行期间不会被其他线程或进程中断,从而避免数据竞争。
但是原子操作只能对整形变量或者位进行保护,而对于结构体或者其他类型的共享资源,就要用其他方法,比如可以用自旋锁,信号量
自旋锁
自旋锁是一种基于忙等待的锁机制。当一个线程尝试获取自旋锁时,如果锁已经被其他线程占用,它不会进入休眠状态,而是不断地循环检查锁是否被释放。自旋锁适用于锁持有时间非常短的场景,如果锁的持有时间较长,自旋锁会浪费大量的CPU资源,导致系统性能下降
所以占用资源比较久的话我们可以使用互斥量或信号量。
信号量
信号量会使等待的线程进入休眠状态,适用于那些占用资源比较久的场合,通过 PV 操作来控制进程或线程的执行顺序。
互斥锁
互斥锁是一种用于保护共享资源的锁机制。它确保同一时间只有一个线程可以持有该锁,从而保证对共享资源的互斥访问。当一个线程尝试获取互斥锁时,如果锁已经被其他线程占用,它会进入休眠状态,等待锁被释放。一旦锁被释放,线程会被唤醒并尝试获取锁。
Linux系统启动过程
一个完整的嵌入式 Linux 系统,包含 bootloader ,Linux内核,根文件系统三个部分。在系统刚一上电的时候,先执行uboot,由uboot引导内核,内核启动成功之后挂载根文件系统。根文件系统挂载成功以后,嵌入式 Linux 系统就启动成功了。在开发板上看到的现象是:用户可以在串口控制台上输入命令与Linux系统进行交互。

blootloader
Linux系统启动以后会先运行一段 bootloader 程序,常用的就是 uboot。
uboot
uboot 的主要作用是引导加载程序,负责初始化各种硬件,加载内核镜像和设备树文件,然后通过启动命令启动内核,并将设备树在内存中的地址传给内核。
Linux内核
Linux 内核启动后解析设备树地址,根据设备树中描述的设备信息初始化设备,然后挂载根文件系统。
根文件系统
根文件系统是 Linux 内核启动时所挂载的第一个文件系统,里面包含了 Linux 系统运行所必需的应用程序和库等
驱动开发
Linux嵌入式驱动开发的流程
1. 了解硬件设备及其规范:首先要对目标硬件设备进行研究,包括芯片型号、外设接口、寄存器规范等。同时,对于设备的功能和特性也需要有基本的了解。
2. 编写设备树(Device Tree)描述文件:Linux内核使用设备树来描述硬件设备的信息。需要编写设备树描述文件,以便内核能够识别和配置硬件设备。
3. 编写驱动程序源码:根据设备的规格和需求,编写对应的驱动程序源码。通常需要涉及到底层寄存器的读写、中断处理、设备初始化和资源分配等操作。
4. 将驱动程序源码添加到内核源码树:将驱动程序源码添加到Linux内核源码树,并在内核配置选项中选择该驱动模块进行编译。
5. 构建并刷写内核镜像:完成驱动程序源码的添加和内核配置后,进行内核的构建。通过编译得到的内核镜像可以刷写到目标嵌入式设备上。
6. 调试和测试:将构建好的内核镜像刷写到目标设备,并进行调试和测试。检查设备与驱动之间的通信,确保驱动程序能够正确地初始化设备并提供所需的功能。
7. 优化和性能测试:根据实际使用情况对驱动程序进行优化,并进行性能测试。通过性能测试来评估驱动程序的性能,并进行必要的调整和优化。
SDK
主要包括:
1.内核源码
SDK通常包含Linux内核的源码,用于编译和定制内核以适配开发板的硬件。
例如,RK3568开发板的SDK中包含Linux 5.10版本的内核源码。
2.Bootloader
包含用于启动开发板的Bootloader(如U-Boot)的源码。
例如,RK3568开发板的SDK中包含基于U-Boot v2017.09版本的源码。
3.根文件系统
SDK提供用于构建根文件系统的工具和脚本,支持多种Linux发行版(如Debian、Ubuntu)。
例如,LubanCat-RK系列开发板的SDK支持构建Debian 11、Debian 12、Ubuntu 20.04和Ubuntu22.04的根文件系统。
4.交叉编译工具链
SDK包含交叉编译工具链,用于在宿主机上编译目标板的代码。
例如,RK3568开发板的SDK中包含预编译的交叉编译工具链。
5.开发工具和脚本
提供编译、打包和烧录工具,如 build.sh 、 mkfirmware.sh 和 rkflash.sh 。
这些工具简化了从源码编译到系统部署的整个过程。
6.硬件驱动和中间件
SDK包含开发板硬件的驱动程序,以及中间件(如网络协议栈、文件系统等)。
例如,NXP i.MX6ULL开发板的SDK包含用于片上外设的驱动示例。
7.示例代码和文档
提供示例代码和开发文档,帮助开发者快速上手。
例如,SDK中可能包含“Hello World”程序、外设驱动示例、网络通信示例等。
8.固件和输出目录
SDK包含用于存放编译输出的固件和镜像文件的目录。
例如, rockdev 目录用于存放编译后的固件。
9.其他资源
SDK可能还包含其他资源,如开发板的配置文件、环境脚本(如 envsetup.sh )和额外的工具(如Dockerfile、OTA工具等)。
字符设备驱动
编写驱动主要为以下七个步骤:
1. 确定主设备号,可以让内核分配,也可以调用 register_chrdev 函数让内核自动分配,返回值为major
2. 定义自己的 file_operations 结构体
3. 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
4. 把 file_operations 结构体告诉内核: register_chrdev
5. 谁来注册驱动程序啊? 得有一个入口函数: register_chrdev 安装驱动程序时,就会去调用这个入口函数
6. 有入口函数就应该有出口函数: 卸载驱动程序时,出口函数调用 unregister_chrdev
7. 其他完善:提供设备信息,自动创建设备节点: class_create , device_create
内核与用户空间主要用两个函数进行数据交互:
copy_to_user() 用于将内核数据传送给用户空间;
copy_from_user() 用于将用户空间的数据传送给内核空间。
总线设备驱动模型
在Linux内核系统中注册一个设备的时候,会寻找与之对应驱动进行匹配;相反地,系统中注册一个驱动的时候,会去寻找一个对应的设备进行匹配。匹配的的工作由总线来完成。
在Linux设备中有的是没有对应的物理总线的,但为了适配Linux的总线模型,内核针对这种没有物理总线的设备开发了一种虚拟总线——platform总线。
将设备和驱动独立开,驱动尽可能写的通用,当来了一个类似的设备后也可以使用这个驱动,让驱动程序可以重用。这体现了Linux驱动的软件架构设计的思想。
按照这个思路,Linux中的设备和驱动都需要挂接在一种总线上,比如i2c总线上的eeprom设备,
eeprom的驱动都挂接在i2c驱动上,spi总线上的dac设备,dac的驱动挂接在spi驱动上。但是在嵌入式系统中,soc系统一般都会集成独立的i2c控制器,控制器也是需要驱动的,但是再按照设备-总线-驱动模型进行设计,就会发现无法找到一个合适总线去挂接控制器设备和控制器驱动了(i2c控制器是挂接在
CPU内部的总线上,而不是i2c总线),所以Linux发明了一种虚拟总线,称为platform总线,相应的设备称为platform_device(控制器设备),对应的驱动为 platform_driver(控制器驱动),用platform总线来承载这些相对特殊的系统。
platform总线的匹配规则是什么,在具体应用上要不要先注册驱动再注册设备,有先后顺序没
总线,设备,驱动。匹配规则就是当有一个新的设备挂起时,总线被唤醒,匹配函数(match)被调用,用 device 名字去跟本总线下的所有驱动名字去比较。相反就是用驱动的名字去device链表中和所有device 的名字比较。如果匹配上,才会调用驱动中的probe函数,否则不调用。至于先后顺序,我觉得不会有影响,不管谁先谁后,平台总线(bus)都会完成匹配工作。
谈谈对Linux设备驱动模型的认识
设备驱动模型的出现主要有三个好处:
设备与驱动分离,驱动可移植性增强;
设备驱动抽象结构以总线结构表示看起来更加清晰明了,谁是属于哪一条bus的;
最后,就是大家最熟悉的热插拔了,设备与驱动分离, 很好的奠定了热插拔机制。
设备树
以LED驱动为例,如果你要更换LED所用的GPIO引脚,需要修改驱动程序源码、重新编译驱动、重新加载驱动。
在内核中,使用同一个芯片的板子,它们所用的外设资源不一样,比如A板用GPIO A,B板用GPIO B。而GPIO的驱动程序既支持GPIO A也支持GPIO B,你需要指定使用哪一个引脚,怎么指定?在c代码中指定。
随着ARM芯片的流行,内核中针对这些ARM板保存有大量的、没有技术含量的文件。
于是,Linux内核开始引入设备树。设备树并不是重新发明出来的,在Linux内核中其他平台如PowerPC,早就使用设备树来描述硬件了。
有一种错误的观点,说“新驱动都是用设备树来写了”。设备树不可能用来写驱动。
请想想,要操作硬件就需要去操作复杂的寄存器,如果设备树可以操作寄存器,那么它就是“驱动”,它就一样很复杂。
设备树只是用来给内核里的驱动程序,指定硬件的信息。比如LED驱动,在内核的驱动程序里去操作寄存器,但是操作哪一个引脚?这由设备树指定。
DTS、DTSI、DTB 和 DTC介绍:
DTS(Device Tree Source):DTS 是设备树的源文件。
DTSI(Device Tree Source Include):DTSI 文件是设备树源文件的包含文件。
DTB(Device Tree Blob):DTB 是设备树的二进制表示形式。
DTC(Device Tree Compiler):DTC 是设备树的编译器。
dts文件包含dtsi文件
设备树文件不需要我们从零写出来,内核支持了某款芯片比如imx6ull,在内核的 arch/arm/boot/dts
目录下就有了能用的设备树模板,一般命名为xxxx.dtsi。“i”表示“include”,被别的文件引用的。
我们使用某款芯片制作出了自己的单板,所用资源跟xxxx.dtsi是大部分相同,小部分不同,所以需要引脚xxxx.dtsi并修改。
dtsi文件跟dts文件的语法是完全一样的。
dts中可以包含.h头文件,也可以包含dtsi文件,在.h头文件中可以定义一些宏。
流程:
1. 编写设备树
2. 编译设备树
3. 替换设备树
4. 内核对设备树处理过程
5. 设备和驱动如何匹配
编写设备树
进入目录:
cd kernel/arch/arm/boot/dts
vi 100ask_imx6ull-14x14.dts
添加设备树节点:
compatible的值,建议取这样的形式:“manufacturer,model”,即“厂家名,模块名”
status:

GPIO:
sr04 {
compatible = "zgl,sr04";
gpios = <&gpio4 19 GPIO_ACTIVE_HIGH>,
<&gpio4 20 GPIO_ACTIVE_HIGH>;
};
irda {
status = "okay";
compatible = "zgl,irda";
gpios = <&gpio4 21 GPIO_ACTIVE_HIGH>;
};
mydht11 {
status = "okay";
compatible = "zgl,dht11";
gpios = <&gpio4 22 GPIO_ACTIVE_HIGH>;
};
sr501 {
status = "okay";
compatible = "zgl,sr501";
gpios = <&gpio4 23 GPIO_ACTIVE_HIGH>;
};

I2C:
reg用来访问寄存器地址:查手册,地址为 0x50 和 0x1e

SPI:
通过查看手册,确认SPI时钟最大频率:

编译设备树
设置ARCH、CROSS_COMPILE、PATH这三个环境变量后,进入ubuntu上板子内核源码的目录,执行如下命令即可编译dtb文件:
ARCH :用于指定目标程序的硬件架构。
CROSS_COMPILE :用于指定交叉编译工具链的前缀,用于生成跨架构的程序。
PATH :用于定义系统查找可执行文件的路径
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-
gnueabihf_sdk-buildroot/bin
cd Linux-4.9.88
make dtbs
替换设备树
将 dts 文件编译后生成的的二进制 dtb 文件放到开发板 boot 目录下:
cp ~/100ask_imx6ull-sdk/Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dtb
~/nfs_rootfs/
cp 100ask_imx6ull-14x14.dtb /boot
内核对设备设备树处理过程
1. dts在PC机上被编译为dtb文件
2. u-boot把dtb文件传给内核
3. 内核解析dtb文件,把每一个节点都转换为device_node结构体;
4. 对于某些device_node结构体,会被转换为platform_device结构体。
哪些设备树节点会被转换为platform_device
根节点下含有compatile属性的子节点
含有特定compatile属性的节点的子节点
如果一个节点的compatile属性,它的值是这4者之一:“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”, 那么它的子结点(需含compatile属性)也可以转换为platform_device。
总线I2C、SPI节点下的子节点:不转换为platform_device
示例:
{
mytest {
compatile = "mytest", "simple-bus";
mytest@0 {
compatile = "mytest_0";
};
};
i2c {
compatile = "samsung,i2c";
at24c02 {
compatile = "at24c02";
};
};
spi {
compatile = "samsung,spi";
flash@0 {
compatible = "winbond,w25q32dw";
spi-max-frequency = <25000000>;
reg = <0>;
};
};
};
/mytest会被转换为platform_device, 因为它兼容"simple-bus"; 它的子节点/mytest/mytest@0 也会被转换为platform_device
/i2c节点一般表示i2c控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
/i2c/at24c02节点不会被转换为platform_device, 它被如何处理完全由父节点的platform_driver决定, 一般是被创建为一个i2c_client。
类似的也有/spi节点, 它一般也是用来表示SPI控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
/spi/flash@0节点不会被转换为platform_device, 它被如何处理完全由父节点的platform_driver决定, 一般是被创建为一个spi_device。
设备和驱动如何匹配
从设备树转换得来的platform_device会被注册进内核里,以后当我们每注册一个platform_driver时,它们就会两两确定能否配对,如果能配对成功就调用platform_driver的probe函数。
四种方法:
1. 最先比较:平台设备是否强制选择某个平台驱动,可以设置 platform_device 的
driver_override ,强制选择某个 platform_driver 。
2. 然后比较:设备树信息中的 compatible 是否和平台驱动的 compatible 匹配,由设备树节点转换得来的 platform_device 中,含有一个结构体:of_node,类型如下:

如果一个 platform_driver 支持设备树,它的 platform_driver.driver.of_match_table
是一个数组,类型如下:

使用设备树信息来判断dev和drv是否配对时:
首先,如果of_match_table中含有compatible值,就跟dev的compatile属性比较,若一致则成功,否则返回失败;
其次,如果of_match_table中含有type值,就跟dev的device_type属性比较,若一致则成功,否则返回失败;
最后,如果of_match_table中含有name值,就跟dev的name属性比较,若一致则成
功,否则返回失败。
注意:设备树中建议不再使用devcie_type和name属性,所以基本上只使用设备节点的compatible属性来寻找匹配的platform_driver。
3. 接下来比较:平台设备和平台驱动的id是否匹配 platform_device_id
比较platform_device. name和platform_driver.id_table[i].name,id_table中可能有多项。
latform_driver.id_table是“platform_device_id”指针,表示该drv支持若干个device,它里面列出了各个device的{.name, .driver_data},其中的“name”表示该drv支持的设备的名字,
driver_data是些提供给该device的私有数据。
4. 最后比较:平台设备和平台驱动的名字是否匹配, platform_device.name 和
platform_driver.driver.name
platform_driver.id_table可能为空,这时可以根据 platform_driver.driver.name 来寻找
同名的 platform_device 。

图解:

I2C驱动

APP通过I2C Controller与I2C Device传输数据,使用一句话概括I2C传输:
使用一句话概括I2C传输:
APP通过I2C Controller与I2C Device传输数据
APP通过i2c_adapter与i2c_client传输i2c_msg
内核函数i2c_transfer
i2c_msg里含有addr,所以这个函数里不需要i2c_client
SPI驱动
SPI控制器驱动程序
SPI 控制器的驱动程序可以基于"平台总线设备驱动"模型来实现:
在设备树里描述 SPI 控制器的硬件信息,在设备树子节点里描述挂在下面的SPI设备的信息
在platform_driver中提供一个probe函数
它会注册一个spi_master
还会解析设备树子节点,创建spi_device结构体
SPI设备驱动程序
跟"平台总线设备驱动模型"类似,Linux中也有一个"SPI总线设备驱动模型":
左边是spi_driver,使用C文件实现,里面有id_table表示能支持哪些 SPI 设备,有probe函数
右边是spi_device,用来描述SPI设备,比如它的片选引脚、频率
可以来自设备树:比如由SPI控制器驱动程序解析设备树后创建、注册spi_device
可以来自C文件:比如使用spi_register_board_info创建、注册spi_device
Linux内核
内核镜像格式有几种,分别有什么区别
1. uboot经过编译直接生成的elf格式的可执行程序是u-boot,这个程序类似于windows下的exe格
式,在操作系统下是可以直接执行的。但是这种格式不能用来烧录下载。我们用来烧录下载的是u-
boot.bin,这个东西是由u-boot使用arm-linux-objcopy工具进行加工(主要目的是去掉一些无用
的)得到的。这个u-boot.bin就叫镜像(image),镜像就是用来烧录到iNand中执行的。
2. linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的
未经任何处理加工的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个 vmlinuz/vmlinux,
而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核 没有.bin后缀),经
过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像
主要目的就是缩减大小,节省磁盘)。
3. 原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage。(因为当年Image大小刚好比一张软盘(软盘有2种,1.2M的和1.44MB两种)大,为了节省1张软盘的钱于是乎设计了这种压缩Image成zImage的技术)。
4. uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,
uboot中有一个工具,可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux
内核只管生成zImage即可,然后uboot中的mkimage工具再去由zImage加工生成uImage来给
uboot启动。这个加工过程其实就是在zImage前面加上64字节的uImage的头信息即可。
5. 原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot中也可以支持zImage,
是否支持就看x210_sd.h中是否定义了LINUX_ZIMAGE_MAGIC这个宏。所以大家可以看出:有些
uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动。
6. 如果直接在kernel底下去make uImage会提供mkimage command not found。解决方案是去
uboot/tools下cp mkimage /usr/local/bin/,复制mkimage工具到系统目录下。再去make
uImage即可。
内核中申请内存有几个函数,有什么区别
kmalloc
void *kmalloc(size_t size, gfp_t flags)
kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函数来实现。kmalloc一次最多能申请的内存大小由include/linux/Kmalloc_size.h的内容来决定,在默认的2.6.18内核版本中,kmalloc一次最多能申请大小为131702B也就是128KB字节的连续物理内存。测试结果表明,如果试图用kmalloc函数分配大于128KB的内存,编译不能通过
vmalloc
void *vmalloc(unsigned long size)
前面几种内存分配方式都是物理连续的,能保证较低的平均访问时间。但是在某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存。 vmalloc对一次能分配的内存大小没有明确限制。出于性能考虑,应谨慎使用vmalloc函数。在 测试过程中,最大能一次分配1GB的空间。
ioremap
void * ioremap (unsigned long offset, unsigned long size)
ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段物理地址映射到内核地址空间。ioremap用到的物理地址空间都是事先确定的,和上面的几种内存分配方式并不太一样,并不是分配一段新的物理内存。ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间。ioremap能映射的内存由原有的物理内存空间决定,所以没有进行测试。
什么是内核空间,用户空间
对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程的最大地址空间为 4G。
操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。
具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。
简单来说就是:每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。换句话说就是, 最高 1G 的内核空间是被所有进程共享的!
下图描述了每个进程 4G 地址空间的分配情况:

为什么需要区分内核空间与用户空间
在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3。
其实 Linux 系统只使用了 Ring0 和 Ring3 两个运行级别(Windows 系统也是一样的)。当进程运行在Ring3 级别时被称为运行在用户态,而运行在 Ring0 级别时被称为运行在内核态。
什么是内核态和用户态用户空间与内核通信方式有哪些
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
对于以前的 DOS 操作系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为所有的代码都是运行在内核态的,因而,用户编写的应用程序代码可以很容易的让操作系统崩溃掉。
对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码。即便是单个应用程序出现错误,也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行(Linux 可是个多任务系统啊!)。所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。
总结:
用户空间与内核空间:
用户空间是用户程序的运行空间,内核空间是内核代码的运行空间。
用户空间和内核空间是隔离的,用户程序无法直接访问内核空间。
用户态与内核态:
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时就处于用户态。
用户态和内核态之间的切换是通过系统调用、中断或异常处理完成的。
用户空间与内核通信方式有哪些
1.使用API
get_user(x,ptr) //在内核中被调用,获取用户空间指定地址的数值并保存到内核变量x中。
put_user(x,ptr) //在内核中被调用,将内核空间的变量x的数值保存到到用户空间指定地址处。
Copy_from_user()/copy_to_user() //主要应用于设备驱动读写函数中,通过系统调用触发。
2.使用proc文件系统
和sysfs文件系统类似,也可以作为内核空间和用户空间交互的手段。/proc 文件系统是一种虚拟文 件系统,通过他可以作为一种linux内核空间和用户空间的。与普通文件不同,这里的虚拟文件的内 容都是动态创建的。使用/proc文件系统的方式很简单。调用create_proc_entry,返回一个 proc_dir_entry指针,然后去填充这个指针指向的结构。
3.使用sysfs文件系统+kobject
每个在内核中注册的kobject都对应着sysfs系统中的一个目录。可以通过读取根目录下的sys目录中的文件来获得相应的信息。除了sysfs文件系统和proc文件系统之外,一些其他的虚拟文件系统也能同样达到这个效果。
4.使用mmap系统调用
可以将内核空间的地址映射到用户空间。在以前做嵌入式的时候用到几次。一方面可以在driver中 修改Struct file_operations结构中的mmap函数指针来重新实现一个文件对应的映射操作。另一方 面,也可以直接打开/dev/mem文件,把物理内存中的某一页映射到进程空间中的地址上。
其实,除了重写Struct file_operations中mmap函数,我们还可以重写其他的方法如ioctl等,来达 到驱动内核空间和用户空间通信的方式。
5.信号
从内核空间向进程发送信号。这个倒是经常遇到,用户程序出现重大错误,内核发送信号杀死相应进程。
设备驱动
请简述主设备号和次设备号的用途
主设备号:主设备号标识设备对应的特定的驱动程序。
次设备号:次设备号由内核使用,用于确定由主设备号对应驱动程序中的各个设备。
可以通过 mknod 命令创建对应带有主设备号和次设备号的设备文件
字符驱动设备怎么创建设备文件
1.手动创建
mknod /dev/led c 250 0 ,其中dev/led 为设备节点 ,c 代表字符设备, 250代表主设备号, 0代表次设备号。
2.自动创建
驱动程序在入口函数里面调用字符设备注册函数(register_chrdev),创建类函数(class_create) 和创建设备函数 (device_create) ,编译运行通过后放在开发板上加载驱动,会自动创建设备,设备放在对应创建的类下面。
驱动中操作物理绝对地址为什么要先ioremap
ioremp是内核中用来将外设寄存器物理地址映射到主存上去的接口,即将io地址空间映射到虚拟地址空间上去,便于操作。为什么非要映射呢,因为保护模式下的cpu只认虚拟地址,不认物理地址,给它物理地址它并不帮你做事,所以你要操作外设上的寄存器必须先映射到虚拟内存空间,拿着虚拟地址去跟cpu 对接,从而操作寄存器。
insmod,rmmod一个驱动模块,会执行模块中的那个函数,在设计上需要注意哪些问题
分别会执行模块初始化函数 (module_init()) 和模块退出函数 (module_exit())。一般在模块初始化函数里面执行设备注册函数,在模块退出函数里面执行设备反注册函数。
Linux系统驱动划分
按设备类型分类
字符设备(Character Devices)
// 特点:以字节流形式访问,通常不支持随机访问
#include <linux/fs.h>
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.read = chrdev_read,.write = chrdev_write,
.open = chrdev_open,
.release = chrdev_release,
};
// 常见字符设备示例:
- 串口 (ttyS0, ttyUSB0)
- 键盘、鼠标 (input事件)
- 打印机 (lp0)
- 随机数生成器 (random, urandom)
- GPIO控制
- I2C设备(传感器等)
块设备(Block Devices)
// 特点:以数据块为单位访问,支持随机访问
#include <linux/blkdev.h>
static struct block_device_operations blkdev_fops = {
.owner = THIS_MODULE,
.open = blkdev_open,
.release = blkdev_release,
.ioctl = blkdev_ioctl,
};
// 常见块设备示例:
- 硬盘 (/dev/sda, /dev/sdb)
- SSD设备
- U盘、SD卡
- 光盘驱动器
- 虚拟块设备 (loop设备)
网络设备(Network Devices)
// 特点:面向数据包,不依赖于文件系统
#include <linux/netdevice.h>
static const struct net_device_ops netdev_ops = {
.ndo_open = netdev_open,
.ndo_stop = netdev_stop,
.ndo_start_xmit = netdev_xmit,
.ndo_get_stats = netdev_get_stats,
};
// 常见网络设备示例:
- 以太网卡 (eth0, enp0s3)
- 无线网卡 (wlan0, wlp2s0)
- 虚拟网卡 (tap0, tun0)
- 回环设备 (lo)
按加载方式分类
内置驱动(Built-in)
# 在内核配置中编译为内核一部分
CONFIG_DRIVER_NAME=y
# 优点:启动速度快,可靠性高
# 缺点:增加内核大小,修改需要重新编译内核
可加载模块(Loadable Modules)
# 动态加载和卸载
sudo insmod driver.ko # 加载驱动
sudo rmmod driver # 卸载驱动
sudo modprobe driver # 智能加载(解决依赖)
# 查看已加载模块
lsmod
按硬件总线类型分类
PCI/PCIe设备驱动
// PCI设备驱动结构
static struct pci_device_id pci_ids[] = {
{ PCI_DEVICE(VENDOR_ID, DEVICE_ID) },
{ 0 }
};
static struct pci_driver pci_driver = {
.name = "pci_example",
.id_table = pci_ids,
.probe = pci_probe,
.remove = pci_remove,
};
USB设备驱动
// USB设备驱动结构
static struct usb_device_id usb_ids[] = {
{ USB_DEVICE(VENDOR_ID, PRODUCT_ID) },
{ }
};
static struct usb_driver usb_driver = {
.name = "usb_example",
.id_table = usb_ids,
.probe = usb_probe,
.disconnect = usb_disconnect,
};
I2C设备驱动
// I2C设备驱动
static const struct i2c_device_id i2c_ids[] = {
{ "i2c_device", 0 },
{ }
};
static struct i2c_driver i2c_driver = {
.driver = {
.name = "i2c_example",
},
.probe = i2c_probe,
.remove = i2c_remove,
.id_table = i2c_ids,
};
SPI设备驱动
// SPI设备驱动
static struct spi_driver spi_driver = {
.driver = {
.name = "spi_example",
},
.probe = spi_probe,
.remove = spi_remove,
}
平台设备驱动(Platform)
// 用于片上系统(SoC)集成设备
static struct platform_driver platform_driver = {
.driver = {
.name = "platform_example",
.of_match_table = of_match_table,
},
.probe = platform_probe,
.remove = platform_remove,
};
按功能领域分类
输入子系统驱动(Input)
// 输入设备驱动(键盘、鼠标、触摸屏)
#include <linux/input.h>、
static struct input_dev *input_dev;
input_dev = input_allocate_device();
input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REL);
帧缓冲驱动(Framebuffer)
// 显示设备驱动
#include <linux/fb.h>
static struct fb_ops fb_ops = {
.owner = THIS_MODULE,
.fb_set_par = fb_set_par,
.fb_blank = fb_blank,
};
声音驱动(ALSA)
// 音频设备驱动
#include <sound/core.h>
#include <sound/pcm.h>
static struct snd_pcm_ops pcm_ops = {
.open = pcm_open,
.close = pcm_close,
.ioctl = pcm_ioctl,
};
多媒体驱动(V4L2)
// 摄像头、视频设备
#include <linux/videodev2.h>
static struct v4l2_file_operations v4l2_fops = {
.owner = THIS_MODULE,
.open = v4l2_open,
.release = v4l2_release,
.unlocked_ioctl = v4l2_ioctl,
};
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)