96SEO 2026-02-19 19:22 16
Linux下一切皆文件使用open系统调用打开文件时会得到一个文件描述符也被叫做文件句柄。

如上图所示在打开该文件进程的PCB中有一个文件描述符表的指针struct
files该指针指向属于该进程的文件描述符表本质上就是一个数组所谓文件句柄就是该数组的下标每打开一个文件就在该数组中放入这个文件的struct
file结构体的定义在使用open时传入的flags、mode等参数都会被记录在这个结构体中在读写文件时文件的当前偏移地址也会保存在f_pos成员里。
字符设备节点是一种特殊类型的文件用于表示字符设备。
这些设备通常以字符为单位进行数据的输入和输出例如键盘或者串口。
如上图所示当应用层使用open打开字符设备节点时在内核中会创建一个struct
file结构体并且将传入的参数记录到该结构体中而且会使用file_operations*
当应用层使用read/write函数进行读写时也会使用file_operations*
f_op结构体成员中的read/write函数指针来实现读写目的这个结构体是由字符设备驱动程序提供的。
如上如所示file_operations结构体的部分定义其中有read和write以及open等函数指针当应用层使用相应的open/write/read系统调用接口时最终会调用内核层中该结构体里对应的函数指针来实现目的。
如上图所示驱动程序的目的就是要在应用层调用open/write/read等系统调用接口时在内核层中调用file_operations里的open/write/read函数指针而函数指针指向的drv_open/drv_read/drv_write等驱动层函数是由我们自己定义的。
在驱动程序中实现硬件的初始化以及数据读写。
前面本喵说过file_operations结构体是由驱动程序提供的而驱动程序又是我们写的所以我们首先要做的就是定义自己的file_operations
如上图所示在hello_drv.c源文件中定义file_operations结构体变量并且进行初始化给函数指针赋值相应的函数。
实现对应的drv_open/drv_read/drv_write等函数。
如上图所示驱动函数的定义由于现在讲解的是框架所以本喵在函数里没有写任何操作只是使用printk打印一些调试信息。
内核中打印调试信息只能使用printk不能使用printf。
使用命令行指令dmesg就能看到日志中的调试信息。
每一个字符设备都有一个主设备号用于标识设备的类型或者设备驱动程序。
不同类型的设备或不同的驱动程序会有不同的主设备号。
例如所有的串口设备可能共享一个主设备号而所有的打印机设备可能又共享另一个不同的主设备号。
建议让内核去分配主设备号因为我们并不是很清楚有哪些主设备号自己决定的是否已经被使用。
0定义一个全局变量major来表示主设备号暂时先给它赋值为0。
暂时可以认为在内核中有一个chardevs[]数组该数组中存放的是字符设备节点的主设备号当使用某一类字符设备时会从该数组中寻找对应设备的file_operations结构体对象。
所谓注册就是将我们自己的字符设备主设备号注册到这个数组中使用register_chrdev函数来实现
第一个参数是主设备号如果传入的是0则返回内核自动分配的主设备号。
第二个参数是字符设备的名称是一个字符串。
第三个参数是我们提供的file_operations结构体指针。
调用该函数后主设备号和file_operations以及设备名称就绑定在了一起而且主设备号放入到了chardevs[]数组中。
将file_operations注册到内核中是由入口函数完成的入口函数使用宏__init修饰
如上图所示在安装驱动程序的时候内核会自动去调用这个hello_init函数在该函数中完成
注册file_operations结构体到内核中并得到主设备号。
创建设备信息类使用class_create实现该类中包含内核需要的设备节点信息更方便内核去创建节点。
创建设备节点使用device_create实现此时在内核中会生成一个/dev/hello路径用来表示节点设备。
如上图所示函数在卸载时会由内核自动调用都要卸载了就要将前面注册到内核中的字符设备移除使用unregister_chrdev实现并且将前面创建的字符设备类和设备节点都销毁使用class_destroy和device_destroy实现。
module_exit(hello_exit);MODULE_LICENSE(GPL);使用module_init告诉内核hello_init函数是入口函数使用module_exit告诉内核hello_exit是出口函数。
使用MODULE_LICENSE表明遵循GPL协议否则是无法使用我们的驱动程序的。
至此已经实现了一个驱动程序框架在命名上以hello为例这个可以作为一个模板在使用的时候只需要将hello改为相应的设备名字即可然后再在我们自己实现的驱动函数中增加一些具体的代码。
abc将abc字符串写入内核缓冲区中。
命令行输入./hello_drv_test
如上图所示代码只需要实现file_operations结构体中的read和write方法也就是对应的hello_drv_read和hello_drv_wite函数其他的没有用到。
用户层的缓冲区不能使用strcpy等应用层函数直接操作而是必须要使用内核提供的复制函数
copy_to_user从内核缓冲区复制数据到用户缓冲区第一个参数是目的buf第二个参数是源kernel_buffer第三个参数是要复制的字节数。
copy_from_user从用户缓冲区复制数据到内核缓冲区参数参考上面。
由于定义的缓冲区大小是1024防止越界使用宏MIN将1024和用户层指定的数据大小size作比较取较小值作为复制数据的大小。
如上图所示使用命令行参数传入-w-r以及要写入的字符串在main函数中
先打开/dev/hello目录下的字符设备节点在应用层看来这就是一个普通文件。
根据命令行中的第二个参数判断
-w使用write将第三个参数的字符串写入到内核缓冲区中。
-r使用read将内核缓冲区的数据读出来。
这里应用层的wite最终会调用驱动层中的hello_drv_write应用层的read最终会调用驱动层中的hello_drv_read。
如上图所示在命令行中输入上面的三条指令设置环境变量从而实现交叉编译环境的配置。
如上图所示使用该Makefile文件来编译驱动文件hello_drv.c和应用层测试文件hello_drv_test.c暂时不用这些指令是什么意思直接用就星。
如上图所示生成hello_drv.ko和hello_drv_test两个文件
.ko后缀表示这是一个内核模块用于在运行时向内核动态添加功能而不需要重新编译整个内核。
如上图将生成的hello_drv.ko驱动模块文件和hello_drv_test测试可执行程序复制到nfs_rootfs文件下。
192.168.5.11:/home/book/nfs_rootfs
/mnt指令将刚刚进行编译等操作的Linux服务器里的根文件系统挂载到开发版上。
nfs_rootfs是一个通过网络文件系统NFS挂载的根文件系统。
网络文件系统NFSNFS
是一个分布式文件系统协议它允许用户在网络上访问存储在远程计算机上的文件就像访问本地存储的文件一样。
根文件系统rootfs根文件系统包含操作系统的核心组件如可执行文件、库文件、配置文件等。
此时在开发板上就相当于有了一个Linux操作系统实际上用的是服务器的系统可以看到服务器的根文件系统中有什么挂载之后的/mnt里就有什么。
如上图所示进入开发板挂载的根文件/mnt中找到我们的hello_drv.ko所在位置然后执行insmod
hello_drv.ko指令安装设备节点的驱动程序安装完毕后在/dev设备节点中可以看到hello设备节点。
在执行可执行程序的命令行参数中使用-w选项写入A-Big-MiaoMi字符串到内核缓冲区中。
再使用-r选项从内核缓冲区中读取刚刚写入数据结果是APP
如上图所示IMX6ULLGPIO框图输出功能的配置和其他芯片一样分为四步
使能GPIO组设置CCM寄存器组中的CCGRx寄存器中的相应位CGx来使能对应的GPIO组。
选择GOIO为通用输入输出功能设置IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPERx寄存器中的MUX_MODE位将IO口设为通用输入输出功能。
选择方向设置GPIOx_GDIR中的相应位0表示输入1表示输出。
写数据寄存器设置GPIOx_DR中的相应位0表示输出低电平1表示输出高电平。
GPIO5_3输出低电平LED灯亮。
GPIO5_3输出低电平LED灯灭。
按照上面的配置步骤寻找GPIO5_3的那几个寄存器和对应的比特位
如上图所示CCM_CCGR1中的CG15是保留的在IMX6ULL中GPIO5这组GPIO默认使能所以不用设置。
CCM_CCGR1的绝对地址是0x020C4000
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3寄存器中的MUX_MODE4位配置为101选择GPIO5_IO03为通用输入输出模式。
该寄存器的绝对地址是0x02290000
如上图使用的是GPIO5_3IO口所以要配置GPIO5_GDIR中的bit3该位为1表示输出该位为0表示输入。
该寄存器的偏移量是0x4。
如上图所示GPIO5的基地址是0x020AC000所以GPIO5_GDIR的绝对地址是0x020AC000
如上图所示GPIO5_DR中的bit3设置为1GPIO5_3就输出高电平设置为0就输出低电平该寄存器的地址偏移量是0x0所以它的绝对地址就是0x020AC000
如上图所示file_operations结构体只初始化三个成员
owner是必须有的其值是该模块所属者的指针THIS_MODULE。
如上图所示在led_open函数中对GPIO5_3进行使能功能选择以及方向选择当应用层调用open系统调用时最终会调用驱动层的led_open函数对GPIO进行初始化。
如山图所示在led_write函数中使用copy_from_user读取应用层调用write系统调用时写入的参数并且复制到val中根据该参数的逻辑值来控制LED灯的状态
用户层写入非0值向GPIO5_DR寄存器的bit3写0LED灯亮。
用户层写入0值向GPIO5_DR寄存器的bit3写1LED灯灭。
如上图所示创建相应寄存器的指针变量然后在入口函数中首先使用resister_chrdev注册设备节点然后再使用ioremap函数映射虚拟地址。
然后再使用class_create和device_create为内核创建设备节点提供信息。
用来指向寄存器的指针使用volatile关键字修饰保持内存可见性。
对于寄存器来说有没有数据写入区别非常大所以要保证每次操作寄存器都能写入不被优化。
前面查芯片手册时看到的寄存器地址是实实在在的物理地址但是在Linux中是不允许直接操作物理地址的。
在led_open和led_write中操作寄存器时使用的指针IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3GPIO5_GDIR以及GPIO5_DR变量其中的地址都是经过映射以后得到的虚拟地址。
如上图所示在Linux系统中存在多个进程假设此时存在两个进程每个进程都有一个PCB结构体里面的struct
不同进程的进程地址空间是相互独立的互不影响。
每个进程地址空间都包含栈区共享区堆区数据段代码段等等区域。
在led_open和led_write驱动函数中使用的寄存器指针它们属于全局变量所以存放在使用该驱动程序进程地址空间的数据段。
如果这两个进程都会调用open和write系统调用来操作LED灯
假设进程地址空间的数据段存放的是GPIO5相关寄存器的物理地址。
进程1对GPIO5_3IO口的操作是正常的符合规范的但是进程2对GPIO5_3IO口的操作是违规的如越界操作溢出等错误操作。
由于进程1和进程2操作的是物理地址所以进程2的错误操作会影响到进程1的正常操作两个进程就相互影响了。
进程地址空间的数据段存放的是GPIO5相关寄存器的映射后的虚拟地址。
实际上采样的就是这种方式使用虚拟地址的方式来管理和保护内存。
上图中的MMU可以把物理地址和虚拟地址建立映射关系当操作进程地址空间中的虚拟地址时
MMU会先判断该操作是否合法对物理地址形成保护防止非法访问。
操作合法时去该虚拟地址所映射的物理地址处进行操作。
此时进程1和进程2就不会互相影响当进程2对寄存器进程非法操作时MMU就会直接驳回它的操作请求。
操作系统Linux运行在保护模式下使用虚拟内存来管理和保护内存同一个物理地址可以被映射到不同进程的不同虚拟地址上。
直接访问物理地址会绕过这层保护可能导致系统不稳定或不安全。
第一个参数要进行映射的物理地址。
第二个参数要映射的内存大小(字节)。
由于IMX6ULL的GPIO5_3涉及到的寄存器都是32位的也就是四个字节所以第二个参数都是4将使用ioremap映射后的3个虚拟地址赋值给那几个寄存器指针全局变量。
如上图所示在出口函数中首先要把映射的虚拟地址销毁掉使用iounmap函数实现只有一个参数就是映射后得到的虚拟地址。
然后就是按照驱动框架中的操作将设备类以及设备节点全部销毁以及销毁设备节点的注册最后再告诉内核入口函数和出口函数以及声明一下使用GPL开源协议。
如上图所示应用层的测试代码led_drv_test.c在执行测试程序时命令行中输入的指令有两种
在main函数中首先判断命令行参数的个数如果个数不为3说明使用错误则提示用法并直接返回-1。
参数正确以后先打开/dev/myled目录下的设备节点可以看到使用的是open系统调用应用层只把它当作一个普通文件并不知道这是一个字符设备。
如果是“on”则将后面要写入内核中的数据status修改为1。
如果是off则不做任何修改status使用创建时的初始值0。
最后使用write系统调用将status这一个字节的数据写入到内核驱动函数根据这个数据的逻辑值来判断是点亮LED灯还是熄灭LED灯。
交叉编译和前面hello驱动程序一样只是需要对Makefile文件稍作修改
led_drv.ko按照myled设备节点然后执行led_drv_test测试程序就可以点亮和熄灭LED灯了这里本喵就不贴板子的效果图了。
通过和硬件无关的hello驱动程序来引出驱动程序的框架。
然后使用该框架实现了IMX6ULL单板上LED灯的驱动程序。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback