18. Linux API 编程预备知识

时间:2024-04-14 07:09:39


【操作系统基础程序】

操作系统提供了一组用户使用计算机所需的基础程序,实现了使用计算机必备的基础功能,比如:用户管理、外存储器管理、文件管理、设置网络连接、编译程序,这组程序所在目录会记录在环境变量中,直接在终端指定其名称即可使用。

多数基础程序执行时需要传入参数,参数赋值有固定规则,若参数用于启动或设置一种功能,则需要前缀-符号。

shutdown -h 0    //shutdown程序用于关机或重启,-h参数表示关机,0为设置的关机倒计时。
gcc -masm=intel  //gcc用于编译代码,-masm参数表示设置汇编语言语法类型,此参数需要使用=符号直接赋值具体类型。

另外多个参数功能不冲突时可以同时指定,此时只需编写一个-符号即可。

tar -czvf ali.tar.gz a.c b.c    //tar用于文件压缩与打包,czvf分别设置一种功能,这里一共设置4种功能,ali.tar.gz为打包文件的名称,a.c和b.c为需要打包的文件。

有些参数不需要使用-符号。

cd ~/data    //cd设置终端工作目录,之后直接编写路径名称即可。


鉴于介绍linux操作系统常用命令的书籍非常多,这里不再重复。


【操作系统API】

API,中文名为应用程序编程接口,是操作系统提供的一组C语言函数库,存放在/usr/include/目录中,提供了程序频繁使用的功能,其中很多功能是通过glibc调用系统调用实现的。

在计算机操作系统发展早期,各种操作系统的API使用方式都有区别,频繁学习不同的API非常耗费精力,为此人们制定了统一的API使用规则,称为POSIX(可移植操作系统接口),它规定了API需要提供哪些功能、每种功能对应函数和全局变量的使用方式,开源操作系统的API基本上都遵循此规则。

掩码

有些API函数需要使用一个数据指定启用哪些功能、不启用哪些功能,此数据称为掩码,掩码使用二进制位记录每个功能是否生效,一般值为1表示生效、值为0不生效,有些功能使用多个二进制位的组合指定是否生效,多个掩码指定的生效功能可以使用按位或运算进行整合。

API函数执行结果

API函数使用返回值告知调用者执行结果,多数情况下执行成功返回0或大于0的整数,执行失败返回-1,若返回值为指针,则返回0表示执行出错。

若执行出错后需判断具体原因,可以使用errno.h文件,此文件内有一个全局变量errno,其存储了API函数执行结果的具体原因,每一种执行结果使用一个数值表示,每个数值表示的信息我们不方便直接观察,可以使用perror函数输出执行结果原因,perror函数会自动读取errno的值并判断执行结果类型。

注:
1.perror的执行依赖stdio.h。
2.errno会随下次执行API函数而被修改。

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
	int result = open("/home/ali/a.txt", O_RDWR | O_APPEND);    //打开文件
	
	if(result > -1)
	{
		printf("打开文件成功,文件描述符编号:%d\n", result);
	}
	else
	{
		perror("open函数执行失败,原因");    //参数为首先输出的字符串,之后输出errno含义
	}
	
	return 0;
}


【GCC编译器】

GCC是GNU项目中编译器子项目的名称,主要用于编译C语言和C++语言代码,编译C语言代码时使用gcc程序,编译C++语言代码时使用g++程序。

将源代码制作为一个ELF文件需要经过三个步骤:编译、汇编、连接,三个步骤分别使用以下三种工具完成:
1.编译器,将高级语言代码转换为对应的汇编语言代码。
2.汇编器,将汇编语言代码转换为对应的指令数据、数学数据。
3.连接器,将编译后的文件添加必要的节组成ELF文件。

gcc是编译器,只有编译功能,没有汇编与连接功能,但是因为编译与汇编、连接通常需要一起使用,所以在使用gcc编译代码时默认会自动调用汇编器、连接器制作为ELF文件。

gcc /home/ali/a.c    //参数指定源代码文件,可以使用相对路径、绝对路径
gcc a.c b.c          //若程序代码在多个文件内,需要同时指定所有的文件路径

预处理

