Linux学习笔记5

Linux文件IO

对于传统的操作系统来说,普通的I/O 操作一般会被内核缓存(因为缓存位于CPU芯片上或者非常接近CPU,所以访问速度特别快,CPU内部的缓存(例如L1、L2、L3缓存)容量非常有限,通常只有几十KB到几MB),这种I/O 被称作缓存I/O

数据直接在磁盘和应用程序地址空间进行传输的文件访问的机制称作为直接I/O

可以把IO操作单纯地理解成读/写文件

缓存I/O

缓存I/O 又被称作标准I/O,大多数文件系统的默认I/O 操作都是缓存I/O。在Linux 的
缓存I/O 机制中,操作系统会将I/O 的数据缓存在文件系统的页缓存( page cache )中,
也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷
贝到应用程序的地址空间

缓存I/O 可以减少读盘的次数,从而提高性能

注释:

页缓存是操作系统中用于缓存磁盘数据的一种机制(技术)。它通过将磁盘上的数据按照页(通常是4KB大小)的单位加载到内存中,并在内存中维护一个映射表,来实现对数据的快速访问。

具体来说,当应用程序请求读取文件时,如果所需数据已经存在于页缓存中,操作系统就可以直接从内存中读取数据,而不必每次都去磁盘上读取,从而提高了读取速度。

内核缓存,通常是指数据或者信息被临时储存在CPU的缓存中,CPU内部有多层缓存,如L1、L2、L3等,它们使用SRAM技术实现,存取速度非常快,但是容量有限

  • L1缓存(一级缓存):这是距离CPU核心最近的缓存,也是最快的缓存类型。L1缓存通常非常小,但存取速度极快,用于存放CPU即时执行指令和数据。每个CPU核心通常都有自己的L1指令缓存和数据缓存。
  • L2缓存(二级缓存):L2缓存位于L1缓存和主内存之间,它的容量比L1大,但速度相对较慢。所有CPU核心通常共享单一的L2缓存,它有助于减少CPU与主内存之间的数据传输延迟。
  • L3缓存(三级缓存):L3缓存通常是多个CPU核心之间共享的最外层缓存,它的速度最慢,但容量最大,可以显著提高多核心处理器的数据共享效率。L3缓存的作用在于进一步减少主内存访问次数,提升处理大量数据时的性能。

  当应用程序尝试读取某块数据的时候,如果这块数据已经存放在了页缓存中,那么这块数
据就可以立即返回给应用程序。如果数据在应用程序读取之前并未被存放在页缓存中,那么就需要先将数据从磁盘读到页缓存中去。

对于写操作来说,应用程序也会将数据先写到页缓存中去,之后是否立即写到磁盘里要看应用程序
所采用的写操作机制:

·同步写机制:数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止

·延迟写机制:应用程序就完全不需要等到数据全部被写回到磁盘,数据只要被写到页缓存中去就可以了,在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上(存在数据丢失的风险)

对文件进行操作

在所有的linux 系统中,如果需要对文件的进行操作,只要包含如下4 个头文件即可。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
上面四个头文件中包含了打开,关闭,创建,读文件,写文件的函数。

open函数

使用open 函数的时候会返回一个文件句柄,open 函数可以建立一个到文件或者设备的访问路径,函数open 的两个原型如下

int open(const char *path, int oflags);        int open(const char *path, int oflags,mode_t mode);

第一个参数path 表示:路径名或者文件名。路径名为绝对路径名。第二个参数oflags 表示:打开文件所采取的动作——O_RDONLY 文件只读/O_WRONLY 文件只写/O_RDWR 文件可读可写

open函数使用实例

·先添加上刚刚说的四个头文件

·之后

注意到test1和test2调用open函数的差别:

之后进入前面实验创建的目录“/home/linuxsystemcode”,使用命令“mkdir iofile”新建iofile 文件夹,将源码open.c 拷贝进去,进入新建的文件夹iofileopen,使用命令“arm-none-linux-gnueabi-gcc -o open open.c -static”编译open 文件,成功后利用ls指令去查看会发现多了个open可执行文件

将这个文件拷贝到U盘,启动开发板,插入U 盘,加载U 盘

可以看见分别给对应的设备打印了句柄ID

之后可以在对应的bin目录下查看新建的“test2”

close函数

调用close 函数之后,会取消open 函数建立的映射关系,句柄将不再有效,占用的空间
将被系统释放。

int close(int fd);//参数fd,使用open 函数打开文件之后返回的句柄。

write函数

