频道栏目
首页 > 系统 > Linux > 正文

《Linux系统编程》笔记 第五章(二)

2016-08-20 09:17:03           
收藏   我要投稿

进程内存布局

进程在内存中被分配到不同的段中,在第一章已经简单介绍过段的概念,下面详细分析:
文本段(.text)-文本段包含程序运行的指令和代码中的常量。
初始化数据段(.data)-被显式初始化过的全局变量和静态变量。
未初始化数据段(.bss)-未被显式初始化过的全局变量和静态变量。这个段中的数据不会占用可执行文件的大小,在程序启动后被初始化为0。
栈(stack)-在程序运行期间动态增长或者收缩,由栈帧组成,系统为每一个被调用的函数分配栈帧,栈帧中存储了函数的局部变量、实参和返回值。
堆(heap)-在程序运行期间动态增长或者收缩,与栈的增长方向相反。
下面的代码测试初始化和未初始化全局变量对exe大小的影响:

//char unInitiualParam[1024*1024];//将其取消注释后可执行文件大小基本不变
char initiualParam[1024*1024] = "1";//将其注释后可执行文件大小减小
int main(int argc,char *argv[])
{
    return 0;
}
int main(int argc,char *argv[])
{
    //static char unInitiualParam[1024*1024];//将其取消注释后可执行文件大小基本不变
    static char initiualParam[1024*1024] = "1";//将其注释后可执行文件大小减小
    return 0;
}

虚拟内存管理

Linux采用虚拟内存机制运行可执行程序,虚拟内存是指可执行程序并不全部加载到内存中,而是加载到虚拟内存中,内核记录其虚拟内存地址与物理内存地址的映射关系。对于每个程序而言,其占用了全部的内存资源,而实际上内存中可能并没有保存该程序的全部数据。当程序访问到某个未加载到内存中的数据时,会发生缺页中断,内核会将需要的数据从硬盘加载到内存中。
内核采用页表(page table)来记录虚拟内存信息,其描述了每一页在内存的地址或者标志其未在内存而是在硬盘上。页表的内容可以动态增加或减少,例如堆、栈空间变化,共享内存的申请和释放等。下面是虚拟内存结构的示意图:
这里写图片描述
使用虚拟内存有几个好处:<喎"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCr34s8zT6734s8wvvfizzNPrxNq6y7XExNq05rfDzsrP4LuluPS++KOs0ru49r34s8zO3reo1rG909DeuMTB7dK7uPa9+LPMtcTE2rTmoaO/ydLUt72x47XEyrXP1sTatOa5ss/to6y8tLbguPa9+LPMtcTSs7HtzPXEv9a4z/LNrNK7ts7E2rTmo6yyydPD1eLSu7y8yvW1xNPQtq/MrMG0vdO/4rrNubLP7cTatOaho7Hg0uvG96Giv6q3osjL1LG2vLK70OjSqrnY0MSzzNDy1NrE2rTm1tC1xM7vwO2yvL7WoaO9+LPMsqKyu8rH1fvM5bzT1Ni1vcTatObW0LXEo6zS8rTLvfizzMv51bzTw7XExNq05r/J0tS089Pazu/A7cTatOajrLTLzeK2r8yst9bF5LXEttG78tXfubLP7cTatOa/1bzksqKyu7vh1ebV/dW808PE2rTmo6zWu9PQ1Nq3w87KyrGy+sn6yLHSs9bQts+687LFu+HTs8nktb3E2rTmyc+how0KPHA+1NrQ6cTixNq05rXE1qez1s/Co6zJz8PmzOG1vbXE0LTKsbi01sbKx9LU0rPOqrWlzru9+NDQtcSjrMjnufvG5NbQ0ru49r34s8yyu7jEseTG5Mirsr+1xMr9vt2jrNC0yrG4tNbGtcTQp8LKv8+2qLvhuN/T2tLUx7C1xLb2urq31sXkt73KvaGjPC9wPg0KPGgyIGlkPQ=="栈与栈帧">栈与栈帧

通过上面章节的虚拟内存结构示意图我们可以发现,虚拟内存中为内核预留了一部分区域,与用户空间的内存区分开,进程无法直接访问,这是提供给系统调用使用的。
在进程中每个函数被调用时,都会在虚拟内存上分配一个新的栈帧,用来保存局部变量、寄存器信息、函数指令等。在每个函数返回时,栈帧出栈,从栈中移除,因此局部变量在函数返回时都会失效(静态局部变量不存放在栈上,因此行为与局部变量不一致)。

5.3 终止进程

终止进程的函数是

#include 
void exit (int status);

该函数没有返回值也不会返回,之后的任何语句都不会被执行。
status参数代表进程的返回状态,最终父进程会收到该值。EXIT_SUCCESS和EXIT_FAILURE两个宏代表成功和失败状态。
在终止进程时,内核会按照注册的逆序来调用之前使用atexit()和on_exit()注册的退出函数,清空所有打开的I/O流,删除由tmpfile()创建的临时文件等。上述操作均在用户空间进行。
之后exit()会调用_exit()来通知内核进行后续处理:

#include 
void _exit (int status);

不再被任何进程使用的资源、信号量等都会被内核关闭,最终内核摧毁进程并通知父进程。