编译器的预处理功能用于对源代码中预处理指令进行解释,主要有以下两种行为:
1.将 #include 连接的文件内容复制到本文件中。
2.将使用 #define 定义的宏代码转换为具体的代码。

GCC操作文件的类型

gcc通过文件后缀名确定文件类型,常用后缀如下:

c,c语言代码文件
cc,c++语言代码文件
cpp,c++语言代码文件
s,汇编语言代码文件
o,目标文件
a,静态连接库文件
so,动态连接库文件

常用参数

-o,小写字母o,指定编译后的文件名称,示例:gcc a.c -o ali.out,a.c为源代码文件路径,ali.out为编译后的文件路径,这里全部使用相对路径。
-O,大写字符O,设置编译优化级别,有4种级别,使用编号0-3表示,0表示不优化,3表示最高优化,默认为0级优化,级别值紧邻参数之后,示例:gcc -O2 a.c。
------------------
-E,只进行预处理,不编译代码。
-S,只进行预处理、编译,不进行汇编、连接。
-c,只进行预处理、编译、汇编,不进行连接,用于制作目标文件。
-fPIC,与-c组合使用,用于制作动态连接库使用的目标文件。
------------------
-shared,将目标文件打包并制作为动态连接库。
-static,调用连接库时使用静态库版本,而非动态库,编译后的程序体积会大很多。
-share,调用连接库时尽量使用动态库版本,生成的文件体积更小。
------------------
-g,添加调试功能相关数据,方便使用调试器调试程序,若不添加此参数则使用调试器调试程序时某些功能不可用,同时注意调试程序时不应该使用-O参数开启优化。
-s,编译后的程序不保存源代码中的数据名。
------------------
-masm,指定汇编代码的语法类型,比如在高级语言中内嵌汇编代码、或使用-S参数只编译代码时,都可以使用此参数,默认使用AT&T语法,若使用英特尔语法则应该添加参数 -masm=intel。
-std,设置C语言、C++语言的版本,比如:c89、c99、c11、c17、c++98、c++03、c++11、c++14、c++17,具体的版本值使用=符号赋值,示例:-std=c99,-std=c++03。
------------------
-Wall,生成所有警告和错误信息(数组越界访问除外),若不指定此参数则只显示必要的警告信息,比如设置了返回值的函数没有为返回值赋值,此时不使用此参数不会有警告。
-fpermissive,将不符合语法规则的某些代码改为可以编译,但是会生成警告信息,比如C++代码中变量指针存储一个常量数据的地址,默认禁止编译,添加此参数即可编译,注意并非针对所有错误代码,有些错误不可忽略。
-fsanitize=address,检查源代码中各种内存错误访问相关信息,原理是在代码中添加各种检查代码,若出错则会在终端输出错误信息,并自动终止程序,一般用于调试程序。
------------------
-v,查看gcc相关属性信息,包括gcc版本号。


【gdb调试器】

编写代码时经常会因为各种原因遇到意想不到的错误,比如:思维不严谨、对某个API函数运行方式不完全了解、对某个功能的运行原理不完全了解,代码错误是不可避免的,使用调试器可以观察程序的各种执行错误,调试器主要功能如下:
1.设置程序断点,程序执行到此处时会暂停。
2.程序单步执行,程序每执行一条指令就暂停。
3.查询程序执行时某些变量的值。
4.程序执行时临时修改某些变量的值。

使用方式

首先在终端执行gdb程序,参数设置为要调试程序的路径,动gdb后输入控制命令启动不同的功能,常用控制命令如下:

