一、main函数

        C程序总是从main函数开始执行,main函数的原型是:

        int main(int argc,char *argv[]);

正如前面提到的,argc是命令行参数的数目,argv是指向个参数的指针组成的数组。

        当内核执行C程序时,在调用main前先调用一个特殊的启动例程。可执行文件将此例程指定为程序的起始地址--这是由编译器设置的,而连接编译器则由c编译器调用(通常是cc)。启动例程从内核取得命令行参数和环境变量值,然后按上述方式为调用main做好安排

二、进程终止

        八种使进程终止的方式:

        (1)从main返回(正常终止)

        (2)调用exit(正常终止)

        (3)调用_exit或_Exit(正常终止)

        (4)最后一个线程从启动例程返回(正常终止)

        (5)最后一个线程调用pthread_exit(正常终止)

        (6)调用abort(异常终止)

        (7)接收到信号并终止(异常终止)

        (8)最后一个线程取消对请求做出的响应(异常终止)

1.exit函数

        区别:_exit和_Exit函数立即进入内核,而exit则先执行一些清理处理(调用执行终止处理程序,关闭所有标准IO流等等),然后进入内核:

#include <stdlib.h>

void exit(int status);
void _Exit(int status);

#include <unistd.h>

void _exit(int status);

        exit函数总是会调用fclose函数关闭已经打开的标准IO流。

        status参数称为终止状态,如果调用时未指定终止状态,或mian执行了一个无返回值的return,或main没有声明返回的类型为整形,则该进程的终止状态使未定义的。可以使用命令:echo $?来打印命令的退出状态码

        return 0;等价于exit(0);

2.atexit函数

        按照ISO C规定,一个进程可以登记多达32个终止处理函数,调用atexit函数来登记终止处理函数。

#include <stdlib.h>

int atexit(void (*func)(void));

        - atexit函数的参数是一个函数地址,调用时不需要传递任何参数,不期待返回任何值。

        - exit调用这些终止处理函数的顺序与登记顺序相反,重复登记一个函数会导致重复调用。

        根据ISO C标准,exit首先调用各终止处理函数,然后按需调用fclose。

POSIX.1扩展了ISO C标准,若程序调用了exec家族的一个函数(其作用是在当前程序在exec调用处启动一个新的程序,旧的进程立即终止不再运行),则清除旧程序的所有终止处理函数,然后执行新程序

        内核使程序执行的唯一方法是调用一个exec函数。进程资源终止的唯一方法是显式或隐式的调用_exit或_Exit。进程也可以非结缘的由一个信号使其终止。

        例:

#include "apue.h"

static void my_exit1(void);
static void my_exit2(void);
int main(void){
    if(atexit(my_exit2)!=0)
        err_sys("can't register my_exit2");
    if(atexit(my_exit1)!=0)
        err_sys("can't register my_exit1");
    if(atexit(my_exit1)!=0)
        err_sys("can't register my_exit1");

    printf("main is done\n");
    return(0);
}

static void my_exit1(void){
    printf("first exit handler");
}
static void my_exit2(void){
    printf("Second exit handler");
}

最终打印:

main is done

first exit handler

first exit handler

second exit handleigemei

三、环境表

        每个程序都有一个环境表,是一个字符指针数组,其中每个指针包含一个以null结束的c字符串的地址。全局变量environ包含了该指针数组的地址:

        extern char **environ;

其中每个字符串结尾都显式的有一个null字符。我们称environ为环境指针,指针数组为环境表。

通常使用getenv和putenv函数来访问特定的环境变量(后面会讲到)

四、C程序的存储空间布局

1.代码段(正文段):储存程序指令,通常正文段是可共享的,所以即使多次调用的程序也只需要保存一个副本即可。正文段只读,防止程序意外修改自身指令。

2.初始化数据段(只读数据段+已初始化的数据段):只读数据段中存储字符串常量、const变量等,已初始化的数据段初始值来自可执行文件(例如c程序中出现在任何函数之外并且已赋初始值的声明)。

