UNIX环境高级编程 学习笔记 第七章 进程环境

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

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

argc是命令行参数的数目,argv是指向参数的各个指针所构成的数组。

内核执行C程序时(使用一个exec函数),在调用main之前先调用一个特殊的启动例程。可执行程序文件将此启动例程设置为程序的起始地址,这是在C编译器调用连接编辑器时设置的。启动例程从内核取得命令行参数和环境变量值,为调用main做好准备。

五种正常终止程序的方式:
1.从main函数返回。
2.调用exit。
3.调用_exit或_Exit。
4.最后一个线程从启动例程返回。
5.从最后一个线程调用pthread_exit。

三种异常终止程序的方式:
1.调用abort。
2.接到一个信号。
3.最后一个线程对取消请求做出响应。

启动例程可能的C代码表示(实际常用汇编写):

exit(main(argc, argv));

以下三个函数用于正常终止一个程序:_Exit和_exit函数立即进入内核,exit函数先执行一些清理处理,再返回内核:
在这里插入图片描述
使用不同头文件原因:exit和_Exit函数是ISC C说明的,而_exit函数是POSIX.1说明的。

由于历史原因,exit函数总是执行一个标准IO库的清理关闭操作:对于所有打开流调用fclose函数,这会使输出缓冲中的所有数据都被冲洗到文件上。

三个退出函数都带一个整型参数,称为终止状态(退出状态),大多UNIX shell都提供检查进程终止状态的方法。如果:
1.调用这些函数时不带终止状态。
2.main执行了一个无返回值的return语句。
3.main没有声明返回类型为整型。
则该进程的终止状态未定义。但,若main的返回类型为整型,且main执行到最后一条语句时返回(隐式返回),则该进程终止状态为0。这种处理由ISO C在标准1999版引入,在此之前若main函数没有显式调用return或exit,那么进程中止状态未定义。

main函数return一个整型值和用该值调用exit等价。

在1999 ISO C之前运行以下程序:

#include <stdio.h>

main() {
    printf("hello world\n");
}

编译后运行,终止码随机,终止码取决于main函数返回时栈和寄存器的内容:
在这里插入图片描述
如编译时启用1999 ISO C编译器扩展,则终止码为0:
在这里插入图片描述
上图编译时有警告,main函数的类型没有显式声明为整型,当我们增加了main的int声明后警告会消失。如我们使编译器的警告都起作用(使用-Wall标志),可能见到"control reaches end of nonvoid function"(控制到达非void函数尾端)。将main声明为int,且在main中用exit代替return,对某些C编译器或UNIX lint(静态C代码检测工具,能检查出程序中的可移植性错误或潜在错误)会产生不必要的警告信息,这是因为它不了解main中的exit和return函数的作用相同,避开此类警告的一种方法是在main中使用return而非exit函数,但这样就不能用grep找出程序中所有exit调用;另一解决方法是将main声明为void而非int,但这会产生其他警告,因为main返回类型应该是带符号类型。ISO C和POSIX.1标准都规定应将main函数的返回类型定义为int。

不同编译器产生警告的详细程度不同,有些编译器会发出不必要的警告信息。

ISO C规定一个进程可以至少登记32个函数,这些函数将由exit函数自动调用,这些函数被称为终止处理程序,可用atexit函数登记这些函数:
在这里插入图片描述
atexit函数的参数是一个函数地址,函数exit调用这些被登记的函数的顺序与它们登记时的顺序相反,同一函数登记多次时,也会被调用多次。

终止处理程序由ANSI C于1989年引入,此前系统(如SVR 3和4.3BSD)都不提供此功能。ISO C要求系统至少应支持32个终止处理程序,可用sysconf函数确定具体值。

ISO C和POSIX.1规定,exit函数首先调用各处理终止程序,然后关闭(fclose)所有打开流。POSIX.1扩展了ISO C标准,若程序调用exec函数族中任一函数,则将清除所有已安装的终止处理程序。

C程序的启动和终止:
在这里插入图片描述
内核使程序执行的唯一方法是调用exec,进程自愿终止的唯一方法是显式或隐式调用三个退出函数之一。进程也可非自愿地由一个信号终止。

使用atexit函数:

#include <iostream>
#include <stdlib.h>
using namespace std;

static void my_exit1();
static void my_exit2();

int main() {
    if (atexit(my_exit2) != 0) {
        cout << "can not register my_exit2" << endl;
        exit(1);
    }
    if (atexit(my_exit1) != 0) {
        cout << "can not register my_exit1" << endl;
        exit(1);
    }
    if (atexit(my_exit1) != 0) {
        cout << "can not register my_exit1" << endl;
        exit(1);
    }
    
    cout << "main is done" << endl;
    return(0);
}