set args,设置被调试程序的main函数参数,示例:set args ali xyy,这里传入了2个参数,分别是ali和xyy。
list,显示程序源代码,示例:list 1,从第一行开始显示源代码,默认每次显示10行,之后按enter键显示下10行,示例:list main,显示main函数的源代码。
------------------
break/b,添加一个断点,可以使用代码行数或函数名指定断点添加处,程序执行到断点处会暂停,示例:break 9,在第9行添加一个断点。
info break/b,查询所有断点。
delete/d,取消一个断点,通过断点编号指定,示例:delete 1,取消编号为1的断点,示例:delete 1-5,取消编号1到5的断点。
disable,禁用指定编号的断点,示例:disable 1。
enable,启用指定编号的断点,示例:enable 1。
------------------
run/r,执行程序开始调试。
continue/c,在断点处恢复执行。
next/n,程序暂停后,单步执行下一条指令,若遇到跳转函数指令则忽略,执行之后的指令。
step/s,程序暂停后,单步执行下一条指令,若遇到跳转函数指令则进入函数,若跳转的函数是动态库函数则会产生错误。
finish/fin,程序暂停后,执行到下一个断点处,若本函数没有断点则执行到本函数末尾,并返回到上一级函数暂停。
until,跳转到源代码指定行数执行。
------------------
display,设置一个跟踪变量,每次暂停后自动查询此变量的值并输出,示例:display a,自动跟踪变量a。
undisplay,取消一个跟踪变量,需要使用跟踪变量编号指定,而不是变量名,示例:undisplay 1,取消编号为1的跟踪变量。
print/p,手动查询指定变量的值,示例:print a,查询变量a的值。
info locals,查询所有局部变量的值。
info reg,查询所有寄存器的值。
set var,修改指定变量的值,示例:set var a=0。
------------------
backtrace/bt,查询本函数调用者的执行顺序,也就是向前查询都有哪些函数执行。
call,跳转到指定函数执行一次并返回,同时输出此函数的返回值,示例:call ali(),跳转到ali函数执行,若有参数则需要同时为参数赋值。
disass,反汇编指定函数,示例:disass ali,反汇编ali函数。
------------------
quit/q,退出gdb。

注:有些参数可以使用简写,比如break可以简写为b。


【make】

大型项目会有几十甚至上万个源代码文件,如果每个源代码文件都需要手动编译的话会非常麻烦,这时候就需要使用make自动执行一些命令,make执行的操作使用make脚本文件说明,脚本文件默认名为makefile或Makefile。

make常用参数如下:

-f,指定make脚本文件的路径,若不指定则在工作目录中查找makefile文件。
-n,输出需要执行的操作,但是不真正的执行这些操作,用于检查makefile文件。
-j,指定使用CPU的几个核心进行工作,示例:make -j 4,使用4个核心。
-k,出现错误后继续执行,而非停止。


makefile脚本文件代码的基础编写规则如下。

注释

使用#符号设置一行注释。

# 这里是单行注释

创建行为

action1 : a.c b.c c.c
    gcc -c a.c -o a.o
    gcc -c b.c -o b.o
    gcc -c c.c -o c.o

第一行用于设置行为名称以及此行为需要使用的文件和其它行为,action1为行为名,:符号之后编写此行为需要使用的文件(可省略)以及调用的其它行为(不可省略),这里指定了3个源代码文件。
行为名之后的行编写此行为执行的命令,每行命令开头需要使用tab键输入一个制表符。

使用 @ 符号

默认情况下make会在终端输出执行了哪些命令,在命令前添加@符号则不输出。

action1 : a.c b.c c.c
    @ gcc -c a.c -o a.o
    @ gcc -c b.c -o b.o
    @ gcc -c c.c -o c.o

使用 - 符号

默认情况下遇到无法执行的命令make会终止执行,剩余命令都不会执行,在命令前添加-符号表示若此命令执行出错则忽略,继续执行之后的命令。

action1 : a.c b.c c.c
    @- gcc -c a.c -o a.o
    @ gcc -c b.c -o b.o
    @ gcc -c c.c -o c.o

使用多个行为

需要执行的命令很多时,往往需要将命令分到不同的行为中,make默认从第一个行为开始执行,并且只执行第一个行为,其他行为需要被调用才能执行,并且被调用的行为会首先执行,就像C语言中的其它函数必须直接或间接的被main函数调用才能执行一样。

# 制作可执行程序
action1 : action2 a.o b.o c.o
    gcc a.o b.o c.o -o ali.out

# 编译目标文件
action2 : a.c b.c c.c
    gcc -c a.c -o a.o
    gcc -c b.c -o b.o
    gcc -c c.c -o c.o

# 清理工作
clean : 
    rm -f a.o b.o c.o    # rm命令用于删除文件

make从action1行为开始执行,此行为又调用了action2行为,action2会首先执行,之后再执行action1,最后的clean行为不会自动执行,只能在终端执行make时手动调用此行为执行,示例:make clean

使用字符串变量

