tag 标签: linux开发

相关博文
  • 热度 1
    2024-10-2 22:01
    94 次阅读|
    0 个评论
    平时遇到键盘、鼠标、触摸板等输入设备无响应等异常情况时,一般通过更换设备判断异常。但在遇到更换正常设备后,输入仍然异常的情况下,可以借助evtest工具查看内核的上报事件信息,协助定位问题所在。 本次使用的是触觉智能 EVB3562开发板 进行演示,搭载瑞芯微RK3562/RK3562J芯片,该方法也适用于瑞芯微、全志、Sigmastar等平台开发板/主板产品。 1、准备evtest(事件响应工具)软件 如果没有安装evtest(事件响应工具)软件,执行下面代码进行安装 sudo apt install evtest 2、查看触摸坐标点的步骤 输入evtest后选择设备 root@rk3562-buildroot:/# evtest No device specified, trying to scan all of /dev/input/event* Available devices: /dev/input/event0: rk805 pwrkey /dev/input/event1: goodix-ts /dev/input/event2: rockchip-rk809 Headset /dev/input/event3: adc-keys Select the device event number : 1 /dev/input/event0:标记为 rk805 pwrkey, 通常是一个电源按键; /dev/input/event1:标记为 goodix-ts,通常 是一个触摸屏控制器; /dev/input/event2:标记为 rockchip-rk809 Headset,通常是一个耳机插孔的事件设备; /dev/input/event3:标记为 adc-keys,通常 是指使用模拟数字转换器(ADC)读取的按键; Select the device event number : 选择要查看的设备,这里我们选择1,触摸 屏控制器; 3、测试点击触摸后有如下触摸坐标信息 注:如触摸异常/无触摸时,则无相关打印信息 4、分析触摸打印相关参数 time 1725359247.668897和time 17253592 47.730242:这些是事件的时间戳,表示事件发 生的确切时间; EV_ABS:绝对事件,用于报告触摸位置和其他 触摸相关的属性; ABS_MT_TRACKING_ID:触摸点的唯一标识符,值 0 表示一个新的触摸点开始,值 -1 表示触摸点结束; ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y:触摸点在屏幕上的位置。这些值是相对于屏幕左上角的坐标; ABS_MT_TOUCH_MAJOR 和 ABS_MT_WIDTH_MAJOR:触摸点的主要轴的长度和宽度,通常用于表示触摸区域的大小; 5、产品简介 触觉智能 EVB3562开发板 ,基于瑞芯微RK3562/RK3562J芯片设计,可用于轻量级人工智能应用。EVB3562开发板配备了PCIE2.1/ USB3.0 OTG/双千兆以太网等各类型接口,支持4G/5G通信、多摄像头及多种视频接口,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示、工业控制等行业领域。 搭载瑞芯微新一代RK3562/RK3562J芯片; 1TOPS算力NPU,支持INT8/INT16/FP16等数据类型运算; 支持4K@30FPS与1080P@60FPS视频解码; 13M ISP,支持HDR与多路摄像头视频采集; 单路MIPI-DSI,最高2048 x 1080@60fps ; 单通道LVDS,最高1366 x 768@60fps ; 三路独立的以太网口,其中两路千兆网口,一路百兆网口; 支持5G/4G/WiFi/蓝牙无线通信; 支持Android,Linux操作系统;
  • 热度 2
    2023-12-13 21:46
    666 次阅读|
    0 个评论
    【Linux API 揭秘】container_of函数详解 Linux Version:6.6 Author:Donge Github: linux-api-insides 1、container_of函数介绍 container_of可以说是内核中使用最为频繁的一个函数了,简单来说,它的主要作用就是根据我们结构体中的已知的成员变量的地址,来寻求该结构体的首地址,直接看图,更容易理解。 image-20231212195328080 下面我们看看linux是如何实现的吧 2、container_of函数实现 /** *container_of-castamemberofastructureouttothecontainingstructure *@ptr:thepointertothemember. *@type:thetypeofthecontainerstructthisisembeddedin. *@member:thenameofthememberwithinthestruct. * *WARNING:anyconstqualifierof@ptrislost. */ # define container_of(ptr,type,member)({\ void*__mptr=(void*)(ptr);\ member)||\ __same_type(*(ptr),void),\ "pointertypemismatchincontainer_of()" );\ ((type*)(__mptr-offsetof(type,member)));}) 函数名称 :container_of 文件位置 : include/linux/container_of.h 该函数里面包括了一些封装好的宏定义以及函数,比如:static_assert、__same_type、offsetof,以及一些指针的特殊用法,比如:(type *)0),下面我们一一拆解来看。 image-20231213140920353 2.1 static_assert /** *static_assert-checkintegerconstantexpressionatbuildtime * *static_assert()isawrapperfortheC11_Static_assert,witha *littlemacromagictomakethemessageoptional(defaultingtothe *stringificationofthetestedexpression). * *ContrarytoBUILD_BUG_ON(),static_assert()canbeusedatglobal *scope,butrequirestheexpressiontobeanintegerconstant *expression(i.e.,itisnotenoughthat__builtin_constant_p()is *trueforexpr). * *AlsonotethatBUILD_BUG_ON()failsthebuildiftheconditionis *true,whilestatic_assert()failsthebuildiftheexpressionis *false. */ # define static_assert(expr,...)__static_assert(expr,##__VA_ARGS__,#expr) # define __static_assert(expr,msg,...)_Static_assert(expr,msg) 函数名称 :static_assert 文件位置 : include/linux/build_bug.h 函数解析 :该宏定义主要用来 在编译时检查常量表达式,如果表达式为假,编译将失败,并打印传入的报错信息 expr:该参数表示传入进来的常量表达式 ...:表示编译失败后,要打印的错误信息 _Static_assert:C11中引入的关键字,用于判断表达式expr并打印错误信息msg。 在container_of函数中,主要用来断言判断 static_assert ( __same_type(*(ptr),((type*) 0 member)||__same_type(*(ptr), void ), "pointertypemismatchincontainer_of()" ); 2.2 __same_type /*Aretwotypes/varsthesametype(ignoringqualifiers)?*/ # ifndef __same_type # define __same_type(a,b)__builtin_types_compatible_p(typeof(a),typeof(b)) # endif 函数名称 :__same_type 文件位置 : include/linux/compiler.h 函数解析 : 该宏定义用于检查两个变量是否是同种类型 __builtin_types_compatible_p:gcc的内建函数,判断两个参数的类型是否一致,如果是则返回1 typeof:gcc的关键字,用于获取变量的类型信息 member),需要先弄明白(type *)0的含义。 更多干货可见: 高级工程师聚集地 ,助力大家更上一层楼! 2.3 (type *)0 (type *)0,该如何理解这个表达式呢? 首先,type是我们传入进来的结构体类型,比如上面讲到的struct test,而这里所做的 可以理解为强制类型转换 :(struct test *)addr。 addr可以表示内存空间的任意的地址,我们在强制转换后,默认后面一片的内存空间存储的是该数据结构。 image-20231213144714508 而(type *)0的作用,也就是默认将0地址处的内存空间,转换为该数据类型。 image-20231213144912371 member,就是获取我们结构体的成员对象。 member:是一种常见的技巧, 用于直接获取结构体type的成员member的类型,而不需要定义一个type类型的对象 。 2.4 offsetof # ifndef offsetof # define MEMBER) # endif 函数名称 :offsetof 文件位置 : include/linux/stddef.h 函数解析 : 该宏定义用于获取结构体中指定的成员,距离该结构体偏移量。 image-20231213152249395 TYPE:表示结构体的类型 MEMBER:表示指定的结构体成员 __builtin_offsetof:gcc内置函数,直接返回偏移量。 在新的linux源码中,直接引用了gcc内置的函数,而在老的内核源码中,该偏移量的实现方式如下: # define MEMBER) 同样用到了((TYPE *)addr),上面我们知道 MEMBER:表示获取该结构体的成员 MEMBER):加了一个&,表示地址,取该成员的内存地址。 MEMBER)就相当于0x00000010+size MEMBER)就相当于size 到这里,我们对container_of函数内部涉及的相关知识了然于胸,下面我们再来看container_of,简直容易到起飞。 2.5 container_of # define container_of(ptr,type,member)({\ void*__mptr=(void*)(ptr);\ member)||\ __same_type(*(ptr),void),\ "pointertypemismatchincontainer_of()" );\ ((type*)(__mptr-offsetof(type,member)));}) static_assert:断言信息,避免我们传入的参数类型不对,而做的编译检查处理,直接忽略。 #definecontainer_of(ptr,type,member)({\ void*__mptr=(void*)(ptr);\ (( type *)(__mptr-offsetof( type ,member)));}) offsetof(type, member):计算的是结构体中的成员的偏移量,这里称为size (__mptr - offsetof(type, member)):也就是根据我们已知的成员变量地址,计算出来结构体的首地址 ((type *)(__mptr - offsetof(type, member))):最后强制转换为(type *),结构体指针。 比如,我们已知的结构体成员的地址为0xffff0000,计算之后如下: image-20231213151416841 3、总结 linux内核中,小小的一个函数,内部包括的技巧如此之多:static_assert、__same_type、(type *)0、offsetof。 了解完内部完整的实现手法之后,我们也可以手码一个container_of了 :) image-20231119211155587
  • 热度 5
    2023-12-12 08:53
    1069 次阅读|
    0 个评论
    Linux内存管理 | 六、物理内存分配——伙伴系统 上一章,我们了解了物理内存的布局以及Linux内核对其的管理方式,页(page)也是物理内存的最小单元,Linux内核对物理内存的分配主要分为两种:一种是整页的分配,采用的是伙伴系统,另一种是小内存块的分配,采用的是slab技术。 下面我们先来看看什么是伙伴系统! 1、伙伴系统(Buddy System) Linux系统中,对物理内存进行分配的核心是 建立在页面级的伙伴系统之上 。Linux内存管理的页大小为4KB,把所有的空闲页分组为11个页块链表,每个链表分别包含很多个大小的页块,有 1、2、4、8、16、32、64、128、256、512 和 1024 个连续页的页块,最大可以申请 1024 个连续页,对应 4MB 大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。 如下图所示: image-20231021143420253 第 i 个页块链表中,页块中页的数目为 2^i。——仔细理解这个页块的含义。 在struct zone结构体中,有下面定义 struct free_area free_area ; # define MAX_ORDER11 free_area:存放不同大小的页块 MAX_ORDER:就是指数 当向内核请求分配 (2^(i-1),2^i] 数目的页块时,按照 2^i 页块请求处理。如果对应的页块链表中没有空闲页块,那我们就在更大的页块链表中去找。当分配的页块中有多余的页时,伙伴系统会根据多余的页块大小插入到对应的空闲页块链表中。 举个例子: 例如,要请求一个 128 个页的页块时,先检查 128 个页的页块链表是否有空闲块。如果没有,则查 256 个页的页块链表;如果有空闲块的话,则将 256 个页的页块分成两份,一份使用,一份插入 128 个页的页块链表中。如果还是没有,就查 512 个页的页块链表;如果有的话,就分裂为 128、128、256 三个页块,一个 128 的使用,剩余两个插入对应页块链表。 上面的这套机制就是伙伴系统所做的事情,它主要负责对物理内存页面进行跟踪,记录哪些是被内核使用的页面,哪些是空闲页面。 2、页面分配器(Page Allocator) 由上一章我们知道,物理内存被分为了几个区域:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM,其中前两个区域的物理页面与虚拟地址空间是线性映射的。 image-20231021153403131 页面分配器主要的工作原理如下 : 如果页面分配器分配的物理页面在ZONE_DMA、ZONE_NORMAL区域,那么对应的虚拟地址到物理地址映射的页目录已经建立,因为是线性映射,两者之间有一个差值PAGE_OFFSET。 如果页面分配器分配的物理页面在ZONE_HIGHMEM区域,那么内核此时还没有对该页面进行映射,因此页面分配器的调用者,首先在虚拟地址空间的动态映射区或者固定映射区分配一个虚拟地址,然后映射到该物理页面上。 以上就是页面分配器的原理,对于我们只需要调用相关接口函数就可以了。 页面分配函数主要有两个:alloc_pages和__get_free_pages,而这两个函数最终也会调用到alloc_pages_node,其实现原理完全一样。 下面我们从代码层面来看页面分配器的工作原理 更多干货可见: 高级工程师聚集地 ,助力大家更上一层楼! 3、gfp_mask 我们先来了解一下gfp_mask,它并不是页面分配器函数,而只是这些页面分配函数中一个重要的参数, 是个用于控制分配行为的掩码,并可以告诉内核应该到哪个zone中分配物理内存页面。 /*PlainintegerGFPbitmasks.Donotusethisdirectly.*/ # define ___GFP_DMA0x01u # define ___GFP_HIGHMEM0x02u # define ___GFP_DMA320x04u # define ___GFP_MOVABLE0x08u # define ___GFP_RECLAIMABLE0x10u # define ___GFP_HIGH0x20u # define ___GFP_IO0x40u # define ___GFP_FS0x80u # define ___GFP_WRITE0x100u # define ___GFP_NOWARN0x200u # define ___GFP_RETRY_MAYFAIL0x400u # define ___GFP_NOFAIL0x800u # define ___GFP_NORETRY0x1000u # define ___GFP_MEMALLOC0x2000u # define ___GFP_COMP0x4000u # define ___GFP_ZERO0x8000u # define ___GFP_NOMEMALLOC0x10000u # define ___GFP_HARDWALL0x20000u # define ___GFP_THISNODE0x40000u # define ___GFP_ATOMIC0x80000u # define ___GFP_ACCOUNT0x100000u # define ___GFP_DIRECT_RECLAIM0x200000u # define ___GFP_KSWAPD_RECLAIM0x400000u # ifdef CONFIG_LOCKDEP # define ___GFP_NOLOCKDEP0x800000u # else # define ___GFP_NOLOCKDEP0 # endif ___GFP_DMA:在ZONE_DMA标识的内存区域中查找空闲页。 ___GFP_HIGHMEM:在ZONE_HIGHMEM标识的内存区域中查找空闲页。 ___GFP_MOVABLE:内核将分配的物理页标记为可移动的。 ___GFP_HIGH:内核允许使用紧急分配链表中的保留内存页。该请求必须以原子方式完成,意味着请求过程不允许被中断。 ___GFP_IO:内核在查找空闲页的过程中可以进行I/O操作,如此内核可以将换出的页写到硬盘。 ___GFP_FS:查找空闲页的过程中允许执行文件系统相关操作。 ___GFP_ZERO:用0填充成功分配出来的物理页。 通常意义上,这些以“__”打头的GFP掩码只限于在内存管理组件内部的代码使用,对于提供给外部的接口,比如驱动程序中所使用的页面分配函数,gfp_mask掩码以“GFP_”的形式出现,而这些掩码基本上就是上面提到的掩码的组合。 例如内核为外部模块提供的最常使用的几个掩码如下: #defineGFP_ATOMIC(__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) #defineGFP_KERNEL(__GFP_RECLAIM|__GFP_IO|__GFP_FS) #defineGFP_KERNEL_ACCOUNT(GFP_KERNEL|__GFP_ACCOUNT) #defineGFP_NOWAIT(__GFP_KSWAPD_RECLAIM) #defineGFP_NOIO(__GFP_RECLAIM) #defineGFP_NOFS(__GFP_RECLAIM|__GFP_IO) #defineGFP_USER(__GFP_RECLAIM|__GFP_IO|__GFP_FS|__GFP_HARDWALL) #defineGFP_DMA__GFP_DMA #defineGFP_DMA32__GFP_DMA32 #defineGFP_HIGHUSER(GFP_USER|__GFP_HIGHMEM) #defineGFP_HIGHUSER_MOVABLE(GFP_HIGHUSER|__GFP_MOVABLE) #defineGFP_TRANSHUGE_LIGHT((GFP_HIGHUSER_MOVABLE|__GFP_COMP|\ __GFP_NOMEMALLOC|__GFP_NOWARN)&~__GFP_RECLAIM) #defineGFP_TRANSHUGE(GFP_TRANSHUGE_LIGHT|__GFP_DIRECT_RECLAIM) GFP_ATOMIC:内核模块中最常使用的掩码之一,用于原子分配。此掩码告诉页面分配器,在分配内存页时,绝对不能中断当前进程或者把当前进程移出调度器。 GFP_KERNEL:内核模块中最常使用的掩码之一,带有该掩码的内存分配可能导致当前进程进入睡眠状态。 GFP_USER:用于为用户空间分配内存页,可能引起进程的休眠。 GFP_NOIO:在分配过程中禁止I/O操作 GFP_NOFS:禁止文件系统相关的函数调用 GFP_DMA:限制页面分配器只能在ZONE_DMA域中分配空闲物理页面,用于分配适用于DMA缓冲区的内存。 通过gfp_mask掩码,更加方便我们控制页面分配器到哪个区域去分配物理内存 ,分配内存的优先级如下: 指定__GFP_HIGHMEM:先在ZONE_HIGHMEM域中查找空闲页,如果无法满足当前分配,页分配器将回退到ZONE_NORMAL域中继续查找,如果依然无法满足当前分配,分配器将回退到ZONE_DMA域,或者成功或者失败。 指定__GFP_DMA:只能在ZONE_DMA中分配物理页面,如果无法满足,则分配失败。 没有__GFP_NORMAL这样的掩码,但是前面已经提到,如果gfp_mask中没有明确指定__GFP_HIGHMEM或者是__GFP_DMA,默认就相当于__GFP_NORMAL,优先在ZONE_NORMAL域中分配,其次是ZONE_DMA域。 4、alloc_pages alloc_pages函数负责分配2^order个连续的物理页面并返回起始页的struct page实例。 alloc_pages的实现源码如下: static inline structpage* alloc_pages ( gfp_t gfp_mask, unsigned int order) { return alloc_pages_current(gfp_mask,order); } /** *alloc_pages_current-Allocatepages. * *@gfp: *%GFP_USERuserallocation, *%GFP_KERNELkernelallocation, *%GFP_HIGHMEMhighmemallocation, *%GFP_FSdon'tcallbackintoafilesystem. *%GFP_ATOMICdon'tsleep. *@order:Poweroftwoofallocationsizeinpages.0isasinglepage. * *Allocateapagefromthekernelpagepool.Whennotin *interruptcontextandapplythecurrentprocessNUMApolicy. *ReturnsNULLwhennopagecanbeallocated. */ structpage* alloc_pages_current ( gfp_t gfp, unsigned order) { struct mempolicy * pol =& default_policy ; struct page * page ; if (!in_interrupt()&&!(gfp&__GFP_THISNODE)) pol=get_task_policy(current); /* mempolicy *norsystemdefault_policy */ if mode==MPOL_INTERLEAVE) page=alloc_page_interleave(gfp,order,interleave_nodes(pol)); else page=__alloc_pages_nodemask(gfp,order, policy_node(gfp,pol,numa_node_id()), policy_nodemask(gfp,pol)); return page; } EXPORT_SYMBOL(alloc_pages_current); alloc_pages调用alloc_pages_current,其中 gfp参数:即上文的gfp_mask,表明我们想要在哪个物理内存区域进行内存分配 order参数:表示分配 2 的 order 次方个页。 __alloc_pages_nodemask为伙伴系统的核心实现,它会调用 get_page_from_freelist。 static structpage* get_page_from_freelist ( gfp_t gfp_mask, unsigned int order, int alloc_flags, const structalloc_context*ac) { ...... nodemask){ struct page * page ; ...... zone,zone,order, migratetype); ...... } 这里面的逻辑也很容易理解,就是在一个循环中先看当前节点的 zone。如果找不到空闲页,则再看备用节点的 zone。 每一个 zone,都有伙伴系统维护的各种大小的队列,就像上面伙伴系统原理里讲的那样。 这里调用 rmqueue 就很好理解了,就是找到合适大小的那个队列,把页面取下来。 伙伴系统的实现代码,感兴趣的可以深入探究。 image-20231022103859552 在调用这个函数的时候,有几种情况: 如果gfp_mask中没有指定__GFP_HIGHMEM,那么分配的物理页面必然来自ZONE_NORMAL或者ZONE_DMA,由于这两个区域内核在初始化的时候就已经建立了映射关系,所以内核很容易就能找到对应的虚拟地址KVA(Kernel Virtual Address) 如果gfp_mask中指定了__GFP_HIGHMEM,那么页分配器将优先在ZONE_HIGHMEM域中分配物理页,但也不排除因为ZONE_HIGHMEM没有足够的空闲页导致页面来自ZONE_NORMAL与ZONE_DMA域的可能性。对于新分配出的高端物理页面,由于内核尚未在页表中为之建立映射关系,所以此时需要: 在内核的动态映射区分配一个KVA 通过操作页表,将第一步中的KVA映射到该物理页面上,通过kmap实现 5、__get_free_pages __get_free_pages该函数负责分配2^ordev个连续的物理页面,返回起始页面所在内核线性地址。 函数的实现如下: /* *Commonhelperfunctions.Neverusewith__GFP_HIGHMEMbecausethereturned *addresscannotrepresenthighmempages.Usealloc_pagesandthenkmapif *youneedtoaccesshighmem. */ unsigned long __get_free_pages( gfp_t gfp_mask, unsigned int order) { struct page * page ; page=alloc_pages(gfp_mask&~__GFP_HIGHMEM,order); if (!page) return 0 ; return ( unsigned long )page_address(page); } EXPORT_SYMBOL(__get_free_pages); 我们可以看到,函数内部调用了alloc_pages函数,并且不能从__GFP_HIGHMEM高端内存分配物理页,最后通过page_address来返回页面的起始页面的内核线性地址。 6、get_zeroed_page get_zeroed_page用于分配一个物理页同时将页面对应的内容填充为0,函数返回页面所在的内核线性地址。 可以看下内核代码: unsigned long get_zeroed_page ( gfp_t gfp_mask) { return __get_free_pages(gfp_mask|__GFP_ZERO, 0 ); } EXPORT_SYMBOL(get_zeroed_page); 仅仅是在__get_free_pages基础上,使用了 __GFP_ZERO标志,来初始化分配页面的初始内容。 7、总结 以上,就是建立在伙伴系统之上的页面级分配器,常用的函数有:alloc_pages、__get_free_pages、get_zeroed_page、__get_dma_pages等,其底层实现都是一样的,只是gfp_mask不同。
  • 热度 4
    2023-11-30 08:32
    1123 次阅读|
    0 个评论
    一文秒懂|Linux字符设备驱动 1、前言 众所周知, Linux 内核主要包括三种驱动模型,字符设备驱动,块设备驱动以及网络设备驱动。 其中, Linux 字符设备驱动,可以说是 Linux 驱动开发中最常见的一种驱动模型。 我们该系列文章,主要为了帮助大家快速入门 Linux 驱动开发,该篇主要来了解一些字符设备驱动的框架和机制。 系列文章基于 Kernel 4.19 2、关键数据结构 2.1 cdev struct cdev { struct kobject kobj ; struct module * owner ; const struct file_operations * ops ; struct list_head list ; dev_t dev ; unsigned int count ; } __randomize_layout ; 结构体名称 : cdev 文件位置 : include/linux/cdev.h 主要作用 : cdev 可以理解为 char device ,用来抽象一个字符设备。 核心成员及含义 : kobj :表示一个内核对象。 owner :指向该模块的指针 ops :指向文件操作的指针,包括 open 、 read 、 write 等操作接口 list :用于将该设备加入到内核模块链表中 dev :设备号,由主设备号和次设备号构成 count :表示有多少个同类型设备,也间接表示设备号的范围 __randomize_layout :一个编译器指令,用于随机化结构体的布局,以增加安全性。 2.2 file _ operations 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 ( * write ) ( struct file * , const char __user * , size_t , loff_t * ); ssize_t ( * read_iter ) ( struct kiocb * , struct iov_iter * ); ssize_t ( * write_iter ) ( struct kiocb * , struct iov_iter * ); int ( * iterate ) ( struct file * , struct dir_context * ); int ( * iterate_shared ) ( struct file * , struct dir_context * ); __poll_t ( * poll ) ( struct file * , struct poll_table_struct * ); long ( * unlocked_ioctl ) ( struct file * , unsigned int , unsigned long ); long ( * compat_ioctl ) ( struct file * , unsigned int , unsigned long ); int ( * mmap ) ( struct file * , struct vm_area_struct * ); unsigned long mmap_supported_flags ; int ( * open ) ( struct inode * , struct file * ); int ( * flush ) ( struct file * , fl_owner_t id ); int ( * release ) ( struct inode * , struct file * ); int ( * fsync ) ( struct file * , loff_t , loff_t , int datasync ); int ( * fasync ) ( int , struct file * , int ); int ( * lock ) ( struct file * , int , struct file_lock * ); 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 ); int ( * check_flags )( int ); int ( * flock ) ( struct file * , int , struct file_lock * ); ssize_t ( * splice_write )( struct pipe_inode_info * , struct file * , loff_t * , size_t , unsigned int ); ssize_t ( * splice_read )( struct file * , loff_t * , struct pipe_inode_info * , size_t , unsigned int ); int ( * setlease )( struct file * , long , struct file_lock ** , void ** ); long ( * fallocate )( struct file * file , int mode , loff_t offset , loff_t len ); void ( * show_fdinfo )( struct seq_file * m , struct file * f ); #ifndef CONFIG_MMU unsigned ( * mmap_capabilities )( struct file * ); #endif ssize_t ( * copy_file_range )( struct file * , loff_t , struct file * , loff_t , size_t , unsigned int ); int ( * clone_file_range )( struct file * , loff_t , struct file * , loff_t , u64 ); int ( * dedupe_file_range )( struct file * , loff_t , struct file * , loff_t , u64 ); int ( * fadvise )( struct file * , loff_t , loff_t , int ); } __randomize_layout ; ​ 结构体名称 : file_operations 文件位置 : include/linux/fs.h 主要作用 :正如其名,主要用来描述文件操作的各种接口, Linux 一切接文件的思想,内核想要操作哪个文件,都需要通过这些接口来实现。 核心成员及含义 : open :打开文件的函数 read :读取文件的函数。 write :写入文件的函数。 release :关闭文件的函数。 flush :刷新文件的函数,通常在关闭文件时调用。 llseek :改变文件读写指针位置的函数。 fsync :将文件数据同步写入磁盘的函数。 poll :询问文件是否可被非阻塞读写 2.3 dev _ t typedef u32 __kernel_dev_t ; ​ typedef __kernel_dev_t dev_t ; 类型名称 : dev_t 文件位置 : include/linux/types.h 主要作用 :表示字符设备对应的设备号,其中包括主设备号和次设备号。 3、数据结构之间关系 上图绘制是对字符设备驱动程序的数据结构以及 API 的关系图, 有需要原始文件,可在公~号【嵌入式艺术】获取。 4、字符设备驱动整体架构 4.1 加载与卸载函数 驱动首先实现的就是加载和卸载函数,也是驱动程序的入口函数。 我们一般这么定义驱动的加载卸载函数: static int __init xxx_init ( void ) { ​ } ​ static void __exit xxx_exit ( void ) { } ​ module_init ( xxx_init ); module_exit ( xxx_exit ); 这段代码就是实现一个通用驱动的加载与卸载,关于 module_init 和 module_exit 的实现机制,可以查看之前总结文章。 4.2 设备号管理 4.2.1 设备号的概念 每一类字符设备都有一个唯一的设备号,其中设备号又分为主设备号和次设备号,那么这两个分别作用是什么呢? 主设备号:用于标识设备的类型, 次设备号:用于区分同类型的不同设备 简单来说,主设备号用于区分是 IIC 设备还是 SPI 设备,而次设备号用于区分 IIC 设备下,具体哪一个设备,是 MPU6050 还是 EEPROM 。 4.2.2 设备号的分配 了解了设备号的概念, Linux 中设备号有那么多,那么我们该如何去使用正确的设备号呢? 设备号的分配方式有两种,一种是动态分配,另一种是静态分配,也可以理解为一种是内核自动分配,一种是手动分配。 静态分配函数 : int register_chrdev_region ( dev_t from , unsigned count , const char * name ); from :表示已知的一个设备号 count :表示连续设备编号的个数,(同类型的设备有多少个) name :表示设备或者驱动的名称 函数作用 :以 from 设备号开始,连续分配 count 个同类型的设备号 动态分配函数 : int alloc_chrdev_region ( dev_t * dev , unsigned baseminor , unsigned count , const char * name ); dev :设备号的指针,用于存放分配的设备号的值 baseminor :次设备号开始分配的起始值 count :表示连续设备编号的个数,(同类型的设备有多少个) name :表示设备或者驱动的名称 函数作用 :从 baseminor 次设备号开始,连续分配 count 个同类型的设备号,并自动分配一个主设备号,将主、次组成的设备号信息赋值给 *dev 这两个函数最大的区别在于 : register_chrdev_region :调用前,已预先定义好了主设备号和次设备号,调用该接口后,会将自定义的设备号登记加入子系统中,方便系统追踪系统设备号的使用情况。 alloc_chrdev_region :调用前,未定义主设备号和次设备号;调用后,主设备号以 0 来表示,以自动分配,并且将自动分配的设备号,同样加入到子系统中,方便系统追踪系统设备号的使用情况。 这两个函数的共同点在于 : 系统维护了一个数组列表,用来登记所有的已使用的设备号信息,这两个接口归根到底也是将其设备号信息,登记到系统维护的设备号列表中,以免后续冲突使用。 在 Linux 中,我们可以通过 cat /proc/devices 命令,查看所有i登记的设备号列表。 后面有时间,我们可以详细聊设备号的自动分配机制,管理机制。 4.2.3 设备号的注销 设备号作为一种系统资源,当所对应的设备卸载时,当然也要将其所占用的设备号归还给系统,无论时静态分配,还是动态分配,最终都是调用下面函数来注销的。 void unregister_chrdev_region ( dev_t from , unsigned count ); from :表示已知的一个设备号 count :表示连续设备编号的个数,(同类型的设备有多少个) 函数作用 :要注销 from 主设备号下的连续 count 个设备 4.2.4 设备号的获取 设备号的管理很简单,在关键数据结构中,我们看到设备号的类型是 dev_t ,也就是 u32 类型表示的一个数值。 其中主设备号和次设备号的分界线,由 MINORBITS 宏定义指定: #define MINORBITS 20 也就是主设备号占用高 12bit ,次设备号占用低 20bit 并且,内核还提供了相关 API 接口,来获取主设备号和次设备号,以及生成设备号的接口,如下: #define MINORMASK ((1U << MINORBITS) - 1) ​ #define MAJOR(dev) MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) 以上,通过移位操作,来实现主次设备号的获取。 4.2.4 通用代码实现 #define CUSTOM_DEVICE_NUM 0 #define DEVICE_NUM 1 #device DEVICE_NAME "XXXXXX" static dev_t global_custom_major = CUSTOM_DEVICE_NUM ; ​ static int __init xxx_init ( void ) { dev_t custom_device_number = MKDEV ( global_custom_major , 0 ); // custom device number /* device number register*/ if ( global_custom_major ) { ret = register_chrdev_region ( custom_device_number , DEVICE_NUM , DEVICE_NAME ); } else { ret = alloc_chrdev_region ( & custom_device_number , 0 , DEVICE_NUM , DEVICE_NAME ); global_custom_major = MAJOR ( custom_device_number ); } } ​ static void __exit xxx_exit ( void ) { unregister_chrdev_region ( MKDEV ( global_mem_major , 0 ), DEVICE_NUM ); } ​ module_init ( xxx_init ); module_exit ( xxx_exit ); 该函数实现了设备号的分配,如果主设备号为 0 ,则采用动态配分的方式,否则采用静态分配的方式。 更多干货可见: 高级工程师聚集地 ,助力大家更上一层楼! 4.3 字符设备的管理 了解完设备号的管理之后,我们来看下字符设备是如何管理的。 4.3.1、字符设备初始化 void cdev_init ( struct cdev * cdev , const struct file_operations * fops ); cdev :一个字符设备对象,也就是我们创建好的字符设备 fops :该字符设备的文件处理接口 函数作用 :初始化一个字符设备,并且将所对应的文件处理指针与字符设备绑定起来。 4.3.2、字符设备注册 int cdev_add ( struct cdev * p , dev_t dev , unsigned count ); p :一个字符设备指针,只想待添加的字符设备对象 dev :该字符设备所负责的第一个设备编号 count :该类型设备的个数 函数作用 :添加一个字符设备驱动到 Linux 系统中。 4.3.3、字符设备注销 void cdev_del ( struct cdev * p ); p :指向字符设备对象的指针 函数作用 :从系统中移除该字符设备驱动 4.4 文件操作接口的实现 因为在 Linux 中,一切皆文件的思想,所以每一个字符设备,也都有一个文件节点来对应。 我们在初始化字符设备的时候,会将 struct file_operations 的对象与字符设备进行绑定,其作用是来处理该字符设备的 open 、 read 、 write 等操作。 我们要做的就是去实现我们需要的函数接口,如: static const struct file_operations global_mem_fops = { . owner = THIS_MODULE , . llseek = global_mem_llseek , . read = global_mem_read , . write = global_mem_write , . unlocked_ioctl = global_mem_ioctl , . open = global_mem_open , . release = global_mem_release , }; 至此,我们一个基本的字符设备驱动程序的框架就基本了然于胸了 5、总结 本篇文章,旨在通俗易懂的讲解: 字符设备驱动相关数据结构 数据结构关系图 核心 API 接口 字符设备驱动整体框架 希望对大家有所帮助。
  • 热度 4
    2023-11-27 08:45
    1092 次阅读|
    0 个评论
    【Linux API 揭秘】module_init与module_exit Linux Version:6.6 Author:Donge Github: linux-api-insides 1、函数作用 module_init和module_exit是驱动中最常用的两个接口,主要用来注册、注销设备驱动程序。 并且这两个接口的实现机制是一样的,我们先以module_init为切入点分析。 2、module_init函数解析 2.1 module_init # ifndef MODULE /** *module_init()-driverinitializationentrypoint *@x:functiontoberunatkernelboottimeormoduleinsertion * *module_init()willeitherbecalledduringdo_initcalls()(if *builtin)oratmoduleinsertiontime(ifamodule).Therecanonly *beonepermodule. */ # define module_init(x)__initcall(x); ...... # else /*MODULE*/ ...... /*Eachmodulemustuseonemodule_init().*/ # define module_init(initfn)\ staticinlineinitcall_t__maybe_unused__inittest(void)\ {returninitfn;}\ intinit_module(void)__copy(initfn)\ __attribute__((alias(#initfn)));\ ___ADDRESSABLE(init_module,__initdata); ...... # endif 函数名称 :module_init 文件位置 : include/linux/module.h 函数解析 : 在Linux内核中,驱动程序可以以两种方式存在:内建(Builtin)和模块(Module)。内建驱动就是在编译时,直接编译进内核镜像中;而模块驱动则是在内核运行过程中动态加载卸载的。 module_init函数的定义位置有两处,使用MODULE宏作为判断依据。MODULE是一个预处理器宏,仅当该驱动作为模块驱动时,编译的时候会加入MODULE的定义。 这里难免会有疑问:为什么会有两套实现呢? 其实,当模块被编译进内核时,代码是存放在内存的.init字段,该字段在内核代码初始化后,就会被释放掉了,所以当可动态加载模块需要加载时,就需要重新定义了。 2.1.1 模块方式 当驱动作为可加载模块时,MODULE宏被定义,我们简单分析一下相关代码 # define module_init(initfn)\ staticinlineinitcall_t__maybe_unused__inittest(void)\ {returninitfn;}\ intinit_module(void)__copy(initfn)\ __attribute__((alias(#initfn)));\ ___ADDRESSABLE(init_module,__initdata); static inline initcall_t __maybe_unused __inittest(void) { return initfn; }:一个内联函数,返回传入的initfn函数。 __maybe_unused :编译器指令,用于告诉编译器,该函数可能不会使用,以避免编译器产生警告信息。 int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));:init_module函数的声明 __copy(initfn):编译器指令,也就是将我们的initfn函数代码复制到init_module中, __attribute__((alias(#initfn))):编译器指令,将init_module函数符号的别名设置为initfn。 ___ADDRESSABLE(init_module, __initdata);:一个宏定义,主要用于将init_module函数的地址放入__initdata段,这样,当模块被加载时,init_module函数的地址就可以被找到并调用。 总的来说,如果是可加载的ko模块,module_init宏主要定义了init_module函数,并且将该函数与initfn函数关联起来,使得当模块被加载时,初始化函数可以被正确地调用。 2.1.2 内建方式 当模块编译进内核时,MODULE宏未被定义,所以走下面流程 # define module_init(x)__initcall(x); 2.2 __initcall # define __initcall(fn)device_initcall(fn) # define device_initcall(fn)__define_initcall(fn,6) # define __define_initcall(fn,id)___define_initcall(fn,id,.initcall##id) # define ___define_initcall(fn,id,__sec)\ __unique_initcall(fn,id,__sec,__initcall_id(fn)) # define __unique_initcall(fn,id,__sec,__iid)\ ____define_initcall(fn,\ __initcall_stub(fn,__iid,id),\ __initcall_name(initcall,__iid,id),\ __initcall_section(__sec,__iid)) # define ____define_initcall(fn,__unused,__name,__sec)\ staticinitcall_t__name__used\ __attribute__((__section__(__sec)))=fn; # define __initcall_stub(fn,__iid,id)fn /*Format: __ _ _ */ # define __initcall_id(fn)\ __PASTE(__KBUILD_MODNAME,\ __PASTE(__,\ __PASTE(__COUNTER__,\ __PASTE(_,\ __PASTE(__LINE__,\ __PASTE(_,fn)))))) /*Format:__ __ */ # define __initcall_name(prefix,__iid,id)\ __PASTE(__,\ __PASTE(prefix,\ __PASTE(__,\ __PASTE(__iid,id)))) # define __initcall_section(__sec,__iid)\ #__sec ".init" /*Indirectmacrosrequiredforexpandedargumentpasting,eg.__LINE__.*/ # define ___PASTE(a,b)a##b # define __PASTE(a,b)___PASTE(a,b) 函数名称 :__initcall 文件位置 : include/linux/init.h 函数解析 :设备驱动初始化函数 2.2.1 代码调用流程 module_init(fn) __initcall(fn) device_initcall(fn) __define_initcall(fn, 6 ) ___define_initcall(fn,id,__sec) __initcall_id(fn) __unique_initcall(fn,id,__sec,__iid) ____define_initcall(fn,__unused,__name,__sec) __initcall_stub(fn,__iid,id) __initcall_name(prefix,__iid,id) __initcall_section(__sec,__iid) ____define_initcall(fn,__unused,__name,__sec) 进行函数分析前,我们先要明白#和##的概念 2.2.2 #和##的作用 符号 作用 举例 ## ##符号 可以是连接的意思 例如 __initcall_##fn##id 为__initcall_fnid那么,fn = test_init,id = 6时,__initcall##fn##id 为 __initcall_test_init6 # #符号 可以是 字符串化的意思 例如 #id 为 "id",id=6 时,#id 为"6" 更多干货可见: 高级工程师聚集地 ,助力大家更上一层楼! 2.2.3 函数解析 下面分析理解比较有难度的函数 # define device_initcall(fn)__define_initcall(fn,6) # define __define_initcall(fn,id)___define_initcall(fn,id,.initcall##id) .initcall##id:通过##来拼接两个字符串:.initcall6 # define ___define_initcall(fn,id,__sec)\ __unique_initcall(fn,id,__sec,__initcall_id(fn)) /*Format: __ _ _ */ # define __initcall_id(fn)\ __PASTE(__KBUILD_MODNAME,\ __PASTE(__,\ __PASTE(__COUNTER__,\ __PASTE(_,\ __PASTE(__LINE__,\ __PASTE(_,fn)))))) /*Indirectmacrosrequiredforexpandedargumentpasting,eg.__LINE__.*/ # define ___PASTE(a,b)a##b # define __PASTE(a,b)___PASTE(a,b) ___PASTE:拼接两个字符串 __initcall_id: 它用于生成一个唯一的标识符,这个标识符用于标记初始化函数 。 __KBUILD_MODNAME:当前正在编译的模块的名称 __COUNTER__:一个每次使用都会递增计数器,用于确保生成名称的唯一性 __LINE__:当前代码的行号 # define __unique_initcall(fn,id,__sec,__iid)\ ____define_initcall(fn,\ __initcall_stub(fn,__iid,id),\ __initcall_name(initcall,__iid,id),\ __initcall_section(__sec,__iid)) # define ____define_initcall(fn,__unused,__name,__sec)\ staticinitcall_t__name__used\ __attribute__((__section__(__sec)))=fn; # define __initcall_stub(fn,__iid,id)fn /*Format:__ __ */ # define __initcall_name(prefix,__iid,id)\ __PASTE(__,\ __PASTE(prefix,\ __PASTE(__,\ __PASTE(__iid,id)))) # define __initcall_section(__sec,__iid)\ #__sec ".init" __unique_initcall:调用____define_initcall,关键实现部分 ____define_initcall:定义一个名为 __name 的 initcall_t 类型的静态变量,并将其初始化为 fn,并放入特定的__sec段中。 __initcall_stub:表示唯一的函数名fn __initcall_name:表示一个唯一的变量名 __initcall_section: 生成一个唯一的段名。 #__sec ".init":将两个字符串拼接起来,比如:__sec=.initcall6,拼接后的段为:.initcall6.init,该段为最终存储的段。 字段通过链接器链接起来,形成一个列表进行统一管理。 这些字段我们可以在arch/arm/kernel/vmlinux.lds中查看。 ...... __initcall6_start=.;KEEP(*(.initcall6.init))KEEP(*(.initcall6s.init)) ...... 3、module_exit函数解析 module_exit和module_init的实现机制几乎没有差别,下面就简单介绍一下。 3.1 module_exit # ifndef MODULE /** *module_exit()-driverexitentrypoint *@x:functiontoberunwhendriverisremoved * *module_exit()willwrapthedriverclean-upcode *withcleanup_module()whenusedwithrmmodwhen *thedriverisamodule.Ifthedriverisstatically *compiledintothekernel,module_exit()hasnoeffect. *Therecanonlybeonepermodule. */ # define module_exit(x)__exitcall(x); ...... # else /*MODULE*/ ...... /*Thisisonlyrequiredifyouwanttobeunloadable.*/ # define module_exit(exitfn)\ staticinlineexitcall_t__maybe_unused__exittest(void)\ {returnexitfn;}\ voidcleanup_module(void)__copy(exitfn)\ __attribute__((alias(#exitfn)));\ ___ADDRESSABLE(cleanup_module,__exitdata); ...... # endif 函数名称 :module_exit 文件位置 : include/linux/module.h 3.1.1 模块方式 作为模块方式,与module_init的实现方式一样,定义cleanup_module与exitfn函数相关联,存放在__exitdata段内。 3.1.2 内建方式 当模块编译进内核时,MODULE宏未被定义,所以走下面流程 # define module_exit(x)__exitcall(x); 3.2 __exitcall # define __exitcall(fn)\ staticexitcall_t__exitcall_##fn__exit_call=fn # define __exit_call__used__section( ".exitcall.exit" ) 函数名称 :__initcall 文件位置 : include/linux/init.h 函数解析 :设备驱动卸载函数 __exitcall_##fn:定义一个新的 exitcall_t 类型的静态变量,并赋值为fn __exit_call:__used __section(".exitcall.exit"),定义该函数存储的段 4、扩展 还记得__define_initcall的定义吗? # define pure_initcall(fn)__define_initcall(fn,0) # define core_initcall(fn)__define_initcall(fn,1) # define core_initcall_sync(fn)__define_initcall(fn,1s) # define postcore_initcall(fn)__define_initcall(fn,2) # define postcore_initcall_sync(fn)__define_initcall(fn,2s) # define arch_initcall(fn)__define_initcall(fn,3) # define arch_initcall_sync(fn)__define_initcall(fn,3s) # define subsys_initcall(fn)__define_initcall(fn,4) # define subsys_initcall_sync(fn)__define_initcall(fn,4s) # define fs_initcall(fn)__define_initcall(fn,5) # define fs_initcall_sync(fn)__define_initcall(fn,5s) # define rootfs_initcall(fn)__define_initcall(fn,rootfs) # define device_initcall(fn)__define_initcall(fn,6) # define device_initcall_sync(fn)__define_initcall(fn,6s) # define late_initcall(fn)__define_initcall(fn,7) # define late_initcall_sync(fn)__define_initcall(fn,7s) # define __initcall(fn)device_initcall(fn) 不同的宏定义,被赋予了不同的调用等级,最后将不同的驱动初始化函数统一汇总到__initcallx_start字段统一管理,形成一个有序的列表。 这样,我们在内核中,按照顺序遍历这个列表,最后执行对应的模块初始化函数fn即可实现驱动的初始化。