UNIX系统和Linux系统介绍,GNU编译工具的使用,静态库和动态库的制作和使用以及优缺点

本文概述了Linux系统、UNIX系统比较,介绍了GNU编译工具gcc的基本用法,重点讲解了环境变量的作用、设置方法,以及静态库与动态库的区别及应用场景。涵盖了Linux内核、POSIX标准、编译参数、预处理指令,以及错误处理和库文件管理的关键知识点。
摘要由CSDN通过智能技术生成

内容介绍:

  1. 系统环境:库、环境变量、编译器、系统特性
  2. 内存管理:操作系统是如何管理内存的
  3. 文件系统:文件读写、目录读写、文件属性、文件管理
  4. 进程管理:多个程序同时运行,解决复杂问题
  5. 信号处理:操作系统与程序之间一种通信机制
  6. 进程通信:多个进程如何交互数据,这是它们协同工作的基础
  7. 线程管理:就是让一个程序同时做若干件事情
  8. 线程同步:让多个线程同时工作时能够不互相破坏、干扰

​ 我们表面上的学习任务是学习如何使用操作系统的API,但核心任务就是学习操作系统的管理机制,了解操作系统的管理规则让程序能够更好的在系统运行,通过大量阅读API的英文使用手册,提高自己的自学能力、英文技术文档的阅读能力。

一、UNIX系统介绍

​ 诞生于1971年美国AT&T公司的贝尔实验室,主要开发者是丹尼斯.里奇、肯.汤普逊。

​ 该系统的主要特点是支持多用户、多任务,并支持多种处理器架构,同时具有高安全性、高可靠性、高稳定性,既可以构建大型关键业务系统的商业服务器,也可以构建面向移动终端、手持设备、可穿戴设备的嵌入式应用。

二、Linux系统介绍

​ 是一款类UNIX系统,免费开源,不同的发行版使用相同的内核,一般用在手机、平板、路由器、台式计算机、大型计算机、超级计算机,从严格意义上来说,Linux仅指的是操作系统内核,隶属于GNU工程,发明人叫 Linus Benedict Torvalds。

Linux系统的Logo:

​ 是一只企鹅,是南极的标志性动物,而目前南极不属于任何国家,为全人类所共有的,而Linux用它来当操作系统就意味这款系统属于全人类。

Minix操作系统:

​ 荷兰的Andrew S. Tanenbaum教授所开发的一款不包含任何UNIX源码的类UNIX系统,Linus Torvalds深受Minix的启发写出了第一版本的Linux内核。

GNU工程:

​ 发起于1984年,由自由软件基金会提供支持,它基本原则就是共享,目的是发展出一个免费且开源的类UNIX系统,名称来自GNU’s Not UNIX!的递归缩写,因为GNU的设计类似UNIX,但它不包含具著作权的Unix代码。GNU的创始人,理查德·马修·斯托曼,将GNU视为“达成社会目的技术方法”。

​ GNU的发展仍未完成,其中最大的问题是具有完备功能的内核尚未被开发成功。GNU的内核,称为Hurd,但是其发展尚未成熟。在实际使用上,多半使用Linux内核、FreeBSD等替代方案,作为系统核心,其中主要的操作系统是Linux的发行版。Linux操作系统包涵了Linux内核与其他自由软件项目中的GNU组件和软件,可以被称为GNU/Linux。

POSIX标准:

​ 可移植操作系统接口(Portable Operating System Interface,缩写为POSIX)是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称。

​ Linux完全遵循了这个标准,所以两个操作系统的API,名字相同、参数相同、返回值相同,在Linux下编写的代码,经过稍微修改可移植到UNIX上。

GPL通用公共许可证:

​ GNU通用公共许可证简称为GPL,是由自由软件基金会发行的用于计算机软件的协议证书,使用该证书的软件被称为自由软件。允许对某成果及其派生成果的重用、修改和复制,对所有人都是自由的,但不能声明做了原始工作,或声明由他人所做。

三、GNU编译工具

​ 是GNU组织为了编译Linux内核源码而开发的一款编译工具,经过长时间的发展目前已经成为一个编译平台,能够支持多种编程语言,能在主流的操作系下使用,编译C代码的工具叫gcc,编译C++代码的工具叫g++。