定义行为时可以使用字符串指定行为属性,行为名称、行为使用文件、行为执行命令都可以使用字符串指定。

# 定义字符串
s1 = a.c b.c c.c
s2 = gcc -c a.c -o a.o
s3 = gcc -c b.c -o b.o
s4 = gcc -c c.c -o c.o
s5 = a.o b.o c.o -o ali.out

# 定义行为
action1 : ${s1}    # 使用 ${字符串名} 或 $(字符串名) 调用字符串
    ${s2}
    ${s3}
    ${s4}
    gcc ${s5}

字符串赋值运算符

=运算符,直接赋值,多次使用=赋值时以最后一次赋值为准。
:=运算符,引用另一个字符串现在的值进行赋值。
?=运算符,有条件赋值,若变量没有赋值则进行赋值,若变量已经赋值则不进行赋值。
+=运算符,增加字符串中的字符,新增字符与原有字符使用空格分隔,常用于为执行命令添加参数。

# 定义字符串
${s1}         # 定义不赋值的字符串
${s2}
s1 += a.c
s1 += b.c
s1 += c.c
s2 ?= gcc ${s1}

# 定义行为
action1 : ${s1}
    ${s2}

嵌套调用字符串

s1 = gcc a.c b.c c.c -o ali.out
s2 = 1
s3 = ${s${s2}}   # 此行将被解释为 s3 = s1

action1 :
    ${s3}

条件语句

s1 = a.c 
${s2}

ifeq (${s1}, "a.c")    # 若s1等于a.c则执行ifeq语句内脚本代码,否则执行else语句内脚本代码,注意ifeq之后需要有空格
s2 = b.c
else
s2 = a.c
endif                  # 条件语句末尾

函数

make内置了一组函数,用于实现脚本代码常用功能,脚本代码可以调用这些函数执行,调用方式:$(函数名 参数),其中()也可写为{}。

使用 wildcard 函数查询文件名

$(wildcard 参数)

参数为要查询的文件名,可以使用通配符,查询到的多个符合条件文件的名称会组合在一起形成一个字符串,中间使用空格隔开,并返回此字符串。
 

s1 = $(wildcard *.o)      # 查询工作目录所有后缀.o的文件
s2 = $(wildcard ~/*.c)    # 查询用户主目录所有后缀.c的文件

action1 :
    @ echo ${s1}       # echo命令用于在终端输出指定字符
    @ echo ${s2}

使用 patsubst 函数修改字符串

$(patsubst 参数1, 参数2, 参数3)

参数1,指定需要修改的部分
参数2,指定修改后的值
参数3,提供需要修改的字符串

s1 = a.c b.c c.c
s2 = $(patsubst %c, %o, ${s1})    # 将s1中的c改为o,s2 = a.o b.o c.o

action1 :
    @ echo ${s2}

使用 foreach 函数循环处理字符串

此函数用于循环分割并修改一个字符串,首先根据字符串内空格将字符串分割为多份,之后对每份进行处理(比如添加字符),最后将处理后的字符串重新组合并返回。

$(foreach 参数1, 参数2, 参数3)

参数1,临时存储每次循环所用变量。
参数2,需要处理的字符串。
参数3,每次循环对字符串的操作。

s1 = a b c
${s2}
s3 = $(foreach s2, ${s1}, ${s2}.o)    # s3 = a.o b.o c.o

action1 :
    @ echo ${s3}

上述代码中,foreach会遍历字符串s1,每次循环以空格作为分割符取其中一段,之后将其临时存储到s2中,之后为此段内容添加.o,循环处理完s1的所有分段后,将处理后的分段组合为一个字符串并返回。

使用 dir 函数读取目录

s1 = $(dir)*.c       # 查询工作目录中所有后缀.c的文件
s2 = $(dir ~/)*.c    # 查询指定目录中所有后缀.c的文件,目录名必须以/符号结尾

action1 :
    @ echo ${s1}
    @ echo ${s2}

进入子目录执行 make

action1 :
    make -C data    # 进入名为data的子目录继续执行make,执行完毕后返回


同时还可以向子目录内的makefile脚本文件传递一个字符串变量。

export s1 = ali     # 使用export命令设置传递的变量
action1 :
    make -C data    # data目录中的makefile脚本文件可以直接使用s1