首页 > 代码库 > Linux-进程描述(5)之进程环境

Linux-进程描述(5)之进程环境

main函数和启动例程

当内核使用一个exec函数执行C程序时,在调用main函数之前先调用一个特殊的启动例程,可执行程序将此例程指定为程序的起始地址。启动例程从内核获取命令行参数和环境变量,然后为调用main函数做好准备。
我们常用gcc main.c -o main命令编译一个程序,其实也可以分三步做,第一步生成汇编代码,第二步生成目标文件,第三步生成可执行文件:

1 $ gcc -S main.c
2 $ gcc -c main.s
3 $ gcc main.o

-S 选项生成汇编代码, -c 选项生成目标文件,此外 -E 选项只做预处理而不编译,如果不加这些选项则 gcc 执行完整的编译步骤,直到最后链接生成可执行文件为止。gcc命令的选项图技术分享这些选项都可以和 -o 搭配使用,给输出的文件重新命名而不使用 gcc 默认的文件名( xxx.c、 xxx.s 、 xxx.o 和 a.out ),例如 gcc main.o -o main 将 main.o 链接成可执行文件 main 。

如果我们用gcc做链接,gcc其实是调用ld将目标文件crt1.o和我们的hello.o链接在一起。crt1.o里面已经提供了_start入口点,我们的汇编程序中再实现一个_start就是多重定义了,链接器不知道该用哪个,只好报错。另外,crt1.o提供的_start需要调用main函数,而我们的汇编程序中没有实现main函数,所以报错。
 如果目标文件是由C代码编译生成的,用gcc做链接就没错了,整个程序的入口点是crt1.o中提供的_start,它首先做一些初始化工作(以下称为启动例程,Startup Routin),然后调用C代码中提供的main函数。所以,以前我们说main函数是程序的入口点其实不准确,_start才是真正的入口点,而main函数是被_start调用的。
 main函数最标准的原型应该是int main(int argc, char *argv[]),也就是说启动例程会传两个参数给main函数,这两个参数的含义我们学了指针以后再解释。我们到目前为止都把main函数的原型写成int main(void),这也是C标准允许的,如果你认真分析了上一节的习题,你就应该知道,多传了参数而不用是没有问题的,少传了参数却用了则会出问题。
由于 main 函数是被启动例程调用的,所以从 main 函数 return 时仍返回到启动例程中, main 函数的返回值被启动例程得到,如果将启动例程表示成等价的C代码(实际上启动例程一般是直接用汇编写的),则它调用 main 函数的形式是:
1 exit(main(argc, argv));

也就是说,启动例程得到 main 函数的返回值后,会立刻用它做参数调用 exit 函数。 exit也是 libc 中的函数,它首先做一些清理工作,然后调用上一章讲过的 _exit 系统调用终止进程, main 函数的返回值最终被传给 _exit 系统调用,成为进程的退出状态。我们也可以在 main 函数中直接调用 exit 函数终止进程而不返回到启动例程,例如:

1 #include <stdlib.h>
2 
3 int main(void)
4 {
5     exit(4);
6 }

这样和 int main(void) { return 4; } 的效果是一样的。在Shell中运行这个程序并查看它的退出状态:

1 ./a.out 
2 echo $?
3 4

按照惯例,退出状态为0表示程序执行成功,退出状态非0表示出错。注意,退出状态只有8位,而且被Shell解释成无符号数,如果将上面的代码改为 exit(-1); 或 return -1; ,则运行结果为

./a.out 
echo $?
255

注意,如果声明一个函数的返回值类型是 int ,函数中每个分支控制流程必须写 return 语句指定返回值,如果缺了 return 则返回值不确定(想想这是为什么),编译器通常是会报警告的,但如果某个分支控制流程调用了 exit 或 _exit 而不写 return ,编译器是允许的,因为它都没有机会返回了,指不指定返回值也就无所谓了。使用 exit 函数需要包含头文件 stdlib.h,而使用 _exit 函数需要包含头文件 unistd.h 。

进程终止

进程终止的方式有8种,前5种为正常终止,后三种为异常终止:
1 从main函数返回;
2 调用exit函数;
3 调用_exit或_Exit;
4 最后一个线程从启动例程返回;
5 最后一个线程调用pthread_exit;
6 调用abort函数;
7 接到一个信号并终止;
8 最后一个线程对取消请求做出响应。

(1) exit函数

1 #include <stdlib.h>
2 void exit( int status );
3 void _Exit( int status );
4 #include <unistd.h>
5 void _exit( int status );

