0%

使用echo向文件写入数据

  • 主要是使用echo的输出重定向功能,也就是>>>
  • 覆盖文件内容用>,也就是
    • echo "Raspberry" > test.txt
  • 追加文件内容用>>
    • echo "string" >> <filename>
    • 就是将string追加到filename文件的末尾

Linux下挂载U盘操作

首先在控制台输入sudo fdisk -l

然后找到自己的U盘比如下图

image-20220115125056297

看到其名称为/dev/sda1,然后在/dev下新建一个文件夹比如叫USB

mkdir /dev/USB

然后将USB设备挂载在这个目录下

sudo mount /dev/sda1 /dev/USB

解除挂载类似,区别仅仅是mount换成unmount

C++复习和Qt(Ubuntu)

namespace

  • 命名空间的定义
  • namespace Ui { class MainWindow; class myThread;}
  • 一个命名空间的定义包含两部分:首先是关键字namespace,随后是命名空间的名字。
  • 在命名空间名字后面是一系列由花括号括起来的声明和定义
  • 只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其它命名空间。
  • 命名空间结束后无须分号,这一点与块类似。

假如在一个类中使用在之后定义的类的对象或者其他内容的话,先在这个类之前使用class <被使用的类名>;声明一下类

class <class name> : <访问权限修饰符> <super class>
{
public:
<函数定义>
protected:
<函数定义>
private:
<函数定义>
}
  • 不写权限默认private

构造函数

  • 构造函数必须与类名同名;
  • 可以重载,(重载?新概念,后面学到什么是重载。);
  • 没有返回类型,即使是 void 也不行。

析构函数

  • 析构函数的格式为~类名();
  • 调用时释放内存(资源);
  • ~类名()不能加参数;
  • 没有返回值,即使是 void 也不行。

this指针

  • this 指针记录对象的内存地址
  • this 只能在成员函数中使用,全局函数、静态函数都不能使用 this。实际上,成员函数 默认第一个参数为 T * const this。也就是一个类里面的成员了函数 int func(int p),func 的原 型在编译器看来应该是 int func(T * const this,int p)。
  • this 在成员函数的开始前构造,在成员函数的结束后清除。
  • this 指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全 局变量。

继承

class <class name> : <访问权限修饰符> <super class>
  • 公有继承(public):当一个类派生继承公有基类时,基类的公有成员也是派生类的公有成 员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但 是可以通过调用基类的公有和保护成员来访问。
  • 保护继承(protected): 当一个类派生继承保护基类时,基类的公有和保护成员将成为派 生类的保护成员。
  • 私有继承(private):当一个类派生继承私有基类时,基类的公有和保护成员将成为派生类 的私有成员。

运算符重载

  • 运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的 C++多态。目的在 于让人能够用同名的函数来完成不同的基本操作。
<返回类型说明符> operator <运算符符号>(<参数表>)
{
<函数体>
}
  • 可重载的函数
    • image-20220114000140555
    • image-20220114000156524
#比如有一个dog类,具有一个属性是体重weight
class Dog
{
...
Dog operator+(const Dog &d)
{
Dog dog;
dog.weight = this->weight+d.weight;
return dog;
}
};
  • 注意传入的参数是引用传递

多态

  • C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数

  • 也就是说用基类的指针调用子类的对象的时候,会根据子类的具体类型进行具体的函数的选择

  • 虚函数是 C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访 问派生类定义的函数

  • 多态的条件

    • 必须存在继承关系;
    • 继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字 virtual 声明的函数,在派 生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
    • 存在基类类型的指针或者引用,通过该指针或引用调用虚函数。

虚函数

  • 我们想要的是在程序中任意点可以根据所调用的对象类 型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
  • virtual ReturnType FunctionName(Parameter)

纯虚函数

  • 若在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基 类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
  • virtual void funtion1()=0

数据封装

  • 数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴 露接口而把具体的实现细节隐藏起来的机制,C++ 通过创建类来支持封装和数据隐藏(public、 protected、private)。

  • 简而言之就是不直接将变量暴露在用户的访问权限下而设置一个专门的函数处理用户对于变量的访问

抽象类

  • 如 果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类
  • 设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。 抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会 导致编译错误。
  • 如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支 持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导 致编译错误。可用于实例化对象的类被称为具体类。

Linux下安装Qt

chmod +x qt-opensource-linux-x64-5.12.9.run
./qt-opensource-linux-x64-5.12.9.run
  • 安装开始会要求登录,登录之后选择安装的内容
  • image-20220114004230454

信号与槽

  • 信号(Signal)就是在特定情况下被发射的事件,例如 PushButton 最常见的信号就是鼠标 单击时发射的 clicked() 信号,一个 ComboBox 最常见的信号是选择的列表项变化时发射的 CurrentIndexChanged() 信号。
  • 槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的 C++函数是一样的,可以 定义在类的任何部分(public、private 或 protected),可以具有任何参数,也可以被直接调用。 槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数 被自动执行。
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
  • 其中,sender 是发射信号的对象的名称,signal() 是信号名称。信号可以看做是特殊的函 数,需要带括号,有参数时还需要指明参数。receiver 是接收信号的对象名称,slot() 是槽函数 的名称,需要带括号,有参数时还需要指明参数。

  • 使用例

QObject::connect(pushButton, SIGNAL(clicked()), MainWindow, SLOT(close()));
  • 其中的pishButton是动作的发出者,MainWindow是信号的接收者

  • 一个信号可以连接多个槽

  • 多个信号也可以链接同一个槽

  • 一个信号可以连接另外一个信号

connect(pushButton, SIGNAL(objectNameChanged(QString)),this, SIGNAL(windowTitelChanged(QString)));
  • 一个信号发射时,也会发射另外一个信号,实现某些特殊的功能。

  • 断开连接使用disconnect(),此处略

信号

  • 信号只需要声明,无需定义
  • 信号需要在signals里面进行声明
  • image-20220114201719988

  • 创建槽的方法也很简单,也是直接在 mianwindow.h 里直接声明槽,在 mianwindow.cpp 里 实现槽的定义,声明槽必须写槽的定义(定义指函数体的实现),否则编译器编译时将会报错

  • 槽可以是任何成员函数、普通全局函数、静态函数

  • 槽函数和信号的参数和返回值要一致

  • image-20220114201840110

举例

  • mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();

signals:
void pushBtnTxtChanged();

public slots:
void changeBtnTxt();

void pushBtnClicked();

private:
QPushButton *pushBtn;
};
#endif // MAINWINDOW_H

  • mainwindow.cpp
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
this->resize(800, 600);

pushBtn = new QPushButton(this);

pushBtn->setText("I am a Button");
connect(pushBtn, SIGNAL(clicked()), this, SLOT(pushBtnClicked()));
connect(this, SIGNAL(pushBtnTxtChanged()), this, SLOT(changeBtnTxt()));
}

MainWindow::~MainWindow()
{
delete pushBtn;
}


void MainWindow:: pushBtnClicked()
{
emit pushBtnTxtChanged();
}

void MainWindow::changeBtnTxt()
{
pushBtn->setText("clicked!");
}

  • 上面的代码中实现了自定义一个信号pushBtnTxtchanged(),将其通过自定义的槽函数pushBtnClicked()emit出来,然后连接到changeBtnTxt()函数上。通过将自定义的槽函数pushBtnClicked()连接到按钮自带的clicked()信号上,实现按下之后修改按键文字的功能。

  • image-20220114203037162

  • image-20220114203057405

C标准IO函数使用笔记

