目录
4.Linux项目自动化构建工具 -- make/Makefile
4.3.1 Access time/Modify time/Change time
4.4.1用于编译链接C语言代码的makefile文件的一种版本
1.1Linux下的安装方式
1.源码安装 -- 把一个程序源代码拷贝到本地,然后进行编译,得到可执行程序。
2.软件包安装(rpm) -- 把一些常用的软件提前编译好,做成软件包放在一个服务器上,获取到该软件包直接进行安装。
3.通过包管理器进行安装(yum/apt(apt-get)) -- 一个软件能正常运行不仅仅需要该软件的软件包,还需要许多该文件需要的依赖包和依赖文件,缺失依赖包可能会带来依赖缺失以及版本兼容性的问题(比如该软件需要使用C++20的特性,但是本地只有C++11,所以这时就会产生版本兼容的问题),通过包管理器进行安装可以直接将一个软件所需的依赖包也进行安装,可以有效的解决依赖缺失和版本兼容的问题。
安装的本质:(1)通过网络将需要安装的文件下载下来。(2)将下载下来的文件拷贝到系统对应的配置文件当中 -- Linux下的配置文件都在根目录下的子目录中,所以在Linux下进行安装的时候需要使用root权限。
1.2什么是软件包
在Linux下安装软件一个通常的办法是下载到程序的源代码,并进行编译,得到可执行程序。但是这样太麻烦了,于是有些人把一些常用的软件提前编译好,做成软件包(可以理解成windows上的安装程序) 放在一个服务器上,通过包管理器可以很方便的获取到这个编译好的软件包,直接进行安装。软件包和软件包管理器就好比"APP"和"应用商店"这样的关系。
yum是Linux下非常常用的一种包管理器。主要应用在Fedora,RedHat,CentOS等发行版上。apt/apt-get主要应用在Ubuntu上。下面用apt进行介绍
注:使用包管理器安装软件时,需要网络畅通。可以通过ping www.baidu.com进行网络是否畅通的验证。
1.3软件包由谁提供
由对应发行版的社区提供,如www.centos.org。这种社区一般是放在云服务器上的。
为什么我的操作系统能自己找到下载链接呢?因为操作系统在下载的时候就内置链接。Linux是外国人发明的,所以内置的链接基本上都是国外的生态,下载就会变慢。所以国内的厂商比如清华大学、阿里云等就把国外的软件包镜像到了国内,通过修改Linux中的镜像源,就能改变下载的下载链接。
1.4查看软件包
通过yum/apt list命令可以罗列出当前一共有哪些软件包。由于包的数目可能非常多,这里我们可以使用grep命令只筛选出我们关注的包。例如:
apt list | grep lrzsz
base -- 稳定软件源,epel -- 扩展软件源 -- CentOS系统下可以观察到。
1.5Ubuntu下如何安装卸载软件
安装软件:
语法:sudo apt/apt-get install 软件名
例如:
sudo apt install sl
卸载软件:
语法:sudo apt/apt-get remove 软件名
1.6更改镜像源
centOS下就是将/etc/yum.repos.d/中的下载链接进行更换。
Ubuntu下就是将/etc/apt/sources.list中的下载链接进行更换。
这里不做具体操作,可以在网上搜索对应镜像的链接然后进行修改,清空缓存并重新生成缓存即可。
2.Linux编辑器 -- vim
2.1vim的基本概念
vim的三种模式(其实有很多种模式,目前介绍三种),分别是命名模式(command mode)、插入模式(insert mode)和底行模式(last line mode),各模式的功能区分如下:
正常/普通/命令模式(Normal mode):控制屏幕光标的移动,字符、字或行的删除,移动复制某区段及进入插入模式或者底行模式。
插入模式(Insert mode):只有在插入模式下才能进行文字输入,在命名模式下按i进入插入模式,按Esc可回到命令模式,该模式是用的最频繁的编辑模式。
底行模式(Last line mode):文件保存或退出,也可以进行文件替换,找字符串,列出行号等操作。在命令模式下按shift + ;进入该模式,要查看所有模式:打开vim,底行模式下直接输入
:help vim-modes
2.2vim的基本操作
1.进入vim,输入 vim [文件名] [行号],就能进入vim全屏幕编辑画面。加行号可以使打开文件后直接将光标定位到对应行。
2.[正常模式]切换至[插入模式]:输入 'a/i/o' 即可。 进入插入模式后会在底行显示如下字符串。
3.[正常模式]切换至[底行模式]:输入'shift' + ';' ,底行会显示 ':' 。
4.[正常模式]切换至[视图模式]:输入'shift' + 'v',底行会显示如下字符串。
批量化注释:
(1)ctrl + v:进入视图模式。
(2)利用h,j,k,l,上下左右移动进行区域选择。[正常模式]下的命令也能用。
(3)shift + i 进入插入模式,输入//,按Esc结束操作。
5. 从其他模式切换至[正常模式]:按Esc键。
6.退出vim及保存文件,底行模式下输入以下字符:
:w -- 保存当前文件
:wq -- 保存当前文件并退出vim
:q -- 不保存退出vim
:q! -- 不保存强制退出vim
2.3vim正常模式命令集
2.4vim底行模式命令集
知识点1:
vim可以直接打开一个不存在的文件,退出时输入:wq可以进行保存,如果不保存退出不会创建该文件。知识点2:底行模式下输入:[vs 文件名] --- 进行分屏操作。知识点3:在分屏的情况下,光标在哪个文件就是对哪个文件的操作。[ctrl + w + w] 可以在分屏操作下切换操作的文件。知识点4:[! + 一个字符]:执行最近执行过一个匹配到该字符的命令。
2.5简单vim配置
配置文件的位置在/etc/下面有个名为vimrc的文件,这是系统中公共的vim配置文件,对所有用户都有效。
而在每个用户的家目录下都可以自己建立私有的配置文件命名为".vimrc"。
常用配置选项:
1.设置语法高亮:syntax on
2.显示行号:set nu
使用插件配置vim:要配置好看的vim,原生的配置可能功能不全,可以选择安装插件来完善配置,保证用户是你要配置的用户。该步骤可以去github上去找插件。
2.6 参考资料
3.Linux编译器 -- gcc/g++
3.1gcc命令
知识点1:实例:gcc test.c -o test可以通过上述命令直接生成可执行文件test,不用一步一步生成中间文件(test.i,test.s,test.o)。知识点2:-E选项不加目标文件,默认生成同名.i文件。-S选项不加目标文件,默认生成同名.s文件。-c选项不加目标文件,默认生成同名.o文件。-o选项不加目标文件,默认生成a.out文件。知识点3:常见的情况是把多个源文件都处理成目标文件(.o文件)然后再进行统一的链接。知识点4:命令:ldd 可执行文件功能:查看可执行程序依赖的函数库。知识点5:
预处理就是编译器修改我们的文本代码,所以支持在最后添加‘-D’选项动态添加宏。
3.1.1预处理
3.1.2编译
3.1.3汇编
3.1.4链接
3.2初步了解函数库
函数库一般分为静态库和动态库两种,是打包起来的一堆.o文件。库是一套方法或数据集,为我们开发提供最基本的保证(基本接口,功能),加速我们二次开发。
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。Linux下其后缀名一般为".a",Windows下其后缀名一般为".lib"。
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库在Linux下一般后缀名为“.so”,在Windows下一般后缀为".dll"。gcc/g++在编译时默认使用动态库。完成了链接之后,gcc/g++就可以生成可执行文件。
gcc/g++默认生成的二进制程序是动态链接的,这点可以通过 file 命令验证。
动静态库对比:
1.动态库形成的可执行程序体积较小。
2.可执行程序对静态库的依赖度低,动态库对于可执行程序不能缺失。
3.程序运行需要加载内存,静态链接会在内存中出现大量的重复代码,浪费资源,动态链接比较节省内存和磁盘资源。
知识点1:
对于动态库:要运行我们自己的程序时,不仅要把自己的程序加载到内存中,还需要把自己程序所关联的动态库加载到内存中,如果未来还有其他可执行程序使用到对应的动态库,只需要在内存中去找对应动态库即可,不需要重新加载了。动态库(共享库)的本质是在语言层面,未来在内存中公共的库中的代码只出现一份。
4.Linux项目自动化构建工具 -- make/Makefile
4.1make/Makefile是什么
make是一个命令,makefile(Makefile)是一个文件,两个搭配使用,完成项目自动化构建。
注:makefile中注释代码用"#"来注释。
4.2依赖关系和依赖方法
下图是makefile中写的两行文本:
上面的文件code依赖code.o,code.o依赖code.s,code.s依赖code.i,code.i依赖code.c。
gcc code.* [-option] code --就是与之对应的依赖方法。
4.3实例介绍
在当前目录下创建mypro.c以及makefile文件。下面依次是两个文件中的代码:
#include <stdio.h>
int main()
{
printf("hello xiaoc\n");
return 0;
}
myproc:myproc.c
gcc -o myproc myproc.c
.PHONY:clean
clean:
rm -f myproc
知识点1:
如果没有用.PHONY(例如第一个依赖关系),已经编译过的文件没被修改不能再次编译,再次编译会出现如下显示:
知识点2:
make命名如何知道bin文件(二进制文件)和.c文件的新旧?这里是通过比较两个文件的Modify time来识别出新旧的,如果.c文件的Modify time比bin文件的Modify time新,则make时会重新进行编译。
知识点3:
下图是makefile中形成的一个依赖链,当该依赖关系中没有对应文件时,会依次在下面的依赖关系中进行寻找。有一个类似于栈的空间,依赖关系从上往下扫描,如果该依赖关系没有对应文件,则对应依赖方法入栈,继续向下扫描,扫描到依赖关系中存在对应文件时,依次出栈执行依赖方法。
4.3.1 Access time/Modify time/Change time
1. Access time(访问时间):这是文件最后一次被访问的时间。例如,当使用cat
命令查看文件内容、使用less
命令浏览文件或者通过脚本读取文件时,文件的访问时间就会更新。
注:因为一个文件被访问是很频繁的事情,所以访问时间的更新不是每次访问完都更新,更新一次之后在接下来的一段时间内(大约20min)再次访问都不会更新访问时间,或者是累计查看到达多少次再次更新,这里时间更新的实现和版本有关,不同版本实现不同。
2.Modify time(修改时间):文件内容最后一次被修改的时间。当对文件进行写操作,如使用文本编辑器修改文件内容、使用echo
命令将新内容追加到文件中等操作时,文件的修改时间会更新。
3.Change time(状态改变时间):文件的属性(如文件的权限、所有者、所属组、链接数等)最后一次改变的时间。当执行chmod
(改变文件权限)、chown
(改变文件所有者)等操作时,文件的状态改变时间会更新。
注:修改文件内容也变相的修改了文件的大小,Modify time也是文件的属性,所以修改文件内容,Modify time和文件大小都变了,Change time也会改变。
4.4makefile中的一些语法介绍
这里的定义变量只是一种语句的替换,并不是C/C++中所说的变量,更像是宏替换。
依赖关系只能有一行,依赖方法可以有多行。
4.4.1用于编译链接C语言代码的makefile文件的一种版本
这里在该目录下有code.c和myproc.c两个.c文件,执行make命令有如下结果:
5.Linux第一个小程序 -- 进度条
5.1回车和换行
回车(\r):将光标移到该行的开头。
换行(\n):将光标移到该位置的下一行对应位置。
注:C语言中的'\n'解析为'\r\n'。
5.2简单了解缓冲区
缓冲区简单来说就是一段内存块。观察下面两段代码以及结果
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello xiaoc\n");
sleep(3);
return 0;
}
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello xiaoc");
sleep(3);
return 0;
}
第一段代码打印之后休眠了3秒,第二段代码休眠了3秒再打印的。
这里并不是先执行了sleep()函数,而是第二段代码没有'\n',执行了打印函数先打印到了缓冲区中,程序结束时刷新了一遍缓冲区然后才显示到屏幕上的。这里的'\n'自带一个行缓冲区的刷新,所以第一段代码就先打印到了屏幕上然后才休眠的。
想手动刷新缓冲区可以用fflush()函数。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello xiaoc");
fflush(stdout);
sleep(3);
return 0;
}
知识点1:
理解显示:如果现在往显示器上打印整型12345,则打印的是'1''2''3''4''5'五个字符。显示器只认识字符。
5.3Base版本的进度条 -- 倒计时程序
#include <stdio.h>
#include <unistd.h>
int main()
{
int i = 10;
while(i >= 0)
{
printf("%-2d\r", i);
fflush(stdout);
i--;
sleep(1);
}
printf("\n");
return 0;
}
打印函数中的"%-2d\r"中,'-'表示左对齐,'2'表示2个位宽,'\r'表示回车。
上述代码的结果就是在同一个位置依次从10显示到0.
5.4进度条实现
这里实现的类似与Linux系统下下载时的进度条。
这里整个项目的实现在processbar目录下,该目录下有头文件process.h,源文件process.c,main.c,以及makefile文件。
5.4.1makefile
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
BIN=processbar
$(BIN):$(OBJ)
gcc -o $@ $^
%.o:%.c
gcc -c $< -std=c11
.PHONY:
clean:
rm -f $(OBJ) $(BIN)
5.4.2process.h
这里在头文件中作函数实现的声明。实现两个版本的进度条。
#pragma once
#include <stdio.h>
#include <string.h>
#include <unistd.h>
//版本1 -- 写死的进度条,不能套用到下载和上传中
void process_v1();
//版本2 -- 进度条刷新函数,根据参数中总下载量total和已下载量current计算出比率打印进度条
void FlushProcess(double total, double current);
5.4.3process.c
这里介绍版本2的进度条实现,首先创建一个101大小的数组空间,用于存储表示进度条的符号"#",初始化时全置为0。创建label字符串,用于后续表示程序还在运行时旋转的符号。传入总大小total和已下载大小current,根据比例填充buffer中"#"的个数。最后根据固定格式打印进度条。
#include "process.h"
#define NUM 101
#define STYLE '#'
//进度条version2 -- 传入总大小和已下载的大小,通过比例打印进度条
void FlushProcess(double total, double current)
{
//初始化进度条
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char* label = "|/-\\";
int len = strlen(label);
static int cnt = 0;
//根据比例填充进度条
int num = (int)(current*100 / total);
for(int i = 0; i < num; i++)
{
buffer[i] = STYLE;
}
double rate = current / total;
cnt %= len;
printf("[%-100s][%.1f%%][%c]\r", buffer, rate*100, label[cnt]);
cnt++;
fflush(stdout);
}
//进度条版本1 -- 这个版本无法套用到下载或上传中,其中进度条的执行是写死的,进度条执行完的时间是固定的
void process_v1()
{
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char* label = "|/-\\";
int len = strlen(label);
int cnt = 0;
while(cnt <= 100)
{
printf("[%-100s][%d%%][%c]\r", buffer, cnt, label[cnt % len]);
fflush(stdout);
buffer[cnt] = STYLE;
cnt++;
usleep(50000);
}
printf("\n");
}
5.4.4main.c
这里创建一个回调函数的指针,用于后面调用版本2的函数对象,每调用一次表示打印一次进度条。休眠时间和每次休眠增加的大小(speed)简单模拟下载和上传,下载和上传完毕之后打印一句表示已下载完成或者以上传完成。
#include "process.h"
typedef void (*callback_t)(double total, double current);
double total = 1024.0;
double speed = 1.0;
void Download(callback_t cb)
{
double current = 0;
while(current <= total)
{
//调一次回调函数cd,打印一次进度条
cb(total, current);
//下载
usleep(3000); //这里模拟下载,每3000us下载1MB,每3000us调用一次FlushProcess()
current += speed;
}
printf("\ndownload %.2lfMB Done\n", total);
}
void Upload(callback_t cb)
{
double current = 0;
while(current <= total)
{
//调一次回调函数cd,打印一次进度条
cb(total, current);
//上传
usleep(5000); //这里模拟上传,每5000us上传1MB,每5000us调用一次FlushProcess()
current += speed;
}
printf("\nupload %.2lfMB Done\n", total);
}
int main()
{
Download(FlushProcess);
Download(FlushProcess);
Upload(FlushProcess);
return 0;
}
注意:上述程序有一个小bug,就是屏幕宽度不能在一行全部显示进度条的时候,每次回车的刷新就会在下一行,这样就会每次刷新进度条多一行。
6.版本控制器Git的简单介绍以及基础操作
在工作或者学习中会遇到这样的情况:在编写各种文档时,为了文档丢失或者更改失误后能恢复原来的版本,不得不复制出一个副本,比如:报告-v1,报告-v2,报告-v3...
每个版本有各自的内容,但最终只会有一份报告需要被我们使用。但在此前的工作都需要这些不同版本的报告,于是每次都是复制粘贴副本,产生出的文件就越来越多,文件多不是问题,问题是:随着版本数量的不断增多,每次版本各自都修改了什么已经不记得了。文档如此,写一整个项目代码的时候也存在这个问题。
6.1版本控制器
为了能够更方便管理这些不同版本的文件,便有了版本控制器。就是能让你了解到一个文件的历史,以及它的发展过程的系统。通俗的讲就是一个可以记录工程的每一次改动和版本迭代的一个管理系统,同时方便多人协作。
目前最主流的版本控制器就是Git,并且是开源的。Git可以控制电脑上所有格式的文件,例如doc,excel,dwg,dgn,rvt等等。对于开发人员来说,Git最重要的就是可以帮助管理软件开发项目中源代码文件。
在Centos7下安装git的命名:sudo yum install -y git
其他的Linux发行版以及Windows系统下的操作就自行进行网上的查阅了。
6.2Git的基本操作
这里通过往gitee上提交代码来进行git操作的演示。
浏览器搜索gitee,进入之后创建自己的账号,然后进行下列操作。
这时候在远端已经创建好了远端仓库。
将复制的内容粘贴到命名行进行运行。
此时在本地就有了对应的文件夹。
进入到test_for_git目录,并输入ls -al命令。
在test_for_git中新建一个文件test.c,此时并没有将test.c添加到本地仓库中。接下来执行git add test.c,将test.c添加到一个.git中的一个暂存区(可以使我们在本地多次add操作,然后一次将所有add的文件提交到本地仓库中)。
第一次操作可能需要进行一下配置,直接复制下列两行命令,粘贴运行即可。
命令行输入 git commit -m "xxx"将暂存区的文件提交到本地仓库。后面的字符串代表这次提交的日志信息,里面的信息不能乱写,不然后续不方便进行日志的查阅。git log可以查看提交的日志信息。
命令行输入git push,然后根据提示输入gitee的账号和密码,将本地仓库推送到远端仓库。
知识点1:
.gitignore文件是帮我们进行文件后缀过滤的,只要在这里面添加对应的文件后缀,则该拥有该后缀的文件无法添加。
知识点2:
如果远端的仓库在多台机器上有本地仓库,每次在本地仓库进行git pull命令,这条命令使你当前的本地仓库和远端仓库进行同步。
7.调试器 -- gdb/cgdb的使用
程序的发布方式有两种,debug模式和release模式,Linux gcc/g++出来的二进制程序,默认使release模式。要使用gdb调试,必须在源代码生成二进制程序的时候,加上 -g 选项,生成debug版本的二进制程序。
下列是后续进行实验的样式代码:
#include <stdio.h>
int Sum(int s, int e)
{
int result = 0;
for (int i = s; i <= e; i++)
{
result += i;
}
return result;
}
int main()
{
int start = 1;
int end = 100;
printf("I will begin\n");
int n = Sum(start, end);
printf("running done, result is: [%d-%d]=%d\n", start, end, n);
return 0;
}
下列第一个是没有加 -g 选项编译后二进制文件的大小,第二个是加 -g 选项编译后的二进制文件的大小。
这里输入下列命令,可以看到debug版本下多了关于调试信息的字段。
7.1gdb的进入和退出
1.进入gdb命令:gdb 二进制文件
2.退出gdb命令:ctrl + d 或者 quit 或者 q。
7.2gdb常用的命令介绍
由于gdb调试时都是黑屏,看不到代码,所有这里推荐用cgdb进行调试,可以在调试时,上方屏幕显示代码且命令的操作和gdb一样。安装命令:
• Ubuntu: sudo apt-get install -y cgdb
• Centos: sudo yum install -y cgdb
如果使用cgdb调试就会使如下页面:
命令 | 作用 | 样式 |
---|---|---|
list/l | 显示源代码,从上次位置开始,每次列出10行 | list/l 1 1list/l 10 |
list/l 函数名 | 列出指定函数的源代码 | list/l main |
list/l ⽂件名:⾏号 | 列出指定⽂件的源代码 | list/l mycmd.c:1 |
r/run | 运行并调试程序(等同于vs2022中的F5) | run |
n/next | 单步执⾏,不进⼊函数内部(等同于vs2022中的F10) | next |
s/step | 单步执⾏,进⼊函数内部(等同于vs2022中的F11) | step |
c/continue | 当程序在某一断点处停止后,用该指令可以继续执行,执行到下一个断点处或者执行到程序结束。 | c/continue |
break/b [⽂件名:]⾏号 | 在指定⾏号设置断点 | break 10 break test.c:10 |
break/b 函数名 | 在函数开头设置断点 | break main |
info/i break/b/breakpoints | 查看当前所有断点的信息 | info break |
finish | 执⾏到当前函数返回,然后停⽌ | finish |
print/p 表达式/变量名 | 打印表达式/变量名的值 | print 2 + i print i |
set var 变量 = 值 | 调试运行时临时修改变量的值 | set var i=10 |
delete/d breakpoints | 删除序号为n的断点 | delete breakpoints 1 |
disable breakpoints disable | 禁⽤所有断点 | disable breakpoints disable |
enable breakpoints enable | 启⽤所有断点 | enable breakpoints enable |
disable 断点编号 | 禁⽤对应断点编号的断点 | disable 1 |
enable 断点编号 | 启⽤对应断点编号的断点 | enable 1 |
display 变量名 | 跟踪显⽰指定变量的值(每次停⽌时) | display x |
undisplay 编号 | 取消对指定编号的变量的跟踪显⽰ | undisplay 1 |
until ⾏号 | 执⾏到指定⾏号 | until 20 |
backtrace/bt | 查看当前执⾏栈的各级函数调⽤及参数 | backtrace |
info/i locals | 查看当前栈帧的局部变量值 | info locals |
display 变量 | 将变量加入监视窗口,每次调试运行代码都会打印变量的值 | display i display &i |
undisplay 被监视变量的编号 | 将该编号对应的变量移出监视窗口 | display 1 |
info/i locals | 查看当前栈帧所有变量的值 | info/i locals |
知识点1:
在gdb调试的过程中,会记录上一次输入的命令,回车直接会运行上一次运行的命令。知识点2:
断点的本质是把代码进行块级别划分,以块为单位进行快速定位,寻找bug所在位置。一般就是整个程序打多个断点进行分块,搭配c/continue,until以及finish命令进行以块为单位的运行。
7.3watch
执行时监视一个表达式(如变量)的值。如果监视的表达式的值在程序运行期间发生变化,gdb会暂停程序的执行,并打印出该值之前和之后的值。删除操作和删除断点一样。
知识点1:
如果有一些变量运行时不应该修改,但是怀疑是因为它的修改导致了问题,可以watch它,如果变化了就发出通知。
7.4条件断点
条件断点的意义就是,运行到该行且满足对应的条件时,调试的程序才停下来,一般用于循环中。
这里就上诉Sum()函数中,对第8行设置条件断点,当 i == 9时触发该条件断点,设置的命令:b 8 if i == 8(b 行号 if 条件)。删除时和删除普通断点的方式一样。
知识点1:
对已经存在的普通断点加条件变为条件断点或者给条件断点更改触发条件的命令:condition 断点编号 条件