2026/4/17 2:22:12
网站建设
项目流程
网站建设实训心得体会300字,大型企业门户网站能力建设探索与实践,服务流程企业网站,电子商务网站 注意Linux 驱动程序开发概述①应用程序、库、内核、驱动程序的关系以点亮一个 LED 为例#xff0c;这 4 层软件的协作关系如下所示#xff1a;应用程序使用库提供的 open 函数打开代表 LED 的设备文件。库根据 open 函数传入的参数执行“swi”指令#xff0c;这条指令会引起 CPU…Linux 驱动程序开发概述①应用程序、库、内核、驱动程序的关系以点亮一个 LED 为例这 4 层软件的协作关系如下所示应用程序使用库提供的 open 函数打开代表 LED 的设备文件。库根据 open 函数传入的参数执行“swi”指令这条指令会引起 CPU 异常进入内核。内核的异常处理函数根据这些参数找到相应的驱动程序返回一个文件句柄给库进而返回给应用程序。应用程序得到文件句柄后使用库提供的 write 或 ioclt 函数发出控制命令。库根据 write 或 ioclt 函数传入的参数执行“swi”指令这条指令会引起 CPU 异常进入内核。内核的异常处理函数根据这些参数调用驱动程序的相关函数点亮 LED。②驱动程序的分类和开发步骤1. 字符设备Character Device核心特征以字节流为单位读写类似文件访问无强制缓冲区要求可自行实现缓冲区提升效率。接口实现驱动需实现open、close、read、write等系统调用应用层通过/dev目录下的设备文件如/dev/ttySAC0访问。典型例子串口设备数据收发以单个字节为单位进行。2. 块设备Block Device核心特征数据以固定块或页为单位存储如NAND Flash按页存储。用户层接口与字符设备一致通过设备文件如/dev/mtdblock0、/dev/hda1调用标准系统调用用户无感知差异特殊之处硬件操作逻辑需先将用户数据组织成块再读写或从设备读块数据后提取用户所需内容。数据格式与内核接口块设备需支持文件系统格式驱动除面向用户层接口外还需向内核提供底层接口支持文件系统挂载mount。块设备本身仅负责 “按固定大小块存储原始数据”类似仓库的货架格子只存东西不分类别但杂乱的数据块无法被用户识别不知道哪个块对应哪个文件。文件系统就是 “整理规则”定义数据块的组织方式如文件名、权限、存储位置、目录结构比如 EXT4、FAT32 就是不同的规则。挂载的本质是 “将文件系统规则绑定到块设备”让内核知道 “如何解读这个块设备里的数据”。而驱动的内核层接口就是 “绑定的关键”文件系统需要通过驱动提供的底层接口实现对块设备的读写控制如读取超级块、分配数据块没有这些接口文件系统无法操作块设备自然无法挂载。3. 网络接口Network Interface核心特征兼具字符/块设备部分特点无法归入前两类数据以非固定大小的报文/包/帧为单位传输。访问方式分配唯一名称如eth0但/dev目录下无对应节点通信不依赖open、readwrite而是通过内核/库提供的数据包传输专用函数。标准开发流程硬件调研查看原理图、设备数据手册明确硬件操作逻辑如寄存器地址、读写时序。模板选型内核中寻找相近驱动作为模板优先复用现有框架特殊场景需从零开发。初始化实现向内核注册驱动使内核能通过应用层传入的文件名匹配到对应驱动。操作函数设计实现核心系统调用对应的函数如open、close、read、write封装硬件操作逻辑。中断服务可选若设备支持中断如按键、串口编写中断服务程序处理硬件中断事件。编译与加载将驱动编译进内核或通过insmod命令动态加载调试阶段优先动态加载。测试验证编写应用程序调用设备文件测试驱动功能完整性、稳定性及异常场景适配性。③驱动程序的加载和卸载可以将驱动程序静态编译进内核中也可以将它作为模块在使用时再加载。当使用 insmod 加载模块时模块的初始化函数被调用它用来向内核注册驱动程序当使用 rmmod 卸载模块时模块的清除函数被调用。在驱动代码中这两个函数要么取固定的名字init_module 和 cleanup_module要么使用以下两行来标记它们假设初始化函数、清除函数为 my_init 和 my_cleanup。宏作用module_init()告诉内核模块加载时调用哪个函数module_exit()告诉内核模块卸载时调用哪个函数module_init(my_init); module_exit(my_cleanup);字符设备驱动程序开发对于每个系统调用驱动程序中都有一个与之对应的函数。对于字符设备驱动程序这些函数集合在一个 file_operations 类型的数据结构中。file_operations 结构在 Linux 内核include/linux/fs.h 文件中定义。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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 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 *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *); 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 (*dir_notify)(struct file *filp, unsigned long arg); 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); };当应用程序使用 open 函数打开某个设备时设备驱动程序的 file_operations 结构中的open 成员就会被调用当应用程序使用 read、write、ioctl 等函数读写、控制设备时驱动程序的 file_operations 结构中的相应成员read、write、ioctl 等就会被调用。从这个角度来说编写字符设备驱动程序就是为具体硬件的 file_operations 结构编写各个函数并不需要全部实现 file_operations 结构中的成员。那么当应用程序通过 open、read、write 等系统调用访问某个设备文件时Linux 系统怎么知道去调用哪个驱动程序的 file_operations 结构中的 open、read、write 等成员呢1设备文件有主/次设备号。设备文件分为字符设备、块设备比如 PC 机上的串口属于字符设备硬盘属于块设备。在 PC 上运行命令“ls /dev/ttyS0 /dev/hda1-l”可以看到brw-rw---- 1 root disk 3, 1 Jan 30 2003 /dev/hda1 crw-rw---- 1 root uucp 4, 64 Jan 30 2003 /dev/ttyS0“brw-rw----”中的“b”表示/dev/hda1 是个块设备它的主设备号为 3次设备号为 1“crw-rw----”中的“c”表示/dev/ttyS0 是个块设备它的主设备号为 4次设备号为 64。2模块初始化时将主设备号与 file_operations 结构一起向内核注册。驱动程序有一个初始化函数在安装驱动程序时会调用它。在初始化函数中会将驱动程序的 file_operations 结构连同其主设备号一起向内核进行注册。对于字符设备使用如下以下函数进行注册int register_chrdev(unsigned int major, const char * name, struct file_ operations *fops);这样应用程序操作设备文件时Linux 系统就会根据设备文件的类型是字符设备还是块设备、主设备号找到在内核中注册的 file_operations 结构对于块设备为 block_device_operations 结构次设备号供驱动程序自身用来分辨它是同类设备中的第几个。编写字符驱动程序的过程大概如下。1编写驱动程序初始化函数。进行必要的初始化包括硬件初始化也可以放其他地方、向内核注册驱动程序等。2构造 file_operations 结构中要用到的各个成员函数。实际的驱动程序当然比上述两个步骤复杂但这两个步骤已经可以让我们编写比较简单的驱动程序比如 LED 控制。LED 驱动程序代码分析下面按照函数调用的顺序进行讲解模块的初始化函数和卸载函数如下86 /* 87 * 执行“insmod s3c24xx_leds.ko”命令时就会调用这个函数 88 */ 89 static int __init s3c24xx_leds_init(void) 90 { 91 int ret; 92 93 /* 注册字符设备驱动程序 94 * 参数为主设备号、设备名字、file_operations 结构 95 * 这样主设备号就和具体的 file_operations 结构联系起来了 96 * 操作主设备为 LED_MAJOR 的设备文件时就会调用 s3c24xx_leds_fops 中的相关成 员函数 97 * LED_MAJOR 可以设为 0表示由内核自动分配主设备号 98 */ 99 ret register_chrdev(LED_MAJOR, DEVICE_NAME, s3c24xx_leds_fops); 100 if (ret 0) { 101 printk(DEVICE_NAME cant register major number\n); 102 return ret; 103 } 104 105 printk(DEVICE_NAME initialized\n); 106 return 0; 107 } 108 109 /* 110 * 执行“rmmod s3c24xx_leds.ko”命令时就会调用这个函数 111 */ 112 static void __exit s3c24xx_leds_exit(void) 113 { 114 /* 卸载驱动程序 */ 115 unregister_chrdev(LED_MAJOR, DEVICE_NAME); 116 } 117 118 /* 这两行指定驱动程序的初始化函数和卸载函数 */ 119 module_init(s3c24xx_leds_init); 120 module_exit(s3c24xx_leds_exit); 121第 119、120 两行用来指明装载、卸载模块时所调用的函数。也可以不使用这两行但是需要将这两个函数的名字改为 init_module、cleanup_module。执行“insmod s3c24xx_leds.ko”命令时就会调用 s3c24xx_leds_init 函数这个函数核心的代码只有第 99 行。它调用 register_chrdev 函数向内核注册驱动程序将主设备号 LED_MAJOR 与file_operations 结构 s3c24xx_leds_fops 联系起来。以后应用程序操作主设备号为 LED_MAJOR 的设备文件时比如 open、read、write、ioctls3c24xx_leds_fops 中的相应成员函数就会被调用。但是s3c24xx_leds_fops 中并不需要全部实现这些函数用到哪个就实现哪个。执行“rmmod s3c24xx_leds.ko”命令时就会调用 s3c24xx_leds_exit 函数它进而调用unregister_chrdev 函数卸载驱动程序它的功能与 register_chrdev 函数相反。s3c24xx_leds_init、s3c24xx_leds_exit 函数前的“_ _init”、“_ _exit”只有在将驱动程序静态链接进内核时才有意义。前者表示 s3c24xx_leds_init 函数的代码被放在“.init.text”段中这个段在使用一次后被释放这可以节省内存后者表示 s3c24xx_leds_exit 函数的代码被放在“.exit.data”段中在连接内核时这个段没有使用因为不可能卸载静态键接的驱动程序。下面来看看 s3c24xx_leds_fops 的组成。76 /* 这个结构是字符设备驱动程序的核心 77 * 当应用程序操作设备文件时所调用的 open、read、write 等函数 78 * 最终会调用这个结构中的对应函数 79 */ 80 static struct file_operations s3c24xx_leds_fops { 81 .owner THIS_MODULE, /* 这是一个宏指向编译模块时自动创建的_ _this_ module 变量 */ 82 .open s3c24xx_leds_open, 83 .ioctl s3c24xx_leds_ioctl, 84 }; 85第 81 行的宏 THIS_MODULE 在 include/linux/module.h 中定义如下_ _this_module 变量在编译模块时自动创建无需关注这点。#define THIS_MODULE (_ _this_module)file_operations 类型的 s3c24xx_leds_fops 结构是驱动中最重要的数据结构编写字符设备驱动程序的主要工作也是填充其中的各个成员。比如本驱动程序中用到 open、ioctl 成员被设为 s3c24xx_leds_open、s3c24xx_leds_ioctl 函数前者用来初始化 LED 所用的 GPIO 引脚后者用来根据用户传入的参数设置 GPIO 的输出电平。s3c24xx_leds_open 函数的代码如下33 /* 应用程序对设备文件/dev/leds 执行 open()时 34 * 就会调用 s3c24xx_leds_open 函数 35 */ 36 static int s3c24xx_leds_open(struct inode *inode, struct file *file) 37 { 38 int i; 39 40 for (i 0; i 4; i) { 41 // 设置 GPIO 引脚的功能本驱动中 LED 所涉及的 GPIO 引脚设为输出功能 42 s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]); 43 } 44 return 0; 45 } 46在应用程序执行 open(“/dev/leds”,...)系统调用时s3c24xx_leds_open 函数将被调用。它用来将 LED 所涉及的 GPIO 引脚设为输出功能。不在模块的初始化函数中进行这些设置的原因是虽然加载了模块但是这个模块却不一定会被用到就是说这些引脚不一定用于这些用途它们可能在其他模块中另作他用。所以在使用时才去设置它我们把对引脚的初始化放在 open 操作中。第 42 行的 s3c2410_gpio_cfgpin 函数是内核里实现的它被用来选择引脚的功能。其实现原理就是设置 GPIO 的控制寄存器。s3c24xx_leds_ioctl 函数的代码如下47 /* 应用程序对设备文件/dev/leds 执行 ioclt()时 48 * 就会调用 s3c24xx_leds_ioctl 函数 49 */ 50 static int s3c24xx_leds_ioctl( 51 struct inode *inode, 52 struct file *file, 53 unsigned int cmd, 54 unsigned long arg) 55 { 56 if (arg 4) { 57 return -EINVAL; 58 } 59 60 switch(cmd) { 61 case IOCTL_LED_ON: 62 // 设置指定引脚的输出电平为 0 63 s3c2410_gpio_setpin(led_table[arg], 0); 64 return 0; 65 66 case IOCTL_LED_OFF: 67 // 设置指定引脚的输出电平为 1 68 s3c2410_gpio_setpin(led_table[arg], 1); 69 return 0; 70 71 default: 72 return -EINVAL; 73 } 74 } 75应用程序执行系统调用 ioclt(fd, cmd, arg)时fd 是前面执行 open 系统调用时返回的文件句柄s3c24xx_leds_ioctl 函数将被调用。第 63、68 行根据传入的 cmd、arg 参数调用 s3c2410_gpio_setpin 函数来设置引脚的输出电平输出 0 时点亮 LED输出 1 时熄灭 LED。s3c2410_gpio_setpin 函数也是内核中实现的它通过 GPIO 的数据寄存器来设置输出电平。驱动程序测试06 #define IOCTL_LED_ON 0 07 #define IOCTL_LED_OFF 1 … 16 int main(int argc, char **argv) 17 { … 24 fd open(/dev/leds, 0); // 打开设备 … 30 led_no strtoul(argv[1], 0, 0) - 1; // 操作哪个 LED … 34 if (!strcmp(argv[2], on)) { 35 ioctl(fd, IOCTL_LED_ON, led_no); // 点亮它 36 } else if (!strcmp(argv[2], off)) { 37 ioctl(fd, IOCTL_LED_OFF, led_no); // 熄灭它 38 } else { 39 goto err; 40 } … 50 }其中的 open、ioclt 最终会调用驱动程序中的 s3c24xx_leds_open、s3c24xx_leds_ioctl函数。-1 说明argv[1]是命令行参数如1用户习惯用1、2、3…驱动中一般用0、1、2…所以要-1最终使用以下命令点灯灭灯# led_test 1 on # led_test 1 off