今天我们继续学习昨天没有学习完的进程的相关知识。
父子进程之间在fork后。有哪些相同,哪些相异之处呢?
父子相同处:
全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处:
1. 进程ID
2. fork返回值
3. 父进程ID
4. 进程运行时间
5. 闹钟(定时器)
6. 未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。
父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己
的逻辑都能节省内存开销。
接下来用一个程序来测试下,父子进程是否共享全局变量。
- #include
- #include
- #include
-
- int var = 100;
-
- int main()
- {
- printf("init var = %d\n",var);
- pid_t pid = fork();
- if(pid<0)
- {
- perror("pid error");
- exit(1);
- }
- else if(pid>0)
- {
- var = 120;
- printf("parent,var = %d\n",var);
- printf("I' am parent pid = %d,getppid = %d\n",getpid(),getppid());
-
- }
- else if(pid == 0)
- {
- var = 140;
- printf("child,var = %d\n",var);
- printf("I' am child pid = %d,getppid = %d\n",getpid(),getppid());
-
- }
- printf("------finish----------\n");
-
- return 0;
- }
结论:
【注】:父子进程不共享全局变量。
【注】:父子进程共享。
1. 文件描述符(打开文件的结构体)
2. mmap
建立的映射区
特别的,fork之后父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法。
fork生成的子进程和父进程的功能一样,如果想让fork生成的子进程的功能不一样,即拥有与父进程不一样的代码段数据段以及堆栈段,应该怎么办呢?
使用exec函数系列。
fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。
exec替换进程映像:
在进程的创建上Unix采用了一个独特的方法,它将进程创建与加载一个新进程映象分离。这样的好处是有更多的余地对两种操作进行管理。
当我们创建了一个进程之后,通常将子进程替换成新的进程映象,这可以用exec系列的函数来进行。当然,exec系列的函数也可以将当前进程替换掉。
例如:在shell命令行执行ps命令,实际上是shell进程调用fork复制一个新的子进程,在利用exec系统调用将新产生的子进程完全替换成ps进程。
exec系列函数(execl,execlp,execle,execv,execvp)
包含头文件
功能:
用exec函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID。exec名下是由多个关联函数组成的一个完整系列。
头文件
extern char **environ;
原型:
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[]);
参数:
path参数表示你要启动程序的名称包括路径名
arg参数表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且arg必须以NULL结束
返回值:成功返回0,失败返回-1
注:上述exec系列函数底层都是通过execve系统调用实现:
#include
int execve(const char *filename, char *const argv[],char *const envp[]);
DESCRIPTION:
execve() executes the program pointed to by filename. filename must be
either a binary executable, or a script starting with a line of the form
若参数file文件名:包含/视为路径名,否则按PATH环境变量去指定各目录搜寻文件
函数名的解读:
(1)l 表示参数以列表(list)方式提供。
(2)v 表示参数以数组[向量(vector)]方式提供。
(3)p 表示用户在PATH环境变量中寻找可执行文件。
(只需简单提供文件名,主要用于shell,因为shell所指向进程通常会从shell继承环境变量)
(4)e 表示会提供给新进程以新的环境变量。
exec系列函数没有一个同时可搜索路径和使用新环境变量的函数。
exec系列函数成功调用不仅改变地址空间与进程映像,
还改变进程的一些属性:
1.任何挂起的信号都会丢失。
2.捕捉的任何信号会还原为缺省的处理方式,因为信号处理函数已经不存在于地址空间中了。
3.任何内存的锁定会丢失。
4.多数线程的属性会还原到缺省值。
5.多数关于进程的统计信息会复位。
6.与进程内存相关的任何数据都会丢失,包括映射的文件。
7.包括c语言库的一些特性(例如aexit())等独立存在于用户空间的数据都会丢失。
未改变的进程属性例如 pid、父进程的pid、优先级、所属的用户和组。
实例:
- #include
- #include
- #include
-
- int main (int argc,char *argv[])
- {
- int ret = 0; //返回值
- printf("Executing ls\n");
-
- /**
- * 调用execl函数
- * 参数1:带路径文件名
- * 参数2:文件名
- * 最后参数:NULL
- */
- ret = execl("/bin/ls","ls","-l",NULL);
-
-
- /**
- * 若execl()函数有返回,说明调用失败
- */
- if(ret == -1){
- perror("execl failed to run ls");
- }
-
- exit(1); //退出
- }
编译及运行结果:
1.编译gcc execl.c -o execl
2.运行:./execl
3.结果:
-rwxrwxr-x 1 hhb hhb 16232 11月 7 14:47 fork
-rw-rw-r-- 1 hhb hhb 563 11月 7 14:47 fork.c
结论:
execl()
调用后紧跟着 perror()
的无条件调用。这是因为若调用程序还存在且 execl()
调用返回,那么肯定是 execl()
调用出错了。
只要 execl()
和其它exec
调用成功,就肯定清除了调用程序而代之以新的程序。
execv函数实例:
- #include
- #include
- #include
-
- int main (int argc,char *argv[])
- {
- char* av[] = {"ls","-1",NULL};
- execv("/bin/ls",av);
- perror("execl failed");
- exit(1);
- }
编译及运行结果:
1.编译gcc execv.c -o execv
2.运行:./execv
3.结果:
-rwxrwxr-x 1 hhb hhb 16232 11月 7 14:47 fork
-rw-rw-r-- 1 hhb hhb 563 11月 7 14:47 fork.c
execlp()和 execvp()(p表示用户在PATH环境变量中寻找可执行文件)
execlp()和 execvp()分别类似于系统调用 execl()和 execv(),主要区别是:
函数名后多了一个p,多用于shell,因为shell所执行进程通常会从shell继承环境变量,表示:
第一个参数指向的是文件名(不包含路径)。
可通过检索 shell 环境变量 PATH指出的目录,来得到该文件名的路径前缀部分。如可在 shell 中用下述命令序列来设置环境变量 PATH:
$PATH=/bin;/usr/bin;/sbin
$export PATH
即: execlp()
和 execvp()
执行时:先在目录/bin
,然后在目录/usr/bin
,最后在目录/sbin
中搜索程序文件。另外, execlp()
和 execvp()
还可以用于运行 shell 程序,而不只是普通的程序。
execvp函数实例
- #include
- #include
- #include
-
- int main (int argc,char *argv[])
- {
- int ret = 0; //返回值
-
- /**
- * 调用execvp函数
- * 参数1:可执行文件名
- * 参数2:参数
- * 最后参数:NULL
- */
- char *const args[] = {"vi","./data.txt",NULL};
- ret = execvp("vi",args);
-
- /**
- * 若execvp()函数有返回,说明调用失败
- */
- if(ret == -1){
- perror("execvp");
- }
-
- exit(1); //退出
- }
execle()
和 execve()
分别类似于系统调用 execl()
和 execv()
,主要区别是:
函数名后多了一个e。
实例:
- #include
- #include
- #include
-
- int main (int argc,char *argv[])
- {
- char *args[] = {"/bin/ls",NULL};
-
- printf("PID = %d\n",getpid());
- if(execve("/bin/ls",args,NULL) < 0)
- {
- perror("execve");
- }
-
- exit(1);
- }
错误的返回值:
回收子进程
孤儿进程
孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
僵尸进程
僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
【注意】:僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
清除掉僵尸进程的方法:
wait();
waitpid();
kill -9 ppid
,使其变成孤儿进程, 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`:
> 0 回收指定`ID`的子进程
-1 回收任意子进程(相当于`wait`)
0 回收和当前调用`waitpid`一个组的所有子进程
<-1 回收指定进程组内的任意子进程
返回0:参3为`WNOHANG`,且子进程正在运行。注意:一次`wait`或`waitpid`调用只能清理一个子进程,清理多个子进程应使用循环。
今天就学到这儿了,明天接着分享。