Linux应用基础

📅 2026/7/23 19:33:15 👁️ 阅读次数 📝 编程学习
Linux应用基础

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命令查看空洞文件时,其大小显示为4K,du命令查看到的 大小是文件实际占用存储块的大小。

3.7 O_APPEND和O_TRUNC标志

O_APPEND标志是,调用open函数打开文件, 当每次使用write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末 尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。用法为fd=open("./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函数可以自行指定,如100,api用法为fd2=dup2(fd1,100),而dup函数的api用法为:fd2=dup(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个gpiochipX(X等于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为0;label代表名字;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的文件夹(gpioX,X表示对应的编号)。如下图:

当用完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目录下生成对应的设备节点(设备文件),设 备节点名称通常为eventX(X表示一个数字编号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 | hexdump

4.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; } /* value=1:按键按下 */ if (ev.value == 1) { /*业务逻辑*/ } /* value=2:长按重复事件 */ else if (ev.value == 2 ) { /*业务逻辑*/ } /* value=0:按键松开,重置所有按键状态标志 */ else if (ev.value == 0) { /*业务逻辑*/ }