文件读写

  • image-20220112223547788

  • 上面的函数实现了将一个每个单元大小为4个byte的浮点数存储在文件中,并且再原样读取回来的功能。

  • 写文件和读文件的参数分别是单个单元的大小为4字节,总长度为5.下面看结果

  • image-20220112223727041

  • 测试可知,文件读写函数用的是同一个文件偏移量,也就是随着文件的写入而顺次增加的偏移量,假如文件读取之前不将偏移量移动回初始位置的话,会什么也读不出来

  • 可见sizeof()函数读取数组的时候,读取的是数组的总空间大小而不是数组的元素个数。图上数组的读取结果是20,也就是5*4

  • 同时还可以推测文件写入模式r+的默认起始位置是0

  • 在上述函数中的fwrite后面添加一个ftell()显示偏移量,可见image-20220112224257063fwrite函数执行完毕的时候文件的偏移量自动增加到了20字节处

格式化文本函数的格式控制字符串

%[flags][width][.precision][length]type
  • flags:标志,可包含 0 个或多个标志;

  • width:输出最小宽度,表示转换后输出字符串的最小宽度;

  • precision:精度,前面有一个点号” . “;

  • length:长度修饰符;

  • type:转换类型,指定待转换数据的类型。

  • type:

    • image-20220112225548985
    • image-20220112225613496
  • flags

    • image-20220112225757743
    • image-20220112230005582
  • width

    • 最小的输出宽度,用十进制数来表示输出的最小位数,若实际的输出位数大于指定的输出的最小位数, 则以实际的位数进行输出,若实际的位数小于指定输出的最小位数,则可按照指定的 flags 标志补 0 或补空 格。
    • image-20220113000901462
  • precision 精度

    • 精度字段以点号” . “开头,后跟一个十进制正数
    • image-20220113000935320
    • image-20220113001001277
  • length

    • 长度修饰符指明待转换数据的长度,因为 type 字段指定的的类型只有 int、unsigned int 以及 double 等 几种数据类型,但是 C 语言内置的数据类型不止这几种,譬如有 16bit 的 short、unsigned short,8bit 的 char、 unsigned char,也有 64bit 的 long long 等,为了能够区别不同长度的数据类型,于是乎,长度修饰符(length) 应运而生,成为转换说明的一部分。
    • image-20220113001057843

示例

printf("%hd\n", 12345); //将数据以 short int 类型进行转换
printf("%ld\n", 12345); //将数据以 long int 类型进行转换
printf("%lld\n", 12345); //将数据以 long long int 类型进行转换

格式化输入

#include <stdio.h>

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
  • 可以看到,这 3 个格式化输入函数也是可变参函数,它们都有一个共同的参数 format,同样也称为格式 控制字符串,用于指定输入数据如何进行格式转换,与格式化输出函数中的 format 参数格式相似,但也有 所不同。

  • 每个函数除了固定参数之外,还可携带 0 个或多个可变参数。

  • scanf()函数可将用户输入(标准输入)的数据进行格式化转换;fscanf()函数从 FILE 指针指定文件中读 取数据,并将数据进行格式化转换;sscanf()函数从参数 str 所指向的字符串中读取数据,并将数据进行格式 化转换。

scanf

int a, b, c;
scanf("%d %d %d", &a, &b, &c);
  • 函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提 供的数目,甚至为零。发生错误则返回负值。

fscanf

  • 注意,该函数的第一个参数可以是标准输入流,此时它的作用与scanf相同
int a, b, c;
fscanf(stdin, "%d %d %d", &a, &b, &c);
  • 函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提供的数目,甚至为零。发生错误则返回负值。

sscanf

char *str = "5454 hello";
char buf[10];
int a;
sscanf(str, "%d %s", &a, buf);

  • 函数调用成功后,将返回成功匹配和分配的输入项的数量;如果较早匹配失败,则该数目可能小于所提 供的数目,甚至为零。发生错误则返回负值。

格式控制字符串

  • 本小节的重点依然是这个 format 参数的格式,与格式化输出函数中的 format 参数格式、写法上比较相 似,但也有一些区别。format 字符串包含一个或多个转换说明,每一个转换说明都是以百分号”%”或者”%n$” 开头(n 是一个十进制数字),关于”%n$”这种开头的转换说明就不介绍了,实际上用的不多。
  • 以%百分号开头的转换说明一般格式如下([]部分是可选的参数)
%[*][width][length]type
%[m][width][length]type
  • %后面可选择性添加星号*或**字母 m**,如果添加了星号*,格式化输入函数会按照转换说明的指示读取输 入,但是丢弃输入,意味着不需要对转换后的结果进行存储,所以也就不需要提供相应的指针参数。
  • 如果添加了 m,它只能与%s、%c 以及%[一起使用,调用者无需分配相应的缓冲区来保存格式转换后的 数据,原因在于添加了 m,这些格式化输入函数内部会自动分配足够大小的缓冲区,并将缓冲区的地址值通 过与该格式转换相对应的指针参数返回出来,该指针参数应该是指向 char *变量的指针。随后,当不再需要 此缓冲区时,调用者应调用 free()函数来释放此缓冲区。
char *buf;

scanf("%ms", &buf);
......
free(buf);
  • width:最大字符宽度;

  • length:长度修饰符,与格式化输出函数的 format 参数中的 length 字段意义相同。

  • type:指定输入数据的类型。

  • type

    • image-20220113130730315
    • image-20220113130752641
  • width最大字符长度限制。

    • 是一个十进制表示的整数,用于指定最大字符宽度,当达到此最大值或发现不匹配的字符时(以先发生 者为准),字符的读取将停止。大多数 type 类型会丢弃初始的空白字符,并且这些丢弃的字符不会计入最 大字符宽度。对于字符串转换来说,scanf()会在字符串末尾自动添加终止符”\0”,最大字符宽度中不包括此 终止符。
scanf("%4s", buf); //匹配字符串,字符串长度不超过 4 个字符
  • 此时输入“abcdef”,存储的是“abcd”
  • length数据长度修饰符
    • image-20220113131301042
scanf("%hd", var); //匹配 short int 类型数据
scanf("%hhd", var); //匹配 signed char 类型数据
scanf("%ld", var); //匹配 long int 类型数据
scanf("%f", var); //匹配 float 类型数据
scanf("%lf", var); //匹配 double 类型数据
scanf("%Lf", var); //匹配 long double 类型数据

使用例:

#include <stdio.h>

int main()
{
float inputBuf;
scanf("%*10[A-Za-z]%f", &inputBuf);
printf("%03.4f", inputBuf);
}
  • 注意[]里面的内容的写法,比如想一次性收取所有字母,使用[A-Za-z],连续写即可,数字写0-9,同
  • 控制台测试输入
  • image-20220113132156094
  • 都能够正确接收到0.01,说明成功识别到了hello并且将其丢弃(因为指定了*符号)

I/O缓冲

略,详见原子教程《I.MX6U嵌入式Linux C应用编程指南》

  • 标准 I/O 所维护的 stdio 缓冲是用户空间 的缓冲区,当应用程序中通过标准 I/O 操作磁盘文件时,为了减少调用系统调用的次数,标准 I/O 函数会将 用户写入或读取文件的数据缓存在 stdio 缓冲区,然后再一次性将 stdio 缓冲区中缓存的数据通过调用系统 调用 I/O(文件 I/O)写入到文件 I/O 内核缓冲区或者拷贝到应用程序的 buf 中。
  • 通过这样的优化操作,当操作磁盘文件时,在用户空间缓存大块数据以减少调用系统调用的次数,使得 效率、性能得到优化。使用标准 I/O 可以使编程者免于自行处理对数据的缓冲,无论是调用 write()写入数 据、还是调用 read()读取数据。
  • 直接 I/O 方式效率、性能比较低,绝大部分应用程序不会使用直接 I/O 方式对文件进行 I/O 操作,通常 只在一些特殊的应用场合下才可能会使用,那我们可以使用直接 I/O 方式来测试磁盘设备的读写速率,这种 测试方式相比普通 I/O 方式就会更加准确。

文件描述符和FILE指针的转化

  • 在应用程序中,在同一个文件上执行 I/O 操作时,还可以将文件 I/O(系统调用 I/O)与标准 I/O 混合使 用,这个时候我们就需要将文件描述符和 FILE 指针对象之间进行转换,此时可以借助于库函数 fdopen()fileno()来完成。
#include <stdio.h>

int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);
  • 对于 fileno()函数来说,根据传入的 FILE 指针得到整数文件描述符,通过返回值得到文件描述符,如果 转换错误将返回-1,并且会设置 errno 来指示错误原因。得到文件描述符之后,便可以使用诸如 read()、write()、 lseek()、fcntl()等文件 I/O 方式操作文件。

  • fdopen()函数与 fileno()功能相反,给定一个文件描述符,得到该文件对应的 FILE 指针,之后便可以使 用诸如 fread()、fwrite()等标准 I/O 方式操作文件了。参数 mode 与 fopen()函数中的 mode 参数含义相同如下表,若该参数与文件描述符 fd 的访问模式不一致,则会导致调用 fdopen()失败。

    • image-20220113163201525
    • image-20220113163221211

