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

原文来自静雅斋,转载请注明出处。javascript

main函数

咱们知道,不管是汇编仍是C语言仍是其余的语言,在编译成实际二进制代码的时候,都是存在着一个入口点,通常来讲,这个入口点就是main函数,C语言都是从main函数处开始执行,在Unix开发中,main函数都是长这样的java

int main(int argc, char *argv[]);复制代码

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

  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系统调用库。ide

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设置,并且资源限制影响和其余的进程属性同样是随着进程派生继承的。

相关文章
相关标签/搜索