5.3.1 其他终止进程的方式

除了显示调用exit()函数外,一个常见的终止进程的方式是在main()函数中返回。事实上编译器会自动在main()函数末尾增加了_exit()系统调用。
此外一些信号的默认处理方式也是终止进程,例如SIGTERM和SIGKILL。
最后则是进程被内核主动杀死的情况,例如执行了非法指令,引起了段错误,耗尽内存(OOM KILLER)等。

5.3.2 atexit()

#include 
int atexit (void (*function)(void));

该函数用于注册在进程正常结束时(main()返回或者exit()被调用)被调用的函数,其接受一个函数指针,该函数声明为无参无返回值。
若进程调用了exec()系列函数,则注册的函数不再生效,因为注册的函数可能不在新进程的地址空间中。此外如果进程是因为收到信号、内核主动杀死而关闭的,注册的函数也不会被调用。注册的函数内不能调用exit(),否则会无限递归下去。

5.3.3 on_exit()

#include 
int on_exit (void (*function)(int , void *), void *arg);

该函数作用与atexit()类似,只是其函数指针的原型为void foo(int status, void* arg);,其中status是exit()的参数或者main()的返回值,arg是调用on_exit注册时的第二个参数,需要注意的是注册的函数被调用时arg指向的数据必须有效。为了可移植性最好使用atexit()。

5.3.4 SIGCHLD

在子进程结束后,父进程会收到SIGCHLD信号,该信号默认会被忽略,可以使用signal()或者sigaction()来处理该信号。

5.4 等待终止的子进程

父进程可以使用

#include 
#include 
pid_t wait (int *status);

来等待任意子进程退出。该函数会阻塞,直到有子进程退出,若子进程在函数调用之前退出,wait()会立即返回相关信息。返回值为子进程的pid,出错时返回-1。当status不为NULL时,该输出参数保存子进程的退出状态。Linux提供了一些宏来提取status的信息:

#include 
int WIFEXITED (status);//子进程使用exit()退出时返回非0
int WIFSIGNALED (status);//子进程因为信号而退出时返回非0
int WIFSTOPPED (status);//子进程停止时返回非0
int WIFCONTINUED (status);//子进程继续运行时返回非0
int WEXITSTATUS (status);//子进程使用exit()退出时返回传给_exit()的低八位
int WTERMSIG (status);//子进程因为信号结束时返回信号的编号
int WSTOPSIG (status);//子进程因为信号停止时返回信号的编号
int WCOREDUMP (status);//发生core dump时返回非0

子进程在结束后没有保留任何信息,父进程是无法获取子进程退出状态的,因此Linux将结束的子进程设置为僵死状态,等待父进程获取相关信息,当父进程未调用wait()相关函数时,内核会一直保存子进程的数据,这时的子进程称为僵尸进程(详细信息见前面章节僵尸进程的描述)。

5.4.1 等待特定进程

当父进程想等待特定的子进程结束时,可以根据子进程的进程号等待。

#include 
#include 
pid_t waitpid (pid_t pid, int *status, int options);

参数pid有多种选择,具体如下:

取值 含义
<-1 等待以pid绝对值未进程组id的子进程
-1 等待任意子进程,与wait()一致
0 等待与父进程同一进程组内的子进程
0
等待进程id为pid的子进程

参数status作用于wait()一致。
参数options可以是0或者以下参数的或运算组合:

取值 含义
WNOHANG 该调用不会等到子进程状态变化时才返回,而是立即返回
WUNTRACED 即使没有跟踪子进程,当子进程停止时函数也会返回
WCONTINUED 即使没有跟踪子进程,当子进程继续运行时函数也会返回

当函数返回时,返回值是对应的pid,当WNOHANG参数被设置且没有子进程满足条件时返回0,错误情况下返回-1。

5.4.2 其他等待子进程的方法

Linux提供了其他的等待子进程的方式:

#include 
int waitid (idtype_t idtype, id_t id, siginfo_t *infop, int options);

该方式获取的信息更全面。
idtype有如下取值:

取值 含义
P_PID 等待以pid是参数id的子进程
P_GID 等待进程组号是参数id的子进程
P_ALL 等待所有子进程,参数id被忽略

options有如下取值,也可以做或运算组合:

取值 含义
WEXITED 等待子进程结束
WSTOPPED 等待子进程收到信号后停止
WCONTINUED 等待子进程继续运行
WNOHANG 如果没有上述条件的子进程,调用也立即返回
WNOWAIT 不会移除子进程的僵死状态,后续可以继续用wait()

调用成功后参数infop会被充填,下列成员一定是有效的:

成员名 含义
si_pid 子进程的pid
si_uid 子进程的uid
si_code 子进程终止、杀死、停止或继续运行分别被设置为CLD_EXITED、CLD_KILLED、CLD_STOPPED、CLD_CONTINUED
si_signo 固定为SIGCHLD
si_status 若si_code为CLD_EXITED,该成员为退出码,否则为信号id

调用成功时该函数返回0,否则返回-1。

上一篇:Linux,数据库,计算机网络以及C++面试问题补充
下一篇:《Linux系统编程》笔记 第五章(三)
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站