进程环境
一、main函数
C程序总是从main函数开始执行。,函数原型:
int main(int argc, char *argv[]);
//argc是命令行参数的数目。
//argv是指向参数的各个指针所构成的数组。
-
内核执行C程序时(exec函数),在调用main前先调用一个特殊的启动例程。
-
可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器设置的,而连接编辑器则由C编译器调用。
-
启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函 数做好安排。
启动例程调用main函数的形式:
exit(main(argc,argv));
二、进程终止
8种方式使进程终止,前5种为正常终止:
- 从main返回;
- 调用exit;
- 调用_exit或_Exit;
- 最后一个线程从其启动例程返回。
- 从最后一个线程调用pthread_exit。
- 调用abort。
- 接到一个信号。
- 最后一个线程对取消请求做出响应。
//终止程序:
//_exit和_Exit立即进入内核
//exit先执行处理,然后返回内核
//status:为终止状态
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
exit 函数
总是执行一个标准 I/O 库的清理关闭操作:对于所有打开流调用fclose
函数。这造成输出缓冲中的所有数据都被冲洗 (写到文件上)。mian返回类型是整型,且mian执行到最后一条语句时返回,则进程终止状态为0。
//main函数返回一个整型值与用该值调用exit是等价的
exit(0);
//等价
return(0);
一个进程可以登记多至32个函数,这些函数将由exit
自动调用。我们称这些函数为终止处理程序,并调用atexit
函数来登记这些函数。
#include <stdlib.h>
int atexit(void (*func)(void));
//atexit的参数是一个函数地址,当调用此函数时无需向它传递任何参数,也不期望它返回一个值。
//exit调用这些函数的顺序与它们登记时候的顺序相反。
//同一函数如若登记多次,也会被调用多次。
//返回值:成功返回0,出错非0
若程序调用exec
函数族中的任一函数,则将清除所有已安装的终止处理程序。下图显示了一个C 程序是如何启动的,以及它终止的各种方式。
内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的 唯一方法是显式或隐式地(通过调用 exit)调用_exit 或_Exit。进程也可非自愿 地由一个信号使其终止。
//使用atexit函数
#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");
}
1.退出处理程序
退出处理程序是由程序设计者提供的函数,可用于进程生命周期的任意时间点注册,并在该进程调用exit()正常终止时自动执行。若程序直接调用_exit()
或因信号而异常终止,则不会调用退出处理程序。
#include<stdlib.h>
int atexit(void (*func)(void));
//将fun加到函数列表中,进程终止时会调用该函数列表的所有函数。
//此函数出错返回非0值。
void fun(void)
{//执行一些操作}
由于atexit()注册的退出处理程序会受到两种限制:
- 退出处理程序在执行时无法获知传递给exit()的状态。
- 无法给退出处理程序指定参数。
摆脱这些限制,提供了on_exit()函数:
例:使用atexit和on_exit函数注册退出处理程序例子。
#include<stdlib.h>
int on_exit(void (*func)(int,void *),void *arg);
//arg:自行支配
//func指针指向如下类型函数:
void func(int status void *arg){//执行清理操作}
//status:提供给exit()
//arg:提供给on_exit()的参数拷贝
//on_exit()出错返回非0值。
#define _BSD_SOURCE /* 从<stdlib获取on_exit()声明。 */
#include <stdlib.h>
static void atexitFunc1(void)
{
printf("atexit function 1 called\n");
}
static void atexitFunc2(void)
{
printf("atexit function 2 called\n");
}
static void onexitFunc(int exitStatus, void *arg)
{
printf("on_exit function called: status=%d, arg=%ld\n",
exitStatus, (long) arg);
}
int main(int argc, char *argv[])
{
if (on_exit(onexitFunc, (void *) 10) != 0)
fatal("on_exit 1");
if (atexit(atexitFunc1) != 0)
fatal("atexit 1");
if (atexit(atexitFunc2) != 0)
fatal("atexit 2");
if (on_exit(onexitFunc, (void *) 20) != 0)
fatal("on_exit 2");
exit(2);
}
三、命令行参数
当执行一个程序时,调用exec的进程可将命令行参数传递给该新程序。
//将其所有命令行参数都回显到标准输出上
#include "apue.h"
int main(int argc, char *argv[])
{
int i;
for (i = 0; i < argc; i++)
printf("argv[%d]: %s\n", i, argv[i]);
exit(0);
}
//要求argv[argc]是一个空指针。修改为
for (i = 0; argv[i] != NULL; i++)
四、环境表
每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null
结束的C字符串的地址。全局变量environ
则 包含了该指针数组的地址:
extern char **environ;
下图所示环境包含5个字符串,每个字符串的结尾的显示的有关null字节。
environ
为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。
环境由name = value
形式字符串组成,main函数可以带3个参数,其中第3个参数是环境表地址:
//规定不需要第3个参数
//查看整个环境变量,必须使用environ指针
int main(int argc, char *argv[], char *envp[]);
五、C程序的存储空间分布
C程序由下列几个部分组成:
-
正文段。由CPU执行的机器指令部分。
-
初始化数据段。此段称为数据段,它包含了程序中需明确地赋初值 的变量。
-
未初始化数据段。此段中的数据初始化为0或空指针。
-
栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。
-
堆。通常在堆中进行动态存储分配。
-
未初始化数据段**的内容并不存放在磁盘程序文件中。 其原因是,内核在程序开始运行前将它们都设置为 0。
-
需要存放在磁盘程序文 件中的段只有正文段和初始化数据段。
-
size(1)命令报告正文段、数据段和bss段的长度(以字节为单位)。
六、共享库
-
共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存 储区中保存这种库例程的一个副本。
-
第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接。减少了每个可执行文件的长度,但增加了一些运行时间开销。
-
这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。
-
共享库的另一个优点是可以用库 函数的新版本代替老版本而无需对使用该库的程序重新连接编辑。
七、存储空间分配
用于存储空间动态分配的函数:
- malloc,分配指定字节数的存储区。此存储区中的初始值不确定。
- calloc,为指定数量指定长度的对象分配存储空间。该空间中的每一 位(bit)都初始化为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);
// 3个函数返回值:若成功,返回非空指针;若出错,返回NULL
void free(void *ptr);
-
3个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对 象。
-
3个alloc 函数都返回通用指针 void *,所以如果在程序中包括了
#include<stdlib.h>
(以获得函数原型),将这些函数返回的指针赋予 一个不同类型的指针时,就不需要显式地执行强制类型转换。 -
未声明函数的默认返回值为int,所以使用没有正确函数声明的强制类型转换可能会隐藏系统错误,因为int类型的长度与函数返回类型值的长度不同(本例中是指针)。
-
函数free 释放ptr指向的存储空间。被释放的空间通常被送入可用存储区池,以后,可在调用上述3个分配函数时再分配。
-
realloc函数使我们可以增、减以前分配的存储区的长度(最常见的用法是增加该区)。realloc的最后一个参数是存储区的新长度,不是新、旧存储区长度之差。若ptr是一个空指针,则realloc的功能与malloc相同,用于 分配一个指定长度为
newsize
的存储区。 -
大多数malloc和free的实现 都不减小进程的存储空间。释放的空间可供以后再分配,但将它们保持在 malloc池中而不返回给内核。
-
在动态分配的缓冲区前或后进行写操作,破坏的可能不仅仅是该区的管理记录信息。在动态分配的缓冲区前后的存储空间很可能用于其他动态分配的对 象。这些对象与破坏它们的代码可能无关,这造成寻求信息破坏的源头更加困 难。
-
其他产生错误是:释放一个已经释放了的块;调用free时所 用的指针不是3个alloc函数的返回值等。如若一个进程调用malloc函数,但却忘 记调用free函数,那么该进程占用的存储空间就会连续增加,这被称为泄漏 。如果不调用free函数释放不再使用的空间,那么进程地址空间长度 就会慢慢增加,直至不再有空闲空间。此时,由于过度的换页开销,会造成性 能下降。
1.替代的存储空间分配程序
有很多可替代malloc和free的函数。某些系统已经提供替代存储空间分配函 数的库。另一些系统只提供标准的存储空间分配程序。
-
libmalloc
-
vmalloc
-
quick-fit
-
jemalloc
-
TCMalloc
-
函数alloca
八、环境变量
//获取环境变量值
#include<stdlib.h>
char *getenv(const char *name);
//此函数返回一个指针,它指向name=value字符串中的value。
//应当使用getenv从环境中取一个指定环境变量的值,而不是直接访问environ。
环境变量:
有时需要设置环境变量,希望改变现有变量的值,或者是增加新的环境变量,如下图提供的函数:
#include <stdlib.h>
int putenv(char *str); //自由传递
//函数返回值:若成功,返回0;若出错,返回非0
int setenv(const char *name, const char *value, int rewrite);//必须分配空间
int unsetenv(const char *name);
//两个函数返回值:若成功,返回0;若出错,返回−1
函数的操作如下:
putenv
取形式为name=valu
e的字符串,将其放到环境表中。如果name已经存在,则先删除其原来的定义。setenv
将name设置为value。如果在环境中name已经存在,那么1.若 rewrite非0,则首先删除其现有的定义;2.若rewrite为0,则不删除其现有定 义(name不设置为新的value,而且也不出错)。unsetenv
删除name的定义。即使不存在这种定义也不算出错。
-
如果修改一个现有的name:
- 若新value的长度少于或等于现有value的长度,将新字符串复 制到原字符串所用的空间中;
- 若新value的长度大于原长度,调用malloc为新字符串分配空 间,将新字符串复制到该空间中,使环境表中针对name的指针指向新分配区。
- 若增加新的name,调用
malloc为name=value
字符串分配空间,然后将该字符串复制到此空间中。- 若第一次增加新name,调用malloc为新的指针表分配空间。将原来的环境表复制到新分配区,并将指向新name=value字符串 的指针存放在该指针表的表尾,然后又将一个空指针存放在其后。最后使 environ指向新指针表。如果原来的环境表位于栈顶之上(这是 一种常见情况),那么必须将此表移至堆中,但大多数指针仍指向栈顶之上的各name=value字符串。
- 不是第一次增加新name,则可知以前已调用malloc在堆中为环境表分配了空间,所以只要调用 realloc,以分配比原空间多存放一个指针的 空间。然后将指向新
name=value
字符串的指针存放在该表表尾,后面跟着一个 空指针。
九、函数setjmp和longjmp
`
C中goto语句不能跨越函数,但函数setjmp和longjmp却能实现此功能。实现在栈上跳过若干调用帧,返回当前函数调用路径的某个函数。
#include <setjmp.h>
int setjmp(jmp_buf env);//存放调用longjmp时用来恢复栈状态的所有消息。
//env全局变量,类型:jmp_buf
//返回值:若直接调用,返回0;若从longjmp返回,则为非0。
void longjmp(jmp_buf env, int val);//将setjmp存放的所有消息恢复,跳转到setjmp函数
//evn:setjmp中保存的消息
//val:作参数在setjmp函数返回,非0值。
1.自动变量、寄存器变量和易失变量
当longjmp返回到main
函数时, 这些变量的值是否能恢复到以前调用setjmp时的值(即回滚到原先值),大多数实现并不回滚这些自动变量和寄存器变量的值,而所有标准则称它们的值是不确定的。如果你有 一个自动变量,而又不想使其值回滚,则可定义其为具有volatile
属性。声明为全局变量或静态变量的值在执行longjmp
时保持不变。
//调用longjmp后,自动变量、全局变量、寄 存器变量、静态变量和易失变量的不同情况。
include "apue.h"
#include <setjmp.h>
static void f1(int, int, int, int);
static void f2(void);
static jmp_buf jmpbuffer;
static int globval;
int main(void)
{
int autoval;
register int regival;
volatile int volaval;
static int statval;
globval = 1; autoval = 2; regival = 3; volaval = 4; statval = 5;
if (setjmp(jmpbuffer) != 0) {
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);
}
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);//恢复setjmp保存的信息,跳转到setjmp函数处
}
运行:
2.自动变量的潜在问题
基本规则是声明自动变量的函数已经返回后,不能再引用这些 自动变量。
#include <stdio.h>
FILE * open_data(void)
{
FILE *fp;
char databuf[BUFSIZ];
if ((fp = fopen("datafile", "r")) == NULL)
return(NULL);
if (setvbuf(fp, databuf, _IOLBF, BUFSIZ) != 0)
return(NULL);
return(fp); 、
}
- 问题是:当open_data返回时,它在栈上所使用的空间将由下一个被调用函 数的栈帧使用。但是,标准I/O库函数仍将使用这部分存储空间作为该流的缓冲 区。这就产生了冲突和混乱。
- 改正:应在全局存储空间静态地 (如static或extern)或者动态地(使用一种alloc函数)为数组databuf分配空间。
十、函数getrusage、getrlimit和setrlimit
//返回调用进程或子进程用掉的各类系统资源的统计信息
int getrusage(int who,struct rusage *res_usage);
//res_usage指向如下结构:
struct rusag{
struct timeval ru_utime;
struct timeval ru_stime;
long ru_maxrss;
long ru_ixrss;
long ru_idrss;
long ru_isrss;
long ru_minflt;
long ru_majflt;
long ru_nswap;
long ru_inblock;
long ru_oublock;
long ru_msgsnd;
long ru_msgrcv;
long ru_nsignals;
long ru_ru_nivcsw;
}
who属性值如下:
RUSAGE_SSELF
:返回调用进程的信息。RUSAGE_GHILDREN
:返回调用进程的所有被终止和处于等待状态的子进程相关信息。RUSAGE_THREAD
:返回调用线程相关信息。
//查询 和更改资源限制
#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;
rlim_t rlim_max;
}
更改资源限制遵循下列3条规则:
- 任何一个进程都可将一个软限制值更改为小于或等于其硬限制值。
- 任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制 值。这种降低,对普通用户而言是不可逆的。
- 只有超级用户进程可以提高硬限制值。
常量RLIM_INFINITY
指定了一个无限量的限制,resource
参数取下列值:
资源限制影响到调用进程并由其子进程继承。这就意味着,为了影响一个 用户的所有后续进程,需将资源限制的设置构造在shell之中。
//打印由系统支持的所有资源当前的软限制和硬限制。
#include "apue.h"
#include <sys/resource.h>
#define doit(name) pr_limits(#name, name)
static void pr_limits(char *, int);
int main(void)
{
#ifdef RLIMIT_AS
doit(RLIMIT_AS);
#endif
doit(RLIMIT_CORE);
doit(RLIMIT_CPU);
doit(RLIMIT_DATA);
doit(RLIMIT_FSIZE);
#ifdef RLIMIT_MEMLOCK
doit(RLIMIT_MEMLOCK);
#endif
#ifdef RLIMIT_MSGQUEUE
doit(RLIMIT_MSGQUEUE);
#endif
#ifdef RLIMIT_NICE
doit(RLIMIT_NICE);
#endif
doit(RLIMIT_NOFILE);
#ifdef RLIMIT_NPROC
doit(RLIMIT_NPROC);
#endif
#ifdef RLIMIT_NPTS
doit(RLIMIT_NPTS);
#endif
#ifdef RLIMIT_RSS
doit(RLIMIT_RSS);
#endif
#ifdef RLIMIT_SBSIZE
doit(RLIMIT_SBSIZE);
#endif
#ifdef RLIMIT_SIGPENDING
doit(RLIMIT_SIGPENDING);
#endif
doit(RLIMIT_STACK);
#ifdef RLIMIT_SWAP
doit(RLIMIT_SWAP);
#endif
#ifdef RLIMIT_VMEM
doit(RLIMIT_VMEM);
#endif
exit(0);
}
static void pr_limits(char *name, int resource)
{
struct rlimit limit;
unsigned long long lim;
if (getrlimit(resource, &limit) < 0)
err_sys("getrlimit error for %s", name);
printf("%-14s ", name);
if (limit.rlim_cur == RLIM_INFINITY) {
printf("(infinite) ");
} else {
lim = limit.rlim_cur;
printf("%10lld ", lim);
}
if (limit.rlim_max == RLIM_INFINITY) {
printf("(infinite)");
} else {
lim = limit.rlim_max;
printf("%10lld", lim);
}
putchar((int)'\n');
}
十一、fork()、stdio缓冲区以及_exit之间的交互
#inclde<stdio.h>
int main(int argc, char *argv[])
{
printf("Hello world\n");
write(STDOUT_FILENO, "Ciao\n", 5);
if (fork() == -1)
errExit("fork");
/* 子级和父级在此处继续执行 */
exit(EXIT_SUCCESS);
}
问题:
-
printf()的输出重新两次,且write()的输出先于printf()。
-
当调用fork函数时,printf函数输出的字符仍在父进程的stdio缓冲区中,并随子进程的创建而产生一份副本。父、子进程调用exit函数会刷新各自的stdio缓冲区,从而导致重复输出结果。
-
write函数的输出并未出现两次,因为write函数会将数据直接传给内核缓冲区,fork函数不会复制这一缓冲区。
解决问题:
- 在调用frok函数之前使用
ffush
含刷新stdio缓冲区。也可使用setvbuf函数和setvuf
来关闭stdio的缓冲功能。 - 子进程可以调用_exit非exit,以便不在刷新stdio缓冲区。