函数原型为ssize_t write(int fd,const void *buf,size_t count)
注释:参数fd,使用open 函数打开文件之后返回的句柄。
参数*buf,需要写入的数据。
参数count,将参数*buf 中最多count 个字节写入文件中。
返回值为ssize 类型,出错会返回-1,其它数值表示实际写入的字节数

eg:

注意,进行写操作之前,必须得到文件的句柄,所以我们要调用open函数(第十八行)。perror函数显示错误信息

最后使用vi 编辑器打开"/bin/testwrite"文件,可以看到这个文件中有字符Hello Write Function!.

read 函数

函数原型为ssize_t read(int fd,void *buf,size_t len)
注释:参数fd,使用open 函数打开文件之后返回的句柄。
参数*buf,读出的数据保存的位置。
参数len,每次最多读len 个字节。
返回值为ssize 类型,出错会返回-1,其它数值表示实际写入的字节数,返回值大于0 小
于len 的数值都是正常的。

字符设备控制

常见的字符设备包括键盘、打印机、鼠标、串口、控制台和LED等

在Linux系统中,字符设备在文件目录树中以设备文件的形式存在,并且拥有相应的节点。这意味着用户可以使用与普通文件相同的文件操作命令来与字符设备进行交互。

字符设备以字符为单位进行数据传输,这意味着它们在I/O操作时一次处理一个字节的数据。

eg:字符类led灯

Led 灯的设备节点在/dev 目录下

对于IO 口(这里的IO 口指的是硬件上的IO 口,
不是指IO 文件)的操作,Linux 专门设计了一个高效的函数ioctl

这个函数在头文件#include<unistd.h>中。
int ioctl( int fd, int request, int cmd);

注释:参数fd,函数open 返回的句柄。
参数request 和参数cmd,由内核驱动决定具体操作,例如request 可以代表那个IO 口,
cmd 代表对IO 进行什么样的操作

通过main 参数传过来的参数是char 字符格式的,如果要传递给ioctl 函数,需要用到数
值转化函数atoi,添加了头文件#include <stdlib.h>

代码分析:主要功能是往某个IO口写入一些数字,所以用ioctrl函数,这个函数使用之前要获取设备的fd句柄,所以要加上open函数

宏定义了两个参数,一个是LED_NUM,另一个是LED_C,代表灯的数量和命令,都是2,所以后续会对输入的参数进行判断,超过则输出报错

之后用open返回一个句柄,之后ioctrl调用这个句柄,然后关闭close

然后在/home/linuxsystemcode目录中新建charcontrol目录,将这个led.c文件拷贝到这里面,使用命令“arm-none-linux-gnueabi-gcc -o leds leds.c -static”编译leds 文件

将编译成的可执行文件leds,拷贝到U 盘,启动开发板,插入U 盘,加载U 盘,运行程
序如下

字符类ADC 模数转换

和led 灯类似,数模转换的设备节点也是在/dev 目录下,模数转换的硬件部分如下图所示

Vadc可以读取我们输入的电压,然后滑动变阻器会通过这个电压移动,V12/V13的比值和Vadc/VDD一样,所以有这个公式:电压Vadc=R12*VDD1V8_EXT/R13

代码:

代码分析:int fd:用于存储设备文件的文件描述符。
char *adc="/dev/adc":指向设备文件路径的指针。
char buffer[512]:用于存储从设备文件中读取的数据的缓冲区。
int len=0, r=0:分别用于存储读取到的数据长度和处理后的结果。

接下来,使用memset()函数将缓冲区清零,然后输出提示信息"adc ready!"。

然后,尝试以读写模式打开设备文件"/dev/adc",并将文件描述符赋值给fd。如果打开失败,输出错误信息"open adc err!";否则,输出成功信息"open adc success!"。

接着,使用read()函数从设备文件中读取最多10个字节的数据,并将其存储在缓冲区buffer中。如果读取到的长度为0,则输出"return null";否则,将缓冲区中的数据转换为整数,并进行一些处理(乘以10000再除以4095),将结果存储在变量r中。

之后进入前面实验创建的目录“/home/linuxsystemcode/charcontrol/”将源码ADC.c 拷贝进去

使用命令“arm-none-linux-gnueabi-gcc -o ADC ADC.c -static”编译ADC 文件,之后会生成可执行文件,将编译成的可执行文件open,拷贝到U 盘,启动开发板,插入U 盘,加载U 盘,运行程序

Linux串口编程

