1. 进程号和父进程号
获取进程号:
#include <unistd.h>
pid_t getpid(void);
// always successfully returns process ID of caller
可以通过 Linux系统特有的/proc/sys/kernel/pid_max 文件来进行查看或调整进程号上限(其值=最大进程号+1)。
一旦进程号达到上限,会将进程号计数器重置为 300,而不是 1。之所以如此,是因为低数值的进程号为系统进程和守护进程所长期占用,在此范围内搜索尚未使用的进程号只会是浪费时间。
父进程进程号:
#include <unistd.h>
pid_t getppid(void);
// always successfully returns process ID of parent of caller
如果子进程的父进程终止,init 进程随即将收养该进程,子进程后续对 getppid () 的调用将返回进程号 1。
2. 进程内存分布
3. 虚拟内存管理
4. 栈和栈帧
5. 命令行参数
当执行程序时,命令行参数(command-line argument)(由 shell 逐一解析)通过两个入参提供给 main () 函数:
- 第一个参数
int argc
,表示命令行参数的个数 - 第二个参数
char *argv[]
,是一个指向命令行参数的指针数组,每一参数又都是以空字符(null)结尾的字符串。- 第一个字符串,亦即
argv[0]
指向的,(通常)是该程序的名称 - argv 中的指针列表以 NULL 指针结尾(即
argv[argc]
为 NULL)
- 第一个字符串,亦即
通过/proc/PID/cmdline文件可以读取任一进程的命令行参数;GNU C可使用program_invocation_name
和program_invocation_short_name
找到程序名称。
参数存储自己上限通过ARG_MAX限定,通过调用sysconf(__SC_ARG_MAC)
确定上限值。
程序使用getopt () 库函数解析命令行选项。
6. 环境列表
可以使用全局变量 char **environ
访问环境列表,C运行时就定义并赋值了
#include "lib/tlpi_hdr.h"
extern char **envirion;
int main(int argc, char *argv[])
{
char **ep;
for (ep = envirion; *ep != NULL; ++ep)
puts(*ep);
exit(EXIT_SUCCESS);
}
可以声明 main()
函数中的第三个参数来访问,但不推荐使用,该参数作用域仅在main () h函数内,而且该特性也不在规范中。
int main(int argc, char *argv[], char *envp[])
getenv () 函数能够从进程环境中检索单个值。
#include <stdlib.h>
char *getenv(const char *name);
// returns pointer to (vlaue) string, or NULL if no such variable
向 getenv () 函数提供环境变量名称,该函数将返回相应字符串指针。
- 如果指定 SHELL 为参数 name,那么将返回/bin/bash。
- 如果不存在指定名称的环境变量,那么 getenv () 函数将返回 NULL。
- 若需要改变一个环境变量的值,可以使用 setenv () 函数或 putenv () 函数
putenv () 函数向调用进程的环境中添加一个新变量,或者修改一个已经存在的变量值。
#include <stdlib.h>
int putenv(char *string);
// returns 0 on success, or nonzero on error
参数 string 是一指针,指向 name=value 形式的字符串。调用 putenv () 函数后,该字符串就成为环境的一部分,而不是复制副本,因此如果修改string变量,进程的环境也将被修改。
此外,string变量不应为自动变量,因为变量函数返回号,内存区域很有可能会被重写
setenv () 函数可以代替 putenv () 函数,向环境中添加一个变量。
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
// returns 0 on success, or -1 on error
setenv()函数为形如name=value的字符串分配,并将所指向的字符串拷贝到缓冲区
unsetenv () 函数从环境中移除由name参数标识的变量
#include <stdlib.h>
int unsetenv(const char *name);
// return 0 on success, or -1 on error
同 setenv () 函数一样,参数 name 不应包含等号字符
#define _BSD_SOURCE
#include <stdlib.h>
int clearenv(void)
// returns 0 on success, or a nonzero on error
示例:修改进程环境
#define _GNU_SOURCE
#include <stdlib.h>
#include "lib/tlpi_hdr.h"
extern char **environ;
int main(int argc, char *argv[])
{
int j;
char **ep;
// erase entire environment
clearenv();
for (j = 1; j < argc; ++j)
if (putenv(argv[j]) != 0)
errExit("putenv: %s", argv[j]);
if (setenv("GREET", "Hello world", 0) == -1)
errExit("setenv");
unsetenv("BYE");
for (ep = environ; *ep != NULL; ++ep)
puts(*ep);
exit(EXIT_SUCCESS);
}
7. 执行非局部跳转:setjmp() 和 longjmp()
指跳转的目标为当前执行函数之外的某个位置
#include <setjmp.h>
int setjmp(jmp_buf env);
// returns 0 on initial call, nonzero on return via longjmp()
void longjmp(jmp_buf env, int val);
通过查看 setjmp()返回的整数值,可以区分 setjmp 调用是初始返回还是第二次“返回”。初始调用返回值为 0,后续“伪”返回的返回值为 longjmp()调用中 val 参数所指定的任意值。通过对 val 参数使用不同值,能够区分出程序中跳转至同一目标的不同起跳位置。
调用 setjmp () 时,env 除了存储当前进程的其他信息外,还保存了程序计数寄存器(指向当前正在执行的机器语言指令)和栈指针寄存器(标记栈顶)的副本。
longjmp() 调用不能跳转到一个已经返回的函数中。
#include <setjmp.h>
#include "tlpi_hdr.h"
static jmp_buf env;
static void f2(void)
{
longjmp(env, 2);
}
static void f1(int argc)
{
if (argc == 1)
longjmp(env, 1);
f2();
}
int main(int argc, int *argv[])
{
switch (setjmp(env)) {
case 0: // the return after the initial setjmp()
printf("Calling f1() after initial setjmp()\n");
f1(argc);
break;
case 1:
printf("We jumped back from f1()\n");
break;
case 2:
printf("We jumped back from f2()\n");
break;
}
exit(EXIT_SUCCESS);
}
执行结果:
$ ./longjmp
Calling f1() after initial setjmp()
We jumped back from f1()
$ ./longjmp x
Calling f1() after initial setjmp()
We jumped back from f2()
setjmp() 使用限制
SUSv3 和 C99 规定,对 setjmp()的调用只能在如下语境中使用。
- 构成选择或迭代语句中(if、switch、while 等)的整个控制表达式。
- 作为一元操作符!(not)的操作对象,其最终表达式构成了选择或迭代语句的整个控制表达式。
- 作为比较操作(
==
、!=
、<
等)的一部分,另一操作对象必须是一个整数常量表达式,且其最终表达式构成选择或迭代语句的整个控制表达式。 - 作为独立的函数调用,且没有嵌入到更大的表达式之中。
注意:C 语言赋值语句不在上述列表之列。以下形式的语句是不符合标准的:
s = setjmp(env); // wrong!
之所以规定这些限制,是因为作为常规函数的 setjmp() 实现无法保证拥有足够信息来保存所有寄存器值和封闭表达式中用到的临时栈位置,以便于在 longjmp() 调用后此类信息能得以正确恢复。
优化编译器的问题
优化编译器会重组程序的指令执行顺序,并在 CPU 寄存器中而非 RAM 中存储某些变量。
此外,某些应用程序二进制接口(ABI)实现的语义要求 longjmp()函数恢复先前 setjmp()调用所保存的 CPU 寄存器副本。这意味着 longjmp()操作会致使经过优化的变量被赋以错误值。
例如:
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
static jmp_buf env;
static void doJump(int nvar, int rvar, int vvar)
{
printf("Inside doJump(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar);
longjmp(env, 1);
}
int main(int argc, char **argv)
{
int nvar;
register int rvar; // 存储于寄存器中
volatile int vvar; // 不优化,该变量要实时更新读取
nvar = 111;
rvar = 222;
vvar = 333;
if (setjmp(env) == 0) { // after setjmp()
nvar = 777;
rvar = 888;
vvar = 999;
doJump(nvar, rvar, vvar);
} else { // after longjmp()
printf("After longjmp(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar);
}
exit(EXIT_SUCCESS);
}
执行结果如下:
$ cc -o setjmp_vars setjmp_vars.c
$ ./setjmp_vars
Inside doJump(): nvar=777 rvar=888 vvar=999
After longjmp(): nvar=777 rvar=888 vvar=999
$ cc -O -o setjmp_vars setjmp_vars.c
$ ./setjmp_vars
Inside doJump(): nvar=777 rvar=888 vvar=999
After longjmp (): nvar=111 rvar=222 vvar=999
此处,在 longjmp()调用后,nvar 和 rvar 参数被重置为 setjmp()初次调用时的值。起因是优化器对代码的重组受到 longjmp()调用的干扰。作为候选优化对象的任一局部变量可能都难免会遇到这类问题,一般包含指针变量和 char、int、float、long 等任何简单类型的变量。
将变量声明为 volatile,是告诉优化器不要对其进行优化,从而避免了代码重组。在上面的程序输出中,无论编译优化与否,声明为 volatile 的变量 vvar 都得到了正确处理。因为不同的优化器有着不同的优化方法,具备良好移植性的程序应在调用 setjmp()的函数中,将上述类型的所有局部变量都声明为 volatile。
若在 GNU C 语言编译器中加入–Wextra(产生额外的警告信息)选项,setjmp_vars.c 程序的编译结果将显示有帮助的警告信息如下:
$ cc -Wall -Wextra -O -o setjmp_vars setjmp_vars.c
setjmp_vars.c: In function 'main':
setjmp_vars.c:17: warning: variable 'nvar' might be clobbered by 'longjmp' or 'vfork'
setjmp_vars.c:18: warning: variable 'rvar' might be clobbered by 'longjmp' or 'vfork'