[单刷APUE系列]第七章——进程环境

山河永寂

目录

[单刷APUE系列]第一章——Unix基础知识[1]
[单刷APUE系列]第一章——Unix基础知识[2]
[单刷APUE系列]第二章——Unix标准及实现
[单刷APUE系列]第三章——文件I/O
[单刷APUE系列]第四章——文件和目录[1]
[单刷APUE系列]第四章——文件和目录[2]
[单刷APUE系列]第五章——标准I/O库
[单刷APUE系列]第六章——系统数据文件和信息
[单刷APUE系列]第七章——进程环境
[单刷APUE系列]第八章——进程控制[1]
[单刷APUE系列]第八章——进程控制[2]
[单刷APUE系列]第九章——进程关系
[单刷APUE系列]第十章——信号[1]

main函数

我们知道,无论是汇编还是C语言还是其他的语言,在编译成实际二进制代码的时候,都是存在着一个入口点,一般来说,这个入口点就是main函数,C语言都是从main函数处开始执行,在Unix开发中,main函数都是长这样的

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

一般情况下,是shell启动程序,shell都是通过内核的exec函数调用程序,但是在启动之前,需要有一个特殊程序将环境变量和启动参数传给main函数。这个特殊程序可以如下表示exit(main(argc, argv));
启动进程以后,就需要考虑进程退出了,Unix系统有8种进程终止行为。

  1. 从main返回

  2. exit函数

  3. _exit或者_Exit

  4. 最后一个线程退出

  5. 最后一个线程调用pthread_exit函数

下面的就是异常退出

  1. 调用abort

  2. 接收到一个信号

  3. 最后一个线程对取消请求作出相应

系统提供了三个函数用于退出进程

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

void _exit(int status);

前两个是标准C库中的函数,后一个是BSD系统调用库。

Before termination, exit() performs the following functions in the order listed:

  1. Call the functions registered with the atexit(3) function, in the reverse order of their registration.

  2. Flush all open output streams.

  3. Close all open streams.

  4. Unlink all files created with the tmpfile(3) function.

exit函数在退出进程之前,总是会进行以上4个步骤。另外两个则是基本一样。
这三个函数都接受一个status参数,也叫作退出状态。如果调用这三个函数不带参数,或者main函数执行return,或者main函数没有返回值,那么就不会定义返回状态。但是如果main返回值是int类型,并且有显式返回行为,那么进程返回状态就是0.

#include <stdio.h>

main(int argc, char *argv[])
{
    printf("Hello, world\n");
}

上面的代码缺少了返回值的声明,我们知道,如果没有返回值的声明,进程的返回状态就是未定义的,原著中就有了两个标准定义的对比,但是经过试验,现在的gcc和clang编译器基本都是默认支持c99标准,也就是说,会自动帮你补全返回值,所以这个也只需要了解就行。
在前面标准C库exit函数的说明中,可以了解到,exit函数在退出进程时候,会进行4个步骤,其中一个就是调用atexit函数注册的函数,并且是以注册顺序的反向来调用。

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

显而易见,就是传入一个函数指针,而且这个函数基本不用关心返回值,因为它根本就没做什么工作。

命令行参数

我们知道,内核想要执行一个程序,只有通过exec函数族,而且当启动一个进程的时候,main函数都会要求命令行参数和环境变量。前面也提到过,启动进程的时候实际上是启动了一个小程序,然后由它来传递各种参数给main函数,而开发者在实际开发的时候只需要关心main函数作为入口点的代码就行了。这里实际上很简单就只讲解一下就为止了。

环境表

每个程序都可以得到环境变量,实际上是进程接收环境表,和参数表一样,环境表实际上也是一个字符指针数组,实际上Unix系统提供了environ全局变量指向该指针数组。

extern char **environ;

数组实际上是一个指针,这个应该很容易理解,所以在这里使用了一个全局二级指针作为数组,数组的每个元素是一个字符指针,用于指向各自的字符串,然后最后就是一个null作为整个数组的结尾,所以在判断的时候只需要if (i = 0; environ[i] != NULL; ++i)即可很简单的使用。
实际上,由于环境变量的重要性,Unix系统规定main函数可以有三个参数,最后一个参数就是环境表参数,但是由于后来ISO C规定main函数只有两个参数,而且第三个参数由于environ全局变量的存在也变得毫无意义,所以POSIX规定也废弃了main的第三个参数,但是一般情况下,我们都不直接存取environ全局变量,都是使用系统提供的getenv和putenv函数来访问,但是如果想要整体查看,就需要访问environ全局变量。