gcc常用的编译参数:
gcc [选项参数] 文件
	-E 			# 预处理
    -S 			# 生成汇编文件
    -c 			# 生成目标文件
    -o 			# 设置编译结果的名字
    -I 			# 设置要导入的头文件的路径
    -l 			# 设置要链接的库名,例如:使用sqrt、pow等数学函数时就需要链接数学库 -lm
    -L 			# 设置要链接的库的路径
    -D 			# 在编译时定义宏
    -g 			# 编译时添加调试信息,这样编译出的程序可以用gdb调试。
    -Wall 		# 显示所有警告,编译器会以更严格的标准检查代码 
    -Werror 	# 把警告当错误处理
    -std 		# 指定编译器要遵循的语法标准,c89,c99,c11,当前系统默认的是c99标准。
    -pedantic 	# 对不符合ANSI/ISO C语言标准的,扩展语法产生警告
gcc相关的文件类型:
	xxx.h 		# 头文件
    xxx.c 		# 源文件
    xxx.i 		# 预处理文件
    xxx.s 		# 汇编文件
    xxx.o 		# 目标文件
    xxx.h.gch 	# 头文件的编译结果,用于检查自定义的头文件是否有语法错误,建议立即删除
    libxxx.a 	# 静态库文件,Windows系统下的静态库文件以lib结尾,例:xxx.lib
    libxxx.so 	# 动态库文件,Windows系统下的动态库文件以以dll结尾,例:libxxx.dll
gcc把C语言变成可执行程序的过程:
# 1、把程序员所编写的代码进行预处理
gcc -E hello.c 				# 把预处理的结果显示到屏幕上
gcc -E hello.c -o hello.i 	# 会生成以.i结尾的预处理文件

# 2、把预处理的结果翻译成汇编代码
gcc -S hello.i 				# 会生成以.s结尾的汇编文件

# 3、把汇编代码翻译成二进制指令
gcc -c hello.s 				# 会生成以.o结尾的目标文件

# 4、把若干个文件目标文件、库文件合并成可执行文件
gcc a.o b.o c.o ... 		# 默认会生成a.out可执行文件,也。
gcc a.o b.o c.o -o hello 	# 可以使用-o指定可执行文件的名字
gcc支持的预处理指令:
#include      // 将指定文件的内容插至此指令处
#include_next // 与#include一样,但从当前目录之后的目录查找,极少用
#define       // 定义宏
#undef        // 删除宏
#if           // 判定
#ifdef        // 判定宏是否已定义
#ifndef       // 判定宏是否未定义
#else         // 与#if、#ifdef、#ifndef结合使用
#elif         // else if多选分支
#endif        // 结束判定
##            // 连接宏内两个连续的字符串
#             // 将宏参数扩展成字符串字面值
#error        // 预处理时产生错误,结束预处理
#warning      // 预处理时产生警告信息
#pragma       // 提供额外信息的标准方法,可用于指定平台
#pragma GCC dependency <文件>     // 若<文件>比此文件新则产生警告
#pragma GCC poison <标识>         // 若出现<标识>则产生错误
#pragma pack(1/2/4/8)             // 按1/2/4/8字节对齐补齐
#line                             // 指定行号
gcc预定义的宏:
void printf_macro (void) 
{
	printf ("__BASE_FILE__     : %s\n", __BASE_FILE__);//得到函数在哪个文件
	printf ("__FILE__          : %s\n", __FILE__);//得到现在在哪个文件
	printf ("__LINE__          : %d\n", __LINE__);//获取行号
	printf ("__FUNCTION__      : %s\n", __FUNCTION__);
	printf ("__func__          : %s\n", __func__);
	printf ("__DATE__          : %s\n", __DATE__);//获取日期
	printf ("__TIME__          : %s\n", __TIME__);//获取时间
	printf ("__INCLUDE_LEVEL__ : %d\n", __INCLUDE_LEVEL__);
	printf ("__cplusplus       : %d\n", __cplusplus);//gcc编译为1,g++编译为0
}

四、环境变量

什么是环境变量:

​ 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置、系统文件夹位置等。

