文章目录
1、引言
2、main函数
- main函数的原型
/*argc是命令行参数的数目,argv是指向各个指针所构成的数组*/ int main(int argc,char *argv[]);
3、进程终止
-
有八种方式使进程终止。其中5种是正常,它们是:
- 从main函数返回
- 调用exit
- 调用_exit或_Exit
- 最后一个线程从其启动例程返回
- 从最后一个线程调用pthread_exit
-
异常终止有三种方式:
- 调用abort
- 收到一个信号
- 最后一个线程对取消请求做出响应
3.1、退出函数
-
以下三个函数用于正常终止一个程序
void _exit(int status); //系统调用,立即进入内核 void _Exit(int status); //系统调用,立即进入内核 void exit(int status); //先执行一些清理工作,然后返回内核。
- exit函数总是执行标准I/O库的清理关闭操作,对于所有打开的流调用
fclose
函数,这会造成输出缓冲中的所有数据被写(冲洗)到文件上。 - 这三个退出函数都有一个参数,即终止状态(或退出状态)。
main
函数返回一个整形值与用该值调用exit
是等价的。于是在main
函数中exit(0);
等同于return 0;
- 当调用这些函数时不带终止状态,或者
main
执行了一个无返回值的return
语句,或者main
没有声明返回类型为整形,则进程的终止状态是未定义的。
- exit函数总是执行标准I/O库的清理关闭操作,对于所有打开的流调用
-
实例:
main
执行了一个无返回值的return
语句/*hello1.c*/ #include <stdio.h> int main() { printf("hello, world\n"); return;//无返回值 }
命令行:
lh@LH_LINUX:~/桌面/apue.3e/environ$ gcc -o hello hello1.c hello1.c: In function ‘main’: hello1.c:6:2: warning: ‘return’ with no value, in function returning non-void return; ^ lh@LH_LINUX:~/桌面/apue.3e/environ$ ./hello hello, world lh@LH_LINUX:~/桌面/apue.3e/environ$ echo $? 13
对程序进程编译然后运行,可见到其终止码是随机的。注意:
$?
指明上一次执行命令的返回值,同时回忆一下$#
、$*
、$?
、$0
、$1
…的作用
3.2、atexit函数
- 一个进程可以登记最多32个函数(一些操作系统实现可能更多)个函数,这些函数将由
exit
自动调用。我们称这些函数为终止处理程序,通过atexit
函数来登记这些函数int atexit(void (*function)(void));
- exit调用这些函数的顺序与登记它们的顺序相反,同一函数如果被登记多次也会被调用多次。
- exit函数会先调用各终止处理程序,再
fclose
所有打开流。然后再调用_exit
函数终止进程。 - 如果程序调用exec函数族,则会清除所有已经注册的终止处理程序。
- 内核使程序执行的唯一方法是调用一个
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"); /*my_exit1登记了两次,则也会调用两次*/ 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);//等价于exit(0); } static void my_exit1(void) { printf("first exit handler\n"); } static void my_exit2(void) { printf("second exit handler\n"); }
可以看到root@LH_LINUX:/home/lh/桌面/apue.3e/environ# ./doatexit main is done first exit handler first exit handler second exit handler
exit
调用这些函数的顺序与登记它们的顺序相反
4、命令行参数
-
调用
exec
函数的进程可以将命令行参数传递给新程序。UNIX内核并不查看这些字符串,它们的解释完全取决于各个应用程序,因此需要通过exec
将这些参数传递给进程 -
实例:将所有命令行参数都回显到标准输出上。
#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); }
命令行:
lh@LH_LINUX:~/桌面/apue.3e/environ$ ./echoarg arg1 arg2 arg3 argv[0]: ./echoarg argv[1]: arg1 argv[2]: arg2 argv[3]: arg3 lh@LH_LINUX:~/桌面/apue.3e/environ$ echo arg1 arg2 arg3 arg1 arg2 arg3
可以看到可执行文件
echoarg
将所有命令行参数都打印了出来,而echo
程序不会回显第0个参数。注意:argv[argc]
是一个空指针。
5、环境表
- 每一个进程都有一张环境表,该表也是一个字符指针数组,其中每个指针指向一个以
null
结束的C字符串地址。全局变量environ
包含了该指针数组的地址。extern char **environ;
- 每一个环境变量由
name=value
形式的字符串构成,其中name
字段一般是大写字母组成。 - 当然也可以通过main函数的第三个参数来访问环境变量(已弃用)
int main(int argc, char* argv[], char**envp);//envp已弃用,规定应使用全局变量environ
- 每一个环境变量由
6、C程序的存储空间布局
-
C程序由以下部分组成
- 正文段(或代码段).text:
由CPU执行的机器指令部分组成(即程序编译之后,编译器会将代码翻译成二进制的机器码,机器码存储在代码段(.text)中)。通常正文段可以共享,所以即使是频繁执行的程序在存储器中也只需有一个副本。并且正文段通常是只读的,以防止程序由于意外而修改其指令。也有可能包含一些只读的常数变量,例如字符串常量等。 - 初始化数据段.data:
通常将此段称为数据段。用于保存有非0初始值的全局变量和静态变量。(局部变量保存在栈中) - 未初始化数据段.bss:
用于保存没有初始值或初值为0的全局变量和静态变量。在程序开始执行之前,内核将此段中的数据初始化为0和空指针。 - 栈stack:
局部变量与每次函数调用时需要保存的信息存放在stack中。每次函数调用时,其返回地址以及调用者的环境信息(如某些寄存器的值)都存放入栈。然后,最近被调用的函数在栈上为其分配栈帧。递归的原理就是每次调用自身时,就用一个新的栈帧,因此一次函数调用实例中的变量不会影响到另一次函数调用实例中的变量。 - 堆heap:
通常在堆中进行动态存储分配(malloc)。位于bss和stack中间。
- 正文段(或代码段).text:
-
可执行文件中还有一些其他类型的段:包含符号表的段;包含调试信息的段;包含动态库链接表的段等。这些部分并不装载到进程执行的程序映像中。
-
可以看出,未初始化数据段的内容并不存放在磁盘程序文件中。因为内核在程序开始运行前将它们都设置为0。需要存放在磁盘程序文件中的只有正文段和初始化数据段。
7、共享库
- 共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例程的一个副本。
- 程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接。这减少了每个可执行文件的长度,但增加了一些运行时间开销。这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使用该库的程序重新编译(假如参数个数与类型都不变)。
- 下面展示无共享库方式和使用共享库方式创建可执行文件。
可以发现:使用共享库比不使用时,可执行文件的正文和数据段的长度都显著减少。注意:lh@LH_LINUX:~/桌面/apue.3e/environ$ gcc -static hello1.c hello1.c:3:1: warning: return type defaults to ‘int’ [-Wimplicit-int] main() ^ lh@LH_LINUX:~/桌面/apue.3e/environ$ ls -l a.out -rwxrwxr-x 1 lh lh 912728 7月 7 20:54 a.out lh@LH_LINUX:~/桌面/apue.3e/environ$ size a.out text data bss dec hex filename 824102 7284 6360 837746 cc872 a.out lh@LH_LINUX:~/桌面/apue.3e/environ$ gcc hello1.c hello1.c:3:1: warning: return type defaults to ‘int’ [-Wimplicit-int] main() ^ lh@LH_LINUX:~/桌面/apue.3e/environ$ ls -l a.out -rwxrwxr-x 1 lh lh 8608 7月 7 20:54 a.out lh@LH_LINUX:~/桌面/apue.3e/environ$ size a.out text data bss dec hex filename 1183 552 8 1743 6cf a.out
size()
报告正文段、数据段和bss段的长度(以字节文单位),结果的第4列和第5列分别以十进制和十六进制表示的3段总长度。
8、存储空间分配
-
以下三个函数用于存储空间动态分配(在堆上分配)
void *malloc(size_t size); //分配指定字节数的存储区,此存储区中的初始值不确定 void *calloc(size_t nmemb, size_t size); //为指定数量指定长度的对象分配存储空间。该空间中的每一位都初始化为0 void *realloc(void *ptr, size_t size); //增加或减少以前分配区的长度。当增加长度时,可能需要将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区的初始值不确定。 void free(void *ptr); //释放ptr指向的存储空间,被释放的空间通常被送入可用存储区池。之后,可在调用上述3个分配函数时再分配这些空间。
-
realloc函数使我们可以增减以前分配的存储区长度。比如我们在堆上有一个数组,想要扩充该数组的长度,并且在该存储区后有足够的空间可供扩充,则可以在原存储区位置上向高地址方向扩充,无需移动原先数组任何内容。如果在原存储区后没有足够空间,则realloc分配另一个足够大的存储区,将现有数组内容全部复制到新分配的存储区,然后释放原存储区,返回新存储区地址。如果ptr是NULL,则realloc与malloc函数功能相同。
-
这些分配函数通常底层使用sbrk系统调用。该系统调用扩充或缩小进程的堆。
-
虽然sbrk可以缩小堆区大小,但是大多数malloc和free的实现都不减少进程的存储空间,释放的空间可供以后再分配,将它们保存在malloc池中而不返回给内核
-
大多数实现所分配的存储空间比所要求的要稍微大一些,额外的空间用来记录管理信息:分配块的长度、指向下一个分配块的指针等。这意味着如果超过一个已分配区的尾端或者在已分配区起始位置之前进行写操作,则会改写另一块的管理记录信息或其他动态分配对象,这种错误是灾难性的。
-
致命错误:释放了一个已经释放了的块;调用free时使用的指针不是3个alloc函数的返回值等。
-
若使用malloc函数在堆上动态分配内存空间但是忘记调用free函数,那么该进程占用的存储空间就会连续增加,这称为内存泄漏。如果不调用free释放不再使用的空间,那么进程地址空间长度会慢慢增加,直至不再有空闲空间。
-
-
在栈上分配内存空间
void *alloca(size_t size);
- 它的调用方式与
malloc
相同,但是在当前函数的栈帧上分配存储空间而不是在堆中。 - 优点:当函数返回时自动释放它所使用的栈帧,不用手动free释放
- 缺点:增加了栈帧的长度,而某些系统的函数在已经被调用后不能增加栈帧长度,于是也不支持alloca函数。
- 它的调用方式与
9、环境变量
-
环境变量字符串形式是:
NAME=value1:value2:value3
- UNIX内核并不查看这些字符串,它们的解释完全取决于各个应用程序。例如shell就是用了大量环境变量。
-
Linux中常用环境变量
环境变量名 说明 PATH 搜索可执行文件的路径前缀列表 HOME 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录) HISTSIZE 指保存历史命令记录的条数。 LOGNAME 指当前用户的登录名。 HOSTNAME 指主机的名称,许多应用程序如果要用到主机名的话,通常是从这个环境变量中来取得的。 SHELL 指当前用户用的是哪种Shell。 LANG/LANGUGE 和语言相关的环境变量,使用多种语言的用户可以修改此环境变量 PS1 命令基本提示符,对于root用户是#,对于普通用户是$ PS2 附属提示符,默认是“>” -
可以通过
getenv
函数获取指定环境变量值char *getenv(const char *name);//此函数返回一个指针,它指向`name=value`字符串中的`value`部分。
- 当我们要获取某个环境变量值时,不建议使用
environ
指针数组(因为这样需要遍历确定哪一个环境变量),而是要使用getenv
函数。
- 当我们要获取某个环境变量值时,不建议使用
-
通过下面函数设置环境变量
int putenv(char *string);//putenv取形式为name=value的字符串,将其放置到环境表中。如果name已存在则删除原来的定义。 int setenv(const char *name, const char *value, int overwrite);//setenv将name设置为value。如果name不存在,则将name与value一起添加到环境中。如果环境中存在name,则如果overwrite为非零,则其值将更改为value;如果overwrite为零,则不会更改其值 int unsetenv(const char *name);
- 我们可以通过上面函数改变现有变量的值,或者是增加新的环境变量。但是注意这里影响的只是当前进程及其子进程,不影响父进程环境(通常是一个shell进程),即你在程序里做的改变不会反映到外部环境中。这里很好理解,因为这几个函数操作的对象都是C程序存储空间中的环境表(见之前的图),因此无法影响到外部。
- 需要注意,putenv函数直接将传递给它的参数放到环境表中(即环境表指针数组中有一个元素直接赋值为该参数),因此将存放在栈中的字符串作为参数传递给putenv就会出错,因为从当前函数返回时,其栈帧占用的存储区可能会被重用。
- 而setenv函数则会复制传入的字符串(即分配存储空间以存放name=value字符串,很显然,在堆中),因此setenv不存在上面putenv中的问题。
-
以上函数具体操作过程
环境表和环境字符串通常存放在进程存储空间顶部(栈之上)。删除一个字符串很简单,在环境表中找到该指针,然后将所有后续指针都向环境表首部移动一个位置;但是增加一个字符串或者修改一个现有字符串则较为困难。环境表和环境字符串通常占用的是进程地址空间顶部,所以不能再向高地址(向上)扩展,同时也不能移动在它之下的各栈帧,所以不能向低地址(向下)扩展。因此该空间长度不能增加。- 修改一个现有环境变量:
- 如果新value长度少于或等于现有value长度,则只需要将新字符串复制到原字符串所用空间
- 如果value长度大于原长度,则必须调用malloc为新字符串分配空间,然后将新字符串复制到该空间中,使环境表中对应指针指向新分配区
- 如果要增加新环境变量,首先调用malloc为name=value字符串分配空间,然后将字符串复制到此空间中。
- 如果这是第一次增加一个新name,则必须调用malloc为新的指针表分配空间。接着将原来的环境表复制到新分配区,并将指向新name=value字符串的指针存放在该指针表的表尾,然后将一个空指针放在其后,将environ指向新指针表。如之前的图所示,如果原来的环境表位于栈顶之上,name必须将此表移至堆中。但是此表中大多数指针仍指向栈顶之上的各name=value字符串。
- 如果这不是第一次增加一个新name,则可知以前已调用malloc为环境表分配了空间,所以只要调用realloc,为该空间多存放一个指针的空间,然后将指向新name=value字符串的指针存放在该表尾,后面跟NULL。
- 修改一个现有环境变量:
10、函数setjmp和longjmp
- 在C中,goto语句不能跨越函数。因此可以执行这种跳转功能函数
setjmp
和longjmp
。 - 实例:
main
函数调用do_line
函数,do_line
函数调用cmd_add
函数#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) 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) { 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 */ }
- 那么此时该进程栈的情况如下:
- 当
cmd_add
函数发现一个错误,那么如果想要返回main
函数的话,因为它出现在main
函数的深层嵌套层中,因此不得不以检查返回值的方法逐层返回,会变得很麻烦。尤其是嵌套层数很多的时候,问题就会更加严重。
- 那么此时该进程栈的情况如下:
- 解决上述问题的方法就是使用非局部goto:
setjmp
和longjmp
函数。这两个函数不是在一个函数内实施跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中。int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val);
- 在希望返回到的位置调用
setjmp
,直接调用该函数返回0。其中参数env是一个特殊类型jmp_buf
,用于存放在调用longjmp
时能用来恢复栈状态的所有信息。因为需在另一个函数中引用env
,因此该变量应定义为全局变量 - 当在调用嵌套函数中发现一个错误(比如说cmd_add函数),可以调用longjmp跳转到setjmp处(会导致抛弃嵌套函数的栈帧)。其中第一个参数是setjmp时的env,第二个参数是一个非0值,会作为跳转到setjmp函数的返回值。注意:一个setjmp可以有多个longjmp。例如,可以在cmd_add中以val为1调用longjmp,也可以在get_token中以val为2调用longjmp。
- 在希望返回到的位置调用
- 实例:经过修改后的
main
和cmd_add
函数(其他两个函数do_line
和get_token
未更改)#include "apue.h" #include <setjmp.h> #define TOK_ADD 5 jmp_buf jmpbuffer; int main(void) { char line[MAXLINE]; if (setjmp(jmpbuffer) != 0) 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); /* rest of processing for this command */ }
- longjump使栈反绕到执行main函数时的情况,也就是抛弃了cmd_add和do_line栈帧。调用longjmp造成了main中setjmp的返回,但是,这一次的返回值为1(longjmp的第二个参数)。
- longjump使栈反绕到执行main函数时的情况,也就是抛弃了cmd_add和do_line栈帧。调用longjmp造成了main中setjmp的返回,但是,这一次的返回值为1(longjmp的第二个参数)。
10.1、自动变量(局部变量)、全局变量、寄存器变量、静态变量和易失变量的不同情况
- 寄存器变量和易失变量
- 寄存器变量:用
register
修饰的变量如register int n
;register
暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度。注意:寄存器变量不是强制的。 - 易失变量:用
volatile
关键字修饰的变量,如volatile int n
;只要变量的值可能意外更改,就应将其声明为volatile。volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从内存中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
- 寄存器变量:用
- 实例:查看longjump后自动变量(局部变量)、全局变量、寄存器变量、静态变量和易失变量的变化情况
不进行任何优化的编译:#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); }
进行全部优化的编译:lh@LH_LINUX:~/桌面/apue.3e/environ$ gcc testjmp.c lh@LH_LINUX:~/桌面/apue.3e/environ$ ./a.out in f1(): globval = 95, autoval = 96, regival = 97, volaval = 98, statval = 99 after longjmp: globval = 95, autoval = 96, regival = 97, volaval = 98, statval = 99
lh@LH_LINUX:~/桌面/apue.3e/environ$ gcc -O testjmp.c lh@LH_LINUX:~/桌面/apue.3e/environ$ ./a.out in f1(): globval = 95, autoval = 96, regival = 97, volaval = 98, statval = 99 after longjmp: globval = 95, autoval = 2, regival = 3, volaval = 98, statval = 99
- 可以看出全局变量,静态变量,易失变量不受优化影响,longjmp之后它们呈现的值是最近的值。 不进行优化时,这五个变量都存放在存储器中(即忽略了register关键字);进行了优化后,局部变量和寄存器变量都存放在了寄存器中(即使局部变量没有register修饰),因此可以从结果中看出寄存器变量和局部变量值回滚了(即为调用setjmp时的值)
- 易失变量,全局变量,静态变量的值在longjmp后不会回滚到原先值(不会回滚到setjmp处,而是保持最新的值),而寄存器变量和局部变量则没有保证,大多数实现并不回滚寄存器变量和局部变量,但是不保证一定是这样。因此如果想定义一个局部变量,又不想其值回滚,则应该定义其为volatile。
10.2、自动变量的潜在问题
- 实例:
注意:声明自动变量的函数已经返回后,不能再引用这些自动变量,本例就犯了这个错误。当#include <stdio.h> FILE* open_data() { FILE* fp; char databuf[BUFSIZ]; fp = fopen("t.txt","r"); setvbuf(fp,databuf,_IOLBF,BUFSIZ);//设置流缓冲区 return fp; }
open_data
函数返回时,它在栈上使用的空间将由下一个被调用函数的栈帧使用。但是标准I/O库仍将使用这部分存储空间作为该流的缓冲区,这样就造成冲突。因此应设置缓冲区为全局的或静态的,或在堆上动态创建缓冲区。
11、函数getrlimit和setrlimit
-
每个进程都有一组资源限制,可以通过
getrlimit
和setrlimit
函数进行查询和修改。int getrlimit(int resource, struct rlimit *rlim); int setrlimit(int resource, const struct rlimit *rlim); struct rlimit { rlim_t rlim_cur; /* 软限制 */ rlim_t rlim_max; /* 硬限制 (rlim_cur最大值) */ };
-
其中第一个参数是宏,指定要访问、修改的资源。第二个参数是限制资源内容。
- 任何进程都可以将软限制更改为小于或等于其硬限制值
- 任何进程都可降低其硬限制值,但是其必须大于等于软限制值
- 只有超级用户进程可以提高硬限制值
- 常量
RLIM_INFINITY
指定无限量的限制
-
第一个参数可以是以下宏(部分)
限制资源 说明 RLIMIT_AS 进程总的可用存储空间最大长度(字节) RLIMIT_CPU CPU时间最大值,若超过此软限制则向该进程发送SIGXCPU信号 RLIMIT_DATA 数据段最大字节长度(这里是.bss.data和heap的总和) RLIMIT_FSIZE 可以创建的文件的最大字节长度。超过此软限制则向该进程发送SIGXFSZ信号 RLIMIT_STACK 栈的最大字节长度 RLIMIT_SIGPENDING 一个进程可排队的信号最大数量,这个限制是sigqueue函数实施的 -
资源限制影响到的是调用进程及其子进程,不会影响到其他进程。因此如果要影响一个用户的所有后续进程,需要将资源限制的设置构造在shell之中。(比如ulimit命令)
-
备注:字符串创建符(#):例如:
name == RLIMIT_AS;
则#name == "RLIMIT_AS";