UNIX-Linux环境编程(三):内存管理

一、错误处理

1. 通过函数的返回值表示错误

  1. 返回合法值表示成功,返回非法值表示失败。
    范例:bad.c
#include <stdio.h>
#include <limits.h>

// 获取文件大小
// 成功返回文件大小,失败返回-1
long fsize (const char* path) 
{
    FILE* fp = fopen (path, "r");
    if (! fp) return -1;

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

    fclose (fp);

    return size;
}

int main (void) 
{
    printf ("文件路径:");
    char path[PATH_MAX+1];
    scanf ("%s", path);

    long size = fsize (path);
    if (size < 0) 
    {
        printf ("获取文件大小失败!\n");
        return -1;
    }

    printf ("文件大小:%d字节\n", size);
    return 0;
}
  1. 返回有效指针表示成功, 返回空指针(NULL/0xFFFFFFFF)表示失败。
    范例:null.c
#include <stdio.h>
#include <string.h>

// 求字符串最大值
// 成功返回参数字符串中的最大值,失败返回NULL
const char* strmax (const char* a, const char* b) 
{
    return a && b ? (strcmp (a, b) > 0 ? a : b) : NULL;
}

int main () 
{
    const char* max = strmax ("hello", "world");
    const char* max = strmax ("hello", NULL);
    if (!max) 
    {
        printf ("求字符串最大值失败!\n");
        return -1;
    }

    printf ("字符串最大值:%s\n", max);
    return 0;
}
  1. 返回0表示成功,返回-1表示失败,不输出数据或通过指针/引用型参数输出数据。
    范例:fail.c
#include <stdio.h>

// 整数取模
// 成功返回0,失败返回-1
int intmod (int a, int b, int* mod) 
{
    if (b == 0) return -1;
    *mod = a % b;
    return 0;
}

int main () 
{
    printf ("两个整数:");
    int a, b;
    scanf ("%d%d", &a, &b);

    int mod;
    if (intmod (a, b, &mod) == -1) 
    {
        printf ("整数取模失败!\n");
        return -1;
    }

    printf ("整数取模:%d\n", mod);
    return 0;
}
  1. 永远成功,如:printf

2. 通过errno表示错误

#include <errno.h>
  1. 根据errno得到错误编号。
  2. 将errno转换为有意义的字符串
    范例:errno.c
#include <stdio.h>
#include <errno.h>

int main () 
{
    FILE* fp = fopen ("none", "r");
    if (! fp) 
    {
        printf ("fopen: %d\n", errno);
        printf ("fopen: %s\n", strerror (errno));
        printf ("fopen: %m\n");
        perror ("fopen");
        return -1;
    }

    fclose (fp);
    return 0;
}
  1. errno在函数执行成功的情况下不会被修改,因此不能以errno非零,作为发生错误判断依据。
    范例:iferr.c
#include <stdio.h>
#include <errno.h>

int main () 
{
    FILE* fp = fopen ("none", "r");
    fp = fopen ("/etc/passwd", "r");
    if (errno) 
    {
        perror ("fopen");
        printf ("fp = %p\n", fp);
        return -1;
    }

    fclose (fp);
    return 0;
}
  1. errno是一个全局变量,其值随时可能发生变化。

二、环境变量

1. 环境表

•1) 每个程序都会接收到一张环境表,是一个以NULL指针结尾的字符指针数组。
•2) 全局变量environ保存环境表的起始地址。
.环境表

2. 环境变量函数

#include <stdlib.h>

环境变量:name=value
getenv   - 根据name获得value。
putenv   - 以name=value的形式设置环境变量,name不存在就添加,存在就覆盖其value。

setenv   - 根据name设置value,注意最后一个参数表示,若name已存在是否覆盖其value。

unsetenv - 删除环境变量。

clearenv - 清空环境变量,environ==NULL。

范例:env.c

#include <stdio.h>

void printenv () 
{
    printf ("---- 环境变量 ----\n");

    extern char** environ;
    char** env;
    for (env = environ; env && *env; ++env)
       printf ("%s\n", *env);

    printf ("------------------\n");
}

int main () 
{
    char env[256];
    const char* name = "MYNAME";
    //添加环境变量
    sprintf (env, "%s=minwei", name);
    putenv (env);
    printf ("%s=%s\n", name, getenv (name));
    //修改环境变量
    sprintf (env, "%s=bjarne", name);
    putenv (env);
    printf ("%s=%s\n", name, getenv (name));
    //不存在就添加,存在不覆盖
    setenv (name, "minwei", 0);
    printf ("%s=%s\n", name, getenv (name));
    //不存在就添加,存在就覆盖
    setenv (name, "minwei", 1);
    printf ("%s=%s\n", name, getenv (name));

    printenv ();
    //删除环境变量
    unsetenv (name);
    printenv ();
    //清空环境变量
    clearenv ();
    printenv ();

    return 0;
}

