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
函数,所以报错。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()退出程序过程
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 "); 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的区别:
(2) atexit函数
1 #include<stdio.h> 2 #include<stdlib.h> 3 void func1(void) 4 { 5 printf("in func1 "); 6 } 7 void func2(void) 8 { 9 printf("in func2 "); 10 } 11 void func3(void) 12 { 13 printf("in func3 "); 14 } 15 int main() 16 { 17 atexit(func3); 18 atexit(func2); 19 atexit(func1);
20 sleep(5); 20 printf("In main "); 21 exit(0); 22 }
过程分析:atexit()函数先注册三个func()函数,然后等待5秒,再打印“int main”(如果main()函数输出部分后面没有“ ”,则main()函数要输出的内容会先放到标准输出缓冲区中,当main()中调用exit()函数的时候,会做一些自身清理工作,同时刷新缓冲区的内容),当执行到exit(0)时,exit()会自动调用这些已注册的函数,但是由于压栈的过程中先入后出的原则,所以先注册的函数最后执行。
一个进程可以登记多达32个函数,这些函数将由exit自动调用,通常这32个函数被称为终止处理程序,并调用atexit函数来登记这些函数,atexit的参数是一个函数地址,当调用此函数时无须传递任何参数,该函数也不能返回值,atexit函数称为终止处理程序注册程序,注册完成以后,当函数终止是exit()函数会主动的调用前面注册的各个函数,但是exit函数调用这些函数的顺序于这些函数登记的顺序是相反的,我认为这实质上是参数压栈造成的,参数由于压栈顺序而先入后出。同时如果一个函数被多次登记,那么该函数也将多次的执行。exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且关闭通过标准I/O函数tmpfile()创建的临时文件。exit()函数用于在程序运行的过程中随时结束程序,exit的参数state是返回给操作系统,返回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 );
m alloc函数分配指定字节数的存储区,该存储区中的初始值不确定; 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中。