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种为正常中止,为:

  1. 从main函数中return返回
  2. 调用exit()函数
  3. 调用_exit()函数或_Exit()
  4. 最后一个线程从其启动例程返回
  5. 从最后一个线程调用pthread_exit()

3种为异常中止:

  1. 调用abort()
  2. 接到一个信号
  3. 最后一个线程对取消请求做出相应

​ 在前面提到的在启动例程(也就是程序运行时最开始执行的一段代码)中,编写了使得在 main 函数返回后立即调用 exit 函数的代码。

​ 也就是说,当程序执行完 main 函数中的所有代码后,会立即跳转到 exit 函数执行相应的操作。一般情况下,exit 函数会结束程序的运行并返回一个指定的退出码,用于告诉操作系统当前程序的运行状态。

​ 启动例程的编写常常使用汇编语言编写,若换成c语言代码的形式表示,上述的启动例程的一段可表示为

1
exit(main(argc, argv));

退出函数

Linux下有3个函数用于正常终止一个函数,_exit()_Exit()立即进入内核,eixt()则先执行一些清理处理,然后才返回内核。

1
2
3
4
5
6
7
// 使用不同头文件的原因是exit 和_Exit 是由ISOC说明的,而_exit 是由POSIX1说明的。
#include <stdlib.h>
void exit(int status);
void _Exit(int status);

#include <unistd.h>
void _exit(int status);

​ exit()函数总是执行一个标准I/O库的清理关闭操作:对于所有已经打开的流调用fclose()函数,使得输出缓冲中的数据都被冲洗(写到文件上去)。

​ 3个函数都有一个相同的整型形参,称之为终止状态(也称退出状态,exit status)。大多数的Unix系统都提供检查进程终止状态的方法。

​ 例如,在执行一个可执行文件后,使用echo $?命令查看进程终止状态。

1
2
3
4
5
6
7
8
# 执行完一个可执行文件后
$ ./a.out
# 执行结果
hello world
# 查看进程终止码
$ echo $?
# 打印进程终止状态
0

在以下情况下,进程的终止状态是随机的,即中止码是随机的,在不同的系统上编译该程序执行后,可能得到不同的终止码。这取决于main函数返回是栈和寄存器的内容:

  1. 调用这三种终止函数时不带终止状态
  2. main函数执行了一个无返回值的return语句
  3. 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
2
#include <stdlib.h>
int atexit(void (*func)(void)); // 返回值:success -> 0; error -> !0

使用例程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>

void hello()
{
printf("hello ");
}

void world()
{
printf("world!\n");
}

int main (int argc, char **argv)
{
printf("主程序先运行\n");
atexit(world);
atexit(hello);
printf("主程序运行结束\n");
return 0;
}

atexit()函数使用示例

这里可以观察到两个现象

  • atexit()注册的函数是在程序结束,执行exit()后调用的
  • atexit()注册的函数类似栈,后入先出,先注册的函数后执行
  • 还有一个特性是一个函数可以被多次注册,然后多次调用,这里没有演示

ISOC要求,系统至少应支持 32 个终止处理程序,但实现经常会提供更多的支持。
为了确定一个给定的平台支持的最大终止处理程序数,可以使用 sysconf 函数。

​ 根据ISO C和POSIX.1,exit 首先调用各终止处理程序,然后关闭(通过 fclose)所有打开流。POSIX.1扩展了ISO C 标准,它说明,如若程序调用 exec 函数族中的任一函数,则将清除所有已安装的终止处理程序。即调用 exec 函数族创建的新的进程不继承已安装好的终止处理程序。需要注意的是,这只影响通过exec函数族创建的新进程,而不影响调用exec函数族的原始进程本身。

exec函数族用于执行新的程序,它会将当前进程替换为一个新的程序。当调用exec函数族中的任意一个函数时,新程序会完全取代原来的程序,包括进程的代码、数据、堆栈等。因此,新的进程会以全新的状态开始运行,不会继承原来进程中的任何属性,包括已安装的终止处理程序。

​ 下图显示了一个C 程序是如何启动的,以及它终止的各种方式

C 程序是如何启动的,以及它终止的各种方式

​ 注意,内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式地(通过调用exit)调用_exit_Exit。进程也可非自愿地由一个信号使其终止(图中没有显示)。