一、错误处理
通过函数返回值表示错误
- 返回值合法表示成功,非法表示失败
- 返回值有效指针表示成功,空指针(NULL/0xffffffff)表示失败
- 返回0表示成功,-1表示失败
- 永远成功,printf
练习1、str_len 求字符串长度,若指针为空则报错
练习2、str_cpy(char* dest size_t dlen char* src)
字符串拷贝函数,考虑目标的溢出问题,如果目标位置无效或超出则报销。
练习3、intmin 求两个整数的最小值,二者相等,则报错。
练习4、intave 求两个整数的平均值,该函数永远成立。
test1-4
#include <stdio.h>
size_t str_len(const char* str)
{
if(NULL == str)
{
return -1;
}
size_t len = 0;
while(str[len]) len++;
return len;
}
char* str_cpy(char* dest,size_t size,const char* src)
{
if(NULL == dest || NULL == src)
{
return NULL;
}
size_t len = str_len(src);
if(size < len)
{
return (void*)0xffffffff;
}
for(int i=0; i<len; i++)
{
dest[i] = src[i];
}
return dest;
}
int intmin(int num1,int num2,int* min)
{
if(num1 == num2)
return -1;
*min = num1 < num2 ? num1 : num2;
return 0;
}
int intavg(int num1,int num2)
{
return num1/2 + num2/2;
}
int main()
{
printf("%d\n",str_len("zzxx"));
char buf[5] = {};
printf("%p\n",str_cpy("hehe",5,"hello world"));
int min = 0;
printf("%d\n",intmin(9,9,&min));
printf("min = %d\n",min);
}
通过errno表示错误
errno 是一个全局变量,它的声明在errno.h
文件中,它的值可能会随时发生变化
可以将它转换成有意义的字符串,strerror(errno) <=> perror(“msg”)
注意:
在函数执行成功的情况下,不会修改errno的值。因此不能以errno的值不等于0就判断函数执行出错了。所以通常会和函数返回值配合,通过返回值判断是否出错,而通过perror查询出了什么类型的错误。
二、环境变量
以NULL结尾的字符串形式存在的,绝大多数数据记录的是路径信息,它表示了当前操作系统的资源配置,环境设置等相关信息。
环境变量表
每个程序运行时,操作系统都会把所有的环境变量记录到一张表中,传给程序。
int main(int argc, char* argv[], char* environ[]);
通过main函数参数获取
extern char** environ;
通过声明为全局变量获取
环境变量函数
char getenv(const char* name);
根据环境变量名,获取环境变量的值
int putenv(char* string);
以name=value形式设置环境变量,如果环境变量存在则更新,不存在则添加。成功返回0,失败返回-1
int setenv(const char* name,const char* value,int overwrite);
设置name环境变量的值为value,如果name存在且overwrite不为零则更新,否则不变。
int unsetenv(const char* name);
从环境变量中删除name
int clearenv(void);
清空环境变量表
注意:
操作系统记录的环境变量的数据记录一块特殊的存储空间,而在程序自己添加的环境变量需要自己准备存储空间。
对于环境变量的修改,只能影响自己,不能影响别人。
练习5、从文件中读取一个程序的配置信息
ServerIP = 192.168.0.1
Port = 8899
MaxSize = 100
ContinueSec = 3
LogPath = /zhizhen/temp/
Datadath = /zhizhen/data/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
void show_environ(void)
{
printf("----------环境变量表----------\n");
extern char** environ;
for(int i=0; environ&&environ[i]; i++)
{
printf("%d %s\n",i,environ[i]);
}
}
void deal_env(char* env)
{
while(*env)
{
if(' ' == *env)
{
memcpy(env,env+1,strlen(env+1)+1);
}
else
env++;
}
}
int load_config(void)
{
FILE* frp = fopen("config","r");
if(NULL == frp)
{
perror("fopen");
return -1;
}
while(true)
{
char* env = malloc(80);
if(NULL==fgets(env,80,frp))
{
free(env);
break;
}
deal_env(env);
env[strlen(env)-1] = '\0';
printf("%d ",putenv(env));
printf("%s",env);
}
}
int main()
{
load_config();
show_environ();
printf("%s\n",getenv("ContinueSec"));
}
练习6、给LIBRARY_PATH添加一个路径
(/home/zhizhen/lib)
LIBRARY_PATH = / home / zhizhen
LIBRARY_PATH = / home / zhizhen : / home / zhizhen / lib
#include <stdio.h>
#include <stdlib.h>
void show_environ(void)
{
printf("----------环境变量表----------\n");
extern char** environ;
for(int i=0; environ&&environ[i]; i++)
{
printf("%d %s\n",i,environ[i]);
}
}
int main()
{
char env[256] = {};
setenv("LIBRARY_PATH",strcat(strcpy(env,getenv("LIBRARY_PATH")),":/home/zhizhen/lib"),1);
puts(getenv("LIBRARY_PATH"));
}
三、内存管理
名称 | 属于 | 调用 |
---|---|---|
自动分配/释放内存auto_ptr | STL | 调用标准C++中的new/delete |
new/delete 构造/析构 | C++ | 标准C中的malloc/free |
malloc/free | 标准C | 调用POSIX |
brk/sbrk | POSIX | 调用Linux系统接口 |
mmap/munmap | Linux | 调用内核接口 |
kmalloc/vmalloc | 内核 | 调用驱动 |
get_free_page | 驱动 | … |
四、进程映像
程序是保存在磁盘上的可执行文件,加载到内存中被操作系统调用执行的程序叫进程(一个程序可以被同时执行多次形成身份不同的进程)。
进程在内存空间中的分布情况叫做进程映像,从低地址到高地址一次排列的是:
- 代码段/只读段:二进制指令、字符串字面值、具有const属性且被初始化过的全局、静态变量。
- 数据段:被初始化过的全局变量和静态变量。
- BSS段:没有初始化过的全局变量和静态变量,进程一旦被加载成功就会把这段内存清零。
- 堆:动态的分配、管理,需要程序员手动操作。(低地址向高地址)
- 栈:非静态的局部变量,包括函数的参数、返回值,从高地址向低地址使用,和堆内存之间存在一段空隙。(拓展使用,增加预留空间;存放共享库、共享内存)
- 命令行参数及环境变量表:命令行参数、环境变量
练习7、在一个程序中打印各段内存的一个地址,然后与操作系统中的内存分配情况表比较,然后一一对应内存的分配情况
getpid() : 可以获取进程的编号
cat / proc / xxxx / maps
size 程序名 查看text data bss 各段的大小
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 常属性全局变量
const int const_global = 100;
// 初始化的全局变量
int init_global = 100;
// 末初始化的全局变量
int uninit_global;
int main(int argc,char* argv[],char* environ[])
{
// 常属性静态局部变量
const static int const_static = 100;
// 初始化的静态局部变量
static int init_static = 100;
// 末初始化的静态局部变量
static int uninit_static;
// 常局部变量
const int const_local;
// 局部变量 int local;
// 堆地址 void* heap_ptr = malloc(4);
// 字符串字面值地址
const char* const_str = "hehe";
printf("----------从低到高依次是----------\n");
printf("代码段:%p\n",main);
printf("只读段:%p %p %p\n",&const_global,&const_static,const_str);
printf("数据段:%p %p\n",&init_global,&init_static);
printf("BSS段:%p %p\n",&uninit_global,&init_static);
printf("堆:%p\n",heap_ptr);
printf("栈:%p %p\n",&const_local,&local);
printf("命令行参数:%p\n",argv[0]);
printf("环境变量表:%p\n",environ[0]);
printf("cat /proc/%d/maps\n",getpid());
getchar();
}
五、虚拟内存
每个进程都有各自独立的4G字节的虚拟地址空间,我们在编程时使用的永远都是这4G的虚拟地址空间中的地址,永远无法直接访问物理内存地址。
操作系统不让程序直接访问物理内存而只能使用虚拟地址空间一方面为了操作系统自身的安全,另一方面可以让程序使用到比物理内存更大的地址空间(把硬盘上的特殊文件与虚拟地址空间进行映射)
4G的虚拟地址空间被分为两个部分:
0~3G 用户空间
3G~4G 内核空间
注意:
- 用户空间的代码不能直接访问内核空间的代码和数据,但可以通过系统调用(不是函数,但以函数形式进行调用)进入到内核态间接与内核交换数据。
- 如果使用了没有映射过的或者访问没有权限的虚拟内存地址,就会产生段错误(非法内存访问)
- 一个进程对应一个用户空间,进程一点切换,用户空间也会发生变化,但内核空间由操作系统管理,它不会随着进程的切换而变化
- 内核空间由内核所管理的一张独立且唯一的init_mm表进行内存映射,而用户空间的表是每个进程一张。
- 每个进程的内存空间是相互独立的,不同的进程间交换虚拟内存地址没有任何意义,进程之间不能直接进行通信,需要由内核进行中转、协调。
- 虚拟内存到物理内存的映射以页为单位(一页等于4K = 4096字节)
- malloc首次映射33页,free释放掉所有内存后,最开始映射的33页仍然存在。
六、内存管理API
他们都可以进行映射内存的取消映射(系统级的内存管理)
#include <unistd.h>
void* sbrk(intptr_t increment);
increment:
等于0:获取未分配前的首地址(也就是已经分配尾地址)
大于0:增加内存空间
小于0:释放内存空间
返回值:未分配前的内存首地址,以字节为单位
int brk(void* addr);
功能:设置未分配内存的首地址
返回值:成功返回0,失败返回-1
它们背后维护这一个指针,该指针记录的是未分配的内存的首地址(当前堆内存的最后一个字节的下一个位置)。
它们都是可以进行映射内存的取消映射的功能(系统级的内存管理),但为了方便起见,sbrk一般用于分配内存,brk用于释放内存。
注意:sbrk/brk分配的释放的都是使用权,真正的映射工作由其他系统调用完成(mmap / munmap:建立和取消虚拟内存地址和物理内存或文件之间的映射关系)
练习8:计算1000以内的素数,存储到堆内存中,不要浪费内存(sbrk / brk)。
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
bool is_prime(int num)
{
for(int i=2; i<=num/2; i++)
{
if(0 == num % i)
{
return false;
}
}
return true;
}
int main()
{
int* start = sbrk(4);
int cnt = 0;
for(int i=2; i<1000; i++)
{
if(is_prime(i))
{
start[cnt++] = i;
sbrk(4);
}
}
for(int i=0; i<cnt; i++)
{
printf("%d ",start[i]);
}
brk(start);
}
练习9:使用sbrk / brk 实现顺序栈
#include <sys/mman.h>
void* mmap(void* addr,size_t lengh,int prot,int flag,int fd,off_t offset);
功能:把虚拟内存地址与物理内存或文件建立映射关系。
addr:要映射的虚拟内存地址,如果为NULL操作系统会自动选择一个虚拟地址与物理内存映射。
length:要映射的字节数
prot:权限
flags:映射标志
fd:文件描述符(与内存映射没有关系)
offset:文件映射偏移值
返回值:映射成功后的虚拟内存地址,如果出错返回值为0xffffffff
int munmap(void *addr, size_t length);
功能:取消映射
addr:需要取消映射的内存首地址
length:需要映射的字节数
返回值:成功返回0,失败返回-1