概述
这一部分介绍的是进程运行的环境。主要包括进程执行时,main
函数是如何被调用的,命令行参数如何传递给进程的,进程的存储空间结构,如何分配存储空间,环境变量的使用,以及进程是怎么终止的。
main
函数和argc
, argv
C语言总是从main
函数开始执行,C语言中main
有两个原型:1
2int main(void);
int main(int argc, char *argv[]);
其中argc
是命令行参数的个数,argv
是一个指针数组,ISO C和POSIX.1都要求argv[argc]
设置为NULL
,所以可以用它作为参数处理的循环终止条件。关于更多指针的信息可以查看。
当内核执行C程序(使用一个exec
函数)时,在调用main
函数之前设置一个特殊的启动例程。可执行程序文件将这个启动例程指定为程序的起始地址,这是由link editor设置的,它会被C编译器调用。启动例程从内核命令获得命令行参数和环境变量值,然后为调用main
函数做好准备。
关于更多C和C++中main
的介绍,可以查看C/C++ main argc argv。
全局变量environ
每个C程序都会接收到一个environment list,和argv
一样,它是一个指针数组。每个指针指向一个以null
结束的C字符串的地址,这个指针数组的地址存放在全局变量environ
中:1
extern char **environ;
在历史上,UNIX大多支持三个参数的main
函数,第三个参数就是environment list:1
int main(int argc, char *argv[], char *envp[]);
但是ISO C规定main
只能有两个参数,POSIX.1也就规定使用全局变量environ
而不是第三个参数。如果要查看所有的环境变量时,使用environ
,而访问某个特定的环境变量时,使用getenv
和setenv
。
进程终止
总共有八种方式可以让进程终止,包括五种正常和三种异常,前五种是正常终止,后五种是异常终止:
- 从
main
返回 - 调用
exit
- 调用
_exit
或者_Exit
- 最后一个线程从其启动例程返回
- 最后一个线程调用
pthread_exit
- 调用
abort
- 接到一个
signal
- 最后一个线程对取消请求做出响应
exit
函数
函数原型
1 |
|
性质
exit
和_Exit
是ISO C的内容,而_exit
是POSIX.1的内容- 它们都用于正常终止一个程序,
_Exit
和_exit
立刻进入内核,而exit
先执行一些清理操作,然后返回内核。exit
函数总是执行一个标准I/O库的关闭操作,对于所有打开的流调用fclose
函数,所有带有未写缓冲的标准I/O流被flush。 - 三个退出函数都需要一个整形的参数,被称为exit status。
- 如果满足以下条件:
- 调用这三个函数不带终止状态
main
执行了一个不带返回值的return
语句main
没有声明返回类型为整形,进程的终止状态是未定义的。
那么这个进程的终止状态是未定义的。
main
返回返回一个整型值和用该值调用exit
是等价的。对于某些C编译器和UNIX lint(1)程序来说,会产生警告信息,因为这些编译器并不了解main
中的return
和exit
的作用是相同的。避开这种警告信息的一种方法是在main
中使用return
而不是exit
,这样做的结果是UNIX grep命令无法找出程序中所有的exit
调用。另一个方法是将main
声明为void
而不是int
,然后调用exit
,但是这不并不是标准,ISO C和POSIX.1定义main
的返回值应当是带符号整形。
关于更多exit
函数的内容,可以查看。
关于exit
和return
的内容,更多可以查看C/C++ exit and return。
atexit
每个进程可以通过atexit
register至多32个由exit
自动调用的函数,这些函数被称为exit handler(终止处理程序)。1
2
3
int atexit(void (*function)(void));
atexit
的参数是一个函数地址,不会有返回值exit
调用atexit
register的程序的顺序和使用atexit
进行register的顺序相反。- ISO C和POSIX.1标准规定,
exit
首先调用各个exit handler,然后使用fclose
关闭所有标准I/O流。 - POSIX.1对ISO C进行了扩展,如果程序调用了任何
exec
函数,清除exit handler。 - 内核执行一个程序的唯一方法是调用一个
exec
函数。进程自愿终止的唯一办法是显式或者隐式的(通过exit
)调用_exit
和_Exit
。
C程序的存储空间布局
更多关于C程序存储空间布局可以查看C/C++ program memory layout。
共享库
共享库使得可执行文件中不再需要包含公用的库函数,只需要在所有进程都可引用的存储区保存这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,使用动态链接的方法将程序和共享库函数链接,这减少了每个可执行文件的长度,但是增加了一些时间运行开销。这种时间开销发生在程序第一次被执行时,或者每个共享库函数第一次被调用时。共享库的另一个优点是可以使用库函数的新版本代替老版本而无需对使用该库的程序重新链接和编辑。
内存空间分配
ISO C说明了三个用于memory allocation的函数,malloc
, calloc
和realloc
,它们的原型如下,更多关于C中malloc
的内容可以查看C/C++ malloc(alloc) free new and delete。
malloc
, calloc
和realloc
原型
1 |
|
malloc
, calloc
和realloc
属性
malloc
,分配指定字节的内存空间,初始值不定。calloc
,为指定长度的固定数量的对象分配空间,每一个bit都被初始化为0。realloc
,增加或者减少已经分配的内存空间的大小。当这个大小增加时,可能需要将之前分配的空间中的数据移到另一个足够大的区域以便于增加大小,新增加的区域内的值是不确定的。- 这三个函数返回的指针一定是对齐的,保证它可以用于任何对象。比如
double
的要求最严格,需要从8的倍数的地址单元开始,这三个函数返回的地址一定满足这个要求。 - 它们的返回类型都是
void*
,需要使用强制类型转换。 realloc
函数可以增加或者减少之前分配的内存空间的大小。比如分配了一个固定大小的数组,后来发小它不够用了,可以使用realloc
对它进行扩充,如果原有的存储后有足够的大小进行扩充,则可以在原存储区的位置上向高地址进行扩充,无需移动原有数组,返回和传入相同的指针。如果原来的内存空间后没有足够的空间,就重新分配一个足够大的内存空间,再将原有数据的内容复制过去,然后释放原来的内存空间,返回新的指针。realloc
传入的参数是存储区的新长度。如果传入的ptr
参数是NULL
指针,那就退化成了malloc
。free
可以释放ptr
指向的内存空间,释放的空间通常送入可用内存池,之后可以通过这三个函数重新分配。malloc
和free
底层通常使用sbrk
系统调用实现,这个系统调用扩充或者减小进程的堆,虽然sbrk
可以扩充或者缩小进程的堆,但是一般malloc
和free
的实现不会减少进程的内存空间,释放的内存空间保存在malloc
池中,而不是交给内核。- 大多数实现分配的空间要比请求的空间大一些,因为需要存储一些管理信息,如block的大小,指向下一个block的指针等等。因此,如果对超过一个分配区域的内存进行读写的话,会造成很严重的错误。
free
一个已经释放了的块,free
的不是alloc
函数的返回值,没有进行free
等等,都有可能造成很严重的后果。
环境变量
环境变量的形式是:
name = value
UNIX内核并不使用环境变量,通常都是应用程序使用这些环境变量。比如shell使用了大量的环境变量。
标准定义
ISO C定义了getenv
函数可以获取环境变量。但是ISO C没有定义任何环境变量,SUS环境变量包括POSIX.1和XSI环境变量。
除了获取环境变量,有时候我们也需要设置环境变量。ISO C没有定义获取环境变量的函数。SUS除了定义了ISO C,还定义了putenv
, setenv
和unsetenv
对环境变量进行操作。
putenv
,setenv
和unsetenv
原型
1 |
|
putenv
,setenv
和unsetenv
性质
putenv
,创建或者重置一个name
。setenv
,如果name
不存在,创建name
;如果name
存在,rewrite
不为零,重写,rewrite
为零,不进行重写。unsetenv
,移除name
的定义。如果name
不存在,不会出错。- 环境变量的修改和增加可能会遇到一些问题。因为环境变量存放在进程地址空间的最上面的一个不可扩展的空间。
如果修改一个存在的name
:
当新的value
的长度小于等于原来的value
长度时,直接覆盖就行;
当新的value
的长度大于原来的value
长度时,只需要给新的name-value
字符串分配空间就行了。使用malloc
为新的字符串分配空间,然后将该字符串复制到新的空间,让environment list中name
的指针指向新分配的存放字符串的空间即可。
如果增加一个新的name
,不仅需要给新的name-value
分配空间,指针数组的元素也增加了,还需要给指针数组分配新的空间。首先给新的name-value
字符串分配空间,调用malloc
先为字符串分配空间,然后将该字符串复制到这个空间:
如果这是第一次增加一个name
,必须调用malloc
为指针数组增加空间。将原来的指针数组复制到新分配的空间中,然后将新字符串的指针放在指针数组的尾部,然后存放一个空指针。最后让全局变量char **environ
指向这个指针数组。如果原来的指针数组存放在栈顶之上,需要将它复制到堆中。需要注意的是,这个指针数组中的未修改的环境变量的指针还是指向栈顶的字符串上。
如果这不是第一个添加name
,只需要使用realloc
重新多分配一个指针数组的空间即可,然后将它指向新字符串的地址即可。
setjmp
和longjmp
setjmp
和longjmp
原型
1 |
|
setjmp
和longjmp
属性
- 自动变量存储在每个函数的栈帧中。
setjmp
和longjmp
实在栈上跳过若干调用栈,返回到当前函数调用路径上的某个函数中。
getrlimit
和setrlimit
每个进程能使用的资源都是有限的,可以使用getrlimt
和setrlimit
进行修改。它们都是XSI扩展,不是ISO C的定义。有些资源可以设置为RLIM_INFINITY
,表示无限。
getrlimit
和setrlimit
性质
- 任何一个进程都可以将
rlim_cur
改为小于等于rlim_max
。 - 任何一个进程都可以将
rlim_max
改小,但是不能小于rlim_cur
,且这个更改是不可逆的。 - 只有root用户可以更改
rlim_max
。
它们的原型原型如下:
getrlimit
和setrlimit
原型
1 |
|
resource种类
- RLIMIT_AS
- RLIMIT_VMEM
- RLIMIT_DATA
- RLIMIT_SWAP
- RLIMIT_STACK
- RLIMIT_NPROC
- RLIMIT_FSIZE
- RLIMIT_NOFILE
- RLIMIT_NICE
- …
结构体
结构体struct rlimt
的定义如下:1
2
3
4
5struct rlimit
{
rlim_t rlim_cur; // soft limit
rlim_t rlim_max; // hard limit
}
参考文献
- 《APUE》第三版