混用两种IO函数的时候的缓冲区问题

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
printf("print");
write(STDOUT_FILENO, "write\n", 6);
exit(0);
}
  • 当混合使用文件 I/O 和标准 I/O 时,需要特别注意缓冲的问题,文件 I/O 会直接将数据写入到内核缓冲 区进行高速缓存,而标准 I/O 则会将数据写入到 stdio 缓冲区,之后再调用 write()将 stdio 缓冲区中的数据写 入到内核缓冲区

  • 执行结果你会发现,先输出了”write”字符串信息,接着再输出了”print”字符串信息

配置Linux开机启动脚本

Linux开机启动脚本一般在/etc/rc.d目录下或者是/etc下的rc.local文件,此时使用vim工具在其后新增文本内容,执行用户指定的.sh脚本即可

image-20220112173132465

exit 0之前,添加下面两行脚本

image-20220112173302603

第一行起到提示的作用,第二行开始执行对应的脚本,再次之前确定具有执行脚本的权限(x权限)。执行效果:

image-20220112180339588

可见开机已经自动执行脚本

Linux文件IO(二)

多次打开同一个文件

  • 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的 时候也需要调用 close 依次关闭各个文件描述符。
  • 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
  • 一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。
  • image-20220112193343091
  • 实例
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char buffer[4];
int fd1, fd2;
int ret;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd1) {
perror("open error");
exit(-1);
}
/* 再次打开 test_file 文件 */
fd2 = open("./test_file", O_RDWR);
if (-1 == fd2) {
perror("open error");
ret = -1;
goto err1;
}
/* 通过 fd1 文件描述符写入 4 个字节数据 */
buffer[0] = 0x11;
buffer[1] = 0x22;
buffer[2] = 0x33;
buffer[3] = 0x44;
ret = write(fd1, buffer, 4);
if (-1 == ret) {
perror("write error");
goto err2;
}
/* 将读写位置偏移量移动到文件头 */
ret = lseek(fd2, 0, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err2;
}
/* 读取数据 */
memset(buffer, 0x00, sizeof(buffer));
ret = read(fd2, buffer, 4);
if (-1 == ret) {
perror("read error");
goto err2;
}
printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],
buffer[2], buffer[3]);
ret = 0;
err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}


  • 当前目录下不存在 test_file 文件,上述代码中,第一次调用 open 函数新建并打开 test_file 文件,第二次 调用 open 函数再次打开它,新建文件时,文件大小为 0;首先通过文件描述符 fd1 写入 4 个字节数据 (0x11/0x22/0x33/0x44),从文件头开始写;然后再通过文件描述符 fd2 读取 4 个字节数据,也是从文件头 开始读取。假如,内存中只有一份动态文件,那么读取得到的数据应该就是 0x11、0x22、0x33、0x44,如 果存在多份动态文件,那么通过 fd2 读取的是与它对应的动态文件中的数据,那就不是 0x11、0x22、0x33、 0x44,而是读取出 0 个字节数据,因为它的文件大小是 0。

  • image-20220112193309416

  • 上图中打印显示读取出来的数据是 0x11/0x22/0x33/0x44,所以由此可知,即使多次打开同一个文件,内 存中也只有一份动态文件。

多次打开同一文件与O_APPEND

  • 假如不使用O_APPEND的话,程序将会以不同的文件标识符为准的偏移量分别写入数据,多个文件标识符同时写一个文件的时候,不会互相更新标识符的位置,因此有可能互相覆盖。
  • 使用O_APPEND标志的时候,会自动将偏移量移动到文件尾部,会互相更新,比如一个文件标识符写入的时候会更新另一个文件标识符的写入偏移量。此时多个文件标识符进行读写的时候就不会互相覆盖了
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
unsigned char buffer1[4], buffer2[4];
int fd1, fd2;
int ret;
int i;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL | O_APPEND,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd1) {
perror("open error");
exit(-1);
}
/* 再次打开 test_file 文件 */
fd2 = open("./test_file", O_RDWR | O_APPEND);
if (-1 == fd2) {
perror("open error");
ret = -1;
goto err1;
}
/* buffer 数据初始化 */
buffer1[0] = 0x11;
buffer1[1] = 0x22;
buffer1[2] = 0x33;
buffer1[3] = 0x44;
buffer2[0] = 0xAA;
buffer2[1] = 0xBB;
buffer2[2] = 0xCC;
buffer2[3] = 0xDD;
/* 循环写入数据 */
for (i = 0; i < 4; i++) {
ret = write(fd1, buffer1, sizeof(buffer1));
if (-1 == ret) {
perror("write error");
goto err2;
}
ret = write(fd2, buffer2, sizeof(buffer2));
if (-1 == ret) {
perror("write error");
goto err2;
}
}
/* 将读写位置偏移量移动到文件头 */
ret = lseek(fd1, 0, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err2;
}
/* 读取数据 */
for (i = 0; i < 8; i++) {
ret = read(fd1, buffer1, sizeof(buffer1));
if (-1 == ret) {
perror("read error");
goto err2;
}
printf("%x%x%x%x", buffer1[0], buffer1[1],
buffer1[2], buffer1[3]);
}
printf("\n");
ret = 0;
err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}
  • image-20220112202602282

可见,每次fd1和fd2写入的内容都是交替出现的,也就是说一个标识符写入之后会自动更新另一个的偏移量

复制文件描述符

  • 在 Linux 系统中,open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件 描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限,譬如使用旧的文件描述符对文件有读写权限,那么新的文件描述符同样也具 有读写权限;在 Linux 系统下,可以使用 dup 或 dup2 这两个系统调用对文件描述符进行复制,本小节就给 大家介绍这两个函数的用法以及它们之间的区别。 复制得到的文件描述符与旧的文件描述符都指向了同一个文件表,假设 fd1 为原文件描述符,fd2 为复 制得到的文件描述符,如下图所示:
  • image-20220112202809873

dup 函数

  • dup 函数用于复制文件描述符
#include <unistd.h>

int dup(int oldfd);
  • oldfd:需要被复制的文件描述符。
  • 成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则; 如果复制失败将返回-1,并且会设置 errno 值。
  • 同一个程序中使用一个描述符和复制得到的描述符进行写内容的时候,会自动连接起来而不是互相覆盖,因为两个描述符使用的是用一个偏移量