static void my_exit1() {
    cout << "first exit handler" << endl;
}

static void my_exit2() {
    cout << "second exit handler" << endl;
}

执行结果:
在这里插入图片描述
执行一个程序时,调用exec的进程可将命令行参数传递给该新程序。

myecho:

#include <iostream>
using namespace std;

int main(int argc, char *argv[]) {
    for (int i = 1; i < argc; ++i) {
        cout << "argv[" << i << "]: " << argv[i] << endl;
    }
}

执行结果:
在这里插入图片描述
ISO C和POSIX.1要求argv[argc]是一个空指针,我们可以将循环改为:

for (int i = 1; argv[i] != NULL; ++i) { /* ... */ } 

每个程序都收到一张环境表,与参数表一样,环境表也是一个字符指针数组,每个指针指向一个以null结束的C风格字符串。全局变量environ包含了该指针数组的地址:

extern char **environ;

称environ为环境指针,如该环境包含五个字符串:
在这里插入图片描述
按惯例,环境由name=value格式的字符串组成,且环境名大多由大写字母组成。

历史上,main函数有三个参数:

int main(int argc, char *argv[], char *envp[]);    // 第三个参数为环境表地址

ISO C规定main只有两个参数,POSIX.1也规定应使用environ。通常用getenv和putenv函数访问特定的环境变量,而非直接访问environ,但如果要查看整个环境,还是要用environ。

C程序由下列几部分组成:
1.正文段,这是CPU执行的机器指令部分,通常正文段可共享,因此即使是频繁执行的程序(如文本编辑器、C编译器、shell等)在存储器中也只需有一个副本,它通常是只读的,防止程序意外修改指令。
2.初始化数据段(数据段),通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。如C的任何函数外的声明:

int maxcount = 99;    // 此变量及其初值存放在初始化数据段中

3.未初始化数据段(bss段),名称来源于汇编程序的一个操作符,含义为由符号开始的块(block started by symbol),程序执行前,内核将此段中数据初始化为0或空指针,如函数外的声明:

int sum[1000];    // 此变量存放在非初始化数据段中

4.栈,自动变量和每次调用函数时需要保存的信息。每次函数调用时,其返回地址和调用者环境信息(如寄存器值等)放在栈中。每次被调用的函数在栈上为其自动和临时变量分配存储空间。递归函数每次调用自身时,就用一个新的栈帧,因此一次调用中的变量值不会影响另一次函数调用中的变量。
5.堆,通常在其中进行动态存储分配,由于历史原因,堆位于未初始化数据段和栈之间。

典型的C存储空间安排,这是逻辑布局:
在这里插入图片描述
对于32位的Intel x86处理器上的Linux,正文段从0x08048000单元开始,栈底在0xC0000000之下开始(这种结构中,栈从高地址向低地址方向增长),堆顶和栈顶间未用的虚地址空间很大。

a.out中还有其他类型的段,如含符号表的段、含调试信息的段、包含动态共享库链接表的段等,但它们不装载到进程执行的程序映像中。

未初始化数据段中内容(指这些变量的值)不存放在磁盘程序文件中,内核在程序运行前将它们设置为0。磁盘程序文件中只有正文段和初始化数据段。

size命令可报告正文段、数据段、bss段长度:
在这里插入图片描述
第四列和第五列分别是以十进制和十六进制表示的三段总长度。

共享库使可执行文件中不再需要包含公用的库函数,只需在所有进程都可引用的存储区保存一个这种库例程(某个系统对外提供的功能接口或服务的集合)的副本。程序第一次执行或调用某个库函数时,用动态链接方法将程序和共享库函数相链接,减少了可执行文件的长度,但增加了运行时开销,时间开销发生在程序第一次被执行或每个库函数第一次被调用时。共享库还可以用新版本库函数代替老版本而无需对使用该库的程序重新编译(假定该库函数的参数列表不变)。

不同系统中用不同方法说明是否要用共享库,使用gcc说明使用共享库的长度变化:
在这里插入图片描述
在这里插入图片描述
ISO C规定了三个用于存储空间动态分配的函数:
1.函数malloc,分配指定字节数的存储区,此存储区中初始值不确定。
2.函数calloc,分配指定数量个指定大小的连续存储空间,该空间中每一位bit都是0。
3.函数realloc,增加或减少以前分配区的长度,增加长度时,可能需要将以前分配区内容移到另一个足够大的区域,以便在尾端提供增加的存储区,新增区域内初始值不确定。