这三个函数用于正常终止一个程序, _exit和_Exit立即进入内核,而exit则要先做一些清理工作(调用执行各终止处理程序,关闭所有标准I/O流),再进入内核。三个函数所带的整型参数称为终止状态或退出状态,如果(a)调用这些函数不带参数,(b) main函数中的return语句无返回值,(c) main函数没有声明返回类型为整型,则进程的终止状态是未定义的。 main函数返回一个整型值与用该值调用exit是等价的。

函数名: exit()
所在头文件:stdlib.h(如果是”VC6.0“的话头文件为:windows.h)
功 能: 关闭所有文件,终止正在执行的进程。
exit(0)表示正常退出,exit(x)(x不为0)都表示异常退出,这个x是返回给操作系统(包括UNIX,Linux,和MS DOS)的,以供其他程序使用。
stdlib.h: void exit(int status);//参 数status,程序退出的返回值。
_exit()与exit的区别:
头文件:exit:#include<stdlib.h>   _exit:#include<unistd.h>
_exit()函数:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。exit()函数与_exit()函数最大的区别就在于 exit()函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件。

exit()退出程序过程

1.调用atexit()注册的函数(出口函数);按ATEXIT注册时相反的顺序调用所有由它注册的函数,这使得我们可以指定在程序终止时执行自己的清理动作。例如,保存程序状态信息于某个文件,解开对共享数据库上的锁等。
2.cleanup();关闭所有打开的流,这将导致写所有被缓冲的输出,删除用TMPFILE函数建立的所有临时文件。
3.最后调用_exit()函数终止进程。
  _exit做3件事(man):
  1,属于此过程的任何打开文件描述符都已关闭;
  2,进程的任何子进程由进程1继承,初始化;
  3,这个过程父进程发送SIGCHLD信号。
  exit执行完清理工作后就调用_exit来终止进程。

程序示例

 1 #include<stdlib.h>
 2 #include<conio.h>
 3 #include<stdio.h>
 4 int main(int argc,char*argv[])
 5 {
 6 int status;
 7 printf("Enter either 1 or 2\n");
 8 status=getch();
 9 /*Sets DOS error level*/
10 exit(status-0);
11 /*Note:this line is never reached*/
12 return 0;
13 }

exit()和return的区别:

按照ANSI C,在最初调用的main()中使用return和exit()的效果相同。但要注意这里所说的是“最初调用”。如果main()在一个递归程序中,exit()仍然会终止程序;但return将控制权移交给递归的前一级,直到最初的那一级,此时return才会终止程序。return和exit()的另一个区别在于,即使在除main()之外的函数中调用exit(),它也将终止程序。

(2) atexit函数

函数名: atexit
头文件:#include<stdlib.h>
功 能: 注册终止函数(即main执行结束后调用的函数)
用 法: void atexit(void (*func)(void));
注意:exit调用这些注册函数的顺序与它们 登记时候的顺序相反。同一个函数如若登记多次,则也会被调用多次。
一个进程可以登记若干个个函数,这些函数由exit自动调用,这些函数被称为终止处理函数, atexit函数可以登记这些函数。 exit调用终止处理函数的顺序和atexit登记的顺序相反,如果一个函数被多次登记,也会被多次调用。按照ISO C的规定,一个进程可以登记至少32个函数,这些函数将由exit自动调用。atexit()注册的函数类型应为不接受任何参数的void函数。
下面是注册了三个函数的程序示例:
 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 void func1(void)
 4 {
 5         printf("in func1\n");
 6 }
 7 void func2(void)
 8 {
 9         printf("in func2\n");
10 }
11 void func3(void)
12 {
13         printf("in func3\n");
14 }
15 int main()
16 {
17         atexit(func3);
18         atexit(func2);
19         atexit(func1);
20 sleep(5);
20 printf("In main\n"); 21 exit(0); 22 }

技术分享

技术分享

 过程分析:atexit()函数先注册三个func()函数,然后等待5秒,再打印“int main”(如果main()函数输出部分后面没有“\n”,则main()函数要输出的内容会先放到标准输出缓冲区中,当main()中调用exit()函数的时候,会做一些自身清理工作,同时刷新缓冲区的内容),当执行到exit(0)时,exit()会自动调用这些已注册的函数,但是由于压栈的过程中先入后出的原则,所以先注册的函数最后执行。

