
1. 基础概念1.1 系统调用系统调用实际上是Linux内核为上层应用程序提供的API接口方便应用程序进行调用类似于SVC。1.2 库函数库函数是应用层里边的东西在系统调用的上层通常以动态库文件.so形式提供。与系统调用区别是库函数是属于应用层而系统调用是内核提供给应用层的编程接口属于系统内核的一部分 库函数运行在用户空间调用系统调用会由用户空间用户态陷入到内核空间内核态 库函数通常是有缓存的而系统调用是无缓存的所以在性能、效率上库函数通常要优于系统调 用 可移植性库函数相比于系统调用具有更好的可移植性。2. I/O基础open/write/read/close等主要说一下lseek。lseek函数用于设置文件读写偏移量其他详细信息看手册。3. Linux系统下的文件管理3.1 静态文件与inode静态文件电脑硬盘、移动硬盘、U 盘等外部存储设 备文件存放在磁盘文件系统中并且以一种固定的形式进行存放我们把他们称为静态文件。inode实际上是一个结构体记录了文件的不同信息并且一个inode对应一个文件多个inode组成一个inode表。可通过命令“ls -i”查看inode号。3.2 磁盘分区磁盘分为数据区和inode区数据区用于存储文件中的数据inode区用于存放inode表。快速格式化仅删除了u盘的inode区但是没有删除数据区因此可以找回数据。3.3 文件状态文件打开过程首先系统根据文件名找对文件对应的inode编号然后通过该编号去inode表里边寻找对应的inode结构体最后根据结构体里边记录的信息确定文件的数据所在的扇区最后读出数据。文件打开状态调用open函数去打开文件的时候内核会申请一段内存一段缓冲区并且将静态文件的数 据内容从磁盘这些存储设备中读取到内存中进行管理、缓存也把内存中的这份文件数据叫做动态文件、内 核缓冲区。打开文件后以后对这个文件的读写操作都是针对内存中这一份动态文件进行相关的操作 而并不是针对磁盘中存放的静态文件。 当我们对动态文件进行读写操作后此时内存中的动态文件和磁盘设备中的静态文件就不同步了数据 的同步工作由内核完成内核会在之后将内存这份动态文件更新同步到磁盘设备中。进程控制块PCB是一个结构体用于记录进程的状态信息、运行特征等。文件描述符表、文件表及inode之间的关系如下图所示PCB中有一个指针指向文件描述符表该表中的每个元素对应一个文件表文件表中存在inode指针指向inode用于读取文件数据。3.4 perror函数该函数用于查看详细的报错信息。比如当调用open函数打开某文件时如果失败的话回返回-1但是并不知道错误的详细原因因此采用这个函数即可了解其用法如下所示如果失败会打印 “open error加具体原因” 。3.5 exit、_exit、_Exit函数这三个函数都用于结束进程类似于return。调用_exit()函数会 清除其使用的内存空间并销毁其在内核中的各种数据结构关闭进程的所有文件描述符并结束进程、将 控制权交给操作系统。_exit()和_Exit()两者等价。exit()函数_exit()函数都是用来终止进程的exit()是一个标准C库函数而_exit()和_Exit()是系统调用。 执行exit()会执行一些清理工作最后调用_exit()函数。一般用exit()。用法如下3.6 空洞文件比如说现在一个文件a大小是4K然后我调用了lseek函数将读写位置偏印到文件头部6k个字节处写数据那么此时0-4k处有数据6k处之后有数据但是4k-6k处没有数据。该区域就成为文件空洞区域该文件也被称为空洞文件。空洞文件逻辑大小和物理大小ls命令查看到空洞文件的大小是8K使用ls命令查看到的大小是文件的逻辑大小自然是包括了 空洞部分大小和真实数据部分大小当使用du命令查看空洞文件时其大小显示为4Kdu命令查看到的 大小是文件实际占用存储块的大小。3.7 O_APPEND和O_TRUNC标志O_APPEND标志是调用open函数打开文件 当每次使用write()函数对文件进行写操作时都会自动把文件当前位置偏移量移动到文件末尾从文件末 尾开始写入数据也就是意味着每次写入数据都是从文件末尾开始。用法为fdopen(./src_file,O_APPEND);O_TRUNC标志是如果使用了这个标志调用open函数打开文件的时候会将文件 原本的内容全部丢弃文件大小变为0。3.8 多次打开同一文件一个进程内多次open打开同一个文件那么会得到多个不同的文件描述符fd同理在关闭文件的 时候也需要调用close依次关闭各个文件描述符。一个进程内多次open打开同一个文件在内存中并不会存在多份动态文件。此处动态文件指的是调用open函数时会将磁盘上的数据读到一个缓存区这个区域的文件数据就叫动态文件。同一个文件被多次打开会得到多个不同的文件描述符也就意味着会有多个不同的文件表而文件读 写偏移量信息就记录在文件表数据结构中所以从这里可以推测不同的文件描述符所对应的读写偏移量是 相互独立的并没有关联在一起并且文件表中i-node指针指向的都是同一个inode。3.9 多次打开同一文件进行写操作重复打开同一个文件进行写操作譬如一个进程中两次调用open函数打开同一个文件分别得到两 个文件描述符fd1和fd2使用这两个文件描述符对文件进行写入操作那么它们是分别写各从各的位置 偏移量开始写还是接续写一个写完另一个接着后面写答案是分开写因为不同的文件描述符对应偏移量是相互独立的。如果想要重复打开同一个文件进行续写需要在调用open函数时增加O_APPEND标志这样就可以在文件的末尾进行续写。3.10 dup和dup2函数dup和dup2函数都是用于复制文件描述符fd代表一个文件。但不同得是dup函数有个缺陷其文件描述符值仅通过系统分配不能自行指定但是dup2函数可以自行指定如100api用法为fd2dup2(fd1,100)而dup函数的api用法为fd2dup(fd1)。接下来详细分析dup函数是复制文件描述符举例说明其作用当你写入一个数据如11223344到fd1以后调用fd2等于dup(fd1)此时将文件描述符fd1复制到fd2然后对fd2写入数据aabbccdd最后读取fd1发现得到的是11223344aabbccdd。通过上述例子表明通过复制文件描述符可以进行续写。note需要注意的是调用write函数和read函数会导致文件读写位置的偏移。举例读一个数据11223344一次读两个字节第一次读取的时候会读取1122但是第二次读取是3344而不是从头读起。4. GPIO应用编程Linux应用和驱动之所以说可以分成两个方向就是因为再做应用时根本不用关心底层的驱动实现细节。在linux下一切皆文件。应用层去操控底层的硬件同样也是通过文件I/O的方式有设备文件/dev、sysfs文件/sys两种方式。有些设备只能通过设备节点进行操控而有些设备只能通过sysfs方式进行操控一般简单设备会使用sysfs 方式操控如LED、GPIO等比较复杂的设备会采用设备节点的方式比如LCD、摄像头等。下边先介绍sysfs文件操控硬件方式。4.1 sysfs文件sysfs是一个虚拟文件系统作用是将内核信息以文件的方式提供给应用层使用可以把连接在系统上的设备和总线组织成一个分级的文件。其挂载在/sys目录下进入目录查看如下所示其中block是块设备的参在目录bus是设备总线类型分类放置的目录class是设备按照功能分类放置的目录dev是按设备号放置的目录devices是系统中所有设备存放的目录firmware是了内核中的固件fs是用于描述系统中所有文件系统kernel 这里是内核中所有可调参数的位置。 module这里有系统中所有模块的信息。power 这里是系统中电源选项有一些属性可以用于控制整个系统的电源状态。4.2 LED控制了解了sysfs文件系统基本概念后下边以LED为例看一下sysfs如何控制LED亮灭的。首先进入/sys/class/leds/sys-led/目录如下所示主要关注brightness、max_brightness以及 trigger 三个文件brightness:代表亮度该文件可读可写用于设置亮度等级0代表熄灭为正整数代表亮正整数越大亮度越亮。max_brightness该文件只读不可写用于获取最大亮度等级trigger触发模式可读可写读表示获取当前LED触发模式写用于设置什么方式触发如none为无触发、timer为闪烁等。用法如下int fd; fd open(/sys/class/leds/sys-led/brightness,O_RDWR); /*回到文件起始位置*/ lseek(fd, 0, SEEK_SET); /*写1亮写0灭*/ write(fd, 1, 1); write(fd, 0, 1);4.3 GPIO控制进入/sys/class/gpio目录如下所示可以看到该目录下包含两个文件export、unexport以及5个gpiochipXX等于0、32、64、96、128命名的文件夹。gpiochipX当前SoC所包含的GPIO控制器分别为GPIO1、GPIO2、GPIO3、GPIO4、GPIO5在这里分别对应gpiochip0、gpiochip32、 gpiochip64、gpiochip96、gpiochip128 这 5 个文件夹每一个文件夹用来管理一组GPIO如下所示主要看base、label、ngpio三个仅读。base代表gpiochipX中的X表示引脚最小编号如gpiochip0的base为0label代表名字ngpio代表这组gpio的管脚数。回到第一个图里边的export。这代表将指定的gpio引脚导出在使用GPIO引脚之前需要将其导出导出成功之后才能使用。注意export文件是只写文件不能读取。用法为在板子上输入# 导出编号为0的GPIO引脚对于imx6ull就是GPIO1_IO0 echo 0 export # 导出GPIO1_IO3 echo 3 export # 导出GPIO2_IO1,要注意编号 echo 33 export导出后就可以在在/sys/class/gpio 目录下生成了一个名为gpioX的文件夹gpioXX表示对应的编号。如下图当用完GPIO引脚之后还需要使用unexport删除导出的引脚如echo 0 unexport。导出之后就可以进入gpio0这个文件夹里边有几个属性文件帮助控制硬件如下所示direction配置GPIO引脚为输入或输出模式。该文件可读、可写读表示查看GPIO当前是输入 还是输出模式写表示将GPIO配置为输入或输出模式读取或写入操作可取的值为out输出模式和in输入模式。value在GPIO配置为输出模式下向value文件写入0控制GPIO引脚输出低电平写入1则 控制GPIO引脚输出高电平。在输入模式下读取value文件获取GPIO引脚当前的输入电平状态。active_low这个属性文件用于控制极性可读可写默认情况下为0。edge控制中断的触发模式该文件可读可写。在配置GPIO引脚的中断触发模式之前需将其设置为输入模式 非中断引脚echo none edge 上升沿触发echo rising edge 下降沿触发echo falling edge 边沿触发echo both edge 当引脚被配置为中断后可以使用poll()函数监听引脚的电平状态变化。下边介绍用法/*配置成输出模式导出的是gpio0代表GPIO1_IO0*/ int fd open(/sys/class/gpio/gpio0/direction,O_WRONLY); write(fd,out,3); /*配置gpio输出低电平*/ int fd open(/sys/class/gpio/gpio0/value,O_WRONLY); write(fd,0,1); write(fd,1,1);/*输出高电平*/4.4 输入设备控制Linux应用中通过input子系统管理如键盘、按键等输入设备。该系统实现了一套兼容所有输入设备的框架能屏蔽硬件的差异向应用层提供一套统一的接口。基于input子系统注册成功的输入设备都会在/dev/input目录下生成对应的设备节点设备文件设 备节点名称通常为eventXX表示一个数字编号0、1、2、3等譬如/dev/input/event0、/dev/input/event1、 /dev/input/event2 等通过读取这些设备节点可以获取输入设备上报的数据。4.4.1 获取输入设备上述数据流程以imx6ull上的按键KEY0为例流程为1、应用程序打开/dev/input/event2设备文件 2、应用程序发起读操作譬如调用read如果没有数据可读则会进入休眠阻塞I/O情况下 3、当有数据可读时应用程序会被唤醒读操作获取到数据返回 4、应用程序对读取到的数据进行解析。当无数据可读时程序会进入休眠状态也就是阻塞有数据可读时应用程序会被唤醒成功读取到数据。下边来看上图的最后一步流程解析数据。此外如果不确定自己的按键是event几的话可以运行下述命令进行查看4.4.2 应用程序解析数据在应用程序发起读操作会返回一个input_event类型的数据如下图所示struct input_event { struct timeval time; /*记录上报时间*/ __u16 type; /*上报类型*/ __u16 code; /*事件类型中的具体事件码*/ __s32 value; /*上报数据*/ };其中type指的是述发生了哪一种类型的事件。比如按键类事件type为EV_KEY(#define EV_KEY 0X01)其他的都定义在linux/input.h头文件中可以自行查看。因此在应用程序中需要包含此头文件。code表示该类事件中的哪一个具体事件如一个键盘上通常有很多按键譬如字母A、B、C、D或者数字1、2、3、4等而code变量则告知应用程序是哪一个按键发生了输入事件比如KEY0按下时code等于114.value表示内核每次上报事件都会向应用层发送一个数据对value值的解释随着code的变化而 变化。比如对于按键来说value等于1则表示KEY0键按下value等于0表示KEY0键松开如果value等于2表示KEY0键长按。也可以在板子上输入以下命令进行测试cat /dev/input/event2 | hexdump4.4.3 输入设备控制用法以按键输入为例如下图struct input_event ev;/*定义input_event结构体变量ev用于存储按键事件*/ ssize_t bytes read(trigger_fd_, ev, sizeof(ev));/*从trigger_fd_读取按键事件存储在ev中*/ if (bytes ! sizeof(ev))/*判断读取的字节数是否等于ev的大小不等于则返回*/ { return; } /* 仅处理 EV_KEY 类型且为目标按键的事件 */ if (ev.type ! EV_KEY || ev.code ! config_.trigger_key_code) { return; } /* value1按键按下 */ if (ev.value 1) { /*业务逻辑*/ } /* value2长按重复事件 */ else if (ev.value 2 ) { /*业务逻辑*/ } /* value0按键松开重置所有按键状态标志 */ else if (ev.value 0) { /*业务逻辑*/ }