dup2函数

  • dup 系统调用分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件 描述符,这是 dup 系统调用的一个缺陷;而 dup2 系统调用修复了这个缺陷,可以手动指定文件描述符,而 不需要遵循文件描述符分配原则,当然在实际的编程工作中,需要根据自己的情况来进行选择。
#include <unistd.h>

int dup2(int oldfd, int newfd);
  • oldfd:需要被复制的文件描述符。

  • newfd:指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。

  • 返回值:成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd;如果复制失败将返 回-1,并且会设置 errno 值。

共享文件

  • 文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode)被 多个独立的读写体同时进行 IO 操作。多个独立的读写体大家可以将其简单地理解为对应于同一个文件的多 个不同的文件描述符,譬如多次打开同一个文件所得到的多个不同的 fd,或使用 dup()(或 dup2)函数复制 得到的多个不同的 fd 等。
  • 同时进行 IO 操作指的是一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作文件
  • 文件共享的意义有很多,多用于多进程或多线程编程环境中,譬如我们可以通过文件共享的方式来实现 多个线程同时操作同一个大文件,以减少文件读写时间、提升效率。
  • 文件共享的核心是:如何制造出多个不同的文件描述符来指向同一个文件。譬如多次调用 open 函数重复打开同一个文件得到多个不同的文件描述符、使用 dup() 或 dup2()函数对文件描述符进行复制以得到多个不同的文件描述符。

同一个进程中多次调用 open 函数打开同一个文件

  • image-20220112205040312

不同进程中分别使用 open 函数打开同一个文件

  • image-20220112205112471

同一个进程中通过 dup(dup2)函数对文件描述符进行复制

  • image-20220112205156025

文件操作中的原子操作和竞争冒险问题

  • 假设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据), 每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志,此时,各数据结构之间的关系 如图 3.8.2 所示。每个进程都有它自己的进程控制块 PCB,有自己的文件表(意味着有自己独立的读写位置 偏移量),但是共享同一个 inode 节点(也就是对应同一个文件)。假定此时进程 A 处于运行状态,B 未处 于等待运行状态,进程 A 调用了 lseek 函数,它将进程 A 的该文件当前位置偏移量设置为 1500 字节处(假 设这里是文件末尾),刚好此时进程 A 的时间片耗尽,然后内核切换到了进程 B,进程 B 执行 lseek 函数, 也将其对该文件的当前位置偏移量设置为 1500 个字节处(文件末尾)。然后进程 B 调用 write 函数,写入 了 100 个字节数据,那么此时在进程 B 中,该文件的当前位置偏移量已经移动到了 1600 字节处。B 进程时 间片耗尽,内核又切换到了进程 A,使进程 A 恢复运行,当进程 A 调用 write 函数时,是从进程 A 的该文 件当前位置偏移量(1500 字节处)开始写入,此时文件 1500 字节处已经不再是文件末尾了,如果还从 1500 字节处写入就会覆盖进程 B 刚才写入到该文件中的数据。

  • image-20220112205422813

  • 以上给大家所描述的这样一种情形就属于竞争状态(也成为竞争冒险),操作共享资源的两个进程或 线程),其操作之后的所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可 预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争 状态。

解决上述问题用到的原子操作

  • 所谓原子操作,是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有 步骤,不可能只执行所有步骤中的一个子集。

  • 使用O_APPEND实现原子操作

    • 当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入 操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,这里“移动当前写位置偏移量到文 件末尾、写入数据”这两个操作步骤就组成了一个原子操作,加入 O_APPEND 标志后,不管怎么写入数据 都会是从文件末尾写,这样就不会导致出现“进程 A 写入的数据覆盖了进程 B 写入的数据”这种情况了。
  • 使用pread()pwrite()进行操作

    • pread()和 pwrite()都是系统调用,与 read()、write()函数的作用一样,用于读取和写入数据。区别在于, pread()和 pwrite()可用于实现原子操作,调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数, 用于指定文件当前读或写的位置偏移量,所以调用 pread 相当于调用 lseek 后再调用 read;同理,调用 pwrite 相当于调用 lseek 后再调用 write。所以可知,使用 pread 或 pwrite 函数不需要使用 lseek 来调整当前位置偏 移量,并会将“移动当前位置偏移量、读或写”这两步操作组成一个原子操作。
#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

  • fd、buf、count 参数与 read 或 write 函数意义相同。

  • offset:表示当前需要进行读或写的位置偏移量。

  • 返回值:返回值与 read、write 函数返回值意义一样。

  • 虽然 pread(或 pwrite)函数相当于 lseek 与 pread(或 pwrite)函数的集合,但还是有下列区别:

    • 调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);
    • 不更新文件表中的当前位置偏移量。
  • **fcntl()ioctl()**详见原子的《I.MX6U嵌入式Linux C应用编程指南》

截断文件

  • 使用系统调用 truncate()或 ftruncate()可将普通文件截断为指定字节长度
#include <unistd.h>
#include <sys/types.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
  • 这两个函数的区别在于:ftruncate()使用文件描述符 fd 来指定目标文件,而 truncate()则直接使用文件路 径 path 来指定目标文件,其功能一样。

  • 这两个函数都可以对文件进行截断操作,将文件截断为参数 length 指定的字节长度,什么是截断?如 果文件目前的大小大于参数 length 所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉 了;如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展,对扩展部分进行读取将得到空字 节”\0”。

  • 使用 ftruncate()函数进行文件截断操作之前,必须调用 open()函数打开该文件得到文件描述符,并且必 须要具有可写权限,也就是调用 open()打开文件时需要指定 O_WRONLY 或 O_RDWR。

  • 调用这两个函数并不会导致文件读写位置偏移量发生改变,所以截断之后一般需要重新设置文件当前 的读写位置偏移量,以免由于之前所指向的位置已经不存在而发生错误(譬如文件长度变短了,文件当前所 指向的读写位置已不存在)。

Linux 文件IO

简单的实例

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

int main(void)
{
char buff[1024];
int fd1, fd2;
int ret;
/* 打开源文件 src_file(只读方式) */
fd1 = open("./src_file", O_RDONLY);
if (-1 == fd1)
return fd1;
/* 打开目标文件 dest_file(只写方式) */
fd2 = open("./dest_file", O_WRONLY);
if (-1 == fd2) {
ret = fd2;
goto out1;
}
/* 读取源文件 1KB 数据到 buff 中 */
ret = read(fd1, buff, sizeof(buff));
if (-1 == ret)
goto out2;
/* 将 buff 中的数据写入目标文件 */
ret = write(fd2, buff, sizeof(buff));
if (-1 == ret)
goto out2;
ret = 0;
out2:
/* 关闭目标文件 */
close(fd2);
out1:
/* 关闭源文件 */
close(fd1);
return ret;
}

文件描述符

  • 调用 open 函数会有一个返回值,譬如示例代码 2.1.1 中的 fd1 和 fd2,这是一个 int 类型的数据,在 open 函数执行成功的情况下,会返回一个非负整数,该返回值就是一个文件描述符(file descriptor),这说明文 件描述符是一个非负整数;对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引。
  • 当调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符,用于指 代被打开的文件,所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件,譬如示例代码 2.1.1 中,当调用 read/write 函数进行文件读写时,会将文件描述符传送给 read/write 函数
  • 所以对于一个进程来说,文件描述符是一种有限资源,文件描述符是从 0 开始分配的,譬如说进程中第 一个被打开的文件对应的文件描述符是 0、第二个文件是 1、第三个文件是 2、第 4 个文件是 3……以此类推,所以由此可知,文件描述符数字最大值为 1023(0~1023)。每一个被打开的文件在同一个进程中都有 一个唯一的文件描述符,不会重复,如果文件被关闭后,它对应的文件描述符将会被释放,那么这个文件描 述符将可以再次分配给其它打开的文件、与对应的文件绑定起来。