在这里插入图片描述
这三个函数返回的指针一定是适当对齐的,使其可用于任何数据对象,如在一个系统上,如果最苛刻的对齐要求是double必须在8的倍数地址单元处开始,那么这些函数返回的指针都应这样对齐。

这三个函数返回类型是void *,在C语言中,void *可隐式转换为任何类型的指针,但在C++中,还需要static_cast才能将空类型指针转换成其他类型指针。

在C函数声明中,若未声明函数的返回值类型,则默认的函数值返回类型为int型。但在C++中,有些编译器不支持以上一点,需要明确指出函数返回类型。

free函数释放指针ptr指向的存储空间。

realloc函数增减以前分配的存储区长度,常用于扩充长度,如果当前存储区有足够空间扩充,则往高地址方向扩充,并返回与传给它的指针值相同的指针,如空间不足,则分配另一个足够大的空间,并把原地址内容移动到新地址,然后返回指向新地址的指针。它的参数是新存储区长度。如果ptr是空指针,则其功能与malloc函数相同。

这些分配例程常用系统调用sbrk实现,它扩充或缩小进程的堆。但大多数malloc和free函数的实现都不减小进程存储空间,释放的空间可供以后再分配,它们在malloc池中而不返回给内核。

存储分配函数的大多实现会分配比所要求的空间稍大的空间,用来记录管理信息,如分配块的长度,指向下一个分配块的指针等,这意味着不能在已分配区的尾后或头前进行写操作,这会破坏管理信息,还可能破坏其他动态分配的对象,这种破坏的源头很难寻找。

其他可能致命的错误:
1.释放一个已经释放过的块。
2.调用free所用的指针不是三个alloc函数的返回值。
3.忘记free会造成内存泄漏。

存储空间分配出错难以跟踪,因此某些系统提供了这些函数的另一实现版本,每次调用alloc或free时都会检错。调用连接编辑器时指定一个专用库,就能在程序中使用这种版本函数。

替代的存储空间分配程序:
1.libmalloc。可用一些函数对存储空间分配程序的操作进行统计。
2.vmalloc。除了vmalloc提供的函数外,还含ISO C存储空间分配函数仿真器。
3.quick_fit。历史上标准malloc函数使用的是首次适配(从空闲分区链首开始查找,直至找到一个能满足其大小要求的空闲分区为止)或最佳适配(分配既能满足要求,又是最小的空闲分区)的分配策略,而quick_fit比较快,但可能使用较多存储空间,该算法基于将存储空间分裂成各种长度的缓冲区,并将未使用的缓冲区按其长度组成不同空闲区列表,现在很多分配程序都基于quick_fit。
4.jemalloc。它是malloc函数族在Free BSD中的实现,可用于多处理器系统中使用多线程的应用。
5.TCMalloc。高性能、高扩展、高存储效率。从高速缓存中分配缓冲区以及释放缓冲区到高速缓存中时,使用线程-本地高速缓存避免锁开销。有内置的堆检查程序和堆分析工具来调试和分析动态存储使用。它是开源的。
6.alloca。它是函数,原型与malloc函数相同,但它是在栈上分配存储空间。在函数返回时,会自动释放它使用的栈。它增加了栈长度,而某些系统在函数被调用后不能再增加栈长度,也就不支持该函数。

对于环境变量name=value,UNIX内核并不查看它,它们的解释完全取决于各个应用。如shell的环境变量。

ISO C定义了函数getenv:
在这里插入图片描述
它返回字符串中的value部分,应使用它而非environ取环境变量值。

SUS的POSIX.1定义了某些环境变量,如支持XSI扩展,也会包含另外一些环境变量定义,下图中POSIX.1定义的环境变量标为*,否则为XSI扩展:
在这里插入图片描述
而ISO C没有定义环境变量。

设置环境变量值只能影响当前进程和其后生成和调用的任何子进程环境,不能影响父进程环境。但并不是所有系统都能修改或新增环境变量,以下是各种系统对环境表函数的支持:
在这里插入图片描述
clearenv函数不是SUS组成部分,它用来清除所有环境表中项。

在这里插入图片描述
函数putenv的参数形式为name=value,将其放到环境表,如果name在环境表中已存在,则先删除原来定义。

setenv函数将name的值设为value,如环境中name已存在,那么当参数rewrite非0时,会更新其现有定义,若参数rewrite为0,则不设为新值,且不出错。

unsetenv函数将name表示的环境变量删除,即使不存在指定环境变量也不报错。

