在上一篇文章中详细介绍了task_struct结构体内的常见成员,然后我们就来看一下具体内容。每个进程都把它的信息放在各自的 task_struct 这个数据结构中,task_struct 主要包含了这些内容:
标示符 : 描述本进程的唯一标示符,用来区别其他进程。
状态 : 任务状态,退出代码,退出信号等。
优先级 : 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据。
I/ O状态信息:包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
进程标识符初探
进程id(PID)。每个进程都有非负的整形表示唯一的进程ID。好比如我们的身份证一样,每个人的身份证号是唯一的.因为进程ID标示符总是唯一的,常将其用来做其他标示符的一部分以保证其唯一性,进程ID(PID)是无法在用户层修改的。调用getpid()函数可以获得当前进程的PID,此函数没有参数,如果执行成功返回当前进程的PID,失败返回-1,出错原因存储于errno。
函数定义:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
例子1:打印自己的进程ID(PID)。
1 #include <unistd.h> 2 #include <stdio.h> 3 int main() 4 { 5 pid_t pid; //pid_t 其实是int 6 pid = getpid(); 7 printf("the current program's pid is %d ",pid); 8 while(1); 9 return 0; 10 }
运行程序,然后使用“ps -u”( ps u 以用户为主的格式来显示程序状况。)命令查看对照如下:
在Linux系统中,PID为0 的进程通常是调度进程,常常被称为交换进程,也是第一个系统进程。第一个用户进程是init进程,其PID为1。
父进程id(PPID)。当前进程的父进程id。任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,被创建的进程称为子进程,父进程ID无法在用户层修改。父进程的进程ID即为子进程的父进程ID(PPID)。用户可以通过调用getppid()函数来获得当前进程的父进程ID(PPID)。此函数没有参数,如果执行成功返回当前进程的父进程ID(PPID),失败返回-1,出错原因存储于errno。
函数定义:
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
例子2:打印自己的父进程PPID。
1 #include <unistd.h> 2 #include <stdio.h> 3 int main() 4 { 5 pid_t ppid; //pid_t 其实是int 6 ppid = getppid(); 7 printf("the current program's ppid is %d ",ppid); 8 while(1); 9 return 0; 10 }
同样,运行程序,然后使用“ps -u”命令查看对照如下:
我们注意到父进程为bash。事实上任何在命令行里运行的进程的父进程都是shell。
进程位置
1. 进程内存映像
Linux下C程序生成主要由四个步骤组成: 预编译、编译、汇编、链接。编译器gcc经过
预编译、编译、汇编3个步骤将源程序文件转换成目标文件。 如果程序有多个目标文件或程序中使用了库函数,则编译器还需要将所有目标文件及所需的库文件链接起来,最后生成可执行程序。当程序执行时,操作系统将可执行程序复制到内存中,程序转为进程通常需要以下步骤:
内核将程序读入内存,为程序分配内存空间;
内核为该进程保存PID及相应的状态信息,把进程放到运行队列中等待执行。程序转化为进程后就可被操作系统的调度程序执行了。 进程的内存映像是指内核在内存中如何存放可执行程序文件。在将程序转化为进程的过程中,操作系统将可执行程序从硬盘复制到内存中, 其布局如下:
例子3: C地址空间测试代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 int g_val = 100; 4 void test() 5 { 6 int a = 10; 7 int b = 10; 8 printf("test stack1 address : 0x%x ", &a); 9 printf("test stack2 address : 0x%x ", &b); 10 } 11 void (*fp)(); 12 int main() 13 { 14 int a = 10; 15 int *heap = malloc(sizeof(int)); 16 fp = test; 17 printf("code address : 0x%x ", fp); 18 printf("data address : 0x%x ", &g_val); 19 printf("heap address : 0x%x ", heap); 20 printf("main stack0 address : 0x%x ", &a); 21 fp(); 22 return 0; 23 }
结果如下:
由打印的地址可以应证上面的说法。
2. 进程映像的位置依赖于使用的内存管理方案。
3. 可执行程序与进程内存映像的不同之处在于:
a. 可执行程序位于磁盘中而内存映像位于内存;
b. 可执行程序没有堆栈,因为程序被加载到内在中才会分配堆栈;
c. 可执行程序虽然也有未初始化数据段但它并不被存储在位于硬盘中的可执行文件中;
d. 可执行程序是静态的、不变的,而内在映像随着程序的执行是在动态变化的,比如数据段随着程序的执行要存储新的变量值,栈在函数调用时也是不断在变化中。
再谈环境变量
1.从命令行参数说起
1 int main(int argc,char* argv[],char* env[])
第一个参数int argc,表示命令行参数的个数。第二个参数char *argv[],是一个指向命令行参数的指针数组,每一个参数又都是以空字符(‘ ’) 结尾的字符串。第一个字符串,亦即argv[0]指向的,(通常)是该程序的名称。argv中的指针列表以NULL指针结尾(即argv[argc]为NULL)。argv[0]包含了调用程序的名称,可以利用这一特性玩个实用的小技巧。首先为同一程序创建多个链接(即名称不同),然后让该程序查看argv[0],并根据调用程序的名称来执行不同任务。gzip(1)、gunzip(1)和zcat(1)命令是该技术应用的一个例子,这些命令链接的都是同一可执行文件。(使用该技术,必须小心处理如下情况:用户通过链接调用程序,但链接名又在该程序的意料之外。) 每个C语言程序都必须有一个称为main()的函数,作为程序启动的起点。当执行程序时,命令行参数(command-line argument)(由shell逐一解析)通过两个入参提供给main()函数。新建一个文件myenv.c输入如下代码
编译链接后它的输出如下:
图解如下:argv* []指针数组,存放指向命令行内输入参数的指针,argc则是指针的个数,末尾永远存有一个NULL,它们描述的是命令行的输入内容,这也是之所以它们才叫做命令行参数。
介绍环境变量
其实命令行参数后面还有一个描述环境变量的参数char* env[],那么同之前的命令行参数类似char* env[]是一个存放char*类型的指针的数组,它的每一个指针指向一个环境变量。如图:
它们在内存中的布局如下
命令行参数与环境变量保存于栈/堆的上方。例子4查看环境变量:
1 #include <stdio.h> 2 int main() 3 { 4 extern char **environ; 5 int i=0; 6 for(;environ[i]!=NULL;i++){ 7 printf("%s ",environ[i]); 8 } 9 return 0; 10 }
常见环境变量
由于父进程在调用fork创建进程时会把自己的环境变量表也复制给子进程,所以a.out打印的环境变量和Shell进程的环境变量是相同的。 按照惯例,环境变量字符串都name=value 这样的形式,大多数name由大写字母加下划线组成,一般把name的部分叫做环境变量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
PATH
可执行文件的搜索路径。 ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH
环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。 PATH环境变量的值可以包含多个目录,由: 号隔开。在Shell中用echo命令可以查看这个环境变量的值:
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
SHELL
当前Shell,它的值通常是/bin/bash。
TERM
当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端却般不行。
LANG
语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。用environ指针可以查看所有环境变量字符串,但是不够方便,如果给出name要在环境变量表中查 找它对应的value,可以用getenv函数。getenv的返回值是指向value的指针,若未找到则为NULL。 修改环境变量可以用以下函数putenv和setenv函数若成功则返回为0,若出错则返回非0。setenv将环境变量name的值设置为value。如果已存在环境变量name,那么 若rewrite为0,则覆盖原来的定义; 若rewrite 为0,则不覆盖原来的定义,也不返回错误。 unsetenv删除name的定义。即使name没有定义也不返回错误。
例子5:修改环境变量
1 #include <stdlib.h> 2 char *getenv(const char *name); 3 #include <stdlib.h> 4 int setenv(const char *name, const char *value, int rewrite); 5 void unsetenv(const char *name);
父进程在创建子进程时会复制一份环境变量给子进程,即,可被子进程继承,但此后两者的环境变量互不影响。进程运行起来之后修改环境变量只能影响自己。