一切皆文件

  • Linux 系统下,一切皆文件,也包括各种硬件设备,使用 open 函数打开任何文件成功情况下便会 返回对应的文件描述符 fd。每一个硬件设备都会对应于 Linux 系统下的某一个文件,把这类文件称为设备文 件。所以设备文件对应的其实是某一硬件设备,应用程序通过对设备文件进行读写等操作、来使用、操控硬 件设备,譬如 LCD 显示器、串口、音频、键盘等。
  • 标准输入一般对应的是键盘,可以理解为 0 便是打开键盘对应的设备文件时所得到的文件描述符;标 准输出一般指的是 LCD 显示器,可以理解为 1 便是打开 LCD 设备对应的设备文件时所得到的文件描述符; 而标准错误一般指的也是 LCD 显示器。

打开文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • pathname:字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信 息,譬如:”./src_file”(当前目录下的 src_file 文件)、”/home/dengtao/hello.c”等;如果 pathname 是一个符号 链接,会对其进行解引用。

  • flags:调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,这些标志使 用宏定义进行描述,都是常量,open 函数提供了非常多的标志,我们传入 flags 参数时既可以单独使用某一 个标志,也可以通过位或运算(|)将多个标志进行组合。

标志 用途 说明
O_WRONLY 只写
O_RDONLY 只读
O_RDWR 可读可写 这三个是文件访问权限标志,传入的 flags 参数中必须要包含其中一种标 志,而且只能包含一种
O_CREAT 如果地址指向的文件不存在就创建文件 使用此标志时,调用 open 函数需要 传入第 3 个参数 mode,参数 mode 用 于指定新建文件的访问权限,稍后将 对此进行说明。 open 函数的第 3 个参数只有在使用 了 O_CREAT 或 O_TMPFILE 标志 时才有效。
O_DIRECTORY 如果地址指向的是目录,返回调用失败
O_EXCL 此标志一般结合 O_CREAT 标志一起使用, 用于专门创建文件。 在 flags 参数同时使用到了 O_CREAT 和 O_EXCL 标志的情况下,如果 pathname 参数 指向的文件已经存在,则 open 函数返回错 误。 可以用于测试一个文件是否存在,如 果不存在则创建此文件,如果存在则 返回错误,这使得测试和创建两者成 为一个原子操作;关于原子操作,在 后面的内容当中将会对此进行说明。
O_NOFOLLOW 如果 pathname 参数指向的是一个符号链接, 将不对其进行解引用,直接返回错误。 不加此标志情况下,如果 pathname 参数是一个符号链接,会对其进行解引用,加了之后会对符号链接直接返回错误。
  • flag可以通过|标志添加大于一个,比如
open("./src_file", O_RDONLY) //单独使用某一个标志
open("./src_file", O_RDONLY | O_NOFOLLOW) //多个标志组合

文件权限(rwx)

前面的博客中已经详细介绍过了,略

写入文件

  • 调用 write 函数可向打开的文件写入数据,其函数原型如下所示
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • fd:文件描述符。关于文件描述符,前面已经给大家进行了简单地讲解,这里不再重述!我们需要将进 行写操作的文件所对应的文件描述符传递给 write 函数。

  • buf:指定写入数据对应的缓冲区。

  • count:指定写入的字节数。

  • 返回值:如果成功将返回写入的字节数(0 表示未写入任何字节)如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。

针对写入文件的开始地址

  • 读写操作都是从文件的当前位置偏移量处开始,当然当前位置偏移量可以通过 lseek 系统 调用进行设置,关于此函数后面再讲;默认情况下当前位置偏移量一般是 0,也就是指向了文件起始位置, 当调用 read、write 函数读写操作完成之后,当前位置偏移量也会向后移动对应字节数

读文件

  • 调用 read 函数可从打开的文件中读取数据
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • fd:文件描述符。与 write 函数的 fd 参数意义相同。
  • buf:指定用于存储读取数据的缓冲区。
  • count:指定需要读取的字节数。
  • 返回值:如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节 数,也有可能会为 0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾。实际读取到的字节数少 于要求读取的字节数,譬如在到达文件末尾之前有 30 个字节数据,而要求读取 100 个字节,则 read 读取成 功只能返回 30;而下一次再调用 read 读,它将返回 0(文件末尾)。

关闭文件

  • close
#include <unistd.h>

int close(int fd);
  • fd:文件描述符,需要关闭的文件所对应的文件描述符。

  • 返回值:如果成功返回 0,如果失败则返回-1。

  • 在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开 的所有文件,也就是说在我们的程序中打开了文件,如果程序终止退出时没有关闭打开的文件,那么内核会 自动将程序中打开的文件关闭。很多程序都利用了这一功能而不显式地用 close 关闭打开的文件。

更改偏移量位置

  • lseek
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
  • fd:文件描述符。

  • offset:偏移量,以字节为单位。

  • whence:用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):

    • SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算);
    • SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处,offset 可以为正、也可以为 负,如果是正数表示往后偏移,如果是负数则表示往前偏移;
    • SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负, 如果是正数表示往后偏移、如果是负数则表示往前偏移。
  • 成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生 错误将返回-1。可以用此函数获取文件此时的偏移量

Linux文件系统简单讲述

  • 磁盘空间包括两个部分,一个是真正存储文件的区域,另一个是存储文件inode的区域(inode见前面的博客)

  • image-20220112130444097

  • windows的快速格式化不会真正删除存储问文件内容的区域,只是删除了inode表的区域

打开文件的过程

  • 系统找到这个文件名所对应的 inode 编号;

  • 通过 inode 编号从 inode table 中找到对应的 inode 结构体

  • 根据 inode 结构体中记录的信息,确定文件数据所在的 block,并读出数据。

  • 文件打开的时候内核会申请一段内存(一段缓冲区),并且将静态文件的数 据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内 核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作。

  • 因为磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,因为块设备硬件本身有读写限制等特征,块 设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动 也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的 读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活, 所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文 件,而不是直接去操作磁盘中的静态文件,不但操作不灵活,效率也会下降很多,因为内存的读写速率远比 磁盘读写快得多。

  • 在 Linux 系统中,内核会为每个进程(关于进程的概念,这是后面的内容,我们可以简单地理解为一个 运行的程序就是一个进程,运行了多个程序那就是存在多个进程)设置一个专门的数据结构用于管理该进 程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写 PCB)。

    • PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文 件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等,进程打开 的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表
    • image-20220112131919826

程序出错

略,详见原子教程pdf

程序退出

基本方法

  • return
    • return 0表示程序正常结束
    • return -1表示程序异常退出

Linux 下的其他方法

  • 进程正常退出除了可以使用 return 之外,还可以使用exit()_exit()以及_Exit()

  • _exit()

  • 调用_exit()函数会 清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将 控制权交给操作系统。

#include <unistd.h>

void _exit(int status);
  • 其中的status含义与上面return的相同,0代表正常,其他数值代表异常

  • _Exit()_exit()等价,不再介绍

#include <stdlib.h>

void _Exit(int status);
  • exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。
    执行 exit()会执行一些清理工作,最后调用_exit()函数。
#include <stdlib.h>

void exit(int status);
  • 不管你用哪一种都可以结束进程,但还是推荐大家使用 exit()