putenv函数直接将传给它的字符串放到环境中(不会拷贝一份值);而setenv函数会分配存储空间(拷贝一份值),以便存放环境变量。因此将存放在栈内存中的字符串传给函数putenv时会发生错误,因为函数返回后,栈的存储区可能被重用。

环境表是字符串指针数组,其中的字符串是环境变量,环境表和环境变量通常都存在进程存储空间的顶部(栈之上),删除一个环境变量只需在环境表找到该指针,然后将后续指针都向环境表首部顺次移动一个位置。而增加或修改一个字符串困难得多,环境表和环境变量在进程空间顶部,它不能再向高地址扩展,同时也不能移动它之下的栈帧,这使得该空间长度不能增加。

修改一个name的value:
1.如新value长度小于等于现有value长度,直接将新value覆盖旧的即可。
2.如新value长大于原长,必须调用malloc为新串分配空间,将新串放到新空间并使环境表指针指向它。

增加新name,要用malloc为name=value串分配空间:
1.如这是第一次增加新name,则要调用malloc为新指针表分配空间,再将原来环境表复制到新分区(此时有两个环境表),在新表尾添加指向name=value串的指针,再将一个空指针放在其后作为新表尾,最后将environ指针指向新表。如果原来环境表在栈顶上(一般是这样),则必须将此表移到堆中,但此表中大多指针仍指向栈顶上的各name=value串。
2.不是第一次新增时,调用realloc分配比原指针表多存放一个指针的空间,新增指针空间存放指向新name=value的字符串指针。

C中goto语句不能跨函数,但setjmp和longjmp函数可以:
在这里插入图片描述
在希望返回的地方调用setjmp,返回值为0,它的参数env是特殊类型jmp_buf,这一类型中存放调用longjmp时用来恢复状态的所有信息,由于需要在返回函数longjmp中使用env变量,所以通常将env变量设置为全局变量。要返回时,调用longjmp,它的第一个参数是setenv时用的env参数,第二个参数是非0值,它成为从setjmp被调用处返回的值,因为可能会从多个位置返回,因此需要该参数。

#include <iostream>
#include <setjmp.h>
using namespace std;

jmp_buf env;

void func() {
    longjmp(env, 1);
}

int main() {
    int status = setjmp(env);
    if (status == 1) {
        cout << "jump from func" << endl;
    } else if (status == 0) {
        cout << "call func" << endl;
        func();
    }
}

以上函数输出:
在这里插入图片描述
上述代码中,在longjmp之后,main函数中的自动变量和寄存器变量值会不会被回滚到调用setjmp时的值取决于具体实现,它们的值是不确定的。如果有一个不想回滚到第一次调用setjmp时的值的自动变量,将它定义为volatile的。全局变量和静态变量在调用longjmp时保持不变。

测试调用longjmp后,各种变量的情况:

#include <iostream>
#include <setjmp.h>
#include <stdlib.h>
using namespace std;

static jmp_buf jmpbuffer;
static int globval;

static void f2() {
    longjmp(jmpbuffer, 1);
}

static void f1(int i, int j, int k, int l) {
    cout << "in f1():\nglobval = " << globval << ", autoval = " << i << ", regival = " << j << ", volaval = " << k << ", statval = " << l << endl;
    f2();
}

int main() {
    int autoval;
    register int regival;
    volatile int volaval;
    static int statval;

    globval = 1; 
    autoval = 2;
    regival = 3;
    volaval = 4;
    statval = 5;

    if (setjmp(jmpbuffer) != 0) {
        cout << "after longjmp:\nglobval = " << globval << ", autoval = " << autoval << ", regival = " << regival << ", volaval = " << volaval << ", statval = " << statval << endl;
        exit(0);
    }

    globval = 95; 
    autoval = 96;
    regival = 97;
    volaval = 98;
    statval = 99;
    
    f1(autoval, regival, volaval, statval);
    exit(0);
}

运行它:
在这里插入图片描述
如上,全局、静态、volatile对象不受优化的影响,调用longjmp之后,它们的值还是最近呈现的值。编译器的优化的作用是根据上下文做出优化,比如:

int i = 0;
if (i == 0) { }

编译器会将上述代码中的if优化掉,因为根据上下文i本来就是0,这在单线程或i不会被其他因素改变时是正确的,但有时变量值会被其他因素修改(如多线程以及上例调用setjmp后,longjmp前),如果变量的值又被修改,此时变量的值应该是修改后的值,但由于编译器优化,导致if被优化掉。而加了volatile之后,每次用到i时,都会再读一次内存以验证i是否还是0,因此,上例的volatile的局部对象没有被优化。

