进程基础概念
进程控制块(PCB)
每个进程在内核中都有一个进程控制块PCB来维护进程的相关信息,Linux的进程控制块事task_struct结构体。
task_struct结构体中主要成员:
- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
- 进程的状态,有就绪、运行、挂起、停止等状态。
- 进程切换时需要保存和恢复的一些CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 当前工作目录(Current Working Directory)。
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息。
- 用户id和组id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit)。
环境变量
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:
① 字符串(本质) ② 有统一的格式:名=值[:值] ③ 值用来描述进程环境信息。
存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:须声明环境变量。extern char ** environ;
1.常见环境变量
按照惯例,环境变量字符串都是name=value这样的形式,大多数name由大写字母加下划线组成,一般把name的部分叫做环境变量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
1)PATH
可执行文件的搜索路径。ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以查看这个环境变量的值:
$ echo $PATH
2)SHELL
当前Shell,它的值通常是/bin/bash。
3)HOME
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
2.函数
1)getenv函数
获取环境变量值
char *getenv(const char *name); 成功:返回环境变量的值;失败:NULL (name不存在)
2)setenv函数
设置环境变量的值
int setenv(const char *name, const char *value, int overwrite); 成功:0;失败:-1
参数overwrite取值: 1:覆盖原环境变量 ;0:不覆盖。(该参数常用于设置新环境变量,如:ABC = haha-day-night)
3)unsetenv函数
删除环境变量name的定义
int unsetenv(const char *name); 成功:0;失败:-1
注意事项:name不存在仍返回0(成功),当name命名为"ABC="时则会出错。
进程状态
进程的基本状态有5种,分别为初始、就绪、运行、运行、挂起和终止。初始一般和就绪结合在一起。
进程控制函数
-
fork函数
pid_t fpid=fork();
只调用一次,可以返回两次。
1)在子进程中,返回值fpid为0;
2)在父进程中,返回值fpid为子进程id;
3)fork失败,返回一个负值。
for(int i=0;i<n;i++){fork();}会产生(2^n )-1个子进程。加上父进程为2^n个进程。
通过返回值可以控制上述循环体只产生n个子进程。
for(int i=0;i<n;i++){
fpid=fork();
if(fpid==0) break;
}
每循环一次,父进程产生一个子进程,子进程不再继续产生子进程。 -
gitpid函数
获取当前进程ID -
getppid函数
获取当前进程的父进程ID -
getuid()
获取实际用户的ID
geteuid()
返回有效用户ID
有效用户ID(EUID)是你最初执行程序时所用的ID
表示该ID是程序的所有者
真实用户ID(UID)是程序执行过程中采用的ID
该ID表明当前运行位置程序的实际执行者
举个例子
程序myprogram的所有者为501/anna
以501运行该程序此时UID和EUID都是501
但是由于中间要访问某些系统资源
需要使用root身份
此时UID为0而EUID仍是501 -
getgid()
返回当前进程实际用户的组ID
getegid()
返回当前进程有效用户的组ID
区别参考getuid()和geteuid()区别
进程共享
父子进程在调用fork之后的相同点与不同点
-
父子相同处
全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式。 -
父子不同处
进程ID、fork返回值、父进程ID、进程运行时间、闹钟(定时器)、未决信号集。
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区
fork之后父进程和子进程的执行顺序不确定,取决于内核调度算法。
gdb调试
使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。
set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。
set follow-fork-mode parent 设置跟踪父进程。
注意,一定要在fork函数调用之前设置才有效。
exec函数
fork创建子进程之后执行的是和父进程相同的程序,子进程可以通过调用exec函数来执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以exec开头的函数,统称exec函数:
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
- execlp函数
加载一个进程,借助PATH环境变量
int execlp(const char *file, const char *arg, …); 成功:无返回;失败:-1
参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。 - execl函数
加载一个进程, 通过 路径+程序名 来加载。
int execl(const char *path, const char *arg, …); 成功:无返回;失败:-1
对比execlp,如加载"ls"命令带有-l,-F参数
execlp(“ls”, “ls”, “-l”, “-F”, NULL); 使用程序名在PATH中搜索。
execl("/bin/ls", “ls”, “-l”, “-F”, NULL); 使用参数1给出的绝对路径搜索。 - execvp函数
加载一个进程,使用自定义环境变量env
int execvp(const char *file, const char *argv[]);
变参形式: ①… ② argv[] (main函数也是变参函数,形式上等同于 int main(int argc, char *argv0, …))
变参终止条件:① NULL结尾 ② 固参指定
execvp与execlp参数形式不同,原理一致。
exec函数族一般规律
调用成功后开始执行新的程序,无返回值。
调用失败返回-1。
事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
回收子进程
-
孤儿进程
父进程先于子进程执行完毕,子进程成为孤儿进程。子进程的父进程变为init进程,由init进程回收掉子进程。 -
僵尸进程
父进程在子进程结束运行后没有进行回收,子进程资源PCB仍留于内核中,于是子进程变为僵尸进程。
僵尸进程的解决办法,杀掉父进程,然后僵尸进程变为孤儿进程,再由init进程进行回收。 -
wait函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
① 阻塞等待子进程退出
② 回收子进程残留资源
③ 获取子进程结束状态(退出原因)。
pid_t wait(int *status); 成功:清理掉的子进程ID;失败:-1 (没有子进程)
当进程终止时,操作系统的隐式回收机制会:
1).关闭所有文件描述符
2).释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1). WIFEXITED(status) 为非0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
2). WIFSIGNALED(status) 为非0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
3). WIFSTOPPED(status) 为非0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行 -
waitpid函数
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options); 成功:返回清理掉的子进程ID;失败:-1(无子进程)
特殊参数和返回情况:
参数pid:
1)> 0 回收指定ID的子进程
2)-1 回收任意子进程(相当于wait)
3)0 回收和当前调用waitpid一个组的所有子进程
4)< -1 回收指定进程组内的任意子进程
返回0:参3为WNOHANG,且子进程正在运行。
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。