​ 环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。例如操作系统中的path环境变量,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path中指定的路径去找。用户通过设置环境变量,来更好的运行进程。

环境变量的主要作用:
设置程序的运行参数:

​ 环境变量相当于给系统或用户给应用程序设置的一些参数,具体起什么作用这当然和具体的环境变量相关,例如一个程序在运行时默认该使用什么语言,界面的大小,日志文件存储在什么位置,临时文件存储在什么位置,都需要通过查询操作系统的环境变量才能确定。

程序共用:

​ 当软件A需要使用到软件B中部分功能时,但无论是操作系统还是软件A、B的作者都无论控制软件安装在什么位置,这时就可以让软件B设置环境变量告诉操作系统它安装在了哪个位置,而软件A就可以通过查询环境变量知道软件B安装在什么地方,从而调用软件B的功能。

系统运行:

​ 用户还可以通过设置环境变量告诉操作系统一些运行参数,如设置当前系统的语言、字符编码、终端的默认大小等。

常见的环境变量:
PS1				# 命令提示符
PATH			# 命令的搜索路径
INCLUDE_PATH	# 头文件的搜索路径
LIBRARY_PATH	# 库文件的搜索路径
LD_LIBRARY_PATH	# 程序执行时动态库的搜索路径
设置环境变量:
查看环境变量:

​ 1、Linux系统使用env命令查看环境变量

​ 2、Windows系统使用set命令查看环境变量

​ 3、在程序中查看

#include <stdio.h>
// 程序在运行时它的父进程都会通过main函数参数传递一份环境变量表的拷贝,是一个以NULL指针结尾的字符指针数组。
int main(int argc,const char* argv[],char* environ[])
{
    for(int i=0; environ[i]; i++)
    {   
        printf("%s\n",environ[i]);
    }   
}

练习1:解析出PATH环境变量的所有路径

通过修改配置文件设置:
1、Linux系统的修改方法
# 打开配置文件:
vi ~/.bashrc			# 只对当前用户用效
vi /etc/environment		# 对所有用户有效
/home/hidin/tools


# 在文件末尾添加内容:
export C_INCLUDE_PATH=<环境变量的内容>
C_INCLUDE_PATH=$C_INCLUDE_PATH:<追加新的内容>
	
# 重新加载配置文件:
source ~/.bashrc
2、Windows系统的修改方法

Windows10系统修改环境变量的方法

Windows11系统修改环境变量的方法

使用标准库函数设置环境变量:
环境变量和格式:name=value

// 根据name获得value
char *getenv(const char *name);

// 以name=value的形式设置环境变量,name不存在就添加,存在就覆盖其value
int putenv(char *string);

// 根据name设置value,注意最后一个参数表示,若name已不存在则添加,如果name已经存在是否覆盖其value由overwrite控制
// If name does exist in the  environment, then its value is changed to value if overwrite is nonzero; if overwrite is zero, then the value of name is not changed。
int setenv(const char *name, const char *value, int overwrite);

// 删除环境变量。
int unsetenv(const char *name);

// 清空环境变量,environ==NULL。
int clearenv(void);
注意:由于当前程序获得的是环境变量表的拷贝(副本),因此在当前程序中对环境变量进行增删改查不会影响其他程序

五、错误处理

通过函数的返回值表示错误:
// 返回合法值表示成功,返回非法值表示失败
long file_size (const char* path) 
{
	FILE* fp = fopen (path, "r");
	if (NULL == fp) 
        return -1;

	fseek (fp, 0, SEEK_END);
	long size = ftell (fp);

	fclose (fp);
	return size;
}

// 返回有效指针表示成功, 返回空指针(NULL/0xFFFFFFFF)表示失败
Node* query_list(Node* list,TYPE key)
{
    for(Node* n=list->head; NULL!=n; n=n->next)
    {
        if(n->data == key)
            return n;
    }
    return NULL;
}

// 返回0表示成功,非零表示失败,如:main、fseek、access等函数
// 永远成功的函数,如:menu类的函数
通过errno表示错误:

​ errno 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。

​ 当调用Linux系统API函数发生异常时,一般会将errno变量赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因,在实际编程中用这一招解决了不少原本看来莫名其妙的问题。

