UNIX高级环境编程(8)进程环境(Process Environment)- 进程的启动和退出、内存布局、环境变量列表
在学习进程控制相关知识之前,我们需要了解一个单进程的运行环境。
本章我们将了解一下的内容:
- 程序运行时,main函数是如何被调用的;
- 命令行参数是如何被传入到程序中的;
- 一个典型的内存布局是怎样的;
- 如何分配内存;
- 程序如何使用环境变量;
- 程序终止的各种方式;
- 跳转(longjmp和setjmp)函数的工作方式,以及如何和栈交互;
- 进程的资源限制
1 main函数
main函数声明:
int main (int argc, char *argv[]);
参数说明:
- argc:命令行参数个数
- argv:指向参数列表数组的指针
main函数启动前:
- C程序由内核执行,通过系统调用exec;
- main函数调用前,执行指定的启动路径(start-up routine);
- 可执行文件认为此地址为程序的启动地址,该地址由链接器指定;
- 启动路径从内核获取参数列表和环境变量,使得main函数可以在稍后被调用时可以获取这些变量。
2 进程终止
一共有8中终止进程的方式,5种正常终止和3种异常终止。
5种正常终止:
- 从main函数返回;
- 调用exit;
- 调用_exit或_Exit;
- 最后一个线程返回;
- 最后一个线程调用pthread_exit。
3种异常终止:
- 调用abort;
- 接收到一个信号;
- 最后一个线程应答或者一个接收到一个退出请求
启动地址(start-up routine)同样也是main函数的返回地址。
要获取该地址,可以通过以下的方式:
exit (main(argc, argv));
退出函数
函数声明:
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
函数细节:
- _exit和_Exit立刻返回到内核;
- exit函数返回内核前会进行一些清理环境工作;
返回一个整数和调用exit函数,并传入该整数的作用是相同的:
exit(0);
return 0;
atexit函数
函数声明
#include <stdlib.h>
int atexit(void (*func)(void));
函数细节
- 每个进程可以注册32个函数,这些函数可以在主函数调用exit时自动被调用
- 通过atexit注册的退出时处理函数称为退出句柄(exit handlers)
- 这些退出句柄的调用顺序为注册时的相反顺序
- exit函数第一次调用退出句柄时,会关闭所有打开的流
- 如果主程序调用了exec系列函数,则所有注册的退出句柄都会被清空
程序启动和终止流程图
Example:
#include "apue.h"
static void my_exit1(void);
static void my_exit2(void);
int
main(void)
{
if (atexit(my_exit2) != 0)
err_sys("can't register my_exit2");
if (atexit(my_exit1) != 0)
err_sys("can't register my_exit1");
if (atexit(my_exit1) != 0)
err_sys("can't register my_exit1");
printf("main is done\n");
return(0);
}
static void
my_exit1(void)
{
printf("first exit handler\n");
}
static void
my_exit2(void)
{
printf("second exit handler\n");
}
执行结果:
3 命令行参数
Example:
#include "apue.h"
int
main(int argc, char *argv[])
{
int i;
for (i = 0; i < argc; i++) /* echo all command-line args */
printf("argv[%d]: %s\n", i, argv[i]);
exit(0);
}
执行结果:
4 环境变量列表
每个程序会接受一个环境变量列表,该列表是一个数组,由一个数组指针指向,该数组指针类型为:
extern char **environ;
例如,如果环境变量里有5个字符串(C风格字符串),如下图所示:
5 C程序的内存布局
典型的C程序的内存布局如下图所示:
上图说明:
- 文本段(Text Segment),保存CPU将要执行的机器指令。文本段是可共享的,所以某个程序多次执行时,对应的文本段只需要在内存中存有一份拷贝。文本段是只读的(read-only),防止程序的指令被修改。
- 已初始化数据段(initialized data segment),保存程序中被初始化的全局变量(定义在任何函数之外)。例如:int maxcount = 99; 全局变量变量maxcount被保存在初始化数据段。
- 未初始化数据段(uninitialized data segment),也被称为BSS(block started by symbol),这个段中的数据在程序执行之前被内核初始化为0或者null。;例如定义一个全局变量(定义在任何函数之外),long sum[1000]; 该变量保存在未初始化数据段中。
- 栈(Stack):存储临时变量,函数相关信息。当一个函数被调用时,返回地址、调用者相关信息(如寄存器信息)会被保存在栈中。该被调用的函数会在栈上分配一部分空间保存它的临时变量。函数的递归调用也是应用这个原理。每一次函数调用自己,都会保存当前函数的信息,然后再栈上开辟一个新的空间用于保存该次函数的信息,和以前的函数并没有影响。
- 堆(Heap):动态内存分配位置。堆的位置位于未初始化数据段和栈的中间。
6 内存分配(Memory Allocation)
有三个函数可以用于内存分配:
- malloc:分配指定字节数的内存,未初始化。
- calloc:分配指定数目的对象大小的内存,内存初始化为0;
- realloc:增加或减小之前分配的内存。移动旧内存的内容到新的更大的内存块,多余的部分内存未初始化。
函数声明:
#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
void free(void* ptr);
函数细节:
- 三个函数返回的内存指针一定是内存对齐的,这样可以用来保存于不同的对象;
- free函数用于释放ptr指向的内存,被分配的内存放入内存池中用于下次的内存分配;
- realloc函数用于改变之前分配的内存的大小。比如运行时我们申请了一段内存用于存储512个元素的数组,后来发现内存大小不够,这时可以调用realloc。如果操作系统发现在当前内存的后面有足够的内存,则直接分配多余的内存到当前内存中,然后返回传入的指针(即直接扩展内存)。但是如果当前内存后面没有足够大小的空间,则系统重新分配一个足够大的内存,将旧内存块中得内容拷贝到新内存块中,然后返回新内存的地址。
- 内存分配函数使用系统调用sbrk来实现。该系统调用的作用是扩展进程的堆。
- 一般实际分配的内存块都比请求的要大,多出来的部分用来存储内存块大小、指向下一内存块的指针等信息。写覆盖信息记录区的错误是非常隐蔽而且严重的。
7 环境变量(Environment Variable)
环境变量的字符串形式:
name=value
内核不关注环境变量,各种应用才会使用环境变量。
获取环境变量值使用函数getenv。
#include <stdlib.h>
char* getenv(const char* name);
// Returns: pointer to value associated with name, NULL if not found
修改环境变量的函数:
#include <stdlib.h>
int putenv(char* str);
int setenv(const char* name, const char* value, int rewrite);
int unsetenv(const char* name);
函数细节:
- 函数putenv传入一个字符串,形式为name=value,加入到环境变量列表中。如果name已经存在,先删除旧的定义。
- 函数setenv传入一个name和一个value,如果name已经存在,则参数rewrite决定是否覆盖旧的定义,如果rewrite为非零,则会覆盖旧的定义。
- 函数unsetenv删除name的定义,如果name不存在,也不报错。
修改环境变量列表的过程是一件很有趣的事情
从上面的C程序内存布局图中可以看到,环境变量列表(保存指向环境变量字符串的一组指针)保存在栈的上方内存中。
在该内存中,删除一个字符串很简单。我们只需要找到该指针,删除该指针和该指针指向的字符串。
但是增加或修改一个环境变量困难得多。因为环境变量列表所在的内存往往在进程的内存空间顶部,下面是栈。所以该内存空间无法被向上或者向下扩展。
所以修改环境变量列表的过程如下所述:
- 如果我们修改一个已经存在的name:
- 如果新的value的大小比已经存在的value小或者相当,直接覆盖旧的value;
- 如果新的value的大小比已经存在的value大,则我们必须为新的value malloc一个新的内存空间,拷贝新value到该内存中,替换指向旧value的指针为指向新value的指针。
- 如果我们新增一个环境变量:
- 首先我们需要调用malloc为字符串name=value分配空间,拷贝该字符串到目标内存中;
- 如果这是我们第一次添加环境变量,我们需要调用malloc分配一个新的空间,拷贝老的环境量列表到新的内存中,并在列表后新增目标环境变量。然后我们设置environ指向新的环境变量列表。
- 如果这不是我们第一次新增环境变量,则我们只需要realloc多分配一个环境变量的空间,新增的环境变量保存在列表尾部,列表最后仍然是一个null指针。
小结
本篇介绍了进程的启动和退出、内存布局、环境变量列表和环境变量的修改。
下一篇将接着学习四个函数setjmp、longjmp、getrlimit和setrlimit。
参考资料:
《Advanced Programming in the UNIX Envinronment 3rd》
UNIX高级环境编程(8)进程环境(Process Environment)- 进程的启动和退出、内存布局、环境变量列表的更多相关文章
- UNIX环境编程学习笔记(20)——进程管理之exec 函数族
lienhua342014-10-07 在文档“进程控制三部曲”中,我们提到 fork 函数创建子进程之后,通常都会调用 exec 函数来执行一个新程序.调用 exec 函数之后,该进程就将执行的程序 ...
- UNIX环境编程学习笔记(17)——进程管理之进程的几个基本概念
lienhua342014-10-05 1 main 函数是如何被调用的? 在编译 C 程序时,C 编译器调用链接器在生成的目标可执行程序文件中,设置一个特殊的启动例程为程序的起始地址.当内核执行 C ...
- UNIX环境编程学习笔记(21)——进程管理之获取进程终止状态的 wait 和 waitpid 函数
lienhua342014-10-12 当一个进程正常或者异常终止时,内核就向其父进程发送 SIGCHLD信号.父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用的函数(信号处理程序).对于这 ...
- UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习
lienhua342014-10-07 在“进程控制三部曲”中,我们学习到了 fork 是三部曲的第一部,用于创建一个新进程.但是关于 fork 的更深入的一些的东西我们还没有涉及到,例如,fork ...
- UNIX环境编程学习笔记(18)——进程管理之进程控制三部曲
lienhua342014-10-05 1 进程控制三部曲概述 UNIX 系统提供了 fork.exec.exit 和 wait 等基本的进程控制原语.通过这些进程控制原语,我们即可完成对进程创建.执 ...
- UNIX环境编程学习笔记(15)——进程管理之进程终止
lienhua342014-10-02 1 进程的终止方式 进程的终止方式有 8 种,其中 5 种为正常终止,它们是 1. 从 main 返回. 2. 调用 exit. 3. 调用_exit 或_Ex ...
- UNIX环境编程学习笔记(16)——进程管理之进程环境变量
lienhua342014-10-03 1 环境表和环境指针 在每个进程启动时,都会接到一张环境表.环境表是一个字符指针数组,其中每个指针包含一个以 null 结束的 C 字符串的地址.全局变量env ...
- UNIX环境编程学习笔记(22)——进程管理之system 函数执行命令行字符串
lienhua342014-10-15 ISO C 定义了 system 函数,用于在程序中执行一个命令字符串.其声明如下, #include <stdlib.h> int system( ...
- UNIX高级环境编程(12)进程关联(Process Relationships)- 终端登录过程 ,进程组,Session
在前面的章节我们了解到,进程之间是有关联的: 每个进程都有一个父进程: 子进程退出时,父进程可以感知并且获取子进程的退出状态. 本章我们将了解: 进程组的更多细节: sessions的内容: logi ...
- UNIX高级环境编程(9)进程控制(Process Control)- fork,vfork,僵尸进程,wait和waitpid
本章包含内容有: 创建新进程 程序执行(program execution) 进程终止(process termination) 进程的各种ID 1 进程标识符(Process Identifie ...
随机推荐
- 微信小程序动态生成保存二维码
起源:最近小程序需要涉及到一些推广方面的功能,所以要写一个动态生成二维码用户进行下载分享,写完之后受益良多,特此来分享一下: 一.微信小程序动态生成保存二维码 wxml: <view class ...
- UOJ #357. 【JOI2017春季合宿】Sparklers
Description 小S和小M去看花火大会. 一共有 n 个人按顺序排成一排,每个人手上有一个仅能被点燃一次的烟花.最开始时第 K 个人手上的烟花是点燃的. 烟花最多能燃烧 T 时间.每当两个人的 ...
- DelegatingFilterProxy类的作用
使用过springSecurity的朋友都知道,首先需要在web.xml进行以下配置 <filter> <filter-name>springSecurityFilterCha ...
- java.sql.SQLException: Access denied for user 'root '@'localhost' (using password: YES) 最蠢
我犯了七年前的错误,一个空格,昨天就想到的,还对比了一下密码有没有空格 问题原因1:多写空格 在datasource.properties 中的username 的值root后面多写了一个空格, jd ...
- Spring JdbcTemplate详解
为了使 JDBC 更加易于使用,Spring 在 JDBCAPI 上定义了一个抽象层, 以此建立一个JDBC存取框架. 作为 SpringJDBC 框架的核心, JDBC 模板的设计目的是为不同类型的 ...
- Ubuntu16.04 LTS上安装Go1.10
原因 Ubuntu资源库上默认使用的是Go1.6.2版本,给最新版本代码编译带来了不少问题.本文就记录下在Ubuntu下直接安装Go最新版1.10的步骤. 准备工作 1.卸载已有版本 # 卸载已经安装 ...
- Access restriction: The type BASE64Encoder is not accessible due to restrict(转载)
Access restriction: The type BASE64Encoder is not accessible due to restrict 2011年11月18日 20:47:06 阅读 ...
- ECMAScript 5和ECMAScript6的新特性以及浏览器支持情况
ECMAScript简介: 它是一种由Ecma国际(前身为欧洲计算机制造商协会)制定和发布的脚本语言规范,javascript在它基础上经行了自己的封装.但通常来说,术语ECMAScript和java ...
- iOS中表视图单元格事件用nib和storyboard的两种写法总结
从ios6开始,苹果公司推出了storyborad技术取代了nib的写法,这样代码量确实少写了很多,也比较简洁.但是,从学习的角度来说,阿堂认为 用nib的写法,虽然多了些代码,但是对于掌握知识和原理 ...
- package.json中devDependencies与dependencies的区别
前言:之前一直不懂既然都是项目的依赖,为什么要分成两个部分,devDependencies和dependencies,有什么区别? 安装方式 我们在通过npm安装插件或库时,有三种方式: npm in ...