三、内存管理

内存管理

四、进程映像

  1. 程序是保存在磁盘上的可执行文件。

  2. 运行程序时,需要将可执行文件加载到内存,形成进程。

  3. 一个程序(文件)可以同时存在多个进程(内存)。

  4. 进程在内存空间中的布局就是进程映像,从低地址到高地址依次为:

     •代码区(text): 
      可执行指令、字面值常量、具有常属性的全局和静态局部变量。只读。
     •数据区(data): 
      初始化的全局和静态局部变量。
     •BSS区: 
      未初始化的全局和静态局部变量。 
      进程一经加载此区即被清0。 
      数据区和BSS区有时被合称为全局区或静态区。
     •堆区(heap): 
      动态内存分配。从低地址向高地址扩展。
     •栈区(stack): 
      非静态局部变量, 
      包括函数的参数和返回值。 
      从高地址向低地址扩展。 
      堆区和栈区之间存在一块间隙,一方面为堆和栈的增长预留空间,			同时共享库、共享内存等亦位于此。
     •命令行参数与环境区: 
      命令行参数和环境变量。 
    

在这里插入图片描述
范例:maps.c


#include <stdio.h>
#include <stdlib.h>

const int const_global = 0; // 常全局变量
int init_global = 0; // 初始化全局变量
int uninit_global; // 未初始化全局变量

int main (int argc, char* argv[]) 
{
    const static int const_static = 0; // 常静态变量
    static int init_static = 0; // 初始化静态变量
    static int uninit_static; // 未初始化静态变量

    const int const_local = 0; // 常局部变量
    int prev_local; // 前局部变量
    int next_local; // 后局部变量

    int* prev_heap = malloc (sizeof (int)); // 前堆变量
    int* next_heap = malloc (sizeof (int)); // 后堆变量

    const char* literal = "literal"; // 字面值常量
    extern char** environ; // 环境变量

    printf ("---- 命令行参数与环境变量 ---- <高>\n");
    printf ("         环境变量:%p\n", environ);
    printf ("       命令行参数:%p\n", argv);
    printf ("-------------- 栈 ------------\n");
    printf ("       常局部变量:%p\n", &const_local);
    printf ("       前局部变量:%p\n", &prev_local);
    printf ("       后局部变量:%p\n", &next_local);
    printf ("-------------- 堆 ------------\n");
    printf ("         后堆变量:%p\n", next_heap);
    printf ("         前堆变量:%p\n", prev_heap);
    printf ("------------- BSS ------------\n");
    printf (" 未初始化全局变量:%p\n", &uninit_global);
    printf (" 未初始化静态变量:%p\n", &uninit_static);
    printf ("------------ 数据 ------------\n");
    printf ("   初始化静态变量:%p\n", &init_static);
    printf ("   初始化全局变量:%p\n", &init_global);
    printf ("------------ 代码 ------------\n");
    printf ("       常静态变量:%p\n", &const_static);
    printf ("       字面值常量:%p\n", literal);
    printf ("       常全局变量:%p\n", &const_global);
    printf ("             函数:%p\n", main);
    printf ("------------------------------ <低>\n");

    printf ("查看/proc/%u/maps,按<回车>退出...", getpid ());
    getchar ();

    return 0;
}

五、虚拟内存

  1. 每个进程都有各自互独立的4G字节虚拟地址空间。

  2. 用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。

  3. 虚拟内存到物理内存的映射由操作系统动态维护。

  4. 虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序,使用比实际物理内存更大的地址空间。

在这里插入图片描述5. 4G进程地址空间分成两部分:

	[0, 3G)为用户空间, 
	如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G; 
	[3G, 4G)为内核空间。
  1. 用户空间中的代码
    不能直接访问内核空间中的代码和数据, 但可以通过系统调用进入内核态, 间接地与系统内核交互。
    kernel

  2. 对内存的越权访问,或试图访问没有映射到物理内存的虚拟内存,将导致段错误

  3. 用户空间对应进程,进程一切换,用户空间即随之变化。

     内核空间由操作系统内核管理,不会随进程切换而改变。 
     内核空间由内核根据独立且唯一的页表init_mm.pgd 进行内存映射,而用户空间的页表则每个进程一份。
    
  4. 每个进程的内存空间完全独立。不同进程之间交换虚拟内存地址是毫无意义的。

  5. 标准库内部通过一个双向链表,管理在堆中动态分配的内存

    malloc函数分配内存时会附加若干(通常是12个)字节,存放控制信息。该信息一旦被意外损坏,可能在后续操作中引发异常。
    

范例:crash.c

#include <stdio.h>
#include <stdlib.h>

