进程的启动和终止
内核执行c程序时,利用exec函数调用一个特殊的启动例程,该启动例程丛内核中获取命令行参数和环境变量值。
进程终止的情况
5种正常终止的情况:
(1)从main函数返回;(2)调用exit;(3)调用_exit和_Exit函数;(4)最后一个线程调用pthread_exit;(5)最后一个线程从其启动例程返回;
3种异常终止情况
(1)调用abort;(2)接到一个信号;(3)最后一个线程对取消请求做出响应;
进程启动和终止图
atexit函数
一个进程最多可以登记32和函数(例如:signal函数),这些函数由exit函数自动调用。在程序终止时调用这些函数,形成终止处理程序,来进行结束进程前的收尾工作。而exit函数通过atexit函数的登记记录来判断调用哪些函数。
exit函数
此函数由ISO C 定义,其操作包括处理终止处理程序,然后关闭所有标准I/O流。需要注意的是,它不会处理文件描述符、多进程(父子进程)以及作业控制。
_e(E)xit函数
ISO C 定义这个函数的目的是为进程提供一种无需运行终止处理程序或信号处理函数的方法而终止程序。但ISO C 对标准I/O流是否进行冲洗,这取决于操作系统的实现。在unix中,是不进行冲洗的。
exit和_e(E)ixt函数的状态码
无论进程怎样结束,它都会在内核上执行同一段代码(由进程启动和退出图可知)。这段代码来关闭所有的文件描述符,释放所有的存储空间。
程序退出后,利用退出码告知该进程的父进程。父进程通过wait或waitpid函数来完成该子进程的善后工作(获取子进程相关信息 释放子进程占用资源)。若父进程没有处理子进程的退出状态,则子进程变成僵死进程。相反的,若父进程在子进程前终止,则子进程变成孤儿进程。孤儿进程会由1号进程(init进程)接收,大致过程如下:
(1)进程终止时,内核逐个检查所有活动的进程;(2)分析查找该终止进程的子进程;(3)将该进程的子进程的父进程ID改为1;
wait和waitpid函数
程序正常或异常终止时,内核都会向父进程发送SIGNAL信号。子进程终止是异步事件,所以该信号也是异步信号。而该信号一般会被父进程默认忽略。或者提供一个信号处理函数来善后。wait和waitpid函数就是其中的信号处理函数的一部分。
wait和waitpid函数区别如下:
(1)wait会阻塞调用者进程等待直至第一个终止的子进程到来;(2)waitpid可以通过参数设置,来实现调用者进程不阻塞,或选择要阻塞等待的子进程;
这里的调用者指的是父进程
环境表和环境变量
环境表结构图
每个程序都接收到一张环境表
环境表也是一个字符指针数组
enrivon叫做环境指针
指针数组叫做环境表
各个指针指向的字符串叫做环境字符串
环境变量
unix内核并不检查环境字符串,它们的解释完全取决于各个应用进程
通常在一个shell启动文件中设置环境变量来控制shell的动作
修改或者增加环境变量时,只能影响当前进程以及其后(之前的不行)生成和调用的任何子进程的环境,但不能影响其父进程的环境
和环境变量相关的函数如下:
#includechar *getenv(const char *name); 返回值:指向与name关联的value的指针;若未找到,返回NULLint putenv(char *str); 返回值:若成功,返回0;若出错,返回非0 int setenv(const char *name, const char *value, int rewrite);int unsetenv(const char *name); 两个函数返回值:若成功,返回0;若出错,返回-1
这些函数如何修改环境表的
环境表和环境字符串通常存放在内存空间的高地址处(顶部)。所以在修改它的值时,内存是不能继续向高地址延伸;但又因为,它之下是各个栈帧,所以也不能向下延伸。如何修改它的值的过程如下:
(1)修改环境表
1)新value <= 旧value,直接覆盖旧value的存储空间2)新value >= 旧value,调用malloc函数,在堆区开辟新的存储空间,将新value复制到这里,再将这片存储区首地址写到环境表相应的位置处。
(2)新增环境表
1)新增一个环境变量,调用malloc函数开辟新的存储空间,将原来的环境表复制到该存储区,其次再添加一个环境变量,然后在尾部赋值为NULL,最后将environ指向该区域;2)在 1)过程的基础上,调用realloc函数,多次添加环境变量;
注意:以这种方式修改的环境变量只在当下程序运行时有效,当程序结束时,相应的存储区被系统回收,这些修改就会失效。
内存存储结构补充说明
内存管理结构图
未初始化数据段(block started by symbol):在程序开始执
行之前,内核将此段中的数据初始化为0或空指针;
栈:每次函数调用时,其返回地址以及调用者的环境信息(如某些机器寄存器的值)都存放在栈中;
共享库:只需在所有进程都可引用的存储区中保存这种库例程的一个副本;
存储空间分配函数
#includevoid *malloc(size_t size);void *calloc(size_t nojy, size_t size);void *realloc(void *ptr, size_t newsize); 3个函数返回值:若成功,返回非空指针;若出错,返回NULL
malloc函数:初始值不确定;底层通过调用sbrk函数实现;
calloc函数:初始值为0;
realloc函数:增加或减少以前分配区的长度;当增加长度时,可能将以前分配区的内容移到另一个足够大的区域,以便在分配区末尾增加存储区,而新增存储区初始值不确定(例如:可变数组的使用);
注意:这些动态分配的函数一般在分配存储空间时,会比要求的大。因为在开辟空间的前后部分存储记录管理信息。因此,在使用时,千万不要越界访问,以免造成不可预知的后果。
函数间跳转策略
在c语言中,goto语句是不能跨函数跳转的。尤其是在函数深层调用时的跳转需求,在出错处理的情况下非常有用。
#includeint setjmp(jmp_buf env); 返回值:若直接调用,返回0;若从longjmp返回,返回非0void longjmp(jmp_buf env, int val);
变量值回滚问题:自动变量和寄存器变量会存在回滚现象。利用volatile属性来避免此类情况的发生。(在给变量赋值时,赋的值回首先存储在内存(存储器变量)中,然后在由cpu取走,存储在cpu的寄存器上(寄存器变量)。在做系统优化时,那些频繁使用的变量,会直接存储到寄存器中而不经过内存。)
寄存器变量会存在回滚现象的探究
在调用setjmp函数时,内核会把当前的栈顶指针保存在env变量中,所以在调用longjmp函数返回该位置时,全局变量、静态变量、易失变量和自动变量如果在调用setjmp和longjmp函数之间它们的值被修改过,是不会回滚到setjmp函数调用之前的值(当然,编译器将auto变量优化为寄存器变量除外)。因为,这些存储器变量的值是存储在内存相应的段中,回到原先栈顶状态时,同样访问的还是原先的内存空间。
然而,对于寄存器变量来说,首先要明确一点:寄存器变量是用动态存储的方式。意思是寄存器变量的值可能存在不同的寄存器中。如果在调setjmp和longjmp函数之间它们的值被修改过,这个值可能不会存到setjmp之前的对其赋值的寄存器中,而在调用longjmp函数后,又回到了调用setjmp函数时的状态。这个时候再读取寄存器变量的值时,读到的是原先那个寄存器中存储的值而不是修改过的那个寄存器中存储的值,所以出现的回滚现象。