Linux APUE学习:3、C程序的启动和终止
Linux APUE学习:3、C程序的启动和终止
学习进程前,线应该了解进程环境,比如main函数是怎么被调用的、命令行参数是怎么传递给新程序的、典型的存储空间布局、如何分配另外的存储空间、进程如何使用环境变量、进程的各种不同中止方式。
main函数
C语言总是从main函数开始执行。main函数的原型是
1 | int main(int argc, char *argv[]); |
argc
是命令行参数的数目,argv
是指向参数的各个指针所构成的数组。
当内核执行c程序时(使用了一个exec函数),在调用main前先调用一个特殊的启动例程(也称为启动代码)。可执行文件将此启动例程指定为程序的起始地址——这是由连接器设置的。
这个启动例程被执行前,可执行文件需要经过连接器的处理,以将各个模块的目标文件链接起来生成可执行文件。在链接过程中,连接器会将启动例程的地址设置为程序的起始地址,这样操作系统就能够通过该地址开始执行程序。
在启动例程中,通常会进行一些初始化工作,例如设置堆栈、初始化全局变量等操作。然后,启动例程会跳转到main函数的入口处,开始执行程序的主要逻辑。
启动例程从内核取得命令行参数和环境变量值,为调用main函数做好安排。
进程终止
一共有8种方式使进程终止,其中5种为正常中止,为:
- 从main函数中return返回
- 调用
exit()
函数 - 调用
_exit()
函数或_Exit()
- 最后一个线程从其启动例程返回
- 从最后一个线程调用
pthread_exit()
3种为异常中止:
- 调用
abort()
- 接到一个信号
- 最后一个线程对取消请求做出相应
在前面提到的在启动例程(也就是程序运行时最开始执行的一段代码)中,编写了使得在
main
函数返回后立即调用exit
函数的代码。 也就是说,当程序执行完
main
函数中的所有代码后,会立即跳转到exit
函数执行相应的操作。一般情况下,exit
函数会结束程序的运行并返回一个指定的退出码,用于告诉操作系统当前程序的运行状态。 启动例程的编写常常使用汇编语言编写,若换成c语言代码的形式表示,上述的启动例程的一段可表示为
1 exit(main(argc, argv));
退出函数
Linux下有3个函数用于正常终止一个函数,_exit()
和_Exit()
立即进入内核,eixt()
则先执行一些清理处理,然后才返回内核。
1 | // 使用不同头文件的原因是exit 和_Exit 是由ISOC说明的,而_exit 是由POSIX1说明的。 |
exit()函数总是执行一个标准I/O库的清理关闭操作:对于所有已经打开的流调用fclose()
函数,使得输出缓冲中的数据都被冲洗(写到文件上去)。
3个函数都有一个相同的整型形参,称之为终止状态(也称退出状态,exit status
)。大多数的Unix系统都提供检查进程终止状态的方法。
例如,在执行一个可执行文件后,使用echo $?
命令查看进程终止状态。
1 | # 执行完一个可执行文件后 |
在以下情况下,进程的终止状态是随机的,即中止码是随机的,在不同的系统上编译该程序执行后,可能得到不同的终止码。这取决于main函数返回是栈和寄存器的内容:
- 调用这三种终止函数时不带终止状态
- main函数执行了一个无返回值的return语句
- main没有声明返回类型为整数,则该进程的终止状态是未定义的
同样也要记住这几点:
- 其他任何普通函数在任何位置调用
return()
只会导致本函数返回,而main()
函数在任何位置调用return()
都会导致程序退出进程终止main()
函数里的return()
会调用exit()
函数,而在其他任何函数的任何位置如果调用exit()
将会导致程序退出- 若
main()
函数的返回类型是整型,即声明int main()
,并且main函数在执行到最后一条语句时返回,即使没有使用return语句或指定了无返回值的return语句,都会默认执行return 0
,终止状态为0,这称之为隐性声明。
atexit()函数
atexit()
称之为注册终止函数,它可以用来登记注册一个函数,当程序执行exit()
函数的时候,会依照atexit()
函数登记的顺序后入先出(类似于栈出栈入栈)的执行这些函数。
一个进程可以登记若干个函数,这些函数由exit自动调用,这些函数被称为终止处理函数,atexit()
函数可以登记这些函数。exit()
调用终止处理函数的顺序和atexit()
登记的顺序相反,如果一个函数被多次登记,也会被多次调用。
按照ISOC的规定,一个进程可以登记多至32 个函数,这些函数将由exit 自动调用。
1 |
|
使用例程示例
1 |
|
这里可以观察到两个现象
atexit()
注册的函数是在程序结束,执行exit()后调用的atexit()
注册的函数类似栈,后入先出,先注册的函数后执行- 还有一个特性是一个函数可以被多次注册,然后多次调用,这里没有演示
ISOC要求,系统至少应支持 32 个终止处理程序,但实现经常会提供更多的支持。
为了确定一个给定的平台支持的最大终止处理程序数,可以使用sysconf
函数。
根据ISO C和POSIX.1,exit
首先调用各终止处理程序,然后关闭(通过 fclose
)所有打开流。POSIX.1扩展了ISO C 标准,它说明,如若程序调用 exec
函数族中的任一函数,则将清除所有已安装的终止处理程序。即调用 exec
函数族创建的新的进程不继承已安装好的终止处理程序。需要注意的是,这只影响通过exec
函数族创建的新进程,而不影响调用exec
函数族的原始进程本身。
exec
函数族用于执行新的程序,它会将当前进程替换为一个新的程序。当调用exec
函数族中的任意一个函数时,新程序会完全取代原来的程序,包括进程的代码、数据、堆栈等。因此,新的进程会以全新的状态开始运行,不会继承原来进程中的任何属性,包括已安装的终止处理程序。
下图显示了一个C 程序是如何启动的,以及它终止的各种方式
注意,内核使程序执行的唯一方法是调用一个exec
函数。进程自愿终止的唯一方法是显式或隐式地(通过调用exit
)调用_exit
或_Exit
。进程也可非自愿地由一个信号使其终止(图中没有显示)。