int main () 
{
    int* p1 = malloc (sizeof (int));
    int* p2 = malloc (sizeof (int));
    printf ("%p, %p\n", p1, p2);

    free (p2);

    p1[3] = 0;
    free (p1);

    return 0;
}
  1. 虚拟内存到物理内存的映射以页(4K=4096字节)为单位

     通过malloc函数首次分配内存,至少映射33页。即使通过free函数释放掉全部内存,最初的33页仍然保留。
    

内存映射

#include <unistd.h>

int getpagesize (void);
//返回内存页的字节数。

范例:page.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void presskey () 
{
    printf ("查看/proc/%u/maps,按<回车>继续...", getpid ());
    getchar ();
}

int main () 
{
    printf ("1页 = %d字节\n", getpagesize ());

    char* pc = malloc (sizeof (char));
    printf ("pc = %p\n", pc);
    presskey ();

    free (pc);
    printf ("free(%p)\n", pc);
    presskey ();

    pc = malloc (sizeof (char));
    printf ("pc = %p\n", pc);
    presskey ();

    setbuf (stdout, NULL);
    size_t i = 0;
    for (;;)
    {
        printf ("向堆内存%p写...", &pc[i]);
        printf ("%c\n", pc[i++] =  (i % 26) + 'A');
    }

    free (pc);

    return 0;
}

malloc

六、内存管理APIs

1. 增量方式分配虚拟内存

#include <unistd.h>
void* sbrk ( intptr_t increment );
//返回上次调用brk/sbrk后的末尾地址,失败返回-1。
increment取值:内存增量(以字节为单位)
 0 - 获取末尾地址。
>0 - 增加内存空间。
<0 - 释放内存空间。

内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。
sbrk函数根据增量参数调整该指针的位置,同时返回该指针原来的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
在这里插入图片描述

2. 修改虚拟内存块末尾地址

	#include <unistd.h>
	int brk ( void* end_data_segment //内存块末尾地址);
	//成功返回0,失败返回-1。

内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。
brk函数根据指针参数设置该指针的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
sbrk/brk底层维护一个指针位置,以页(4K)为单位分配和释放虚拟内存。
简便起见,可用sbrk分配内存,用brk释放内存。

3. 创建虚拟内存到物理内存或文件的映射

#include <sys/mman.h>

void* mmap (
void*  start,  // 映射区内存起始地址,NULL系统自动选定,成功返回之
size_t length, // 字节长度,自动按页(4K)对齐
int    prot,   // 映射权限
int    flags,  // 映射标志
int    fd,     // 文件描述符
off_t  offset  // 文件偏移量,自动按页(4K)对齐
);

成功返回映射区内存起始地址,失败返回MAP_FAILED(-1)。

prot取值:

PROT_EXEC  - 映射区域可执行。

PROT_READ  - 映射区域可读取。

PROT_WRITE - 映射区域可写入。

PROT_NONE  - 映射区域不可访问。

flags取值:

MAP_FIXED     - 若在start上无法创建映射,
            则失败(无此标志系统会自动调整)。

MAP_SHARED    - 对映射区域的写入操作直接反映到文件中。

MAP_PRIVATE   - 对映射区域的写入操作只反映到缓冲区中,
            不会真正写入文件。

MAP_ANONYMOUS - 匿名映射,
            将虚拟地址映射到物理内存而非文件,
            忽略fd。

MAP_DENYWRITE - 拒绝其它对文件的写入操作。

MAP_LOCKED    - 锁定映射区域,保证其不被置换。

4. 销毁虚拟内存到物理内存或文件的映射

int munmap (
void*  start,  // 映射区内存起始地址
size_t length, // 字节长度,自动按页(4K)对齐
);

成功返回0,失败返回-1。

范例:mmap.c

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

#define MAX_TEXT 256

int main (void) 
{
    char* psz = (char*)mmap (/*sbrk (0)*/NULL, MAX_TEXT * sizeof (char),
        PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
    if (psz == MAP_FAILED) 
    {
        perror ("mmap");
        return -1;
    }

    sprintf (psz, "Hello, World !");
    printf ("%s\n", psz);

    printf ("psz = %p\n", psz);
    printf ("查看/proc/%u/maps,按<回车>退出...", getpid ());
    getchar ();

    if (munmap (psz, MAX_TEXT * sizeof (char)) == -1) 
    {
        perror ("munmap");
        return -1;
    }

    return 0;
}

mmap/munmap底层不维护任何东西,只是返回一个首地址,所分配内存位于堆中。
brk/sbrk底层维护一个指针,记录所分配的内存结尾,所分配内存位于堆中,底层调用mmap/munmap。
malloc底层维护一个双向链表和必要的控制信息,不可越界访问,所分配内存位于堆中,底层调用brk/sbrk。
每个进程都有4G的虚拟内存空间,虚拟内存地址只是一个数字,并没有和实际的物理内存将关系。
所谓内存分配与释放,其本质就是建立或取消虚拟内存和物理内存间的映射关系。

猜你喜欢

转载自blog.csdn.net/perror_0/article/details/106871298