ISO C规定函数printf可以这样用:

printf("string1""string2");
printf("string1string2");    // 两者等价

函数不能返回局部对象的引用或指针。

可以用以下函数查询和修改进程的部分资源限制:
在这里插入图片描述
如上图,出错时一般返回-1。

进程的资源限制通常是在系统初始化时由0进程建立的,然后由后续进程继承。

每次调用以上两个函数都需要一个资源名和一个指向下列结构的指针:

struct rlimit {
    rlim_t rlim_cur;    // 软限制值,当前限制
    rlim_t rlim_max;    // 硬限制值,最大值
};

更改资源的限制:
1.任一进程都能将软限制改为小于等于硬限制的值。
2.任一进程都能降低硬限制值,但其必须大于等于软限制值,这种降低对普通用户而言是不可逆的。
3.只有root才能提高硬限制值。

RLIM_INFINITY常量指定了一个无限量的限制。

以上函数的resource参数:
1.RLIMIT_AS:进程总存储空间最大字节。影响sbrk函数和mmap函数。
2.RLIMIT_CORE:core文件最大字节数,值为0时阻止创建core文件。
3.RLIMIT_CPU:CPU时间最大量值(单位为秒),超过此值时,向进程发送SIGXCPU信号。
4.RLIMIT_DATA:数据段最大字节长度,即初始化数据、未初始化数据(bss)及堆的总和。
5.RLIMIT_FSIZE:可创建文件的最大字节长度,超过时向进程发送SIGXFSZ信号。
6.RLIMIT_MEMLOCK:进程使用mlock函数能锁定的存储空间中的最大字节长度。
7.RLIMIT_MSGQUEUE:为POSIX消息队列可分配的最大存储字节数。
8.RLIMIT_NICE:nice值(影响进程的调度优先级)可设置的最大值。
9.RLIMIT_NOFILE:每个进程能打开的最大文件数。更改它会影响sysconf函数返回的_SC_OPEN_MAX配置值。
10.RLIMIT_NPROC:每个实际用户ID可拥有的最大子进程数,会影响到sysconf函数返回的_SC_CHILD_MAX配置值。
11.RLIMIT_NPTS:用户可打开的伪终端的最大数量。
12.RLIMIT_RSS:最大驻内存集字节长度(resident set size in bytes,RSS),如果可用的物理存储器非常少,则内核从进程处收回超过RSS的部分。
13.RLIMIT_SBSIZE:一个用户可以占用的套接字缓冲区的最大长度(字节)。
14.RLIMIT_SIGPENDING:一个进程可排队的信号最大数量,此限制由sigqueue函数实施(enforced)。
15.RLIMIT_STACK:栈的最大字节长度。
16.RLIMIT_SWAP:最大交换空间字节数。
17.RLIMIT_VMEM:RLIMIT_AS的同义词。

资源限制影响到调用进程和其子进程,为影响一个用户的所有后续进程,需将资源限制的设置构造在shell中,很多shell有内置的ulimit命令或limit命令,umask和chdir也是shell内置的。

打印资源限制值:

#include <iostream>
#include <sys/resource.h>
#include <stdlib.h>
using namespace std;

#define doit(name) pr_limits(#name, name)

static void pr_limits(char *, int);

int main() {
    #ifdef RLIMIT_AS
        doit(RLIMIT_AS);
    #endif

    doit(RLIMIT_CORE);
    exit(0);
}

static void pr_limits(char *name, int resource) {
    struct rlimit limit;
    unsigned long long lim;    // 使用unsigned long long表示,有的系统用unsigned long表示,用ull只用处理一种格式

    if (getrlimit(resource, &limit) < 0) {
        cout << "getrlimit error for" << name << endl;
        exit(1);
    }

    cout << name << " ";
    
    if (limit.rlim_cur == RLIM_INFINITY) {
        cout << "(infinity) ";
    } else {
        lim = limit.rlim_cur;
        cout << lim << " ";
    }

    if (limit.rlim_max == RLIM_INFINITY) {
        cout << "(infinity) ";
    } else {
        lim = limit.rlim_max;
        cout << lim << ends;
    }
    cout << endl;
}

以上代码的宏定义使用了ISO C的字符串创建算符#,作用为:

doit(RLIMIT_CORE);
// 等价于:
pr_limits("RLIMIT_CORE", RLIMIT_CORE);

运行它:
在这里插入图片描述
参数表和环境都不是由内核解释的,但内核起到了从exec的调用者将这两者传递给新进程的作用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值