1、概述
i、当程序执行时,main函数如何被调用;ii、命令行参数如何传递给新程序;iii、典型C程序的存储空间布局;iv、如何分配另外的存储空间(利用malloc、calloc、realloc及free函数);v、进程如何使用环境变量;vi、进程的7种不同终止方式;vii、longjmp和setjmp函数及它们与栈的交互作用;viii、查看进程资源限制。
2、main函数
C程序是从main函数开始。
启动C程序时,调用main函数之前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这由连接编辑器设置,而连接编辑器则由C编辑器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。
3、进程终止方式讲解
8种方式使进程终止,其中5中为正常终止。
i、从main返回;ii、调用exit;iii、调用_exit或_Exit;iv、最后一个线程从其启动例程返回;v、从最后一个线程调用pthread_exit;vi、调用abort;vii、接到一个信号;viii、最后一个线程对取消请求做出响应。
a、退出函数
3个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则先执行一些清理处理(如,对数据缓冲进行冲洗),然后返回内核。隐式返回的终止状态码是0;
b、函数atexit
一个进程可以登记多到32个函数(也被称为终止处理程序,通过atexit函数来登记这些函数),这些函数将由exit自动调用。
#include<stdlib.h>
int atexit(void(*fun)(void));
//返回值:若成功,返回0;若出错,返回非0
下图是对执行程序和终止程序调用函数的最清晰说明。
内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式(通过调用exit)调用_exit或_Exit。进程也可非自愿地由一个信号使其终止。
#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");
}
4、命令行参数及环境表及环境变量
在程序中命令参数的调用,即利用argv[1]、argv[2]...但agrv[argc]是一个空指针。
每个程序都会收到一张环境表,其是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。如下图所示:
环境由如name=value字符串组成。大多数预定义名完全由大写字母组成(惯例而已)。
#include<stdlib.h>
char* getenv(const char *name);
//返回值:指向与name关联的value的指针;若未找到,返回NULL
int putenv(char* str);//将指定环境变量放置到环境表中
//函数返回值:若成功,返回0;若出错,返回非0
int setenv(const char* name, const char* value, int rewrite);
int unsetenv(const char* name);
//两个函数返回值:若成功,返回0;若出错,返回-1
i、函数getenv,可以用其取环境变量值,该函数返回一个指针,指向name=value字符串中的value。一般应使用getenv从环境中取一个指定环境变量的值,而不是直接访问environ。
ii、putenv取形式为name=value的字符串,将其放到环境表中。如果name已经存在,则先删除其原先的定义。
iii、setenv将name设置为value。如果在环境中name已经存在,那么(a)若rewrite非0,则首先删除其现有的定义;(b)若rewrite为0,则不删除其现有定义(name不设置为新的value,而且也不出错)。
vi、unsetenv删除name的定义。即使不存在这种定义也不算出错。
5、C程序的存储空间布局(太TM重要啦!)
i、正文段。由cpu执行的机器指令部分。通常正文段是可共享的,即使频繁执行的程序在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外而修改其指令。
ii、初始化数据段。即数据段(data段),其包含了程序中需明确地赋初值的变量。如,int maxcount=99;此变量及初始值存放在data段中
iii、未初始化数据段。即bss段。这一块的内容,在程序开始执行之前,内核将此段中的数据初始化为0或空指针。如,long sum[1000];此变量存放在非初始化数据段中。
iv、栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址以及调用者的环境信息(如某些机器寄存器的值)都存放在栈中。然后,最近被调用的函数在栈上为其自动和临时变量分配存储空间。最后,每当有一次函数调用时,就用一个新的栈帧。
v、堆。通常在堆中进行动态存储分配。堆位于未初始化数据段和栈之间。
一个执行程序当然还包括其他段,只是上述所列举的是最基本。此外还有,符号表的段,调度信息的段,动态共享库链接表的段等,但并不会装载到进程执行的程序映像中。
简单以图片呈现存储空间安排的内容。
利用size命令可以报告正文段、数据段、bss段的长度。
6、存储空间分配的3个函数
#include<stdlib.h>
void* malloc(size_t size);
void* calloc(size_t nobj, size_t size);
void* realloc(void* ptr, size_t newsize);
//3个函数返回值:若成功,返回非空指针;若出错,返回NULL
void free(void* ptr);
使用时注意点:
i、三个函数返回的都是void*,但返回时,一律统一成显式强制类型转换,以避免不必要的错误。
ii、realloc的最后一个参数是存储区的新长度,不是新、旧存储区长度之差。若ptr是一个空指针,则realloc的功能与malloc相同,用于分配一个指定长度为newsize的存储区。
iii、大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度、指向下一个分配块的指针等。
7、setjmp和longjmp函数及其与栈的交互作用
首先,需要明确的是,setjmp和longjmp的优点是可以执行跨函数转跳功能,而goto只能在函数体内进行转跳。
i、考虑以下的代码及其对应的开僻的栈帧。
#include "apue.h"
#define TOK_ADD 5
void do_line(char *);
void cmd_add(void);
int get_token(void);
int
main(void)
{
char line[MAXLINE];
while (fgets(line, MAXLINE, stdin) != NULL)//stdin标准输入流函数,以换行符为终止符,最后一个字符自动添加为null字节。
do_line(line);
exit(0);
}
char *tok_ptr; /* global pointer for get_token() */
void
do_line(char *ptr) /* process one line of input */
{
int cmd;
tok_ptr = ptr;
while ((cmd = get_token()) > 0) {//get_token的功能:从该输入行中取下一个标记
switch (cmd) { /* one case for each command */
case TOK_ADD:
cmd_add();
break;
}
}
}
void
cmd_add(void)
{
int token;
token = get_token();
/* rest of processing for this command */
}
int
get_token(void)
{
/* fetch next token from line pointed to by tok_ptr */
}
ii、采用setjmp和longjmp函数进行转跳后的改写代码及其栈帧。
#include<setjmp.h>
int setjmp(jmp_buf env);//jmp_buf是一种特殊类型,而env通常定义为全局变量
//返回值:若直接调用,返回0;若从longjmp返回,则为非0
void longjmp(jmp_buf env, int val);
在希望返回到的位置调用setjmp,直接调用该函数,其返回值为0。setjmp参数env的类型是一个特殊类型jmp_buf。该数据类型是某种形式的数组,其中存放在调用longjmp时能用来恢复栈状态的所有信息。同时因为需在另一个函数中引用env变量,所以通常将env变量定义为全局变量。
以两个参数调用longjmp函数,第一个就是在调用setjmp时所用的env;第二个参数是非0值的val,它将成为从setjmp处返回的值。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。可以通过测试返回值可判断是哪个造成返回的longjmp的函数。
#include "apue.h"
#include <setjmp.h>
#define TOK_ADD 5
jmp_buf jmpbuffer;//jmpbuffer变量一般设置为全局变量
int
main(void)
{
char line[MAXLINE];
if (setjmp(jmpbuffer) != 0)//通过setjmp,在此处设置一个标志
printf("error");
while (fgets(line, MAXLINE, stdin) != NULL)
do_line(line);
exit(0);
}
. . .
void
cmd_add(void)
{
int token;
token = get_token();
if (token < 0) /* an error has occurred */
longjmp(jmpbuffer, 1);//如若出现问题,则可跨函数转跳到setjmp所设置的标志处,并返回第二个参数值1。
/* rest of processing for this command */
}
iii、自动变量、寄存器变量和易失变量
自动变量:存放在栈中,可放在存储器中,也可放在寄存器中。(有时会被回滚)
寄存器变量:由register修饰。(会被回滚)
易失变量:由volatile关键字进行修饰,此时变量一定会被放在存储器中,而非寄存器中。(自动变量,不想使其值回滚,就声明为volatile属性)
以下代码利用setjmp和longjmp函数对回滚现象导致变量值的更改进行举例说明。
#include "apue.h"
#include <setjmp.h>
static void f1(int, int, int, int);
static void f2(void);
static jmp_buf jmpbuffer;//定义jmpbuffer,方便回跳
static int globval;//全局变量
int
main(void)
{
int autoval;//自动变量
register int regival;//寄存器变量
volatile int volaval;//volatile修饰的易失变量,使得变量存放在存储器而非寄存器中
static int statval;//静态变量
globval = 1; autoval = 2; regival = 3; volaval = 4; statval = 5;
if (setjmp(jmpbuffer) != 0) {//放置jmpbuffer,等待longjmp回来
printf("after longjmp:\n");
printf("globval = %d, autoval = %d, regival = %d,"
" volaval = %d, statval = %d\n",
globval, autoval, regival, volaval, statval);
exit(0);
}
/*
* Change variables after setjmp, but before longjmp.
*/
globval = 95; autoval = 96; regival = 97; volaval = 98;
statval = 99;
f1(autoval, regival, volaval, statval); /* never returns */
exit(0);//由于会在f2中进行longjmp转跳,此函数exit(0)不会执行。
}
static void
f1(int i, int j, int k, int l)
{
printf("in f1():\n");
printf("globval = %d, autoval = %d, regival = %d,"
" volaval = %d, statval = %d\n", globval, i, j, k, l);
f2();
}
static void
f2(void)
{
longjmp(jmpbuffer, 1);
}
8、查看进程资源限制
#include<sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit* rlptr);
//两个函数返回值:若成功,返回0;若出错,返回非0
struct rlimit {
rlim_t rlim_cur; /* soft limit:current limit *///软限制
rlim_t rlim_max; /* hard limit:maximum value for rlim_cur *///硬限制
};
两个函数,每次调用指定一个资源以及上述结构的指针。
在更改资源限制时,须遵循下列3条规则:
i、任何一个进程都可将一个软限制值更改为小于或等于其硬限制值;
ii、任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值。这种降低,对普通用户而言是不可逆的。
iii、只有超级用户进程可以提高硬限制值。
常用查看限制资源命令为ulimit -a
如需更改core文件的最大可利用块,命令为ulimit -c 2048