空洞文件

  • 什么是空洞文件(hole file)?在上一章内容中,笔者给大家介绍了 lseek()系统调用,使用 lseek 可以修 改文件的当前读写位置偏移量,此函数不但可以改变位置偏移量,并且还允许文件偏移量超出文件长度,这 是什么意思呢?譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调 用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,大家想一想会怎样?如果笔者没有提前告诉 大家,大家觉得不能这样操作,但事实上 lseek 函数确实可以这样操作。

  • 接下来使用 write()函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写 入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形 成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件

  • 文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它 分配对应的空间,但是**空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的**,这点需要注意。

  • 空洞文件对多线程共同操作文件是及其有用的,有时候我们创建 一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多 段,然后使用多线程来操作每个线程负责其中一段数据的写入

测试

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
int fd;
int ret;
char buffer[1024];
int i;
/* 打开文件 */
fd = open("./hole_file", O_WRONLY | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
ret = lseek(fd, 4096, SEEK_SET);
if (-1 == ret)
{
perror("lseek error");
goto err;
}
/* 初始化 buffer 为 0xFF */
memset(buffer, 0xFF, sizeof(buffer));
/* 循环写入 4 次,每次写入 1K */
for (i = 0; i < 4; i++) {
ret = write(fd, buffer, sizeof(buffer));
if (-1 == ret)
{
perror("write error");
goto err;
}
}
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}
  • 上面的代码从文件的4k位置开始,一次写入1k,写入4次。

  • 下面查看文件的大小

  • image-20220112143912056

  • 利用ls察看文件大小的时候,虽然文件只有4k有数据,但是文件大小查出来逻辑大小,也就是8k。

  • 但是使用du查看的时候,只有文件占用的实际存储的大小,也就是4k。

O_APPEND 和 O_TRUNC 标志

在上一章给大家讲解 open 函数的时候介绍了一些 open 函数的 flags 标志,譬如 O_RDONLY、 O_WRONLY、O_CREAT、O_EXCL 等,本小节再给大家介绍两个标志,分别是 O_APPEND 和 O_TRUNC, 接下来对这两个标志分别进行介绍。

O_TRUNC 标志

  • O_TRUNC 这个标志的作用非常简单,如果使用了这个标志,调用 open 函数打开文件的时候会将文件 原本的内容全部丢弃,文件大小变为 0;这里我们直接测试即可!测试代码如下所示:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int fd;
/* 打开文件 */
fd = open("./test_file", O_WRONLY | O_TRUNC);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 关闭文件 */
close(fd);
exit(0);
}

O_APPEND 标志

  • 如果 open 函数携带了 O_APPEND 标志,调用 open 函数打开文件, 当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末 尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
char buffer[16];
int fd;
int ret;
/* 打开文件 */
fd = open("./test_file", O_RDWR | O_APPEND);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 初始化 buffer 中的数据 */
memset(buffer, 0x55, sizeof(buffer));
/* 写入数据: 写入 4 个字节数据 */
ret = write(fd, buffer, 4);
if (-1 == ret) {
perror("write error");
goto err;
}
/* 将 buffer 缓冲区中的数据全部清 0 */
memset(buffer, 0x00, sizeof(buffer));
/* 将位置偏移量移动到距离文件末尾 4 个字节处 */
ret = lseek(fd, -4, SEEK_END);
if (-1 == ret) {
perror("lseek error");
goto err;
}
/* 读取数据 */
ret = read(fd, buffer, 4);
if (-1 == ret) {
perror("read error");
goto err;
}
printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],
buffer[2], buffer[3]);
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}

  • image-20220112145240871

  • 通过控制台可知,读出的内容确实是最后四个字节为0x55

  • O_APPEND 标志并不会影响读文件当读取文件时,O_APPEND 标志并不会影响读位置偏移量,即使使用了 O_APPEND 标志,读文件位置偏移量默认情况下依然是文件头,关于这个问题大家可以自己进行测试,编程是一个实践 性很强的工作,有什么不能理解的问题,可以自己编写程序进行测试。

  • 使用 lseek 函数来改变 write()时的写位置偏移量,其实这种做法并不会成功,这就是笔 者给大家提的第二个细节,使用了 O_APPEND 标志,即使是通过 lseek 函数也是无法修改写文件时对应的 位置偏移量(注意笔者这里说的是写文件,并不包括读),写入数据依然是从文件末尾开始,lseek 并不会 该变写位置偏移量

  • 测试

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

int main()
{
int fd1;
char readBuff[1024];
char wrBuff[1024];
char * wrCont = "Hello World";
int i = 0;
while(wrCont[i]!=0)
{
wrBuff[i] = wrCont[i];
i++;
}
wrBuff[i] = 0;
fd1 = open("./test1.txt", O_RDWR|O_APPEND);
for(int j = 0;j<4;j++)
{
sprintf(wrBuff,", 0x%02x", j);
printf(wrBuff);
write(fd1, wrBuff, 6);
}

close(fd1);
}
  • 此处函数不对文件偏移位置进修修改的前提下,在文件写入4次, 0xXX的字符,观察效果

  • image-20220112151123797

  • 可以看出,在文件最后多出了四个字符串,可见每次写入的时候,文件的偏移量都自动移动到了文件的最后,即使文件在这个过程中并没有被保存。

DSP28377d学习(一)

仿真器配置

此文档仅作为官方手册的补充

  • 在开始烧录之前要先配置Target configuration file
  • image-20220110221639291

打开后,选择

  • image-20220110221716259

然后

  • image-20220110221731391

  • image-20220110221832321

  • 此处一般不需要修改

板子链接

  • 先将仿真器和目标板链接然后再给板子上电

创建项目

image-20220110222621315

  • 点击下面的选项打开Target configuration面板

image-20220111164750076

  • 找到自己刚才新建的Target configuration,然后选择launch

image-20220111165533087

image-20220111165625183

image-20220111165556571

  • 在Flash烧写之前,需要手动新建一个predidined symbol,在其中(已经有一个CPU1)新建一个FLASH
  • 右键点击项目(图中红圈),然后选择“properties”
  • image-20220111164926308

image-20220111165509718

  • debug的时候注意在链接CPU、下载程序、开始调试结束后执行程序之前再下断点不要带着断点进入调试,否则main()函数可能没法运行

gcc和MakeFile基础

gcc常用参数

  • gcc -c 指的是只把源码编译为目标文件而不进行链接。如果GCC不带-C参数,编译一个源代码文件(test.c)。那么会自动将编译和链接一步完成,并生成可执行文件。对于多个文件,需要先编译成中间目标文件(一般是.o文件),在链接成可执行文件,一般习惯目标文件都是以.o后缀,也没有硬性规定可执行文件不能用.o文件。
  • gcc -o指的是output_filename,确定输出文件的名称为output_filename,同时这个名称不能和源文件同名。如果不给出这个选项,gcc就给出预设的可执行文件a.out。
  • 其他参数详见 https://www.runoob.com/w3cnote/gcc-parameter-detail.html

MakeFile是什么

  • 使用 GCC 编译器在 Linux 进行 C 语言编译,通过在终端执行 gcc 命 令来完成 C 文件的编译,如果我们的工程只有一两个 C 文件还好,需要输入的命令不多,当文 件有几十、上百甚至上万个的时候用终端输入 GCC 命令的方法显然是不现实的。如果我们能够 编写一个文件,这个文件描述了编译哪些源码文件、如何编译那就好了,每次需要编译工程的 时只需要使用这个文件就行了。这种问题怎么可能难倒聪明的程序员,为此提出了一个解决大 工程编译的工具:make,描述哪些文件需要编译、哪些需要重新编译的文件就叫做 Makefile, Makefile 就跟脚本文件一样,Makefile 里面还可以执行系统命令。使用的时候只需要一个 make命令即可完成整个工程的自动编译,极大的提高了软件开发的效率。

  • 在 Linux 下用的最多的是 GCC 编译器,这是个没有 UI 的编译器,因此 Makefile 就需要我们自己来编写了。