串口通信是指一次只传送一个数据位。虽然在通信的时候串口有8 位或者9 位等,但是在
物理层面传输的时候,它仍然是以单个bit 的方式传输的,串口一般特指RS232 标准的接口。

RS232是一种串行通信接口,最被广泛使用的是RS-232C,它将mark(on)比特的电压定义为-3V 到-12V 之间,而将space(off)的电压定义到+3V 到+12V 之间。

流控

用于数据协调,

软件流控制:软件流控制是通过特殊字符来控制数据流的开始和结束【开始(XON,DC1,八进
制数021)或者结束(XOFF,DC3 或者八进制数023)数据流】,这些字符通常是在ASCII编码中定义的。但是这种方式只适用于传输文本信息,不能用于其他类型的信息

硬件流控制:这种方法使用RS-232 标准的CTS 和RTS 信号来取代之前提到的特殊字符。当准备就绪时,接受一方会将CTS 信号设置成为space 电压,而尚未准备就绪时它会被设置成为mark 电压。相应得,发送方会在准备就绪的情况下将RTS 设置成space电压,可以用于传输任何类型的信息。

Linux串口编程的流程

开始,打开串口,初始化串口,发送信息,关闭

打开串口一般都用open函数,会返回一个句柄

初始化串口,串口需要配置波特率,数据位,校验位等等一系列的参数

因为串口属于字符设备(是一种比较特殊的字符设备),所以发送接收函数可以用read/write实现

设置启动文件"hello world"

确保U盘上有helloworld文件,然后插入U盘,并将U盘上的名为"helloworld"的文件夹拷贝到系统的"/bin"目录下。

使用"ls"命令确认拷贝操作是否成功。

使用"chmod 777 /bin/helloworld"命令修改"helloworld"程序的权限,确保可以执行。

使用"vi /etc/init.d/rcS"命令编辑系统启动文件。

在文件的最后一行添加"/bin/helloworld &",以在系统启动时执行"helloworld"程序。

保存并退出编辑器。

最后,设置超级终端保存系统启动日志,以便查找打印信息。可以通过控制面板中的日志设置来完成此操作。

重启开发板,查看是否能在启动日志中找到关键词"Hello world!",以确认程序是否成功开机启动。

设备节点的确定

在几乎所有的Linux 系统中,在dev 目录下都会有tty*的设备节点,如下图所示,启动开发板,在超级终端中,进入dev 目录,输入查找命令“ls tty*”

这个设备节点的确定要看你选哪个串口,我用的是itop4412,使用的是GPS_TXD,去原理图上找发现对应的是XuRXD3,所以可以确定设备节点是“ttySAC3”(反正用哪个就去原理图看是XuRXD几)

串口打开设备节点测试程序

uartopen.c头文件

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>

uartopen的main函数

void main(){
int fd;
char *uart3 ="/dev/ttySAC3";
if((fd = open(uart3,O_RDWRI|O_CREAT,0777))<0)
{printf("open gs failed!/n",uart3)};
else{
printf("open s is success!/n",uart3)};
close(fd);}

在Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode/”,使用命令“mkdir uartapp”新建uartapp 文件夹,将源码uartopen.c 拷贝进去

使用命令“arm-none-linux-gnueabi-gcc -o uartopen uartopen.c -static”编译生成uartopen 文件

将编译成的可执行文件uartopen,拷贝到U 盘,启动开发板,插入U 盘,加载U 盘,运
行程序,如下图所示,使用命令“./mnt/udisk/uartopen ”运行程序uartopen,打印出“open
/dev/ttySAC3 is success”说明串口打开成功,并没有被其它程序占用。

串口初始化
用到的函数介绍

1)函数tcgetattr 用于读取当前串口的参数值,需要用到头文件“#include <termios.h>”和“#include <unistd.h>”。
函数原型为int tcgetattr(int fd, struct termios *termios_p)。
参数1:fd 是open 返回的文件句柄。
参数2:*termios_p 下面注释里会提及并解释
使用这个函数前可以先定义一个termios 结构体,用于存储旧的参数

注释:

成员tcflag_t c_iflag:输入模式标志
成员tcflag_t c_oflag:输出模式标志
成员tcflag_t c_cflag:控制模式标志

成员tcflag_t c_lflag:本地模式标志
成员cc_t c_line:行间规则
成员cc_t c_cc[NCC]:控制特点

给串口初始化之前必须读取串口的句柄,也就是先要使用open 函数,可以正常返回fd