C程序的存储空间布局

在前面讲解其他东西的时候笔者就略微的提到了C语言的存储布局,一直以来,C程序的布局都是沿袭了汇编的习惯,由以下部分组成

  1. 正文段。这是主要用于执行的部分,而且由于正文段的频繁读取执行,所以正文段一般都是只读的,防止误操作导致破坏或者被恶意程序窃取。前面讲解的保存正文位保存的就是这一段

  2. 初始化数据段。这一段实际上是在C语言中以明确的语句初始化的变量。例如:int i = 0;

  3. 未初始化数据段。也叫作bss段,也就是语句中没有明确赋予初值的变量。

  4. 栈。自动变量以及函数调用的场景信息都被放在这里,栈具有先进后出的特性,非常适合函数调用和变量分配空间,回收空间的行为。并且,这些都是由系统自动管理的。

  5. 堆。堆通常用于用户自行分配空间,也就是开发者通常的malloc分配。

除了这些意外,根据各个系统的不同,还存在着不同的段,但是这些都和学习Unix开发关系不大。为了查看分段信息,可以使用size命令

~/Development/Unix $ size echoarg                                                                                                                           
__TEXT    __DATA    __OBJC    others    dec    hex
4096    4096    0    4294971392    4294979584    100003000

这就是苹果系统下的情况,实际上其他Unix实现也是差不多的。

共享库

共享库是很重要的东西,基本上稍微和系统开发有关的都会接触到静态库和动态库。我们知道一个二进制程序实际上是由各个段组成,很多情况下,程序开发都会共享很多代码,所以就有了这两个链接库方式,在学习C语言编译过程的时候,就知道了编译实际上分预处理、代码生成、汇编和链接,预处理、代码生成和汇编都是只针对自己编写的代码,我们在编写代码的时候用到其他的库,当时只导入了头文件,让编译通过,至于实际代码就是在链接阶段加载,静态库和动态库区别就在于是否将实际代码链接到二进制文件。
不同的编译系统可能会有不同的参数用于说明是否要使用动态库,所以用户实际上应当参考用户手册的说明,这里就不讲述了。

存储空间分配

ISO C有三个分配存储空间的函数

void *malloc(size_t size);
void *calloc(size_t count, size_t size);
void *realloc(void *ptr, size_t size);

void free(void *ptr);

这四个函数应该每个人都用过了,不需要过多讲解。不过这三个内存分配管理函数实际上是归属标准C库的,也就是说,这并不是不可替代的东西,在底层调用上,实际都是调用sbrk系统调用,前面也讲到过,sbrk系统调用实质上是更改进程内存区域段的大小,但是不管内存的具体分配,而malloc函数族则是在逻辑层面上分配内存,当我们使用free回收内存时,实际上是依旧保留在进程的堆空间中的,并不是归还给系统,直到进程退出,所有内存被释放,内存管理问题实际上一直是老大难问题,也是追踪bug的重点,但是笔者个人非常推崇引用计数,这在C++乃至其他语言中都是很好用的方法,虽然简单,但是只要小心,一般都不会出错。
作为malloc和free的替代,实际上有不少系统和类库都提供了相关的函数,现在有libmalloc、vmalloc、quick-fit、jemalloc、tcmalloc和alloca等,但是根据性能对比,jemalloc和tcmalloc实际上是最好的两个,在实际开发中都能有效的提升内存管理的效率。

环境变量

环境变量主要的形式就是name=value键值对,对于Unix本身来说,这些东西没有任何意义,但是环境变量可以让应用程序以最快捷方便的形式进行读取信息,所以ISO C标准也规定了相关的函数

char *getenv(const char *name);
int putenv(char *string);
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);

getenv函数接收一个name指针,返回name=value中对应的value字符串指针,putenv取得形式为name=value的字符串,将其放到环境表中,如果name存在则删除原先的定义,setenv将name设置为value,overwrite参数控制是否重写,unsetenv则删除name对应的环境变量。
大家可能会奇怪putenv和setenv有什么区别,下面就是官方解释