3.非初始化数据段(BSS段):在程序开始之前将此段内的数据初始化为0或空指针(例如c程序中出现在任何函数之外并且未赋初始值的声明)。

4.:自动变量(局部变量)以及每次函数调用时所需保存的信息(返回地址即函数结束后返回到哪、调用者环境即保存的寄存器值、传递给函数的参数、局部变量)都存放在此段中。每次调用函数时,返回的地址以及调用者的环境信息都存放在栈中。通过这种方式调用栈,可以递归调用c函数,递归函数每次调用自身时就使用一个新的栈帧,因此一个函数调用实例中的变量及不会影响另一个函数调用实例中的变量(一个栈帧对应一次调用,每个栈帧相互独立)。栈是编译器自动管理的。地址由高向低增长。

5.:进行动态内存分配,由程序员手动管理,地址由低向高增长,容易出现内存碎片、内存泄漏等问题。

典型的地址空间:

- 栈底和堆顶之间有很大的虚地址空间

- 有图中注意到,未初始化的数据段内容并不存放在磁盘上的程序文件中,因为内核在程序开始运行之前将它们都设置为0.需要存放在程序文件中的段只要正文段和初始化数据段。

- size命令查看程序的空间占用情况

五、存储器分配

        ISO C说明了三个用于存储空间动态分配的函数。

        (1)malloc分配指定字节数的存储区,此存储区中的初始值不确定。

        (2)calloc为指定数量具有指定长度的对象分配存储空间,该空间中每一位都初始化为0

        (3)realloc更改以前分配的长度(可增加或减少)。增加长度时可能会将目标移动到另一个足够大的区域,新增区的初始值不确定。

#include <stdlib.h>

void *malloc(size_t size);
void *calloc(size_t nobj,size_t size);
void *realloc(void *ptr,size_t newsize);//newsize是指新的存储区的长度
//成功则返回非空指针,失败则返回NULL

void free(void *ptr);

        因为这三个函数都返回通用指针void*,所以如果在程序中包括了stdlib.h库(获得函数原型),则我们将返回指针赋予一个不同类型的指针时,就不需要显式的执行类型强制转换。

什么是内存对齐?

内存对齐是指数据在内存中的起始地址必须是某个值的整数倍,这个值由数据类型决定,这是硬件的要求,不是语言的规定。例如:

char c;     // 1字节对齐(任何地址都可以)
short s;    // 2字节对齐(地址必须是2的倍数)
int i;      // 4字节对齐(地址必须是4的倍数)
double d;   // 8字节对齐(地址必须是8的倍数)
long long l;// 8字节对齐

malloc返回的指针一定是适当对齐的,可以用于任何数据对象。这句话是什么意思?假设本系统最严格的要求是dobule类型(参考上述例子),为8字节对齐,那么这三个函数返回的指针就会自动对齐到8字节,这样对于char的1字节、short的2字节、int的4字节都可以对齐了,所以我们可以将返回指针赋予任何一个类型的指针。

free释放ptr指向的存储空间。被释放的空间通常被送入可用存储区池,以后可以再次进行动态分配。

        realloc可以增加或减少原分配区的长度,例如,如果为一个数组分配存储空间(512),然后再运行过程中填充他,但一段时间后发现原先的长度不够,就可以调用realloc扩充相应空间。需要注意的是,不能使用realloc延长一个静态的数组(我们常用的int arr[10]={0}就是静态分配)

        大多数实现所分配的空间比所要求的大一点,这是因为要有额外的空间来记录管理信息(分配块的长度、指向下一个分配块的指针等等)。这会导致一个严重的问题,假如有一个分配块已经写满,而程序继续向其中写入数据,那么数据就会写入下一个分配块的管理信息。

        其他致命错误:释放一个已经释放了的块;调用free时所用的指针不是三个alloc函数的返回值等等。开辟的空间忘记释放,这种情况被称为泄露。

        alloca函数:它的调用序列与malloc相同,但是它再当前函数的栈帧上分配空间。优点是:当函数返回时,自动释放它所使用的栈帧,不用手动释放。缺点是:alloca函数增加了栈帧的长度,而某些系统在函数已经被调用后就不能再增加栈帧,所以这些系统并不支持alloca函数。

