请君浏览
前言
本专题将介绍关于Linux操作系统的种种,那么上一章我们讲解了Linux中最常用的编辑器vim和编译器gcc,本章将继续讲解Linux中的其他的基本开发工具。(本章节默认使用的环境是centos 7.8)
1. 自动化构建
1.1 背景
首先,我们要知道什么是自动化构建,自动化构建到底有什么作用。
自动化构建是提高开发流程效率与质量的工具。在Linux中最常用的自动化构建工具由两部分组成:make和makefile
- make是⼀个命令⼯具,用于解释makefile中指令的命令⼯具
- makefile是一个文件,用于定义构建规则和依赖关系
两者搭配使用,完成项目自动化构建。
那么自动化构建适用于哪里、有什么作用呢?
在我们编写一些项目时,通常会把各种声明和定义以及测试放在不同的文件中,例如我们之前自己实现数据结构的时候:
我们会把栈的各种声明放在Stack.h的头文件中,把其中函数的具体实现放在Stack.c中,最后在main.c中进行测试以及使用,放在不同的文件中可以提高代码的可维护性以及方便代码的复用等好处,特别是在大型项目这些好处体现的就会更加明显。
我们知道,对于这种多文件的编译我们是先把所有的.c文件编译为.o文件,也就是可重定位目标二进制文件,使之停留在汇编阶段,然后再将所有.o文件链接形成最后的可执行文件。那么在Linux下我们该如何操作呢?其实很简单,如下图所示:
我们只需要用gcc同时编译需要链接的.c文件,这样它们就可以链接在一起了。虽然目前来看也并没有难度,可是如果我们需要链接的一起的源文件有几十个、几百个、甚至上千个呢?如果再靠我们一个一个手动去输入的话恐怕等输入完黄花菜都凉了。这个时候就需要用到我们的自动化构建工具——make了。我们可以通过一个make命令来完成这繁琐以及更复杂的功能。
⼀个⼯程中的源⽂件不计数,其按类型、功能、模块分别放在若⼲个⽬录中,我们可以在makefile文件中定义⼀系列的规则来指定,哪些⽂件需要先编译,哪些⽂件需要后编译,哪些⽂件需要重新编译,甚⾄于进⾏更复杂的功能操作。通过make软件来执行我们makefile文件中的操作,可以实现大面积的自动化。
makefile带来的好处就是⸺“⾃动化编译”,⼀旦写好,只需要⼀个make命令,整个⼯程完全⾃动编译,极⼤的提⾼了软件开发的效率。
下面让我们来看看该如何使用make。
1.2 基本语法
使用make最重要的就是编写makefile文件了,只有在makefile文件中合理且正确的写出我们需要的指令,才能使用make来达到我们想要的效果,下面让我们简单来看一看如何在makefile中编写指令从而可以编译我们上面的代码:
我们先来看一个最简单但没有通用性的:
例如上图,test.c是一个我们最基础的C语言代码,我们通过编写Makefile文件执行make命令,就可以自动编译该文件:
下面让我们来看一看Makefile文件中的每一句指令都是什么意思,代表了什么:
- 目标文件:首先目标文件也就是我们通过依赖关系和依赖方法需要形成的文件
- 依赖关系:依赖关系是我们生成目标文件所需要的文件
- 依赖方法:依赖方法是我们通过依赖关系生成目标文件的命令
- 伪目标:在Makefile中,伪目标是一种特殊的目标,它们不对应于实际的文件,而是用来执行特定的命令。伪目标通常用于清理项目(如删除编译生成的文件),执行测试,或者其他不产生文件的操作。
1.3 make的运行原理
像clean
这种,没有被第⼀个⽬标⽂件直接或间接关联,那么它后⾯所定义的命令将不会被⾃动执⾏,不过,我们可以显⽰要make执⾏。即命令⸺make clean
,以此来清除所有的⽬标⽂件,以便重编译。因此,我们通常将它设置为伪目标。用.PHONY
修饰,伪⽬标的特性是,总是被执⾏的。
总是可执行我们该如何理解呢?想要理解这个概念我们要知道make是如何⼯作的,在默认的⽅式下,也就是我们只输⼊make
命令。那么:
- 首先,make会在当前⽬录下找名字叫
Makefile
或makefile
的⽂件,在找到Makefile文件后会自定向下扫描Makefile文件,默认形成第一个目标文件,如果想要指定形成,可以使用make+targetname
命令。还是以上面的Makefile
为例,它会找到test
这个文件,并把这个文件作为最终的目标文件。
我们修改一下Makefile中的内容,来演示一下在Makefile中如果依赖关系暂时不存在时make的运行:
如上图所示,使用make默认形成的最终文件是test,它的依赖是test.o
,可是我们在当前目录下并没有找到test.o
文件,那么那么 make 会寻找⽬标为test.o
⽂件的依赖性,如果找到则再根据那⼀个规则⽣成tesc.o
⽂件。(这有点像⼀个堆栈的过程),具体的过程如下图所示:
这就是整个make的依赖性,make会⼀层⼜⼀层地去找⽂件的依赖关系,直到最终编译出第⼀个⽬标⽂件。在找寻的过程中,如果出现错误,⽐如最后被依赖的⽂件找不到,那么make就会直接退出,并报错。make只管⽂件的依赖性,即,如果在我找到了依赖关系之后,冒号后⾯的⽂件还是不存在,那么就会报错。
那么如果我们连续使用make命令会发生什么呢?
我们拿上面的Makefile
为例,如果test文件不存在,或者 test 所依赖的后⾯的 test.c ⽂件的⽂件修改时间要⽐ test 这个⽂件新,那么,他就会执⾏后⾯所定义的命令也就是依赖方法来⽣成test
这个⽂件。 但是当test存在且它所依赖的test.c
文件的修改时间要小于test
的修改时间,那么我们再次执行make命令时系统会给我们一个提示,并且make不再执行:
如上图所示,我们在使用一次make后再次使用系统给了我们一个提示:test文件以及是最新的了。如果再次进行编译得到的结果也与之前一样。那么make是怎么识别的呢?它其实看的就是test文件与test.c文件修改时间的比较,test.c的修改时间小于test:
那么什么是修改时间呢?这里就涉及到了文件属性中的三大时间,分别为:
- 访问时间(atime):最近一次访问的时间,使用cat、more等指令时该时间会改变。
- 修改时间(mtime):文件内容最后被修改的时间,对文件进行写入操作并保存后该时间会改变。
- 状态改动时间(ctime):文件属性最后被修改的时间,文件的属性包括大小、权限等等,当它们发生变化的时候该时间会改变。
(小贴士:我们可以用stat命令查看文件的详细属性,用file命令查看文件的详细类型)
其中,make比较的是文件的修改时间(mtime),也就是Modify时间,通过下图相信大家能有更深的理解:
上面的原理我们理解之后再来看伪目标的总是被执行的特性就可以理解了,也就是说伪目标可以让make忽略源⽂件和可执⾏⽬标⽂件的M时间对⽐,每次使用时都可以被执行。
1.4通用的makefile
上面我们所写的makefile局限性很大,它限制了我们源文件的文件名以及个数,那么我们该如何实现一个通用的makefile文件呢?
- 首先,对于很多的程序它的源文件都不止一个,我们需要先将这些源文件都编译成
.o
文件,然后再进行链接形成可执行程序。
我们通过下图来介绍一下,这就是一个比较通用的Makefile文件:
- 在Makefile文件中
#
用来注释,使用变量需要用$(变量名)
- 在命令前加上@符号可以在make执行时不在shell上再显示执行的命令,也就是使命令不再回显
- 我们用
echo
可以显示我们使用make的过程 $^
代表依赖文件列表,$@
代表目标文件名,$<
代表对展开的依赖.c
文件,一个个交给gcc%.c
:展开当前目录下所有的.c
文件%.o
:展开当前目录下所有的.o
文件
下面让我们来看一看效果,我们用最开始的代码为例:
当前我们只有两个源文件,已经能够管中窥豹,如果是在大型项目中,使用自动化构建make会非常的方便。当然,上面的通用Makefile文件也只是一个简单的版本,我们还可以加入更多的指令去满足我们更多样化的需求,在使用中相信大家会逐渐体会到make对于提高编译效率方面的好处。
2. 牛刀小试–Linux第一个小程序
工具学到这里,我们已经学会了编辑器vim、编译器gcc和自动化构建make,这些已经可以支撑我们在Linux中写一些有趣的小程序了。俗话说得好,光说不练假把式。既然我们已经学了这么多工具,那么接下来让我们来实际的上手操作一下,巩固之前的学习,让我们来一个小程序叫做进度条。
2.1 回车与换行
在写进度条之前我们需要先明确两个概念:回车与换行。我们日常经常把回车和换行看作是同一种东西,其实并不然:
- 回车:回到所在行的起始位置
- 换行:换到当前位置的下一行
如下图所示:
可以看到,我们实际上的换行操作是换行加上回车,我们的'\n'
就是一个典型的例子,键盘上的Enter键的图标也在提醒我们:
在C语言中,我们用'\r'
来表示回车。
2.2 行缓冲区
接着我们再来看一看行缓冲区的概念。这里提出一个问题:我们使用printf函数打印的内容是直接出现在显示器上的吗?
我们可以通过代码来验证一下:
通过对两种情况的测试我们发现,在带\n
时,我们打印的内容是先出现在显示屏后再等待两秒;而不带\n
时,是先等待了两秒后我们打印的内容才显示在显示器上。
这是因为在内存上存在一片缓冲区,我们显示器的刷新策略是行刷新:一行一行的刷新。在遇到'\n'
时由于要进行换行,所以直接刷新在显示器上,而没有\n
时,要打印的内容一直在缓冲区中,当程序运行完后才刷新到显示器上。那么有没有什么方法让它立即刷新呢?C语言提供了一个名为fflush
的函数,可以立即刷新缓冲区中的内容:
那么在使用fflush函数后我们可以发现,即便没有'\n'
,我们的程序也是先打印出内容后再进行等待:
2.3 倒计时小程序
那么基于此,我们便可以写一个简单的倒计时程序:
#include<stdio.h>
#include<unistd.h>
int main()
{
int ret = 10;
while(ret >= 0)
{
printf("%-2d\r", ret--);
fflush(stdout);
sleep(1);
}
printf("\n");
return 0;
}
每一次打印后’\r’回到起始位置,然后使用fflush函数将缓冲区中的内容刷出,每次打印间隔一秒,这样,我们就得到了一个简单的倒计时程序。
2.4 进度条小程序
上面通过简单的倒计时小程序我们学会了'\r'
结合fflush
的小用法,那么接下来让我们更进一步,在设计一个进度条程序。
原理
我们再下载程序以及上传软件等状况下都会有一个进度条来显示我们的进度。一个简单的进度条我们可以看作三个部分组成:条形图、百分比、旋转光标
我们知道,在进度条运行的过程中总长度并不会改变,只是条形图中的填充物会逐渐增多,百分比逐渐变大,旋转光标不停的转动,代码进度条在正常运行。因此,我们只需要提前预留出若干个空间,用作填充条形图,预留空间是固定条形图的总长度;然后根据倒计时小程序去设计百分比不断的变大,直到达到百分之百;而旋转光标我们只需要用一个字符数组,不断遍历其中的字符即可。
代码
思路有了,那么让我们直接用vim来编写我们在Linux中的C语言程序吧:
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101
#define STYLE '='
int main()
{
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char *lable="|/-\\";
int len = strlen(lable);
int cnt = 0;
while(cnt <= 100)
{
printf("[%-100s][%d%%][%c] \r", buffer, cnt, lable[cnt%len]);
fflush(stdout);
buffer[cnt]= STYLE;
cnt++;
usleep(50000);
}
printf("\n");
}
运行后我们可以发现跟我们的预期基本一致。当然,我们这个只是非常简易的进度条,并没有什么实际用途,我们只是用作对之前所学工具的巩固。大家感兴趣的话也可以自己去模拟变化的网速去设计更加有效果的进度条玩一玩。
3. 版本控制器git
3.1 认识
我们该如何理解版本控制呢?这里举一些简单的例子:
当我们再玩一些单机的游戏时,由于我们的各种游戏数据都存储在本地,因此当我们在打boss前或者去进行一些危险的操作时可以提前把我们的游戏数据进行备份,这样在我们打boss失败了还损失了强力道具时我们可以通过之前的备份来恢复到我们打boss前的状态。当然我们可以在不同的时间段留下不同的备份,可以使我们恢复到想要的时间段。
我们写论文的时候也是如此,当我们写完初稿后会找导师进行请教,根据导师的建议不断完善我们的论文,每一次修改前我们都需要做好相应的备份,以便失误后能够恢复到原来的版本。
这些不同的备份我们通常将其称作不同的版本,当我们的版本越来越多之后,我们该如何知道每个版本各自都是修改了什么呢?
我们写的项目代码也是如此,随着不断的迭代产生了许多不同的版本。为了方便管理这些不同版本的文件,便有了版本控制器。
所谓的版本控制器,就是能让你了解到⼀个⽂件的历史,以及它的发展过程的系统。通俗的讲就是⼀个可以记录⼯程的每⼀次改动和版本迭代的⼀个管理系统,同时也⽅便多⼈协同作业。
⽬前最主流的版本控制器就是 Git 。Git 可以控制电脑上所有格式的⽂件,例如 doc、excel、dwg、dgn、rvt等等。对于我们开发⼈员来说,Git 最重要的就是可以帮助我们管理软件开发项⽬中的源代码⽂件!⾃诞⽣于 2005 年以来,Git ⽇臻成熟完善,在⾼度易⽤的同时,仍然保留着初期设定的⽬标。 它的速度⻜快,极其适合管理⼤项⽬,有着令⼈难以置信的⾮线性分⽀管理系统。
但是这些文件只是放在本地可能会遭遇硬件受损等外力影响导致文件丢失,于是,基于Git的代码托管服务平台就诞生了,例如我们耳熟能详的Github,这是一个全球程序员都在使用的代码托管平台。但是由于某些原因,我们在国内想要使用GitHub不太容易。但是国内也有自己的代码托管平台,例如Gitee
3.2 git的使用
我们要想使自己的项目上传到远端,也就是GitHub或者Gitee上,首先需要有相应平台的账户(由于某些原因,我们以Gitee为例),然后新建一个仓库:
然后根据我们的需要填写相关的内容:
创建好之后我们点击克隆/下载
,复制对应的链接 :
之后我们在Linux下就可以下载项目到本地了(windows的操作一样):git clone [url]
,url就是我们刚才复制的链接。
当然,如果你在GitHub和Gitee上想要下载开源项目也是一样的操作,复制项目的链接使用上面的命令下载到本地。
我们可以先检查一下当前环境下是否安装了git,通过命令
git --version
。如果没有安装,我们使用命令
yum install -y git
进行安装。
我们来看一下下载到本地后都有些什么文件:
- 前两个不用再进行解释了,分别代表了上级目录和当前目录
README.md
:可以理解为项目说明书,一份中文版,一份英文版。.git
:本地仓库。里面包含了我们所有的修改记录。.gitignore
:在该文件中进行配置可以忽略一些特殊不需要或者不想add的文件。
Git提交的时候,只会提交变化的部分。例如我们有一个100行代码的文件,我们对其最后一行进行了修改,随后提交到了git上,这里git上只是保存了我们变化的那一部分,而不是全部保存。
三板斧
git add
将文件放到刚才下载好的目录中
git add [文件名]
第一步将需要用git管理的文件告知git。
git commit
提交改动到本地
git commit -m "说明"
提交的时候应该注明提交⽇志, 描述改动的详细内容。
git push
同步到远端服务器上
git push
需要填⼊⽤⼾名和密码,同步成功后, 刷新 Gitee⻚⾯就能看到代码改动了。
我们来操作一下:
我们在拉取项目到本地后的目录称为工作区,我们在工作区中新建一个文件:
这个时候我们的code文件还不在本地仓库中,我们需要把他先add到一块命为暂存区的地方,然后commit把暂存区中的所有内容提交到本地仓库,最后在push同步到远端仓库上:
(我们可以使用git add *
将当前目录下所有陌生的文件添加到暂存区中)
push我们需要输入账户名和密码:
成功之后刷新我们的Gitee网页,就可以看到代码已经同步了:
3.3 其他
我们可以使用git log
来查看我们该仓库所有的提交记录:
使用git进行版本管理,我们只进行管理源文件,也就是.c .cpp .h
等,不管理各种临时文件,上面我们提到的.gitignore
文件中就是各种临时文件的后缀名,可以帮助我们过滤这些文件,在提交的时候不提交这些文件:
当然,我们也可以进行修改,例如加入一些我们不需要的特殊文件。
git status
命令是一个用于查看 Git 仓库当前状态的命令。它显示了工作目录和暂存区的状态,帮助开发者了解哪些文件已修改、哪些文件未跟踪以及哪些文件已暂存。
至于git还有更多的操作,这里就不再详细的讲解了,我们现在的目的是能够简单的使用git上传和下载项目就可以了。
4. 调试器gdb/cgdb
4.1 了解
相信之前使用vs2022这些集成式开发环境时大家都领略到了调试的魅力,通过调试我们可以快速的找到代码中的问题所在。那么在Linux下我们可以对代码进行调试吗?答案是当然可以。
在Linux下有专门的调试器gdb来帮助我们进行调试的工作。
程序的发布方式有两种,一种是debug模式,一种是release模式,在vs2022上我们想要对代码进行调试需要在debug模式上,在Linux中的gdb也是如此,而我们在Linux下使用gcc/g++进行编译出来的程序默认是release模式,因此要想使用gdb进行调试,我们需要在编译时带上-g选项gcc xxx.c -o xxx -g
,使其以debug模式发布。我们日常使用的各种软件都是release模式。
可以看到我们debug模式下的可执行程序的内存是要大于release模式下的,这是因为在debug模式中包含了我们的调试信息,正是有了这些调试信息我们才能对其进行调试。
两种模式下运行结果也是一样的:
我们调试的对象是携带调试信息的可执行程序(也就是debug模式),而不是我们的源文件。
4.2 使用
开始:我们直接用gdb加上需要调试的程序名即可使用gdb:
gdb [name]
退出:
ctrl+d
或者quit
调试命令
下面是一些gdb的常用命令:
命令 | 作⽤ | 样例 |
---|---|---|
list/l |
显⽰源代码,从上次位置开始,每次列出10⾏ | list/l 10 |
list/l 函数名 |
列出指定函数的源代码 | list/l main |
list/l ⽂件名:⾏号 |
列出指定⽂件的源代码 | list/l mycmd.c:1 |
r/run |
从程序开始连续执⾏ | run |
n/next |
单步执⾏,不进⼊函数内部 | next |
s/step |
单步执⾏,进⼊函数内部 | step |
break/b [⽂件名:]⾏号 |
在指定⾏号设置断点 | break 10break test.c:10 |
break/b 函数名 |
在函数开头设置断点 | break main |
info break/b |
查看当前所有断点的信息 | info break |
finish |
执⾏到当前函数返回,然后停⽌ | finish |
print/p 表达式 |
打印表达式的值 | print start+end |
p 变量 |
打印指定变量的值 | p x |
set var 变量=值 |
修改变量的值 | set var i=10 |
continue/c |
从当前位置开始连续执⾏程序 | continue |
删除所有断点 |
delete breakpoints | |
删除序号为n的断点 |
delete breakpoints 1 | |
disable breakpoints/[断点编号] |
禁⽤所有/指定断点 | disable breakpoints |
enable breakpoints/[断点编号] |
启⽤所有/指定断点 | enable breakpoints |
info/i breakpoints |
查看当前设置的断点列表 | info breakpoints |
display 变量名 |
跟踪显⽰指定变量的值(每次停⽌时) | display x |
undisplay 编号 |
取消对指定编号的变量的跟踪显⽰ | undisplay 1 |
until X⾏号 |
执⾏到指定⾏号 | until 20 |
backtrace/bt |
查看当前执⾏栈的各级函数调⽤及参数 | backtrace |
info/i locals |
查看当前栈帧的局部变量值 | info locals |
quit |
退出GDB调试器 | quit |
调试的本质:帮助我们找到代码的问题所在。
4.3 小技巧
在没有图形化界面进行调试还是很难操作的,下面的一些技巧可以帮助我们更好的进行调试。
4.3.1 安装cgdb
我们用gdb的时候只能通过list命令看到我们的代码,不是很方便,cgdb这个工具的命令与gdb基本一样,不过它可以使我们的代码在上方展现,使我们的调试过程更加清晰一点:
- Ubuntu:
sudo apt-get install -y cgdb
- Centos:
sudo yum install -y cgdb
4.3.2 watch
执⾏时监视⼀个表达式(如变量)的值。如果监视的表达式在程序运⾏期间的值发⽣变化,gdb会暂停程序的执⾏,并通知使⽤者:
如果你有⼀些变量不应该修改,但是你怀疑它修改导致了问题,你可以watch它,如果变化了,就会通知你。
4.3.3 set var
可以直接修改变量的值,可以帮助我们直接确定原因所在:set var [变量名]=[值]
4.3.4 条件断点
我们在建立断点时可以给予其特定的条件,使其在满足条件时才可以触发:b [行号] [条件]
当然,我们也可以给已经存在的断点设定条件,使用命令:condition [断点编号] [条件]
注意:
- 上面条件断点添加两种⽅式语法略有不同,第一种条件前需要加
if
,第二种则不用。- cgdb中可以使用esc进入上方代码屏,用i回退到下方gdb屏。
尾声
本章讲解就到此结束了,若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!