1 UC
- 1、环境变量
- 2、环境变量表
- 3、错误处理
- 4、库文件
- 4.1 静态库
- 4.2 动态库
- 4.3 动态库的动态加载
- 5、虚拟地址
1、环境变量
- 什么是环境变量?
- 每个进程都有一张自己的环境变量表,表中的每个条目都是形如“键=值”形式的环境变量。
- 进程可以通过环境变量访问计算机的资凉
- 在终端下输入env命令,可以查看环境变量列表
- 通过echo Sname可以查看某个环境变量的值
注意:env命令出来的都是shell中全局的环境变量
- 环境变量的添加
- 在终端窗口输入FOOD=zpyl,表示向当前进程中增加名为“FOOD”值为“zpyl"的环境变量,如果环境变量已经存在,则更改其值。
- 注意:在“=”左右不要加空格
- 环境变量的类别
- 全局环境变量:当前shell和其子进程都是可见的
- 局部环境变量:只有当前shell可见
- 将局部环境变量设置成全局变量: export name
- 删除环境变量: unset name
2、环境变量表
- 每个进程都有一张独立的环境变量表,其中的每个条目都是一个形如“键=值”形式的环境变量
- 所谓环境变量表就是一个以NU儿L指针结束的字符指针数组,其中的每个元素都是一个字符指针,指向一个以空字符结尾的字符串,该字符串就是形如”键=值”形式的环境变量。该指针数组的地址保存在全局变量environ中
#include <stdio.h>
int main(void){
// 当前进程的字符指针数组的首元素地址
extern char ** environ;
for(char** pp=environ;*pp;pp++){
printf("%s\n",*pp); // 输出的就是当前shell的环境变量信息
}
return 0;
}
- main函数的第三个参数
#include <stdio.h>
int main(int argc,char* argv[],char* envp[]){
// argc:命令行参数的个数
// argv:命令行参数的内容
// envp:环境变量表的其实地址
// 当前进程的字符指针数组的首元素地址
extern char ** environ;
printf("%p,%p\n",environ,envp);// 相等
for(char** pp=environ;*pp;pp++){
printf("%s\n",*pp);
}
return 0;
}
3、错误处理
针对因为运行环境、人为操作等原因导致的错误,程序的设计者需要提前有所考虑,向函数的调用者提供必要的信息,以明确发生了结误,以及具体是什么错误。
- 通过错误号了解具体的错误原因
- 系统于定义的整数类型全局变量errno中存储了最近一次系统调用的错误编号
- 头文件errno.h中包含了对errno全局变量的外部声明和各种错误号的宏定义
// 文件位置
/usr/include/errno.h
/usr/include/asm-generic/errno.h
/usr/include/asm-generic/errno-base.h
- 相关函数
- char* strerror(int errnum)
- 头文件:string.h
- 功能:将整数形式的错误号转换为有意义的字符串,返回值是参数对应的描述
- void perror(char const* tag)
- 头文件:stdio.h
- 功能:在标准输出错误设备上打印最近一次函数调用的错误信息
- tag参数是用户自定义的提示内容,在输出信息时,会将提示内容输出出来
- char* strerror(int errnum)
// 错误处理
#include<stdio.h>
#include<stdlib.h>//malloc() free()
#include<errno.h> // int errno
#include <string.h> // strerror
int main(){
int *p = malloc(0xfffffffffffffff);
if(p==NULL){
printf("malloc失败\n");
printf("errno = %d\n",errno); //cat /usr/include/asm-generic/errno-base.h
printf("malloc:%s\n",strerror(errno));
perror("在程序第7行:");
return -1;
}
free(p);
p=NULL;
return 0;
}
- 执行结果
4、库文件
- 单一模型:
- 将程序中所有功能全部实现于一个单一的源文件内部。编译时间长,不易于维护和升级,不易于协作开发。
- 分离模型:
- 将程序中的不同功能模块划分到不同的源文件中。缩短编译时间,易于维护和升级,易于协作开发。
- 库文件:将多个目标文件统一整理合成一个库文件,一般分为两种,一种是静态库,一种是动态库。
4.1 静态库
- 静态库的本质就是将多个目标文件打包成一个文件。
- 链接静态库就是将库中被调用的代码复制到调用模块中
- 静态库的拓展名是.a 例:libxxx.a
构建静态库 - 实现相关函数
- 编译成目标文件
- 打包成静态库
(1)编辑相关的代码和接口声明
// 书写calc.h calc.c show.h show.c
// calc.h文件
#ifndef __CALC_H_
#define __CALC_H_
// 加法
int add(int,int);
// 减法
int sub(int,int);
#endif
// calc.c文件
#include "calc.h"
int add(int a,int b){
return a + b;
}
int sub(int a,int b){
return a - b;
}
// show.h
#ifndef __SHOW_H_
#define __SHOW_H_
void show(int,char,int,int);// 显示等式
#endif
// show.c
#include "show.h"
#include <stdio.h>
void show(int a,char op,int b,int c){
printf("%d %c %d = %d\n",a,op,b,c);
}
(2) 编译成目标文件
gcc -c calc.c
gcc -c show.c
(3) 打包成静态库
ar -r libmath.a calc.o show.o
注意:静态库的名称是以lib开头,以.a为扩展名的
使用静态库
// 使用
#include <stdio.h>
#include "calc.h"
#include "show.h"
int main(){
int a=8,b=5;
show(a,'+',b,add(a,b));
show(a,'-',b,sub(a,b));
return 0;
}
// 与静态库在同一个目录下时
gcc main.c libmath.a
// 与静态库不在一个目录下时
gcc main.c -lmath -L..
// -l 库名,不需要书写开头的lib -L库路径
// 也可以使用环境变量来指定库路径
export LIBRARY_PATH=$LIBRARY_PATH:.
gcc main.c -lmath
4.2 动态库
- 动态库和静态库不同,链接动态库不需要将被调用的函数代码复制到包含调用代码的可执行文件中,相反链接器会在调用语句处嵌入一段指令,在该程序执行到这段指令时,会加载该动态库并寻找被调用函数的入口地址并执行之。
- 如果动态库中的代码同时为多个进程所用,动态库在内存的实例仅需一份,为所有使用该库的进程所共享,因此动态库亦称共享库。
- 动态库的拓展名是.so 例libxxx.so
构建静态库 - 实现相关函数
- 编译成目标文件
- 打包成动态库
(1)实现相关的函数,这里使用上文提到的calc.h calc.c show.h show.c
(2)编译成目标文件
gcc -c -fpic calc.c
gcc -c -fpic show.c
// -fpic 在编译阶段告诉编译器产生位置无关码
(3)打包成动态库
gcc -shared calc.o show.o -o libmath.so
// 其中文件名必须以lib开头,扩展名为so
(4)使用
gcc main.c libmath.so
注意:在生成的可执行文件执行时,会抛出一下异常,这是因为动态库在链接过程中,链接器会找到库所在的位置,所以需要做一下的配置,在环境变量中加入库所在的文件
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
再次执行即可
-
总结
PATH:bash用来找命令的
LIBRARY_PATH:gcc在编译阶段找库用的
LD_LIBRARY_PATH:链接器在执行阶段找库用的 -
静态库和动态库的优缺点:
- 静态库的体积比动态库要大,执行的效率比动态库要高,在执行过程静态库不依赖库本身
- 动态库在执行过程中依赖库本身,链接器需要找到依赖库,所以动态库更有利于软件更新
4.3 动态库的动态加载
在程序执行的过程中,当需要链接动态库中的函数时则加载,反之则释放,以下是常用函数
1:dlopen
void* dlopen(char const* filename,int flag);
- 功能:将共享库载入内存并获得其访问句柄
- 参数:
- filename:动态库路径,若只给文件名不带目录,则根据LD_LIBRARY_PATH环境变量的值搜索动态库
- flag:加载方式,可取以下值:
RTLD_LAZY-延迟加载,使用动态库中的符号时才真的加载进内存
RTLD_NOW-立即加载。
- 返回值:成功返回动态库的访问句柄,失败返回NULL。
- 句柄:句柄唯一地标识了系统内核所维护的共享库对象,将作为后续函数调用的参数
2:dlsym
void* dlsym(void* handle,char const* symbol);
- 功能:从已被加载的动态库中获取特定名称的符号地址
- 参数:
- handle:动态库访问句柄
- symbol:符号名
- 返回值:成功返回给定符号的地址,失败返回NULL。
- 该函数所返回的指针为void* 类型,需要造型为与实际目标类型相一致的指针后才能使用。
3:dlclose
int dlclose(void* handle);
- 功能:从内存中卸载动态库
- 参数:
- handle:动态库句柄
- 返回值:成功返回0,失败返回非0。
- 注意:所卸载的共享库未必会真的从内存中立消失,因为其他程序可能还需要使用该库,只有所有使用该库的程序都显示或隐式地卸载了该库,该库所占用的内存空间才会真正得到释放,但是无论所卸载的共享库是否真正被释放,传递给dlclose函数的句柄都会在该函数成功返回后立即失效
4:dlerror
char* dlerror(void);
- 功能:获取在加载、使用和卸载共享库过程中所发生的错误
- 返回值:有错误则返回指向错误信息字符串的指针,否则返回NULL。
- 使用
// 演示动态库的动态加载
#include <stdio.h>
#include <dlfcn.h>// dlopen dlsym dlclose dlerror
int main(){
// 载入内存
void* handle = dlopen("./shared/libmath.so",RTLD_NOW);
if(handle == NULL){
fprintf(stderr,"dlopen:%s\n",dlerror());
return -1;
}
// 使用库中的函数
int (*add)(int,int) = dlsym(handle,"add");
if(add == NULL){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
void (*show)(int,char,int,int) = dlsym(handle,"show");
if(show == NULL){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
int a=8,b=5;
show(a,'+',b,add(a,b));
// 卸载库
dlclose(handle);
return 0;
}
在使用上面函数的时候,需要引入dlfcn.h的头文件,在编译时,还需要链接到该库
gcc load.c -ldl
5、虚拟地址
// 虚拟地址空间的布局
#include <stdio.h>
#include <stdlib.h>
const int const_global = 1;// 常全局变量
int init_global = 2;// 初始化全局变量
int uninit_global; // 未初始化全局变量
int main(int argc,char* argv[],char* envp[]){
const static int const_static=3;// 常静态变量
static int init_static = 4;// 初始化静态变量
static int uninit_static;// 未初始化静态变量
const int const_loacl = 5;// 常局部变量
int local;// 局部变量
int* heap = malloc(sizeof(int));// 堆变量
char* string = "hello";// 字面值常量
printf("----------参数和环境区------------\n");
printf(" 命令行参数:%p\n",argv);
printf(" 环境变量:%p\n",envp);
printf("---------栈区-------------------\n");
printf(" 常局部变量:%p\n",&const_loacl);
printf(" 局部变量:%p\n",&local);
printf("--------堆区--------------------\n");
printf(" 堆变量:%p\n",heap);
printf("---------BSS区------------------\n");
printf("未初始化全局变量:%p\n",&uninit_global);
printf("未初始化静态变量:%p\n",&uninit_static);
printf("--------数据区-------------------\n");
printf(" 初始化全局变量:%p\n",&init_global);
printf(" 初始化静态变量:%p\n",&init_static);
printf("--------代码区-------------------\n");
printf(" 函数:%p\n",main);
printf(" 字面值常量:%p\n",string);
printf(" 常全局变量:%p\n",&const_global);
printf(" 常静态变量:%p\n",&const_static);
printf("----------------------------------\n");
return 0;
}
结果如下:
- 相关函数
1:mmap
// 头文件 sys/mman.h
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
- 功能:建立虚拟内存到物理内存或磁盘文件的映射
- 参数:
- start:映射区虚拟内存的起始地址,NULL系统自动选定后返回。
- length:映射区字节数,自动按页取整。
- prot:映射区操作权限,可做以下值:
PROT_READ - 映射区可读
PROT_WRITE - 映射区可写
PROT_EXEC - 映射区可执行
PROT_NONE - 映射区不可访问
- flags:映射标志,可取以下值:
MAP_ANONYMOUS - 匿名映射,将虚拟内存映射到物理内存而非文件,忽略fd和offset参数
MAP_PRIVATE - 对映射区的写操作只反映到缓冲区中并不会真正写入文件
MAP_SHARED - 对映射区的写操作接反映到文件中
MAP_DENYWRITE - 拒绝其它文件的写操作
MAP_FIXED - 若在start上无法创建映射,则失败(无此标志系统会自动调整)
- fd:文件描述符
- offset:文件偏移量,自动按页(4K)对齐
- 返回值:成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(-1)。
2:munmap
// 头文件 sys/mman.h
int munmap(void* start,size_t length);
- 功能:解除虚拟内存到物理内存或磁盘文件的映射
- 参数:
- start:映射区虚拟内存的起始地址。
- length:映射区字节数,自动按页取整。
- 返回值:成功返回0,失败返回-1。
munmap允许对映射区的一部分解映射,但必须按页处理
- 例子
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
int main(){
// 建立映射
char* start = mmap(NULL,4096,PROT_READ | PROT_WRITE,MAP_ANONYMOUS | MAP_PRIVATE,0,0);
if(start == MAP_FAILED){
perror("mmap");
return -1;
}
// 使用
strcpy(start,"zpyl");
printf("%s\n",start);
// 解除映射
if(munmap(start,4096)==-1){
perror("munmap");
return -1;
}
return 0;
}
3:sbrk
// 头文件 unistd.h
void* sbrk(intptr_t increment);
- 功能:以相对方式分配和释放虚拟内存
- 参数:increment堆内存的字节增量(以字节为单位)
\> 0 - 分配内存
\< 0 - 释放内存
\= 0 - 当前堆尾
- 返回值:成功返回调用该函数前的堆尾指针,失败返回-1。
/* 系统内部维护一个指针,指向当前堆尾,即堆区最后一个字节的下一个位置,sbrk函数根据增量参数调整该指针的位置,同时返回该指针在调整前的位置,其间若发现内存页耗尽或空闲,则自动追加或取消内存页的映射 */
#include <stdio.h>
#include <unistd.h>
int main(){
printf("当前堆尾:%p\n",sbrk(0));
int* p=sbrk(4);
*p = 123;
printf("p=%p\n",p);
double* p2 = sbrk(8);
*p2 = 3.14;
printf("p2=%p\n",p2);
printf("%d %lg\n",*p,*p2);
// 释放
sbrk(-(4+8));
printf("当前堆尾:%p\n",sbrk(0));
return 0;
}
4:brk
// 头文件 unistd.h
int brk(void* end_data_segment);
- 功能:以绝对方式分配和释放虚拟内存
- 参数:end_data_segment:堆尾指针的目标位置
\> 堆尾指针的原位置 - 分配内存
\< 堆尾指针的原位置 - 释放内存
\= 堆尾指针的原位置 - 空操作
- 返回值:成功返回0,失败返回-1。
/* 系统内部维护一个指针,指向当前堆尾,即堆区最后一个字节的下一个位置,brk函数根据指针参数设置该指针的位置,其间若发现内存页耗尽或空闲,则自动追加或取消内存页的映射*/
#include <stdio.h>
#include <unistd.h>
int main(){
printf("当前堆尾:%p\n",sbrk(0));
int* p =sbrk(0);
brk(p+1);
printf("p=%p\n",p);
*p=123;
double* p2 = sbrk(0);
brk(p2+1);
printf("p2=%p\n",p2);
*p2=3.14;
printf("%d %lg\n",*p,*p2);
// 释放
brk(p);
printf("当前堆尾:%p\n",sbrk(0));
return 0;
}