应用举例

  • 我们完成这样一个小工程,通过键盘输入两个整形数字,然后计算他们的和并将结果显示在屏幕上,在这个工程中我们有 main.c、input.c 和 calcu.c 这三个 C 文件和 input.h、calcu.h 这 两个头文件。其中 main.c 是主体,input.c 负责接收从键盘输入的数值,calcu.c 进行任意两个数 相加,其中 main.c 文件内容如下:
//main.c
#include <stdio.h>
#include "input.h"
#include "calcu.h"

int main(int argc, char *argv[])
{
int a, b, num;

input_int(&a, &b);
num = calcu(a, b);
printf("%d + %d = %d\r\n", a, b, num);
}

  • input.c 文件内容如下:
//input.c
#include <stdio.h>
#include "input.h"

void input_int(int *a, int *b)
{
printf("input two num:");
scanf("%d %d", a, b);
printf("\r\n");
}

  • calcu.c 文件内容如下:
#include "calcu.h"
int calcu(int a, int b)
{
return (a + b);
}
  • 文件 input.h 内容如下:
#ifndef _INPUT_H
#define _INPUT_H

void input_int(int *a, int *b);
#endif
  • 文件 calcu.h 内容如下:
#ifndef _CALCU_H
#define _CALCU_H

int calcu(int a, int b);
#endif
  • 假如使用gcc编译程序,那么此时需要再命令行输入
gcc main.c calcu.c input.c -o main

上面命令的意思就是使用 gcc 编译器对 main.c、calcu.c 和 input.c 这三个文件进行编译,编 译生成的可执行文件叫做 main

image-20220109170757542

执行程序用到的命令是./main,含义是执行当前目录下的main文件

makefile的重要性

可以看出我们的代码按照我们所设想的工作了,使用命令“gcc main.c calcu.c input.c -o main” 看起来很简单是吧,只需要一行就可以完成编译,但是我们这个工程只有三个文件啊!如果几 千个文件呢?再就是如果有一个文件被修改了,使用上面的命令编译的时候所有的文件都会重新编译,如果工程有几万个文件(Linux 源码就有这么多文件!),想想这几万个文件编译一次 所需要的时间就可怕。最好的办法肯定是哪个文件被修改了,只编译这个被修改的文件即可, 其它没有修改的文件就不需要再次重新编译了,为此我们改变我们的编译方法,如果第一次编译工程,我们先将工程中的文件都编译一遍,然后后面修改了哪个文件就编译哪个文件,命令 如下:

gcc -c main.c
gcc -c input.c
gcc -c calcu.c
gcc main.o input.o calcu.o -o main
  • 注意,gcc的-c选项的意思是将程序编译为.o文件但是不链接为最终的可执行文件,最后一句gcc main.o input.o calcu.o -o main的意思是将三个.o文件链接为一个可执行文件
  • 假如我们现在修改了 calcu.c 这个文件,只需要将 caclue.c 这一个文件重新编译成.o 文件,然后在将所有的.o 文件链接成可执行文件,只需要下面两条命令即可:
gcc -c calcu.c
gcc main.o input.o calcu.o -o main

makefile的作用

  • 如果工程没有编译过,那么工程中的所有.c 文件都要被编译并且链接成可执行程序。
  • 如果工程中只有个别 C 文件被修改了,那么只编译这些被修改的 C 文件即可。
  • 如果工程的头文件被修改了,那么我们需要编译所有引用这个头文件的 C 文件,并且 链接成可执行文件。

makeFile的使用

  • 在工程目录下创建名为“Makefile”的文件, 文件名一定要叫做“Makefile”!!!区分大小写的哦!
  • image-20220109171722524

Makefile文件:

main: main.o input.o calcu.o
gcc -o main main.o input.o calcu.o
main.o: main.c
gcc -c main.c
input.o: input.c
gcc -c input.c
calcu.o: calcu.c
gcc -c calcu.c

clean:
rm *.o
rm main

  • 上述代码中所有行首需要空出来的地方一定要使用TAB键!不要使用空格键!这是 Makefile 的语法要求

  • image-20220109172156926

  • Makefile 编写好以后我们就可以使用 make 命令来编译我们的工程了,直接在命令行中输入make即可,make 命令会在当前目录下查找是否存在Makefile这个文件,如果存在的 话就会按照 Makefile 里面定义的编译方式进行编译

image-20220109172335268

MakeFile中一般存在的错误

  • Makefile 中命令缩进没有使用 TAB 键!
  • VI/VIM 编辑器使用空格代替了 TAB 键,修改文件/etc/vim/vimrc,在文件最后面加上如 下所示代码:
    • set noexpandtab

此时修改一下input.c文件,重新编译看结果

image-20220109172511759

可以看出因为我们修改了 input.c 这个文件,所以 input.c 和最后的可执行文 件 main 重新编译了,其它没有修改过的文件就没有编译

Makefile语法

image-20220109173249859

  • 比如image-20220109174534114
    • 这条规则的目标是 main,main.o、input.o 和 calcu.o 是生成 main 的依赖文件,如果要更新 目标 main,就必须先更新它的所有依赖文件,如果依赖文件中的任何一个有更新,那么目标也 必须更新“更新”就是执行一遍规则中的命令列表
    • 每条命令以tab开始
    • make 命令会为 Makefile 中的每个以 TAB 开始的命令创建一个 Shell 进程去执行。
  • 重新看一下上面的代码
main: main.o input.o calcu.o
gcc -o main main.o input.o calcu.o
main.o: main.c
gcc -c main.c
input.o: input.c
gcc -c input.c
calcu.o: calcu.c
gcc -c calcu.c

clean:
rm *.o
rm main
  • 首先更新第一条规则中的 main,第一条规则的目标成为默认目标,只要默认目标更新了那 么就认为 Makefile 的工作。在第一次编译的时候由于 main 还不存在,因此第一条规则会执行, 第一条规则依赖于文件 main.o、input.o 和 calcu.o 这个三个.o 文件,这三个.o 文件目前还都没 有,因此必须先更新这三个文件。make 会查找以这三个.o 文件为目标的规则并执行以 main.o 为例,发现更新 main.o 的是第二条规则,因此会执行第二条规则,第二条规则里面的命令为“gcc –c main.c”,这行命令很熟悉了吧,就是不链接编译 main.c,生成 main.o,其它两个.o 文件同理。 最后一个规则目标是 clean,它没有依赖文件,因此会默认为依赖文件都是最新的,所以其对应 的命令不会执行,当我们想要执行 clean 的话可以直接使用命令make clean,执行以后就会删 除当前目录下所有的.o 文件以及 main,因此 clean 的功能就是完成工程的清理
  • image-20220109180157902
  • 可见这条命令将除了源文件和Makefile以外的编译产物都删除了

总结一下Makefile的编译过程

  • make 命令会在当前目录下查找以 Makefile(makefile 其实也可以)命名的文件。
  • 当找到 Makefile 文件以后就会按照 Makefile 中定义的规则去编译生成最终的目标文件。
  • 当发现目标文件不存在,或者目标所依赖的文件比目标文件新(也就是最后修改时间比 目标文件晚)的话就会执行后面的命令来更新目标。

这就是 make 的执行过程,make 工具就是在 Makefile 中一层一层的查找依赖关系,并执行相应的命令。