六、环境变量

环境字符串的形式通常如下:

        name = value

UNIX内核并不查看这些字符串,它们的解释完全取决于各个应用程序。

        ISO C标准定义了getenv函数来取环境变量值:

#include <stdlib.h>

char *getenv(const char *name);
//返回值:指向与name关联的value指针,若未找到则返回NULL

UNIX的某些实现中已经定义的环境变量:

我们还可以修改或新增环境变量的值(下节会讲到,我们能影响的只是当前进程及调用的任何子进程的环境,不能影响父进程的环境),并不是所有的系统都支持这个功能,具体实现如下:

#include <stdlib.h>

int putenv(char *str);
int setenv(const char *name,const char *value,int rewrite);
int unsetenv(const char *name);
//成功返回0,否则返回非0值

- putenv取形式为“name=value”的字符串,若name已存在,则替换原来的定义

- setenv将name设置为value,若name已存在且rewrite为非0,则替换现有的定义;若name存在且rewrite为0则不发生变化

- unsetenv删除name的定义,即使name不存在也不会报错

七、setjmp和longjmp函数

        在c语言中goto不能跨越函数,执行函数间跳转的是setjmp和longjmp函数,这两个函数对于处理发生在深层嵌套函数调用中的出错情况是非常有用的

#include <setjmp.h>

int setjmp(jmp_buf env);

void longjmp(jmp_buf env,int val);

        除了能够在函数间跳转之外,这两个函数和goto没什么区别。对于返回值,longjmp函数是没有返回值的;setjmp函数第一次直接调用时返回值为0,后续从longjmp函数再跳转回setjmp函数时,setjmp函数被再次调用,返回非0值(后续会有示例)。

        jmp_buf用来保存现场(栈指针、程序计数器、寄存器、信号掩码等),需要我们事先定义

#include <stdio.h>
#include <setjmp.h>

static jmp_buf error_env;//全局静态变量,保存跳转环境

void level3_function(){
    printf("Level 3: detecting error...\n");
    longjmp(error_env,1);//跳转
    printf("This line will NEVER execute\n");
}

void level2_function(){
    printf("Level 2: calling level 3...\n");
    level3_function();
    printf("Level 2: this will be skipped\n"); 
}

void level1_function(){
    printf("Level 1: calling level 2...\n");
    level2_function();
    printf("Level 1: this will be skipped\n"); 
}

int main(){
    int ret = setjmp(error_env);//设置锚点

    if(ret == 0){//第一次调用setjmp时返回值ret为0
        printf("Main: starting normal execution\n");
        level1_function();
        printf("Main: normal completion (never reached)\n");
    }
    else{//longjmp返回后,setjmp的返回值非0
        printf("Main: error caught,code = %d\n",ret);
    }

    printf("Main: cleanup and exit\n");
    return 0;
}

输出如下:

Main: starting normal execution
Level 1: calling level 2...
Level 2: calling level 3...
Level 3: detecting error...
Main: error caught, code = 1
Main: cleanup and exit

八、getrlimit和setrlimit函数

        每个进程都有一组资源限制,其中一些可以用getrlimit和setrlimit函数查询和更改。

#include <sys/resource.h>

int getrlimit(int resource,struct rlimit *rlptr);
int setrlimit(int resource,const struct rlimit *rlptr);
//成功返回0 ,失败返回非0值

        每次调用都会指定一个资源以及一个指针指向下列结构:

        struct rlimit{

        rlim_t rlim_cur;

        rlim_t rlim_max;

        }

更改资源限制时需要遵循以下规则:

        (1)任何一个进程都可以将一个软限制值更改为小于或等于其硬限制值。

        (2)任何一个进程都可以降低其硬限制值,但它必须大于或等于其软限制值。这种降低对于普通用户而言是不可逆的

          (3) 这两个函数的resourcecabs参数可取如下值:

资源限制影响到调用进程并由其子进程继承

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