2)函数tcgetattr 用于读取当前串口的参数值,在实际应用中,一般用于先确认该串口是否能
够配置,做检测用。
需要用到头文件“#include <termios.h>”和“#include <unistd.h>”。
函数原型为int tcgetattr(int fd, struct termios *termios_p)。
参数1:fd 是open 返回的文件句柄。
参数2:*termios_p 是前面介绍的结构体。
使用这个函数前可以先定义一个termios 结构体,用于存储旧的参数

3)函数cfsetispeed 和cfsetospeed 用于修改串口的波特率

4)函数cfgetispeed 和cfgetospeed 可以用于获取当前波特率。

注释:波特率相关的函数需要用到头文件“#include <termios.h>”和“#include <unistd.h>”

5)函数tcflush 用于清空串口中没有完成的输入或者输出数据

原型为int tcflush(int fd, int queue_selector);
参数1:fd 是open 返回的文件句柄。
参数2:控制tcflush 的操作。
有三个常用数值,TCIFLUSH 清除正收到的数据,且不会读取出来;TCOFLUSH 清除正
写入的数据,且不会发送至终端;TCIOFLUSH 清除所有正在发生的I/O 数据。

6)tcsetattr 函数是设置参数的函数。
原型为int tcsetattr(int fd, int optional_actions,const struct termios *termios_p);
参数1:fd 是open 返回的文件句柄。
参数2:optional_actions 是参数生效的时间。
有三个常用的值:TCSANOW:不等数据传输完毕就立即改变属性;TCSADRAIN:等待
所有数据传输结束才改变属性;TCSAFLUSH:清空输入输出缓冲区才改变属性。
参数3:*termios_p 在旧的参数基础上修改的后的参数。
执行成功返回0,失败返回-1,一般在初始化最后会使用这个函数。

流程

linux串口初始化——>获取需要配置的串口句柄fd——>定义结构体新旧两个termios结构体
——>函数tcgetattr获取当前参数,用于检测——>设置需要的参数,波特率,校验位,停止位等等
——>函数tcflush用于清除寄存器——>函数tcsetattr设置新的参数

串口初始化代码

首先定义一个初始化函数
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop),然后定义结构体newtio 和oldtio

termios结构体是用于表示串口设备的参数的数据结构,它包含了串口通信中各种参数的设置,比如波特率、数据位、奇偶校验位、停止位等

在第一张图里,新建termios结构体newtio和oldtio,利用tcgetattr 函数,将串口设备的原始参数保存在名为 oldtio 的结构体中,如果保存不成功返回一个-1

第二张图里:使用bzero函数将newtio结构体的内存空间清零

使用位操作符 |newtio结构体中的c_cflag成员进行设置(CLOCAL 表示忽略调制解调器控制信号,CREAD 表示启用接收器)

第五行,根据参数nBits的值进行分支选择,如果nBits等于7,将newtio结构体中的c_cflag成员设置为CS7,表示使用7位数据位,如果nBits等于8,将newtio结构体中的c_cflag成员设置为CS8,表示使用8位数据位

另一个switch语句,根据参数nEvent的值进行分支选择:

nEvent 的值为字符 'O' 时,

使用位操作符 |= PARENB 标志位设置在 newtio.c_cflag 变量中。PARENB 用于启用奇偶校验,表示在数据传输过程中进行奇偶校验。使用位操作符 |=PARODD 标志位设置在 newtio.c_cflag 变量中。PARODD 用于设置奇偶校验方式为奇校验

使用位操作符 |= (INPCK | ISTRIP) 中的标志位同时设置在 newtio.c_iflag 变量中。INPCK 表示启用输入奇偶校验,ISTRIP 表示去除第8位

剩下'E' 'N'的同理,我就不解释了

然后设置波特率

注释:

cfsetispeed 函数是用于设置串口的输入波特率的函数,它的原型如下:

int cfsetispeed(struct termios *termios_p, speed_t speed);

这个函数接受两个参数:

termios_p:指向要设置的串口配置结构体的指针。

speed:要设置的输入波特率,是一个整数类型的值。

cfsetospeed同理

之后

这段代码是用于设置串口通信的停止位数以及清空串口输入缓冲区的操作,VTIME 是串口读取函数的超时时间,设置为 0 表示读取操作将会立即返回,VMIN 字段设置为 0。VMIN 是串口读取函数读取的最小字符数,设置为 0 表示不需要等待任何字符即可返回,tcflush 函数用于清空串口输入缓冲区

最后

使用 TCSANOW 参数意味着所做的更改将立即生效,而不是等待所有数据传递完成

【关于linxu串口部分未完待续】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值