The string pointed to by string becomes part of the environment.  A program should not alter or free the string, and should not use stack or other transient string variables as arguments to putenv().  The setenv() function is strongly preferred to putenv().

putenv函数的字符串指针直接被放到了环境表中,所以不能修改或者释放这个字符串,所以不应该使用栈或者堆分配的字符串作为参数,而setenv函数则没有这个问题。
原著中还写了有关环境表修改时的操作,但是笔者个人认为并没有必要在这里继续讲,所以各位可以看看原著,里面写的已经足够了。

跳转

goto语句应该都知道,在C语言中,goto语句是不能超过函数的,所以系统也提供了两个函数用于执行跳转

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

在汇编语言中,曾经有过这么一个概念,地址分为段地址和偏移地址,8086芯片是20位地址编码,然后它将其分为这两种地址,然后可以寻址1m地址空间,这里也有个长跳转和短跳转,当然,这里就讲讲C语言中两种跳转,在实际开发中,我们经常遇到函数持续嵌套调用,然后栈中有一堆的保存状态,结果在栈顶的函数发现一个错误,那么可能需要打印一个出错信息,然后返回main函数,但是如果嵌套过多,我们就不得不不断的一层层检查返回值,最终回到main函数,这个时候,就是非局部跳转起作用的时候了。
setjmp在调用的位置保存调用环境到env参数中,然后会返回0。longjmp函数需要两个参数,一个就是先前调用的时候保存的env环境,第二个是具非0的val,这将成为重新开始点的返回值,根据返回值就能简单的判断出错了,而且还不需要经过层层的栈返回。

All accessible objects have values as of the time longjmp() routine was called, except that the values of objects of automatic storage invocation duration that do not have the volatile type and have been changed between the setjmp() invocation and longjmp() call are indeterminate.

在苹果系统下的声明就是如上所示,只有volatile类型的自动变量会被不回滚,其他自动变量都会被不确定的值回滚,而且由于不同的系统的实现不同,所以实际使用也极为困难。

资源限制

资源是有限的,所以每个进程实际上都有固定的资源限制,系统提供了两个函数用于查询一些限制

int getrlimit(int resource, struct rlimit *rlp);
int setrlimit(int resource, const struct rlimit *rlp);

     The resource parameter is one of the following:

     RLIMIT_CORE     The largest size (in bytes) core file that may be created.

     RLIMIT_CPU      The maximum amount of cpu time (in seconds) to be used by each process.

     RLIMIT_DATA     The maximum size (in bytes) of the data segment for a process; this defines how far a program may extend its break with the sbrk(2) system
                     call.

     RLIMIT_FSIZE    The largest size (in bytes) file that may be created.

     RLIMIT_MEMLOCK  The maximum size (in bytes) which a process may lock into memory using the mlock(2) function.

     RLIMIT_NOFILE   The maximum number of open files for this process.

     RLIMIT_NPROC    The maximum number of simultaneous processes for this user id.

     RLIMIT_RSS      The maximum size (in bytes) to which a process's resident set size may grow.  This imposes a limit on the amount of physical memory to be
                     given to a process; if memory is tight, the system will prefer to take memory from processes that are exceeding their declared resident set
                     size.

     RLIMIT_STACK    The maximum size (in bytes) of the stack segment for a process; this defines how far a program's stack segment may be extended.  Stack exten-
                     sion is performed automatically by the system.

rlimit的结构体长这样

struct rlimit {
    rlim_t  rlim_cur;       /* current (soft) limit */
    rlim_t  rlim_max;       /* hard limit */
};

而上面说明中的就是能获取或者修改的资源的名称,实际上进程只能调小自身限额,而不能提高限额,只有root权限才能提升限额,所以一般很少用setrlimit设置,而且资源限制影响和其他的进程属性一样是随着进程派生继承的。

阅读 3.2k

静雅斋
码代码|Minecraft|Node.jser&PHPer|iOS移动端开发者|Web前端码农
2.4k 声望
157 粉丝
0 条评论
2.4k 声望
157 粉丝
文章目录
宣传栏