一个进程可以登记多达32个函数,这些函数将由exit自动调用,通常这32个函数被称为终止处理程序,并调用atexit函数来登记这些函数,atexit的参数是一个函数地址,当调用此函数时无须传递任何参数,该函数也不能返回值,atexit函数称为终止处理程序注册程序,注册完成以后,当函数终止是exit()函数会主动的调用前面注册的各个函数,但是exit函数调用这些函数的顺序于这些函数登记的顺序是相反的,我认为这实质上是参数压栈造成的,参数由于压栈顺序而先入后出。同时如果一个函数被多次登记,那么该函数也将多次的执行。

exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且关闭通过标准I/O函数tmpfile()创建的临时文件。

exit()函数用于在程序运行的过程中随时结束程序,exit的参数state是返回给操作系统,返回0表示程序正常结束,非0表示程序非正常结束。

环境表

每个程序都会收到一张环境表, 环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,环境指针environ是一个全局变量,指向指针数组的地址。通常用getenv和putenv函数来访问特定的环境变量,而不是environ全局变量。如果要查看整个环境,则必须用environ全局变量。

C程序的存储空间布局

正文段:CUP执行的机器指令部分,是共享和只读的。
初始化数据段:又称作数据段,包含了程序中明确需要赋初值的变量。
非初始化数据段:在程序开始执行前,内核将此段中的数据初始化为0或空指针。
栈:自动变量以及每次函数调用时所需保存的数据都存放在此段中。
堆:用于动态存储分配。堆位于栈和非初始化数据段之间。

存储器分配

#include <stdlib.h>
void *malloc( size_t size );
void *calloc( size_t nobj, size_t size );
void *realloc( void *ptr, size_t newsize );
void free( void *ptr );
malloc函数分配指定字节数的存储区,该存储区中的初始值不确定; calloc函数为指定数量且指定长度的对象分配存储空间,该空间中的每一位都初始化为0; realloc函数更改存储区的长度(增加或减少),新增区域内的初始值不确定,如果ptr为空, realloc和malloc的功能相同。
以上函数的大多数实现所分配的存储空间都比所要求的要大一些,额外的空间用来存储管理信息。如果在一个超过已分配区的尾端进行写操作,就会重写下一个分配区的管理记录;同样,在一个已分配区的起始位置之前写入,会重写本分配区的管理记录。这种错误是灾难性的,但因为不会很快暴露出来,所以很难发现。
环境变量:环境字符串的形式如: name=value,它们的解释完全取决于各个应用程序,而与内核无关。
#include <stdlib.h>
char *getenv( const char *name );
int putenv( char *str );
int setenv( const char *name, const char *value, int rewrite );
int unsetenv( const char *name );
getenv函数返回指向name=value中的value的指针; putenv函数把字符串name=value放入环境表中,如果name已经存在,则先删除原来的定义。
setenv函数将name设置为value,如果name存在且rewrite非0,则删除其现有定义,若rewrite为0,则不删除其现有定义; unsetenv函数删除name的定义,即使不存在也不会出错。

setjmp和longjmp

#include <setjmp.h>
int setjmp( jmp_buf env );
void longjmp( jmp_buf env, int val );
setjmp和longjmp函数用于处理发生在深层次函数调用中的出错情况longjmp函数可以在栈上跳过若干个调用帧,返回到当前函数调用路径上的某个函数中。在希望返回到的位置调用setjmp,数据类型jmp_buf是某种形式的数组,存放在调用longjmp时能用来恢复栈状态的所有信息。因为需要在另一个函数中引用env变量,所以将env定义为全局变量。当检查到一个错误时,调用longjmp函数,第一个参数env就是在调用setjmp时所用的env,第二个参数val非0,它将成为从setjmp处返回的值。使用第二个参数的原因是一个setjmp可以对应多个longjmp,这样就可以根据返回值来判断造成返回的longjmp函数在那个函数中,从而确定出错的位置。

getrlimit和setrlimit函数

#include <sys/resource.h>
int getrlimit( int resource, struct rlimit *rlptr );
int setrlimit( int resource, const struct rlimit *rlptr );
getrlimit和setrlimit函数用于获取或设置进程的资源限制。资源限制通常是由进程0建立的,由每个后续进程继承。更改资源限制时,注意以下三条规则:
1 进程的软限制值只能用于或等于硬限制值;
2 任意进程都可以降低其硬限制值,但它必须用于或等于其软限制值,这种操作对普通用户是不可逆的;
3 只有超级用户进程可以提高硬限制值。
资源限制影响到调用进程并由其子进程继承,这意味着为了影响一个用户的所有进程,需要将资源限制构造在shell中。





 
 














Linux-进程描述(5)之进程环境