




已阅读5页,还剩51页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1 / 56 linux 设备驱动归纳总结 一 .linux设备驱动的作用 内 核:用于管理软硬件资源,并提供运行环境。如分配 4G虚拟空间等。 linux 设备驱动:是连接硬件和内核之间的桥梁。 linux系统按个人理解可按下划分: 应用层:包括 POSIX 接口, LIBC,图形库等,用于给用户提供访问 内核的接口。属于用户态, ARM运行在用户模式 (usr)或 者系统 模式 (sys)下。 内核层:应用程序调用相关接口后,会通过系统调用,执行SWI 指 令切换 ARM 的工作模式到超级用户 (svc)模式下,根据用 户函数的要求执行相应的操作。 硬件层:硬件设备,当用户需要操作硬件时,内核会根据驱动接口 操作硬件设备 2 / 56 图结构如下: 举一个相对比较邪恶的类比: 在深圳的酒店经常会在门缝看到一些卡片,上面说可以通过打电话送货上门 提供某中服务。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 二 .内核代码树介绍 arch : 包含和硬件体系结构相关的代码 block : 硬盘调度算法,不是驱动 firmware : 固件,如 BOIS Documentation: 标准官方文档 dirver : linux 设备驱动 3 / 56 fs : 内核所支持的文件体系 include :头文件。 linux/ linux/ 常用库。 init :库文件代码, C 库函数在内核中的实现。 init/ -start_kernel-内核执行第一条代码 ipc : 进程件通信 mm :内存管理 kernel : 内核核心部分,包括进程调度等 net :网络协议 sound : 所有音频相关 其中,跟设备驱动有关并且经常查阅的文件夹有: init 4 / 56 include : linux, asm-arm drivers: arch: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 三 .内核补丁: 补丁一般都是基于某个版本内核生成的,用于升级旧内核。 打补丁需要注意: 1.对应版本的补丁只能用于对应版本的内核。 2.如果在已打补丁的内核再打补丁,需要先卸载原来补丁。 打补丁的方法: 5 / 56 1.制作补丁: diff -Nur / / /N为新加的文件全部修改 / 旧版本 / 新版本 /目标补丁 2.打补丁: cd /!注意在原文件夹的目录中打补丁 patch -p1 3.恢复: cd /!注意在原文件 夹的目录中打补丁 patch -R xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 6 / 56 四 .内核中的 Makefile: 对于内核, Makefile 分为 5 类: Documentation/kbuild/描述如下: 50 The Makefiles have five parts: 51 52 Makefile 总 Makefile,控制内核的编译 53 .config 内核配置文件,配置内核时生成, 如 make menuconfig后 54 arch/$(ARCH)/Makefile 对应体系结构的 Makefile 55 scripts/Makefile.* Makefile 共用的规则 56 kbuild Makefiles 各子目录下的 Makefile,被上层的Makefile调用。 简单来说,编译内核会执行以下两步骤,它们分别干了以下的事情。 7 / 56 1 一般的,我们会拷贝一个对应体系结构的配置文件到主目录下并改名为 .config,这样就在 make menuconfig 生成的图形配置中 已经有了一些默认的配置,减少用户的劳动量。不过这一步不做也没关系的。 menuconfig 、由总 Makefile决定编译的体系结构 (ARCH). 编译工具 (CROSS_COMPILE),并知道需要进去哪些内核根下的哪些目录进行编译。 、由 arch/$(ARCH)/Makefile,决定 arch/$(ARCH)下还有 的哪些目录和文件需要编译。 、知道了需要编译的目录后,递归的进入哪些目录下,读取每一个 Kconfig 的信息,生成了图形配置的界面。 、通过我们在图形配置界面中选项为 *、 M或者 。 、保存并退出配置,会根据配置生成一份新的配置文件 .config,并在同时生成 8 / 56 include/config/。文件里面保存着 CONFIG_XXXX 等变量应该取 y还是取 m。 、根据 Makefile包含的目录和配置文件的要求,进去个子目录进行编译,最后会在各子目录下 生成一个 .o 或者 .a文件,然后总 Makefile 指定的连接脚 本 arch/$(ARCH)/kernel/生成 vmlinux,并通过 压缩编程bzImage,或者按要求在对应的子目录下编译成 模块。 但是,具体是怎么生成配置文件的呢? 注:我使用的内核是。 1.在 总 Makefile 中,根据以下语句进入需要编译的目录 470 # Objects we will link into vmlinux / subdirs we need to visit 471 init-y := init/ 9 / 56 472 drivers-y := drivers/ sound/ firmware/ 473 net-y := net/ 474 libs-y := lib/ 475 core-y := usr/ 476 endif # KBUILD_EXTMOD 639 core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/ 上面说明了,根目录下的 init、 driver、 sound、 firmware、net、 lib、 usr 等目录,在编译时都会进去读取目录下的Makefile并进行编译。 2.在总 Makefile 中包含的目录还是不够的,内核还需要根据对应的 CPU 体系架构, 决定还需要将哪些子目录将要编译进内核。在总 Makefile中 有一个语句: 529 include $(srctree)/arch/$(SRCARCH)/Makefile /在10 / 56 这里,我定义 SRCARCH = arm 可以看出,在总 Makefile 中进去读取相应体系 结构的Makefile-arch/$(SRCARCH)/Makefile 。 arch/$(SRCARCH)/Makefile中指定 arch/$(SRCARCH)路径下的哪些子目录需要被编译。 在 arch/arm/Makefile 下: 95 head-y := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_ 187 # If we have a machine-specific directory, then include it in the build. 188 core-y += arch/arm/kernel/ arch/arm/mm/ arch/arm/common/ 189 core-y += $(machdirs) $(platdirs) 190 core-$(CONFIG_FPE_NWFPE) += arch/arm/nwfpe/ 191 core-$(CONFIG_FPE_FASTFPE) += $(FASTFPE_OBJ) 11 / 56 192 core-$(CONFIG_VFP) += arch/arm/vfp/ 193 194 drivers-$(CONFIG_OPROFILE) += arch/arm/oprofile/ 195 196 libs-y := arch/arm/lib/ $(libs-y) linux设备驱动归纳总结: 2.操作硬件 IO内存 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 在之前章节的驱动,都没有对硬件进行操作,接写来将从我之前学的裸板驱 动开始,讲解在 linux系统下如何访问硬件。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 一、 IO端口与 IO 内存 12 / 56 介绍之前可以看看以下的博客: http:/u2/66435/showart_ x86体系和 ARM体系的寻址方式是有差别的: 在 x86下,为了能够满足 CPU高速地运行,内存与 CPU之间通过北桥相连并通过地址方式访问,而外设通过南桥与 CPU相连并通过端口访问。 在 ARM下也实现了类似的操作,通过两条不同的总线来连接不同访问速度的外设。但是它与 x86不同,无论是内存还是外设, ARM 都是通过地址访问。 因为这两种访问方式的不同, linux 分出了两种不同的访问操作: 以地址 方式访问硬件 使用 IO内存操作。 以端口方式访问硬件 使用 IO端口操作。 13 / 56 在 ARM下,访问寄存器就像访问内存一样 从指定的寄存器地址获取数据,修改。所以, ARM 下一般是使用 IO内存的操作。但这并不是说 IO 端口的操作在 ARM 下不能用,它们的代码差不多,只是没有使用的必要,下面也将介绍 IO 内存操作。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 二、如何使用 IO内存获得硬件的地址 之前已经说过,不能在 linux使用实际的物理地址,要对指定的物理地址进行操作,必须要先将物理地址与虚拟地址对应,通过虚拟地址访问。于是有了以下的物理地址映射函数: #include void *ioremap(unsigned long phys_addr, unsigned long size); 其实这也是上一节介绍的内存分配的一种方式,它同样会建立新页表来管理虚拟地址。函数传入两个参数,需要访问的14 / 56 物理内存的首地址 phys_addr和这段内存区域的大小 size,返回与该段物理地址对应的虚拟地址。这段地址可以多次被映射,当然,每次映射的虚拟地址也不一样。 对应的也有撤销映射关系的函数: void ioumap(void *addr); 接下来,我将会从一个裸板的 ARMled驱动开始,讲解 linux下的操作和裸板有什么不一样。 我的 ARM 裸板程序是在 linux 下编写的,我不知道这跟 win下使用 ADS有什么区别,在裸板驱动中,一般我是通过这样的办法来操作寄存器的: 首先,先给个地址定义个容易记的名字: #define GPECON *(volatile unsigned long *) 0x56000040 接着,我就要操作这个 GPECON寄存器了: *GPECON &= (3 *GPECON |= (1 可以看到,操作15 / 56 寄存器其实就是拿个地址出来进行操作。其实在 linux下也是一样,只是操作的时候不能使用物理地址,需要用映射出来的虚拟地址。 上个函数,这个程序我将要点亮连在我开发板上的 led灯,这个灯接在我开发板的 GPE12上,如果需要下载程序运行,需要改一下接口。 /*5th_mm_2/1st/*/ 1 #include 2 #include 3 4 #include /上面介绍的函数需要包含该头文件 5 6 volatile unsigned long virt, phys; /用于存放虚拟地址和物理地址 16 / 56 7 volatile unsigned long *GPECON, *GPEDAT, *GPEUP; /用与存放三个寄存器的地址 8 9 void led_device_init(void) 10 11 phys = 0x56000000; /1、指定物理地址 12 virt = (unsigned long)ioremap(phys, 0x0c); /2、通过 ioremap获得对应的虚拟地址 13 /0x0c 表示只要 12字节的大小 14 GPECON = (unsigned long *)(virt + 0x40); /3、指定需要操作的三个寄存器的地址 15 GPEDAT = (unsigned long *)(virt + 0x44); 17 / 56 16 GPEUP = (unsigned long *)(virt + 0x48); 17 18 19 void led_configure(void) /led 配置函数 20 21 *GPECON &= (3 22 *GPECON |= (1 23 24 *GPEUP |= (1 25 26 27 void led_on(void) /点亮 led 28 29 *GPEDAT &= (1 30 18 / 56 31 32 void led_off(void) /灭掉 led 33 34 *GPEDAT |= (1 35 36 37 static int _init test_init(void) /模块初始化函数 38 39 led_device_init(); 40 led_configure(); 41 led_on(); 42 printk(hello led!n); 19 / 56 43 return 0; 44 45 46 static void _exit test_exit(void) /模块卸载函数 47 48 led_off(); 49 iounmap(void *)virt); /注意,即使取消了映射,通过之前的虚拟地址还能访问硬件, 50 printk(byen); /但不是肯定可以,只要该虚拟地址被内核改动后就不行了。 51 52 53 module_init(test_init); 20 / 56 54 module_exit(test_exit); 55 56 MODULE_LICENSE(GPL); 57 MODULE_AUTHOR(xoao bai); 58 MODULE_VERSION(); 从上面的程序可以看到,除了获得地址有点和裸板驱动不一样外,寄存器的操作还是一样的。 接下来验证一下: root: 1st# insmod hello led! /这时候灯亮了 root: 1st# rmmod test 21 / 56 bye /灯灭了 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 三、改进函数,使用更好的内存访问接口 为了实现更好的移植性,上面的程序就有缺陷了。内核建议,尽量使用内核提供的内存访问接口: #include /从内存读取数据 ,返回值是指 定内存地址中的值 unsigned int ioread8(void *addr) unsigned int ioread16(void *addr) unsigned int ioread32(void *addr) /往指定内存地址写入数据 22 / 56 void iowrite8(u8 value, void *addr) void iowrite16(u16 value, void *addr) void iowrite32(u32 value, void *addr) 一般常用的是 32位内存存取接口。 接下 来就改进一下函数,其实实质没有改变,上面的函数是根据对应的平台体系结构编写的,这样可以提高驱动的移植性。 /*5th_mm_2/1st/*/ 1 #include 2 #include 一般的,用户空间使用函数 malloc 在堆上分配内存空间,同样的,在内核空间同样有一套类似的函数来分配空间。下面的知识会涉及页式管理的内存机制,如果不懂的要先复习一下,在 S3C2440 数据手册的 MMU 部分有介绍。 23 / 56 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 一、内核空间和用户空间有什么不同 学 c 语言的时候应该学过,从用户空间看,每个进程都傻乎乎的以为自己有 4G的内存空间,其中位于高地址的 1G空间给内核用,另外的 3G 都是它一个人独占的。所以用户空间很慷慨的把 3G 的空间分了好几个区域,如堆、栈、代码段等。其中, malloc()分配的空间位于堆,而程序中的自动变量,如你在函数内定义的 “int i” ,它是放在栈上,同时。用户空间的栈是可变栈,即随着数据的增多,对应函数的栈空间也会增多。 跟每个用户空间的进程不一样,内核只有 1G的空间,同时,除了自己本身有进程运行外,内核还要允许用户空间进程调用系统调用进入内核空间去执行。所以,内核对此相当吝啬,它规定在内核中的每个进程都只有 4KB或 8KB的定长栈。出于这样的原因,大的数据结构就不能在栈中分配,只能请求内核分配新的空间来存放数据,如函数 kmalloc()。 24 / 56 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 二、内存的基本单位是字节吗? 在介绍分配内存空间的函数前,我们还要了解一下内存是怎么被划分的。 内核不仅知道用户空间中看到的 1G 内核空间是假的,它还知道实际的物理内存是多少。所以,内核的其中一个任务就是,当这段虚假内存中的数据需要调用时,内核把这段虚拟内存与实际的物理内存对应上,运行完后又把两段内存的对应关系撤销掉给另外的虚拟内存用。 既然知道虚拟内存与物理内存的关系,那它们是怎么对应的,难道是一个一个字节?如果这样子做的话内核肯定觉得崩溃。 页是内存管理的基本单位。内存管理器通常以页为单位进行出来。页是内存管理的最小单位。在 32 位的系统中,一页的大小为 4KB。所以, 64M 的物理内存将被分为 16384个页。每一个物理页对应地用一个 struct page 来维护,注意,该25 / 56 结构体是用来维护物理页,而不是虚拟也,结构体记录该页是否被使用,对应的虚拟地址是多少等信息。 由于内存访问的限制,内核又把内存分成了 3个区。 如有些硬件的访问只能在 24 位的地 址空间寻址,出于这总访问限制, linux 把前 16MB 划分为 ZONE_DMA 用于直接内存访问。 在 x86 体系里,高于 896M 的内存空间称为高端内存,这段内存区域的页和普通的内存页操作后有差异,这段区域划分为 ZONE_HIGHMEM。 剩下的,加载这两段区域之间的就是我们平时用的普通 内存区域 ZONE_NORMAL。 这这里要注意一下: 1)这些 分区是指 linux 自己分的,当然,如果普通分区不够用,当然也可以占用其他区的空间。 26 / 56 2)分区的大小是根据体系结构而定的,一般的 ARM 下,ZONE_NORMAL 就是所有的可用内存区域。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 三、分配内存时使用的标记 gfp_mask 在讲如何分配内存之前,先讲一下分配内存时将会用到的gfp_mask。简单地讲,这个标记指定了分配内存时的要求。具体分三类: 行为修饰符:表示内核应当如何分配内存,如指定不能休眠等。 区修饰符:指定内存将要分配到上面讲的三个区中的哪一个。 类型标记:这包含了上面两种修饰符,这些标记是为了让用户更好地去使用。 27 / 56 标记有很多,我这里不一一介绍,需要的可以自己查阅 linux内核设计与实现 P238页。这里我讲两个常用的类型标记: 1) GFP_KERNEL:最常用的标记,用于可睡眠的进程上下文。 2) GFP_ATOMIC:使用了这个标记,内存分配函数不会引起随眠。 3) GFP_USER:当需要给用户空间分配内存空间时使用该标记。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 四、分配内存的第一种方法 按页分配 这是内核提供的一种请求内存的底层机制,都是以页为单位分配内存。以下函数包含在 这分为两个步骤: 28 / 56 1、请求内核 分配页,获得物理页对应的结构体 struct page: static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) 使用: 该函数用于申请 (1 返回值: 成功返回一个指针,指向这连续物理页的第一个 struct page结构体,失败返回 NULL。 2、分配页后还不能直接用,需要得到该页对应的虚拟地址: void *page_address(struct page *page) 其实这个函数就是获取 page 的成员 virtual,但千万不要直接访问,需要使用这个函数。函数返回的是物理页对应的虚拟地址,注意,如果你申请了多个物理页,分配的物理页是连续的,对应的虚拟地址也是连续的。 上面的两个步骤其实可以合成一个函数: unsigned long _get_free_pages(gfp_t gfp_mask, 29 / 56 unsigned int order) 这个函数的传参和 alloc_pages 的一样,不过它直接返回申请的物理页对应的虚拟地址。 当然,无论使用上面的哪种方法,当内存不用时,需要调用函数释放: 1、如果你使用上面的第一种方法: void _free_pages(struct page *page, unsigned int order) 2、如果你使用的是第二种方法: void free_pages(unsigned long addr, unsigned int order) 下面来个程序: /*5th_mm/5th_mm_1/1st/*/ 1 #include 30 / 56 2 #include 3 4 #include 5 6 struct page *p; 7 char *s; 8 9 static int _init test_init(void) /模块初始化函数 10 11 unsigned long virt, phys; 12 31 / 56 13 #define SWITCH 0 /通过定义这个来切换校验这两种不同的方法 14 #if SWITCH 15 /alloc 2 pages 16 p = alloc_pages(GFP_KERNEL, 1); 17 if (NULL = p) /必须检验错误 18 printk(alloc page error!n); 19 return - ENOMEM; 20 21 s = page_address(p); 22 #else 32 / 56 23 s = (char *)_get_free_pages(GFP_KERNEL, 1); 24 if (NULL = s) 25 printk(alloc page error!n); 26 return - ENOMEM; 27 28 #endif 29 30 phys = _pa(unsigned long)s); /通过虚拟地址获得对应的物理地址 31 virt = (unsigned long)_va(phys); /通过物理地址获得对应的虚拟地址 32 printk(virtual, s%pn, s); /打印获得的虚拟地址 33 printk(%pn, (void *)phys); /打印对应的物理地址 33 / 56 34 printk(%pn, (void *)virt); /再打印虚拟地址,其实就是分配函数返回的地址 35 36 memcpy(s, hello mm, 20); 37 38 printk(hello kerneln); 39 return 0; 40 41 42 static void _exit test_exit(void) /模块卸载函数 不管我们 学习什么编程语言,和我们见面的第一个程序就是“hello world ! ” 相信各位道上的朋友都遇到过这种个程序! 34 / 56 学习驱动程序也不例外,我学的第一个驱动程序就是“hello world ! ” 具体的程序代码如下: #include #include MODULE_LICENSE(Dual BSD/GPL); static int hello_init(void) printk(KERN_ALERTHello, world! n); return 0; static void hello_exit(void) 35 / 56 printk(KERN_ALERTbyby FriendyARM mini2440!n); module_init(hello_init); module_exit(hello_exit); 将其复制到工作目录下,并编写一个简单的 Makefile 文件: 由于每个人使用的 Linux系统不一样且每个人内核源代码所存放的位置也不是一样的。所以编写 Makefile文件的时候,参考别人的进行修改是一个很不错的 的学习 Makefile 文件的方法。当然你能把 Linux 内核的Makefile文件了解一下,对你了解 Linux 内核有很大的帮助的。 学习心得: 36 / 56 1、驱动模块运行在内核空间,运行是不能依赖任何函数库和模块连接,所以在写驱动程序的时候 所调用的函数只能是作为内核一部分的函数。 2、驱动模块和应用程序的一个重要不同是:应用程序退出时可不管资源释放或者其他的清除 工作,但模块的退出啊哈念书必须仔细撤销初始化函数所做的一切,否则,在系统想重新引导之前某些 东西就会残留在系统中。 3、处理器的多种工作模式其实就是为了操作系统的用户空间和内核空间设计的,在 Unix类的操作系统 中只是用到了两个级别:最高级别和最低级别。 4、要十分注意驱动程序的并发处理。在 Linux 驱动程序中必须解决的一个问题就是多个进程对共享资源的并发访问 .Linux 对解决并发访问可能导致的竟态问题提供了几种机制:中断屏蔽、原子操作、自旋锁、信号量等机制。 37 / 56 5、内核 API 中具有下划线的函数,通常是接口的底层组件,应该慎用。 6、内核代码不能实现浮点运算。内核中没有提供一套进行浮点运算的完整的环境。 7、 Makefile文件的分析: obj-m := 代表了我们要构建的模块名为, make会子啊该目录下自动找到文件进行编译。如果文件是有其他的源文件生成 (比如和 )的,则在下面加上: hello-objs := . $(MAKE) -C $(KERNELDIR) M=$(PWD) modules 其中 -C $(KERNELDIR)指定了内核源代码的位置,其中保存有内核的 顶层 makefile 文件。 M=$(PWD) 指定了模块源代码的位置 38 / 56 modules 目标指向 obj-m 变量中设定的模块 8、 insmod 使用公共内核符号表来解析模块中未定义的符号,公共内核符号表中包含了的、所有的全局内核项 (即函数和变量的地址 ),这是实现模块化驱动程序所必须的。 9、 Linux使用模块层叠技术,我们可以将模块划分为多个层次,通过简化每个层可以缩短开发周期。如果一个模块需要向其他模块导出符号,则使用下面宏: EXPORT_SYMBOL(name); EXPORT_SYMBOL_GPL(name); 符号必须子啊模块文件的全局变量部分导出,因为这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。 10、所有的模块代码都必须包含下面两个头文件: #include 39 / 56 #include 11、所有模块代码都应指定所使用的许可证: MODULE_LICENSE(Dual BSD/GPL); 12、初始化和关闭 初始化的实际定义通常是: staticint _ _init initialization_function(void) /*初始化代码 */ module_init(initialization_function) 清除函数的实际定义是: 40 / 56 static int _ _exit cleanup_function(void) /*清除代码 */ module_exit(cleanup_function) 13、还有一些是可选的其他的描述型的定义: MODULE_AUTHOR(); MODULE_DESCRIPTION(); MODULE_VERSION(); MODULE_ALIAS(); MODULE_DEVICE_TABLE(); 41 / 56 这些模块的声明习惯性的放在模块程序的最后面。 14、 Linux 内核模块的初始化出错处理一般使用 “goto” 语句,通常情况下很少使用 “goto” ,但是出错处理是 (可能是唯一的情况 ),它却非常的有用。 在大一学习 C语言的时候,老师就建议不要使用 “goto” 语句,并说很少会用到,在这里遇到 第一个建议使用 “goto”语句的。在追求效率的代码中使用 goto 语句一直是最好的错误恢复机制。下面是我截下来的一段关于使用 goto 语句实现错误处理的程序: struct something*item1; struct somethingelse*item2; int stuff_ok; void my_cleanup(void) 42 / 56 if (item1) release_thing(item1); if (item2) release_thing2(item2); if (stuff_ok) unregister_stuff(); return; int _init my_init(void) int err= -ENOMEM; 43 / 56 item1= allocate_thing(arguments); item2= allocate_thing2(arguments2); if (!item2| !item2) goto fail; err= register_stuff(item1, item2); if (!err) stuff_ok= 1; else goto fail; return 0; /* success*/ fail: my_cleanup( ); return err; 44 / 56 Linux字符设备驱动 1、 字符设备 cdev结构体 在内核中使用 cdev 结构体描述字符设备, cdev 结构体定义如下: struct cdev struct kobject kobj; /内嵌的 kobject对象 struct module *owner; /所属模块 struct file_operations *opt;/文件操作结构体,定义了字符 设备驱动提供给虚 拟文件系统的接口函数 struct list_head list; 45 / 56 dev_t dev; /设备号 ?MAJOR(dev_t dev) 从 dev_t获得主设备号 ? ?MINOR(dev_t dev) 从 dev_t获得次设备号 ?MKDEV(int major, int minor) 通过主次设备号生成 dev_t? unsigned int count; ; 操作 cdev函数: 内核提供了一组函数用于操作 cdev 结构体,如下: void cdev_init(struct cdev *, struct file_operation *); 用于初始化 cdev的成员,并建立和 file_operation 之间的连接。 struct cdev *cdev_alloc(void); 用于动态申请一个cdev内存。 46 / 56 void cdev_put(struct cdev *p); int cdev_add(struct cdev *,dev_t, unsigned); 向系统添加一个 cdev void cdev_del(struct cdev *); 从系统删除一个 cdev 申请设备号: 在调用 cdev_add()函数向系统注册字符设备之前,调用申请函 数 向 系 统 申 请 设 备 号 。 int register_chrdev_region(dev_t from, unsigned count, const char *name);用于已知设备号的情况; int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name); 用于设备号未知,向系统动态的申请未被占用的设备号的情况; 释放设备号: void unregister_chrdev_region(dev_t from, unsigned count);在调用 cdev_del()函数从系统注销字符设备之后调用以释放原先申 请的设备号。 47 / 56 file_operation 结构体: file_operationh 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的open()、 write()、 read()、 close()等系统调用时最终被调用,这里对其主要的成员进行介绍: loff_t (*llseek)(struct file *,loff_t,int);用来修改一个文件的当前读写位置,并将新位置返回,在出错时,返回一个负值。对文件的起始位置可以是文件开头 、当前位置和文件结尾, loff_t为偏移量。 ssize_t (*read)(struct file *,char _user *,size_t, loff_t *);用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。 ssize_t (*write)(struct file *,const char _user *,size_t, loff_t *);用来向设备发送数据,成功时返回写入的字节数,如果未实现,当用户进行 write()系统调用时,48 / 56 将得到 -EINVAL返回值。 int (*ioctl) (struct inode *, struct file *,unsigned int, unsigned long);提供设备相关控制命 令的实现,当调用成功时,返回给系统一个非负值。内核本身识别部分控制命令,而不必调用设备驱动中的 ioctl()。如果设备不提供ioctl()函数,对于内核不能识别的命令,用户进行 ioctl()系统调用时将获得 -EINVAL 返回值。其中第三个参数是事先定义的 I/O命令( Linux系统建议如下表方式定义 ioctl() 2、 字符设备驱动 在 li
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 泸州职业技术学院《建筑给水排水工程实验》2024-2025学年第一学期期末试卷
- 2025年物资供应链管理与操作实务试题集
- 2025年金融分析师备考指南与模拟题集
- 2025年特岗教师招聘考试备考指南高中音乐版
- 南阳科技职业学院《画法几何与工程制图一》2024-2025学年第一学期期末试卷
- 2025年特岗教师招聘考试初中地理模拟试题及答案
- 广西机电职业技术学院《外事礼仪》2024-2025学年第一学期期末试卷
- 四川现代职业学院《Orace数据库设计》2024-2025学年第一学期期末试卷
- 贵州省黔东南苗族侗族自治州2025届高三2月联考生物试题(解析版)
- 2025年心理咨询师高级面试指南及预测题详解
- 2023-2028全球及中国热保护器行业市场调研及投资前景分析报告3篇
- GB/T 22085.1-2008电子束及激光焊接接头缺欠质量分级指南第1部分:钢
- 高二下学期期末化学试卷及答案解析
- GB/T 10238-2015油井水泥
- 建筑工程技术标通用
- 临床执业助理医师呼吸系统
- 建设生态文明ppt模板课件
- T∕CGMA 033001-2018 压缩空气站能效分级指南
- 《创新方法》课程教学大纲
- REFLEXW使用指南规范.doc
- 赛摩6001B皮带校验说明书
评论
0/150
提交评论