​ errno在函数执行成功的情况下不会被修改,因此不能以errno非零,作为发生错误判断依据, errno是一个全局变量,其值随时可能发生变化,所以要配合函数的返回值来使用errno。

#include <stdio.h>
#include <errno.h>

int main () 
{
	FILE* fp = fopen ("none", "r");
	if (NULL == fp) 
	{
        // 查看错误编号
		printf ("fopen: %d\n", errno);
        // 根据错误编号查看错误提示
		printf ("fopen: %s\n", strerror (errno));
        // 自动根据错误编号查看错误提示
		printf ("fopen: %m\n");
        // 自动根据错误编号查看错误提示
		perror ("fopen");
		return -1;
	}
	fclose (fp);
	return 0;
}

六、库文件的制作与使用

什么是库文件:

​ 库文件是计算机上的一类文件,提供给使用者一些开箱即用的变量、函数或类,它是若干个目标文件的集合,也可以对源码进行保密。库文件分为静态库和动态库,静态库和动态库的区别体现在程序的链接阶段。

静态库与动态库的区别:

​ 静态库在程序的链接阶段被复制到了程序中,动态库在链接阶段没有被复制到程序中,而是在程序运行时由系统动态加载到内存中供程序调用,这是它们最本质的区别。

静态库的制作与使用:
创建静态库:
# 编译出目标文件:
gcc -c xxx1.c
gcc -c xxx2.c
...

# 把目标文件打包成静态库文件
ar -r libxxx.a x1.o x2.o ...

# ar 是一个专门控制静态库的命令
	-r # 把目录文件合并成一个静态库,如果静态库文件已经存在则更新。
	-q # 向静态库中添加目标文件
	-t # 查看静态库中有哪些目标文件
	-d # 从静态库中删除目标文件
	-x # 把静态库展开为目标文件 
使用静态库:
# 方法1、直接调用,把共享库当作目标文件一样,与调用者的目标文件一起合并出可执行文件。
gcc main.c libxxx.a

# 方法2、通过设置LIBRARY_PATH环境变量来指定库的路径,通过-l参数来指定库名
gcc main.c -lxxx

# 方法3、通过gcc -L参数来指定库的路径,通过-l参数来指定库名
gcc main.c -L<PATH> -lxxx
动态库的制件与使用:
创建动态库:
# 编译出目标文件,-fpic编译出位置无关代码,在代码中使用相对地址,这样共享库就可以加载到内存的任何位置。
gcc -c -fpic xxx1.c
gcc -c -fpic xxx2.c
...

# 把目标文件打包成共享库:
gcc -shared xxx1.o xxx2.o ... -o libxxx.so
使用动态库:
# 方法1、直接调用
gcc main.c libxxx.so

# 方法2、通过设置LIBRARY_PATH环境变量来指定库的路径,需要通过-l来指定库名
gcc main.c -lxxx 
    

# 3、通过gcc -L参数来指定库的路径
gcc main.c -L<PATH> -lxxx

# 注意1:如果执行无法运行,需要检查操作系统是否能加载动态库,检查LD_LIBRARY_PATH环境变量
# 注意2:如果静态库和共享库同时存在,优先使用共享库,通过-static可以指定使用静态库。
动态加载共享库:
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
功能:打开共享库
filename:共享库的路径
flag:打开方式
	RTLD_LAZY:延迟加载,使用到共享库时再加载
	RTLD_NOW:立即加载
返回值:成功返回共享库的句柄,失败返回NULL

void *dlsym(void *handle, const char *symbol);
功能:通过函数名在共享库中获取函数指针
handle:共享库的句柄
symbol:函数名
返回值:函数地址,失败返回NULL

char *dlerror(void);
功能:获取错误信息

int dlclose(void *handle);
功能:卸载共享库
注意:编译时添加-ldl参数
使用库文件时的辅助工具:
# 查看目标文件、可执行文件、静态库、共享库文件的符号列表。
nm <flie>

# 删除目标文件、可执行文件、静态库、共享库文件中符号列表、调试信息,可以有效降低文件的大小。
strip <file>