Makefile 变量

  • Makefile 中的变量都是字符串

  • 实例

#Makefile 变量的使用
objects = main.o input.o calcu.o
main: $(objects)
gcc -o main $(objects)
  • Makefile 中可以写注释,注释开头要 用符号“#”

  • 变量的引用方法是$(变量名)

不同赋值符号的区别

  • 幅值符=
    • 类似于引用传参,幅值的变量的值会随着被赋给它的变量的值的改变去改变
name = zzk
curname = $(name)
name = zuozhongkai
print:
@echo curname: $(curname)

​ 此时输出的是zuozhongkai,意味着变量的内容随着变量的值更新而更新

@的意思是使得make在执行的过程中输出执行过程,否则不会输出

  • 幅值符:=

    • 同样执行上面的代码,将=改为:=,则可见输出还是”zzk”,因为:=在幅值的时候不会采用变量修改后的值
  • 幅值符?=

    • curname ?= zuozhongkai的意思是,假如curname前面没有被赋值,那么此变量就是“zuozhongkai”, 如果前面已经赋过值了,那么就使用前面赋的值。
  • 幅值符+=

    • Makefile 中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进 去,此时就要使用到符号“+=”,比如

    • objects = main.o inpiut.oobjects += calcu.o执行完之后,objects就变成了main.o input.o calcu.o

Makefile模式规则

  • 自动匹配

    • 模式规则中,至少在规则的目标定定义中要包涵%,否则就是一般规则,目标中的% 表示对文件名的匹配,%表示长度任意的非空字符串,比如“%.c”就是所有的以.c 结尾的 文件,类似与通配符,a.%.c 就表示以 a.开头,以.c 结束的所有文件。

    • 当“%”出现在目标中的时候,目标中“%”所代表的值决定了依赖中的“%”值,比如%.o : %.c中的%代表的是同样的内容

前面的代码可以修改如下:

objects = main.o input.o calcu.o
main: $(objects)
gcc -o main $(objects)

%.o : %.c
#命令
clean:
rm *.o
rm main

Makefile自动化变量

  • 如何通过一行命令来从不同的依赖文件中生 成对应的目标?自动化变量就是完成这个功能的!所谓自动化变量就是这种变量会把模式中所 定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完,类似于python中的变量解包,将一个数组中的变量一个一个的拆出来。自动化变量只应该出现在规则的命令中。
  • image-20220110002525509
objects = main.o input.o calcu.o
main: $(objects)
gcc -o main $(objects)
%.o : %.c
gcc -c $<
clean:
rm *.o
rm main

  • 上面的代码中,$<代表依赖文件(.c)的一系列集合

  • 上述规则中并没有创建文件 clean 的命令,因此工作目录下永远都不会存在文件 clean,当 我们输入“make clean”以后,后面的“rm *.o”和“rm main”总是会执行。可是如果我们“手贱”,在工作目录下创建一个名为“clean”的文件,那就不一样了,当执行“make clean”的时 候,规则因为没有依赖文件,所以目标被认为是最新的,因此后面的 rm 命令也就不会执行,我 们预先设想的清理工程的功能也就无法完成。为了避免这个问题,我们可以将 clean 声明为伪 目标,声明方式如下:

.PHONY : clean
objects = main.o input.o calcu.o
main: $(objects)
gcc -o main $(objects)

.PHONY : clean

%.o : %.c
gcc -c $<
clean:
rm *.o
rm main

  • 声明 clean 为伪目标,声明 clean 为伪目标以后不管当前目录下是否存在名 为“clean”的文件,输入“make clean”的话规则后面的 rm 命令都会执行

Makefile条件判断

语法

<条件关键字>
<条件为真时执行的语句>
endif

<条件关键字>
<条件为真时执行的语句>
else
<条件为假时执行的语句>
endif

条件关键字的组成

  • ifeqifneq,判断的是是否相等和是否不等
    • 语法
ifeq (<参数 1>, <参数 2>)
ifeq ‘<参数 1 >’,‘ <参数 2>’
ifeq “<参数 1>”, “<参数 2>”
ifeq “<参数 1>”, ‘<参数 2>’
ifeq ‘<参数 1>’, “<参数 2>”
  • ifdefifndef
ifdef <变量名>

​ 如果“变量名”的值非空,那么表示表达式为真,否则表达式为假。“变量名”同样可以是 一个函数的返回值。ifndef 用法类似,但是含义用户 ifdef 相反。

Makefile函数

  • Makefile 支持函数,类似 C 语言一样,Makefile 中的函数是已经定义好的,我们直接使用, 不支持我们自定义函数。make 所支持的函数不多,但是绝对够我们使用了
$(函数名 参数集合)
${函数名 参数集合}
  • 可以看出,调用函数和调用普通变量一样,使用符号“$”来标识。参数集合是函数的多个 参数,参数之间以逗号“,”隔开函数名和参数之间以“空格”分隔开,函数的调用以“$”开 头。

sbust

  • 函数 subst 用来完成字符串替换,调用形式如下:
$(subst <from>,<to>,<text>)
  • 此函数的功能是将字符串中的内容替换为,函数返回被替换以后的字符 串,比如如下示例:
$(subst zzk,ZZK,my name is zzk)
  • 把字符串“my name is zzk”中的“zzk”替换为“ZZK”,替换完成以后的字符串为“my name is ZZK”。

patsubst

  • 函数 patsubst 用来完成模式字符串替换,使用方法如下:
$(patsubst <pattern>,<replacement>,<text>)
  • 此函数查找字符串中的单词是否符合模式,如果匹配就用来 替换掉,可以使用通配符“%”,表示任意长度的字符串,函数返回值就是替换后的字 符串。如果中也包涵“%”,那么中的“%”将是中的那个 “%”所代表的字符串,也就是说%代表的字符串的内容不变
$(patsubst %.c,%.o,a.c b.c c.c)
  • 将字符串a.c b.c c.c中的所有符合%.c的字符串,替换为%.o,替换完成以后的字 符串为“a.o b.o c.o”,注意此时a,b和c是不变的。

dir

  • 函数 dir 用来获取目录
$(dir <names…>)
  • 此函数用来从文件名序列中提取出目录部分,返回值是文件名序列的目录 部分
$(dir </src/a.c>)
  • 提取文件“/src/a.c”的目录部分,也就是“/src”。

notdir

  • 函数 notdir 看名字就是知道去除文件中的目录部分,也就是提取文件名
$(notdir <names…>)
  • 此函数用与从文件名序列中提取出文件名非目录部分
$(notdir </src/a.c>)
  • 提取文件“/src/a.c”中的非目录部分,也就是文件名“a.c”。

foreach

  • foreach 函数用来完成循环
$(foreach <var>, <list>,<text>)
  • 此函数的意思就是把参数中的单词逐一取出来放到参数中,然后再执行所 包含的表达式。每次都会返回一个字符串,循环的过程中,**中所包含的每个字符串 会以空格隔开,最后当整个循环结束时,所返回的每个字符串所组成的整个字符串将会是 函数 foreach 函数的返回值**

wildcard

  • 通配符“%”只能用在规则中只有在规则中它才会展开,如果在变量定义函数使用时, 通配符不会自动展开,这个时候就要用到函数 wildcard
$(wildcard PATTERN…)

比如

$(wildcard *.c)

上面的代码是用来获取当前目录下所有的.c 文件,类似“%”

使用例

(文件夹下有a.c, b.cc.c三个文件)

#!/bin/bash
list=$(wildcard *.c)
files=$(foreach filename, $(list), $(filename) hello)
main:

@echo ${list}

@echo $(files)

.PHONY : clean
clean:
rm *.o

输出

image-20220110111644323