# 查看可执行文件依赖了哪些共享库
ldd <file>
   
# 把二进制文件的内容转换成汇编代码,就是传说中的反汇编,但如果二进行文件进行加密机制则无法成功转换
objdump <file>
静态库的优点:
使用方便:

​ 在链接目标文件生成程序时,只需要把静态库与目标文件一起编译,编译器就会把目标文件中使用到的静态的内容拷贝到程序中,程序在运行时就不需要静态库了。

运行速度快:

​ 静态库文件内容拷贝到程序中,因此在程序运行过程中,使用到静态库中的函数、变量时,只会在程序的内部跳转,因此使用静态库的程序要比使用动态库的程序运行速度快。

静态库的缺点:
更新麻烦:

​ 如果静态库中的内容发生了改变,如:版本升级,修改BUG,那么使用了相关静态库的程序就需要重新编译,如果是应用程序,用户就需要重新下载。

浪费内存:

​ 假如有一个叫libxxx.a的静态库,a.out、b.out、c.out程序都使用使用它,那libxxx.a的内容会被分别拷贝一份到a.out、b.out、c.out,当这三个程序运行时,libxxx.a静态库文件中的内容就有三份存在于内存中,这样就有了冗余,造成内存的浪费。

动态库的缺点:

​ 在链接目标文件时,虽然动态库需要与使用它的目标文件一起编译,但编译器只是记录被调用的内容(函数、变量)在动态库中的位置,生成的程序中只有跳转到动态库的相关指令,当程序运行时需要把动态库一起加载到内存,当执行到动态库的相关内容时,才跳转到动态库所在的内存中执行,完成后再返回,这样会导致两个问题。

运行速度慢:

​ 使用动态库程序需要在程序和动态库中来回跳转,因此要比使用静态库的程序运行的速度慢。

程序无法执行:

​ 如果程序使用了动态库,当它运行时就需要系统把它使用的动态库一起加载到内存,如果系统找不到相应的动态库,那么程序就无法运行(Windows系统经常提示的xxx.dll文件缺失,程序无法运行),产生这种错误的原因有很多,如:环境变量配置错误,动态库文件存储位置错误,可执行文件拷贝到其它计算机上运行时没有一起拷贝动态库文件。

总结:

​ 当一个模块不会再发生改变,并且执行速度有一些要求,适合把它封装成静态库。

动态库的优点:
节约内存:

​ 使用的动态库只需被系统加载一次,不同的程序都可以使用到内存中的动态库,因此节约了很多内存,由于多个程序可共享使用一个动态库,所以动态库也叫共享库。

更新方便:

​ 如果动态库中的函数格式没有变化(返回值、函数名、参数列表),而只是函数中的业务逻辑代码发生变化,那么只需要重新编译动态库即可,不需要重新编译相关的可执行文件,这也是某些应用程序可以自动更新的原因。

总结:
程序无法执行:

​ 如果程序使用了动态库,当它运行时就需要系统把它使用的动态库一起加载到内存,如果系统找不到相应的动态库,那么程序就无法运行(Windows系统经常提示的xxx.dll文件缺失,程序无法运行),产生这种错误的原因有很多,如:环境变量配置错误,动态库文件存储位置错误,可执行文件拷贝到其它计算机上运行时没有一起拷贝动态库文件。

总结:

​ 当一个模块不会再发生改变,并且执行速度有一些要求,适合把它封装成静态库。

动态库的优点:
节约内存:

​ 使用的动态库只需被系统加载一次,不同的程序都可以使用到内存中的动态库,因此节约了很多内存,由于多个程序可共享使用一个动态库,所以动态库也叫共享库。

更新方便:

​ 如果动态库中的函数格式没有变化(返回值、函数名、参数列表),而只是函数中的业务逻辑代码发生变化,那么只需要重新编译动态库即可,不需要重新编译相关的可执行文件,这也是某些应用程序可以自动更新的原因。

总结:

​ 随着计算机性能的不断提升,弥补了动态库运行速度慢的缺点,再加上它能节约内存、更新方便,最主要的是计算机硬件一直在升级,所以就导致大多数代码需要不断的升级,因此我们大多数情况下把模块封装成动态库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值